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 current = cx.current();
287 let bounds = cx.transformed_bounds(current);
288 let thumb_bounds = cx.transformed_bounds(thumb);
289 let thumb_size = match self.orientation.get() {
290 Orientation::Horizontal => thumb_bounds.width(),
291 Orientation::Vertical => thumb_bounds.height(),
292 };
293 let min = self.range.get().start;
294 let max = self.range.get().end;
295 let step = self.step.get();
296
297 let is_rtl = matches!(
298 cx.style.direction.get(current).copied(),
299 Some(Direction::RightToLeft)
300 );
301
302 let mut dx = match self.orientation.get() {
303 Orientation::Horizontal => {
304 let span = (bounds.width() - thumb_size).max(f32::EPSILON);
305 let raw_dx =
306 (cx.mouse.left.pos_down.0 - bounds.left() - thumb_size / 2.0)
307 / span;
308 if is_rtl { 1.0 - raw_dx } else { raw_dx }
309 }
310
311 Orientation::Vertical => {
312 let span = (bounds.height() - thumb_size).max(f32::EPSILON);
313 (bounds.height()
314 - (cx.mouse.left.pos_down.1 - bounds.top())
315 - thumb_size / 2.0)
316 / span
317 }
318 };
319
320 dx = dx.clamp(0.0, 1.0);
321
322 let mut val = min + dx * (max - min);
323
324 val = step * (val / step).ceil();
325 val = val.clamp(min, max);
326
327 if let Some(callback) = self.on_change.take() {
328 (callback)(cx, val);
329
330 self.on_change = Some(callback);
331 }
332 }
333 }
334
335 WindowEvent::MouseUp(button) if *button == MouseButton::Left => {
336 self.is_dragging = false;
337 cx.focus_with_visibility(false);
338 cx.release();
339 cx.with_current(Entity::root(), |cx| {
340 cx.set_pointer_events(true);
341 });
342 }
343
344 WindowEvent::MouseMove(x, y) => {
345 if self.is_dragging {
346 let thumb = cx.get_entities_by_class("thumb").first().copied().unwrap();
347 let current = cx.current();
348 let bounds = cx.transformed_bounds(current);
349 let thumb_bounds = cx.transformed_bounds(thumb);
350 let thumb_size = match self.orientation.get() {
351 Orientation::Horizontal => thumb_bounds.width(),
352 Orientation::Vertical => thumb_bounds.height(),
353 };
354
355 let min = self.range.get().start;
356 let max = self.range.get().end;
357 let step = self.step.get();
358
359 let is_rtl = matches!(
360 cx.style.direction.get(current).copied(),
361 Some(Direction::RightToLeft)
362 );
363
364 let mut dx = match self.orientation.get() {
365 Orientation::Horizontal => {
366 let span = (bounds.width() - thumb_size).max(f32::EPSILON);
367 let raw_dx = (*x - bounds.left() - thumb_size / 2.0) / span;
368 if is_rtl { 1.0 - raw_dx } else { raw_dx }
369 }
370
371 Orientation::Vertical => {
372 let span = (bounds.height() - thumb_size).max(f32::EPSILON);
373 (bounds.height() - (*y - bounds.top()) - thumb_size / 2.0) / span
374 }
375 };
376
377 dx = dx.clamp(0.0, 1.0);
378
379 let mut val = min + dx * (max - min);
380
381 val = step * (val / step).ceil();
382 val = val.clamp(min, max);
383
384 if let Some(callback) = &self.on_change {
385 (callback)(cx, val);
386 }
387 }
388 }
389
390 WindowEvent::MouseDoubleClick(button) if *button == MouseButton::Left => {
391 let is_thumb_target = cx
392 .get_entities_by_class("thumb")
393 .first()
394 .copied()
395 .map(|thumb| thumb == meta.target)
396 .unwrap_or(false);
397
398 if is_thumb_target {
399 cx.focus_with_visibility(false);
400 cx.release();
401 cx.with_current(Entity::root(), |cx| {
402 cx.set_pointer_events(true);
403 });
404 self.is_dragging = false;
405 cx.emit(SliderEvent::ResetDefault);
406 }
407 }
408
409 WindowEvent::ActionRequest(action) => match action.action {
410 Action::Increment => {
411 let min = self.range.get().start;
412 let max = self.range.get().end;
413 let step = self.step.get();
414 let mut val = self.value.get() + step;
415 val = step * (val / step).ceil();
416 val = val.clamp(min, max);
417 if let Some(callback) = &self.on_change {
418 (callback)(cx, val);
419 }
420 }
421
422 Action::Decrement => {
423 let min = self.range.get().start;
424 let max = self.range.get().end;
425 let step = self.step.get();
426 let mut val = self.value.get() - step;
427 val = step * (val / step).ceil();
428 val = val.clamp(min, max);
429 if let Some(callback) = &self.on_change {
430 (callback)(cx, val);
431 }
432 }
433
434 Action::SetValue => {
435 if let Some(ActionData::NumericValue(val)) = action.data {
436 let min = self.range.get().start;
437 let max = self.range.get().end;
438 let mut v = val as f32;
439 v = v.clamp(min, max);
440 if let Some(callback) = &self.on_change {
441 (callback)(cx, v);
442 }
443 }
444 }
445
446 _ => {}
447 },
448
449 _ => {}
450 });
451 }
452}
453
454pub trait SliderModifiers: Sized {
455 fn on_change<F>(self, callback: F) -> Self
476 where
477 F: 'static + Fn(&mut EventContext, f32);
478
479 fn range<U: Into<Range<f32>> + Clone + 'static>(self, range: impl Res<U> + 'static) -> Self;
500
501 fn vertical<U: Into<bool> + Clone + 'static>(self, vertical: impl Res<U> + 'static) -> Self;
520
521 fn step<U: Into<f32> + Clone + 'static>(self, step: impl Res<U> + 'static) -> Self;
540
541 fn default_value<U: Into<f32> + Clone + 'static>(
543 self,
544 default_value: impl Res<U> + 'static,
545 ) -> Self;
546}
547
548impl<S> SliderModifiers for Handle<'_, Slider<S>>
549where
550 S: SignalGet<f32> + 'static,
551{
552 fn on_change<F>(self, callback: F) -> Self
553 where
554 F: 'static + Fn(&mut EventContext, f32),
555 {
556 self.modify(|slider| slider.on_change = Some(Box::new(callback)))
557 }
558
559 fn range<U: Into<Range<f32>> + Clone + 'static>(self, range: impl Res<U> + 'static) -> Self {
560 let range = range.to_signal(self.cx);
561 self.bind(range, move |handle| {
562 let range = range.get();
563 let range = range.into();
564 handle.modify(|slider| {
565 slider.range.set(range);
566 });
567 })
568 }
569
570 fn vertical<U: Into<bool> + Clone + 'static>(self, vertical: impl Res<U> + 'static) -> Self {
571 let vertical = vertical.to_signal(self.cx);
572 self.bind(vertical, move |handle| {
573 let vertical = vertical.get().into();
574
575 let orientation =
576 if vertical { Orientation::Vertical } else { Orientation::Horizontal };
577 handle.modify(|slider| {
578 slider.orientation.set(orientation);
579 });
580 })
581 }
582
583 fn step<U: Into<f32> + Clone + 'static>(self, step: impl Res<U> + 'static) -> Self {
584 let step = step.to_signal(self.cx);
585 self.bind(step, move |handle| {
586 let step = step.get();
587 let step = step.into();
588 handle.modify(|slider| {
589 slider.step.set(step);
590 });
591 })
592 }
593
594 fn default_value<U: Into<f32> + Clone + 'static>(
595 self,
596 default_value: impl Res<U> + 'static,
597 ) -> Self {
598 let default_value = default_value.to_signal(self.cx);
599 self.bind(default_value, move |handle| {
600 let default_value = default_value.get().into();
601 handle.modify(|slider| {
602 slider.default_value.set(default_value);
603 });
604 })
605 }
606}