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