From 4d1d1af413adc66895f54f1336d9719c8a762831 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Tue, 13 Oct 2026 23:36:04 +0100 Subject: [PATCH] Polish bar alignment and battery indicator, dismiss launcher on outside click --- shell/ferese-shell/src/activities.rs | 2 +- shell/ferese-shell/src/bar.rs | 17 ++--- shell/ferese-shell/src/battery_indicator.rs | 80 ++++++++++++++++++++ shell/ferese-shell/src/launcher/mod.rs | 2 + shell/ferese-shell/src/launcher/view.rs | 15 ++-- shell/ferese-shell/src/main.rs | 1 + shell/ferese-shell/src/panel_hub.rs | 2 +- shell/ferese-shell/src/panel_hub/activity.rs | 27 +------ shell/ferese-shell/src/panel_hub/battery.rs | 58 +------------- shell/ferese-shell/src/status_ui/bar.rs | 25 ++---- shell/ferese-shell/src/status_ui/battery.rs | 45 ++--------- shell/ferese-shell/src/status_ui/media.rs | 6 +- shell/ferese-shell/src/status_ui/mod.rs | 9 ++- shell/ferese-shell/src/status_ui/system.rs | 28 ++++--- 14 files changed, 148 insertions(+), 169 deletions(-) create mode 100644 shell/ferese-shell/src/battery_indicator.rs diff --git a/shell/ferese-shell/src/activities.rs b/shell/ferese-shell/src/activities.rs index 513af87..5a55cf1 100644 --- a/shell/ferese-shell/src/activities.rs +++ b/shell/ferese-shell/src/activities.rs @@ -576,7 +576,7 @@ impl FereseShell { } let panel = self.panel_for_bar(id).id.clone(); let item = item.id.clone(); - let button = button::custom(content) + let button = button::custom(crate::bar_content(content, height)) .name(format!( "Activities: {}, {}. Open list", activity.data.title, diff --git a/shell/ferese-shell/src/bar.rs b/shell/ferese-shell/src/bar.rs index 3659c8d..46730cc 100644 --- a/shell/ferese-shell/src/bar.rs +++ b/shell/ferese-shell/src/bar.rs @@ -452,21 +452,23 @@ impl FereseShell { content = content.push( self.render .text(date) - .size(12) + .size(bar.text_size) + .line_height(cosmic::iced::widget::text::LineHeight::Absolute(20.into())) .wrapping(cosmic::iced::widget::text::Wrapping::None) .class(theme::Text::Color(foreground)), ); } let content = content.push( self.render - .text(time) + .text(time.to_ascii_uppercase()) .size(bar.text_size) + .line_height(cosmic::iced::widget::text::LineHeight::Absolute(20.into())) .wrapping(cosmic::iced::widget::text::Wrapping::None) .class(theme::Text::Color(foreground)), ); motion::button( self.render, - button::custom(container(content).center_y(bar.group_item_height)) + button::custom(bar_content(content, bar.group_item_height)) .padding([0, 4]) .height(bar.group_item_height) .name("Open calendar") @@ -751,14 +753,7 @@ pub(super) fn bar_content<'a>( content: impl Into>>, height: f32, ) -> cosmic::widget::Container<'a, cosmic::Action, cosmic::Theme, cosmic::Renderer> { - // A one-pixel optical offset below the geometric center. Compact bars - // reduce the offset so the 20 px icons keep their full size. - container(content) - .padding(cosmic::iced::Padding { - top: (height - 20.0).clamp(0.0, 2.0), - ..Default::default() - }) - .center_y(height) + container(content).center_y(height) } pub(super) fn overview_control(bar: BarMetrics, foreground: Color) -> Element<'static, cosmic::Action> { diff --git a/shell/ferese-shell/src/battery_indicator.rs b/shell/ferese-shell/src/battery_indicator.rs new file mode 100644 index 0000000..79f7e2b --- /dev/null +++ b/shell/ferese-shell/src/battery_indicator.rs @@ -0,0 +1,80 @@ +use crate::{FereseShell, Message, ShellTheme, color_with_opacity}; +use cosmic::iced::{Alignment, Background, Border}; +use cosmic::widget::{Space, container, row}; +use cosmic::{Element, theme}; + +const BODY_WIDTH: f32 = 28.; + +impl FereseShell { + pub(crate) fn battery_indicator( + &self, + theme: ShellTheme, + opacity: f32, + show_percentage: bool, + ) -> Element<'static, cosmic::Action> { + let battery = self.status.battery.as_ref(); + let percent = battery.map_or(0, |battery| battery.percent.min(100)); + let palette = theme.palette(); + let foreground = color_with_opacity(theme.text_primary, opacity); + let outline = if battery.is_some_and(|battery| { + battery.percent < self.config.status.low_battery_threshold + && matches!(battery.status.as_str(), "Discharging" | "Pending discharge" | "Empty") + }) { + palette.error.scale_alpha(opacity) + } else if battery.is_some_and(|battery| matches!(battery.status.as_str(), "Charging" | "Pending charge")) { + palette.accent.scale_alpha(opacity) + } else { + foreground + }; + let fill = container(Space::new().width((BODY_WIDTH - 4.) * f32::from(percent) / 100.).height(10)).class( + theme::Container::custom(move |_| container::Style { + background: Some(Background::Color(outline.scale_alpha(0.16))), + border: Border { + shape: cosmic::iced::border::Shape::Continuous, + radius: 4.0.into(), + ..Default::default() + }, + ..Default::default() + }), + ); + let number = self + .render + .text(if show_percentage { + battery.map_or_else(|| "–".into(), |_| percent.to_string()) + } else { + String::new() + }) + .size(9.5) + .line_height(cosmic::iced::widget::text::LineHeight::Relative(1.)) + .class(theme::Text::Color(foreground)); + let body = container(cosmic::iced::widget::stack![ + container(fill).padding(2).width(BODY_WIDTH).height(14), + container(number).center_x(BODY_WIDTH).center_y(14), + ]) + .width(BODY_WIDTH) + .height(14) + .class(theme::Container::custom(move |_| container::Style { + border: Border { + color: outline, + width: 1.2, + shape: cosmic::iced::border::Shape::Continuous, + radius: 5.5.into(), + ..Default::default() + }, + ..Default::default() + })); + let terminal = + container(Space::new().width(1.5).height(4)).class(theme::Container::custom(move |_| container::Style { + background: Some(Background::Color(outline)), + border: Border { + radius: 1.0.into(), + ..Default::default() + }, + ..Default::default() + })); + row![body, terminal] + .spacing(1) + .align_y(Alignment::Center) + .into() + } +} diff --git a/shell/ferese-shell/src/launcher/mod.rs b/shell/ferese-shell/src/launcher/mod.rs index 7a0dd8d..2a5c814 100644 --- a/shell/ferese-shell/src/launcher/mod.rs +++ b/shell/ferese-shell/src/launcher/mod.rs @@ -17,6 +17,7 @@ use std::sync::Arc; #[derive(Clone, Debug)] pub(super) enum Message { + Dismiss(window::Id), Input(window::Id, String), Browse(window::Id, Category), ActivateRow(window::Id, sources::ResultId), @@ -368,6 +369,7 @@ impl FereseShell { return Task::none(); }; match message { + Message::Dismiss(id) if id == launcher.id => self.close_launcher(), Message::AppsScrolled(id, first) if id == launcher.id && launcher.apps_browse() => { launcher.apps_first_row = first.min(launcher.rows.len().div_ceil(4).saturating_sub(1)); Task::none() diff --git a/shell/ferese-shell/src/launcher/view.rs b/shell/ferese-shell/src/launcher/view.rs index d3eaa5d..66cf9c6 100644 --- a/shell/ferese-shell/src/launcher/view.rs +++ b/shell/ferese-shell/src/launcher/view.rs @@ -1,6 +1,6 @@ use super::*; use cosmic::iced::border::Shape; -use cosmic::iced::widget::{Space, rich_text, span, stack}; +use cosmic::iced::widget::{Space, mouse_area, opaque, rich_text, span, stack}; use cosmic::widget::{column, text_input}; type Ui<'a> = Element<'a, cosmic::Action>; @@ -515,8 +515,8 @@ impl FereseShell { }, ..Default::default() })); - container(motion::scaled( - panel.into(), + let panel = container(motion::scaled( + opaque(panel), 0.97 + 0.03 * opacity, launcher.regions.clone(), )) @@ -525,7 +525,12 @@ impl FereseShell { .padding(cosmic::iced::Padding { top, ..Default::default() - }) - .into() + }); + let dismiss = message(Message::Dismiss(id)); + let backdrop = mouse_area(Space::new().width(Length::Fill).height(Length::Fill)) + .on_press(dismiss.clone()) + .on_right_press(dismiss.clone()) + .on_middle_press(dismiss); + stack![backdrop, panel].into() } } diff --git a/shell/ferese-shell/src/main.rs b/shell/ferese-shell/src/main.rs index bed0ea6..a4815a4 100644 --- a/shell/ferese-shell/src/main.rs +++ b/shell/ferese-shell/src/main.rs @@ -1,5 +1,6 @@ mod activities; mod bar; +mod battery_indicator; mod surfaces; use bar::*; diff --git a/shell/ferese-shell/src/panel_hub.rs b/shell/ferese-shell/src/panel_hub.rs index dfed0fc..b433e00 100644 --- a/shell/ferese-shell/src/panel_hub.rs +++ b/shell/ferese-shell/src/panel_hub.rs @@ -3465,7 +3465,7 @@ mod tests { // Exercise the nested battery button as well as its recording parent. if name == "peek-recording-low-battery" { for (label, expected) in [ - ("12%", status_ui::Menu::Battery), + ("12", status_ui::Menu::Battery), ("Recording", status_ui::Menu::Overflow), ] { let rect = bounds.0.iter().find(|(text, _)| text == label).unwrap().1; diff --git a/shell/ferese-shell/src/panel_hub/activity.rs b/shell/ferese-shell/src/panel_hub/activity.rs index 2a53ca5..26dc61e 100644 --- a/shell/ferese-shell/src/panel_hub/activity.rs +++ b/shell/ferese-shell/src/panel_hub/activity.rs @@ -115,23 +115,10 @@ impl FereseShell { Length::Fill }; if priority == Peek::Battery { - let percent = self - .hub_low_battery() - .or_else(|| self.status.battery.as_ref().map(|b| b.percent)); - let label = percent.map_or_else( - || "Battery".into(), - |percent| { - if expanded { - format!("Low battery · {percent}%") - } else { - format!("{percent}% battery") - } - }, - ); return row![ - icons::tinted(icons::BATTERY_25, 18, warning), + self.battery_indicator(self.config.theme, opacity, true), self.render - .text(label) + .text("Low battery") .size(13) .class(theme::Text::Color(foreground)) .width(width) @@ -191,15 +178,7 @@ impl FereseShell { }; strip = strip.push( button::custom( - row![ - icons::tinted(icons::BATTERY_25, 16, warning), - self.render - .text(format!("{percent}%")) - .size(12) - .class(theme::Text::Color(warning)) - ] - .spacing(4) - .align_y(Alignment::Center), + crate::bar_content(self.battery_indicator(self.config.theme, opacity, true), 28.) ) .name(format!("Low battery, {percent} percent. Open battery controls")) .padding(0) diff --git a/shell/ferese-shell/src/panel_hub/battery.rs b/shell/ferese-shell/src/panel_hub/battery.rs index f334878..176735a 100644 --- a/shell/ferese-shell/src/panel_hub/battery.rs +++ b/shell/ferese-shell/src/panel_hub/battery.rs @@ -1,9 +1,7 @@ //! A fixed-width battery silhouette; the percentage is text, not regenerated SVG. use super::*; -use cosmic::iced::{Background, Border}; -use cosmic::widget::Space; -pub(super) const WIDTH: f64 = 34.; +pub(super) const WIDTH: f64 = 38.; impl FereseShell { pub(super) fn hub_battery( @@ -14,65 +12,15 @@ impl FereseShell { ) -> Option>> { let battery = self.status.battery.as_ref()?; let percent = battery.percent.min(100); - let palette = self.config.theme.palette(); let foreground = color_with_opacity(self.config.theme.text_primary, opacity); - let outline = if self.hub_low_battery().is_some() { - palette.error.scale_alpha(opacity) - } else if matches!(battery.status.as_str(), "Charging" | "Pending charge") { - palette.accent.scale_alpha(opacity) - } else { - foreground - }; - let fill = container(Space::new().width(20. * f32::from(percent) / 100.).height(10)).class( - theme::Container::custom(move |_| container::Style { - background: Some(Background::Color(outline.scale_alpha(0.16))), - border: Border { - shape: cosmic::iced::border::Shape::Continuous, - radius: 4.0.into(), - ..Default::default() - }, - ..Default::default() - }), - ); - let number = self - .render - .text(percent.to_string()) - .size(9.5) - .line_height(cosmic::iced::widget::text::LineHeight::Relative(1.)) - .class(theme::Text::Color(foreground)); - let body = container(cosmic::iced::widget::stack![ - container(fill).padding(2).width(24).height(14), - container(number).center_x(24).center_y(14), - ]) - .width(24) - .height(14) - .class(theme::Container::custom(move |_| container::Style { - border: Border { - color: outline, - width: 1.2, - shape: cosmic::iced::border::Shape::Continuous, - radius: 5.5.into(), - ..Default::default() - }, - ..Default::default() - })); - let terminal = - container(Space::new().width(1.5).height(4)).class(theme::Container::custom(move |_| container::Style { - background: Some(Background::Color(outline)), - border: Border { - radius: 1.0.into(), - ..Default::default() - }, - ..Default::default() - })); Some(motion::button( self.render, - button::custom(row![body, terminal].spacing(1).align_y(Alignment::Center)) + button::custom(crate::bar_content(self.battery_indicator(self.config.theme, opacity, true), 28.)) .name(format!( "Battery, {percent} percent, {}. Open battery controls", battery.status )) - .padding([7, 3]) + .padding([0, 3]) .width(WIDTH as f32) .height(28) .on_press(cosmic::Action::App(Message::HubOpen(id, status_ui::Menu::Battery))), diff --git a/shell/ferese-shell/src/status_ui/bar.rs b/shell/ferese-shell/src/status_ui/bar.rs index 8141bc0..6278235 100644 --- a/shell/ferese-shell/src/status_ui/bar.rs +++ b/shell/ferese-shell/src/status_ui/bar.rs @@ -57,14 +57,12 @@ impl FereseShell { } else { theme.text_muted }); - let mut content = row![accented_icon( - source, - metrics.icon_size, - foreground, - color(theme.accent) - )] - .spacing(4) - .align_y(Alignment::Center); + let indicator = if kind == Menu::Battery { + self.battery_indicator(theme, 1.0, battery_percentage) + } else { + accented_icon(source, metrics.icon_size, foreground, color(theme.accent)).into() + }; + let mut content = row![indicator].spacing(4).align_y(Alignment::Center); if kind == Menu::Recording && let Some(elapsed) = self.recorder.elapsed() @@ -77,17 +75,6 @@ impl FereseShell { ); } - if kind == Menu::Battery - && battery_percentage - && let Some(battery) = &self.status.battery - { - content = content.push( - self.render - .text(format!("{}%", battery.percent)) - .size(metrics.text_size) - .class(theme::Text::Color(foreground)), - ); - } if kind == Menu::Notifications && self .status diff --git a/shell/ferese-shell/src/status_ui/battery.rs b/shell/ferese-shell/src/status_ui/battery.rs index 4fb4f26..86c3fc9 100644 --- a/shell/ferese-shell/src/status_ui/battery.rs +++ b/shell/ferese-shell/src/status_ui/battery.rs @@ -1,54 +1,21 @@ -use cosmic::iced::{Alignment, Color, Length}; +use cosmic::iced::{Alignment, Length}; use cosmic::widget::{button, column, container, row}; use cosmic::{Element, theme}; -use super::bar::status_icon; -use super::controls::{control_card, level_meter, slider_row}; -use super::{Menu, MenuRows, MenuStyle}; +use super::controls::{control_card, slider_row}; +use super::{MenuRows, MenuStyle}; use crate::status::Action; -use crate::{FereseShell, Message, ShellTheme, accented_icon, bar_icon, color_with_opacity, motion}; +use crate::{FereseShell, Message, ShellTheme, bar_icon, color_with_opacity, motion}; pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> { let MenuStyle { theme, - primary, + primary: _, muted, opacity: p, } = style; if let Some(b) = &shell.status.battery { - let battery_color = if b.percent < shell.config.status.low_battery_threshold && b.status != "Charging" { - Color { - a: p, - ..Color::from_rgb8(230, 172, 90) - } - } else { - primary - }; - rows = rows.push( - container( - row![ - shell - .render - .text(format!("{}%", b.percent)) - .size(24) - .width(Length::Fill), - accented_icon( - status_icon(Menu::Battery, &shell.status).0, - 24, - battery_color, - color_with_opacity(theme.accent, p), - ), - ] - .align_y(Alignment::Center), - ) - .padding([2, 0]) - .class(theme::Container::custom(move |_| container::Style { - text_color: Some(battery_color), - icon_color: Some(battery_color), - ..Default::default() - })), - ); - rows = rows.push(level_meter(shell.render, b.percent, battery_color, p)); + rows = rows.push(container(shell.battery_indicator(theme, p, true)).padding([2, 0])); rows = rows.push( shell .render diff --git a/shell/ferese-shell/src/status_ui/media.rs b/shell/ferese-shell/src/status_ui/media.rs index c4dd8c9..efebd39 100644 --- a/shell/ferese-shell/src/status_ui/media.rs +++ b/shell/ferese-shell/src/status_ui/media.rs @@ -81,7 +81,8 @@ pub(super) fn bar<'a>( return shell.render.text("").into(); }; let foreground = color(if selected { palette.accent } else { palette.text_primary }); - let height = (metrics.group_item_height - 4.0).max(0.0); + let vertical_padding = ((metrics.group_item_height - f32::from(metrics.icon_size)) * 0.5).clamp(0.0, 2.0); + let height = metrics.group_item_height - 2.0 * vertical_padding; let side = height.min(24.0); let cover: Element<'_, cosmic::Action> = match shell.media.artwork.as_ref().and_then(|art| art.handle.as_ref()) { @@ -178,8 +179,9 @@ pub(super) fn bar<'a>( .spacing(2) .align_y(Alignment::Center), ) - .padding(2) + .padding([vertical_padding, 2.0]) .height(metrics.group_item_height) + .align_y(cosmic::iced::alignment::Vertical::Center) .class(theme::Container::custom(move |_| bar_style)); let mut area = mouse_area(controls); diff --git a/shell/ferese-shell/src/status_ui/mod.rs b/shell/ferese-shell/src/status_ui/mod.rs index d1acb0f..157ce65 100644 --- a/shell/ferese-shell/src/status_ui/mod.rs +++ b/shell/ferese-shell/src/status_ui/mod.rs @@ -365,7 +365,9 @@ impl FereseShell { let kind = menu.kind; let primary = color_with_opacity(theme.text_primary, p); let muted = color_with_opacity(theme.text_muted, p); - let badge = ferese_theme::menus::badge( + let heading_icon = if kind == Menu::Battery { + self.battery_indicator(theme, p, false) + } else { accented_icon( status_icon(kind, &self.status).0, 22, @@ -373,7 +375,10 @@ impl FereseShell { color_with_opacity(theme.accent, p), ) .width(Length::Fixed(22.)) - .into(), + .into() + }; + let badge = ferese_theme::menus::badge( + heading_icon, color(theme.accent), p, self.render.radius(20.), diff --git a/shell/ferese-shell/src/status_ui/system.rs b/shell/ferese-shell/src/status_ui/system.rs index 235f3ef..71ce74d 100644 --- a/shell/ferese-shell/src/status_ui/system.rs +++ b/shell/ferese-shell/src/status_ui/system.rs @@ -111,16 +111,24 @@ pub(super) fn view<'a>( )); } - if let Some(b) = &shell.status.battery { - connections = connections.push(connection_control( - shell.render, - "Battery", - &format!("{}%", b.percent), - status_icon(Menu::Battery, &shell.status).0, - b.status == "Charging", - None, - style, - )); + if shell.status.battery.is_some() { + connections = connections.push( + container( + column![ + container(shell.battery_indicator(theme, p, true)) + .center_x(Length::Fill) + .center_y(28), + shell.render.text("Battery").size(11) + .width(Length::Fill).height(16) + .align_x(alignment::Horizontal::Center) + .class(theme::Text::Color(muted)), + ] + .spacing(2) + .align_x(Alignment::Center), + ) + .width(Length::FillPortion(1)) + .padding(4), + ); } if shell.status.network.is_some() || shell.status.bluetooth.is_some() || shell.status.battery.is_some() {