1use crate::core::border::{self, Border};
32use crate::core::keyboard;
33use crate::core::keyboard::key::{self, Key};
34use crate::core::layout;
35use crate::core::mouse;
36use crate::core::renderer;
37use crate::core::touch;
38use crate::core::widget::tree::{self, Tree};
39use crate::core::window;
40use crate::core::{
41 self, Background, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point, Rectangle,
42 Shell, Size, Theme, Widget,
43};
44
45use std::ops::RangeInclusive;
46
47pub struct Slider<'a, T, Message, Theme = crate::Theme>
84where
85 Theme: Catalog,
86{
87 range: RangeInclusive<T>,
88 step: T,
89 shift_step: Option<T>,
90 value: T,
91 default: Option<T>,
92 on_change: Box<dyn Fn(T) -> Message + 'a>,
93 on_release: Option<Message>,
94 width: Length,
95 height: f32,
96 class: Theme::Class<'a>,
97 status: Option<Status>,
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 status: None,
145 }
146 }
147
148 pub fn default(mut self, default: impl Into<T>) -> Self {
152 self.default = Some(default.into());
153 self
154 }
155
156 pub fn on_release(mut self, on_release: Message) -> Self {
163 self.on_release = Some(on_release);
164 self
165 }
166
167 pub fn width(mut self, width: impl Into<Length>) -> Self {
169 self.width = width.into();
170 self
171 }
172
173 pub fn height(mut self, height: impl Into<Pixels>) -> Self {
175 self.height = height.into().0;
176 self
177 }
178
179 pub fn step(mut self, step: impl Into<T>) -> Self {
181 self.step = step.into();
182 self
183 }
184
185 pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
189 self.shift_step = Some(shift_step.into());
190 self
191 }
192
193 #[must_use]
195 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
196 where
197 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
198 {
199 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
200 self
201 }
202
203 #[cfg(feature = "advanced")]
205 #[must_use]
206 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
207 self.class = class.into();
208 self
209 }
210}
211
212impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Slider<'_, 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 &mut 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 update(
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 ) {
254 let state = tree.state.downcast_mut::<State>();
255
256 let mut update = || {
257 let current_value = self.value;
258
259 let locate = |cursor_position: Point| -> Option<T> {
260 let bounds = layout.bounds();
261
262 if cursor_position.x <= bounds.x {
263 Some(*self.range.start())
264 } else if cursor_position.x >= bounds.x + bounds.width {
265 Some(*self.range.end())
266 } else {
267 let step = if state.keyboard_modifiers.shift() {
268 self.shift_step.unwrap_or(self.step)
269 } else {
270 self.step
271 }
272 .into();
273
274 let start = (*self.range.start()).into();
275 let end = (*self.range.end()).into();
276
277 let percent = f64::from(cursor_position.x - bounds.x) / 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
286 let increment = |value: T| -> Option<T> {
287 let step = if state.keyboard_modifiers.shift() {
288 self.shift_step.unwrap_or(self.step)
289 } else {
290 self.step
291 }
292 .into();
293
294 let steps = (value.into() / step).round();
295 let new_value = step * (steps + 1.0);
296
297 if new_value > (*self.range.end()).into() {
298 return Some(*self.range.end());
299 }
300
301 T::from_f64(new_value)
302 };
303
304 let decrement = |value: T| -> Option<T> {
305 let step = if state.keyboard_modifiers.shift() {
306 self.shift_step.unwrap_or(self.step)
307 } else {
308 self.step
309 }
310 .into();
311
312 let steps = (value.into() / step).round();
313 let new_value = step * (steps - 1.0);
314
315 if new_value < (*self.range.start()).into() {
316 return Some(*self.range.start());
317 }
318
319 T::from_f64(new_value)
320 };
321
322 let change = |new_value: T| {
323 if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
324 shell.publish((self.on_change)(new_value));
325
326 self.value = new_value;
327 }
328 };
329
330 match &event {
331 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
332 | Event::Touch(touch::Event::FingerPressed { .. }) => {
333 if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
334 if state.keyboard_modifiers.command() {
335 let _ = self.default.map(change);
336 state.is_dragging = false;
337 } else {
338 let _ = locate(cursor_position).map(change);
339 state.is_dragging = true;
340 }
341
342 shell.capture_event();
343 }
344 }
345 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
346 | Event::Touch(touch::Event::FingerLifted { .. })
347 | Event::Touch(touch::Event::FingerLost { .. }) => {
348 if state.is_dragging {
349 if let Some(on_release) = self.on_release.clone() {
350 shell.publish(on_release);
351 }
352 state.is_dragging = false;
353 }
354 }
355 Event::Mouse(mouse::Event::CursorMoved { .. })
356 | Event::Touch(touch::Event::FingerMoved { .. }) => {
357 if state.is_dragging {
358 let _ = cursor.land().position().and_then(locate).map(change);
359
360 shell.capture_event();
361 }
362 }
363 Event::Mouse(mouse::Event::WheelScrolled { delta })
364 if state.keyboard_modifiers.control() =>
365 {
366 if cursor.is_over(layout.bounds()) {
367 let delta = match delta {
368 mouse::ScrollDelta::Lines { x: _, y } => y,
369 mouse::ScrollDelta::Pixels { x: _, y } => y,
370 };
371
372 if *delta < 0.0 {
373 let _ = decrement(current_value).map(change);
374 } else {
375 let _ = increment(current_value).map(change);
376 }
377
378 shell.capture_event();
379 }
380 }
381 Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
382 if cursor.is_over(layout.bounds()) {
383 match key {
384 Key::Named(key::Named::ArrowUp) => {
385 let _ = increment(current_value).map(change);
386 shell.capture_event();
387 }
388 Key::Named(key::Named::ArrowDown) => {
389 let _ = decrement(current_value).map(change);
390 shell.capture_event();
391 }
392 _ => (),
393 }
394 }
395 }
396 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
397 state.keyboard_modifiers = *modifiers;
398 }
399 _ => {}
400 }
401 };
402
403 update();
404
405 let current_status = if state.is_dragging {
406 Status::Dragged
407 } else if cursor.is_over(layout.bounds()) {
408 Status::Hovered
409 } else {
410 Status::Active
411 };
412
413 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
414 self.status = Some(current_status);
415 } else if self.status.is_some_and(|status| status != current_status) {
416 shell.request_redraw();
417 }
418 }
419
420 fn draw(
421 &self,
422 _tree: &Tree,
423 renderer: &mut Renderer,
424 theme: &Theme,
425 _style: &renderer::Style,
426 layout: Layout<'_>,
427 _cursor: mouse::Cursor,
428 _viewport: &Rectangle,
429 ) {
430 let bounds = layout.bounds();
431
432 let style = theme.style(&self.class, self.status.unwrap_or(Status::Active));
433
434 let (handle_width, handle_height, handle_border_radius) = match style.handle.shape {
435 HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius.into()),
436 HandleShape::Rectangle {
437 width,
438 border_radius,
439 } => (f32::from(width), bounds.height, border_radius),
440 };
441
442 let value = self.value.into() as f32;
443 let (range_start, range_end) = {
444 let (start, end) = self.range.clone().into_inner();
445
446 (start.into() as f32, end.into() as f32)
447 };
448
449 let offset = if range_start >= range_end {
450 0.0
451 } else {
452 (bounds.width - handle_width) * (value - range_start) / (range_end - range_start)
453 };
454
455 let rail_y = bounds.y + bounds.height / 2.0;
456
457 renderer.fill_quad(
458 renderer::Quad {
459 bounds: Rectangle {
460 x: bounds.x,
461 y: rail_y - style.rail.width / 2.0,
462 width: offset + handle_width / 2.0,
463 height: style.rail.width,
464 },
465 border: style.rail.border,
466 ..renderer::Quad::default()
467 },
468 style.rail.backgrounds.0,
469 );
470
471 renderer.fill_quad(
472 renderer::Quad {
473 bounds: Rectangle {
474 x: bounds.x + offset + handle_width / 2.0,
475 y: rail_y - style.rail.width / 2.0,
476 width: bounds.width - offset - handle_width / 2.0,
477 height: style.rail.width,
478 },
479 border: style.rail.border,
480 ..renderer::Quad::default()
481 },
482 style.rail.backgrounds.1,
483 );
484
485 renderer.fill_quad(
486 renderer::Quad {
487 bounds: Rectangle {
488 x: bounds.x + offset,
489 y: rail_y - handle_height / 2.0,
490 width: handle_width,
491 height: handle_height,
492 },
493 border: Border {
494 radius: handle_border_radius,
495 width: style.handle.border_width,
496 color: style.handle.border_color,
497 },
498 ..renderer::Quad::default()
499 },
500 style.handle.background,
501 );
502 }
503
504 fn mouse_interaction(
505 &self,
506 tree: &Tree,
507 layout: Layout<'_>,
508 cursor: mouse::Cursor,
509 _viewport: &Rectangle,
510 _renderer: &Renderer,
511 ) -> mouse::Interaction {
512 let state = tree.state.downcast_ref::<State>();
513
514 if state.is_dragging {
515 if cfg!(target_os = "windows") {
518 mouse::Interaction::Pointer
519 } else {
520 mouse::Interaction::Grabbing
521 }
522 } else if cursor.is_over(layout.bounds()) {
523 if cfg!(target_os = "windows") {
524 mouse::Interaction::Pointer
525 } else {
526 mouse::Interaction::Grab
527 }
528 } else {
529 mouse::Interaction::default()
530 }
531 }
532}
533
534impl<'a, T, Message, Theme, Renderer> From<Slider<'a, T, Message, Theme>>
535 for Element<'a, Message, Theme, Renderer>
536where
537 T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
538 Message: Clone + 'a,
539 Theme: Catalog + 'a,
540 Renderer: core::Renderer + 'a,
541{
542 fn from(slider: Slider<'a, T, Message, Theme>) -> 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, PartialEq)]
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, PartialEq)]
586pub struct Rail {
587 pub backgrounds: (Background, Background),
589 pub width: f32,
591 pub border: Border,
593}
594
595#[derive(Debug, Clone, Copy, PartialEq)]
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, PartialEq)]
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.base.color,
658 Status::Hovered => palette.primary.strong.color,
659 Status::Dragged => palette.primary.weak.color,
660 };
661
662 Style {
663 rail: Rail {
664 backgrounds: (color.into(), palette.background.strong.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}