use alloc::boxed::Box;
use alloc::vec::Vec;
use rlvgl_core::event::Event;
use rlvgl_core::raster::{Obb, PointF};
use rlvgl_core::renderer::Renderer;
use rlvgl_core::widget::{Color, Rect, Widget};
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ClockTime {
pub seconds_of_day: f64,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct HandAngles {
pub hour: f32,
pub minute: f32,
pub second: f32,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ClockState {
pub time: ClockTime,
pub angles: HandAngles,
}
impl ClockTime {
pub fn to_angles(self) -> HandAngles {
const TAU: f32 = core::f32::consts::TAU;
let s = self.seconds_of_day as f32;
HandAngles {
hour: TAU * frac01(s / 43_200.0),
minute: TAU * frac01(s / 3_600.0),
second: TAU * frac01(s / 60.0),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum TickOutcome {
Skipped,
Painted {
dirty_px: u32,
layers_painted: u8,
},
FullRepaint {
dirty_px: u32,
layers_painted: u8,
},
}
pub trait ClockLayer {
fn bbox(&self, state: &ClockState, bounds: Rect) -> Rect;
fn dirty(&self, prev: Option<&ClockState>, state: &ClockState, bounds: Rect) -> Rect;
fn paint(&self, renderer: &mut dyn Renderer, state: &ClockState, bounds: Rect);
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum HandKind {
Hour,
Minute,
Second,
}
pub struct AnalogHand {
pub kind: HandKind,
pub length: f32,
pub tail: f32,
pub width: f32,
pub color: Color,
}
impl AnalogHand {
pub const fn hour(color: Color) -> Self {
Self {
kind: HandKind::Hour,
length: 0.55,
tail: 0.10,
width: 0.040,
color,
}
}
pub const fn minute(color: Color) -> Self {
Self {
kind: HandKind::Minute,
length: 0.80,
tail: 0.12,
width: 0.028,
color,
}
}
pub const fn second(color: Color) -> Self {
Self {
kind: HandKind::Second,
length: 0.92,
tail: 0.18,
width: 0.012,
color,
}
}
fn obb(&self, state: &ClockState, bounds: Rect) -> Obb {
let r = (bounds.width.min(bounds.height) as f32) * 0.5;
let cx = bounds.x as f32 + bounds.width as f32 * 0.5;
let cy = bounds.y as f32 + bounds.height as f32 * 0.5;
let len = (self.length + self.tail) * r;
let width = self.width * r;
let angle = match self.kind {
HandKind::Hour => state.angles.hour,
HandKind::Minute => state.angles.minute,
HandKind::Second => state.angles.second,
};
let cos_t = libm::sinf(angle);
let sin_t = -libm::cosf(angle);
let offset = ((self.length - self.tail) * 0.5) * r;
let center = PointF::new(cx + cos_t * offset, cy + sin_t * offset);
Obb::from_axis(center, len, width, cos_t, sin_t)
}
}
pub struct SubsecondDot {
pub orbit_radius: f32,
pub dot_radius: f32,
pub color: Color,
}
impl SubsecondDot {
pub const fn standard(color: Color) -> Self {
Self {
orbit_radius: 0.65,
dot_radius: 0.020,
color,
}
}
fn position(&self, state: &ClockState, bounds: Rect) -> (PointF, f32) {
let r = (bounds.width.min(bounds.height) as f32) * 0.5;
let cx = bounds.x as f32 + bounds.width as f32 * 0.5;
let cy = bounds.y as f32 + bounds.height as f32 * 0.5;
let s = state.time.seconds_of_day;
let i = s as i64 as f64;
let frac = (s - i) as f32;
let angle = core::f32::consts::TAU * frac;
let orbit = self.orbit_radius * r;
let dot = self.dot_radius * r;
let cos_t = libm::sinf(angle);
let sin_t = -libm::cosf(angle);
let center = PointF::new(cx + cos_t * orbit, cy + sin_t * orbit);
(center, dot)
}
fn bbox_for(&self, state: &ClockState, bounds: Rect) -> Rect {
let (center, dot) = self.position(state, bounds);
let pad = dot + 1.0;
Rect {
x: (center.x - pad) as i32 - 1,
y: (center.y - pad) as i32 - 1,
width: (pad * 2.0) as i32 + 3,
height: (pad * 2.0) as i32 + 3,
}
}
}
impl ClockLayer for SubsecondDot {
fn bbox(&self, state: &ClockState, bounds: Rect) -> Rect {
self.bbox_for(state, bounds)
}
fn dirty(&self, prev: Option<&ClockState>, state: &ClockState, bounds: Rect) -> Rect {
let cur = self.bbox_for(state, bounds);
match prev {
None => cur,
Some(p) => {
if p.time.seconds_of_day == state.time.seconds_of_day {
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
}
} else {
union_rect(cur, self.bbox_for(p, bounds))
}
}
}
}
fn paint(&self, renderer: &mut dyn Renderer, state: &ClockState, bounds: Rect) {
let (center, dot) = self.position(state, bounds);
renderer.fill_disc_aa(center, dot, self.color);
}
}
pub struct CenterCap {
pub radius: f32,
pub color: Color,
}
impl CenterCap {
pub const fn standard(color: Color) -> Self {
Self {
radius: 0.04,
color,
}
}
fn center_radius(&self, bounds: Rect) -> (PointF, f32) {
let r = (bounds.width.min(bounds.height) as f32) * 0.5;
let cx = bounds.x as f32 + bounds.width as f32 * 0.5;
let cy = bounds.y as f32 + bounds.height as f32 * 0.5;
(PointF::new(cx, cy), self.radius * r)
}
}
impl ClockLayer for CenterCap {
fn bbox(&self, _state: &ClockState, bounds: Rect) -> Rect {
let (center, r) = self.center_radius(bounds);
let pad = r + 1.0;
Rect {
x: (center.x - pad) as i32 - 1,
y: (center.y - pad) as i32 - 1,
width: (pad * 2.0) as i32 + 3,
height: (pad * 2.0) as i32 + 3,
}
}
fn dirty(&self, prev: Option<&ClockState>, state: &ClockState, bounds: Rect) -> Rect {
match prev {
None => self.bbox(state, bounds),
Some(_) => Rect {
x: 0,
y: 0,
width: 0,
height: 0,
},
}
}
fn paint(&self, renderer: &mut dyn Renderer, _state: &ClockState, bounds: Rect) {
let (center, r) = self.center_radius(bounds);
renderer.fill_disc_aa(center, r, self.color);
}
}
pub struct TickMark {
pub angle: f32,
pub outer_radius: f32,
pub length: f32,
pub width: f32,
pub color: Color,
}
impl TickMark {
fn obb(&self, bounds: Rect) -> Obb {
let r = (bounds.width.min(bounds.height) as f32) * 0.5;
let cx = bounds.x as f32 + bounds.width as f32 * 0.5;
let cy = bounds.y as f32 + bounds.height as f32 * 0.5;
let outer = self.outer_radius * r;
let inner = (self.outer_radius - self.length).max(0.0) * r;
let mid_r = (outer + inner) * 0.5;
let len = outer - inner;
let width = self.width * r;
let cos_t = libm::sinf(self.angle);
let sin_t = -libm::cosf(self.angle);
let center = PointF::new(cx + cos_t * mid_r, cy + sin_t * mid_r);
Obb::from_axis(center, len, width, cos_t, sin_t)
}
}
impl ClockLayer for TickMark {
fn bbox(&self, _state: &ClockState, bounds: Rect) -> Rect {
self.obb(bounds).aabb()
}
fn dirty(&self, prev: Option<&ClockState>, _state: &ClockState, bounds: Rect) -> Rect {
match prev {
None => self.obb(bounds).aabb(),
Some(_) => Rect {
x: 0,
y: 0,
width: 0,
height: 0,
},
}
}
fn paint(&self, renderer: &mut dyn Renderer, _state: &ClockState, bounds: Rect) {
renderer.fill_obb_aa(self.obb(bounds), self.color);
}
}
pub struct ClockFace {
pub hour_color: Color,
pub minute_color: Option<Color>,
pub hour_size: TickSize,
pub minute_size: TickSize,
}
#[derive(Copy, Clone, Debug)]
pub struct TickSize {
pub outer_radius: f32,
pub length: f32,
pub width: f32,
}
impl ClockFace {
pub const fn standard(hour_color: Color, minute_color: Color) -> Self {
Self {
hour_color,
minute_color: Some(minute_color),
hour_size: TickSize {
outer_radius: 0.96,
length: 0.10,
width: 0.030,
},
minute_size: TickSize {
outer_radius: 0.96,
length: 0.05,
width: 0.012,
},
}
}
pub const fn hours_only(color: Color) -> Self {
Self {
hour_color: color,
minute_color: None,
hour_size: TickSize {
outer_radius: 0.96,
length: 0.10,
width: 0.030,
},
minute_size: TickSize {
outer_radius: 0.0,
length: 0.0,
width: 0.0,
},
}
}
pub fn push_layers(&self, clock: &mut Clock) {
if let Some(minute_color) = self.minute_color {
for i in 0..60 {
if i % 5 == 0 {
continue;
}
let angle = core::f32::consts::TAU * i as f32 / 60.0;
clock.push_layer(TickMark {
angle,
outer_radius: self.minute_size.outer_radius,
length: self.minute_size.length,
width: self.minute_size.width,
color: minute_color,
});
}
}
for i in 0..12 {
let angle = core::f32::consts::TAU * i as f32 / 12.0;
clock.push_layer(TickMark {
angle,
outer_radius: self.hour_size.outer_radius,
length: self.hour_size.length,
width: self.hour_size.width,
color: self.hour_color,
});
}
}
}
impl ClockLayer for AnalogHand {
fn bbox(&self, state: &ClockState, bounds: Rect) -> Rect {
self.obb(state, bounds).aabb()
}
fn dirty(&self, prev: Option<&ClockState>, state: &ClockState, bounds: Rect) -> Rect {
let cur = self.bbox(state, bounds);
match prev {
None => cur,
Some(p) => {
let prev_angle = match self.kind {
HandKind::Hour => p.angles.hour,
HandKind::Minute => p.angles.minute,
HandKind::Second => p.angles.second,
};
let cur_angle = match self.kind {
HandKind::Hour => state.angles.hour,
HandKind::Minute => state.angles.minute,
HandKind::Second => state.angles.second,
};
if prev_angle == cur_angle {
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
}
} else {
union_rect(cur, self.bbox(p, bounds))
}
}
}
}
fn paint(&self, renderer: &mut dyn Renderer, state: &ClockState, bounds: Rect) {
renderer.fill_obb_aa(self.obb(state, bounds), self.color);
}
}
pub struct Clock {
bounds: Rect,
layers: Vec<Box<dyn ClockLayer>>,
state: Option<ClockState>,
dirty_union: Option<Rect>,
last_outcome: TickOutcome,
needs_full_repaint: bool,
}
impl Clock {
pub fn new(bounds: Rect) -> Self {
Self {
bounds,
layers: Vec::new(),
state: None,
dirty_union: None,
last_outcome: TickOutcome::Skipped,
needs_full_repaint: true,
}
}
pub fn push_layer<L: ClockLayer + 'static>(&mut self, layer: L) {
self.layers.push(Box::new(layer));
self.needs_full_repaint = true;
}
pub fn invalidate(&mut self) {
self.needs_full_repaint = true;
}
pub fn last_outcome(&self) -> TickOutcome {
self.last_outcome
}
pub fn set_target_time(&mut self, target_time: ClockTime) -> TickOutcome {
let new_state = ClockState {
time: target_time,
angles: target_time.to_angles(),
};
let prev: Option<&ClockState> = if self.needs_full_repaint {
None
} else {
self.state.as_ref()
};
let mut union: Option<Rect> = None;
for layer in &self.layers {
let d = layer.dirty(prev, &new_state, self.bounds);
if d.width > 0 && d.height > 0 {
union = Some(match union {
None => d,
Some(u) => union_rect(u, d),
});
}
}
if let Some(mut u) = union {
loop {
let mut grew = false;
for layer in &self.layers {
let bb = layer.bbox(&new_state, self.bounds);
if bb.width == 0 || bb.height == 0 {
continue;
}
if rects_intersect(bb, u) {
let merged = union_rect(u, bb);
if merged != u {
u = merged;
grew = true;
}
}
}
if !grew {
break;
}
}
union = Some(u);
}
let union_clipped = union.and_then(|u| rect_intersect(u, self.bounds));
let outcome = match (self.needs_full_repaint, union_clipped) {
(_, None) => TickOutcome::Skipped,
(full, Some(r)) => {
let dirty_px = (r.width as u32).saturating_mul(r.height as u32);
let layers_painted = self
.layers
.iter()
.filter(|l| rects_intersect(l.bbox(&new_state, self.bounds), r))
.count() as u8;
if full {
TickOutcome::FullRepaint {
dirty_px,
layers_painted,
}
} else {
TickOutcome::Painted {
dirty_px,
layers_painted,
}
}
}
};
self.dirty_union = union_clipped;
self.state = Some(new_state);
self.needs_full_repaint = false;
self.last_outcome = outcome;
outcome
}
}
impl Widget for Clock {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, renderer: &mut dyn Renderer) {
let Some(state) = self.state.as_ref() else {
return;
};
let union = self.dirty_union.unwrap_or(self.bounds);
for layer in &self.layers {
if rects_intersect(layer.bbox(state, self.bounds), union) {
layer.paint(renderer, state, self.bounds);
}
}
}
fn handle_event(&mut self, _event: &Event) -> bool {
false
}
fn clear_region(&mut self) -> Option<Rect> {
self.dirty_union.take()
}
}
#[inline]
fn frac01(x: f32) -> f32 {
let i = x as i64 as f32;
let f = x - i;
if f < 0.0 { f + 1.0 } else { f }
}
fn union_rect(a: Rect, b: Rect) -> Rect {
if a.width == 0 || a.height == 0 {
return b;
}
if b.width == 0 || b.height == 0 {
return a;
}
let x0 = a.x.min(b.x);
let y0 = a.y.min(b.y);
let x1 = (a.x + a.width).max(b.x + b.width);
let y1 = (a.y + a.height).max(b.y + b.height);
Rect {
x: x0,
y: y0,
width: x1 - x0,
height: y1 - y0,
}
}
fn rect_intersect(a: Rect, b: Rect) -> Option<Rect> {
let x0 = a.x.max(b.x);
let y0 = a.y.max(b.y);
let x1 = (a.x + a.width).min(b.x + b.width);
let y1 = (a.y + a.height).min(b.y + b.height);
if x1 > x0 && y1 > y0 {
Some(Rect {
x: x0,
y: y0,
width: x1 - x0,
height: y1 - y0,
})
} else {
None
}
}
fn rects_intersect(a: Rect, b: Rect) -> bool {
rect_intersect(a, b).is_some()
}