//! 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);
}
}