1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum BarMode {
12 Normal,
14 Symmetrical,
16 Range,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum BarOrientation {
23 Auto,
25 Horizontal,
27 Vertical,
29}
30
31pub 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 pub style: Style,
42 pub indicator_color: Color,
44}
45
46impl Bar {
47 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 pub fn value(&self) -> i32 {
64 self.value
65 }
66
67 pub fn set_value(&mut self, value: i32) {
69 self.value = self.clamp_to_range(value);
70 }
71
72 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 pub fn set_start_value(&mut self, value: i32) {
86 self.start_value = self.clamp_to_range(value);
87 }
88
89 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 pub fn min_value(&self) -> i32 {
102 self.min
103 }
104
105 pub fn max_value(&self) -> i32 {
107 self.max
108 }
109
110 pub fn set_mode(&mut self, mode: BarMode) {
112 self.mode = mode;
113 }
114
115 pub fn mode(&self) -> BarMode {
117 self.mode
118 }
119
120 pub fn set_orientation(&mut self, orientation: BarOrientation) {
122 self.orientation = orientation;
123 }
124
125 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}