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()
}
}