1use core::fmt::Write as _;
2use embedded_graphics_core::pixelcolor::{Rgb565, WebColors};
3use heapless::String;
4
5use crate::{
6 block::Block,
7 geometry::Rect,
8 render::{CHAR_WIDTH, Compositor, RenderCtx, TextAlign, TextStyle},
9 style::{Border, VisualState, WidgetStyle},
10 widget::{PropertyError, PropertyKey, PropertyValue, Widget},
11};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub struct SpinboxWidget {
16 pub value: i32,
17 pub min: i32,
18 pub max: i32,
19 pub step: i32,
20 pub digits: u8,
21 pub decimals: u8,
22 pub focused_digit: u8,
23}
24
25impl SpinboxWidget {
26 pub const fn new(min: i32, max: i32, value: i32) -> Self {
27 Self {
28 value,
29 min,
30 max,
31 step: 1,
32 digits: 4,
33 decimals: 0,
34 focused_digit: 0,
35 }
36 }
37
38 pub const fn with_decimals(mut self, decimals: u8) -> Self {
39 self.decimals = decimals;
40 self
41 }
42
43 pub const fn with_digits(mut self, digits: u8) -> Self {
44 self.digits = if digits == 0 { 1 } else { digits };
45 self
46 }
47
48 pub const fn with_step(mut self, step: i32) -> Self {
49 self.step = step;
50 self
51 }
52
53 pub fn current_digit_multiplier(&self) -> i32 {
55 let mut mult: i32 = 1;
56 for _ in 0..self.focused_digit {
57 mult = mult.saturating_mul(10);
58 }
59 mult
60 }
61
62 pub fn increment(&mut self) {
64 let delta = self.current_digit_multiplier();
65 self.value = self.value.saturating_add(delta).clamp(self.min, self.max);
66 }
67
68 pub fn decrement(&mut self) {
70 let delta = self.current_digit_multiplier();
71 self.value = self.value.saturating_sub(delta).clamp(self.min, self.max);
72 }
73
74 pub fn prev_digit(&mut self) {
76 if self.focused_digit + 1 < self.digits {
77 self.focused_digit += 1;
78 }
79 }
80
81 pub fn next_digit(&mut self) {
83 if self.focused_digit > 0 {
84 self.focused_digit -= 1;
85 }
86 }
87
88 pub fn format_text(&self, out: &mut String<16>) {
89 out.clear();
90 let abs_val = self.value.abs();
91 let sign = if self.value < 0 { "-" } else { "" };
92
93 if self.decimals == 0 {
94 let _ = write!(
95 out,
96 "{}{:0width$}",
97 sign,
98 abs_val,
99 width = self.digits as usize
100 );
101 } else {
102 let mut divisor = 1;
103 for _ in 0..self.decimals {
104 divisor *= 10;
105 }
106 let int_part = abs_val / divisor;
107 let frac_part = abs_val % divisor;
108 let _ = write!(
109 out,
110 "{}{:0w$}.{:0d$}",
111 sign,
112 int_part,
113 frac_part,
114 w = (self.digits.saturating_sub(self.decimals)) as usize,
115 d = self.decimals as usize
116 );
117 }
118 }
119
120 pub fn render<D, C>(
121 &self,
122 ctx: &mut RenderCtx<'_, D, C>,
123 rect: Rect,
124 style: WidgetStyle,
125 state: VisualState,
126 ) -> Result<(), D::Error>
127 where
128 D: embedded_graphics_core::draw_target::DrawTarget<Color = Rgb565>,
129 C: Compositor<D>,
130 {
131 let resolved = style.resolve(state);
132 let block = Block::styled(resolved);
133 block.render(rect, ctx)?;
134
135 let inner = block.inner(rect);
136 let mut formatted: String<16> = String::new();
137 self.format_text(&mut formatted);
138
139 let char_w = CHAR_WIDTH;
140 let line_h = resolved.font.line_height();
141
142 let total_chars = formatted.len() as u32;
143 let text_w = total_chars * char_w;
144 let start_x = inner.x + (inner.w.saturating_sub(text_w) / 2) as i32;
145 let start_y = inner.y + (inner.h.saturating_sub(line_h) / 2) as i32;
146
147 ctx.draw_text_in(
149 Rect::new(start_x, start_y, text_w, line_h),
150 formatted.as_str(),
151 TextStyle::new(resolved.text).with_font(resolved.font),
152 )?;
153
154 if self.focused_digit < self.digits {
156 let digit_from_right = self.focused_digit as u32
157 + if self.decimals > 0 && self.focused_digit >= self.decimals {
158 1
159 } else {
160 0
161 };
162 let char_idx = total_chars
163 .saturating_sub(1)
164 .saturating_sub(digit_from_right);
165 let digit_x = start_x + (char_idx * char_w) as i32;
166 let underline = Rect::new(digit_x, start_y + line_h as i32 + 1, char_w, 2);
167 ctx.fill_rect(underline, Rgb565::CSS_CYAN)?;
168 }
169
170 if inner.w >= 80 {
172 let left_btn = Rect::new(inner.x + 2, inner.y + 2, 14, inner.h.saturating_sub(4));
173 let right_btn = Rect::new(
174 inner.right() - 16,
175 inner.y + 2,
176 14,
177 inner.h.saturating_sub(4),
178 );
179 ctx.stroke_rect(left_btn, Border::one(Rgb565::CSS_GRAY))?;
180 ctx.stroke_rect(right_btn, Border::one(Rgb565::CSS_GRAY))?;
181 ctx.draw_text_in(
182 left_btn,
183 "-",
184 TextStyle::new(resolved.text)
185 .with_font(resolved.font)
186 .with_align(TextAlign::Center),
187 )?;
188 ctx.draw_text_in(
189 right_btn,
190 "+",
191 TextStyle::new(resolved.text)
192 .with_font(resolved.font)
193 .with_align(TextAlign::Center),
194 )?;
195 }
196
197 Ok(())
198 }
199}
200
201impl Widget for SpinboxWidget {
202 fn render_widget_bounds(&self, _bounds: Rect, _style: &crate::style::Style) {}
203
204 fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
205 match key {
206 PropertyKey::Value => Some(PropertyValue::Int(self.value)),
207 PropertyKey::Min => Some(PropertyValue::Int(self.min)),
208 PropertyKey::Max => Some(PropertyValue::Int(self.max)),
209 _ => None,
210 }
211 }
212
213 fn set_property<'a>(
214 &mut self,
215 key: PropertyKey,
216 val: PropertyValue<'a>,
217 ) -> Result<(), PropertyError> {
218 match (key, val) {
219 (PropertyKey::Value, PropertyValue::Int(v)) => {
220 self.value = v.clamp(self.min, self.max);
221 Ok(())
222 }
223 (PropertyKey::Min, PropertyValue::Int(m)) => {
224 self.min = m;
225 Ok(())
226 }
227 (PropertyKey::Max, PropertyValue::Int(m)) => {
228 self.max = m;
229 Ok(())
230 }
231 _ => Err(PropertyError::NotFound),
232 }
233 }
234}