1use crate::core::border::{self, Border};
32use crate::core::event::{self, Event};
33use crate::core::keyboard;
34use crate::core::keyboard::key::{self, Key};
35use crate::core::layout;
36use crate::core::mouse;
37use crate::core::renderer;
38use crate::core::touch;
39use crate::core::widget::tree::{self, Tree};
40use crate::core::{
41 self, Background, Clipboard, Color, Element, Layout, Length, Pixels, Point,
42 Rectangle, Shell, Size, Theme, Widget,
43};
44
45use std::ops::RangeInclusive;
46
47#[allow(missing_debug_implementations)]
84pub struct Slider<'a, T, Message, Theme = crate::Theme>
85where
86 Theme: Catalog,
87{
88 range: RangeInclusive<T>,
89 step: T,
90 shift_step: Option<T>,
91 value: T,
92 default: Option<T>,
93 on_change: Box<dyn Fn(T) -> Message + 'a>,
94 on_release: Option<Message>,
95 width: Length,
96 height: f32,
97 class: Theme::Class<'a>,
98}
99
100impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme>
101where
102 T: Copy + From<u8> + PartialOrd,
103 Message: Clone,
104 Theme: Catalog,
105{
106 pub const DEFAULT_HEIGHT: f32 = 16.0;
108
109 pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
118 where
119 F: 'a + Fn(T) -> Message,
120 {
121 let value = if value >= *range.start() {
122 value
123 } else {
124 *range.start()
125 };
126
127 let value = if value <= *range.end() {
128 value
129 } else {
130 *range.end()
131 };
132
133 Slider {
134 value,
135 default: None,
136 range,
137 step: T::from(1),
138 shift_step: None,
139 on_change: Box::new(on_change),
140 on_release: None,
141 width: Length::Fill,
142 height: Self::DEFAULT_HEIGHT,
143 class: Theme::default(),
144 }
145 }
146
147 pub fn default(mut self, default: impl Into<T>) -> Self {
151 self.default = Some(default.into());
152 self
153 }
154
155 pub fn on_release(mut self, on_release: Message) -> Self {
162 self.on_release = Some(on_release);
163 self
164 }
165
166 pub fn width(mut self, width: impl Into<Length>) -> Self {
168 self.width = width.into();
169 self
170 }
171
172 pub fn height(mut self, height: impl Into<Pixels>) -> Self {
174 self.height = height.into().0;
175 self
176 }
177
178 pub fn step(mut self, step: impl Into<T>) -> Self {
180 self.step = step.into();
181 self
182 }
183
184 pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
188 self.shift_step = Some(shift_step.into());
189 self
190 }
191
192 #[must_use]
194 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
195 where
196 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
197 {
198 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
199 self
200 }
201
202 #[cfg(feature = "advanced")]
204 #[must_use]
205 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
206 self.class = class.into();
207 self
208 }
209}
210
211impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
212 for Slider<'a, T, Message, Theme>
213where
214 T: Copy + Into<f64> + num_traits::FromPrimitive,
215 Message: Clone,
216 Theme: Catalog,
217 Renderer: core::Renderer,
218{
219 fn tag(&self) -> tree::Tag {
220 tree::Tag::of::<State>()
221 }
222
223 fn state(&self) -> tree::State {
224 tree::State::new(State::default())
225 }
226
227 fn size(&self) -> Size<Length> {
228 Size {
229 width: self.width,
230 height: Length::Shrink,
231 }
232 }
233
234 fn layout(
235 &self,
236 _tree: &mut Tree,
237 _renderer: &Renderer,
238 limits: &layout::Limits,
239 ) -> layout::Node {
240 layout::atomic(limits, self.width, self.height)
241 }
242
243 fn on_event(
244 &mut self,
245 tree: &mut Tree,
246 event: Event,
247 layout: Layout<'_>,
248 cursor: mouse::Cursor,
249 _renderer: &Renderer,
250 _clipboard: &mut dyn Clipboard,
251 shell: &mut Shell<'_, Message>,
252 _viewport: &Rectangle,
253 ) -> event::Status {
254 let state = tree.state.downcast_mut::<State>();
255
256 let is_dragging = state.is_dragging;
257 let current_value = self.value;
258
259 let locate = |cursor_position: Point| -> Option<T> {
260 let bounds = layout.bounds();
261 let new_value = if cursor_position.x <= bounds.x {
262 Some(*self.range.start())
263 } else if cursor_position.x >= bounds.x + bounds.width {
264 Some(*self.range.end())
265 } else {
266 let step = if state.keyboard_modifiers.shift() {
267 self.shift_step.unwrap_or(self.step)
268 } else {
269 self.step
270 }
271 .into();
272
273 let start = (*self.range.start()).into();
274 let end = (*self.range.end()).into();
275
276 let percent = f64::from(cursor_position.x - bounds.x)
277 / f64::from(bounds.width);
278
279 let steps = (percent * (end - start) / step).round();
280 let value = steps * step + start;
281
282 T::from_f64(value.min(end))
283 };
284
285 new_value
286 };
287
288 let increment = |value: T| -> Option<T> {
289 let step = if state.keyboard_modifiers.shift() {
290 self.shift_step.unwrap_or(self.step)
291 } else {
292 self.step
293 }
294 .into();
295
296 let steps = (value.into() / step).round();
297 let new_value = step * (steps + 1.0);
298
299 if new_value > (*self.range.end()).into() {
300 return Some(*self.range.end());
301 }
302
303 T::from_f64(new_value)
304 };
305
306 let decrement = |value: T| -> Option<T> {
307 let step = if state.keyboard_modifiers.shift() {
308 self.shift_step.unwrap_or(self.step)
309 } else {
310 self.step
311 }
312 .into();
313
314 let steps = (value.into() / step).round();
315 let new_value = step * (steps - 1.0);
316
317 if new_value < (*self.range.start()).into() {
318 return Some(*self.range.start());
319 }
320
321 T::from_f64(new_value)
322 };
323
324 let change = |new_value: T| {
325 if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
326 shell.publish((self.on_change)(new_value));
327
328 self.value = new_value;
329 }
330 };
331
332 match event {
333 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
334 | Event::Touch(touch::Event::FingerPressed { .. }) => {
335 if let Some(cursor_position) =
336 cursor.position_over(layout.bounds())
337 {
338 if state.keyboard_modifiers.command() {
339 let _ = self.default.map(change);
340 state.is_dragging = false;
341 } else {
342 let _ = locate(cursor_position).map(change);
343 state.is_dragging = true;
344 }
345
346 return event::Status::Captured;
347 }
348 }
349 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
350 | Event::Touch(touch::Event::FingerLifted { .. })
351 | Event::Touch(touch::Event::FingerLost { .. }) => {
352 if is_dragging {
353 if let Some(on_release) = self.on_release.clone() {
354 shell.publish(on_release);
355 }
356 state.is_dragging = false;
357
358 return event::Status::Captured;
359 }
360 }
361 Event::Mouse(mouse::Event::CursorMoved { .. })
362 | Event::Touch(touch::Event::FingerMoved { .. }) => {
363 if is_dragging {
364 let _ = cursor.position().and_then(locate).map(change);
365
366 return event::Status::Captured;
367 }
368 }
369 Event::Mouse(mouse::Event::WheelScrolled { delta })
370 if state.keyboard_modifiers.control() =>
371 {
372 if cursor.is_over(layout.bounds()) {
373 let delta = match delta {
374 mouse::ScrollDelta::Lines { x: _, y } => y,
375 mouse::ScrollDelta::Pixels { x: _, y } => y,
376 };
377
378 if delta < 0.0 {
379 let _ = decrement(current_value).map(change);
380 } else {
381 let _ = increment(current_value).map(change);
382 }
383
384 return event::Status::Captured;
385 }
386 }
387 Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
388 if cursor.is_over(layout.bounds()) {
389 match key {
390 Key::Named(key::Named::ArrowUp) => {
391 let _ = increment(current_value).map(change);
392 }
393 Key::Named(key::Named::ArrowDown) => {
394 let _ = decrement(current_value).map(change);
395 }
396 _ => (),
397 }
398
399 return event::Status::Captured;
400 }
401 }
402 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
403 state.keyboard_modifiers = modifiers;
404 }
405 _ => {}
406 }
407
408 event::Status::Ignored
409 }
410
411 fn draw(
412 &self,
413 tree: &Tree,
414 renderer: &mut Renderer,
415 theme: &Theme,
416 _style: &renderer::Style,
417 layout: Layout<'_>,
418 cursor: mouse::Cursor,
419 _viewport: &Rectangle,
420 ) {
421 let state = tree.state.downcast_ref::<State>();
422 let bounds = layout.bounds();
423 let is_mouse_over = cursor.is_over(bounds);
424
425 let style = theme.style(
426 &self.class,
427 if state.is_dragging {
428 Status::Dragged
429 } else if is_mouse_over {
430 Status::Hovered
431 } else {
432 Status::Active
433 },
434 );
435
436 let (handle_width, handle_height, handle_border_radius) =
437 match style.handle.shape {
438 HandleShape::Circle { radius } => {
439 (radius * 2.0, radius * 2.0, radius.into())
440 }
441 HandleShape::Rectangle {
442 width,
443 border_radius,
444 } => (f32::from(width), bounds.height, border_radius),
445 };
446
447 let value = self.value.into() as f32;
448 let (range_start, range_end) = {
449 let (start, end) = self.range.clone().into_inner();
450
451 (start.into() as f32, end.into() as f32)
452 };
453
454 let offset = if range_start >= range_end {
455 0.0
456 } else {
457 (bounds.width - handle_width) * (value - range_start)
458 / (range_end - range_start)
459 };
460
461 let rail_y = bounds.y + bounds.height / 2.0;
462
463 renderer.fill_quad(
464 renderer::Quad {
465 bounds: Rectangle {
466 x: bounds.x,
467 y: rail_y - style.rail.width / 2.0,
468 width: offset + handle_width / 2.0,
469 height: style.rail.width,
470 },
471 border: style.rail.border,
472 ..renderer::Quad::default()
473 },
474 style.rail.backgrounds.0,
475 );
476
477 renderer.fill_quad(
478 renderer::Quad {
479 bounds: Rectangle {
480 x: bounds.x + offset + handle_width / 2.0,
481 y: rail_y - style.rail.width / 2.0,
482 width: bounds.width - offset - handle_width / 2.0,
483 height: style.rail.width,
484 },
485 border: style.rail.border,
486 ..renderer::Quad::default()
487 },
488 style.rail.backgrounds.1,
489 );
490
491 renderer.fill_quad(
492 renderer::Quad {
493 bounds: Rectangle {
494 x: bounds.x + offset,
495 y: rail_y - handle_height / 2.0,
496 width: handle_width,
497 height: handle_height,
498 },
499 border: Border {
500 radius: handle_border_radius,
501 width: style.handle.border_width,
502 color: style.handle.border_color,
503 },
504 ..renderer::Quad::default()
505 },
506 style.handle.background,
507 );
508 }
509
510 fn mouse_interaction(
511 &self,
512 tree: &Tree,
513 layout: Layout<'_>,
514 cursor: mouse::Cursor,
515 _viewport: &Rectangle,
516 _renderer: &Renderer,
517 ) -> mouse::Interaction {
518 let state = tree.state.downcast_ref::<State>();
519 let bounds = layout.bounds();
520 let is_mouse_over = cursor.is_over(bounds);
521
522 if state.is_dragging {
523 mouse::Interaction::Grabbing
524 } else if is_mouse_over {
525 mouse::Interaction::Grab
526 } else {
527 mouse::Interaction::default()
528 }
529 }
530}
531
532impl<'a, T, Message, Theme, Renderer> From<Slider<'a, T, Message, Theme>>
533 for Element<'a, Message, Theme, Renderer>
534where
535 T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
536 Message: Clone + 'a,
537 Theme: Catalog + 'a,
538 Renderer: core::Renderer + 'a,
539{
540 fn from(
541 slider: Slider<'a, T, Message, Theme>,
542 ) -> Element<'a, Message, Theme, Renderer> {
543 Element::new(slider)
544 }
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
548struct State {
549 is_dragging: bool,
550 keyboard_modifiers: keyboard::Modifiers,
551}
552
553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
555pub enum Status {
556 Active,
558 Hovered,
560 Dragged,
562}
563
564#[derive(Debug, Clone, Copy)]
566pub struct Style {
567 pub rail: Rail,
569 pub handle: Handle,
571}
572
573impl Style {
574 pub fn with_circular_handle(mut self, radius: impl Into<Pixels>) -> Self {
577 self.handle.shape = HandleShape::Circle {
578 radius: radius.into().0,
579 };
580 self
581 }
582}
583
584#[derive(Debug, Clone, Copy)]
586pub struct Rail {
587 pub backgrounds: (Background, Background),
589 pub width: f32,
591 pub border: Border,
593}
594
595#[derive(Debug, Clone, Copy)]
597pub struct Handle {
598 pub shape: HandleShape,
600 pub background: Background,
602 pub border_width: f32,
604 pub border_color: Color,
606}
607
608#[derive(Debug, Clone, Copy)]
610pub enum HandleShape {
611 Circle {
613 radius: f32,
615 },
616 Rectangle {
618 width: u16,
620 border_radius: border::Radius,
622 },
623}
624
625pub trait Catalog: Sized {
627 type Class<'a>;
629
630 fn default<'a>() -> Self::Class<'a>;
632
633 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
635}
636
637pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
639
640impl Catalog for Theme {
641 type Class<'a> = StyleFn<'a, Self>;
642
643 fn default<'a>() -> Self::Class<'a> {
644 Box::new(default)
645 }
646
647 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
648 class(self, status)
649 }
650}
651
652pub fn default(theme: &Theme, status: Status) -> Style {
654 let palette = theme.extended_palette();
655
656 let color = match status {
657 Status::Active => palette.primary.strong.color,
658 Status::Hovered => palette.primary.base.color,
659 Status::Dragged => palette.primary.strong.color,
660 };
661
662 Style {
663 rail: Rail {
664 backgrounds: (color.into(), palette.secondary.base.color.into()),
665 width: 4.0,
666 border: Border {
667 radius: 2.0.into(),
668 width: 0.0,
669 color: Color::TRANSPARENT,
670 },
671 },
672 handle: Handle {
673 shape: HandleShape::Circle { radius: 7.0 },
674 background: color.into(),
675 border_color: Color::TRANSPARENT,
676 border_width: 0.0,
677 },
678 }
679}