This commit is contained in:
2026-09-16 12:07:30 +01:00
parent 6a62d14755
commit fe07022015
3 changed files with 110 additions and 14 deletions
@@ -0,0 +1,8 @@
//! Battery module messages.
/// A module-local message for the battery tile (bar/popup interactions).
#[derive(Clone)]
pub enum BatteryAction {
/// Open the battery info popup.
OpenPopup,
}
@@ -1,7 +1,9 @@
//! Messages owned by bar modules.
mod battery;
mod clock;
mod weather;
pub use battery::*;
pub use clock::*;
pub use weather::*;
+100 -14
View File
@@ -1,9 +1,12 @@
//! Battery module: Android-style battery icon fed by the battery service.
use iced::widget::{container, row, stack, text};
use iced::widget::{column, container, mouse_area, row, stack, text};
use iced::{alignment, Border, Color, Element, Length, Task, Theme};
use common::{BarModule, BatteryEvent, BatteryMsg, BatteryStatus, ModuleEffect, Service, Wire};
use common::{
BarModule, BatteryAction, BatteryEvent, BatteryMsg, BatteryStatus, Endpoint, ModuleEffect,
PopupSettings, Service, Wire,
};
use serde::Deserialize;
use toml::Table;
@@ -16,9 +19,14 @@ const PAD: f32 = 2.0;
pub struct Battery {
percent: Option<i32>,
status: BatteryStatus,
eta_secs: Option<u64>,
numbers_in_icon: bool,
}
/// Battery info popup size (logical px).
const POPUP_W: u32 = 280;
const POPUP_H: u32 = 160;
/// `module.battery` table; everything optional.
#[derive(Deserialize, Default)]
struct BatteryConfig {
@@ -32,6 +40,7 @@ impl Battery {
Self {
percent: None,
status: BatteryStatus::default(),
eta_secs: None,
numbers_in_icon: config
.and_then(|table| table.get("battery").cloned())
.and_then(|battery| battery.try_into().ok())
@@ -51,6 +60,18 @@ impl Battery {
}
}
/// Human form of a duration: `1 h 5 min`, `42 min`, or `30 s`.
fn fmt_duration(secs: u64) -> String {
let (hours, mins) = (secs / 3600, secs % 3600 / 60);
if hours > 0 {
format!("{hours} h {mins} min")
} else if mins > 0 {
format!("{mins} min")
} else {
format!("{secs} s")
}
}
/// Icon ink: the theme's success green while charging, bar text otherwise.
fn ink(status: BatteryStatus, theme: &Theme) -> Color {
let palette = theme.palette();
@@ -104,12 +125,10 @@ fn bold() -> iced::Font {
}
}
impl BarModule for Battery {
fn id(&self) -> &'static str {
"battery"
}
fn view(&self, _window_id: Option<iced::window::Id>) -> Element<'_, Wire> {
impl Battery {
/// Bar tile: icon (plus `%` label unless numbers live in the icon),
/// left-click opens the info popup.
fn bar(&self) -> Element<'_, Wire> {
let status = self.status;
let shell = container(
container(text(""))
@@ -152,7 +171,7 @@ impl BarModule for Battery {
]
.spacing(2)
.align_y(iced::Alignment::Center);
if self.numbers_in_icon {
let bar: Element<'_, Wire> = if self.numbers_in_icon {
icon.into()
} else {
let label = match self.percent {
@@ -163,19 +182,86 @@ impl BarModule for Battery {
.spacing(6)
.align_y(iced::Alignment::Center)
.into()
};
// Container carries the module id so the popup can anchor to it.
container(
mouse_area(bar).on_press(Wire::module(
Endpoint::module(self.id()),
self.id(),
BatteryAction::OpenPopup,
)),
)
.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 = text(match self.status {
BatteryStatus::Charging => "Charging".to_string(),
BatteryStatus::Discharging => "Discharging".to_string(),
BatteryStatus::Empty => "Empty".to_string(),
BatteryStatus::Full => "Fully charged".to_string(),
BatteryStatus::Unknown => "Unknown".to_string(),
});
let mut lines = column![charge, status].spacing(4);
if let Some(secs) = self.eta_secs {
let eta = match self.status {
BatteryStatus::Charging => format!("Full in {}", fmt_duration(secs)),
BatteryStatus::Discharging => format!("{} remaining", fmt_duration(secs)),
_ => fmt_duration(secs),
};
lines = lines.push(text(eta));
}
container(lines)
.padding(16)
.width(Length::Fill)
.into()
}
}
impl BarModule for Battery {
fn id(&self) -> &'static str {
"battery"
}
fn view(&self, window_id: Option<iced::window::Id>) -> Element<'_, Wire> {
match window_id {
Some(_) => self.popup(),
None => self.bar(),
}
}
fn update(&mut self, msg: Wire) -> Task<ModuleEffect> {
match msg.downcast::<BatteryEvent>() {
Some(BatteryEvent::PercentageChanged(percent)) => self.percent = Some(*percent),
Some(BatteryEvent::StatusChanged(status)) => self.status = *status,
_ => {}
if let Some(event) = msg.downcast::<BatteryEvent>() {
match event {
BatteryEvent::PercentageChanged(percent) => self.percent = Some(*percent),
BatteryEvent::StatusChanged(status) => self.status = *status,
BatteryEvent::EtaChanged(eta) => self.eta_secs = *eta,
}
return Task::none();
}
match msg.downcast::<BatteryAction>() {
Some(BatteryAction::OpenPopup) => {
Task::done(ModuleEffect::RequestPopup(PopupSettings {
module_id: self.id().into(),
element_id: self.id().into(),
gap: 8,
}))
}
None => Task::none(),
}
Task::none()
}
fn services(&self) -> Vec<Service> {
vec![BatteryMsg::Events.key()]
}
fn popup_size(&self) -> Option<(u32, u32)> {
Some((POPUP_W, POPUP_H))
}
}