//! Clock module: subscribes to the clock service, renders the time in the //! bar. Left-click toggles the seconds suffix — and pokes the running clock //! service to switch which kind it publishes; right-click opens a popup. use iced::widget::{container, mouse_area, text}; use iced::{Element, Task}; use common::{BarModule, ClockKind, ClockMsg, ClockPayload, Endpoint, ModuleEffect, Service, Wire}; use serde::Deserialize; use toml::Table; /// Big-text popup size (logical px). const POPUP_W: u32 = 360; const POPUP_H: u32 = 160; /// Errors digesting the `module.clock` config table. #[derive(Debug, thiserror::Error)] pub enum ClockError { /// Config value was present but not a bool. #[error("module.clock.{0} must be a bool")] NotBool(&'static str), } pub struct Clock { value: String, show_seconds: bool, } #[derive(Deserialize, Default)] struct ClockConfig { #[serde(default)] show_seconds: bool, } impl Clock { pub fn new(config: Option) -> Self { match config_clock(config) { Some(x) => Self { value: "--:--:--".to_string(), show_seconds: x.show_seconds, }, None => Self::default(), } } /// Display string: HH:MM:SS, or HH:MM when seconds are hidden. fn shown(&self) -> String { if self.show_seconds { self.value.clone() } else { self.value .split(':') // [hh, mm, ss] .take(2) .collect::>() .join(":") } } } impl Default for Clock { fn default() -> Self { Self { value: "--:--:--".to_string(), show_seconds: false, } } } fn config_clock(table: Option
) -> Option { let clock = table?.get("clock")?.clone(); clock.try_into().ok() } impl BarModule for Clock { fn id(&self) -> &'static str { "clock" } fn view(&self, window_id: Option) -> Element<'_, Wire> { let me = Endpoint::module(self.id()); match window_id { // Popup surface: current time in large text. Some(_) => container(text(&self.value).size(56)).padding(20).into(), // Bar surface: clickable time. Container carries the module id // so the popup can anchor to these bounds. None => container( mouse_area(text(self.shown()).size(16)) .on_press(Wire::module(me, self.id(), ClockMsg::ToggleSeconds)) .on_right_press(Wire::module(me, self.id(), ClockMsg::OpenPopup)), ) .id(iced::widget::Id::from(self.id())) .into(), } } fn update(&mut self, msg: Wire) -> Task { // The service publishes a raw `ClockPayload`; UI sends `ClockMsg`. if let Some(payload) = msg.downcast::() { self.value = payload.value.clone(); return Task::none(); } match msg.downcast::() { Some(ClockMsg::ToggleSeconds) => { self.show_seconds = !self.show_seconds; tracing::debug!(show_seconds = self.show_seconds, "clock toggle"); let want = if self.show_seconds { ClockKind::Seconds } else { ClockKind::Mins }; // Poke the running clock service: tell it which kind to publish. Task::done(ModuleEffect::Send(Wire::service( Endpoint::module(self.id()), ClockKind::Seconds.key().0, want, ))) } Some(ClockMsg::OpenPopup) => Task::done(ModuleEffect::RequestPopup( self.id().to_string(), self.id().to_string(), )), None => Task::none(), } } fn services(&self) -> Vec { vec![ClockKind::Seconds.key()] } fn popup_size(&self) -> Option<(u32, u32)> { Some((POPUP_W, POPUP_H)) } } #[cfg(test)] mod tests { use super::*; /// Regression: `new(None)` used to call `default()`, which called /// `new(None)` — infinite recursion (hard stack overflow) whenever the /// clock config was absent or failed to parse. #[test] fn no_config_terminates() { assert!(!Clock::new(None).show_seconds); assert!(!Clock::default().show_seconds); } }