git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/widget/display/status_dot.rs (4.2K)

  1 //! Narrow-trait status dot (Phase 5c leaf sweep).
  2 //!
  3 //! **Deliberate behavior fix:** the legacy `WidgetHost` impl only set `color()` and never emitted
  4 //! geometry on any render path (`all_quads` and `all_rounded_quads` were both empty for it, and
  5 //! `render_widget` never reads `color()` directly), so the dot was **invisible** — a probe test
  6 //! against the legacy widget confirmed zero rects emitted through `render_widget`. The narrow
  7 //! [`Paint`] default emits the color quad, so the dot now actually shows. The probe test at the
  8 //! bottom documents the fix.
  9 
 10 use crate::scene::layout::Rect;
 11 use crate::scene::paint::PaintCtx;
 12 use crate::widget::{Adapted, Input, Layout, Paint};
 13 
 14 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 15 pub enum DotStatus {
 16     Active,
 17     Inactive,
 18     Warning,
 19     Error,
 20 }
 21 
 22 #[derive(Debug, Clone)]
 23 pub struct StatusDot {
 24     pub status: DotStatus,
 25 }
 26 
 27 impl StatusDot {
 28     /// The dot's default side.
 29     pub const SIZE: f32 = 12.0;
 30 
 31     pub fn new(status: DotStatus) -> Adapted<StatusDot> {
 32         Adapted::new(StatusDot { status })
 33     }
 34 
 35     pub fn set_status(&mut self, status: DotStatus) {
 36         self.status = status;
 37     }
 38 }
 39 
 40 impl Layout for StatusDot {
 41     /// A dot: a fixed small disc unless the host sizes it.
 42     fn intrinsic_size(&self) -> Option<crate::scene::layout::Size> {
 43         Some(crate::scene::layout::Size::new(StatusDot::SIZE, StatusDot::SIZE))
 44     }
 45 }
 46 
 47 impl Paint for StatusDot {
 48     fn color(&self) -> [f32; 4] {
 49         match self.status {
 50             DotStatus::Active => [0.20, 0.70, 0.35, 1.0],
 51             DotStatus::Inactive => [0.50, 0.50, 0.55, 1.0],
 52             DotStatus::Warning => [0.90, 0.60, 0.10, 1.0],
 53             DotStatus::Error => [0.85, 0.25, 0.25, 1.0],
 54         }
 55     }
 56 
 57     /// A disc: the colour as a rounded rect whose radius is half the short side. A rounded
 58     /// rect rather than a circle prim so it survives every legacy bridge (`all_rounded_quads`,
 59     /// `render_widget`), which carry rounded rects but not circles.
 60     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
 61         let r = rect.width.min(rect.height) * 0.5;
 62         ctx.rounded_rect(rect, r, (true, true, true, true), self.color());
 63     }
 64 }
 65 
 66 impl Input for StatusDot {
 67     fn blocks_root_plate_drag(&self) -> bool {
 68         false
 69     }
 70 }
 71 
 72 #[cfg(test)]
 73 mod tests {
 74     use super::*;
 75     use crate::widget::{WidgetHost, UiContext};
 76 
 77     #[test]
 78     fn emits_its_disc_through_the_rounded_bridge() {
 79         let mut dot = StatusDot::new(DotStatus::Warning);
 80         WidgetHost::set_rect(&mut dot, 5.0, 6.0, 10.0, 10.0);
 81         assert_eq!(
 82             WidgetHost::all_rounded_quads(&dot, &UiContext::new()),
 83             vec![(5.0, 6.0, 10.0, 10.0, 5.0, [0.90, 0.60, 0.10, 1.0], (true, true, true, true))],
 84             "a disc: the rect at half-side radius",
 85         );
 86         assert!(WidgetHost::extra_quads(&dot).is_empty(), "nothing on the plain path (apps read both)");
 87         // Drags pass through, as legacy declared.
 88         assert!(!WidgetHost::blocks_root_plate_drag(&dot));
 89         // State mutation through Deref, as call sites write it.
 90         dot.set_status(DotStatus::Error);
 91         assert_eq!(dot.status, DotStatus::Error);
 92     }
 93 
 94     /// Documents the behavior fix: the legacy `StatusDot` emitted **zero** rects through
 95     /// `render_widget` (probe run against the pre-migration widget), i.e. the dot was invisible
 96     /// wherever it was used. The migrated widget emits exactly one.
 97     #[test]
 98     fn render_widget_now_draws_the_dot() {
 99         struct Probe {
100             rects: Vec<(f32, f32, f32, f32, [f32; 4])>,
101         }
102         impl crate::layout::RenderTarget for Probe {
103             fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
104                 self.rects.push((x, y, w, h, color));
105             }
106             fn text(&mut self, _c: &str, _x: f32, _y: f32, _s: f32, _col: [f32; 4]) {}
107         }
108 
109         let mut ctx = UiContext::new();
110         let mut probe = Probe { rects: Vec::new() };
111         let mut dot = StatusDot::new(DotStatus::Active);
112         crate::layout::render_widget(&mut probe, &mut dot, 10.0, 10.0, 10.0, 10.0, &mut ctx);
113         assert_eq!(probe.rects.len(), 1, "the dot is visible now (legacy emitted 0 here)");
114         assert_eq!(probe.rects[0].4, [0.20, 0.70, 0.35, 1.0], "active-status green");
115     }
116 }