Skip to main content

fui/controls/
progress_bar.rs

1use super::*;
2use crate::node::WeakFlexBox;
3use crate::Border;
4
5const PROGRESS_BORDER_WIDTH: f32 = 1.0;
6
7fn inset_radius(radius: f32) -> f32 {
8    (radius - PROGRESS_BORDER_WIDTH).max(0.0)
9}
10
11#[derive(Clone, Copy)]
12struct ResolvedProgressBarSizing {
13    length: f32,
14    thickness: f32,
15}
16
17fn resolve_sizing(sizing: Option<ProgressBarSizing>) -> ResolvedProgressBarSizing {
18    let sizing = sizing.unwrap_or_default();
19    ResolvedProgressBarSizing {
20        length: if sizing.has_length() {
21            sizing.length_px()
22        } else {
23            PROGRESS_LENGTH
24        },
25        thickness: if sizing.has_thickness() {
26            sizing.thickness_px()
27        } else {
28            PROGRESS_THICKNESS
29        },
30    }
31}
32
33#[derive(Clone)]
34pub struct ProgressBar {
35    root: FlexBox,
36    fill: FlexBox,
37    min: Rc<Cell<f32>>,
38    max: Rc<Cell<f32>>,
39    value: Rc<Cell<f32>>,
40    orientation: Rc<Cell<Orientation>>,
41    sizing_value: Rc<Cell<Option<ProgressBarSizing>>>,
42    colors_value: Rc<Cell<Option<ProgressBarColors>>>,
43    weak_root: WeakFlexBox,
44    weak_fill: WeakFlexBox,
45}
46
47impl Default for ProgressBar {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl ProgressBar {
54    pub fn new() -> Self {
55        let root = flex_box();
56        let fill = flex_box();
57        root.semantic_orientation(Orientation::Horizontal)
58            .semantic_value_range(0.0, 0.0, 100.0)
59            .child(&fill);
60        let control = Self {
61            weak_root: root.downgrade(),
62            weak_fill: fill.downgrade(),
63            root,
64            fill,
65            min: Rc::new(Cell::new(0.0)),
66            max: Rc::new(Cell::new(100.0)),
67            value: Rc::new(Cell::new(0.0)),
68            orientation: Rc::new(Cell::new(Orientation::Horizontal)),
69            sizing_value: Rc::new(Cell::new(None)),
70            colors_value: Rc::new(Cell::new(None)),
71        };
72        control.install_visual_subscriptions();
73        control.set_value(0.0);
74        control.handle_theme_changed();
75        control
76    }
77
78    pub fn min(&self, value: f32) -> &Self {
79        self.min.set(value);
80        if self.max.get() < value {
81            self.max.set(value);
82        }
83        self.set_value(self.value.get());
84        self
85    }
86
87    pub fn max(&self, value: f32) -> &Self {
88        self.max.set(value);
89        if self.min.get() > value {
90            self.min.set(value);
91        }
92        self.set_value(self.value.get());
93        self
94    }
95
96    pub fn value(&self, value: f32) -> &Self {
97        self.set_value(value);
98        self
99    }
100
101    pub fn length(&self, value: f32) -> &Self {
102        let value = clamp_direct_dimension("length", value);
103        let current = self.sizing_value.get().unwrap_or_default();
104        let mut sizing = ProgressBarSizing::new().length(value);
105        if current.has_thickness() {
106            sizing = sizing.thickness(current.thickness_px());
107        }
108        self.sizing_value.set(Some(sizing));
109        self.sync_geometry();
110        self
111    }
112
113    pub fn thickness(&self, value: f32) -> &Self {
114        let value = clamp_direct_dimension("thickness", value);
115        let current = self.sizing_value.get().unwrap_or_default();
116        let mut sizing = ProgressBarSizing::new().thickness(value);
117        if current.has_length() {
118            sizing = sizing.length(current.length_px());
119        }
120        self.sizing_value.set(Some(sizing));
121        self.sync_geometry();
122        self.sync_visual_state();
123        self
124    }
125
126    pub fn orientation(&self, orientation: Orientation) -> &Self {
127        let orientation = if orientation == Orientation::Vertical {
128            Orientation::Vertical
129        } else {
130            Orientation::Horizontal
131        };
132        self.orientation.set(orientation);
133        self.root.semantic_orientation(orientation);
134        self.sync_geometry();
135        self
136    }
137
138    pub fn sizing(&self, sizing: ProgressBarSizing) -> &Self {
139        self.sizing_value.set(Some(sizing));
140        self.sync_geometry();
141        self.sync_visual_state();
142        self
143    }
144
145    pub fn clear_sizing(&self) -> &Self {
146        self.sizing_value.set(None);
147        self.sync_geometry();
148        self.sync_visual_state();
149        self
150    }
151
152    pub fn colors(&self, colors: ProgressBarColors) -> &Self {
153        self.colors_value.set(Some(colors));
154        self.sync_visual_state();
155        self
156    }
157
158    pub fn clear_colors(&self) -> &Self {
159        self.colors_value.set(None);
160        self.sync_visual_state();
161        self
162    }
163
164    pub fn corner_radius(&self, radius: f32) -> &Self {
165        self.root.corner_radius(radius);
166        self.fill.corner_radius(inset_radius(radius));
167        self
168    }
169
170    pub fn current_value(&self) -> f32 {
171        self.value.get()
172    }
173
174    fn install_visual_subscriptions(&self) {
175        let weak_root = self.weak_root.clone();
176        let weak_fill = self.weak_fill.clone();
177        let sizing_value = self.sizing_value.clone();
178        let colors_value = self.colors_value.clone();
179        let guard = subscribe(move |_theme| {
180            let Some(root) = weak_root.upgrade() else {
181                return;
182            };
183            let Some(fill) = weak_fill.upgrade() else {
184                return;
185            };
186            sync_progress_visual_state(
187                &root,
188                &fill,
189                resolve_sizing(sizing_value.get()),
190                colors_value.get(),
191            );
192        });
193        self.root
194            .retained_node_ref()
195            .retain_attachment(Rc::new(guard));
196    }
197
198    fn handle_theme_changed(&self) {
199        self.sync_visual_state();
200        self.sync_geometry();
201    }
202
203    fn set_value(&self, value: f32) {
204        self.value.set(value.clamp(self.min.get(), self.max.get()));
205        self.sync_geometry();
206    }
207
208    fn sync_geometry(&self) {
209        sync_progress_geometry(
210            &self.root,
211            &self.fill,
212            self.min.get(),
213            self.max.get(),
214            self.value.get(),
215            self.orientation.get(),
216            resolve_sizing(self.sizing_value.get()),
217        );
218    }
219
220    fn sync_visual_state(&self) {
221        sync_progress_visual_state(
222            &self.root,
223            &self.fill,
224            resolve_sizing(self.sizing_value.get()),
225            self.colors_value.get(),
226        );
227    }
228}
229
230#[derive(Clone)]
231struct ProgressBarThemeTarget {
232    root: WeakFlexBox,
233    fill: WeakFlexBox,
234    min: Rc<Cell<f32>>,
235    max: Rc<Cell<f32>>,
236    value: Rc<Cell<f32>>,
237    orientation: Rc<Cell<Orientation>>,
238    sizing_value: Rc<Cell<Option<ProgressBarSizing>>>,
239    colors_value: Rc<Cell<Option<ProgressBarColors>>>,
240}
241
242impl ProgressBarThemeTarget {
243    fn upgrade(&self) -> Option<ProgressBar> {
244        Some(ProgressBar {
245            root: self.root.upgrade()?,
246            fill: self.fill.upgrade()?,
247            min: self.min.clone(),
248            max: self.max.clone(),
249            value: self.value.clone(),
250            orientation: self.orientation.clone(),
251            sizing_value: self.sizing_value.clone(),
252            colors_value: self.colors_value.clone(),
253            weak_root: self.root.clone(),
254            weak_fill: self.fill.clone(),
255        })
256    }
257}
258
259fn clamp_direct_dimension(method: &str, value: f32) -> f32 {
260    if value > 0.0 {
261        return value;
262    }
263    crate::logger::warn(
264        "Layout",
265        &format!("ProgressBar.{method}() received {value}; clamping to 1.0."),
266    );
267    1.0
268}
269
270impl Node for ProgressBar {
271    fn retained_node_ref(&self) -> NodeRef {
272        self.root.retained_node_ref()
273    }
274
275    fn build_self(&self) {
276        self.sync_visual_state();
277        self.sync_geometry();
278        self.root.build_self();
279    }
280}
281
282impl HasFlexBoxRoot for ProgressBar {
283    fn flex_box_root(&self) -> &FlexBox {
284        &self.root
285    }
286}
287
288impl ThemeBindable for ProgressBar {
289    fn theme_binding_node(&self) -> NodeRef {
290        self.root.retained_node_ref()
291    }
292
293    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
294        let target = ProgressBarThemeTarget {
295            root: self.weak_root.clone(),
296            fill: self.weak_fill.clone(),
297            min: self.min.clone(),
298            max: self.max.clone(),
299            value: self.value.clone(),
300            orientation: self.orientation.clone(),
301            sizing_value: self.sizing_value.clone(),
302            colors_value: self.colors_value.clone(),
303        };
304        Box::new(move || target.upgrade())
305    }
306}
307
308fn sync_progress_geometry(
309    root: &FlexBox,
310    fill: &FlexBox,
311    min: f32,
312    max: f32,
313    current_value: f32,
314    orientation: Orientation,
315    sizing: ResolvedProgressBarSizing,
316) {
317    let range = max - min;
318    let fraction = if range > 0.0 {
319        ((current_value - min) / range).clamp(0.0, 1.0)
320    } else {
321        0.0
322    };
323    let inset = PROGRESS_BORDER_WIDTH * 2.0;
324    let inner_length = (sizing.length - inset).max(0.0);
325    let inner_thickness = (sizing.thickness - inset).max(0.0);
326    let fill_length = inner_length * fraction;
327    if orientation == Orientation::Vertical {
328        root.flex_direction(FlexDirection::Column)
329            .justify_content(JustifyContent::End)
330            .width(sizing.thickness, Unit::Pixel)
331            .height(sizing.length, Unit::Pixel);
332        fill.width(inner_thickness, Unit::Pixel)
333            .height(fill_length, Unit::Pixel);
334    } else {
335        root.flex_direction(FlexDirection::Row)
336            .justify_content(JustifyContent::Start)
337            .width(sizing.length, Unit::Pixel)
338            .height(sizing.thickness, Unit::Pixel);
339        fill.width(fill_length, Unit::Pixel)
340            .height(inner_thickness, Unit::Pixel);
341    }
342    root.semantic_value_range(current_value, min, max)
343        .default_semantic_label(format!(
344            "Progress bar, value {}, range {} to {}",
345            current_value, min, max
346        ));
347}
348
349fn sync_progress_visual_state(
350    root: &FlexBox,
351    fill: &FlexBox,
352    sizing: ResolvedProgressBarSizing,
353    colors: Option<ProgressBarColors>,
354) {
355    let theme = current_theme();
356    let track_color = colors
357        .filter(ProgressBarColors::has_track)
358        .map(|colors| colors.track_color())
359        .unwrap_or(theme.colors.scrollbar_track);
360    let fill_color = colors
361        .filter(ProgressBarColors::has_fill)
362        .map(|colors| colors.fill_color())
363        .unwrap_or(theme.colors.accent);
364    root.apply_presenter_style(
365        crate::PresenterHostStyle::new()
366            .background(track_color)
367            .border(Border::solid(PROGRESS_BORDER_WIDTH, theme.colors.border))
368            .corners(crate::Corners::all(sizing.thickness * 0.5)),
369    );
370    let corners = root
371        .resolved_host_style()
372        .corners
373        .unwrap_or_else(|| crate::Corners::all(sizing.thickness * 0.5));
374    fill.corners(
375        inset_radius(corners.top_left),
376        inset_radius(corners.top_right),
377        inset_radius(corners.bottom_right),
378        inset_radius(corners.bottom_left),
379    )
380    .bg_color(fill_color);
381}