Skip to main content

zest_widget/widget/
progress_bar.rs

1//! Passive progress bar: a track with a filled portion reflecting a value.
2//!
3//! Non-interactive — ignores touch entirely. The host owns the value and
4//! rebuilds the widget each frame. The fraction shown is `(value - min) /
5//! (max - min)`, clamped to `0.0..=1.0`. The default range is `0.0..=1.0`,
6//! so `ProgressBar::new(0.5)` is half full.
7//!
8//! Colors default to the theme: the filled portion uses `accent.base`
9//! (override with [`color`](ProgressBar::color)) and the track uses
10//! `background.divider`.
11
12use super::Widget;
13use core::marker::PhantomData;
14use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
15use zest_core::{Constraints, Length, RenderError, Renderer, TouchPhase};
16use zest_theme::Theme;
17
18/// Default bar height in pixels.
19const BAR_H: u32 = 10;
20/// Default intrinsic width when sized `Shrink`.
21const INTRINSIC_W: u32 = 160;
22
23/// Passive progress bar reflecting a value within a range.
24pub struct ProgressBar<C: PixelColor, M: Clone> {
25    rect: Rectangle,
26    value: f32,
27    min: f32,
28    max: f32,
29    color: Option<C>,
30    width: Length,
31    height: Length,
32    _phantom: PhantomData<M>,
33}
34
35impl<C: PixelColor, M: Clone> ProgressBar<C, M> {
36    /// New progress bar showing `value`. Default range is `0.0..=1.0`.
37    pub fn new(value: f32) -> Self {
38        Self {
39            rect: Rectangle::zero(),
40            value,
41            min: 0.0,
42            max: 1.0,
43            color: None,
44            width: Length::Fill,
45            height: Length::Fixed(BAR_H),
46            _phantom: PhantomData,
47        }
48    }
49
50    /// Inclusive value range mapped to the fill fraction.
51    #[must_use]
52    pub fn range(mut self, min: f32, max: f32) -> Self {
53        self.min = min;
54        self.max = max;
55        self
56    }
57
58    /// Explicit fill color (default: `theme.accent.base`).
59    #[must_use]
60    pub fn color(mut self, color: C) -> Self {
61        self.color = Some(color);
62        self
63    }
64
65    /// Width sizing intent.
66    #[must_use]
67    pub fn width(mut self, width: impl Into<Length>) -> Self {
68        self.width = width.into();
69        self
70    }
71
72    /// Height sizing intent.
73    #[must_use]
74    pub fn height(mut self, height: impl Into<Length>) -> Self {
75        self.height = height.into();
76        self
77    }
78
79    /// Fraction (0.0..=1.0) of the value within the range.
80    fn fraction(&self) -> f32 {
81        if self.max <= self.min {
82            0.0
83        } else {
84            ((self.value - self.min) / (self.max - self.min)).clamp(0.0, 1.0)
85        }
86    }
87}
88
89impl<C: PixelColor, M: Clone> Widget<C, M> for ProgressBar<C, M> {
90    fn measure(&mut self, constraints: Constraints) -> Size {
91        let w = self.width.resolve(INTRINSIC_W, constraints.max.width);
92        let h = self.height.resolve(BAR_H, constraints.max.height);
93        constraints.clamp(Size::new(w, h))
94    }
95
96    fn preferred_size(&self) -> (Length, Length) {
97        (self.width, self.height)
98    }
99
100    fn arrange(&mut self, rect: Rectangle) {
101        self.rect = rect;
102    }
103
104    fn rect(&self) -> Rectangle {
105        self.rect
106    }
107
108    fn handle_touch(&mut self, _point: Point, _phase: TouchPhase) -> Option<M> {
109        None
110    }
111
112    fn draw<'t>(
113        &self,
114        renderer: &mut dyn Renderer<C>,
115        theme: &Theme<'t, C>,
116    ) -> Result<(), RenderError> {
117        // Track.
118        renderer.fill_rect(self.rect, theme.background.divider)?;
119
120        // Filled portion.
121        let fill_w = (self.rect.size.width as f32 * self.fraction()) as u32;
122        if fill_w > 0 {
123            let fill_color = self.color.unwrap_or(theme.accent.base);
124            let filled =
125                Rectangle::new(self.rect.top_left, Size::new(fill_w, self.rect.size.height));
126            renderer.fill_rect(filled, fill_color)?;
127        }
128
129        Ok(())
130    }
131}