diff --git a/docs/configuration.md b/docs/configuration.md index 77fff77..d9b714a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -353,6 +353,7 @@ returns. Ferese does not change the system's suspend policy. | `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 | | `low_battery_threshold` | integer 0–100 | `20` | Warning-color threshold | | `settings_command` | argument array | `["ferese-settings"]` | Settings launcher; `[]` hides the action | diff --git a/shell/ferese-settings/src/schema.rs b/shell/ferese-settings/src/schema.rs index b9f02b2..1663cad 100644 --- a/shell/ferese-settings/src/schema.rs +++ b/shell/ferese-settings/src/schema.rs @@ -705,6 +705,12 @@ pub fn fields(page: Page) -> 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( "theme.geometry.top_bar_height", "Height", diff --git a/shell/ferese-shell/src/config.rs b/shell/ferese-shell/src/config.rs index 2b3a102..ea4d766 100644 --- a/shell/ferese-shell/src/config.rs +++ b/shell/ferese-shell/src/config.rs @@ -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>, } @@ -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()]), } diff --git a/shell/ferese-shell/src/main.rs b/shell/ferese-shell/src/main.rs index 3165a25..d5c7fe3 100644 --- a/shell/ferese-shell/src/main.rs +++ b/shell/ferese-shell/src/main.rs @@ -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(¬e_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 { diff --git a/shell/ferese-shell/src/status_ui.rs b/shell/ferese-shell/src/status_ui.rs index 060c114..c7829f2 100644 --- a/shell/ferese-shell/src/status_ui.rs +++ b/shell/ferese-shell/src/status_ui.rs @@ -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, - theme: ShellTheme, + palette: ShellTheme, opacity: f32, ) -> Element<'a, cosmic::Action> { - 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> { + 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> { 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()