1use std::ops::RangeInclusive;
32
33pub use crate::slider::{Handle, HandleShape, Rail, Style};
34
35use crate::core::border::Border;
36use crate::core::keyboard;
37use crate::core::keyboard::key::{self, Key};
38use crate::core::layout::{self, Layout};
39use crate::core::mouse;
40use crate::core::renderer;
41use crate::core::theme::palette;
42use crate::core::touch;
43use crate::core::widget::Operation;
44use crate::core::widget::operation::accessible::{Accessible, Orientation, Role, Value};
45use crate::core::widget::operation::focusable::Focusable;
46use crate::core::widget::tree::{self, Tree};
47use crate::core::window;
48use crate::core::{
49 self, Color, Element, Event, Length, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme,
50 Widget,
51};
52
53pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme>
90where
91 Theme: Catalog,
92{
93 range: RangeInclusive<T>,
94 step: T,
95 shift_step: Option<T>,
96 value: T,
97 default: Option<T>,
98 on_change: Box<dyn Fn(T) -> Message + 'a>,
99 on_release: Option<Message>,
100 width: f32,
101 height: Length,
102 class: Theme::Class<'a>,
103 status: Option<Status>,
104}
105
106impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme>
107where
108 T: Copy + From<u8> + std::cmp::PartialOrd,
109 Message: Clone,
110 Theme: Catalog,
111{
112 pub const DEFAULT_WIDTH: f32 = 16.0;
114
115 pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
124 where
125 F: 'a + Fn(T) -> Message,
126 {
127 let value = if value >= *range.start() {
128 value
129 } else {
130 *range.start()
131 };
132
133 let value = if value <= *range.end() {
134 value
135 } else {
136 *range.end()
137 };
138
139 VerticalSlider {
140 value,
141 default: None,
142 range,
143 step: T::from(1),
144 shift_step: None,
145 on_change: Box::new(on_change),
146 on_release: None,
147 width: Self::DEFAULT_WIDTH,
148 height: Length::Fill,
149 class: Theme::default(),
150 status: None,
151 }
152 }
153
154 pub fn default(mut self, default: impl Into<T>) -> Self {
158 self.default = Some(default.into());
159 self
160 }
161
162 pub fn on_release(mut self, on_release: Message) -> Self {
169 self.on_release = Some(on_release);
170 self
171 }
172
173 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
175 self.width = width.into().0;
176 self
177 }
178
179 pub fn height(mut self, height: impl Into<Length>) -> Self {
181 self.height = height.into();
182 self
183 }
184
185 pub fn step(mut self, step: T) -> Self {
187 self.step = step;
188 self
189 }
190
191 pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
195 self.shift_step = Some(shift_step.into());
196 self
197 }
198
199 #[must_use]
201 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
202 where
203 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
204 {
205 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
206 self
207 }
208
209 #[cfg(feature = "advanced")]
211 #[must_use]
212 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
213 self.class = class.into();
214 self
215 }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
219struct State {
220 is_dragging: bool,
221 is_focused: bool,
222 focus_visible: bool,
223 keyboard_modifiers: keyboard::Modifiers,
224}
225
226impl Focusable for State {
227 fn is_focused(&self) -> bool {
228 self.is_focused
229 }
230
231 fn focus(&mut self) {
232 self.is_focused = true;
233 self.focus_visible = true;
234 }
235
236 fn unfocus(&mut self) {
237 self.is_focused = false;
238 self.focus_visible = false;
239 }
240}
241
242impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
243 for VerticalSlider<'_, T, Message, Theme>
244where
245 T: Copy + Into<f64> + num_traits::FromPrimitive,
246 Message: Clone,
247 Theme: Catalog,
248 Renderer: core::Renderer,
249{
250 fn tag(&self) -> tree::Tag {
251 tree::Tag::of::<State>()
252 }
253
254 fn state(&self) -> tree::State {
255 tree::State::new(State::default())
256 }
257
258 fn size(&self) -> Size<Length> {
259 Size {
260 width: Length::Shrink,
261 height: self.height,
262 }
263 }
264
265 fn layout(
266 &mut self,
267 _tree: &mut Tree,
268 _renderer: &Renderer,
269 limits: &layout::Limits,
270 ) -> layout::Node {
271 layout::atomic(limits, self.width, self.height)
272 }
273
274 fn operate(
275 &mut self,
276 tree: &mut Tree,
277 layout: Layout<'_>,
278 _renderer: &Renderer,
279 operation: &mut dyn Operation,
280 ) {
281 let state = tree.state.downcast_mut::<State>();
282
283 operation.accessible(
284 None,
285 layout.bounds(),
286 &Accessible {
287 role: Role::Slider,
288 value: Some(Value::Numeric {
289 current: self.value.into(),
290 min: (*self.range.start()).into(),
291 max: (*self.range.end()).into(),
292 step: Some(self.step.into()),
293 }),
294 orientation: Some(Orientation::Vertical),
295 ..Accessible::default()
296 },
297 );
298
299 operation.focusable(None, layout.bounds(), state);
300 }
301
302 fn update(
303 &mut self,
304 tree: &mut Tree,
305 event: &Event,
306 layout: Layout<'_>,
307 cursor: mouse::Cursor,
308 _renderer: &Renderer,
309 shell: &mut Shell<'_, Message>,
310 _viewport: &Rectangle,
311 ) {
312 let state = tree.state.downcast_mut::<State>();
313 let is_dragging = state.is_dragging;
314 let current_value = self.value;
315
316 let locate = |cursor_position: Point| -> Option<T> {
317 let bounds = layout.bounds();
318
319 if cursor_position.y >= bounds.y + bounds.height {
320 Some(*self.range.start())
321 } else if cursor_position.y <= bounds.y {
322 Some(*self.range.end())
323 } else {
324 let step = if state.keyboard_modifiers.shift() {
325 self.shift_step.unwrap_or(self.step)
326 } else {
327 self.step
328 }
329 .into();
330
331 let start = (*self.range.start()).into();
332 let end = (*self.range.end()).into();
333
334 let percent =
335 1.0 - f64::from(cursor_position.y - bounds.y) / f64::from(bounds.height);
336
337 let steps = (percent * (end - start) / step).round();
338 let value = steps * step + start;
339
340 T::from_f64(value.min(end))
341 }
342 };
343
344 let increment = |value: T| -> Option<T> {
345 let step = if state.keyboard_modifiers.shift() {
346 self.shift_step.unwrap_or(self.step)
347 } else {
348 self.step
349 }
350 .into();
351
352 let steps = (value.into() / step).round();
353 let new_value = step * (steps + 1.0);
354
355 if new_value > (*self.range.end()).into() {
356 return Some(*self.range.end());
357 }
358
359 T::from_f64(new_value)
360 };
361
362 let decrement = |value: T| -> Option<T> {
363 let step = if state.keyboard_modifiers.shift() {
364 self.shift_step.unwrap_or(self.step)
365 } else {
366 self.step
367 }
368 .into();
369
370 let steps = (value.into() / step).round();
371 let new_value = step * (steps - 1.0);
372
373 if new_value < (*self.range.start()).into() {
374 return Some(*self.range.start());
375 }
376
377 T::from_f64(new_value)
378 };
379
380 let page_increment = |value: T| -> Option<T> {
381 let step = self.step.into() * 10.0;
382 let new_value = value.into() + step;
383
384 if new_value > (*self.range.end()).into() {
385 return Some(*self.range.end());
386 }
387
388 T::from_f64(new_value)
389 };
390
391 let page_decrement = |value: T| -> Option<T> {
392 let step = self.step.into() * 10.0;
393 let new_value = value.into() - step;
394
395 if new_value < (*self.range.start()).into() {
396 return Some(*self.range.start());
397 }
398
399 T::from_f64(new_value)
400 };
401
402 let mut change = |new_value: T| {
403 if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
404 shell.publish((self.on_change)(new_value));
405
406 self.value = new_value;
407 }
408 };
409
410 match event {
411 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
412 | Event::Touch(touch::Event::FingerPressed { .. }) => {
413 if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
414 if state.keyboard_modifiers.control() || state.keyboard_modifiers.command() {
415 let _ = self.default.map(change);
416 state.is_dragging = false;
417 } else {
418 let _ = locate(cursor_position).map(change);
419 state.is_dragging = true;
420 }
421
422 state.is_focused = true;
423 state.focus_visible = false;
424
425 shell.capture_event();
426 } else {
427 state.is_focused = false;
428 state.focus_visible = false;
429 }
430 }
431 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
432 | Event::Touch(touch::Event::FingerLifted { .. })
433 | Event::Touch(touch::Event::FingerLost { .. }) => {
434 if is_dragging {
435 if let Some(on_release) = self.on_release.clone() {
436 shell.publish(on_release);
437 }
438 state.is_dragging = false;
439 }
440 }
441 Event::Mouse(mouse::Event::CursorMoved { .. })
442 | Event::Touch(touch::Event::FingerMoved { .. }) => {
443 if is_dragging {
444 let _ = cursor.land().position().and_then(locate).map(change);
445
446 shell.capture_event();
447 }
448 }
449 Event::Mouse(mouse::Event::WheelScrolled { delta })
450 if state.keyboard_modifiers.control() =>
451 {
452 if cursor.is_over(layout.bounds()) {
453 let delta = match *delta {
454 mouse::ScrollDelta::Lines { x: _, y } => y,
455 mouse::ScrollDelta::Pixels { x: _, y } => y,
456 };
457
458 if delta < 0.0 {
459 let _ = decrement(current_value).map(change);
460 } else {
461 let _ = increment(current_value).map(change);
462 }
463
464 shell.capture_event();
465 }
466 }
467 Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
468 if cursor.is_over(layout.bounds()) || state.is_focused {
469 match key {
470 Key::Named(key::Named::ArrowUp | key::Named::ArrowRight) => {
471 let _ = increment(current_value).map(change);
472 shell.capture_event();
473 }
474 Key::Named(key::Named::ArrowDown | key::Named::ArrowLeft) => {
475 let _ = decrement(current_value).map(change);
476 shell.capture_event();
477 }
478 Key::Named(key::Named::PageUp) => {
479 let _ = page_increment(current_value).map(change);
480 shell.capture_event();
481 }
482 Key::Named(key::Named::PageDown) => {
483 let _ = page_decrement(current_value).map(change);
484 shell.capture_event();
485 }
486 Key::Named(key::Named::Home) => {
487 change(*self.range.start());
488 shell.capture_event();
489 }
490 Key::Named(key::Named::End) => {
491 change(*self.range.end());
492 shell.capture_event();
493 }
494 Key::Named(key::Named::Escape) => {
495 if state.is_focused {
496 state.is_focused = false;
497 state.focus_visible = false;
498 shell.capture_event();
499 }
500 }
501 _ => (),
502 }
503 }
504 }
505 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
506 state.keyboard_modifiers = *modifiers;
507 }
508 _ => {}
509 }
510
511 let current_status = if state.is_dragging {
512 Status::Dragged
513 } else if state.focus_visible {
514 Status::Focused
515 } else if cursor.is_over(layout.bounds()) {
516 Status::Hovered
517 } else {
518 Status::Active
519 };
520
521 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
522 self.status = Some(current_status);
523 } else if self.status.is_some_and(|status| status != current_status) {
524 shell.request_redraw();
525 }
526 }
527
528 fn draw(
529 &self,
530 _tree: &Tree,
531 renderer: &mut Renderer,
532 theme: &Theme,
533 _style: &renderer::Style,
534 layout: Layout<'_>,
535 _cursor: mouse::Cursor,
536 _viewport: &Rectangle,
537 ) {
538 let bounds = layout.bounds();
539
540 let style = theme.style(&self.class, self.status.unwrap_or(Status::Active));
541
542 let (handle_width, handle_height, handle_border_radius) = match style.handle.shape {
543 HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius.into()),
544 HandleShape::Rectangle {
545 width,
546 border_radius,
547 } => (f32::from(width), bounds.width, border_radius),
548 };
549
550 let value = self.value.into() as f32;
551 let (range_start, range_end) = {
552 let (start, end) = self.range.clone().into_inner();
553
554 (start.into() as f32, end.into() as f32)
555 };
556
557 let offset = if range_start >= range_end {
558 0.0
559 } else {
560 (bounds.height - handle_width) * (value - range_end) / (range_start - range_end)
561 };
562
563 let rail_x = bounds.x + bounds.width / 2.0;
564
565 renderer.fill_quad(
566 renderer::Quad {
567 bounds: Rectangle {
568 x: rail_x - style.rail.width / 2.0,
569 y: bounds.y,
570 width: style.rail.width,
571 height: offset + handle_width / 2.0,
572 },
573 border: style.rail.border,
574 ..renderer::Quad::default()
575 },
576 style.rail.backgrounds.1,
577 );
578
579 renderer.fill_quad(
580 renderer::Quad {
581 bounds: Rectangle {
582 x: rail_x - style.rail.width / 2.0,
583 y: bounds.y + offset + handle_width / 2.0,
584 width: style.rail.width,
585 height: bounds.height - offset - handle_width / 2.0,
586 },
587 border: style.rail.border,
588 ..renderer::Quad::default()
589 },
590 style.rail.backgrounds.0,
591 );
592
593 renderer.fill_quad(
594 renderer::Quad {
595 bounds: Rectangle {
596 x: rail_x - handle_height / 2.0,
597 y: bounds.y + offset,
598 width: handle_height,
599 height: handle_width,
600 },
601 border: Border {
602 radius: handle_border_radius,
603 width: style.handle.border_width,
604 color: style.handle.border_color,
605 },
606 shadow: style.handle.shadow,
607 ..renderer::Quad::default()
608 },
609 style.handle.background,
610 );
611 }
612
613 fn mouse_interaction(
614 &self,
615 tree: &Tree,
616 layout: Layout<'_>,
617 cursor: mouse::Cursor,
618 _viewport: &Rectangle,
619 _renderer: &Renderer,
620 ) -> mouse::Interaction {
621 let state = tree.state.downcast_ref::<State>();
622
623 if state.is_dragging {
624 if cfg!(target_os = "windows") {
627 mouse::Interaction::Pointer
628 } else {
629 mouse::Interaction::Grabbing
630 }
631 } else if cursor.is_over(layout.bounds()) {
632 if cfg!(target_os = "windows") {
633 mouse::Interaction::Pointer
634 } else {
635 mouse::Interaction::Grab
636 }
637 } else {
638 mouse::Interaction::default()
639 }
640 }
641}
642
643impl<'a, T, Message, Theme, Renderer> From<VerticalSlider<'a, T, Message, Theme>>
644 for Element<'a, Message, Theme, Renderer>
645where
646 T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
647 Message: Clone + 'a,
648 Theme: Catalog + 'a,
649 Renderer: core::Renderer + 'a,
650{
651 fn from(
652 slider: VerticalSlider<'a, T, Message, Theme>,
653 ) -> Element<'a, Message, Theme, Renderer> {
654 Element::new(slider)
655 }
656}
657
658#[derive(Debug, Clone, Copy, PartialEq, Eq)]
660pub enum Status {
661 Active,
663 Hovered,
665 Dragged,
667 Focused,
669}
670
671pub trait Catalog: Sized {
673 type Class<'a>;
675
676 fn default<'a>() -> Self::Class<'a>;
678
679 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
681}
682
683pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
685
686impl Catalog for Theme {
687 type Class<'a> = StyleFn<'a, Self>;
688
689 fn default<'a>() -> Self::Class<'a> {
690 Box::new(default)
691 }
692
693 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
694 class(self, status)
695 }
696}
697
698pub fn default(theme: &Theme, status: Status) -> Style {
700 let palette = theme.palette();
701
702 let color = match status {
703 Status::Active => palette.primary.base.color,
704 Status::Hovered => palette.primary.strong.color,
705 Status::Dragged => palette.primary.weak.color,
706 Status::Focused => palette.primary.strong.color,
707 };
708
709 Style {
710 rail: Rail {
711 backgrounds: (color.into(), palette.background.strong.color.into()),
712 width: 4.0,
713 border: Border {
714 radius: 2.0.into(),
715 width: 0.0,
716 color: Color::TRANSPARENT,
717 },
718 },
719 handle: Handle {
720 shape: HandleShape::Circle { radius: 7.0 },
721 background: color.into(),
722 border_color: match status {
723 Status::Focused => {
724 let accent = palette.primary.strong.color;
725 let page_bg = palette.background.base.color;
726 palette::focus_border_color(color, accent, page_bg)
727 }
728 _ => Color::TRANSPARENT,
729 },
730 border_width: match status {
731 Status::Focused => 2.0,
732 _ => 0.0,
733 },
734 shadow: match status {
735 Status::Focused => palette::focus_shadow(
736 palette.primary.strong.color,
737 palette.background.base.color,
738 ),
739 _ => Shadow::default(),
740 },
741 },
742 }
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748 use crate::core::widget::operation::focusable::Focusable;
749
750 #[test]
751 fn focusable_trait() {
752 let mut state = State::default();
753 assert!(!state.is_focused());
754 assert!(!state.focus_visible);
755 state.focus();
756 assert!(state.is_focused());
757 assert!(state.focus_visible);
758 state.unfocus();
759 assert!(!state.is_focused());
760 assert!(!state.focus_visible);
761 }
762
763 #[test]
764 fn default_state_not_focused() {
765 let state = State::default();
766 assert!(!state.is_focused);
767 assert!(!state.is_dragging);
768 assert!(!state.focus_visible);
769 }
770
771 #[test]
772 fn focus_independent_of_drag() {
773 let mut state = State::default();
774
775 state.focus();
776 assert!(!state.is_dragging);
777
778 state.is_dragging = true;
779 assert!(state.is_focused());
780
781 state.unfocus();
782 assert!(state.is_dragging);
783 }
784}