Skip to main content

rlvgl_widgets/
bar.rs

1//! LVGL-parity integer bar widget.
2
3use rlvgl_core::draw::{draw_widget_bg, fill_rounded_rect};
4use rlvgl_core::event::Event;
5use rlvgl_core::renderer::Renderer;
6use rlvgl_core::style::Style;
7use rlvgl_core::widget::{Color, Rect, Widget};
8
9/// Filling behavior for a [`Bar`].
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum BarMode {
12    /// Fill from the configured minimum value to the current value.
13    Normal,
14    /// Fill from zero to the current value when zero is inside the range.
15    Symmetrical,
16    /// Fill from `start_value` to the current value.
17    Range,
18}
19
20/// Orientation for a [`Bar`].
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum BarOrientation {
23    /// Resolve from geometry: horizontal when width is at least height.
24    Auto,
25    /// Draw the indicator left-to-right along the horizontal axis.
26    Horizontal,
27    /// Draw the indicator bottom-to-top along the vertical axis.
28    Vertical,
29}
30
31/// LVGL-parity bar widget with normal, symmetrical, and range modes.
32pub struct Bar {
33    bounds: Rect,
34    min: i32,
35    max: i32,
36    value: i32,
37    start_value: i32,
38    mode: BarMode,
39    orientation: BarOrientation,
40    /// Background and border style for the bar track.
41    pub style: Style,
42    /// Color used for the active indicator region.
43    pub indicator_color: Color,
44}
45
46impl Bar {
47    /// Create a bar with `bounds` and a value range.
48    pub fn new(bounds: Rect, min: i32, max: i32) -> Self {
49        Self {
50            bounds,
51            min,
52            max,
53            value: min,
54            start_value: min,
55            mode: BarMode::Normal,
56            orientation: BarOrientation::Auto,
57            style: Style::default(),
58            indicator_color: Color(0, 0, 0, 255),
59        }
60    }
61
62    /// Current value, clamped to the configured range.
63    pub fn value(&self) -> i32 {
64        self.value
65    }
66
67    /// Set the current value, clamped to the configured range.
68    pub fn set_value(&mut self, value: i32) {
69        self.value = self.clamp_to_range(value);
70    }
71
72    /// Start value used by [`BarMode::Range`].
73    ///
74    /// Outside range mode this returns the configured minimum value, matching
75    /// LVGL's user-facing getter behavior.
76    pub fn start_value(&self) -> i32 {
77        if self.mode == BarMode::Range {
78            self.start_value
79        } else {
80            self.min
81        }
82    }
83
84    /// Set the start value used by [`BarMode::Range`].
85    pub fn set_start_value(&mut self, value: i32) {
86        self.start_value = self.clamp_to_range(value);
87    }
88
89    /// Set the bar's inclusive value range.
90    ///
91    /// Reversed ranges are supported: `min > max` reverses the numeric
92    /// direction without needing a separate direction flag.
93    pub fn set_range(&mut self, min: i32, max: i32) {
94        self.min = min;
95        self.max = max;
96        self.value = self.clamp_to_range(self.value);
97        self.start_value = self.clamp_to_range(self.start_value);
98    }
99
100    /// Configured minimum value.
101    pub fn min_value(&self) -> i32 {
102        self.min
103    }
104
105    /// Configured maximum value.
106    pub fn max_value(&self) -> i32 {
107        self.max
108    }
109
110    /// Set the bar filling mode.
111    pub fn set_mode(&mut self, mode: BarMode) {
112        self.mode = mode;
113    }
114
115    /// Return the current filling mode.
116    pub fn mode(&self) -> BarMode {
117        self.mode
118    }
119
120    /// Set the bar orientation.
121    pub fn set_orientation(&mut self, orientation: BarOrientation) {
122        self.orientation = orientation;
123    }
124
125    /// Return the configured orientation.
126    pub fn orientation(&self) -> BarOrientation {
127        self.orientation
128    }
129
130    fn resolved_orientation(&self) -> BarOrientation {
131        match self.orientation {
132            BarOrientation::Auto if self.bounds.width >= self.bounds.height => {
133                BarOrientation::Horizontal
134            }
135            BarOrientation::Auto => BarOrientation::Vertical,
136            orientation => orientation,
137        }
138    }
139
140    fn clamp_to_range(&self, value: i32) -> i32 {
141        if self.min <= self.max {
142            value.clamp(self.min, self.max)
143        } else {
144            value.clamp(self.max, self.min)
145        }
146    }
147
148    fn offset_for_value(&self, value: i32, length: i32) -> i32 {
149        if length <= 0 || self.min == self.max {
150            return 0;
151        }
152
153        let den = i64::from(self.max) - i64::from(self.min);
154        let den_abs = den.unsigned_abs() as i64;
155        let num = if den > 0 {
156            i64::from(value) - i64::from(self.min)
157        } else {
158            i64::from(self.min) - i64::from(value)
159        }
160        .clamp(0, den_abs);
161
162        ((num * i64::from(length)) / den_abs) as i32
163    }
164
165    fn indicator_offsets(&self, length: i32) -> Option<(i32, i32)> {
166        if self.min == self.max {
167            return None;
168        }
169
170        let value = self.offset_for_value(self.value, length);
171        let (start, end) = match self.mode {
172            BarMode::Normal => (0, value),
173            BarMode::Range => (self.offset_for_value(self.start_value, length), value),
174            BarMode::Symmetrical if self.range_crosses_zero() => {
175                (self.offset_for_value(0, length), value)
176            }
177            BarMode::Symmetrical => (0, value),
178        };
179        (start != end).then_some((start.min(end), start.max(end)))
180    }
181
182    fn range_crosses_zero(&self) -> bool {
183        (self.min <= 0 && self.max >= 0) || (self.max <= 0 && self.min >= 0)
184    }
185
186    fn indicator_rect(&self) -> Option<Rect> {
187        if self.bounds.width <= 0 || self.bounds.height <= 0 {
188            return None;
189        }
190
191        match self.resolved_orientation() {
192            BarOrientation::Horizontal | BarOrientation::Auto => {
193                let (start, end) = self.indicator_offsets(self.bounds.width)?;
194                Some(Rect {
195                    x: self.bounds.x + start,
196                    y: self.bounds.y,
197                    width: end - start,
198                    height: self.bounds.height,
199                })
200            }
201            BarOrientation::Vertical => {
202                let (start, end) = self.indicator_offsets(self.bounds.height)?;
203                Some(Rect {
204                    x: self.bounds.x,
205                    y: self.bounds.y + self.bounds.height - end,
206                    width: self.bounds.width,
207                    height: end - start,
208                })
209            }
210        }
211    }
212}
213
214impl Widget for Bar {
215    fn bounds(&self) -> Rect {
216        self.bounds
217    }
218
219    fn draw(&self, renderer: &mut dyn Renderer) {
220        draw_widget_bg(renderer, self.bounds, &self.style);
221
222        let color = self.indicator_color.with_alpha(self.style.alpha);
223        if color.3 == 0 {
224            return;
225        }
226        if let Some(rect) = self.indicator_rect() {
227            fill_rounded_rect(renderer, rect, color, self.style.radius);
228        }
229    }
230
231    fn handle_event(&mut self, _event: &Event) -> bool {
232        false
233    }
234
235    fn set_bounds(&mut self, bounds: Rect) {
236        self.bounds = bounds;
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn value_and_start_values_clamp_for_reversed_ranges() {
246        let mut bar = Bar::new(rect(0, 0, 100, 10), 100, 0);
247
248        bar.set_value(150);
249        assert_eq!(bar.value(), 100);
250        bar.set_value(-10);
251        assert_eq!(bar.value(), 0);
252
253        bar.set_start_value(75);
254        assert_eq!(bar.start_value(), 100);
255        bar.set_mode(BarMode::Range);
256        assert_eq!(bar.start_value(), 75);
257    }
258
259    #[test]
260    fn horizontal_normal_indicator_uses_value_fraction() {
261        let mut bar = Bar::new(rect(10, 20, 100, 8), 0, 100);
262        bar.set_value(40);
263
264        assert_eq!(bar.indicator_rect(), Some(rect(10, 20, 40, 8)));
265    }
266
267    #[test]
268    fn vertical_range_indicator_uses_bottom_origin() {
269        let mut bar = Bar::new(rect(0, 0, 8, 100), 0, 100);
270        bar.set_orientation(BarOrientation::Vertical);
271        bar.set_mode(BarMode::Range);
272        bar.set_start_value(25);
273        bar.set_value(75);
274
275        assert_eq!(bar.indicator_rect(), Some(rect(0, 25, 8, 50)));
276    }
277
278    #[test]
279    fn symmetrical_indicator_uses_zero_baseline_when_inside_range() {
280        let mut bar = Bar::new(rect(0, 0, 100, 8), -100, 100);
281        bar.set_mode(BarMode::Symmetrical);
282        bar.set_value(-50);
283
284        assert_eq!(bar.indicator_rect(), Some(rect(25, 0, 25, 8)));
285    }
286
287    #[test]
288    fn set_bounds_adopts_layout_bounds() {
289        let mut bar = Bar::new(rect(0, 0, 10, 2), 0, 10);
290        bar.set_bounds(rect(3, 4, 20, 5));
291
292        assert_eq!(bar.bounds(), rect(3, 4, 20, 5));
293    }
294
295    fn rect(x: i32, y: i32, width: i32, height: i32) -> Rect {
296        Rect {
297            x,
298            y,
299            width,
300            height,
301        }
302    }
303}