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

src/widget/display/progress_bar.rs (7.5K)

  1 //! The first widget migrated off `WidgetHost` onto the narrow traits (Phase 5c). `ProgressBar`
  2 //! implements only [`Layout`] + [`Paint`] + [`Input`]; [`ProgressBar::new`] returns it already
  3 //! wrapped in [`Adapted`], so construction sites (`Box::new(ProgressBar::new(0.65))`, optionally
  4 //! `.with_label(..)`) are unchanged by the migration.
  5 
  6 use crate::colors;
  7 use crate::scene::layout::{Rect, Size};
  8 use crate::scene::paint::PaintCtx;
  9 use crate::widget::{Adapted, Input, Layout, Paint};
 10 
 11 pub struct ProgressBar {
 12     value: f32,
 13     /// Recessed-track style, the Slider's: the track is a well carved into the
 14     /// plate below — no track fill, the plate is the floor — with the progress
 15     /// fill inset onto that floor. Defaults to `control_relief()`; the flat
 16     /// style keeps the filled, rounded track.
 17     recessed: Option<bool>,
 18 }
 19 
 20 impl ProgressBar {
 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<ProgressBar> {
 29         Adapted::new(ProgressBar { value, recessed: None })
 30     }
 31 }
 32 
 33 impl Adapted<ProgressBar> {
 34     /// Recessed style: see the `recessed` field.
 35     pub fn with_recessed(mut self, recessed: bool) -> Self {
 36         self.recessed = Some(recessed);
 37         self
 38     }
 39 }
 40 
 41 impl Layout for ProgressBar {
 42     fn intrinsic_size(&self) -> Option<Size> {
 43         // Height is the bar's own; width comes from the container (legacy `preferred_height`).
 44         Some(Size::new(0.0, crate::layout::progressbar_height()))
 45     }
 46 }
 47 
 48 impl Paint for ProgressBar {
 49     fn color(&self) -> [f32; 4] {
 50         colors::progress_bg()
 51     }
 52 
 53     fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
 54         Some((crate::layout::slider_corner_radius(), (true, true, true, true)))
 55     }
 56 
 57     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
 58         let radius = crate::layout::slider_corner_radius();
 59         if self.recessed() {
 60             // The Slider's recessed composition: the fill sits on the well's flat
 61             // floor (past the wall's inner half-span), the carve comes after it so
 62             // the walls' shading modulates what they cross.
 63             let depth = crate::layout::bevel_width().min(rect.height * 0.2);
 64             let inset = depth;
 65             let floor = Rect { x: rect.x + inset, y: rect.y + inset, width: rect.width - 2.0 * inset, height: rect.height - 2.0 * inset };
 66             let fill_w = floor.width * self.value.clamp(0.0, 1.0);
 67             if fill_w > 0.0 {
 68                 ctx.rounded_rect(
 69                     Rect { width: fill_w, ..floor },
 70                     radius.min(floor.height / 2.0),
 71                     (true, true, true, true),
 72                     colors::progress_fill(),
 73                 );
 74             }
 75             let (well, radii) = crate::layout::carve_inside(rect, (radius, radius, radius, radius), depth);
 76             ctx.recess(well, radii, depth);
 77             return;
 78         }
 79         // Track.
 80         ctx.rounded_rect(rect, radius, (true, true, true, true), colors::progress_bg());
 81         // Fill.
 82         let fill_w = rect.width * self.value.clamp(0.0, 1.0);
 83         if fill_w > 0.0 {
 84             ctx.rounded_rect(
 85                 Rect { x: rect.x, y: rect.y, width: fill_w, height: rect.height },
 86                 radius.min(rect.height / 2.0),
 87                 (true, true, true, true),
 88                 colors::progress_fill(),
 89             );
 90         }
 91     }
 92 }
 93 
 94 impl Input for ProgressBar {}
 95 
 96 #[cfg(test)]
 97 mod tests {
 98     use super::*;
 99     use crate::widget::{WidgetHost, UiContext};
100 
101     /// The reverse bridge reproduces the legacy `all_rounded_quads` output: track quad at the
102     /// content rect, fill quad at `w * value` with the radius clamped to half the height.
103     #[test]
104     fn reverse_bridge_matches_legacy_geometry() {
105         let ctx = UiContext::new();
106         let mut bar = ProgressBar::new(0.5).with_recessed(false);
107         WidgetHost::set_rect(&mut bar, 10.0, 20.0, 100.0, 8.0);
108 
109         let quads = WidgetHost::all_rounded_quads(&bar, &ctx);
110         let radius = crate::layout::slider_corner_radius();
111         assert_eq!(quads.len(), 2, "track + fill");
112         assert_eq!(quads[0], (10.0, 20.0, 100.0, 8.0, radius, colors::progress_bg(), (true, true, true, true)));
113         assert_eq!(
114             quads[1],
115             (10.0, 20.0, 50.0, 8.0, radius.min(4.0), colors::progress_fill(), (true, true, true, true)),
116         );
117     }
118 
119     /// Value is clamped like the legacy widget: over 1.0 fills the whole track, 0 emits no fill.
120     #[test]
121     fn fill_clamps_to_track() {
122         let ctx = UiContext::new();
123         let mut over = ProgressBar::new(2.0).with_recessed(false);
124         WidgetHost::set_rect(&mut over, 0.0, 0.0, 100.0, 8.0);
125         let quads = WidgetHost::all_rounded_quads(&over, &ctx);
126         assert_eq!(quads[1].2, 100.0, "over-1 value fills the whole track");
127 
128         let mut empty = ProgressBar::new(0.0).with_recessed(false);
129         WidgetHost::set_rect(&mut empty, 0.0, 0.0, 100.0, 8.0);
130         assert_eq!(WidgetHost::all_rounded_quads(&empty, &ctx).len(), 1, "zero value emits track only");
131     }
132 
133     /// The recessed style draws no track of its own: the fill on the well floor, then
134     /// the carve — and nothing else, so the plate below is the floor.
135     #[test]
136     fn recessed_style_is_fill_then_carve() {
137         use crate::scene::paint::Prim;
138         let bar = ProgressBar::new(0.5).with_recessed(true);
139         let mut pc = PaintCtx::new();
140         Paint::paint(bar.inner(), Rect { x: 0.0, y: 0.0, width: 100.0, height: 16.0 }, &mut pc);
141         let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
142         assert_eq!(prims.len(), 2, "fill + carve: {prims:?}");
143         assert!(matches!(prims[0], Prim::RoundedRect { .. }), "the fill first");
144         assert!(matches!(prims[1], Prim::Recess { .. }), "then the well");
145         if let Prim::RoundedRect { rect, .. } = &prims[0] {
146             assert!(rect.x > 0.0 && rect.width < 50.0, "the fill is inset onto the floor: {rect:?}");
147         }
148     }
149 
150     /// The one detached-label convention: the assigned rect is the whole block, the
151     /// label strip at its top and the track painted below it (config-independent: the
152     /// strip is read back from the widget).
153     #[test]
154     fn label_strip_heads_the_block_and_insets_paint() {
155         let ctx = UiContext::new();
156         let mut bar = ProgressBar::new(0.5).with_recessed(false).with_label("Progress");
157         let offset = WidgetHost::label_strip(&bar);
158         assert!(offset > 0.0, "a labeled bar carries a strip");
159         WidgetHost::set_rect(&mut bar, 0.0, 10.0, 100.0, 8.0 + offset);
160 
161         let (_, y, _, h) = WidgetHost::rect(&bar);
162         assert_eq!(h, 8.0 + offset, "the rect is the block it was given");
163         assert_eq!(y, 10.0, "origin is unchanged");
164 
165         let quads = WidgetHost::all_rounded_quads(&bar, &ctx);
166         assert_eq!(quads[0].1, 10.0 + offset, "track is painted below the label region");
167         assert_eq!(quads[0].3, 8.0, "track keeps the assigned height");
168 
169         // preferred_height is the content height; the label strip is `label_strip`.
170         assert_eq!(WidgetHost::preferred_height(&bar), Some(crate::layout::progressbar_height()));
171         assert_eq!(WidgetHost::label_strip(&bar), offset);
172         // Runtime type-name matching still sees "ProgressBar", not Adapted<..>.
173         assert_eq!(WidgetHost::type_name(&bar), "ProgressBar");
174     }
175 }