Polish bar alignment and battery indicator, dismiss launcher on outside click

This commit is contained in:
Samuel Onoja committed 2026-10-13 23:36:04 +01:00
1 parent d8bdb68b2a
commit 4d1d1af413
14 files changed
+148 -169

No files matched your search

+1 -1
View File
@@ -576,7 +576,7 @@ impl FereseShell {
} }
let panel = self.panel_for_bar(id).id.clone(); let panel = self.panel_for_bar(id).id.clone();
let item = item.id.clone(); let item = item.id.clone();
let button = button::custom(content) let button = button::custom(crate::bar_content(content, height))
.name(format!( .name(format!(
"Activities: {}, {}. Open list", "Activities: {}, {}. Open list",
activity.data.title, activity.data.title,
+6 -11
View File
@@ -452,21 +452,23 @@ impl FereseShell {
content = content.push( content = content.push(
self.render self.render
.text(date) .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) .wrapping(cosmic::iced::widget::text::Wrapping::None)
.class(theme::Text::Color(foreground)), .class(theme::Text::Color(foreground)),
); );
} }
let content = content.push( let content = content.push(
self.render self.render
.text(time) .text(time.to_ascii_uppercase())
.size(bar.text_size) .size(bar.text_size)
.line_height(cosmic::iced::widget::text::LineHeight::Absolute(20.into()))
.wrapping(cosmic::iced::widget::text::Wrapping::None) .wrapping(cosmic::iced::widget::text::Wrapping::None)
.class(theme::Text::Color(foreground)), .class(theme::Text::Color(foreground)),
); );
motion::button( motion::button(
self.render, self.render,
button::custom(container(content).center_y(bar.group_item_height)) button::custom(bar_content(content, bar.group_item_height))
.padding([0, 4]) .padding([0, 4])
.height(bar.group_item_height) .height(bar.group_item_height)
.name("Open calendar") .name("Open calendar")
@@ -751,14 +753,7 @@ pub(super) fn bar_content<'a>(
content: impl Into<Element<'a, cosmic::Action<Message>>>, content: impl Into<Element<'a, cosmic::Action<Message>>>,
height: f32, height: f32,
) -> cosmic::widget::Container<'a, cosmic::Action<Message>, cosmic::Theme, cosmic::Renderer> { ) -> cosmic::widget::Container<'a, cosmic::Action<Message>, cosmic::Theme, cosmic::Renderer> {
// A one-pixel optical offset below the geometric center. Compact bars container(content).center_y(height)
// 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)
} }
pub(super) fn overview_control(bar: BarMetrics, foreground: Color) -> Element<'static, cosmic::Action<Message>> { pub(super) fn overview_control(bar: BarMetrics, foreground: Color) -> Element<'static, cosmic::Action<Message>> {
@@ -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<Message>> {
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()
}
}
+2
View File
@@ -17,6 +17,7 @@ use std::sync::Arc;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(super) enum Message { pub(super) enum Message {
Dismiss(window::Id),
Input(window::Id, String), Input(window::Id, String),
Browse(window::Id, Category), Browse(window::Id, Category),
ActivateRow(window::Id, sources::ResultId), ActivateRow(window::Id, sources::ResultId),
@@ -368,6 +369,7 @@ impl FereseShell {
return Task::none(); return Task::none();
}; };
match message { match message {
Message::Dismiss(id) if id == launcher.id => self.close_launcher(),
Message::AppsScrolled(id, first) if id == launcher.id && launcher.apps_browse() => { 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)); launcher.apps_first_row = first.min(launcher.rows.len().div_ceil(4).saturating_sub(1));
Task::none() Task::none()
+10 -5
View File
@@ -1,6 +1,6 @@
use super::*; use super::*;
use cosmic::iced::border::Shape; 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}; use cosmic::widget::{column, text_input};
type Ui<'a> = Element<'a, cosmic::Action<super::super::Message>>; type Ui<'a> = Element<'a, cosmic::Action<super::super::Message>>;
@@ -515,8 +515,8 @@ impl FereseShell {
}, },
..Default::default() ..Default::default()
})); }));
container(motion::scaled( let panel = container(motion::scaled(
panel.into(), opaque(panel),
0.97 + 0.03 * opacity, 0.97 + 0.03 * opacity,
launcher.regions.clone(), launcher.regions.clone(),
)) ))
@@ -525,7 +525,12 @@ impl FereseShell {
.padding(cosmic::iced::Padding { .padding(cosmic::iced::Padding {
top, top,
..Default::default() ..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()
} }
} }
+1
View File
@@ -1,5 +1,6 @@
mod activities; mod activities;
mod bar; mod bar;
mod battery_indicator;
mod surfaces; mod surfaces;
use bar::*; use bar::*;
+1 -1
View File
@@ -3465,7 +3465,7 @@ mod tests {
// Exercise the nested battery button as well as its recording parent. // Exercise the nested battery button as well as its recording parent.
if name == "peek-recording-low-battery" { if name == "peek-recording-low-battery" {
for (label, expected) in [ for (label, expected) in [
("12%", status_ui::Menu::Battery), ("12", status_ui::Menu::Battery),
("Recording", status_ui::Menu::Overflow), ("Recording", status_ui::Menu::Overflow),
] { ] {
let rect = bounds.0.iter().find(|(text, _)| text == label).unwrap().1; let rect = bounds.0.iter().find(|(text, _)| text == label).unwrap().1;
+3 -24
View File
@@ -115,23 +115,10 @@ impl FereseShell {
Length::Fill Length::Fill
}; };
if priority == Peek::Battery { 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![ return row![
icons::tinted(icons::BATTERY_25, 18, warning), self.battery_indicator(self.config.theme, opacity, true),
self.render self.render
.text(label) .text("Low battery")
.size(13) .size(13)
.class(theme::Text::Color(foreground)) .class(theme::Text::Color(foreground))
.width(width) .width(width)
@@ -191,15 +178,7 @@ impl FereseShell {
}; };
strip = strip.push( strip = strip.push(
button::custom( button::custom(
row![ crate::bar_content(self.battery_indicator(self.config.theme, opacity, true), 28.)
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),
) )
.name(format!("Low battery, {percent} percent. Open battery controls")) .name(format!("Low battery, {percent} percent. Open battery controls"))
.padding(0) .padding(0)
+3 -55
View File
@@ -1,9 +1,7 @@
//! A fixed-width battery silhouette; the percentage is text, not regenerated SVG. //! A fixed-width battery silhouette; the percentage is text, not regenerated SVG.
use super::*; 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 { impl FereseShell {
pub(super) fn hub_battery( pub(super) fn hub_battery(
@@ -14,65 +12,15 @@ impl FereseShell {
) -> Option<Element<'_, cosmic::Action<Message>>> { ) -> Option<Element<'_, cosmic::Action<Message>>> {
let battery = self.status.battery.as_ref()?; let battery = self.status.battery.as_ref()?;
let percent = battery.percent.min(100); let percent = battery.percent.min(100);
let palette = self.config.theme.palette();
let foreground = color_with_opacity(self.config.theme.text_primary, opacity); 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( Some(motion::button(
self.render, 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!( .name(format!(
"Battery, {percent} percent, {}. Open battery controls", "Battery, {percent} percent, {}. Open battery controls",
battery.status battery.status
)) ))
.padding([7, 3]) .padding([0, 3])
.width(WIDTH as f32) .width(WIDTH as f32)
.height(28) .height(28)
.on_press(cosmic::Action::App(Message::HubOpen(id, status_ui::Menu::Battery))), .on_press(cosmic::Action::App(Message::HubOpen(id, status_ui::Menu::Battery))),
+6 -19
View File
@@ -57,14 +57,12 @@ impl FereseShell {
} else { } else {
theme.text_muted theme.text_muted
}); });
let mut content = row![accented_icon( let indicator = if kind == Menu::Battery {
source, self.battery_indicator(theme, 1.0, battery_percentage)
metrics.icon_size, } else {
foreground, accented_icon(source, metrics.icon_size, foreground, color(theme.accent)).into()
color(theme.accent) };
)] let mut content = row![indicator].spacing(4).align_y(Alignment::Center);
.spacing(4)
.align_y(Alignment::Center);
if kind == Menu::Recording if kind == Menu::Recording
&& let Some(elapsed) = self.recorder.elapsed() && 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 if kind == Menu::Notifications
&& self && self
.status .status
+6 -39
View File
@@ -1,54 +1,21 @@
use cosmic::iced::{Alignment, Color, Length}; use cosmic::iced::{Alignment, Length};
use cosmic::widget::{button, column, container, row}; use cosmic::widget::{button, column, container, row};
use cosmic::{Element, theme}; use cosmic::{Element, theme};
use super::bar::status_icon; use super::controls::{control_card, slider_row};
use super::controls::{control_card, level_meter, slider_row}; use super::{MenuRows, MenuStyle};
use super::{Menu, MenuRows, MenuStyle};
use crate::status::Action; 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> { pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
let MenuStyle { let MenuStyle {
theme, theme,
primary, primary: _,
muted, muted,
opacity: p, opacity: p,
} = style; } = style;
if let Some(b) = &shell.status.battery { if let Some(b) = &shell.status.battery {
let battery_color = if b.percent < shell.config.status.low_battery_threshold && b.status != "Charging" { rows = rows.push(container(shell.battery_indicator(theme, p, true)).padding([2, 0]));
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( rows = rows.push(
shell shell
.render .render
+4 -2
View File
@@ -81,7 +81,8 @@ pub(super) fn bar<'a>(
return shell.render.text("").into(); return shell.render.text("").into();
}; };
let foreground = color(if selected { palette.accent } else { palette.text_primary }); 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 side = height.min(24.0);
let cover: Element<'_, cosmic::Action<Message>> = let cover: Element<'_, cosmic::Action<Message>> =
match shell.media.artwork.as_ref().and_then(|art| art.handle.as_ref()) { match shell.media.artwork.as_ref().and_then(|art| art.handle.as_ref()) {
@@ -178,8 +179,9 @@ pub(super) fn bar<'a>(
.spacing(2) .spacing(2)
.align_y(Alignment::Center), .align_y(Alignment::Center),
) )
.padding(2) .padding([vertical_padding, 2.0])
.height(metrics.group_item_height) .height(metrics.group_item_height)
.align_y(cosmic::iced::alignment::Vertical::Center)
.class(theme::Container::custom(move |_| bar_style)); .class(theme::Container::custom(move |_| bar_style));
let mut area = mouse_area(controls); let mut area = mouse_area(controls);
+7 -2
View File
@@ -365,7 +365,9 @@ impl FereseShell {
let kind = menu.kind; let kind = menu.kind;
let primary = color_with_opacity(theme.text_primary, p); let primary = color_with_opacity(theme.text_primary, p);
let muted = color_with_opacity(theme.text_muted, 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( accented_icon(
status_icon(kind, &self.status).0, status_icon(kind, &self.status).0,
22, 22,
@@ -373,7 +375,10 @@ impl FereseShell {
color_with_opacity(theme.accent, p), color_with_opacity(theme.accent, p),
) )
.width(Length::Fixed(22.)) .width(Length::Fixed(22.))
.into(), .into()
};
let badge = ferese_theme::menus::badge(
heading_icon,
color(theme.accent), color(theme.accent),
p, p,
self.render.radius(20.), self.render.radius(20.),
+18 -10
View File
@@ -111,16 +111,24 @@ pub(super) fn view<'a>(
)); ));
} }
if let Some(b) = &shell.status.battery { if shell.status.battery.is_some() {
connections = connections.push(connection_control( connections = connections.push(
shell.render, container(
"Battery", column![
&format!("{}%", b.percent), container(shell.battery_indicator(theme, p, true))
status_icon(Menu::Battery, &shell.status).0, .center_x(Length::Fill)
b.status == "Charging", .center_y(28),
None, shell.render.text("Battery").size(11)
style, .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() { if shell.status.network.is_some() || shell.status.bluetooth.is_some() || shell.status.battery.is_some() {