1use std::ops::Range;
2
3use crate::prelude::*;
4use accesskit::ActionData;
5
6pub(crate) enum SliderEvent {
8 Increment,
9 Decrement,
10 SetMin,
11 SetMax,
12 ResetDefault,
13}
14
15pub struct Slider<S> {
61 value: S,
62 is_dragging: bool,
63 orientation: Signal<Orientation>,
65 range: Signal<Range<f32>>,
67 step: Signal<f32>,
69 default_value: Signal<f32>,
71 on_change: Option<Box<dyn Fn(&mut EventContext, f32)>>,
72}
73
74impl<S> Slider<S>
75where
76 S: SignalGet<f32> + SignalMap<f32> + Copy + 'static,
77{
78 pub fn new(cx: &mut Context, value: S) -> Handle<Self> {
96 let range = Signal::new(0.0..1.0);
97 let orientation = Signal::new(Orientation::Horizontal);
98 let step = Signal::new(0.01);
99 let default_value = Signal::new(value.get());
100
101 Self { value, is_dragging: false, orientation, range, step, default_value, on_change: None }
102 .build(cx, move |cx| {
103 Keymap::from(vec![
104 (
105 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
106 KeymapEntry::new("Increment", |cx| cx.emit(SliderEvent::Increment)),
107 ),
108 (
109 KeyChord::new(Modifiers::empty(), Code::ArrowRight),
110 KeymapEntry::new("Increment", |cx| cx.emit(SliderEvent::Increment)),
111 ),
112 (
113 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
114 KeymapEntry::new("Decrement", |cx| cx.emit(SliderEvent::Decrement)),
115 ),
116 (
117 KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
118 KeymapEntry::new("Decrement", |cx| cx.emit(SliderEvent::Decrement)),
119 ),
120 (
121 KeyChord::new(Modifiers::empty(), Code::Home),
122 KeymapEntry::new("Set Min", |cx| cx.emit(SliderEvent::SetMin)),
123 ),
124 (
125 KeyChord::new(Modifiers::empty(), Code::End),
126 KeymapEntry::new("Set Max", |cx| cx.emit(SliderEvent::SetMax)),
127 ),
128 ])
129 .build(cx);
130
131 HStack::new(cx, move |cx| {
133 let active_normalized = Memo::new(move |_| {
134 let active_range = range.get();
135 let val = value.get().clamp(active_range.start, active_range.end);
136 (val - active_range.start) / (active_range.end - active_range.start)
137 });
138
139 let active_width = Memo::new(move |_| {
140 let normal_val = active_normalized.get();
141 if orientation.get() == Orientation::Horizontal {
142 Percentage(normal_val * 100.0)
143 } else {
144 Stretch(1.0)
145 }
146 });
147
148 let active_height = Memo::new(move |_| {
149 let normal_val = active_normalized.get();
150 if orientation.get() == Orientation::Horizontal {
151 Stretch(1.0)
152 } else {
153 Percentage(normal_val * 100.0)
154 }
155 });
156
157 VStack::new(cx, move |cx| {
159 let dir = cx.environment().direction;
160
161 let thumb_translate: Memo<Translate> = Memo::new(move |_| {
162 let thumb_range = range.get();
163 let val = value.get().clamp(thumb_range.start, thumb_range.end);
164 let normal_val =
165 (val - thumb_range.start) / (thumb_range.end - thumb_range.start);
166 let is_rtl = dir.get() == Direction::RightToLeft;
173 if orientation.get() == Orientation::Horizontal {
174 if is_rtl {
175 (Percentage(-100.0 * (1.0 - normal_val)), Pixels(0.0)).into()
176 } else {
177 (Percentage(100.0 * (1.0 - normal_val)), Pixels(0.0)).into()
178 }
179 } else {
180 (Pixels(0.0), Percentage(-100.0 * (1.0 - normal_val))).into()
181 }
182 });
183
184 Element::new(cx).class("thumb").translate(thumb_translate);
186 })
187 .class("range")
188 .width(active_width)
189 .height(active_height)
190 .layout_type(orientation.map(|o| {
191 if *o == Orientation::Horizontal {
192 LayoutType::Row
193 } else {
194 LayoutType::Column
195 }
196 }))
197 .alignment(orientation.map(|o| {
198 if *o == Orientation::Horizontal {
199 Alignment::Right
200 } else {
201 Alignment::TopCenter
202 }
203 }));
204 })
205 .class("track");
206 })
207 .orientation(orientation)
208 .role(Role::Slider)
209 .numeric_value(value.map(|v| (*v as f64 * 100.0).round() / 100.0))
210 .text_value(value.map(|v| format!("{}", (*v as f64 * 100.0).round() / 100.0)))
211 .navigable(true)
212 }
213}
214
215impl<S> View for Slider<S>
216where
217 S: SignalGet<f32> + 'static,
218{
219 fn element(&self) -> Option<&'static str> {
220 Some("slider")
221 }
222
223 fn accessibility(&self, _cx: &mut AccessContext, node: &mut AccessNode) {
224 node.set_numeric_value_step(self.step.get() as f64);
225 node.set_min_numeric_value(self.range.get().start as f64);
226 node.set_max_numeric_value(self.range.get().end as f64);
227 }
228
229 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
230 event.map(|slider_event, _| match slider_event {
231 SliderEvent::Increment => {
232 let min = self.range.get().start;
233 let max = self.range.get().end;
234 let step = self.step.get();
235 let mut val = self.value.get() + step;
236 val = val.clamp(min, max);
237 if let Some(callback) = &self.on_change {
238 (callback)(cx, val);
239 }
240 }
241
242 SliderEvent::Decrement => {
243 let min = self.range.get().start;
244 let max = self.range.get().end;
245 let step = self.step.get();
246 let mut val = self.value.get() - step;
247 val = val.clamp(min, max);
248 if let Some(callback) = &self.on_change {
249 (callback)(cx, val);
250 }
251 }
252
253 SliderEvent::SetMin => {
254 if let Some(callback) = &self.on_change {
255 (callback)(cx, self.range.get().start);
256 }
257 }
258
259 SliderEvent::SetMax => {
260 if let Some(callback) = &self.on_change {
261 (callback)(cx, self.range.get().end);
262 }
263 }
264
265 SliderEvent::ResetDefault => {
266 let min = self.range.get().start;
267 let max = self.range.get().end;
268 let val = self.default_value.get().clamp(min, max);
269 if let Some(callback) = &self.on_change {
270 (callback)(cx, val);
271 }
272 }
273 });
274
275 event.map(|window_event, meta| match window_event {
276 WindowEvent::MouseDown(button) if *button == MouseButton::Left => {
277 if !cx.is_disabled() {
278 self.is_dragging = true;
279 cx.capture();
280 cx.focus_with_visibility(false);
281 cx.with_current(Entity::root(), |cx| {
282 cx.set_pointer_events(false);
283 });
284
285 let thumb = cx.get_entities_by_class("thumb").first().copied().unwrap();
286 let thumb_size = match self.orientation.get() {
287 Orientation::Horizontal => cx.cache.get_width(thumb),
288 Orientation::Vertical => cx.cache.get_height(thumb),
289 };
290 let min = self.range.get().start;
291 let max = self.range.get().end;
292 let step = self.step.get();
293
294 let current = cx.current();
295 let width = cx.cache.get_width(current);
296 let height = cx.cache.get_height(current);
297 let posx = cx.cache.get_posx(current);
298 let posy = cx.cache.get_posy(current);
299
300 let is_rtl = matches!(
301 cx.style.direction.get(current).copied(),
302 Some(Direction::RightToLeft)
303 );
304
305 let mut dx = match self.orientation.get() {
306 Orientation::Horizontal => {
307 let raw_dx = (cx.mouse.left.pos_down.0 - posx - thumb_size / 2.0)
308 / (width - thumb_size);
309 if is_rtl { 1.0 - raw_dx } else { raw_dx }
310 }
311
312 Orientation::Vertical => {
313 (height - (cx.mouse.left.pos_down.1 - posy) - thumb_size / 2.0)
314 / (height - thumb_size)
315 }
316 };
317
318 dx = dx.clamp(0.0, 1.0);
319
320 let mut val = min + dx * (max - min);
321
322 val = step * (val / step).ceil();
323 val = val.clamp(min, max);
324
325 if let Some(callback) = self.on_change.take() {
326 (callback)(cx, val);
327
328 self.on_change = Some(callback);
329 }
330 }
331 }
332
333 WindowEvent::MouseUp(button) if *button == MouseButton::Left => {
334 self.is_dragging = false;
335 cx.focus_with_visibility(false);
336 cx.release();
337 cx.with_current(Entity::root(), |cx| {
338 cx.set_pointer_events(true);
339 });
340 }
341
342 WindowEvent::MouseMove(x, y) => {
343 if self.is_dragging {
344 let thumb = cx.get_entities_by_class("thumb").first().copied().unwrap();
345 let thumb_size = match self.orientation.get() {
346 Orientation::Horizontal => cx.cache.get_width(thumb),
347 Orientation::Vertical => cx.cache.get_height(thumb),
348 };
349
350 let min = self.range.get().start;
351 let max = self.range.get().end;
352 let step = self.step.get();
353
354 let current = cx.current();
355 let width = cx.cache.get_width(current);
356 let height = cx.cache.get_height(current);
357 let posx = cx.cache.get_posx(current);
358 let posy = cx.cache.get_posy(current);
359
360 let is_rtl = matches!(
361 cx.style.direction.get(current).copied(),
362 Some(Direction::RightToLeft)
363 );
364
365 let mut dx = match self.orientation.get() {
366 Orientation::Horizontal => {
367 let raw_dx = (*x - posx - thumb_size / 2.0) / (width - thumb_size);
368 if is_rtl { 1.0 - raw_dx } else { raw_dx }
369 }
370
371 Orientation::Vertical => {
372 (height - (*y - posy) - thumb_size / 2.0) / (height - thumb_size)
373 }
374 };
375
376 dx = dx.clamp(0.0, 1.0);
377
378 let mut val = min + dx * (max - min);
379
380 val = step * (val / step).ceil();
381 val = val.clamp(min, max);
382
383 if let Some(callback) = &self.on_change {
384 (callback)(cx, val);
385 }
386 }
387 }
388
389 WindowEvent::MouseDoubleClick(button) if *button == MouseButton::Left => {
390 let is_thumb_target = cx
391 .get_entities_by_class("thumb")
392 .first()
393 .copied()
394 .map(|thumb| thumb == meta.target)
395 .unwrap_or(false);
396
397 if is_thumb_target {
398 cx.focus_with_visibility(false);
399 cx.release();
400 cx.with_current(Entity::root(), |cx| {
401 cx.set_pointer_events(true);
402 });
403 self.is_dragging = false;
404 cx.emit(SliderEvent::ResetDefault);
405 }
406 }
407
408 WindowEvent::ActionRequest(action) => match action.action {
409 Action::Increment => {
410 let min = self.range.get().start;
411 let max = self.range.get().end;
412 let step = self.step.get();
413 let mut val = self.value.get() + step;
414 val = step * (val / step).ceil();
415 val = val.clamp(min, max);
416 if let Some(callback) = &self.on_change {
417 (callback)(cx, val);
418 }
419 }
420
421 Action::Decrement => {
422 let min = self.range.get().start;
423 let max = self.range.get().end;
424 let step = self.step.get();
425 let mut val = self.value.get() - step;
426 val = step * (val / step).ceil();
427 val = val.clamp(min, max);
428 if let Some(callback) = &self.on_change {
429 (callback)(cx, val);
430 }
431 }
432
433 Action::SetValue => {
434 if let Some(ActionData::NumericValue(val)) = action.data {
435 let min = self.range.get().start;
436 let max = self.range.get().end;
437 let mut v = val as f32;
438 v = v.clamp(min, max);
439 if let Some(callback) = &self.on_change {
440 (callback)(cx, v);
441 }
442 }
443 }
444
445 _ => {}
446 },
447
448 _ => {}
449 });
450 }
451}
452
453pub trait SliderModifiers: Sized {
454 fn on_change<F>(self, callback: F) -> Self
475 where
476 F: 'static + Fn(&mut EventContext, f32);
477
478 fn range<U: Into<Range<f32>> + Clone + 'static>(self, range: impl Res<U> + 'static) -> Self;
499
500 fn vertical<U: Into<bool> + Clone + 'static>(self, vertical: impl Res<U> + 'static) -> Self;
519
520 fn step<U: Into<f32> + Clone + 'static>(self, step: impl Res<U> + 'static) -> Self;
539
540 fn default_value<U: Into<f32> + Clone + 'static>(
542 self,
543 default_value: impl Res<U> + 'static,
544 ) -> Self;
545}
546
547impl<S> SliderModifiers for Handle<'_, Slider<S>>
548where
549 S: SignalGet<f32> + 'static,
550{
551 fn on_change<F>(self, callback: F) -> Self
552 where
553 F: 'static + Fn(&mut EventContext, f32),
554 {
555 self.modify(|slider| slider.on_change = Some(Box::new(callback)))
556 }
557
558 fn range<U: Into<Range<f32>> + Clone + 'static>(self, range: impl Res<U> + 'static) -> Self {
559 let range = range.to_signal(self.cx);
560 self.bind(range, move |handle| {
561 let range = range.get();
562 let range = range.into();
563 handle.modify(|slider| {
564 slider.range.set(range);
565 });
566 })
567 }
568
569 fn vertical<U: Into<bool> + Clone + 'static>(self, vertical: impl Res<U> + 'static) -> Self {
570 let vertical = vertical.to_signal(self.cx);
571 self.bind(vertical, move |handle| {
572 let vertical = vertical.get().into();
573
574 let orientation =
575 if vertical { Orientation::Vertical } else { Orientation::Horizontal };
576 handle.modify(|slider| {
577 slider.orientation.set(orientation);
578 });
579 })
580 }
581
582 fn step<U: Into<f32> + Clone + 'static>(self, step: impl Res<U> + 'static) -> Self {
583 let step = step.to_signal(self.cx);
584 self.bind(step, move |handle| {
585 let step = step.get();
586 let step = step.into();
587 handle.modify(|slider| {
588 slider.step.set(step);
589 });
590 })
591 }
592
593 fn default_value<U: Into<f32> + Clone + 'static>(
594 self,
595 default_value: impl Res<U> + 'static,
596 ) -> Self {
597 let default_value = default_value.to_signal(self.cx);
598 self.bind(default_value, move |handle| {
599 let default_value = default_value.get().into();
600 handle.modify(|slider| {
601 slider.default_value.set(default_value);
602 });
603 })
604 }
605}