kas_widgets/
spin_box.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! SpinBox widget
7
8use 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
14/// Requirements on type used by [`SpinBox`]
15///
16/// Implementations are provided for standard float and integer types.
17///
18/// The type must support conversion to and approximate conversion from `f64`
19/// in order to enable programmatic control (e.g. tests, accessibility tools).
20/// NOTE: this restriction might be revised in the future once Rust supports
21/// specialization.
22pub 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    /// The default step size (usually 1)
33    fn default_step() -> Self;
34
35    /// Add `step` without wrapping
36    ///
37    /// The implementation should saturate on overflow, at least for fixed-precision types.
38    fn add_step(self, step: Self) -> Self;
39
40    /// Subtract `step` without wrapping
41    ///
42    /// The implementation should saturate on overflow, at least for fixed-precision types.
43    fn sub_step(self, step: Self) -> Self;
44
45    /// Clamp `self` to the range `l_bound..=u_bound`
46    ///
47    /// The default implementation is equivalent to the `std` implementations
48    /// for [`Ord`] and for floating-point types.
49    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    /// Returns new value if different
141    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    /// A numeric entry widget with up/down arrows
187    ///
188    /// The value is constrained to a given `range`. Increment and decrement
189    /// operations advance to the next/previous multiple of `step`.
190    ///
191    /// Recommendations for optimal behaviour:
192    ///
193    /// -   Ensure that range end points are a multiple of `step`
194    /// -   With floating-point types, ensure that `step` is exactly
195    ///     representable, e.g. an integer or a power of 2.
196    ///
197    /// ### Messages
198    ///
199    /// [`SetValueF64`] may be used to set the input value.
200    ///
201    /// [`IncrementStep`] and [`DecrementStep`] change the value by one step.
202    ///
203    /// [`SetValueText`] may be used to set the input as a text value.
204    /// [`ReplaceSelectedText`] may be used to replace the selected text.
205    #[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        /// Construct a spin box
224        ///
225        /// Values vary within the given `range`. The default step size is
226        /// 1 for common types (see [`SpinValue::default_step`]).
227        #[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        /// Construct a spin box
252        ///
253        /// - Values vary within the given `range`
254        /// - The default step size is 1 for common types (see [`SpinValue::default_step`])
255        /// - `state_fn` extracts the current state from input data
256        /// - A message generated by `msg_fn` is emitted when toggled
257        #[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        /// Send the message generated by `f` on change
267        #[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        /// Call the handler `f` on change
277        ///
278        /// This closure is called when the value is changed, specifically:
279        ///
280        /// -   If the increment/decrement buttons, <kbd>Up</kbd>/<kbd>Down</kbd>
281        ///     keys or mouse scroll wheel is used and the value changes
282        /// -   If the value is adjusted via the edit box and the result is valid
283        /// -   If <kbd>Enter</kbd> is pressed in the edit box
284        #[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        /// Set the text class used
293        ///
294        /// The default is: `TextClass::Edit(false)`.
295        #[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        /// Get the text class used
303        #[inline]
304        pub fn class(&self) -> TextClass {
305            self.edit.class()
306        }
307
308        /// Adjust the width allocation
309        #[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        /// Adjust the width allocation (inline)
315        #[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        /// Set the unit
323        ///
324        /// This is an annotation shown after the value.
325        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        /// Set the unit (inline)
332        ///
333        /// This method should only be used before the UI has started.
334        pub fn with_unit(mut self, unit: impl ToString) -> Self {
335            self.unit.set_text(unit.to_string());
336            self
337        }
338
339        /// Set the step size
340        #[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}