vizia_core/views/slider.rs
1use std::ops::Range;
2
3use accesskit::ActionData;
4
5use crate::prelude::*;
6
7/// Internal data used by the slider.
8#[derive(Clone, Debug, Default, Data)]
9pub struct SliderDataInternal {
10 /// The orientation of the slider.
11 pub orientation: Orientation,
12 /// The range of the slider.
13 pub range: Range<f32>,
14 /// The step of the slider.
15 pub step: f32,
16 /// How much the slider should change in response to keyboard events.
17 pub keyboard_fraction: f32,
18}
19
20/// The slider control can be used to select from a continuous set of values.
21///
22/// The slider control consists of three main parts, a **thumb** element which can be moved between the extremes of a linear **track**,
23/// and an **active** element which fills the slider to indicate the current value.
24///
25/// # Examples
26///
27/// ## Basic Slider
28/// In the following example, a slider is bound to a value. The `on_change` callback is used to send an event to mutate the
29/// bound value when the slider thumb is moved, or if the track is clicked on.
30/// ```
31/// # use vizia_core::prelude::*;
32/// # use vizia_derive::*;
33/// # let mut cx = &mut Context::default();
34/// # #[derive(Lens, Default)]
35/// # pub struct AppData {
36/// # value: f32,
37/// # }
38/// # impl Model for AppData {}
39/// # AppData::default().build(cx);
40/// Slider::new(cx, AppData::value)
41/// .on_change(|cx, value| {
42/// debug!("Slider on_change: {}", value);
43/// });
44/// ```
45///
46/// ## Slider with Label
47/// ```
48/// # use vizia_core::prelude::*;
49/// # use vizia_derive::*;
50/// # let mut cx = &mut Context::default();
51/// # #[derive(Lens, Default)]
52/// # pub struct AppData {
53/// # value: f32,
54/// # }
55/// # impl Model for AppData {}
56/// # AppData::default().build(cx);
57/// HStack::new(cx, |cx|{
58/// Slider::new(cx, AppData::value)
59/// .on_change(|cx, value| {
60/// debug!("Slider on_change: {}", value);
61/// });
62/// Label::new(cx, AppData::value.map(|val| format!("{:.2}", val)));
63/// });
64/// ```
65#[derive(Lens)]
66pub struct Slider<L: Lens> {
67 lens: L,
68 is_dragging: bool,
69 internal: SliderDataInternal,
70 on_change: Option<Box<dyn Fn(&mut EventContext, f32)>>,
71}
72
73impl<L> Slider<L>
74where
75 L: Lens<Target = f32>,
76{
77 /// Creates a new slider bound to the value targeted by the lens.
78 ///
79 /// ```
80 /// # use vizia_core::prelude::*;
81 /// # use vizia_derive::*;
82 /// # let mut cx = &mut Context::default();
83 /// # #[derive(Lens, Default)]
84 /// # pub struct AppData {
85 /// # value: f32,
86 /// # }
87 /// # impl Model for AppData {}
88 /// # AppData::default().build(cx);
89 /// Slider::new(cx, AppData::value)
90 /// .on_change(|cx, value| {
91 /// debug!("Slider on_change: {}", value);
92 /// });
93 /// ```
94 pub fn new(cx: &mut Context, lens: L) -> Handle<Self> {
95 Self {
96 lens,
97 is_dragging: false,
98
99 internal: SliderDataInternal {
100 orientation: Orientation::Horizontal,
101 range: 0.0..1.0,
102 step: 0.01,
103 keyboard_fraction: 0.1,
104 },
105
106 on_change: None,
107 }
108 .build(cx, move |cx| {
109 Binding::new(cx, Slider::<L>::internal, move |cx, slider_data| {
110 // Track
111 HStack::new(cx, move |cx| {
112 let slider_data = slider_data.get(cx);
113 let orientation = slider_data.orientation;
114 let range = slider_data.range;
115
116 // Active track
117 VStack::new(cx, |cx| {
118 // Thumb
119 Element::new(cx).class("thumb").bind(lens, move |handle, value| {
120 let val = value.get(&handle).clamp(range.start, range.end);
121 let normal_val = (val - range.start) / (range.end - range.start);
122 if orientation == Orientation::Horizontal {
123 handle.translate((
124 Percentage(100.0 * (1.0 - normal_val)),
125 Pixels(0.0),
126 ));
127 } else {
128 handle.translate((
129 Pixels(0.0),
130 Percentage(-100.0 * (1.0 - normal_val)),
131 ));
132 }
133 });
134 })
135 .class("active")
136 .bind(lens, move |handle, value| {
137 let val = value.get(&handle).clamp(range.start, range.end);
138 let normal_val = (val - range.start) / (range.end - range.start);
139
140 if orientation == Orientation::Horizontal {
141 handle
142 .height(Stretch(1.0))
143 .width(Percentage(normal_val * 100.0))
144 .layout_type(LayoutType::Row)
145 .alignment(Alignment::Right);
146 } else {
147 handle
148 .width(Stretch(1.0))
149 .height(Percentage(normal_val * 100.0))
150 .layout_type(LayoutType::Column)
151 .alignment(Alignment::TopCenter);
152 }
153 });
154 })
155 .class("track");
156 });
157 })
158 .toggle_class(
159 "vertical",
160 Self::internal.map(|slider_data| slider_data.orientation == Orientation::Vertical),
161 )
162 .role(Role::Slider)
163 .numeric_value(lens.map(|val| (*val as f64 * 100.0).round() / 100.0))
164 .text_value(lens.map(|val| {
165 let v = (*val as f64 * 100.0).round() / 100.0;
166 format!("{}", v)
167 }))
168 .navigable(true)
169 }
170}
171
172impl<L: Lens<Target = f32>> View for Slider<L> {
173 fn element(&self) -> Option<&'static str> {
174 Some("slider")
175 }
176
177 fn accessibility(&self, _cx: &mut AccessContext, node: &mut AccessNode) {
178 node.set_numeric_value_step(self.internal.step as f64);
179 node.set_min_numeric_value(self.internal.range.start as f64);
180 node.set_max_numeric_value(self.internal.range.end as f64);
181 }
182
183 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
184 event.map(|window_event, _| match window_event {
185 WindowEvent::MouseDown(button) if *button == MouseButton::Left => {
186 if !cx.is_disabled() {
187 self.is_dragging = true;
188 cx.capture();
189 cx.focus_with_visibility(false);
190 cx.with_current(Entity::root(), |cx| {
191 cx.set_pointer_events(false);
192 });
193
194 let thumb = cx.get_entities_by_class("thumb").first().copied().unwrap();
195 let thumb_size = match self.internal.orientation {
196 Orientation::Horizontal => cx.cache.get_width(thumb),
197 Orientation::Vertical => cx.cache.get_height(thumb),
198 };
199 let min = self.internal.range.start;
200 let max = self.internal.range.end;
201 let step = self.internal.step;
202
203 let current = cx.current();
204 let width = cx.cache.get_width(current);
205 let height = cx.cache.get_height(current);
206 let posx = cx.cache.get_posx(current);
207 let posy = cx.cache.get_posy(current);
208
209 let mut dx = match self.internal.orientation {
210 Orientation::Horizontal => {
211 (cx.mouse.left.pos_down.0 - posx - thumb_size / 2.0)
212 / (width - thumb_size)
213 }
214
215 Orientation::Vertical => {
216 (height - (cx.mouse.left.pos_down.1 - posy) - thumb_size / 2.0)
217 / (height - thumb_size)
218 }
219 };
220
221 dx = dx.clamp(0.0, 1.0);
222
223 let mut val = min + dx * (max - min);
224
225 val = step * (val / step).ceil();
226 val = val.clamp(min, max);
227
228 if let Some(callback) = self.on_change.take() {
229 (callback)(cx, val);
230
231 self.on_change = Some(callback);
232 }
233 }
234 }
235
236 WindowEvent::MouseUp(button) if *button == MouseButton::Left => {
237 self.is_dragging = false;
238 cx.focus_with_visibility(false);
239 cx.release();
240 cx.with_current(Entity::root(), |cx| {
241 cx.set_pointer_events(true);
242 });
243 }
244
245 WindowEvent::MouseMove(x, y) => {
246 if self.is_dragging {
247 let thumb = cx.get_entities_by_class("thumb").first().copied().unwrap();
248 let thumb_size = match self.internal.orientation {
249 Orientation::Horizontal => cx.cache.get_width(thumb),
250 Orientation::Vertical => cx.cache.get_height(thumb),
251 };
252
253 let min = self.internal.range.start;
254 let max = self.internal.range.end;
255 let step = self.internal.step;
256
257 let current = cx.current();
258 let width = cx.cache.get_width(current);
259 let height = cx.cache.get_height(current);
260 let posx = cx.cache.get_posx(current);
261 let posy = cx.cache.get_posy(current);
262
263 let mut dx = match self.internal.orientation {
264 Orientation::Horizontal => {
265 (*x - posx - thumb_size / 2.0) / (width - thumb_size)
266 }
267
268 Orientation::Vertical => {
269 (height - (*y - posy) - thumb_size / 2.0) / (height - thumb_size)
270 }
271 };
272
273 dx = dx.clamp(0.0, 1.0);
274
275 let mut val = min + dx * (max - min);
276
277 val = step * (val / step).ceil();
278 val = val.clamp(min, max);
279
280 if let Some(callback) = &self.on_change {
281 (callback)(cx, val);
282 }
283 }
284 }
285
286 WindowEvent::KeyDown(Code::ArrowUp | Code::ArrowRight, _) => {
287 let min = self.internal.range.start;
288 let max = self.internal.range.end;
289 let step = self.internal.step;
290 let mut val = self.lens.get(cx) + step;
291 val = val.clamp(min, max);
292 if let Some(callback) = &self.on_change {
293 (callback)(cx, val);
294 }
295 }
296
297 WindowEvent::KeyDown(Code::ArrowDown | Code::ArrowLeft, _) => {
298 let min = self.internal.range.start;
299 let max = self.internal.range.end;
300 let step = self.internal.step;
301 let mut val = self.lens.get(cx) - step;
302 val = val.clamp(min, max);
303 if let Some(callback) = &self.on_change {
304 (callback)(cx, val);
305 }
306 }
307
308 WindowEvent::ActionRequest(action) => match action.action {
309 Action::Increment => {
310 let min = self.internal.range.start;
311 let max = self.internal.range.end;
312 let step = self.internal.step;
313 let mut val = self.lens.get(cx) + step;
314 val = step * (val / step).ceil();
315 val = val.clamp(min, max);
316 if let Some(callback) = &self.on_change {
317 (callback)(cx, val);
318 }
319 }
320
321 Action::Decrement => {
322 let min = self.internal.range.start;
323 let max = self.internal.range.end;
324 let step = self.internal.step;
325 let mut val = self.lens.get(cx) - step;
326 val = step * (val / step).ceil();
327 val = val.clamp(min, max);
328 if let Some(callback) = &self.on_change {
329 (callback)(cx, val);
330 }
331 }
332
333 Action::SetValue => {
334 if let Some(ActionData::NumericValue(val)) = action.data {
335 let min = self.internal.range.start;
336 let max = self.internal.range.end;
337 let mut v = val as f32;
338 v = v.clamp(min, max);
339 if let Some(callback) = &self.on_change {
340 (callback)(cx, v);
341 }
342 }
343 }
344
345 _ => {}
346 },
347
348 _ => {}
349 });
350 }
351}
352
353impl<L: Lens> Handle<'_, Slider<L>> {
354 /// Sets the callback triggered when the slider value is changed.
355 ///
356 /// Takes a closure which triggers when the slider value is changed,
357 /// either by pressing the track or dragging the thumb along the track.
358 ///
359 /// ```
360 /// # use vizia_core::prelude::*;
361 /// # use vizia_derive::*;
362 /// # let mut cx = &mut Context::default();
363 /// # #[derive(Lens, Default)]
364 /// # pub struct AppData {
365 /// # value: f32,
366 /// # }
367 /// # impl Model for AppData {}
368 /// # AppData::default().build(cx);
369 /// Slider::new(cx, AppData::value)
370 /// .on_change(|cx, value| {
371 /// debug!("Slider on_change: {}", value);
372 /// });
373 /// ```
374 pub fn on_change<F>(self, callback: F) -> Self
375 where
376 F: 'static + Fn(&mut EventContext, f32),
377 {
378 self.modify(|slider| slider.on_change = Some(Box::new(callback)))
379 }
380
381 /// Sets the range of the slider.
382 ///
383 /// If the bound data is outside of the range then the slider will clip to min/max of the range.
384 ///
385 /// ```
386 /// # use vizia_core::prelude::*;
387 /// # use vizia_derive::*;
388 /// # let mut cx = &mut Context::default();
389 /// # #[derive(Lens, Default)]
390 /// # pub struct AppData {
391 /// # value: f32,
392 /// # }
393 /// # impl Model for AppData {}
394 /// # AppData::default().build(cx);
395 /// Slider::new(cx, AppData::value)
396 /// .range(-20.0..50.0)
397 /// .on_change(|cx, value| {
398 /// debug!("Slider on_change: {}", value);
399 /// });
400 /// ```
401 pub fn range<U: Into<Range<f32>>>(self, range: impl Res<U>) -> Self {
402 self.bind(range, |handle, range| {
403 let range = range.get(&handle).into();
404 handle.modify(|slider| {
405 slider.internal.range = range;
406 });
407 })
408 }
409
410 /// Sets the orientation of the slider.
411 ///
412 /// ```
413 /// # use vizia_core::prelude::*;
414 /// # use vizia_derive::*;
415 /// # let mut cx = &mut Context::default();
416 /// # #[derive(Lens, Default)]
417 /// # pub struct AppData {
418 /// # value: f32,
419 /// # }
420 /// # impl Model for AppData {}
421 /// # AppData::default().build(cx);
422 /// Slider::new(cx, AppData::value)
423 /// .orientation(Orientation::Vertical)
424 /// .on_change(|cx, value| {
425 /// debug!("Slider on_change: {}", value);
426 /// });
427 /// ```
428 pub fn orientation<U: Into<Orientation>>(self, orientation: impl Res<U>) -> Self {
429 self.bind(orientation, |handle, orientation| {
430 let orientation = orientation.get(&handle).into();
431 handle.modify(|slider: &mut Slider<L>| {
432 slider.internal.orientation = orientation;
433 });
434 })
435 }
436
437 /// Set the step value for the slider.
438 ///
439 /// ```
440 /// # use vizia_core::prelude::*;
441 /// # use vizia_derive::*;
442 /// # let mut cx = &mut Context::default();
443 /// # #[derive(Lens, Default)]
444 /// # pub struct AppData {
445 /// # value: f32,
446 /// # }
447 /// # impl Model for AppData {}
448 /// # AppData::default().build(cx);
449 /// Slider::new(cx, AppData::value)
450 /// .step(0.1)
451 /// .on_change(|cx, value| {
452 /// debug!("Slider on_change: {}", value);
453 /// });
454 /// ```
455 pub fn step<U: Into<f32>>(self, step: impl Res<U>) -> Self {
456 self.bind(step, |handle, step| {
457 let step = step.get(&handle).into();
458 handle.modify(|slider| {
459 slider.internal.step = step;
460 });
461 })
462 }
463
464 /// Sets the fraction of a slider that a press of an arrow key will change.
465 ///
466 /// ```
467 /// # use vizia_core::prelude::*;
468 /// # use vizia_derive::*;
469 /// # let mut cx = &mut Context::default();
470 /// # #[derive(Lens, Default)]
471 /// # pub struct AppData {
472 /// # value: f32,
473 /// # }
474 /// # impl Model for AppData {}
475 /// # AppData::default().build(cx);
476 /// Slider::new(cx, AppData::value)
477 /// .keyboard_fraction(0.05)
478 /// .on_change(|cx, value| {
479 /// debug!("Slider on_change: {}", value);
480 /// });
481 /// ```
482 pub fn keyboard_fraction<U: Into<f32>>(self, keyboard_fraction: impl Res<U>) -> Self {
483 self.bind(keyboard_fraction, |handle, keyboard_fraction| {
484 let keyboard_fraction = keyboard_fraction.get(&handle).into();
485 handle.modify(|slider| {
486 slider.internal.keyboard_fraction = keyboard_fraction;
487 });
488 })
489 }
490}