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 }