diff --git a/crates/modules/src/battery.rs b/crates/modules/src/battery.rs index 0959c60..a278bb5 100644 --- a/crates/modules/src/battery.rs +++ b/crates/modules/src/battery.rs @@ -4,8 +4,8 @@ use iced::widget::{column, container, mouse_area, row, stack, text}; use iced::{alignment, Border, Color, Element, Length, Task, Theme}; use common::{ - BarModule, BatteryAction, BatteryEvent, BatteryMsg, BatteryStatus, Endpoint, ModuleEffect, - PopupSettings, Service, Wire, + pill, BarModule, BatteryAction, BatteryEvent, BatteryMsg, BatteryStatus, Endpoint, + ModuleEffect, PopupSettings, Service, Wire, }; use serde::Deserialize; use toml::Table; @@ -15,6 +15,8 @@ const SHELL_W: f32 = 28.0; const SHELL_H: f32 = 14.0; const BORDER_W: f32 = 1.5; const PAD: f32 = 2.0; +/// Charge at or below this shows the warning color while discharging. +const LOW_PCT: i32 = 20; pub struct Battery { percent: Option, @@ -72,20 +74,22 @@ fn fmt_duration(secs: u64) -> String { } } -/// Icon ink: the theme's success green while charging, bar text otherwise. -fn ink(status: BatteryStatus, theme: &Theme) -> Color { +/// Icon ink: success green while charging, danger red when low and +/// discharging, bar text otherwise. +fn ink(status: BatteryStatus, percent: Option, theme: &Theme) -> Color { let palette = theme.palette(); match status { BatteryStatus::Charging => palette.success, + BatteryStatus::Discharging if percent.is_some_and(|p| p <= LOW_PCT) => palette.danger, _ => palette.text, } } /// Transparent shell with a rounded outline tinted by charge state. -fn shell_style(status: BatteryStatus) -> impl Fn(&Theme) -> container::Style { +fn shell_style(status: BatteryStatus, percent: Option) -> impl Fn(&Theme) -> container::Style { move |theme: &Theme| container::Style { border: Border { - color: ink(status, theme), + color: ink(status, percent, theme), width: BORDER_W, radius: 4.0.into(), }, @@ -95,7 +99,11 @@ fn shell_style(status: BatteryStatus) -> impl Fn(&Theme) -> container::Style { /// Solid fill (and nub cap) tinted by charge state. `dim` renders the /// Android-style muted fill that sits behind the in-icon number. -fn solid_style(status: BatteryStatus, dim: bool) -> impl Fn(&Theme) -> container::Style { +fn solid_style( + status: BatteryStatus, + percent: Option, + dim: bool, +) -> impl Fn(&Theme) -> container::Style { move |theme: &Theme| { let palette = theme.palette(); let background = if dim { @@ -104,7 +112,7 @@ fn solid_style(status: BatteryStatus, dim: bool) -> impl Fn(&Theme) -> container ..palette.text } } else { - ink(status, theme) + ink(status, percent, theme) }; container::Style { background: Some(background.into()), @@ -134,12 +142,12 @@ impl Battery { container(text("")) .width(Length::Fixed(self.fill_w())) .height(Length::Fill) - .style(solid_style(status, self.numbers_in_icon)), + .style(solid_style(status, self.percent, self.numbers_in_icon)), ) .width(Length::Fixed(SHELL_W)) .height(Length::Fixed(SHELL_H)) .padding(PAD) - .style(shell_style(status)); + .style(shell_style(status, self.percent)); // Optional percentage readout centered over the fill, in the frame's // light color — legible on the dimmed fill, like Android. let shell: Element<'_, Wire> = match (self.numbers_in_icon, self.percent) { @@ -167,7 +175,7 @@ impl Battery { container(text("")) .width(Length::Fixed(2.5)) .height(Length::Fixed(6.0)) - .style(solid_style(status, false)), + .style(solid_style(status, self.percent, false)), ] .spacing(2) .align_y(iced::Alignment::Center); @@ -191,15 +199,20 @@ impl Battery { BatteryAction::OpenPopup, )), ) + .style(pill) .id(iced::widget::Id::from(self.id())) .into() } /// Popup surface: charge, status, and time to full/empty. fn popup(&self) -> Element<'_, Wire> { - let charge = match self.percent { - Some(percent) => text(format!("{percent}%")).size(44), - None => text("--").size(44), + let (status, percent) = (self.status, self.percent); + let warn = move |theme: &Theme| text::Style { + color: Some(ink(status, percent, theme)), + }; + let charge = match percent { + Some(percent) => text(format!("{percent}%")).size(44).style(warn), + None => text("--").size(44).style(warn), }; let status = text(match self.status { BatteryStatus::Charging => "Charging".to_string(),