Split shell, settings, and compositor into focused modules

This commit is contained in:
Samuel Onoja committed 2026-10-01 12:47:26 +01:00
1 parent 2b9f30fa5d
commit cbf048566a
38 files changed
+9998 -9817

No files matched your search

+337
View File
@@ -0,0 +1,337 @@
use super::{
Background, BarMetrics, Border, Color, Element, FereseShell, Length, Message, ShellSnapshot, ShellTheme, alignment,
bar_icon, button, color, color_with_opacity, container, control, motion, row, status_ui, text, theme, window,
};
impl FereseShell {
pub(super) fn output_for_bar(&self, id: window::Id) -> Option<&control::OutputSnapshot> {
let name = self.outputs.iter().find(|entry| entry.bar == id)?.name.as_deref()?;
self.snapshot.outputs.iter().find(|output| output.name == name)
}
pub(super) fn bar_hidden(&self, id: window::Id) -> bool {
output_bar_hidden(&self.snapshot, self.output_for_bar(id).map(|output| output.id))
}
pub(super) fn view_layer(&self, id: window::Id) -> Element<'_, cosmic::Action<Message>> {
if self.bar_hidden(id) {
return container(text("")).width(Length::Fill).height(Length::Fill).into();
}
let focused_output = self.output_for_bar(id);
let shell_theme = self.config.theme.for_bar();
let bar = BarMetrics::from(shell_theme);
let mut workspace_row = row::with_capacity(self.snapshot.workspaces.len() + 1)
.spacing(3)
.align_y(cosmic::iced::Alignment::Center);
workspace_row = workspace_row.push(motion::button(
button::custom(overview_control(bar, color(shell_theme.accent)))
.height(bar.control_height)
.padding([0, 7])
.on_press(cosmic::Action::App(Message::ToggleOverview)),
color(shell_theme.text_primary),
self.overview_active,
1.0,
));
let mut workspace_buttons = row::with_capacity(self.snapshot.workspaces.len())
.spacing(1)
.align_y(cosmic::iced::Alignment::Center);
for workspace in &self.snapshot.workspaces {
let active = workspace_active_on_bar(workspace.id, focused_output);
let owner = workspace
.output
.and_then(|id| self.snapshot.outputs.iter().find(|output| output.id == id));
let occupied = self
.snapshot
.windows
.iter()
.any(|window| window.workspace == workspace.id);
let indicator = workspace_indicator(
&workspace.name,
active,
workspace.active && !active,
occupied,
bar,
shell_theme,
);
let workspace_button = button::custom(indicator)
.name(format!(
"Workspace {}{}{}",
workspace.name,
if active { ", active" } else { "" },
owner.map_or(String::new(), |output| format!(", on {}", output.name))
))
.height(bar.control_height)
.padding(0)
.on_press(cosmic::Action::App(Message::ActivateWorkspace(workspace.id)));
let selector = motion::button(
workspace_button,
color(if active {
shell_theme.accent
} else {
shell_theme.text_primary
}),
false,
1.0,
);
// Iced tooltips are overlays inside this bar-height layer surface;
// viewport clamping puts them back over the selector. Keep the
// accessible button name, without an overlay stealing its target.
workspace_buttons = workspace_buttons.push(selector);
}
workspace_row = workspace_row.push(
container(workspace_buttons)
.padding([0, 3])
.class(theme::Container::custom(move |_| bar_group_style(shell_theme))),
);
let foreground = color(shell_theme.text_primary);
let left = cosmic::iced::widget::scrollable(workspace_row)
.direction(cosmic::iced::widget::scrollable::Direction::Horizontal(
cosmic::iced::widget::scrollable::Scrollbar::default()
.width(0)
.scroller_width(0),
))
.width(Length::Fill)
.height(bar.control_height);
let (date, time) = self.clock.split_once(", ").unwrap_or(("", &self.clock));
let clock = container(motion::button(
button::custom(
row![
text(date).size(12).class(theme::Text::Color(foreground)),
text(time).size(bar.text_size).class(theme::Text::Color(foreground)),
]
.spacing(8)
.align_y(cosmic::iced::Alignment::Center),
)
.padding([4, 8])
.height(bar.control_height)
.name("Open calendar")
.on_press_with_rectangle(move |offset, bounds| {
cosmic::Action::App(Message::OpenMenuOn(
id,
status_ui::Menu::Calendar,
cosmic::iced::Rectangle {
x: (bounds.x - offset.x).round() as i32,
y: (bounds.y - offset.y).round() as i32,
width: bounds.width.round() as i32,
height: bounds.height.round() as i32,
},
))
}),
foreground,
self.menu
.as_ref()
.is_some_and(|menu| menu.kind == status_ui::Menu::Calendar),
1.0,
))
.height(bar.control_height)
.align_y(alignment::Vertical::Center)
.class(theme::Container::custom(move |_| bar_group_style(shell_theme)));
let right = row![
self.view_status_bar().map(move |action| match action {
cosmic::Action::App(Message::OpenMenu(kind, anchor)) =>
cosmic::Action::App(Message::OpenMenuOn(id, kind, anchor)),
other => other,
}),
clock
]
.spacing(8)
.align_y(cosmic::iced::Alignment::Center);
let available = self
.outputs
.iter()
.find(|output| output.bar == id)
.and_then(|output| output.size)
.map_or(0., |(width, _)| width as f32)
- 2. * (shell_theme.panel_padding + shell_theme.bar_margin_horizontal as f32);
let title_width = (available - 920.).clamp(0., 360.);
let title = if self.config.status.window_title && title_width > 0. {
focused_bar_title(&self.snapshot, focused_output)
} else {
""
};
let center = text(title)
.size(bar.text_size)
.width(title_width)
.height(bar.control_height)
.align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Center)
.wrapping(cosmic::iced::widget::text::Wrapping::None)
.ellipsize(cosmic::iced::widget::text::Ellipsize::End(
cosmic::iced::advanced::text::EllipsizeHeightLimit::Lines(1),
))
.class(theme::Text::Color(foreground));
let right = cosmic::iced::widget::scrollable(container(right).width(Length::Shrink))
.direction(cosmic::iced::widget::scrollable::Direction::Horizontal(
cosmic::iced::widget::scrollable::Scrollbar::default()
.width(0)
.scroller_width(0),
))
.anchor_right()
.width(Length::Shrink)
.height(bar.control_height);
let content = row![
container(left).width(Length::Fill),
center,
container(right)
.width(Length::Fill)
.align_x(alignment::Horizontal::Right),
]
.spacing(8)
.align_y(cosmic::iced::Alignment::Center)
.height(Length::Fill);
let compositor_material = self
.outputs
.iter()
.any(|entry| entry.bar == id && entry.effects.is_some());
container(content)
.width(Length::Fill)
.height(Length::Fill)
.padding([0, shell_theme.panel_padding.round() as u16])
.class(theme::Container::custom(move |_| {
bar_style(shell_theme, compositor_material)
}))
.into()
}
}
pub(super) fn overview_control(bar: BarMetrics, foreground: Color) -> Element<'static, cosmic::Action<Message>> {
container(bar_icon(
ferese_theme::icons::FERESE,
bar.overview_icon_size,
foreground,
))
.width(bar.overview_icon_size)
.height(bar.control_height)
.align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Center)
.into()
}
#[cfg(test)]
pub(super) fn bar_hidden(snapshot: &ShellSnapshot) -> bool {
let output = snapshot
.outputs
.iter()
.find(|output| output.focused)
.or_else(|| snapshot.outputs.first())
.map(|output| output.id);
output_bar_hidden(snapshot, output)
}
pub(super) fn output_bar_hidden(snapshot: &ShellSnapshot, output: Option<u64>) -> bool {
let active_workspace = snapshot
.outputs
.iter()
.find(|candidate| Some(candidate.id) == output)
.map(|output| output.active_workspace);
active_workspace.is_some_and(|workspace| {
snapshot
.windows
.iter()
.any(|window| window.workspace == workspace && window.fullscreen)
})
}
pub(super) fn workspace_indicator(
name: &str,
active: bool,
active_elsewhere: bool,
occupied: bool,
bar: BarMetrics,
shell_theme: ShellTheme,
) -> Element<'static, cosmic::Action<Message>> {
let foreground = if active {
shell_theme.accent
} else if occupied {
shell_theme.text_primary
} else {
shell_theme.text_muted
};
container(
text(name.to_owned())
.size(12)
.class(theme::Text::Color(color(foreground))),
)
.width(24)
.height(bar.control_height)
.align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Center)
.class(theme::Container::custom(move |_| {
workspace_selector_style(active, active_elsewhere, occupied, shell_theme)
}))
.into()
}
pub(super) fn focused_bar_title<'a>(snapshot: &'a ShellSnapshot, output: Option<&control::OutputSnapshot>) -> &'a str {
let Some(output) = output else {
return "";
};
snapshot
.windows
.iter()
.find(|window| window.focused && window.workspace == output.active_workspace)
.map_or("", |window| {
if window.title.trim().is_empty() {
&window.app_id
} else {
&window.title
}
})
}
pub(super) fn workspace_active_on_bar(workspace: u64, output: Option<&control::OutputSnapshot>) -> bool {
output.is_some_and(|output| output.active_workspace == workspace)
}
pub(super) fn workspace_selector_style(
active: bool,
active_elsewhere: bool,
occupied: bool,
shell_theme: ShellTheme,
) -> container::Style {
container::Style {
background: if active {
Some(Background::Color(color_with_opacity(shell_theme.accent, 0.16)))
} else if occupied {
Some(Background::Color(color_with_opacity(shell_theme.text_primary, 0.04)))
} else {
None
},
border: Border {
color: color_with_opacity(shell_theme.accent, 0.55),
width: if active_elsewhere { 1.0 } else { 0.0 },
radius: shell_theme.material_radius.min(14.0).into(),
},
..Default::default()
}
}
pub(super) fn bar_style(theme: ShellTheme, compositor_material: bool) -> container::Style {
let mut style = ferese_theme::controls::surface_appearance(color(theme.bar_background), theme.bar_radius);
if compositor_material {
style.background = None;
}
style.text_color = Some(color(theme.text_primary));
style.icon_color = style.text_color;
style.snap = true;
style
}
pub(super) fn bar_group_style(theme: ShellTheme) -> container::Style {
container::Style {
background: Some(Background::Color(color_with_opacity(theme.text_primary, 0.035))),
border: Border {
color: color_with_opacity(theme.text_muted, 0.12),
width: 1.0,
radius: theme.material_radius.min(16.0).into(),
},
..Default::default()
}
}
+734
View File
@@ -0,0 +1,734 @@
use super::{
Anchor, Background, Border, Color, Element, FereseShell, IcedMargin, IcedOutput, KeyboardInteractivity, Layer,
Length, Limits, Message, NoteDrag, SctkLayerSurfaceSettings, Task, Zoned, alignment, button, color, config,
configured_font, container, destroy_layer_surface, format_bar_time, motion, note_store, row, set_anchor,
set_margin, stacked_clock_digits, text, theme, window,
};
impl FereseShell {
pub(super) fn flush_note_changes(&mut self) -> Task<Message> {
if self.note_saving || self.note_pending.is_empty() {
return Task::none();
}
let Some(path) = config::config_path() else {
self.note_error = Some("Configuration path unavailable.".into());
return Task::none();
};
self.note_saving = true;
self.note_inflight = std::mem::take(&mut self.note_pending);
let edits = self.note_inflight.clone();
cosmic::task::future(async move {
let (send, receive) = cosmic::iced::futures::channel::oneshot::channel();
std::thread::spawn(move || {
let _ = send.send(note_store::save(&path, &edits));
});
Message::NotesStored(
receive
.await
.unwrap_or_else(|_| Err("Note save worker stopped.".into())),
)
})
}
pub(super) fn set_note_text(&mut self, id: &str, value: String) -> Task<Message> {
if let Some(note) = self.config.desktop_widgets.notes.iter_mut().find(|note| note.id == id)
&& (note.text != value || self.note_error.is_some())
{
note.text = value.clone();
self.note_pending.push(note_store::Edit::Text(id.to_owned(), value));
}
self.flush_note_changes()
}
pub(super) fn finish_note_edit(&mut self) -> Task<Message> {
if let Some(editor) = self.note_editor.take() {
self.set_note_text(&editor.id, editor.content.text())
} else {
self.flush_note_changes()
}
}
pub(super) fn begin_note_drag(&mut self, id: String, surface: window::Id) -> Task<Message> {
self.begin_widget_drag(Some(id), surface)
}
pub(super) fn begin_widget_drag(&mut self, id: Option<String>, surface: window::Id) -> Task<Message> {
if self.note_drag.is_some() {
return Task::none();
}
let Some(entry) = self
.outputs
.iter()
.find(|entry| entry.clock == Some(surface) || entry.notes.iter().any(|(_, window)| *window == surface))
else {
return Task::none();
};
let Some(size) = entry.size else {
return Task::none();
};
let (origin, dimensions) = if let Some(id) = &id {
let Some(note) = self.config.desktop_widgets.notes.iter().find(|note| &note.id == id) else {
return Task::none();
};
(note_origin(note, size), (note.width, note.height))
} else {
let clock = &self.config.desktop_widgets.clock;
(
widget_origin(
clock.anchor,
clock.margin_x,
clock.margin_y,
(clock.width, clock.height),
size,
),
(clock.width, clock.height),
)
};
let output = entry.output.clone();
let mut obstacles = Vec::new();
let clock = &self.config.desktop_widgets.clock;
if id.is_some() && clock.on_output(entry.name.as_deref()) {
let position = widget_origin(
clock.anchor,
clock.margin_x,
clock.margin_y,
(clock.width, clock.height),
size,
);
obstacles.push(cosmic::iced::Rectangle::new(
position,
cosmic::iced::Size::new(clock.width as f32, clock.height as f32),
));
}
for note in &self.config.desktop_widgets.notes {
if Some(&note.id) != id.as_ref() && note.on_output(entry.name.as_deref()) {
obstacles.push(cosmic::iced::Rectangle::new(
note_origin(note, size),
cosmic::iced::Size::new(note.width as f32, note.height as f32),
));
}
}
let overlay = window::Id::unique();
self.note_drag = Some(NoteDrag {
id,
source: surface,
overlay,
origin,
position: origin,
start: self.note_pointer.get(&surface).copied().unwrap_or_default(),
output_size: size,
obstacles,
});
let action = cosmic::surface::action::app_layer_shell::<Self>(
|_| Default::default(),
move |_| SctkLayerSurfaceSettings {
id: overlay,
layer: Layer::Overlay,
keyboard_interactivity: KeyboardInteractivity::None,
input_zone: Some(Vec::new()),
anchor: Anchor::TOP | Anchor::LEFT,
margin: IcedMargin {
top: origin.y.round() as i32,
left: origin.x.round() as i32,
..Default::default()
},
output: IcedOutput::Output(output.clone()),
namespace: "ferese-shell-note-drag".into(),
exclusive_zone: -1,
size: Some((Some(dimensions.0), Some(dimensions.1))),
size_limits: Limits::NONE,
},
Some(Box::new(Self::view_note_drag)),
);
Task::batch([
self.finish_note_edit(),
cosmic::task::message(cosmic::Action::Surface(action)),
])
}
pub(super) fn view_note_drag(&self) -> Element<'_, cosmic::Action<Message>> {
let Some(drag) = &self.note_drag else {
return text("").into();
};
if let Some(id) = &drag.id {
self.view_note_content(id, drag.source)
} else {
self.view_clock_content()
}
}
pub(super) fn finish_note_drag(&mut self, save: bool) -> Task<Message> {
let Some(drag) = self.note_drag.take() else {
return Task::none();
};
let mut tasks = vec![destroy_layer_surface(drag.overlay)];
if save {
if drag.id.is_none() {
let clock = &mut self.config.desktop_widgets.clock;
clock.anchor = ferese_core::desktop::Anchor::TopLeft;
clock.margin_x = drag.position.x.round() as i32;
clock.margin_y = drag.position.y.round() as i32;
for entry in &self.outputs {
if let Some(id) = entry.clock {
tasks.push(set_anchor(id, Anchor::TOP | Anchor::LEFT));
tasks.push(set_margin(id, clock.margin_y, 0, 0, clock.margin_x));
}
}
self.note_pending
.push(note_store::Edit::ClockPosition(clock.margin_x, clock.margin_y));
}
if let Some(note) = self
.config
.desktop_widgets
.notes
.iter_mut()
.find(|note| Some(&note.id) == drag.id.as_ref())
{
note.anchor = ferese_core::desktop::Anchor::TopLeft;
note.margin_x = drag.position.x.round() as i32;
note.margin_y = drag.position.y.round() as i32;
for entry in &self.outputs {
if let Some((_, id)) = entry.notes.iter().find(|(id, _)| Some(id) == drag.id.as_ref()) {
tasks.push(set_anchor(*id, Anchor::TOP | Anchor::LEFT));
tasks.push(set_margin(*id, note.margin_y, 0, 0, note.margin_x));
}
}
self.note_pending.push(note_store::Edit::Position(
note.id.clone(),
note.margin_x,
note.margin_y,
));
}
tasks.push(self.flush_note_changes());
}
Task::batch(tasks)
}
pub(super) fn rebuild_notes(&mut self, reset: bool) -> Task<Message> {
let mut tasks = Vec::new();
for entry in &mut self.outputs {
if reset {
for (_, id) in entry.notes.drain(..) {
self.note_pointer.remove(&id);
tasks.push(destroy_layer_surface(id));
}
}
for note in &self.config.desktop_widgets.notes {
if !note.on_output(entry.name.as_deref()) || entry.notes.iter().any(|(id, _)| id == &note.id) {
continue;
}
let id = window::Id::unique();
entry.notes.push((note.id.clone(), id));
let note = note.clone();
let note_id = note.id.clone();
let output = entry.output.clone();
let action = cosmic::surface::action::app_layer_shell::<Self>(
|_| Default::default(),
move |_| {
let (anchor, margin) = widget_placement(note.anchor, note.margin_x, note.margin_y);
SctkLayerSurfaceSettings {
id,
layer: Layer::Bottom,
keyboard_interactivity: if note.interactive {
KeyboardInteractivity::OnDemand
} else {
KeyboardInteractivity::None
},
input_zone: if note.interactive { None } else { Some(Vec::new()) },
anchor,
margin,
output: IcedOutput::Output(output.clone()),
namespace: "ferese-shell-sticky-note".into(),
exclusive_zone: -1,
size: Some((Some(note.width), Some(note.height))),
size_limits: Limits::NONE,
}
},
Some(Box::new(move |app| app.view_note(&note_id, id))),
);
tasks.push(cosmic::task::message(cosmic::Action::Surface(action)));
}
}
Task::batch(tasks)
}
pub(super) fn view_note(&self, id: &str, surface: window::Id) -> Element<'_, cosmic::Action<Message>> {
if self
.note_drag
.as_ref()
.is_some_and(|drag| drag.id.as_deref() == Some(id))
{
return container(text("")).width(Length::Fill).height(Length::Fill).into();
}
self.view_note_content(id, surface)
}
pub(super) fn view_note_content(&self, id: &str, surface: window::Id) -> Element<'_, cosmic::Action<Message>> {
let Some(note) = self.config.desktop_widgets.notes.iter().find(|note| note.id == id) else {
return text("").into();
};
let alignment = match note.alignment {
ferese_core::desktop::Alignment::Left => alignment::Horizontal::Left,
ferese_core::desktop::Alignment::Center => alignment::Horizontal::Center,
ferese_core::desktop::Alignment::Right => alignment::Horizontal::Right,
};
let font = configured_font(
note.font_family
.as_deref()
.filter(|s| !s.trim().is_empty())
.or(self.config.font_family.as_deref()),
);
let mut foreground = color(
note.color
.as_deref()
.and_then(config::parse_color)
.unwrap_or(self.config.theme.text_primary),
);
foreground.a *= note.opacity;
let background = if note.background.as_deref() == Some("") {
None
} else {
let mut tint = color(
note.background
.as_deref()
.and_then(config::parse_color)
.unwrap_or(self.config.theme.surface_base),
);
tint.a *= note.opacity;
Some(Background::Color(tint))
};
let mut body = cosmic::widget::column([]).spacing(note.gap).align_x(alignment);
if note.interactive {
let title = ferese_theme::text(if note.title.is_empty() { "⋮⋮" } else { &note.title }, font)
.size(note.title_size)
.class(theme::Text::Color(foreground));
let header = cosmic::widget::mouse_area(container(title).width(Length::Fill))
.interaction(cosmic::iced::mouse::Interaction::Grab)
.on_press(cosmic::Action::App(Message::BeginNoteDrag(id.to_owned(), surface)));
let editing = self.note_editor.as_ref().is_some_and(|editor| editor.id == id);
body = body.push(
row([]).push(header).push(motion::button(
button::custom(text(if editing { "Done" } else { "Edit" }))
.padding([4, 8])
.on_press(cosmic::Action::App(if editing {
Message::FinishNoteEdit
} else {
Message::BeginNoteEdit(id.to_owned())
})),
foreground,
false,
1.0,
)),
);
} else if !note.title.is_empty() {
body = body.push(
ferese_theme::text(
note.title.clone(),
cosmic::font::Font {
weight: cosmic::iced::font::Weight::Semibold,
..font
},
)
.size(note.title_size)
.width(Length::Fill)
.align_x(alignment)
.class(theme::Text::Color(foreground)),
);
}
if let Some(editor) = &self.note_editor
&& editor.id == id
{
let id = id.to_owned();
body = body.push(
cosmic::widget::TextEditor::new(&editor.content)
.style(|theme, status| {
use cosmic::iced::widget::text_editor::Catalog;
let mut style = theme.style(&<cosmic::Theme as Catalog>::default(), status);
style.border.radius = motion::radius(f32::MAX).into();
style
})
.height(Length::Fill)
.font(font)
.size(note.text_size)
.on_action(move |action| cosmic::Action::App(Message::NoteAction(id.clone(), action))),
);
} else {
body = body.push(
ferese_theme::text(note.text.clone(), font)
.size(note.text_size)
.width(Length::Fill)
.align_x(alignment)
.class(theme::Text::Color(foreground)),
);
}
if let Some(error) = &self.note_error {
body = body.push(text(error.clone()).size(10).class(theme::Text::Color(foreground)));
}
let radius = self.config.theme.material_radius;
container(body)
.padding(note.padding)
.width(Length::Fill)
.height(Length::Fill)
.class(theme::Container::custom(move |_| container::Style {
background,
border: Border {
radius: radius.into(),
..Default::default()
},
..Default::default()
}))
.into()
}
pub(super) fn rebuild_clocks(&mut self, reset: bool) -> Task<Message> {
self.desktop_clock = self
.config
.desktop_widgets
.clock
.labels(&Zoned::now())
.unwrap_or_default();
let mut tasks = Vec::new();
for entry in &mut self.outputs {
if !reset && entry.clock.is_some() {
continue;
}
if let Some(id) = entry.clock.take() {
self.note_pointer.remove(&id);
tasks.push(destroy_layer_surface(id));
}
let clock = self.config.desktop_widgets.clock.clone();
if !clock.on_output(entry.name.as_deref()) {
continue;
}
let id = window::Id::unique();
entry.clock = Some(id);
let output = entry.output.clone();
let action = cosmic::surface::action::app_layer_shell::<Self>(
|_| Default::default(),
move |_| {
let (anchor, margin) = clock_placement(&clock);
SctkLayerSurfaceSettings {
id,
layer: Layer::Bottom,
keyboard_interactivity: KeyboardInteractivity::OnDemand,
input_zone: None,
anchor,
margin,
output: IcedOutput::Output(output.clone()),
namespace: "ferese-shell-desktop-clock".into(),
exclusive_zone: -1,
size: Some((Some(clock.width), Some(clock.height))),
size_limits: Limits::NONE,
}
},
Some(Box::new(move |app| app.view_desktop_clock(id))),
);
tasks.push(cosmic::task::message(cosmic::Action::Surface(action)));
}
Task::batch(tasks)
}
pub(super) fn view_desktop_clock(&self, id: window::Id) -> Element<'_, cosmic::Action<Message>> {
if self
.note_drag
.as_ref()
.is_some_and(|drag| drag.id.is_none() && drag.source == id)
{
return container(text("")).width(Length::Fill).height(Length::Fill).into();
}
cosmic::widget::mouse_area(self.view_clock_content())
.on_press(cosmic::Action::App(Message::BeginClockDrag(id)))
.interaction(cosmic::iced::mouse::Interaction::Grab)
.into()
}
pub(super) fn view_clock_content(&self) -> Element<'_, cosmic::Action<Message>> {
use ferese_core::desktop::Alignment as ClockAlignment;
let clock = &self.config.desktop_widgets.clock;
let alignment = match clock.alignment {
ClockAlignment::Left => alignment::Horizontal::Left,
ClockAlignment::Center => alignment::Horizontal::Center,
ClockAlignment::Right => alignment::Horizontal::Right,
};
let pixel = clock.style == ferese_core::desktop::ClockStyle::Pixel;
let custom_font = clock.font_family.as_deref().filter(|family| !family.trim().is_empty());
let mut font = configured_font(if pixel {
custom_font.or(Some("Cantarell"))
} else {
custom_font.or(self.config.font_family.as_deref())
});
if pixel && custom_font.is_none() {
font.weight = cosmic::iced::font::Weight::Black;
} else if clock.bold {
font.weight = cosmic::iced::font::Weight::Bold;
}
let date_font = configured_font(custom_font.or(self.config.font_family.as_deref()));
let tint = |custom: &Option<String>, fallback| {
let mut tint = color(custom.as_deref().and_then(config::parse_color).unwrap_or(fallback));
tint.a *= clock.opacity;
tint
};
let foreground = tint(&clock.color, self.config.theme.text_primary);
let accent = color(self.config.theme.accent);
let custom_color = clock.color.as_deref().and_then(config::parse_color).is_some();
let digit_colors = [0.8, 0.45].map(|text_mix| {
if custom_color {
return foreground;
}
Color {
r: foreground.r * text_mix + accent.r * (1.0 - text_mix),
g: foreground.g * text_mix + accent.g * (1.0 - text_mix),
b: foreground.b * text_mix + accent.b * (1.0 - text_mix),
a: foreground.a,
}
});
let mut labels = cosmic::widget::column([]).spacing(clock.gap).align_x(alignment);
if pixel && clock.show_date {
labels = labels.push(
ferese_theme::text(self.desktop_clock.1.clone(), date_font)
.size(clock.date_size)
.class(theme::Text::Color(tint(
&clock.date_color,
self.config.theme.text_primary,
))),
);
}
if let Some(digits) = pixel.then(|| stacked_clock_digits(&self.desktop_clock.0)).flatten() {
let available_height = (clock.height as f32
- clock.padding * 2.0
- if clock.show_date {
clock.date_size * 1.3 + clock.gap
} else {
0.0
})
.max(16.0);
let size = clock
.time_size
.min(available_height / 1.6)
.min((clock.width as f32 - clock.padding * 2.0).max(16.0) / 1.5);
let mut stack = cosmic::widget::column([]).spacing(0).align_x(alignment);
for (line, digits) in digits.into_iter().enumerate() {
let mut digit_row = row([]).spacing(0);
for (position, digit) in digits.chars().enumerate() {
digit_row = digit_row.push(
ferese_theme::text(digit.to_string(), font)
.size(size)
.line_height(cosmic::iced::widget::text::LineHeight::Relative(0.8))
.class(theme::Text::Color(digit_colors[(line + position) % 2])),
);
}
stack = stack.push(digit_row);
}
labels = labels.push(stack);
} else {
let characters = self.desktop_clock.0.chars().count().max(1) as f32;
let available_width = (clock.width as f32 - clock.padding * 2.0).max(1.0);
let available_height = (clock.height as f32
- clock.padding * 2.0
- if clock.show_date {
clock.date_size * 1.3 + clock.gap
} else {
0.0
})
.max(1.0);
let size = clock
.time_size
.min(available_width / (characters * 0.75))
.min(available_height / 1.3);
labels = labels.push(
ferese_theme::text(self.desktop_clock.0.clone(), font)
.size(size)
.width(Length::Fill)
.align_x(alignment)
.class(theme::Text::Color(foreground)),
);
}
if !pixel && clock.show_date {
labels = labels.push(
ferese_theme::text(self.desktop_clock.1.clone(), font)
.size(clock.date_size)
.width(Length::Fill)
.align_x(alignment)
.class(theme::Text::Color(tint(
&clock.date_color,
self.config.theme.text_muted,
))),
);
}
let background = clock.background.as_deref().and_then(config::parse_color).map(|rgba| {
let mut tint = color(rgba);
tint.a *= clock.opacity;
Background::Color(tint)
});
let radius = self.config.theme.material_radius;
container(labels)
.padding(clock.padding)
.width(Length::Fill)
.height(Length::Fill)
.align_x(alignment)
.center_y(Length::Fill)
.class(theme::Container::custom(move |_| container::Style {
background,
border: Border {
radius: radius.into(),
..Default::default()
},
..Default::default()
}))
.into()
}
}
/// Sweep each axis, allowing edge sliding without tunnelling through a widget
/// when pointer events skip ahead. Coordinates match integer layer margins.
pub(super) fn avoid_widget_overlap(
previous: cosmic::iced::Point,
requested: cosmic::iced::Point,
size: (u32, u32),
obstacles: &[cosmic::iced::Rectangle],
) -> cosmic::iced::Point {
let mut position = previous;
let (width, height) = (size.0 as f32, size.1 as f32);
let mut x = requested.x.round();
for obstacle in obstacles {
if position.y < obstacle.y + obstacle.height && position.y + height > obstacle.y {
if x > position.x && position.x + width <= obstacle.x {
x = x.min((obstacle.x - width).floor());
} else if x < position.x && position.x >= obstacle.x + obstacle.width {
x = x.max((obstacle.x + obstacle.width).ceil());
}
}
}
position.x = x;
let mut y = requested.y.round();
for obstacle in obstacles {
if position.x < obstacle.x + obstacle.width && position.x + width > obstacle.x {
if y > position.y && position.y + height <= obstacle.y {
y = y.min((obstacle.y - height).floor());
} else if y < position.y && position.y >= obstacle.y + obstacle.height {
y = y.max((obstacle.y + obstacle.height).ceil());
}
}
}
position.y = y;
position
}
pub(super) fn clamp_note_position(
position: cosmic::iced::Point,
output: (i32, i32),
size: (u32, u32),
) -> cosmic::iced::Point {
cosmic::iced::Point::new(
position.x.clamp(0., (output.0 as f32 - size.0 as f32).clamp(0., 8192.)),
position.y.clamp(0., (output.1 as f32 - size.1 as f32).clamp(0., 8192.)),
)
}
pub(super) fn note_origin(note: &ferese_core::desktop::StickyNote, output: (i32, i32)) -> cosmic::iced::Point {
widget_origin(
note.anchor,
note.margin_x,
note.margin_y,
(note.width, note.height),
output,
)
}
pub(super) fn widget_origin(
anchor: ferese_core::desktop::Anchor,
margin_x: i32,
margin_y: i32,
size: (u32, u32),
output: (i32, i32),
) -> cosmic::iced::Point {
use ferese_core::desktop::Anchor as A;
let x = match anchor {
A::TopLeft | A::CenterLeft | A::BottomLeft => margin_x as f32,
A::TopRight | A::CenterRight | A::BottomRight => output.0 as f32 - size.0 as f32 - margin_x as f32,
_ => (output.0 as f32 - size.0 as f32) / 2.,
};
let y = match anchor {
A::TopLeft | A::TopCenter | A::TopRight => margin_y as f32,
A::BottomLeft | A::BottomCenter | A::BottomRight => output.1 as f32 - size.1 as f32 - margin_y as f32,
_ => (output.1 as f32 - size.1 as f32) / 2.,
};
clamp_note_position(cosmic::iced::Point::new(x, y), output, size)
}
pub(super) fn clock_placement(clock: &ferese_core::desktop::Clock) -> (Anchor, IcedMargin) {
widget_placement(clock.anchor, clock.margin_x, clock.margin_y)
}
pub(super) fn widget_placement(
position: ferese_core::desktop::Anchor,
margin_x: i32,
margin_y: i32,
) -> (Anchor, IcedMargin) {
use ferese_core::desktop::Anchor as Position;
let horizontal = match position {
Position::TopLeft | Position::CenterLeft | Position::BottomLeft => Anchor::LEFT,
Position::TopRight | Position::CenterRight | Position::BottomRight => Anchor::RIGHT,
_ => Anchor::empty(),
};
let vertical = match position {
Position::TopLeft | Position::TopCenter | Position::TopRight => Anchor::TOP,
Position::BottomLeft | Position::BottomCenter | Position::BottomRight => Anchor::BOTTOM,
_ => Anchor::empty(),
};
(
horizontal | vertical,
IcedMargin {
top: if vertical == Anchor::TOP { margin_y } else { 0 },
bottom: if vertical == Anchor::BOTTOM { margin_y } else { 0 },
left: if horizontal == Anchor::LEFT { margin_x } else { 0 },
right: if horizontal == Anchor::RIGHT { margin_x } else { 0 },
},
)
}
pub(super) fn current_time() -> String {
format_bar_time(&Zoned::now())
}
File diff suppressed because it is too large. Load diff
+556
View File
@@ -0,0 +1,556 @@
use super::{
Anchor, Background, BarMetrics, Color, Connection, ContentFit, Dispatch, EFFECT_FRAME_PENDING, Element, Event,
EventQueue, FereseEffectsManagerV1, FereseShell, FereseSurfaceEffectsV1, GlobalListContents, IcedMargin,
IcedOutput, KeyboardInteractivity, Layer, Length, Limits, Message, Ordering, OutputSurfaces, PlatformSpecific,
Proxy, QueueHandle, SctkLayerSurfaceSettings, Task, WallpaperMode, avoid_widget_overlap, clamp_note_position,
config, container, delegate_noop, destroy_layer_surface, ferese_surface_effects_v1, image, output_bar_hidden,
registry_queue_init, set_margin, status_ui, text, theme, wayland, window, wl_output, wl_registry, wl_surface,
};
impl FereseShell {
pub(super) fn output_event(&mut self, event: wayland::OutputEvent, output: wl_output::WlOutput) -> Task<Message> {
if matches!(event, wayland::OutputEvent::Removed) {
if let Some(index) = self.outputs.iter().position(|entry| entry.output == output) {
let entry = self.outputs.remove(index);
let menu = if self.bar_surface_id == entry.bar {
self.destroy_menu()
} else {
Task::none()
};
let mut tasks = vec![destroy_layer_surface(entry.bar), menu, self.rebuild_system_modal()];
if self.note_drag.as_ref().is_some_and(|drag| {
entry.clock == Some(drag.source) || entry.notes.iter().any(|(_, id)| *id == drag.source)
}) {
tasks.push(self.finish_note_drag(false));
}
if let Some(wallpaper) = entry.wallpaper {
tasks.push(destroy_layer_surface(wallpaper));
}
if let Some(clock) = entry.clock {
tasks.push(destroy_layer_surface(clock));
}
for (_, id) in entry.notes {
self.note_pointer.remove(&id);
tasks.push(destroy_layer_surface(id));
}
return Task::batch(tasks);
}
return Task::none();
}
let size = match &event {
wayland::OutputEvent::Created(info) => info.as_ref().and_then(|info| info.logical_size),
wayland::OutputEvent::InfoUpdate(info) => info.logical_size,
_ => None,
};
let name = match event {
wayland::OutputEvent::Created(info) => info.and_then(|info| info.name),
wayland::OutputEvent::InfoUpdate(info) => info.name,
_ => None,
};
if let Some(entry) = self.outputs.iter_mut().find(|entry| entry.output == output) {
if size.is_some() {
entry.size = size;
}
let changed = name.is_some() && entry.name != name;
if name.is_some() {
entry.name = name;
}
return if changed {
Task::batch([self.rebuild_clocks(true), self.rebuild_notes(true)])
} else {
Task::none()
};
}
let bar_surface_id = window::Id::unique();
let wallpaper_surface_id = window::Id::unique();
let shell_theme = self.config.theme;
let bar = BarMetrics::from(shell_theme);
let wallpaper_output = output.clone();
let bar_output = output.clone();
let hidden = output_bar_hidden(
&self.snapshot,
self.snapshot
.outputs
.iter()
.find(|output| Some(output.name.as_str()) == name.as_deref())
.map(|output| output.id),
);
self.outputs.push(OutputSurfaces {
output,
name,
bar: bar_surface_id,
wallpaper: std::env::var_os("FERESE_COMPOSITOR_WALLPAPER")
.is_none()
.then_some(wallpaper_surface_id),
effects: None,
clock: None,
notes: Vec::new(),
size,
hidden,
});
let wallpaper_action = cosmic::surface::action::app_layer_shell::<Self>(
|_| Default::default(),
move |_| SctkLayerSurfaceSettings {
id: wallpaper_surface_id,
layer: Layer::Background,
keyboard_interactivity: KeyboardInteractivity::None,
input_zone: Some(Vec::new()),
anchor: Anchor::TOP | Anchor::RIGHT | Anchor::BOTTOM | Anchor::LEFT,
output: IcedOutput::Output(wallpaper_output.clone()),
namespace: "ferese-shell-wallpaper".to_owned(),
// Wallpaper covers the full output, including beneath the bar
// and its floating margins. Zero would use the remaining workspace.
exclusive_zone: -1,
size: Some((None, None)),
size_limits: Limits::NONE,
..Default::default()
},
Some(Box::new(Self::view_wallpaper)),
);
let bar_action = cosmic::surface::action::app_layer_shell::<Self>(
|_| Default::default(),
move |_| SctkLayerSurfaceSettings {
id: bar_surface_id,
input_zone: hidden.then(Vec::new),
layer: Layer::Top,
keyboard_interactivity: KeyboardInteractivity::None,
anchor: Anchor::TOP | Anchor::LEFT | Anchor::RIGHT,
output: IcedOutput::Output(bar_output.clone()),
namespace: "ferese-shell-top-bar".to_owned(),
margin: IcedMargin {
top: shell_theme.bar_margin_top,
right: shell_theme.bar_margin_horizontal,
bottom: 0,
left: shell_theme.bar_margin_horizontal,
},
size: Some((None, Some(bar.height.round() as u32))),
size_limits: Limits::NONE,
// Reserve breathing room below the visible bar; layer-shell
// accounts for the top margin separately.
exclusive_zone: (bar.height.round() as i32).saturating_add(shell_theme.bar_window_gap),
},
Some(Box::new(move |app| app.view_layer(bar_surface_id))),
);
// Submit the small interactive surface before the full-screen image.
let mut surfaces = vec![bar_action];
if std::env::var_os("FERESE_COMPOSITOR_WALLPAPER").is_none() {
surfaces.push(wallpaper_action);
}
let tasks = surfaces
.into_iter()
.map(cosmic::Action::Surface)
.map(cosmic::task::message);
Task::batch([
Task::batch(tasks),
self.rebuild_clocks(false),
self.rebuild_notes(false),
self.rebuild_system_modal(),
cosmic::task::message(cosmic::Action::App(Message::ShowGuide)),
])
}
pub(super) fn handle_event(&mut self, event: Event, id: window::Id) -> Task<Message> {
if let Event::Mouse(mouse) = &event {
if !self
.outputs
.iter()
.any(|entry| entry.clock == Some(id) || entry.notes.iter().any(|(_, surface)| *surface == id))
{
return Task::none();
}
match mouse {
cosmic::iced::mouse::Event::CursorMoved { position } => {
self.note_pointer.insert(id, *position);
if let Some(drag) = &mut self.note_drag
&& drag.source == id
{
let delta = *position - drag.start;
let dimensions = if let Some(id) = &drag.id {
self.config
.desktop_widgets
.notes
.iter()
.find(|note| &note.id == id)
.map(|note| (note.width, note.height))
} else {
let clock = &self.config.desktop_widgets.clock;
Some((clock.width, clock.height))
};
if let Some(dimensions) = dimensions {
let requested = clamp_note_position(drag.origin + delta, drag.output_size, dimensions);
drag.position = avoid_widget_overlap(drag.position, requested, dimensions, &drag.obstacles);
// Move a compact, cached buffer in the compositor instead of
// repainting an output-sized buffer for every pointer event.
return set_margin(
drag.overlay,
drag.position.y.round() as i32,
0,
0,
drag.position.x.round() as i32,
);
}
}
}
cosmic::iced::mouse::Event::ButtonReleased(cosmic::iced::mouse::Button::Left)
if self.note_drag.as_ref().is_some_and(|drag| drag.source == id) =>
{
return self.finish_note_drag(true);
}
_ => {}
}
}
if let Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Tab),
modifiers,
..
}) = &event
&& !modifiers.control()
&& self.system_modal.as_ref().is_some_and(|modal| modal.contains(id))
{
return self.navigate_system_modal(modifiers.shift());
}
if matches!(
&event,
Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape),
..
})
) {
if self.system_modal.is_some() {
return self.close_system_modal();
}
if self.note_drag.is_some() {
return self.finish_note_drag(false);
}
if self.note_editor.is_some() {
return self.finish_note_edit();
}
if self.notifications.history_open {
return self.close_notification_history();
}
}
match event {
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output(event, output))) => {
self.output_event(event, output)
}
Event::Window(window::Event::Opened { .. })
if self.outputs.iter().any(|entry| entry.bar == id)
|| self.menu.as_ref().is_some_and(|menu| menu.id == id)
|| self.system_modal.as_ref().is_some_and(|modal| modal.contains(id))
|| self
.notification_surface
.as_ref()
.is_some_and(|surface| surface.id == id) =>
{
Task::batch([
self.focus_system_modal(id),
window::run(id, native_wayland_surface)
.map(move |surface| cosmic::Action::App(Message::NativeSurface(id, surface))),
])
}
Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape),
..
}) if self.menu.is_some() => self.close_menu(),
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Popup(event, surface, popup_id))) => {
if let Some(menu) = &mut self.menu
&& menu.id == popup_id
{
match event {
wayland::PopupEvent::Done => {
if menu.kind == status_ui::Menu::Notifications {
self.notifications.history_open = false;
self.notifications.hovered = None;
}
self.menu = None;
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
return self.sync_notification_surface();
}
wayland::PopupEvent::Configured { .. } => {
menu.prepare_surface(&surface);
}
_ => {}
}
if let Some(effects) = &menu.effects {
let _ = effects.set_opacity(menu.progress());
}
}
Task::none()
}
Event::Window(window::Event::Closed) if self.menu.as_ref().is_some_and(|menu| menu.id == id) => {
self.menu = None;
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
Task::none()
}
Event::Window(window::Event::Closed)
if self.system_modal.as_ref().is_some_and(|modal| modal.contains(id)) =>
{
self.close_system_modal()
}
Event::Window(window::Event::Closed) => Task::none(),
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(_, surface, frame_id)))
if self.menu.as_ref().is_some_and(|menu| menu.id == frame_id) =>
{
if let Some(menu) = &mut self.menu
&& menu.effects.is_none()
{
menu.prepare_surface(&surface);
}
Task::none()
}
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(_, surface, frame_id)))
if self.outputs.iter().any(|entry| entry.bar == frame_id) =>
{
self.attach_effects(frame_id, &surface);
Task::none()
}
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer(_, surface, layer_id)))
if self
.notification_surface
.as_ref()
.is_some_and(|entry| entry.id == layer_id) =>
{
let entry = self.notification_surface.as_mut().unwrap();
if entry.effects.is_none() {
match EffectsBinding::attach_role(&surface, None, 1.0) {
Ok(binding) => entry.effects = Some(binding),
Err(error) => {
eprintln!("ferese-shell: notification effects unavailable: {error}")
}
}
}
Task::none()
}
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer(_, surface, layer_id)))
if self.outputs.iter().any(|entry| entry.bar == layer_id) =>
{
self.attach_effects(layer_id, &surface);
self.attach_power_material(layer_id, &surface);
Task::none()
}
_ => Task::none(),
}
}
pub(super) fn attach_effects(&mut self, id: window::Id, surface: &wl_surface::WlSurface) {
let hidden = self.bar_hidden(id);
let Some(entry) = self.outputs.iter_mut().find(|entry| entry.bar == id) else {
return;
};
if entry.effects.is_some() {
return;
}
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
match EffectsBinding::attach(surface, !hidden) {
Ok(binding) => entry.effects = Some(binding),
Err(error) => eprintln!("ferese-shell: panel material unavailable: {error}"),
}
}
pub(super) fn view_wallpaper(&self) -> Element<'_, cosmic::Action<Message>> {
let Some(handle) = self.wallpaper.as_ref() else {
return container(text(""))
.width(Length::Fill)
.height(Length::Fill)
.class(theme::Container::custom(wallpaper_fallback_style))
.into();
};
let content_fit = match self.config.wallpaper.mode {
WallpaperMode::Fill => ContentFit::Cover,
WallpaperMode::Fit => ContentFit::Contain,
};
container(
image(handle.clone())
.width(Length::Fill)
.height(Length::Fill)
.content_fit(content_fit),
)
.width(Length::Fill)
.height(Length::Fill)
.class(theme::Container::custom(wallpaper_fallback_style))
.into()
}
}
// Resolve a surface while iced guarantees the native handles are alive. Keep
// the borrowed-display connection alive until the effects binding takes over.
pub(super) fn native_wayland_surface(
window: &dyn cosmic::iced::window::Window,
) -> Result<(Connection, wl_surface::WlSurface), String> {
use cosmic::iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle};
let display = window.display_handle().map_err(|error| error.to_string())?;
let handle = window.window_handle().map_err(|error| error.to_string())?;
let (RawDisplayHandle::Wayland(display), RawWindowHandle::Wayland(handle)) = (display.as_raw(), handle.as_raw())
else {
return Err("shell material requires a Wayland surface".to_owned());
};
// SAFETY: iced lends matching, live Wayland handles for this callback.
// This backend borrows the display; it never disconnects iced's connection.
// Both handles belong to the runtime, which outlives the shell bindings.
let backend = unsafe { wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) };
let connection = Connection::from_backend(backend);
// SAFETY: raw-window-handle identifies this pointer as a live wl_surface.
let id = unsafe {
wayland_client::backend::ObjectId::from_ptr(wl_surface::WlSurface::interface(), handle.surface.as_ptr().cast())
}
.map_err(|error| error.to_string())?;
let surface = wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
Ok((connection, surface))
}
pub(super) fn wallpaper_fallback_style(_theme: &cosmic::Theme) -> container::Style {
let [red, green, blue] = config::default_background();
container::Style {
background: Some(Background::Color(Color::from_rgb8(red, green, blue))),
..container::Style::default()
}
}
pub(super) struct EffectsBinding {
connection: Connection,
_manager: FereseEffectsManagerV1,
pub(super) surface: FereseSurfaceEffectsV1,
_queue: EventQueue<EffectsState>,
regions: std::cell::RefCell<Option<Vec<[i32; 5]>>>,
opacity: std::cell::Cell<Option<u32>>,
}
impl EffectsBinding {
pub(super) fn attach(surface: &wl_surface::WlSurface, visible: bool) -> Result<Self, Box<dyn std::error::Error>> {
Self::attach_role(
surface,
visible.then_some(ferese_surface_effects_v1::Role::Panel),
if visible { 1.0 } else { 0.0 },
)
}
pub(super) fn attach_role(
surface: &wl_surface::WlSurface,
role: Option<ferese_surface_effects_v1::Role>,
opacity: f32,
) -> Result<Self, Box<dyn std::error::Error>> {
let backend = surface
.backend()
.upgrade()
.ok_or("libcosmic Wayland connection is no longer alive")?;
let connection = Connection::from_backend(backend);
let (globals, queue) = registry_queue_init::<EffectsState>(&connection)?;
let qh = queue.handle();
let manager = globals.bind::<FereseEffectsManagerV1, _, _>(&qh, 1..=3, ())?;
let effects = manager.get_surface_effects(surface, &qh, ());
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
if effects.version() >= 3 {
effects.set_opacity(opacity);
}
if let Some(role) = role {
effects.set_role(role);
} else {
effects.clear_role();
}
connection.flush()?;
Ok(Self {
connection,
_manager: manager,
surface: effects,
_queue: queue,
regions: Default::default(),
opacity: std::cell::Cell::new(Some(opacity)),
})
}
pub(super) fn set_regions(&self, regions: &[[i32; 5]]) -> Result<(), Box<dyn std::error::Error>> {
self.set_material_regions(regions, ferese_surface_effects_v1::Role::Popover)
}
pub(super) fn set_material_regions(
&self,
regions: &[[i32; 5]],
role: ferese_surface_effects_v1::Role,
) -> Result<(), Box<dyn std::error::Error>> {
if self.surface.version() < 2 || self.regions.borrow().as_deref() == Some(regions) {
return Ok(());
}
self.surface.set_regions(
regions
.iter()
.flat_map(|r| r.iter().flat_map(|v| v.to_ne_bytes()))
.collect(),
);
if regions.is_empty() {
self.surface.clear_role();
} else {
self.surface.set_role(role);
}
self.connection.flush()?;
*self.regions.borrow_mut() = Some(regions.to_vec());
Ok(())
}
pub(super) fn set_visible(&self, visible: bool) -> Result<(), Box<dyn std::error::Error>> {
self.set_opacity(1.0)?;
if visible {
self.surface.set_role(ferese_surface_effects_v1::Role::Panel);
} else {
self.surface.clear_role();
}
self.connection.flush()?;
Ok(())
}
pub(super) fn set_opacity(&self, opacity: f32) -> Result<(), Box<dyn std::error::Error>> {
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
if self.surface.version() >= 3 && self.opacity.get() != Some(opacity) {
self.surface.set_opacity(opacity);
self.connection.flush()?;
self.opacity.set(Some(opacity));
}
Ok(())
}
}
struct EffectsState;
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for EffectsState {
fn event(
_state: &mut Self,
_registry: &wl_registry::WlRegistry,
_event: wl_registry::Event,
_data: &GlobalListContents,
_connection: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
delegate_noop!(EffectsState: ignore FereseEffectsManagerV1);
delegate_noop!(EffectsState: ignore FereseSurfaceEffectsV1);