Skip to main content

rlvgl_widgets/
clock.rs

1//! Analog clock widget with sub-pixel anti-aliased hand rotation.
2//!
3//! The clock is composed from z-ordered [`ClockLayer`]s. Each layer reports
4//! its bbox and self-induced dirty rect; the widget unions them, compositor
5//! restores from pristine background under the union, then layers paint in
6//! z-order. The face background lives in the framebuffer's pristine copy —
7//! this widget animates moving parts only.
8//!
9//! Driver path:
10//! 1. Application predicts present-time `target_time` and calls
11//!    [`Clock::set_target_time`]; angles + dirty union are computed and the
12//!    method returns a [`TickOutcome`] for telemetry.
13//! 2. Compositor reads [`Widget::clear_region`] (consumes dirty union) and
14//!    queues pristine restore via DMA2D (or equivalent).
15//! 3. Compositor calls [`Widget::draw`]; layers whose bbox intersects the
16//!    union repaint in z-order.
17
18use 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/// Time-of-day input to the clock.
26///
27/// Sub-second precision via fractional seconds; the driver supplies absolute
28/// time so missed ticks don't accumulate drift.
29#[derive(Copy, Clone, Debug, PartialEq)]
30pub struct ClockTime {
31    /// Seconds since midnight, fractional. May exceed 86400; the angle
32    /// derivation reduces modulo each hand's period.
33    pub seconds_of_day: f64,
34}
35
36/// Hand angles in radians. `0` is 12 o'clock; angles increase clockwise.
37#[derive(Copy, Clone, Debug, PartialEq)]
38pub struct HandAngles {
39    /// Hour hand angle (period 12 h).
40    pub hour: f32,
41    /// Minute hand angle (period 1 h).
42    pub minute: f32,
43    /// Second hand angle (period 1 min).
44    pub second: f32,
45}
46
47/// Snapshot of clock state for one frame: source time and derived angles.
48#[derive(Copy, Clone, Debug, PartialEq)]
49pub struct ClockState {
50    /// Source target time supplied by the driver.
51    pub time: ClockTime,
52    /// Angles derived once per tick by [`Clock::set_target_time`].
53    pub angles: HandAngles,
54}
55
56impl ClockTime {
57    /// Convert this time to clock-hand angles.
58    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/// Outcome of a [`Clock::set_target_time`] call. Reports dirty pixel count
70/// and how many layers will repaint, for telemetry pairing with DWT cycle
71/// counters.
72#[derive(Copy, Clone, Debug, PartialEq)]
73pub enum TickOutcome {
74    /// Dirty union was empty after the first paint; no draw work needed.
75    Skipped,
76    /// Normal incremental frame.
77    Painted {
78        /// Pixel count of the dirty union (`width * height`).
79        dirty_px: u32,
80        /// Number of layers whose bbox intersects the dirty union.
81        layers_painted: u8,
82    },
83    /// First frame after construction or [`Clock::invalidate`]. Entire face
84    /// is repainted. Sample this for "worst case" telemetry baselines.
85    FullRepaint {
86        /// Pixel count of the dirty union.
87        dirty_px: u32,
88        /// Number of layers in this paint.
89        layers_painted: u8,
90    },
91}
92
93/// One visual component of a clock — a hand, center cap, sub-second dot,
94/// digital readout, etc. Layers compose in insertion (z-) order.
95pub trait ClockLayer {
96    /// Pixels this layer might write at `state`, in absolute framebuffer
97    /// coordinates. Used for cross-layer dirty union math.
98    fn bbox(&self, state: &ClockState, bounds: Rect) -> Rect;
99
100    /// Self-induced dirty between `prev` and `state`. An empty rect means
101    /// "I have not changed"; the compositor will still call `paint` if
102    /// another layer's dirty rect intersects this layer's `bbox`.
103    fn dirty(&self, prev: Option<&ClockState>, state: &ClockState, bounds: Rect) -> Rect;
104
105    /// Paint this layer at `state`. Layers must paint within their declared
106    /// `bbox` — the compositor relies on that for dirty-rect correctness.
107    fn paint(&self, renderer: &mut dyn Renderer, state: &ClockState, bounds: Rect);
108}
109
110/// Which hand-angle component an [`AnalogHand`] reads from [`HandAngles`].
111#[derive(Copy, Clone, Debug, PartialEq)]
112pub enum HandKind {
113    /// Hour hand.
114    Hour,
115    /// Minute hand.
116    Minute,
117    /// Second hand.
118    Second,
119}
120
121/// Analog hand laid out as an oriented bounding box rotating around the
122/// face center. Geometry is expressed as fractions of face radius so the
123/// same hand renders correctly at any clock size.
124pub struct AnalogHand {
125    /// Which angle this hand follows.
126    pub kind: HandKind,
127    /// Length from pivot to tip, fraction of face radius.
128    pub length: f32,
129    /// Length from pivot to tail end, fraction of face radius (typically
130    /// `0.0..=0.25`). `0.0` = no tail past the pivot.
131    pub tail: f32,
132    /// Hand width, fraction of face radius.
133    pub width: f32,
134    /// Hand color (alpha modulated by AA coverage at edges).
135    pub color: Color,
136}
137
138impl AnalogHand {
139    /// Construct an hour hand with sensible defaults.
140    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    /// Construct a minute hand with sensible defaults.
151    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    /// Construct a second hand with sensible defaults.
162    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        // Clock convention: 0 rad = 12 o'clock (straight up, i.e. -y on
184        // a screen-coord framebuffer), increasing clockwise. The OBB's
185        // major axis points at the hand's tip:
186        //   12 o'clock → (0, -1), 3 o'clock → (1, 0), 6 → (0, 1), 9 → (-1, 0).
187        let cos_t = libm::sinf(angle);
188        let sin_t = -libm::cosf(angle);
189        // OBB center is offset from the pivot toward the tip by half the
190        // tip-vs-tail asymmetry.
191        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
197/// Sub-second indicator: a small disc orbiting the face center at the
198/// sub-second fractional rate. Completes one full orbit per wall-clock
199/// second, giving visible per-frame motion at 30 Hz / 60 Hz tick rates
200/// — useful for confirming the driver is delivering target times to the
201/// widget on time, and for visualising end-to-end render pipeline
202/// latency.
203pub struct SubsecondDot {
204    /// Orbit radius as a fraction of face radius (typical `0.4..=0.7`).
205    pub orbit_radius: f32,
206    /// Dot radius as a fraction of face radius (typical `0.015..=0.04`).
207    pub dot_radius: f32,
208    /// Dot color (alpha modulated by AA coverage at edges).
209    pub color: Color,
210}
211
212impl SubsecondDot {
213    /// Default dot at 65% orbit radius, 2% dot radius.
214    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        // Sub-second fraction in [0, 1). `as i64 as f64` floor works for
227        // any non-negative `seconds_of_day` and avoids needing
228        // `f64::floor` (not in core).
229        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        // Same convention as hands: 0 = 12 o'clock (up), clockwise.
236        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
284/// Filled circular cap at the face center, drawn on top of the hands to
285/// hide the small visual gap where the three hand pivots meet. Static
286/// after first paint; the same union-expansion pass that protects tick
287/// marks ensures pristine restore covers the cap before any blend.
288pub struct CenterCap {
289    /// Cap radius as a fraction of face radius (typical `0.03..=0.06`).
290    pub radius: f32,
291    /// Cap color (alpha modulated by AA coverage at edges).
292    pub color: Color,
293}
294
295impl CenterCap {
296    /// Default cap sized to comfortably cover the typical pivot region.
297    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
342/// Static tick mark on the clock face. Each tick is its own
343/// [`ClockLayer`], so a hand sweeping across the face only triggers
344/// repaint of the few ticks it crosses, not the whole face. Geometry is
345/// expressed as fractions of face radius for size-agnostic scaling.
346///
347/// The angle convention matches the hands: `0` is 12 o'clock; angle
348/// increases clockwise.
349pub struct TickMark {
350    /// Angle in radians; `0` = 12 o'clock, clockwise.
351    pub angle: f32,
352    /// Distance from face center to outer end of the mark, fraction of
353    /// face radius. `1.0` = exactly at the face edge.
354    pub outer_radius: f32,
355    /// Length along the radial direction, fraction of face radius.
356    pub length: f32,
357    /// Width perpendicular to the radial direction, fraction of face radius.
358    pub width: f32,
359    /// Mark color (alpha modulated by AA coverage at edges).
360    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        // OBB major axis = radial direction; same `(sin, -cos)` mapping as
374        // the hands so 0 rad points up at 12 o'clock.
375        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
404/// Helper that constructs a standard set of [`TickMark`] layers (12 hour
405/// marks and optionally 48 minute marks at non-hour positions) and pushes
406/// them onto a [`Clock`] in z-order. Call this *before* pushing hand
407/// layers so the hands paint on top of the face.
408///
409/// Sizes are deliberately conservative defaults; for custom faces
410/// construct individual [`TickMark`]s and push them with
411/// [`Clock::push_layer`].
412pub struct ClockFace {
413    /// Color for hour marks (12 marks at cardinal positions).
414    pub hour_color: Color,
415    /// Color for minute marks; `None` disables them.
416    pub minute_color: Option<Color>,
417    /// Outer radius / length / width of hour marks (fractions of radius).
418    pub hour_size: TickSize,
419    /// Outer radius / length / width of minute marks.
420    pub minute_size: TickSize,
421}
422
423/// Geometry triple shared by hour and minute mark configurations.
424#[derive(Copy, Clone, Debug)]
425pub struct TickSize {
426    /// Distance from face center to outer end of mark, fraction of radius.
427    pub outer_radius: f32,
428    /// Length along the radial direction, fraction of radius.
429    pub length: f32,
430    /// Width perpendicular to the radial direction, fraction of radius.
431    pub width: f32,
432}
433
434impl ClockFace {
435    /// Standard 12-hour-mark + 48-minute-mark face.
436    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    /// 12 hour marks, no minute marks.
454    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    /// Push tick layers onto `clock` in z-order: minute marks first (so
472    /// hour marks paint on top at cardinal positions if their bboxes
473    /// overlap).
474    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
542/// Analog clock widget. Owns layers in z-order; composes them per frame
543/// via dirty-rect union math.
544pub 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    /// Create a new clock occupying `bounds`. The first call to
555    /// [`set_target_time`](Self::set_target_time) will return
556    /// [`TickOutcome::FullRepaint`].
557    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    /// Append a layer. Layers paint in insertion order (later layers on top).
569    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    /// Force the next [`set_target_time`](Self::set_target_time) call to be
575    /// treated as a full repaint. Use after extent changes, theme changes,
576    /// or when the widget re-enters visibility.
577    pub fn invalidate(&mut self) {
578        self.needs_full_repaint = true;
579    }
580
581    /// Telemetry from the most recent `set_target_time` call.
582    pub fn last_outcome(&self) -> TickOutcome {
583        self.last_outcome
584    }
585
586    /// Compute the next frame's plan from the driver-supplied target time.
587    /// Returns telemetry about what will happen during draw.
588    ///
589    /// Idempotent on a fixed `(target_time, bounds)` — safe for the driver
590    /// to skip frames; absolute-time math means no accumulator drift.
591    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        // Expand the union to fixpoint: any layer whose bbox intersects
614        // the current union will paint, and its full bbox must be inside
615        // the union so the compositor's pristine restore covers every
616        // pixel the layer will write. Without this, static layers with
617        // partial overlap (e.g. a tick mark partially crossed by a hand
618        // sweep) re-blend AA edges over un-restored pixels each frame and
619        // drift darker over time.
620        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}