Center focused window titles and simplify Control Center controls

This commit is contained in:
Samuel Onoja committed 2026-09-28 09:53:07 +01:00
1 parent e9dcc968e1
commit f933484c7b
5 files changed
+177 -68

No files matched your search

+6
View File
@@ -705,6 +705,12 @@ pub fn fields(page: Page) -> Vec<Field> {
),
],
Page::Bar => vec![
toggle(
"status.window_title",
"Focused window title",
"Show the focused window title in the center of the bar when space allows.",
true,
),
range(
"theme.geometry.top_bar_height",
"Height",
+2
View File
@@ -20,6 +20,7 @@ pub(crate) struct ShellConfig {
#[serde(default)]
pub(crate) struct StatusConfig {
pub(crate) battery_percentage: bool,
pub(crate) window_title: bool,
pub(crate) low_battery_threshold: u8,
pub(crate) settings_command: Option<Vec<String>>,
}
@@ -28,6 +29,7 @@ impl Default for StatusConfig {
fn default() -> Self {
Self {
battery_percentage: true,
window_title: true,
low_battery_threshold: 20,
settings_command: Some(vec!["ferese-settings".into()]),
}
+79 -5
View File
@@ -1229,7 +1229,6 @@ impl FereseShell {
size_limits: Limits::NONE,
..Default::default()
},
Some(Box::new(Self::view_note_drag)),
);
@@ -1360,7 +1359,6 @@ impl FereseShell {
..Default::default()
}
},
Some(Box::new(move |app| app.view_note(&note_id, id))),
);
@@ -1586,7 +1584,6 @@ impl FereseShell {
..Default::default()
}
},
Some(Box::new(move |app| app.view_desktop_clock(id))),
);
@@ -2199,11 +2196,47 @@ impl FereseShell {
]
.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),
container(right).width(Length::Shrink),
center,
container(right)
.width(Length::Fill)
.align_x(alignment::Horizontal::Right),
]
.spacing(16)
.spacing(8)
.align_y(cosmic::iced::Alignment::Center)
.height(Length::Fill);
@@ -2362,6 +2395,26 @@ fn workspace_indicator(
.into()
}
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
}
})
}
fn workspace_active_on_bar(workspace: u64, output: Option<&control::OutputSnapshot>) -> bool {
output.is_some_and(|output| output.active_workspace == workspace)
}
@@ -3072,6 +3125,27 @@ mod tests {
assert!(!output_bar_hidden(&snapshot, Some(2)));
}
#[test]
fn bar_title_tracks_focus_and_the_outputs_active_workspace() {
let mut snapshot = snapshot_with_fullscreen_window(7);
assert_eq!(
focused_bar_title(&snapshot, snapshot.outputs.first()),
"Test"
);
snapshot.windows[0].focused = false;
assert_eq!(focused_bar_title(&snapshot, snapshot.outputs.first()), "");
snapshot.windows[0].focused = true;
snapshot.windows[0].workspace = 8;
assert_eq!(focused_bar_title(&snapshot, snapshot.outputs.first()), "");
snapshot.windows[0].workspace = 7;
snapshot.windows[0].title.clear();
assert_eq!(
focused_bar_title(&snapshot, snapshot.outputs.first()),
"dev.ferese.Test"
);
assert_eq!(focused_bar_title(&snapshot, None), "");
}
fn snapshot_with_fullscreen_window(workspace: u64) -> ShellSnapshot {
ShellSnapshot {
outputs: vec![OutputSnapshot {
+89 -63
View File
@@ -652,8 +652,7 @@ impl FereseShell {
}
if combined || menu.kind == Menu::Notifications {
if let Some(n) = &self.status.notifications {
let toggle =
toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), primary, p);
let toggle = toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), theme, p);
rows = rows.push(if combined {
control_card(toggle, primary, p)
} else {
@@ -802,7 +801,6 @@ impl FereseShell {
}
if combined {
let mut actions = column::with_capacity(2).spacing(8);
let mut power = row::with_capacity(3).spacing(6).width(Length::Fill);
if self
.config
.status
@@ -818,33 +816,6 @@ impl FereseShell {
p,
));
}
if self.status.poweroff {
power = power.push(footer_button(
"Power off…",
Message::ConfirmPower(Action::Poweroff),
primary,
p,
));
}
if self.status.reboot {
power = power.push(footer_button(
"Restart…",
Message::ConfirmPower(Action::Reboot),
primary,
p,
));
}
if self.status.suspend {
power = power.push(footer_button(
"Suspend…",
Message::ConfirmPower(Action::Suspend),
primary,
p,
));
}
if self.status.poweroff || self.status.reboot || self.status.suspend {
actions = actions.push(power);
}
rows = rows.push(control_card(actions.into(), primary, p));
}
if !menu.kind.available(&self.status) {
@@ -1022,33 +993,23 @@ fn connection_control<'a>(
source: &'static [u8],
enabled: bool,
action: Option<Action>,
theme: ShellTheme,
palette: ShellTheme,
opacity: f32,
) -> Element<'a, cosmic::Action<Message>> {
let foreground = color_with_opacity(theme.text_primary, opacity);
let icon_accent = color_with_opacity(theme.accent, opacity);
let accent = color_with_opacity(theme.accent, opacity * 0.14);
let icon = container(accented_icon(source, 18, foreground, icon_accent))
let foreground = color_with_opacity(palette.text_primary, opacity);
let icon_color = color_with_opacity(
if enabled {
palette.accent
} else {
palette.text_muted
},
opacity,
);
let icon = container(accented_icon(source, 18, icon_color, icon_color))
.width(32)
.height(32)
.align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Center)
.class(theme::Container::custom(move |_| container::Style {
background: enabled.then_some(Background::Color(accent)),
icon_color: Some(color_with_opacity(
if enabled {
theme.text_primary
} else {
theme.text_muted
},
opacity,
)),
border: Border {
radius: motion::radius(20.0).into(),
..Default::default()
},
..Default::default()
}));
.align_y(alignment::Vertical::Center);
let content = column![
icon,
text(label).size(13),
@@ -1058,14 +1019,12 @@ fn connection_control<'a>(
.height(18)
.wrapping(cosmic::iced::widget::text::Wrapping::None)
.class(theme::Text::Color(color_with_opacity(
theme.text_muted,
palette.text_muted,
opacity
)))
]
.spacing(6)
.width(Length::Fill);
// Battery is informational, not a toggle. Match its tile's layout without
// giving it a misleading clickable/disabled-button appearance.
let Some(action) = action else {
return container(content)
.width(Length::FillPortion(1))
@@ -1074,8 +1033,9 @@ fn connection_control<'a>(
};
container(motion::button(
button::custom(content)
.width(Length::FillPortion(1))
.width(Length::Fill)
.padding(4)
.name(format!("{label}: {}", if enabled { "on" } else { "off" }))
.on_press(cosmic::Action::App(Message::Control(action))),
foreground,
false,
@@ -1267,21 +1227,87 @@ fn footer_button<'a>(
)
}
fn shell_switch<'a>(
label: &str,
enabled: bool,
action: Action,
palette: ShellTheme,
opacity: f32,
) -> Element<'a, cosmic::Action<Message>> {
let track = if enabled {
color(palette.accent)
} else {
Color {
a: 0.22,
..color(palette.text_primary)
}
};
let thumb = if enabled {
ferese_theme::foreground(track, color(palette.text_primary))
} else {
color(palette.text_primary)
};
let knob = container(text(""))
.width(16)
.height(16)
.class(theme::Container::custom(move |_| container::Style {
background: Some(
Color {
a: opacity,
..thumb
}
.into(),
),
border: Border {
radius: motion::radius(8.).into(),
..Default::default()
},
..Default::default()
}));
let toggle = container(knob)
.width(36)
.height(20)
.padding(2)
.align_x(if enabled {
alignment::Horizontal::Right
} else {
alignment::Horizontal::Left
})
.class(theme::Container::custom(move |_| container::Style {
background: Some(
Color {
a: track.a * opacity,
..track
}
.into(),
),
border: Border {
radius: motion::radius(10.).into(),
..Default::default()
},
..Default::default()
}));
motion::button(
button::custom(toggle)
.padding(4)
.name(format!("{label}: {}", if enabled { "on" } else { "off" }))
.on_press(cosmic::Action::App(Message::Control(action))),
color(palette.text_primary),
false,
opacity,
)
}
fn toggle_row<'a>(
label: &'a str,
on: bool,
action: Action,
foreground: Color,
palette: ShellTheme,
opacity: f32,
) -> Element<'a, cosmic::Action<Message>> {
row![
text(label).width(Length::Fill).size(14),
menu_button(
if on { "On" } else { "Off" },
Message::Control(action),
foreground,
opacity
)
shell_switch(label, on, action, palette, opacity)
]
.align_y(Alignment::Center)
.into()