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
14pub 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#[derive(Clone, Copy, Debug, PartialEq)]
29pub enum SpinboxIcons {
30 PlusMinus,
32 Chevrons,
34}
35
36impl_res_simple!(SpinboxIcons);
37
38impl Spinbox {
39 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 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 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 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 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 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 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 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}