Skip to main content

vizia_core/views/
spinbox.rs

1use crate::icons::{
2    ICON_CHEVRON_DOWN, ICON_CHEVRON_LEFT, ICON_CHEVRON_RIGHT, ICON_CHEVRON_UP, ICON_MINUS,
3    ICON_PLUS,
4};
5use crate::prelude::*;
6
7pub(crate) enum SpinboxEvent {
8    Increment,
9    Decrement,
10    SetMin,
11    SetMax,
12}
13
14/// A view which represents a value which can be incremented or decremented.
15pub struct Spinbox {
16    value: Signal<f64>,
17    orientation: Signal<Orientation>,
18    icons: Signal<SpinboxIcons>,
19    min: Signal<Option<f64>>,
20    max: Signal<Option<f64>>,
21
22    on_change: Option<Box<dyn Fn(&mut EventContext, f64)>>,
23    on_decrement: Option<Box<dyn Fn(&mut EventContext) + Send + Sync>>,
24    on_increment: Option<Box<dyn Fn(&mut EventContext) + Send + Sync>>,
25}
26
27/// And enum which represents the icons that can be used for the increment and decrement buttons of the [Spinbox].
28#[derive(Clone, Copy, Debug, PartialEq)]
29pub enum SpinboxIcons {
30    /// A plus icon for the increment button and a minus icon for the decrement button.
31    PlusMinus,
32    /// A right chevron for the increment button and a left chevron for the decrement button.
33    Chevrons,
34}
35
36impl_res_simple!(SpinboxIcons);
37
38impl Spinbox {
39    /// Creates a new [Spinbox] view.
40    pub fn new<S, T>(cx: &mut Context, value: S) -> Handle<Spinbox>
41    where
42        S: Copy + SignalGet<T> + SignalMap<T> + Res<T> + 'static,
43        T: Clone + Into<f64> + 'static,
44    {
45        let numeric_value = value.map(|v| v.clone().into()).to_signal(cx);
46
47        let orientation = Signal::new(Orientation::Horizontal);
48        let icons = Signal::new(SpinboxIcons::Chevrons);
49        let min = Signal::new(None::<f64>);
50        let max = Signal::new(None::<f64>);
51
52        Self {
53            value: numeric_value,
54            orientation,
55            icons,
56            min,
57            max,
58            on_change: None,
59            on_decrement: None,
60            on_increment: None,
61        }
62            .build(cx, move |cx| {
63                Keymap::from(vec![
64                    (
65                        KeyChord::new(Modifiers::empty(), Code::ArrowUp),
66                        KeymapEntry::new("Increment", |cx| cx.emit(SpinboxEvent::Increment)),
67                    ),
68                    (
69                        KeyChord::new(Modifiers::empty(), Code::ArrowRight),
70                        KeymapEntry::new("Increment", |cx| cx.emit(SpinboxEvent::Increment)),
71                    ),
72                    (
73                        KeyChord::new(Modifiers::empty(), Code::ArrowDown),
74                        KeymapEntry::new("Decrement", |cx| cx.emit(SpinboxEvent::Decrement)),
75                    ),
76                    (
77                        KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
78                        KeymapEntry::new("Decrement", |cx| cx.emit(SpinboxEvent::Decrement)),
79                    ),
80                    (
81                        KeyChord::new(Modifiers::empty(), Code::Home),
82                        KeymapEntry::new("Set Min", |cx| cx.emit(SpinboxEvent::SetMin)),
83                    ),
84                    (
85                        KeyChord::new(Modifiers::empty(), Code::End),
86                        KeymapEntry::new("Set Max", |cx| cx.emit(SpinboxEvent::SetMax)),
87                    ),
88                ])
89                .build(cx);
90
91                let at_min = Memo::new(move |_| {
92                    matches!((min.get(), numeric_value.get()), (Some(min), value) if value <= min)
93                });
94                let at_max = Memo::new(move |_| {
95                    matches!((max.get(), numeric_value.get()), (Some(max), value) if value >= max)
96                });
97
98                Binding::new(cx, orientation, move |cx| match orientation.get() {
99                    Orientation::Horizontal => {
100                        Button::new(cx, |cx| {
101                            Svg::new(
102                                cx,
103                                icons.map(|icons| match icons {
104                                    SpinboxIcons::PlusMinus => ICON_MINUS,
105                                    SpinboxIcons::Chevrons => ICON_CHEVRON_LEFT,
106                                }),
107                            )
108                        })
109                        .on_press(|ex| ex.emit(SpinboxEvent::Decrement))
110                        .disabled(at_min)
111                        .navigable(false)
112                        .name(Localized::new("decrement"))
113                        .variant(ButtonVariant::Text)
114                        .class("spinbox-button");
115                    }
116
117                    Orientation::Vertical => {
118                        Button::new(cx, |cx| {
119                            Svg::new(
120                                cx,
121                                icons.map(|icons| match icons {
122                                    SpinboxIcons::PlusMinus => ICON_PLUS,
123                                    SpinboxIcons::Chevrons => ICON_CHEVRON_UP,
124                                }),
125                            )
126                        })
127                        .on_press(|ex| ex.emit(SpinboxEvent::Increment))
128                        .disabled(at_max)
129                        .navigable(false)
130                        .name(Localized::new("increment"))
131                        .variant(ButtonVariant::Text)
132                        .class("spinbox-button");
133                    }
134                });
135                Textbox::new(cx, numeric_value).class("spinbox-value").role(Role::SpinButton);
136                Binding::new(cx, orientation, move |cx| match orientation.get() {
137                    Orientation::Horizontal => {
138                        Button::new(cx, |cx| {
139                            Svg::new(
140                                cx,
141                                icons.map(|icons| match icons {
142                                    SpinboxIcons::PlusMinus => ICON_PLUS,
143                                    SpinboxIcons::Chevrons => ICON_CHEVRON_RIGHT,
144                                }),
145                            )
146                        })
147                        .on_press(|ex| ex.emit(SpinboxEvent::Increment))
148                        .disabled(at_max)
149                        .navigable(false)
150                        .name(Localized::new("increment"))
151                        .variant(ButtonVariant::Text)
152                        .class("spinbox-button");
153                    }
154
155                    Orientation::Vertical => {
156                        Button::new(cx, |cx| {
157                            Svg::new(
158                                cx,
159                                icons.map(|icons| match icons {
160                                    SpinboxIcons::PlusMinus => ICON_MINUS,
161                                    SpinboxIcons::Chevrons => ICON_CHEVRON_DOWN,
162                                }),
163                            )
164                        })
165                        .on_press(|ex| ex.emit(SpinboxEvent::Decrement))
166                        .disabled(at_min)
167                        .navigable(false)
168                        .name(Localized::new("decrement"))
169                        .variant(ButtonVariant::Text)
170                        .class("spinbox-button");
171                    }
172                });
173            })
174            .orientation(orientation)
175            .navigable(false)
176    }
177
178    fn clamp_value(&self, value: f64) -> f64 {
179        let value = if let Some(min) = self.min.get() { value.max(min) } else { value };
180        if let Some(max) = self.max.get() { value.min(max) } else { value }
181    }
182
183    fn emit_change(&self, cx: &mut EventContext, value: f64) {
184        if let Some(callback) = &self.on_change {
185            (callback)(cx, self.clamp_value(value));
186        }
187    }
188}
189
190impl Handle<'_, Spinbox> {
191    /// Sets the callback triggered when the spinbox value is changed.
192    pub fn on_change<F>(self, callback: F) -> Self
193    where
194        F: 'static + Fn(&mut EventContext, f64),
195    {
196        self.modify(|spinbox| spinbox.on_change = Some(Box::new(callback)))
197    }
198
199    /// Sets the callback which is triggered when the [Spinbox] value is incremented.
200    pub fn on_increment<F>(self, callback: F) -> Self
201    where
202        F: 'static + Fn(&mut EventContext) + Send + Sync,
203    {
204        self.modify(|spinbox: &mut Spinbox| spinbox.on_increment = Some(Box::new(callback)))
205    }
206
207    /// Sets the callback which is triggered when the [Spinbox] value is decremented.
208    pub fn on_decrement<F>(self, callback: F) -> Self
209    where
210        F: 'static + Fn(&mut EventContext) + Send + Sync,
211    {
212        self.modify(|spinbox: &mut Spinbox| spinbox.on_decrement = Some(Box::new(callback)))
213    }
214
215    /// Sets the orientation of the [Spinbox] to vertical.
216    pub fn vertical<U: Into<bool> + Clone + 'static>(
217        self,
218        vertical: impl Res<U> + 'static,
219    ) -> Self {
220        let vertical = vertical.to_signal(self.cx);
221        self.bind(vertical, move |handle| {
222            let vertical = vertical.get().into();
223            let orientation =
224                if vertical { Orientation::Vertical } else { Orientation::Horizontal };
225            handle.modify(move |spinbox| spinbox.orientation.set(orientation));
226        })
227    }
228
229    /// Set the icons which should be used for the increment and decrement buttons of the [Spinbox]
230    pub fn icons(self, icons: impl Res<SpinboxIcons> + 'static) -> Self {
231        let icons = icons.to_signal(self.cx);
232        self.bind(icons, move |handle| {
233            let icons = icons.get();
234            handle.modify(move |spinbox| spinbox.icons.set(icons));
235        })
236    }
237
238    /// Sets the minimum value of the [Spinbox], disabling the decrement button when reached.
239    pub fn min<U: Into<f64> + Clone + 'static>(self, min: impl Res<U> + 'static) -> Self {
240        let min_signal = min.to_signal(self.cx);
241        self.bind(min_signal, move |handle| {
242            let val: f64 = min_signal.get().into();
243            handle.modify(move |spinbox| spinbox.min.set(Some(val)));
244        })
245    }
246
247    /// Sets the maximum value of the [Spinbox], disabling the increment button when reached.
248    pub fn max<U: Into<f64> + Clone + 'static>(self, max: impl Res<U> + 'static) -> Self {
249        let max_signal = max.to_signal(self.cx);
250        self.bind(max_signal, move |handle| {
251            let val: f64 = max_signal.get().into();
252            handle.modify(move |spinbox| spinbox.max.set(Some(val)));
253        })
254    }
255}
256
257impl View for Spinbox {
258    fn element(&self) -> Option<&'static str> {
259        Some("spinbox")
260    }
261
262    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
263        event.map(|spinbox_event, _| match spinbox_event {
264            SpinboxEvent::Increment => {
265                if self.on_change.is_some() {
266                    self.emit_change(cx, self.value.get() + 1.0);
267                }
268
269                if let Some(callback) = &self.on_increment {
270                    (callback)(cx)
271                }
272            }
273
274            SpinboxEvent::Decrement => {
275                if self.on_change.is_some() {
276                    self.emit_change(cx, self.value.get() - 1.0);
277                }
278
279                if let Some(callback) = &self.on_decrement {
280                    (callback)(cx)
281                }
282            }
283
284            SpinboxEvent::SetMin => {
285                if let Some(min) = self.min.get() {
286                    self.emit_change(cx, min);
287                }
288            }
289
290            SpinboxEvent::SetMax => {
291                if let Some(max) = self.max.get() {
292                    self.emit_change(cx, max);
293                }
294            }
295        });
296    }
297}