Skip to main content

fui/controls/
progress_bar.rs

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