Skip to main content

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::{
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
17/// Requirements on type used by [`SpinBox`]
18///
19/// Implementations are provided for standard float and integer types.
20///
21/// The type must support conversion to and approximate conversion from `f64`
22/// in order to enable programmatic control (e.g. tests, accessibility tools).
23/// NOTE: this restriction might be revised in the future once Rust supports
24/// specialization.
25pub 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    /// The default step size (usually 1)
36    fn default_step() -> Self;
37
38    /// Add `step` without wrapping
39    ///
40    /// The implementation should saturate on overflow, at least for fixed-precision types.
41    fn add_step(self, step: Self) -> Self;
42
43    /// Subtract `step` without wrapping
44    ///
45    /// The implementation should saturate on overflow, at least for fixed-precision types.
46    fn sub_step(self, step: Self) -> Self;
47
48    /// Clamp `self` to the range `l_bound..=u_bound`
49    ///
50    /// The default implementation is equivalent to the `std` implementations
51    /// for [`Ord`] and for floating-point types.
52    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    /// Returns new value if different
144    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    /// A numeric entry widget with up/down arrows
194    ///
195    /// The value is constrained to a given `range`. Increment and decrement
196    /// operations advance to the next/previous multiple of `step`.
197    ///
198    /// Recommendations for optimal behaviour:
199    ///
200    /// -   Ensure that range end points are a multiple of `step`
201    /// -   With floating-point types, ensure that `step` is exactly
202    ///     representable, e.g. an integer or a power of 2.
203    ///
204    /// ### Messages
205    ///
206    /// [`SetValueF64`] may be used to set the input value.
207    ///
208    /// [`IncrementStep`] and [`DecrementStep`] change the value by one step.
209    ///
210    /// [`SetValueText`] may be used to set the input as a text value.
211    /// [`ReplaceSelectedText`] may be used to replace the selected text.
212    #[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        /// Construct a spin box
232        ///
233        /// Values vary within the given `range`. The default step size is
234        /// 1 for common types (see [`SpinValue::default_step`]).
235        #[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        /// Construct a spin box
260        ///
261        /// - Values vary within the given `range`
262        /// - The default step size is 1 for common types (see [`SpinValue::default_step`])
263        /// - `state_fn` extracts the current state from input data
264        /// - A message generated by `msg_fn` is emitted when toggled
265        #[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        /// Send the message generated by `f` on change
275        #[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        /// Call the handler `f` on change
285        ///
286        /// This closure is called when the value is changed, specifically:
287        ///
288        /// -   If the increment/decrement buttons, <kbd>Up</kbd>/<kbd>Down</kbd>
289        ///     keys or mouse scroll wheel is used and the value changes
290        /// -   If the value is adjusted via the edit box and the result is valid
291        /// -   If <kbd>Enter</kbd> is pressed in the edit box
292        #[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        /// Set the text class used
301        ///
302        /// The default is: `TextClass::Edit(false)`.
303        #[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        /// Get the text class used
311        #[inline]
312        pub fn class(&self) -> TextClass {
313            self.edit.class()
314        }
315
316        /// Adjust the width allocation
317        #[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        /// Adjust the width allocation (inline)
323        #[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        /// Set the unit
331        ///
332        /// This is an annotation shown after the value.
333        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        /// Set the unit (inline)
339        ///
340        /// This method should only be used before the UI has started.
341        pub fn with_unit(mut self, unit: impl ToString) -> Self {
342            self.unit.set_text(unit.to_string());
343            self
344        }
345
346        /// Set the step size
347        #[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}