use alloc::string::String;
use core::fmt::Write as FmtWrite;
use rlvgl_core::draw::draw_widget_bg;
use rlvgl_core::event::Event;
use rlvgl_core::font::{FontMetrics, WidgetFont, shape_text_ltr};
use rlvgl_core::renderer::{ClipRenderer, Renderer};
use rlvgl_core::style::Style;
use rlvgl_core::widget::{Color, Rect, Widget};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpinboxDigitStepDirection {
Right,
Left,
}
pub struct Spinbox {
bounds: Rect,
value: i32,
min: i32,
max: i32,
rollover: bool,
digit_count: u8,
separator_position: u8,
step: i32,
digit_step_direction: SpinboxDigitStepDirection,
pub style: Style,
pub text_color: Color,
pub cursor_color: Color,
font: WidgetFont,
}
impl Spinbox {
pub fn new(bounds: Rect) -> Self {
Self {
bounds,
value: 0,
min: -99_999,
max: 99_999,
rollover: false,
digit_count: 5,
separator_position: 0,
step: 1,
digit_step_direction: SpinboxDigitStepDirection::Right,
style: Style::default(),
text_color: Color(20, 20, 20, 255),
cursor_color: Color(0, 120, 215, 255),
font: WidgetFont::new(),
}
}
pub fn set_value(&mut self, value: i32) {
self.value = value.clamp(self.min, self.max);
}
pub fn value(&self) -> i32 {
self.value
}
pub fn set_range(&mut self, min: i32, max: i32) {
self.min = min;
self.max = max;
self.value = self.value.clamp(self.min, self.max);
}
pub fn min_value(&self) -> i32 {
self.min
}
pub fn max_value(&self) -> i32 {
self.max
}
pub fn set_rollover(&mut self, rollover: bool) {
self.rollover = rollover;
}
pub fn rollover(&self) -> bool {
self.rollover
}
pub fn set_digit_format(&mut self, digit_count: u8, separator_position: u8) {
self.digit_count = digit_count.clamp(1, 10);
self.separator_position = separator_position;
let max_repr = max_for_digits(self.digit_count);
self.min = self.min.clamp(-max_repr, max_repr);
self.max = self.max.clamp(-max_repr, max_repr);
self.value = self.value.clamp(self.min, self.max);
}
pub fn digit_count(&self) -> u8 {
self.digit_count
}
pub fn separator_position(&self) -> u8 {
self.separator_position
}
pub fn set_step(&mut self, step: i32) {
self.step = step.max(1);
}
pub fn step(&self) -> i32 {
self.step
}
pub fn set_digit_step_direction(&mut self, dir: SpinboxDigitStepDirection) {
self.digit_step_direction = dir;
}
pub fn digit_step_direction(&self) -> SpinboxDigitStepDirection {
self.digit_step_direction
}
pub fn step_next(&mut self) {
let cap = max_step(self.digit_count);
if self.step < cap {
self.step = (self.step.saturating_mul(10)).min(cap);
}
}
pub fn step_prev(&mut self) {
if self.step > 1 {
self.step /= 10;
}
}
pub fn increment(&mut self) {
let v = self.value;
let s = self.step;
let new = if v < 0 && v.saturating_add(s) > 0 {
0
} else {
v.saturating_add(s)
};
if new > self.max {
self.value = if self.rollover { self.min } else { self.max };
} else {
self.value = new;
}
}
pub fn decrement(&mut self) {
let v = self.value;
let s = self.step;
let new = if v > 0 && v.saturating_sub(s) < 0 {
0
} else {
v.saturating_sub(s)
};
if new < self.min {
self.value = if self.rollover { self.max } else { self.min };
} else {
self.value = new;
}
}
pub fn on_arrow_up(&mut self) {
self.increment();
}
pub fn on_arrow_down(&mut self) {
self.decrement();
}
pub fn on_arrow_right(&mut self) {
match self.digit_step_direction {
SpinboxDigitStepDirection::Right => self.step_prev(),
SpinboxDigitStepDirection::Left => self.step_next(),
}
}
pub fn on_arrow_left(&mut self) {
match self.digit_step_direction {
SpinboxDigitStepDirection::Right => self.step_next(),
SpinboxDigitStepDirection::Left => self.step_prev(),
}
}
pub fn text(&self) -> String {
let dc = self.digit_count as usize;
let sep = self.separator_position as usize;
let show_sign = self.min < 0;
let abs_val = self.value.unsigned_abs() as u64;
let mut digits = [b'0'; 10];
let mut temp = abs_val;
for i in (0..dc).rev() {
digits[i] = b'0' + (temp % 10) as u8;
temp /= 10;
}
let mut s = String::new();
if show_sign {
let sign = if self.value < 0 { '-' } else { '+' };
s.push(sign);
}
let sep_valid = sep > 0 && sep < dc;
let sep_insert_before = dc - sep;
for (i, &digit) in digits[..dc].iter().enumerate() {
if sep_valid && i == sep_insert_before {
s.push('.');
}
s.push(digit as char);
}
s
}
pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
self.font.set(font);
}
fn selected_char_index(&self) -> usize {
let dc = self.digit_count as usize;
let sep = self.separator_position as usize;
let show_sign = self.min < 0;
let digit_from_right = {
let mut d = 0usize;
let mut s = self.step;
while s > 1 {
s /= 10;
d += 1;
}
d
};
let digit_from_left = dc.saturating_sub(1).saturating_sub(digit_from_right);
let sep_valid = sep > 0 && sep < dc;
let sep_insert_before = dc - sep;
let sep_before = if sep_valid && digit_from_left >= sep_insert_before {
1
} else {
0
};
let sign_offset = if show_sign { 1 } else { 0 };
sign_offset + digit_from_left + sep_before
}
}
fn max_for_digits(digit_count: u8) -> i32 {
let n = digit_count.min(9) as u32; let mut v: i32 = 1;
for _ in 0..n {
v = v.saturating_mul(10);
}
v.saturating_sub(1)
}
fn max_step(digit_count: u8) -> i32 {
let n = digit_count.saturating_sub(1).min(9) as u32;
let mut v: i32 = 1;
for _ in 0..n {
v = v.saturating_mul(10);
}
v
}
impl Widget for Spinbox {
fn bounds(&self) -> Rect {
self.bounds
}
fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
Some(&mut self.font)
}
fn set_bounds(&mut self, bounds: Rect) {
self.bounds = bounds;
}
fn draw(&self, renderer: &mut dyn Renderer) {
draw_widget_bg(renderer, self.bounds, &self.style);
let text = self.text();
let font = self.font.resolve();
let metrics = font.line_metrics();
let baseline = self.bounds.y + metrics.ascent as i32;
let shaped = shape_text_ltr(font, &text, (self.bounds.x, baseline), 0);
let color = self.text_color.with_alpha(self.style.alpha);
{
let mut clipped = ClipRenderer::new(renderer, self.bounds);
clipped.draw_text_shaped(&shaped, (0, 0), color);
}
let char_idx = self.selected_char_index();
if let Some(glyph) = shaped.glyphs.get(char_idx) {
let ext = glyph.extent();
let cursor_rect = Rect {
x: ext.x,
y: ext.y + ext.height,
width: ext.width.max(1),
height: 2,
};
renderer.fill_rect(cursor_rect, self.cursor_color.with_alpha(self.style.alpha));
}
}
fn handle_event(&mut self, _event: &Event) -> bool {
false
}
}
#[allow(unused_imports)]
use FmtWrite as _;
#[cfg(test)]
mod tests {
use super::*;
fn rect(x: i32, y: i32, w: i32, h: i32) -> Rect {
Rect {
x,
y,
width: w,
height: h,
}
}
#[test]
fn default_value_and_range() {
let sb = Spinbox::new(rect(0, 0, 80, 20));
assert_eq!(sb.value(), 0);
assert_eq!(sb.min_value(), -99_999);
assert_eq!(sb.max_value(), 99_999);
assert_eq!(sb.digit_count(), 5);
assert_eq!(sb.separator_position(), 0);
assert_eq!(sb.step(), 1);
assert!(!sb.rollover());
}
#[test]
fn set_value_clamps_to_range() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_value(200_000);
assert_eq!(sb.value(), 99_999);
sb.set_value(-200_000);
assert_eq!(sb.value(), -99_999);
}
#[test]
fn set_range_clamps_value() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_value(50);
sb.set_range(0, 30);
assert_eq!(sb.value(), 30);
}
#[test]
fn digit_format_shows_plus_and_leading_zeros() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_value(42);
let t = sb.text();
assert!(t.starts_with('+'), "expected '+' prefix, got: {t}");
assert!(t.contains("00042"), "expected leading zeros in: {t}");
}
#[test]
fn positive_only_range_omits_sign() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_range(0, 99_999);
sb.set_value(5);
let t = sb.text();
assert!(
!t.starts_with('+') && !t.starts_with('-'),
"unexpected sign prefix in: {t}"
);
assert!(t.contains("00005"), "expected leading zeros in: {t}");
}
#[test]
fn negative_value_shows_minus() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_value(-123);
let t = sb.text();
assert!(t.starts_with('-'), "expected '-' prefix, got: {t}");
}
#[test]
fn separator_position_inserts_dot() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_digit_format(5, 2); sb.set_value(12345);
let t = sb.text();
assert!(t.contains('.'), "expected '.' separator in: {t}");
}
#[test]
fn separator_zero_hides_dot() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_digit_format(5, 0);
sb.set_value(123);
assert!(!sb.text().contains('.'), "unexpected '.' in: {}", sb.text());
}
#[test]
fn increment_decrement_basic() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_value(10);
sb.increment();
assert_eq!(sb.value(), 11);
sb.decrement();
sb.decrement();
assert_eq!(sb.value(), 9);
}
#[test]
fn zero_crossing_increment_lands_on_zero() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_step(5);
sb.set_value(-3);
sb.increment(); assert_eq!(sb.value(), 0);
}
#[test]
fn zero_crossing_decrement_lands_on_zero() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_step(5);
sb.set_value(3);
sb.decrement(); assert_eq!(sb.value(), 0);
}
#[test]
fn rollover_wraps_at_max() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_range(0, 10);
sb.set_rollover(true);
sb.set_value(10);
sb.increment();
assert_eq!(sb.value(), 0);
}
#[test]
fn rollover_wraps_at_min() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_range(0, 10);
sb.set_rollover(true);
sb.set_value(0);
sb.decrement();
assert_eq!(sb.value(), 10);
}
#[test]
fn no_rollover_clamps_at_max() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_range(0, 10);
sb.set_value(10);
sb.increment();
assert_eq!(sb.value(), 10);
}
#[test]
fn step_next_multiplies_by_ten() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
assert_eq!(sb.step(), 1);
sb.step_next();
assert_eq!(sb.step(), 10);
sb.step_next();
assert_eq!(sb.step(), 100);
}
#[test]
fn step_prev_divides_by_ten() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_step(100);
sb.step_prev();
assert_eq!(sb.step(), 10);
sb.step_prev();
assert_eq!(sb.step(), 1);
sb.step_prev(); assert_eq!(sb.step(), 1);
}
#[test]
fn step_next_capped_at_digit_count_max() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_digit_format(3, 0); for _ in 0..10 {
sb.step_next();
}
assert_eq!(sb.step(), 100);
}
#[test]
fn set_bounds_adopted() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_bounds(rect(5, 5, 100, 30));
assert_eq!(sb.bounds(), rect(5, 5, 100, 30));
}
#[test]
fn digit_format_clamps_digit_count() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
sb.set_digit_format(20, 0);
assert_eq!(sb.digit_count(), 10);
}
#[test]
fn handle_event_always_returns_false() {
let mut sb = Spinbox::new(rect(0, 0, 80, 20));
assert!(!sb.handle_event(&Event::PressRelease { x: 10, y: 10 }));
}
}