1use alloc::boxed::Box;
19use alloc::vec::Vec;
20use rlvgl_core::event::Event;
21use rlvgl_core::raster::{Obb, PointF};
22use rlvgl_core::renderer::Renderer;
23use rlvgl_core::widget::{Color, Rect, Widget};
24
25#[derive(Copy, Clone, Debug, PartialEq)]
30pub struct ClockTime {
31 pub seconds_of_day: f64,
34}
35
36#[derive(Copy, Clone, Debug, PartialEq)]
38pub struct HandAngles {
39 pub hour: f32,
41 pub minute: f32,
43 pub second: f32,
45}
46
47#[derive(Copy, Clone, Debug, PartialEq)]
49pub struct ClockState {
50 pub time: ClockTime,
52 pub angles: HandAngles,
54}
55
56impl ClockTime {
57 pub fn to_angles(self) -> HandAngles {
59 const TAU: f32 = core::f32::consts::TAU;
60 let s = self.seconds_of_day as f32;
61 HandAngles {
62 hour: TAU * frac01(s / 43_200.0),
63 minute: TAU * frac01(s / 3_600.0),
64 second: TAU * frac01(s / 60.0),
65 }
66 }
67}
68
69#[derive(Copy, Clone, Debug, PartialEq)]
73pub enum TickOutcome {
74 Skipped,
76 Painted {
78 dirty_px: u32,
80 layers_painted: u8,
82 },
83 FullRepaint {
86 dirty_px: u32,
88 layers_painted: u8,
90 },
91}
92
93pub trait ClockLayer {
96 fn bbox(&self, state: &ClockState, bounds: Rect) -> Rect;
99
100 fn dirty(&self, prev: Option<&ClockState>, state: &ClockState, bounds: Rect) -> Rect;
104
105 fn paint(&self, renderer: &mut dyn Renderer, state: &ClockState, bounds: Rect);
108}
109
110#[derive(Copy, Clone, Debug, PartialEq)]
112pub enum HandKind {
113 Hour,
115 Minute,
117 Second,
119}
120
121pub struct AnalogHand {
125 pub kind: HandKind,
127 pub length: f32,
129 pub tail: f32,
132 pub width: f32,
134 pub color: Color,
136}
137
138impl AnalogHand {
139 pub const fn hour(color: Color) -> Self {
141 Self {
142 kind: HandKind::Hour,
143 length: 0.55,
144 tail: 0.10,
145 width: 0.040,
146 color,
147 }
148 }
149
150 pub const fn minute(color: Color) -> Self {
152 Self {
153 kind: HandKind::Minute,
154 length: 0.80,
155 tail: 0.12,
156 width: 0.028,
157 color,
158 }
159 }
160
161 pub const fn second(color: Color) -> Self {
163 Self {
164 kind: HandKind::Second,
165 length: 0.92,
166 tail: 0.18,
167 width: 0.012,
168 color,
169 }
170 }
171
172 fn obb(&self, state: &ClockState, bounds: Rect) -> Obb {
173 let r = (bounds.width.min(bounds.height) as f32) * 0.5;
174 let cx = bounds.x as f32 + bounds.width as f32 * 0.5;
175 let cy = bounds.y as f32 + bounds.height as f32 * 0.5;
176 let len = (self.length + self.tail) * r;
177 let width = self.width * r;
178 let angle = match self.kind {
179 HandKind::Hour => state.angles.hour,
180 HandKind::Minute => state.angles.minute,
181 HandKind::Second => state.angles.second,
182 };
183 let cos_t = libm::sinf(angle);
188 let sin_t = -libm::cosf(angle);
189 let offset = ((self.length - self.tail) * 0.5) * r;
192 let center = PointF::new(cx + cos_t * offset, cy + sin_t * offset);
193 Obb::from_axis(center, len, width, cos_t, sin_t)
194 }
195}
196
197pub struct SubsecondDot {
204 pub orbit_radius: f32,
206 pub dot_radius: f32,
208 pub color: Color,
210}
211
212impl SubsecondDot {
213 pub const fn standard(color: Color) -> Self {
215 Self {
216 orbit_radius: 0.65,
217 dot_radius: 0.020,
218 color,
219 }
220 }
221
222 fn position(&self, state: &ClockState, bounds: Rect) -> (PointF, f32) {
223 let r = (bounds.width.min(bounds.height) as f32) * 0.5;
224 let cx = bounds.x as f32 + bounds.width as f32 * 0.5;
225 let cy = bounds.y as f32 + bounds.height as f32 * 0.5;
226 let s = state.time.seconds_of_day;
230 let i = s as i64 as f64;
231 let frac = (s - i) as f32;
232 let angle = core::f32::consts::TAU * frac;
233 let orbit = self.orbit_radius * r;
234 let dot = self.dot_radius * r;
235 let cos_t = libm::sinf(angle);
237 let sin_t = -libm::cosf(angle);
238 let center = PointF::new(cx + cos_t * orbit, cy + sin_t * orbit);
239 (center, dot)
240 }
241
242 fn bbox_for(&self, state: &ClockState, bounds: Rect) -> Rect {
243 let (center, dot) = self.position(state, bounds);
244 let pad = dot + 1.0;
245 Rect {
246 x: (center.x - pad) as i32 - 1,
247 y: (center.y - pad) as i32 - 1,
248 width: (pad * 2.0) as i32 + 3,
249 height: (pad * 2.0) as i32 + 3,
250 }
251 }
252}
253
254impl ClockLayer for SubsecondDot {
255 fn bbox(&self, state: &ClockState, bounds: Rect) -> Rect {
256 self.bbox_for(state, bounds)
257 }
258
259 fn dirty(&self, prev: Option<&ClockState>, state: &ClockState, bounds: Rect) -> Rect {
260 let cur = self.bbox_for(state, bounds);
261 match prev {
262 None => cur,
263 Some(p) => {
264 if p.time.seconds_of_day == state.time.seconds_of_day {
265 Rect {
266 x: 0,
267 y: 0,
268 width: 0,
269 height: 0,
270 }
271 } else {
272 union_rect(cur, self.bbox_for(p, bounds))
273 }
274 }
275 }
276 }
277
278 fn paint(&self, renderer: &mut dyn Renderer, state: &ClockState, bounds: Rect) {
279 let (center, dot) = self.position(state, bounds);
280 renderer.fill_disc_aa(center, dot, self.color);
281 }
282}
283
284pub struct CenterCap {
289 pub radius: f32,
291 pub color: Color,
293}
294
295impl CenterCap {
296 pub const fn standard(color: Color) -> Self {
298 Self {
299 radius: 0.04,
300 color,
301 }
302 }
303
304 fn center_radius(&self, bounds: Rect) -> (PointF, f32) {
305 let r = (bounds.width.min(bounds.height) as f32) * 0.5;
306 let cx = bounds.x as f32 + bounds.width as f32 * 0.5;
307 let cy = bounds.y as f32 + bounds.height as f32 * 0.5;
308 (PointF::new(cx, cy), self.radius * r)
309 }
310}
311
312impl ClockLayer for CenterCap {
313 fn bbox(&self, _state: &ClockState, bounds: Rect) -> Rect {
314 let (center, r) = self.center_radius(bounds);
315 let pad = r + 1.0;
316 Rect {
317 x: (center.x - pad) as i32 - 1,
318 y: (center.y - pad) as i32 - 1,
319 width: (pad * 2.0) as i32 + 3,
320 height: (pad * 2.0) as i32 + 3,
321 }
322 }
323
324 fn dirty(&self, prev: Option<&ClockState>, state: &ClockState, bounds: Rect) -> Rect {
325 match prev {
326 None => self.bbox(state, bounds),
327 Some(_) => Rect {
328 x: 0,
329 y: 0,
330 width: 0,
331 height: 0,
332 },
333 }
334 }
335
336 fn paint(&self, renderer: &mut dyn Renderer, _state: &ClockState, bounds: Rect) {
337 let (center, r) = self.center_radius(bounds);
338 renderer.fill_disc_aa(center, r, self.color);
339 }
340}
341
342pub struct TickMark {
350 pub angle: f32,
352 pub outer_radius: f32,
355 pub length: f32,
357 pub width: f32,
359 pub color: Color,
361}
362
363impl TickMark {
364 fn obb(&self, bounds: Rect) -> Obb {
365 let r = (bounds.width.min(bounds.height) as f32) * 0.5;
366 let cx = bounds.x as f32 + bounds.width as f32 * 0.5;
367 let cy = bounds.y as f32 + bounds.height as f32 * 0.5;
368 let outer = self.outer_radius * r;
369 let inner = (self.outer_radius - self.length).max(0.0) * r;
370 let mid_r = (outer + inner) * 0.5;
371 let len = outer - inner;
372 let width = self.width * r;
373 let cos_t = libm::sinf(self.angle);
376 let sin_t = -libm::cosf(self.angle);
377 let center = PointF::new(cx + cos_t * mid_r, cy + sin_t * mid_r);
378 Obb::from_axis(center, len, width, cos_t, sin_t)
379 }
380}
381
382impl ClockLayer for TickMark {
383 fn bbox(&self, _state: &ClockState, bounds: Rect) -> Rect {
384 self.obb(bounds).aabb()
385 }
386
387 fn dirty(&self, prev: Option<&ClockState>, _state: &ClockState, bounds: Rect) -> Rect {
388 match prev {
389 None => self.obb(bounds).aabb(),
390 Some(_) => Rect {
391 x: 0,
392 y: 0,
393 width: 0,
394 height: 0,
395 },
396 }
397 }
398
399 fn paint(&self, renderer: &mut dyn Renderer, _state: &ClockState, bounds: Rect) {
400 renderer.fill_obb_aa(self.obb(bounds), self.color);
401 }
402}
403
404pub struct ClockFace {
413 pub hour_color: Color,
415 pub minute_color: Option<Color>,
417 pub hour_size: TickSize,
419 pub minute_size: TickSize,
421}
422
423#[derive(Copy, Clone, Debug)]
425pub struct TickSize {
426 pub outer_radius: f32,
428 pub length: f32,
430 pub width: f32,
432}
433
434impl ClockFace {
435 pub const fn standard(hour_color: Color, minute_color: Color) -> Self {
437 Self {
438 hour_color,
439 minute_color: Some(minute_color),
440 hour_size: TickSize {
441 outer_radius: 0.96,
442 length: 0.10,
443 width: 0.030,
444 },
445 minute_size: TickSize {
446 outer_radius: 0.96,
447 length: 0.05,
448 width: 0.012,
449 },
450 }
451 }
452
453 pub const fn hours_only(color: Color) -> Self {
455 Self {
456 hour_color: color,
457 minute_color: None,
458 hour_size: TickSize {
459 outer_radius: 0.96,
460 length: 0.10,
461 width: 0.030,
462 },
463 minute_size: TickSize {
464 outer_radius: 0.0,
465 length: 0.0,
466 width: 0.0,
467 },
468 }
469 }
470
471 pub fn push_layers(&self, clock: &mut Clock) {
475 if let Some(minute_color) = self.minute_color {
476 for i in 0..60 {
477 if i % 5 == 0 {
478 continue;
479 }
480 let angle = core::f32::consts::TAU * i as f32 / 60.0;
481 clock.push_layer(TickMark {
482 angle,
483 outer_radius: self.minute_size.outer_radius,
484 length: self.minute_size.length,
485 width: self.minute_size.width,
486 color: minute_color,
487 });
488 }
489 }
490 for i in 0..12 {
491 let angle = core::f32::consts::TAU * i as f32 / 12.0;
492 clock.push_layer(TickMark {
493 angle,
494 outer_radius: self.hour_size.outer_radius,
495 length: self.hour_size.length,
496 width: self.hour_size.width,
497 color: self.hour_color,
498 });
499 }
500 }
501}
502
503impl ClockLayer for AnalogHand {
504 fn bbox(&self, state: &ClockState, bounds: Rect) -> Rect {
505 self.obb(state, bounds).aabb()
506 }
507
508 fn dirty(&self, prev: Option<&ClockState>, state: &ClockState, bounds: Rect) -> Rect {
509 let cur = self.bbox(state, bounds);
510 match prev {
511 None => cur,
512 Some(p) => {
513 let prev_angle = match self.kind {
514 HandKind::Hour => p.angles.hour,
515 HandKind::Minute => p.angles.minute,
516 HandKind::Second => p.angles.second,
517 };
518 let cur_angle = match self.kind {
519 HandKind::Hour => state.angles.hour,
520 HandKind::Minute => state.angles.minute,
521 HandKind::Second => state.angles.second,
522 };
523 if prev_angle == cur_angle {
524 Rect {
525 x: 0,
526 y: 0,
527 width: 0,
528 height: 0,
529 }
530 } else {
531 union_rect(cur, self.bbox(p, bounds))
532 }
533 }
534 }
535 }
536
537 fn paint(&self, renderer: &mut dyn Renderer, state: &ClockState, bounds: Rect) {
538 renderer.fill_obb_aa(self.obb(state, bounds), self.color);
539 }
540}
541
542pub struct Clock {
545 bounds: Rect,
546 layers: Vec<Box<dyn ClockLayer>>,
547 state: Option<ClockState>,
548 dirty_union: Option<Rect>,
549 last_outcome: TickOutcome,
550 needs_full_repaint: bool,
551}
552
553impl Clock {
554 pub fn new(bounds: Rect) -> Self {
558 Self {
559 bounds,
560 layers: Vec::new(),
561 state: None,
562 dirty_union: None,
563 last_outcome: TickOutcome::Skipped,
564 needs_full_repaint: true,
565 }
566 }
567
568 pub fn push_layer<L: ClockLayer + 'static>(&mut self, layer: L) {
570 self.layers.push(Box::new(layer));
571 self.needs_full_repaint = true;
572 }
573
574 pub fn invalidate(&mut self) {
578 self.needs_full_repaint = true;
579 }
580
581 pub fn last_outcome(&self) -> TickOutcome {
583 self.last_outcome
584 }
585
586 pub fn set_target_time(&mut self, target_time: ClockTime) -> TickOutcome {
592 let new_state = ClockState {
593 time: target_time,
594 angles: target_time.to_angles(),
595 };
596 let prev: Option<&ClockState> = if self.needs_full_repaint {
597 None
598 } else {
599 self.state.as_ref()
600 };
601
602 let mut union: Option<Rect> = None;
603 for layer in &self.layers {
604 let d = layer.dirty(prev, &new_state, self.bounds);
605 if d.width > 0 && d.height > 0 {
606 union = Some(match union {
607 None => d,
608 Some(u) => union_rect(u, d),
609 });
610 }
611 }
612
613 if let Some(mut u) = union {
621 loop {
622 let mut grew = false;
623 for layer in &self.layers {
624 let bb = layer.bbox(&new_state, self.bounds);
625 if bb.width == 0 || bb.height == 0 {
626 continue;
627 }
628 if rects_intersect(bb, u) {
629 let merged = union_rect(u, bb);
630 if merged != u {
631 u = merged;
632 grew = true;
633 }
634 }
635 }
636 if !grew {
637 break;
638 }
639 }
640 union = Some(u);
641 }
642
643 let union_clipped = union.and_then(|u| rect_intersect(u, self.bounds));
644
645 let outcome = match (self.needs_full_repaint, union_clipped) {
646 (_, None) => TickOutcome::Skipped,
647 (full, Some(r)) => {
648 let dirty_px = (r.width as u32).saturating_mul(r.height as u32);
649 let layers_painted = self
650 .layers
651 .iter()
652 .filter(|l| rects_intersect(l.bbox(&new_state, self.bounds), r))
653 .count() as u8;
654 if full {
655 TickOutcome::FullRepaint {
656 dirty_px,
657 layers_painted,
658 }
659 } else {
660 TickOutcome::Painted {
661 dirty_px,
662 layers_painted,
663 }
664 }
665 }
666 };
667
668 self.dirty_union = union_clipped;
669 self.state = Some(new_state);
670 self.needs_full_repaint = false;
671 self.last_outcome = outcome;
672 outcome
673 }
674}
675
676impl Widget for Clock {
677 fn bounds(&self) -> Rect {
678 self.bounds
679 }
680
681 fn draw(&self, renderer: &mut dyn Renderer) {
682 let Some(state) = self.state.as_ref() else {
683 return;
684 };
685 let union = self.dirty_union.unwrap_or(self.bounds);
686 for layer in &self.layers {
687 if rects_intersect(layer.bbox(state, self.bounds), union) {
688 layer.paint(renderer, state, self.bounds);
689 }
690 }
691 }
692
693 fn handle_event(&mut self, _event: &Event) -> bool {
694 false
695 }
696
697 fn clear_region(&mut self) -> Option<Rect> {
698 self.dirty_union.take()
699 }
700}
701
702#[inline]
703fn frac01(x: f32) -> f32 {
704 let i = x as i64 as f32;
705 let f = x - i;
706 if f < 0.0 { f + 1.0 } else { f }
707}
708
709fn union_rect(a: Rect, b: Rect) -> Rect {
710 if a.width == 0 || a.height == 0 {
711 return b;
712 }
713 if b.width == 0 || b.height == 0 {
714 return a;
715 }
716 let x0 = a.x.min(b.x);
717 let y0 = a.y.min(b.y);
718 let x1 = (a.x + a.width).max(b.x + b.width);
719 let y1 = (a.y + a.height).max(b.y + b.height);
720 Rect {
721 x: x0,
722 y: y0,
723 width: x1 - x0,
724 height: y1 - y0,
725 }
726}
727
728fn rect_intersect(a: Rect, b: Rect) -> Option<Rect> {
729 let x0 = a.x.max(b.x);
730 let y0 = a.y.max(b.y);
731 let x1 = (a.x + a.width).min(b.x + b.width);
732 let y1 = (a.y + a.height).min(b.y + b.height);
733 if x1 > x0 && y1 > y0 {
734 Some(Rect {
735 x: x0,
736 y: y0,
737 width: x1 - x0,
738 height: y1 - y0,
739 })
740 } else {
741 None
742 }
743}
744
745fn rects_intersect(a: Rect, b: Rect) -> bool {
746 rect_intersect(a, b).is_some()
747}