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.4K)

  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;
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 mut bar = ProgressBar::new(0.5).with_recessed(false);
106         WidgetHost::set_rect(&mut bar, 10.0, 20.0, 100.0, 8.0);
107 
108         let quads = crate::widget::shown_rounded_quads(&bar);
109         let radius = crate::layout::slider_corner_radius();
110         assert_eq!(quads.len(), 2, "track + fill");
111         assert_eq!(quads[0], (10.0, 20.0, 100.0, 8.0, radius, colors::progress_bg(), (true, true, true, true)));
112         assert_eq!(
113             quads[1],
114             (10.0, 20.0, 50.0, 8.0, radius.min(4.0), colors::progress_fill(), (true, true, true, true)),
115         );
116     }
117 
118     /// Value is clamped like the legacy widget: over 1.0 fills the whole track, 0 emits no fill.
119     #[test]
120     fn fill_clamps_to_track() {
121         let mut over = ProgressBar::new(2.0).with_recessed(false);
122         WidgetHost::set_rect(&mut over, 0.0, 0.0, 100.0, 8.0);
123         let quads = crate::widget::shown_rounded_quads(&over);
124         assert_eq!(quads[1].2, 100.0, "over-1 value fills the whole track");
125 
126         let mut empty = ProgressBar::new(0.0).with_recessed(false);
127         WidgetHost::set_rect(&mut empty, 0.0, 0.0, 100.0, 8.0);
128         assert_eq!(crate::widget::shown_rounded_quads(&empty).len(), 1, "zero value emits track only");
129     }
130 
131     /// The recessed style draws no track of its own: the fill on the well floor, then
132     /// the carve — and nothing else, so the plate below is the floor.
133     #[test]
134     fn recessed_style_is_fill_then_carve() {
135         use crate::scene::paint::Prim;
136         let bar = ProgressBar::new(0.5).with_recessed(true);
137         let mut pc = PaintCtx::new();
138         Paint::paint(bar.inner(), Rect { x: 0.0, y: 0.0, width: 100.0, height: 16.0 }, &mut pc);
139         let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
140         assert_eq!(prims.len(), 2, "fill + carve: {prims:?}");
141         assert!(matches!(prims[0], Prim::RoundedRect { .. }), "the fill first");
142         assert!(matches!(prims[1], Prim::Recess { .. }), "then the well");
143         if let Prim::RoundedRect { rect, .. } = &prims[0] {
144             assert!(rect.x > 0.0 && rect.width < 50.0, "the fill is inset onto the floor: {rect:?}");
145         }
146     }
147 
148     /// The one detached-label convention: the assigned rect is the whole block, the
149     /// label strip at its top and the track painted below it (config-independent: the
150     /// strip is read back from the widget).
151     #[test]
152     fn label_strip_heads_the_block_and_insets_paint() {
153         let mut bar = ProgressBar::new(0.5).with_recessed(false).with_label("Progress");
154         let offset = WidgetHost::label_strip(&bar);
155         assert!(offset > 0.0, "a labeled bar carries a strip");
156         WidgetHost::set_rect(&mut bar, 0.0, 10.0, 100.0, 8.0 + offset);
157 
158         let (_, y, _, h) = WidgetHost::rect(&bar);
159         assert_eq!(h, 8.0 + offset, "the rect is the block it was given");
160         assert_eq!(y, 10.0, "origin is unchanged");
161 
162         let quads = crate::widget::shown_rounded_quads(&bar);
163         assert_eq!(quads[0].1, 10.0 + offset, "track is painted below the label region");
164         assert_eq!(quads[0].3, 8.0, "track keeps the assigned height");
165 
166         // preferred_height is the content height; the label strip is `label_strip`.
167         assert_eq!(crate::widget::WidgetHostExt::preferred_height(&bar), Some(crate::layout::progressbar_height()));
168         assert_eq!(WidgetHost::label_strip(&bar), offset);
169         // Runtime type-name matching still sees "ProgressBar", not Adapted<..>.
170         assert_eq!(WidgetHost::type_name(&bar), "ProgressBar");
171     }
172 }