1use alloc::string::String;
18use core::fmt::Write as FmtWrite;
19use rlvgl_core::draw::draw_widget_bg;
20use rlvgl_core::event::Event;
21use rlvgl_core::font::{FontMetrics, WidgetFont, shape_text_ltr};
22use rlvgl_core::renderer::{ClipRenderer, Renderer};
23use rlvgl_core::style::Style;
24use rlvgl_core::widget::{Color, Rect, Widget};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum SpinboxDigitStepDirection {
29 Right,
31 Left,
33}
34
35pub struct Spinbox {
51 bounds: Rect,
52 value: i32,
53 min: i32,
54 max: i32,
55 rollover: bool,
56 digit_count: u8,
57 separator_position: u8,
58 step: i32,
59 digit_step_direction: SpinboxDigitStepDirection,
60 pub style: Style,
62 pub text_color: Color,
64 pub cursor_color: Color,
66 font: WidgetFont,
69}
70
71impl Spinbox {
72 pub fn new(bounds: Rect) -> Self {
74 Self {
75 bounds,
76 value: 0,
77 min: -99_999,
78 max: 99_999,
79 rollover: false,
80 digit_count: 5,
81 separator_position: 0,
82 step: 1,
83 digit_step_direction: SpinboxDigitStepDirection::Right,
84 style: Style::default(),
85 text_color: Color(20, 20, 20, 255),
86 cursor_color: Color(0, 120, 215, 255),
87 font: WidgetFont::new(),
88 }
89 }
90
91 pub fn set_value(&mut self, value: i32) {
93 self.value = value.clamp(self.min, self.max);
94 }
95
96 pub fn value(&self) -> i32 {
98 self.value
99 }
100
101 pub fn set_range(&mut self, min: i32, max: i32) {
103 self.min = min;
104 self.max = max;
105 self.value = self.value.clamp(self.min, self.max);
106 }
107
108 pub fn min_value(&self) -> i32 {
110 self.min
111 }
112
113 pub fn max_value(&self) -> i32 {
115 self.max
116 }
117
118 pub fn set_rollover(&mut self, rollover: bool) {
120 self.rollover = rollover;
121 }
122
123 pub fn rollover(&self) -> bool {
125 self.rollover
126 }
127
128 pub fn set_digit_format(&mut self, digit_count: u8, separator_position: u8) {
136 self.digit_count = digit_count.clamp(1, 10);
137 self.separator_position = separator_position;
138 let max_repr = max_for_digits(self.digit_count);
139 self.min = self.min.clamp(-max_repr, max_repr);
140 self.max = self.max.clamp(-max_repr, max_repr);
141 self.value = self.value.clamp(self.min, self.max);
142 }
143
144 pub fn digit_count(&self) -> u8 {
146 self.digit_count
147 }
148
149 pub fn separator_position(&self) -> u8 {
151 self.separator_position
152 }
153
154 pub fn set_step(&mut self, step: i32) {
156 self.step = step.max(1);
157 }
158
159 pub fn step(&self) -> i32 {
161 self.step
162 }
163
164 pub fn set_digit_step_direction(&mut self, dir: SpinboxDigitStepDirection) {
166 self.digit_step_direction = dir;
167 }
168
169 pub fn digit_step_direction(&self) -> SpinboxDigitStepDirection {
171 self.digit_step_direction
172 }
173
174 pub fn step_next(&mut self) {
178 let cap = max_step(self.digit_count);
179 if self.step < cap {
180 self.step = (self.step.saturating_mul(10)).min(cap);
181 }
182 }
183
184 pub fn step_prev(&mut self) {
188 if self.step > 1 {
189 self.step /= 10;
190 }
191 }
192
193 pub fn increment(&mut self) {
198 let v = self.value;
199 let s = self.step;
200 let new = if v < 0 && v.saturating_add(s) > 0 {
201 0
202 } else {
203 v.saturating_add(s)
204 };
205 if new > self.max {
206 self.value = if self.rollover { self.min } else { self.max };
207 } else {
208 self.value = new;
209 }
210 }
211
212 pub fn decrement(&mut self) {
217 let v = self.value;
218 let s = self.step;
219 let new = if v > 0 && v.saturating_sub(s) < 0 {
220 0
221 } else {
222 v.saturating_sub(s)
223 };
224 if new < self.min {
225 self.value = if self.rollover { self.max } else { self.min };
226 } else {
227 self.value = new;
228 }
229 }
230
231 pub fn on_arrow_up(&mut self) {
233 self.increment();
234 }
235
236 pub fn on_arrow_down(&mut self) {
238 self.decrement();
239 }
240
241 pub fn on_arrow_right(&mut self) {
243 match self.digit_step_direction {
244 SpinboxDigitStepDirection::Right => self.step_prev(),
245 SpinboxDigitStepDirection::Left => self.step_next(),
246 }
247 }
248
249 pub fn on_arrow_left(&mut self) {
251 match self.digit_step_direction {
252 SpinboxDigitStepDirection::Right => self.step_next(),
253 SpinboxDigitStepDirection::Left => self.step_prev(),
254 }
255 }
256
257 pub fn text(&self) -> String {
265 let dc = self.digit_count as usize;
266 let sep = self.separator_position as usize;
267 let show_sign = self.min < 0;
268 let abs_val = self.value.unsigned_abs() as u64;
269
270 let mut digits = [b'0'; 10];
272 let mut temp = abs_val;
273 for i in (0..dc).rev() {
274 digits[i] = b'0' + (temp % 10) as u8;
275 temp /= 10;
276 }
277
278 let mut s = String::new();
279
280 if show_sign {
281 let sign = if self.value < 0 { '-' } else { '+' };
282 s.push(sign);
283 }
284
285 let sep_valid = sep > 0 && sep < dc;
286 let sep_insert_before = dc - sep; for (i, &digit) in digits[..dc].iter().enumerate() {
289 if sep_valid && i == sep_insert_before {
290 s.push('.');
291 }
292 s.push(digit as char);
293 }
294
295 s
296 }
297
298 pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
301 self.font.set(font);
302 }
303
304 fn selected_char_index(&self) -> usize {
309 let dc = self.digit_count as usize;
310 let sep = self.separator_position as usize;
311 let show_sign = self.min < 0;
312
313 let digit_from_right = {
315 let mut d = 0usize;
316 let mut s = self.step;
317 while s > 1 {
318 s /= 10;
319 d += 1;
320 }
321 d
322 };
323 let digit_from_left = dc.saturating_sub(1).saturating_sub(digit_from_right);
325
326 let sep_valid = sep > 0 && sep < dc;
328 let sep_insert_before = dc - sep;
329 let sep_before = if sep_valid && digit_from_left >= sep_insert_before {
330 1
331 } else {
332 0
333 };
334 let sign_offset = if show_sign { 1 } else { 0 };
335
336 sign_offset + digit_from_left + sep_before
337 }
338}
339
340fn max_for_digits(digit_count: u8) -> i32 {
341 let n = digit_count.min(9) as u32; let mut v: i32 = 1;
343 for _ in 0..n {
344 v = v.saturating_mul(10);
345 }
346 v.saturating_sub(1)
347}
348
349fn max_step(digit_count: u8) -> i32 {
350 let n = digit_count.saturating_sub(1).min(9) as u32;
352 let mut v: i32 = 1;
353 for _ in 0..n {
354 v = v.saturating_mul(10);
355 }
356 v
357}
358
359impl Widget for Spinbox {
360 fn bounds(&self) -> Rect {
361 self.bounds
362 }
363
364 fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
365 Some(&mut self.font)
366 }
367
368 fn set_bounds(&mut self, bounds: Rect) {
369 self.bounds = bounds;
370 }
371
372 fn draw(&self, renderer: &mut dyn Renderer) {
373 draw_widget_bg(renderer, self.bounds, &self.style);
374
375 let text = self.text();
376 let font = self.font.resolve();
377 let metrics = font.line_metrics();
378 let baseline = self.bounds.y + metrics.ascent as i32;
379 let shaped = shape_text_ltr(font, &text, (self.bounds.x, baseline), 0);
380 let color = self.text_color.with_alpha(self.style.alpha);
381
382 {
383 let mut clipped = ClipRenderer::new(renderer, self.bounds);
384 clipped.draw_text_shaped(&shaped, (0, 0), color);
385 }
386
387 let char_idx = self.selected_char_index();
389 if let Some(glyph) = shaped.glyphs.get(char_idx) {
390 let ext = glyph.extent();
391 let cursor_rect = Rect {
392 x: ext.x,
393 y: ext.y + ext.height,
394 width: ext.width.max(1),
395 height: 2,
396 };
397 renderer.fill_rect(cursor_rect, self.cursor_color.with_alpha(self.style.alpha));
398 }
399 }
400
401 fn handle_event(&mut self, _event: &Event) -> bool {
402 false
405 }
406}
407
408#[allow(unused_imports)]
414use FmtWrite as _;
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 fn rect(x: i32, y: i32, w: i32, h: i32) -> Rect {
421 Rect {
422 x,
423 y,
424 width: w,
425 height: h,
426 }
427 }
428
429 #[test]
430 fn default_value_and_range() {
431 let sb = Spinbox::new(rect(0, 0, 80, 20));
432 assert_eq!(sb.value(), 0);
433 assert_eq!(sb.min_value(), -99_999);
434 assert_eq!(sb.max_value(), 99_999);
435 assert_eq!(sb.digit_count(), 5);
436 assert_eq!(sb.separator_position(), 0);
437 assert_eq!(sb.step(), 1);
438 assert!(!sb.rollover());
439 }
440
441 #[test]
442 fn set_value_clamps_to_range() {
443 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
444 sb.set_value(200_000);
445 assert_eq!(sb.value(), 99_999);
446 sb.set_value(-200_000);
447 assert_eq!(sb.value(), -99_999);
448 }
449
450 #[test]
451 fn set_range_clamps_value() {
452 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
453 sb.set_value(50);
454 sb.set_range(0, 30);
455 assert_eq!(sb.value(), 30);
456 }
457
458 #[test]
459 fn digit_format_shows_plus_and_leading_zeros() {
460 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
461 sb.set_value(42);
462 let t = sb.text();
463 assert!(t.starts_with('+'), "expected '+' prefix, got: {t}");
464 assert!(t.contains("00042"), "expected leading zeros in: {t}");
465 }
466
467 #[test]
468 fn positive_only_range_omits_sign() {
469 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
470 sb.set_range(0, 99_999);
471 sb.set_value(5);
472 let t = sb.text();
473 assert!(
474 !t.starts_with('+') && !t.starts_with('-'),
475 "unexpected sign prefix in: {t}"
476 );
477 assert!(t.contains("00005"), "expected leading zeros in: {t}");
478 }
479
480 #[test]
481 fn negative_value_shows_minus() {
482 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
483 sb.set_value(-123);
484 let t = sb.text();
485 assert!(t.starts_with('-'), "expected '-' prefix, got: {t}");
486 }
487
488 #[test]
489 fn separator_position_inserts_dot() {
490 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
491 sb.set_digit_format(5, 2); sb.set_value(12345);
493 let t = sb.text();
494 assert!(t.contains('.'), "expected '.' separator in: {t}");
495 }
496
497 #[test]
498 fn separator_zero_hides_dot() {
499 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
500 sb.set_digit_format(5, 0);
501 sb.set_value(123);
502 assert!(!sb.text().contains('.'), "unexpected '.' in: {}", sb.text());
503 }
504
505 #[test]
506 fn increment_decrement_basic() {
507 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
508 sb.set_value(10);
509 sb.increment();
510 assert_eq!(sb.value(), 11);
511 sb.decrement();
512 sb.decrement();
513 assert_eq!(sb.value(), 9);
514 }
515
516 #[test]
517 fn zero_crossing_increment_lands_on_zero() {
518 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
519 sb.set_step(5);
520 sb.set_value(-3);
521 sb.increment(); assert_eq!(sb.value(), 0);
523 }
524
525 #[test]
526 fn zero_crossing_decrement_lands_on_zero() {
527 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
528 sb.set_step(5);
529 sb.set_value(3);
530 sb.decrement(); assert_eq!(sb.value(), 0);
532 }
533
534 #[test]
535 fn rollover_wraps_at_max() {
536 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
537 sb.set_range(0, 10);
538 sb.set_rollover(true);
539 sb.set_value(10);
540 sb.increment();
541 assert_eq!(sb.value(), 0);
542 }
543
544 #[test]
545 fn rollover_wraps_at_min() {
546 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
547 sb.set_range(0, 10);
548 sb.set_rollover(true);
549 sb.set_value(0);
550 sb.decrement();
551 assert_eq!(sb.value(), 10);
552 }
553
554 #[test]
555 fn no_rollover_clamps_at_max() {
556 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
557 sb.set_range(0, 10);
558 sb.set_value(10);
559 sb.increment();
560 assert_eq!(sb.value(), 10);
561 }
562
563 #[test]
564 fn step_next_multiplies_by_ten() {
565 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
566 assert_eq!(sb.step(), 1);
567 sb.step_next();
568 assert_eq!(sb.step(), 10);
569 sb.step_next();
570 assert_eq!(sb.step(), 100);
571 }
572
573 #[test]
574 fn step_prev_divides_by_ten() {
575 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
576 sb.set_step(100);
577 sb.step_prev();
578 assert_eq!(sb.step(), 10);
579 sb.step_prev();
580 assert_eq!(sb.step(), 1);
581 sb.step_prev(); assert_eq!(sb.step(), 1);
583 }
584
585 #[test]
586 fn step_next_capped_at_digit_count_max() {
587 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
588 sb.set_digit_format(3, 0); for _ in 0..10 {
590 sb.step_next();
591 }
592 assert_eq!(sb.step(), 100);
593 }
594
595 #[test]
596 fn set_bounds_adopted() {
597 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
598 sb.set_bounds(rect(5, 5, 100, 30));
599 assert_eq!(sb.bounds(), rect(5, 5, 100, 30));
600 }
601
602 #[test]
603 fn digit_format_clamps_digit_count() {
604 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
605 sb.set_digit_format(20, 0);
606 assert_eq!(sb.digit_count(), 10);
607 }
608
609 #[test]
610 fn handle_event_always_returns_false() {
611 let mut sb = Spinbox::new(rect(0, 0, 80, 20));
612 assert!(!sb.handle_event(&Event::PressRelease { x: 10, y: 10 }));
613 }
614}