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