use super::{Widget, WidgetBase, WidgetId, LayoutContext, PaintContext, EventContext};
use crate::css::{ClassList, WidgetState};
use crate::event::{Event, EventResult, MouseEventKind, MouseButton};
use crate::geometry::{Point, Rect, Size};
use crate::layout::{Constraints, LayoutResult};
use crate::render::Painter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ClockFormat {
TwelveHour,
#[default]
TwentyFourHour,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DateFormat {
Short,
#[default]
Long,
DayOfWeek,
Full,
}
pub struct Clock {
base: WidgetBase,
format: ClockFormat,
show_seconds: bool,
show_date: bool,
date_format: DateFormat,
on_click: Option<Box<dyn Fn() + Send + Sync>>,
cached_time: String,
cached_date: String,
}
impl Clock {
pub fn new() -> Self {
Self {
base: WidgetBase::new().with_class("clock"),
format: ClockFormat::default(),
show_seconds: false,
show_date: false,
date_format: DateFormat::default(),
on_click: None,
cached_time: "12:00".to_string(),
cached_date: "January 1".to_string(),
}
}
pub fn format(mut self, format: ClockFormat) -> Self {
self.format = format;
self
}
pub fn show_seconds(mut self, show: bool) -> Self {
self.show_seconds = show;
self
}
pub fn show_date(mut self, show: bool) -> Self {
self.show_date = show;
self
}
pub fn date_format(mut self, format: DateFormat) -> Self {
self.date_format = format;
self
}
pub fn on_click<F>(mut self, handler: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_click = Some(Box::new(handler));
self
}
pub fn class(mut self, class: &str) -> Self {
self.base.classes.add(class);
self
}
pub fn update(&mut self) {
let now = std::time::SystemTime::now();
if let Ok(duration) = now.duration_since(std::time::UNIX_EPOCH) {
let secs = duration.as_secs();
let hours = ((secs / 3600) % 24) as u32;
let minutes = ((secs / 60) % 60) as u32;
let seconds = (secs % 60) as u32;
self.cached_time = match self.format {
ClockFormat::TwelveHour => {
let (h, ampm) = if hours == 0 {
(12, "AM")
} else if hours < 12 {
(hours, "AM")
} else if hours == 12 {
(12, "PM")
} else {
(hours - 12, "PM")
};
if self.show_seconds {
format!("{}:{:02}:{:02} {}", h, minutes, seconds, ampm)
} else {
format!("{}:{:02} {}", h, minutes, ampm)
}
}
ClockFormat::TwentyFourHour => {
if self.show_seconds {
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
} else {
format!("{:02}:{:02}", hours, minutes)
}
}
};
let days = secs / 86400;
let month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
let day_names = ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"];
let day_of_week = day_names[(days % 7) as usize];
let month = month_names[((days / 30) % 12) as usize];
let day_of_month = (days % 30) + 1;
self.cached_date = match self.date_format {
DateFormat::Short => format!("{} {}", month, day_of_month),
DateFormat::Long => format!("{} {}, 2024", month, day_of_month),
DateFormat::DayOfWeek => day_of_week.to_string(),
DateFormat::Full => format!("{}, {} {}, 2024", day_of_week, month, day_of_month),
};
}
}
}
impl Default for Clock {
fn default() -> Self {
Self::new()
}
}
impl Widget for Clock {
fn id(&self) -> WidgetId {
self.base.id
}
fn type_name(&self) -> &'static str {
"clock"
}
fn element_id(&self) -> Option<&str> {
self.base.element_id.as_deref()
}
fn classes(&self) -> &ClassList {
&self.base.classes
}
fn state(&self) -> WidgetState {
self.base.state
}
fn intrinsic_size(&self, _ctx: &LayoutContext) -> Size {
let time_width = self.cached_time.len() as f32 * 8.0;
let date_width = if self.show_date { self.cached_date.len() as f32 * 7.0 } else { 0.0 };
let width = time_width.max(date_width) + 16.0;
let height = if self.show_date { 40.0 } else { 24.0 };
Size::new(width, height)
}
fn layout(&mut self, constraints: Constraints, ctx: &LayoutContext) -> LayoutResult {
self.update(); let size = constraints.constrain(self.intrinsic_size(ctx));
self.base.bounds.size = size;
LayoutResult::new(size)
}
fn paint(&self, painter: &mut Painter, rect: Rect, ctx: &PaintContext) {
let theme = ctx.style_ctx.theme;
if self.base.state.hovered && self.on_click.is_some() {
painter.fill_rect(rect, theme.colors.accent.with_alpha(0.1));
}
let center_x = rect.x() + rect.width() / 2.0;
if self.show_date {
let time_width = self.cached_time.len() as f32 * 8.0;
painter.draw_text(
&self.cached_time,
Point::new(center_x - time_width / 2.0, rect.y() + 18.0),
theme.colors.foreground,
14.0,
);
let date_width = self.cached_date.len() as f32 * 6.0;
painter.draw_text(
&self.cached_date,
Point::new(center_x - date_width / 2.0, rect.y() + 34.0),
theme.colors.muted_foreground,
11.0,
);
} else {
let time_width = self.cached_time.len() as f32 * 8.0;
painter.draw_text(
&self.cached_time,
Point::new(center_x - time_width / 2.0, rect.y() + rect.height() * 0.7),
theme.colors.foreground,
14.0,
);
}
}
fn handle_event(&mut self, event: &Event, ctx: &mut EventContext) -> EventResult {
if let Event::Mouse(mouse) = event {
let in_bounds = self.bounds().contains(mouse.position);
match mouse.kind {
MouseEventKind::Move | MouseEventKind::Enter => {
if in_bounds && !self.base.state.hovered && self.on_click.is_some() {
self.base.state.hovered = true;
ctx.request_redraw();
} else if !in_bounds && self.base.state.hovered {
self.base.state.hovered = false;
ctx.request_redraw();
}
}
MouseEventKind::Leave => {
if self.base.state.hovered {
self.base.state.hovered = false;
ctx.request_redraw();
}
}
MouseEventKind::Up if mouse.button == Some(MouseButton::Left) && in_bounds => {
if let Some(handler) = &self.on_click {
handler();
}
return EventResult::Handled;
}
_ => {}
}
}
EventResult::Ignored
}
fn bounds(&self) -> Rect {
self.base.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.base.bounds = bounds;
}
}