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

+1
View File
@@ -353,6 +353,7 @@ returns. Ferese does not change the system's suspend policy.
| `status` key | Type | Default | Meaning | | `status` key | Type | Default | Meaning |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `window_title` | boolean | `true` | Focused window title in the bar center when space allows |
| `battery_percentage` | boolean | `true` | Show percentage beside icon | | `battery_percentage` | boolean | `true` | Show percentage beside icon |
| `low_battery_threshold` | integer 0–100 | `20` | Warning-color threshold | | `low_battery_threshold` | integer 0–100 | `20` | Warning-color threshold |
| `settings_command` | argument array | `["ferese-settings"]` | Settings launcher; `[]` hides the action | | `settings_command` | argument array | `["ferese-settings"]` | Settings launcher; `[]` hides the action |
+6
View File
@@ -705,6 +705,12 @@ pub fn fields(page: Page) -> Vec<Field> {
), ),
], ],
Page::Bar => vec![ 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( range(
"theme.geometry.top_bar_height", "theme.geometry.top_bar_height",
"Height", "Height",
+2
View File
@@ -20,6 +20,7 @@ pub(crate) struct ShellConfig {
#[serde(default)] #[serde(default)]
pub(crate) struct StatusConfig { pub(crate) struct StatusConfig {
pub(crate) battery_percentage: bool, pub(crate) battery_percentage: bool,
pub(crate) window_title: bool,
pub(crate) low_battery_threshold: u8, pub(crate) low_battery_threshold: u8,
pub(crate) settings_command: Option<Vec<String>>, pub(crate) settings_command: Option<Vec<String>>,
} }
@@ -28,6 +29,7 @@ impl Default for StatusConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
battery_percentage: true, battery_percentage: true,
window_title: true,
low_battery_threshold: 20, low_battery_threshold: 20,
settings_command: Some(vec!["ferese-settings".into()]), settings_command: Some(vec!["ferese-settings".into()]),
} }
+79 -5
View File
@@ -1229,7 +1229,6 @@ impl FereseShell {
size_limits: Limits::NONE, size_limits: Limits::NONE,
..Default::default() ..Default::default()
}, },
Some(Box::new(Self::view_note_drag)), Some(Box::new(Self::view_note_drag)),
); );
@@ -1360,7 +1359,6 @@ impl FereseShell {
..Default::default() ..Default::default()
} }
}, },
Some(Box::new(move |app| app.view_note(&note_id, id))), Some(Box::new(move |app| app.view_note(&note_id, id))),
); );
@@ -1586,7 +1584,6 @@ impl FereseShell {
..Default::default() ..Default::default()
} }
}, },
Some(Box::new(move |app| app.view_desktop_clock(id))), Some(Box::new(move |app| app.view_desktop_clock(id))),
); );
@@ -2199,11 +2196,47 @@ impl FereseShell {
] ]
.spacing(8) .spacing(8)
.align_y(cosmic::iced::Alignment::Center); .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![ let content = row![
container(left).width(Length::Fill), 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) .align_y(cosmic::iced::Alignment::Center)
.height(Length::Fill); .height(Length::Fill);
@@ -2362,6 +2395,26 @@ fn workspace_indicator(
.into() .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 { fn workspace_active_on_bar(workspace: u64, output: Option<&control::OutputSnapshot>) -> bool {
output.is_some_and(|output| output.active_workspace == workspace) output.is_some_and(|output| output.active_workspace == workspace)
} }
@@ -3072,6 +3125,27 @@ mod tests {
assert!(!output_bar_hidden(&snapshot, Some(2))); 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 { fn snapshot_with_fullscreen_window(workspace: u64) -> ShellSnapshot {
ShellSnapshot { ShellSnapshot {
outputs: vec![OutputSnapshot { outputs: vec![OutputSnapshot {
+89 -63
View File
@@ -652,8 +652,7 @@ impl FereseShell {
} }
if combined || menu.kind == Menu::Notifications { if combined || menu.kind == Menu::Notifications {
if let Some(n) = &self.status.notifications { if let Some(n) = &self.status.notifications {
let toggle = let toggle = toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), theme, p);
toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), primary, p);
rows = rows.push(if combined { rows = rows.push(if combined {
control_card(toggle, primary, p) control_card(toggle, primary, p)
} else { } else {
@@ -802,7 +801,6 @@ impl FereseShell {
} }
if combined { if combined {
let mut actions = column::with_capacity(2).spacing(8); let mut actions = column::with_capacity(2).spacing(8);
let mut power = row::with_capacity(3).spacing(6).width(Length::Fill);
if self if self
.config .config
.status .status
@@ -818,33 +816,6 @@ impl FereseShell {
p, 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)); rows = rows.push(control_card(actions.into(), primary, p));
} }
if !menu.kind.available(&self.status) { if !menu.kind.available(&self.status) {
@@ -1022,33 +993,23 @@ fn connection_control<'a>(
source: &'static [u8], source: &'static [u8],
enabled: bool, enabled: bool,
action: Option<Action>, action: Option<Action>,
theme: ShellTheme, palette: ShellTheme,
opacity: f32, opacity: f32,
) -> Element<'a, cosmic::Action<Message>> { ) -> Element<'a, cosmic::Action<Message>> {
let foreground = color_with_opacity(theme.text_primary, opacity); let foreground = color_with_opacity(palette.text_primary, opacity);
let icon_accent = color_with_opacity(theme.accent, opacity); let icon_color = color_with_opacity(
let accent = color_with_opacity(theme.accent, opacity * 0.14); if enabled {
let icon = container(accented_icon(source, 18, foreground, icon_accent)) palette.accent
} else {
palette.text_muted
},
opacity,
);
let icon = container(accented_icon(source, 18, icon_color, icon_color))
.width(32) .width(32)
.height(32) .height(32)
.align_x(alignment::Horizontal::Center) .align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::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()
}));
let content = column![ let content = column![
icon, icon,
text(label).size(13), text(label).size(13),
@@ -1058,14 +1019,12 @@ fn connection_control<'a>(
.height(18) .height(18)
.wrapping(cosmic::iced::widget::text::Wrapping::None) .wrapping(cosmic::iced::widget::text::Wrapping::None)
.class(theme::Text::Color(color_with_opacity( .class(theme::Text::Color(color_with_opacity(
theme.text_muted, palette.text_muted,
opacity opacity
))) )))
] ]
.spacing(6) .spacing(6)
.width(Length::Fill); .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 { let Some(action) = action else {
return container(content) return container(content)
.width(Length::FillPortion(1)) .width(Length::FillPortion(1))
@@ -1074,8 +1033,9 @@ fn connection_control<'a>(
}; };
container(motion::button( container(motion::button(
button::custom(content) button::custom(content)
.width(Length::FillPortion(1)) .width(Length::Fill)
.padding(4) .padding(4)
.name(format!("{label}: {}", if enabled { "on" } else { "off" }))
.on_press(cosmic::Action::App(Message::Control(action))), .on_press(cosmic::Action::App(Message::Control(action))),
foreground, foreground,
false, 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>( fn toggle_row<'a>(
label: &'a str, label: &'a str,
on: bool, on: bool,
action: Action, action: Action,
foreground: Color, palette: ShellTheme,
opacity: f32, opacity: f32,
) -> Element<'a, cosmic::Action<Message>> { ) -> Element<'a, cosmic::Action<Message>> {
row![ row![
text(label).width(Length::Fill).size(14), text(label).width(Length::Fill).size(14),
menu_button( shell_switch(label, on, action, palette, opacity)
if on { "On" } else { "Off" },
Message::Control(action),
foreground,
opacity
)
] ]
.align_y(Alignment::Center) .align_y(Alignment::Center)
.into() .into()