1use crate::{EditField, EditGuard, MarkButton};
9use kas::messages::{DecrementStep, IncrementStep, ReplaceSelectedText, SetValueF64, SetValueText};
10use kas::prelude::*;
11use kas::theme::{Background, FrameStyle, MarkStyle, Text, TextClass};
12use std::ops::RangeInclusive;
13
14pub trait SpinValue:
23 Copy
24 + PartialOrd
25 + std::fmt::Debug
26 + std::str::FromStr
27 + ToString
28 + Cast<f64>
29 + ConvApprox<f64>
30 + 'static
31{
32 fn default_step() -> Self;
34
35 fn add_step(self, step: Self) -> Self;
39
40 fn sub_step(self, step: Self) -> Self;
44
45 fn clamp(self, l_bound: Self, u_bound: Self) -> Self {
50 assert!(l_bound <= u_bound);
51 if self < l_bound {
52 l_bound
53 } else if self > u_bound {
54 u_bound
55 } else {
56 self
57 }
58 }
59}
60
61macro_rules! impl_float {
62 ($t:ty) => {
63 impl SpinValue for $t {
64 fn default_step() -> Self {
65 1.0
66 }
67 fn add_step(self, step: Self) -> Self {
68 self + step
69 }
70 fn sub_step(self, step: Self) -> Self {
71 self - step
72 }
73 fn clamp(self, l_bound: Self, u_bound: Self) -> Self {
74 <$t>::clamp(self, l_bound, u_bound)
75 }
76 }
77 };
78}
79
80impl_float!(f32);
81impl_float!(f64);
82
83macro_rules! impl_int {
84 ($t:ty) => {
85 impl SpinValue for $t {
86 fn default_step() -> Self {
87 1
88 }
89 fn add_step(self, step: Self) -> Self {
90 self.saturating_add(step)
91 }
92 fn sub_step(self, step: Self) -> Self {
93 self.saturating_sub(step)
94 }
95 fn clamp(self, l_bound: Self, u_bound: Self) -> Self {
96 Ord::clamp(self, l_bound, u_bound)
97 }
98 }
99 };
100 ($($t:ty),*) => {
101 $(impl_int!($t);)*
102 };
103}
104
105impl_int!(i8, i16, i32, i64, i128, isize);
106impl_int!(u8, u16, u32, u64, u128, usize);
107
108#[derive(Clone, Copy, Debug)]
109enum SpinBtn {
110 Down,
111 Up,
112}
113
114#[derive(Debug)]
115struct ValueMsg<T>(T);
116
117#[autoimpl(Debug ignore self.state_fn where T: trait)]
118struct SpinGuard<A, T: SpinValue> {
119 start: T,
120 end: T,
121 step: T,
122 value: T,
123 parsed: Option<T>,
124 state_fn: Box<dyn Fn(&ConfigCx, &A) -> T>,
125}
126
127impl<A, T: SpinValue> SpinGuard<A, T> {
128 fn new(range: RangeInclusive<T>, state_fn: Box<dyn Fn(&ConfigCx, &A) -> T>) -> Self {
129 let (start, end) = range.into_inner();
130 SpinGuard {
131 start,
132 end,
133 step: T::default_step(),
134 value: start,
135 parsed: None,
136 state_fn,
137 }
138 }
139
140 fn handle_btn(&mut self, btn: SpinBtn) -> Option<T> {
142 let old_value = self.value;
143 let value = match btn {
144 SpinBtn::Down => old_value.sub_step(self.step),
145 SpinBtn::Up => old_value.add_step(self.step),
146 };
147
148 self.value = value.clamp(self.start, self.end);
149 (value != old_value).then_some(value)
150 }
151}
152
153impl<A, T: SpinValue> EditGuard for SpinGuard<A, T> {
154 type Data = A;
155
156 fn update(edit: &mut EditField<Self>, cx: &mut ConfigCx, data: &A) {
157 edit.guard.value = (edit.guard.state_fn)(cx, data);
158 edit.set_string(cx, edit.guard.value.to_string());
159 }
160
161 fn focus_lost(edit: &mut EditField<Self>, cx: &mut EventCx, _: &A) {
162 if let Some(value) = edit.guard.parsed.take() {
163 edit.guard.value = value;
164 cx.push(ValueMsg(value));
165 } else {
166 edit.set_string(cx, edit.guard.value.to_string());
167 }
168 }
169
170 fn edit(edit: &mut EditField<Self>, cx: &mut EventCx, _: &A) {
171 let is_err;
172 if let Ok(value) = edit.as_str().parse::<T>() {
173 edit.guard.value = value.clamp(edit.guard.start, edit.guard.end);
174 edit.guard.parsed = Some(edit.guard.value);
175 is_err = false;
176 } else {
177 edit.guard.parsed = None;
178 is_err = true;
179 };
180 edit.set_error_state(cx, is_err);
181 }
182}
183
184#[impl_self]
185mod SpinBox {
186 #[widget]
206 #[layout(
207 frame!(row![self.edit, self.unit, column! [self.b_up, self.b_down]])
208 .with_style(FrameStyle::EditBox)
209 )]
210 pub struct SpinBox<A, T: SpinValue> {
211 core: widget_core!(),
212 #[widget]
213 edit: EditField<SpinGuard<A, T>>,
214 unit: Text<String>,
215 #[widget(&())]
216 b_up: MarkButton<SpinBtn>,
217 #[widget(&())]
218 b_down: MarkButton<SpinBtn>,
219 on_change: Option<Box<dyn Fn(&mut EventCx, &A, T)>>,
220 }
221
222 impl Self {
223 #[inline]
228 pub fn new(
229 range: RangeInclusive<T>,
230 state_fn: impl Fn(&ConfigCx, &A) -> T + 'static,
231 ) -> Self {
232 SpinBox {
233 core: Default::default(),
234 edit: EditField::new(SpinGuard::new(range, Box::new(state_fn)))
235 .with_width_em(3.0, 8.0),
236 unit: Default::default(),
237 b_up: MarkButton::new_msg(
238 MarkStyle::Chevron(Direction::Up),
239 "Increment",
240 SpinBtn::Up,
241 ),
242 b_down: MarkButton::new_msg(
243 MarkStyle::Chevron(Direction::Down),
244 "Decrement",
245 SpinBtn::Down,
246 ),
247 on_change: None,
248 }
249 }
250
251 #[inline]
258 pub fn new_msg<M: std::fmt::Debug + 'static>(
259 range: RangeInclusive<T>,
260 state_fn: impl Fn(&ConfigCx, &A) -> T + 'static,
261 msg_fn: impl Fn(T) -> M + 'static,
262 ) -> Self {
263 SpinBox::new(range, state_fn).with_msg(msg_fn)
264 }
265
266 #[inline]
268 #[must_use]
269 pub fn with_msg<M>(self, f: impl Fn(T) -> M + 'static) -> Self
270 where
271 M: std::fmt::Debug + 'static,
272 {
273 self.with(move |cx, _, state| cx.push(f(state)))
274 }
275
276 #[inline]
285 #[must_use]
286 pub fn with(mut self, f: impl Fn(&mut EventCx, &A, T) + 'static) -> Self {
287 debug_assert!(self.on_change.is_none());
288 self.on_change = Some(Box::new(f));
289 self
290 }
291
292 #[inline]
296 #[must_use]
297 pub fn with_class(mut self, class: TextClass) -> Self {
298 self.edit = self.edit.with_class(class);
299 self
300 }
301
302 #[inline]
304 pub fn class(&self) -> TextClass {
305 self.edit.class()
306 }
307
308 #[inline]
310 pub fn set_width_em(&mut self, min_em: f32, ideal_em: f32) {
311 self.edit.set_width_em(min_em, ideal_em);
312 }
313
314 #[inline]
316 #[must_use]
317 pub fn with_width_em(mut self, min_em: f32, ideal_em: f32) -> Self {
318 self.set_width_em(min_em, ideal_em);
319 self
320 }
321
322 pub fn set_unit(&mut self, cx: &mut EventState, unit: impl ToString) {
326 self.unit.set_text(unit.to_string());
327 let act = self.unit.reprepare_action();
328 cx.action(self, act);
329 }
330
331 pub fn with_unit(mut self, unit: impl ToString) -> Self {
335 self.unit.set_text(unit.to_string());
336 self
337 }
338
339 #[inline]
341 #[must_use]
342 pub fn with_step(mut self, step: T) -> Self {
343 self.edit.guard.step = step;
344 self
345 }
346 }
347
348 impl Layout for Self {
349 fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) {
350 kas::MacroDefinedLayout::set_rect(self, cx, rect, hints);
351 }
352
353 fn draw(&self, mut draw: DrawCx) {
354 let mut draw_edit = draw.re();
355 draw_edit.set_id(self.edit.id());
356 let bg = if self.edit.has_error() {
357 Background::Error
358 } else {
359 Background::Default
360 };
361 draw_edit.frame(self.rect(), FrameStyle::EditBox, bg);
362
363 self.edit.draw(draw_edit);
364 self.unit.draw(draw.re());
365 self.b_up.draw(draw.re());
366 self.b_down.draw(draw.re());
367 }
368 }
369
370 impl Tile for Self {
371 fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
372 Role::SpinButton {
373 min: self.edit.guard.start.cast(),
374 max: self.edit.guard.end.cast(),
375 step: self.edit.guard.step.cast(),
376 value: self.edit.guard.value.cast(),
377 }
378 }
379
380 fn probe(&self, coord: Coord) -> Id {
381 self.b_up
382 .try_probe(coord)
383 .or_else(|| self.b_down.try_probe(coord))
384 .unwrap_or_else(|| self.edit.id())
385 }
386 }
387
388 impl Events for Self {
389 type Data = A;
390
391 fn configure(&mut self, cx: &mut ConfigCx) {
392 cx.text_configure(&mut self.unit);
393 }
394
395 fn handle_event(&mut self, cx: &mut EventCx, data: &A, event: Event) -> IsUsed {
396 let mut value = None;
397 match event {
398 Event::Command(cmd, code) => {
399 let btn = match cmd {
400 Command::Down => {
401 cx.depress_with_key(self.b_down.id(), code);
402 SpinBtn::Down
403 }
404 Command::Up => {
405 cx.depress_with_key(self.b_up.id(), code);
406 SpinBtn::Up
407 }
408 _ => return Unused,
409 };
410 value = self.edit.guard.handle_btn(btn);
411 }
412 Event::Scroll(delta) => {
413 if let Some(y) = delta.as_wheel_action(cx) {
414 let (count, btn) = if y > 0 {
415 (y as u32, SpinBtn::Up)
416 } else {
417 ((-y) as u32, SpinBtn::Down)
418 };
419 for _ in 0..count {
420 value = self.edit.guard.handle_btn(btn);
421 }
422 } else {
423 return Unused;
424 }
425 }
426 _ => return Unused,
427 }
428
429 if let Some(value) = value {
430 if let Some(ref f) = self.on_change {
431 f(cx, data, value);
432 }
433 }
434 Used
435 }
436
437 fn handle_messages(&mut self, cx: &mut EventCx, data: &A) {
438 let new_value = if let Some(ValueMsg(value)) = cx.try_pop() {
439 Some(value)
440 } else if let Some(btn) = cx.try_pop::<SpinBtn>() {
441 self.edit.guard.handle_btn(btn)
442 } else if let Some(SetValueF64(v)) = cx.try_pop() {
443 match v.try_cast_approx() {
444 Ok(value) => Some(value),
445 Err(err) => {
446 log::warn!("Slider failed to handle SetValueF64: {err}");
447 None
448 }
449 }
450 } else if let Some(IncrementStep) = cx.try_pop() {
451 Some(self.edit.guard.value.add_step(self.edit.guard.step))
452 } else if let Some(DecrementStep) = cx.try_pop() {
453 Some(self.edit.guard.value.sub_step(self.edit.guard.step))
454 } else if let Some(SetValueText(string)) = cx.try_pop() {
455 self.edit.set_string(cx, string);
456 SpinGuard::edit(&mut self.edit, cx, data);
457 self.edit.guard.parsed
458 } else if let Some(ReplaceSelectedText(text)) = cx.try_pop() {
459 self.edit.replace_selection(cx, &text);
460 SpinGuard::edit(&mut self.edit, cx, data);
461 self.edit.guard.parsed
462 } else {
463 None
464 };
465
466 if let Some(value) = new_value {
467 if let Some(ref f) = self.on_change {
468 f(cx, data, value);
469 }
470 }
471 }
472 }
473}