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

src/widget/display/usage_bar.rs (5.7K)

  1 //! Narrow-trait usage bar (Phase 5c leaf sweep). The narrow [`Paint::paint`] emits a rounded
  2 //! track and fill (the ProgressBar's composition, in both styles), which reach legacy render
  3 //! loops through the adapter's `all_rounded_quads` reverse bridge.
  4 
  5 use crate::scene::layout::Rect;
  6 use crate::scene::paint::PaintCtx;
  7 use crate::widget::{Adapted, Input, Layout, Paint};
  8 
  9 #[derive(Debug, Clone)]
 10 pub struct UsageBar {
 11     pub value: f32, // 0.0 to 1.0
 12     pub fill_color: [f32; 4],
 13     pub bg_color: [f32; 4],
 14     /// Recessed-track style, the ProgressBar's: a well carved into the plate,
 15     /// `bg_color` unused (the plate is the floor), the fill inset onto it.
 16     /// Defaults to `control_relief()`.
 17     recessed: Option<bool>,
 18 }
 19 
 20 impl UsageBar {
 21     /// The style in force: the per-widget override (`with_recessed`) when set, else
 22     /// the DE's `control_relief`, read live so a runtime switch
 23     /// (`layout::set_control_relief`) restyles every control at once.
 24     fn recessed(&self) -> bool {
 25         self.recessed.unwrap_or_else(crate::layout::control_relief)
 26     }
 27 
 28     pub fn new(value: f32) -> Adapted<UsageBar> {
 29         Adapted::new(UsageBar {
 30             value: value.clamp(0.0, 1.0),
 31             fill_color: [0.30, 0.50, 0.32, 1.0], // green-ish
 32             bg_color: [0.15, 0.15, 0.24, 1.0],   // dark-ish
 33             recessed: None,
 34         })
 35     }
 36 
 37     pub fn set_value(&mut self, value: f32) {
 38         self.value = value.clamp(0.0, 1.0);
 39     }
 40 }
 41 
 42 /// By-value builders can't flow through `Deref`, so the legacy `with_colors` chain
 43 /// (`UsageBar::new(v).with_colors(..)`) is mirrored on the wrapped type.
 44 impl Adapted<UsageBar> {
 45     pub fn with_colors(mut self, fill: [f32; 4], bg: [f32; 4]) -> Self {
 46         self.fill_color = fill;
 47         self.bg_color = bg;
 48         self
 49     }
 50 
 51     /// Recessed style: see the `recessed` field.
 52     pub fn with_recessed(mut self, recessed: bool) -> Self {
 53         self.recessed = Some(recessed);
 54         self
 55     }
 56 }
 57 
 58 impl Layout for UsageBar {
 59     /// A track, the progress bar's height.
 60     fn intrinsic_size(&self) -> Option<crate::scene::layout::Size> {
 61         Some(crate::scene::layout::Size::new(0.0, crate::layout::progressbar_height()))
 62     }
 63 }
 64 
 65 impl Paint for UsageBar {
 66     fn color(&self) -> [f32; 4] {
 67         [0.0, 0.0, 0.0, 0.0]
 68     }
 69 
 70     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
 71         if self.recessed() {
 72             // The ProgressBar's recessed composition: fill on the well floor, then
 73             // the carve, rounded like the sliders' tracks.
 74             let radius = crate::layout::slider_corner_radius();
 75             let depth = crate::layout::bevel_width().min(rect.height * 0.2);
 76             let inset = depth;
 77             let floor = Rect { x: rect.x + inset, y: rect.y + inset, width: rect.width - 2.0 * inset, height: rect.height - 2.0 * inset };
 78             let fill_w = floor.width * self.value;
 79             if fill_w > 0.0 {
 80                 ctx.rounded_rect(Rect { width: fill_w, ..floor }, radius.min(floor.height / 2.0), (true, true, true, true), self.fill_color);
 81             }
 82             let (well, radii) = crate::layout::carve_inside(rect, (radius, radius, radius, radius), depth);
 83             ctx.recess(well, radii, depth);
 84             return;
 85         }
 86         // The flat style: the ProgressBar's rounded track and fill.
 87         let radius = crate::layout::slider_corner_radius();
 88         ctx.rounded_rect(rect, radius, (true, true, true, true), self.bg_color);
 89         let fill_w = rect.width * self.value;
 90         if fill_w > 0.0 {
 91             ctx.rounded_rect(Rect { width: fill_w, ..rect }, radius.min(rect.height / 2.0), (true, true, true, true), self.fill_color);
 92         }
 93     }
 94 }
 95 
 96 impl Input for UsageBar {}
 97 
 98 #[cfg(test)]
 99 mod tests {
100     use super::*;
101     use crate::widget::WidgetHost;
102 
103     /// The flat style is the ProgressBar's: a full-width rounded track, then a fill scaled by
104     /// the clamped value with its radius clamped to half the height — on the rounded getter,
105     /// nothing on the plain one (apps read both).
106     #[test]
107     fn flat_style_is_rounded_track_then_fill() {
108         let mut bar = UsageBar::new(0.5).with_recessed(false).with_colors([0.1, 0.2, 0.3, 1.0], [0.4, 0.5, 0.6, 1.0]);
109         WidgetHost::set_rect(&mut bar, 12.0, 30.0, 200.0, 8.0);
110         let radius = crate::layout::slider_corner_radius();
111         let all = (true, true, true, true);
112         assert_eq!(
113             WidgetHost::all_rounded_quads(&bar, &crate::widget::UiContext::new()),
114             vec![
115                 (12.0, 30.0, 200.0, 8.0, radius, [0.4, 0.5, 0.6, 1.0], all),
116                 (12.0, 30.0, 100.0, 8.0, radius.min(4.0), [0.1, 0.2, 0.3, 1.0], all),
117             ],
118         );
119         assert!(WidgetHost::extra_quads(&bar).is_empty(), "no plain quad leaks to flat hosts");
120     }
121 
122     /// Recessed: no bg quad at all — the fill on the floor, then the carve.
123     #[test]
124     fn recessed_style_is_fill_then_carve() {
125         use crate::scene::paint::{PaintCtx, Prim};
126         let bar = UsageBar::new(0.5).with_recessed(true);
127         let mut pc = PaintCtx::new();
128         Paint::paint(bar.inner(), Rect { x: 0.0, y: 0.0, width: 100.0, height: 16.0 }, &mut pc);
129         let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
130         assert_eq!(prims.len(), 2, "{prims:?}");
131         assert!(matches!(prims[0], Prim::RoundedRect { .. }));
132         assert!(matches!(prims[1], Prim::Recess { .. }));
133         assert!(WidgetHost::extra_quads(&bar).is_empty(), "no plain bg quad leaks to flat hosts");
134     }
135 
136     #[test]
137     fn value_clamps_on_both_paths() {
138         let bar = UsageBar::new(7.0);
139         assert_eq!(bar.value, 1.0, "constructor clamps");
140         let mut bar = UsageBar::new(0.5);
141         bar.set_value(-3.0);
142         assert_eq!(bar.value, 0.0, "setter clamps (through Deref)");
143     }
144 }