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 }