1use std::ops::RangeInclusive;
32
33pub use crate::slider::{
34 default, Catalog, Handle, HandleShape, RailBackground, Status, Style,
35 StyleFn,
36};
37
38use crate::core::border::Border;
39use crate::core::event::{self, Event};
40use crate::core::keyboard;
41use crate::core::keyboard::key::{self, Key};
42use crate::core::layout::{self, Layout};
43use crate::core::mouse;
44use crate::core::renderer;
45use crate::core::touch;
46use crate::core::widget::tree::{self, Tree};
47use crate::core::{
48 self, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, Size,
49 Widget,
50};
51
52#[allow(missing_debug_implementations)]
89pub 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}
104
105impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme>
106where
107 T: Copy + From<u8> + std::cmp::PartialOrd,
108 Message: Clone,
109 Theme: Catalog,
110{
111 pub const DEFAULT_WIDTH: f32 = 16.0;
113
114 pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
123 where
124 F: 'a + Fn(T) -> Message,
125 {
126 let value = if value >= *range.start() {
127 value
128 } else {
129 *range.start()
130 };
131
132 let value = if value <= *range.end() {
133 value
134 } else {
135 *range.end()
136 };
137
138 VerticalSlider {
139 value,
140 default: None,
141 range,
142 step: T::from(1),
143 shift_step: None,
144 on_change: Box::new(on_change),
145 on_release: None,
146 width: Self::DEFAULT_WIDTH,
147 height: Length::Fill,
148 class: Theme::default(),
149 }
150 }
151
152 pub fn default(mut self, default: impl Into<T>) -> Self {
156 self.default = Some(default.into());
157 self
158 }
159
160 pub fn on_release(mut self, on_release: Message) -> Self {
167 self.on_release = Some(on_release);
168 self
169 }
170
171 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
173 self.width = width.into().0;
174 self
175 }
176
177 pub fn height(mut self, height: impl Into<Length>) -> Self {
179 self.height = height.into();
180 self
181 }
182
183 pub fn step(mut self, step: T) -> Self {
185 self.step = step;
186 self
187 }
188
189 pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
193 self.shift_step = Some(shift_step.into());
194 self
195 }
196
197 #[must_use]
199 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
200 where
201 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
202 {
203 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
204 self
205 }
206
207 #[cfg(feature = "advanced")]
209 #[must_use]
210 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
211 self.class = class.into();
212 self
213 }
214}
215
216impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
217 for VerticalSlider<'a, T, Message, Theme>
218where
219 T: Copy + Into<f64> + num_traits::FromPrimitive,
220 Message: Clone,
221 Theme: Catalog,
222 Renderer: core::Renderer,
223{
224 fn tag(&self) -> tree::Tag {
225 tree::Tag::of::<State>()
226 }
227
228 fn state(&self) -> tree::State {
229 tree::State::new(State::default())
230 }
231
232 fn size(&self) -> Size<Length> {
233 Size {
234 width: Length::Shrink,
235 height: self.height,
236 }
237 }
238
239 fn layout(
240 &self,
241 _tree: &mut Tree,
242 _renderer: &Renderer,
243 limits: &layout::Limits,
244 ) -> layout::Node {
245 layout::atomic(limits, self.width, self.height)
246 }
247
248 fn on_event(
249 &mut self,
250 tree: &mut Tree,
251 event: Event,
252 layout: Layout<'_>,
253 cursor: mouse::Cursor,
254 _renderer: &Renderer,
255 _clipboard: &mut dyn Clipboard,
256 shell: &mut Shell<'_, Message>,
257 _viewport: &Rectangle,
258 ) -> event::Status {
259 let state = tree.state.downcast_mut::<State>();
260 let is_dragging = state.is_dragging;
261 let current_value = self.value;
262
263 let locate = |cursor_position: Point| -> Option<T> {
264 let bounds = layout.bounds();
265
266 let new_value = if cursor_position.y >= bounds.y + bounds.height {
267 Some(*self.range.start())
268 } else if cursor_position.y <= bounds.y {
269 Some(*self.range.end())
270 } else {
271 let step = if state.keyboard_modifiers.shift() {
272 self.shift_step.unwrap_or(self.step)
273 } else {
274 self.step
275 }
276 .into();
277
278 let start = (*self.range.start()).into();
279 let end = (*self.range.end()).into();
280
281 let percent = 1.0
282 - f64::from(cursor_position.y - bounds.y)
283 / f64::from(bounds.height);
284
285 let steps = (percent * (end - start) / step).round();
286 let value = steps * step + start;
287
288 T::from_f64(value.min(end))
289 };
290
291 new_value
292 };
293
294 let increment = |value: T| -> Option<T> {
295 let step = if state.keyboard_modifiers.shift() {
296 self.shift_step.unwrap_or(self.step)
297 } else {
298 self.step
299 }
300 .into();
301
302 let steps = (value.into() / step).round();
303 let new_value = step * (steps + 1.0);
304
305 if new_value > (*self.range.end()).into() {
306 return Some(*self.range.end());
307 }
308
309 T::from_f64(new_value)
310 };
311
312 let decrement = |value: T| -> Option<T> {
313 let step = if state.keyboard_modifiers.shift() {
314 self.shift_step.unwrap_or(self.step)
315 } else {
316 self.step
317 }
318 .into();
319
320 let steps = (value.into() / step).round();
321 let new_value = step * (steps - 1.0);
322
323 if new_value < (*self.range.start()).into() {
324 return Some(*self.range.start());
325 }
326
327 T::from_f64(new_value)
328 };
329
330 let change = |new_value: T| {
331 if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
332 shell.publish((self.on_change)(new_value));
333
334 self.value = new_value;
335 }
336 };
337
338 match event {
339 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
340 | Event::Touch(touch::Event::FingerPressed { .. }) => {
341 if let Some(cursor_position) =
342 cursor.position_over(layout.bounds())
343 {
344 if state.keyboard_modifiers.control()
345 || state.keyboard_modifiers.command()
346 {
347 let _ = self.default.map(change);
348 state.is_dragging = false;
349 } else {
350 let _ = locate(cursor_position).map(change);
351 state.is_dragging = true;
352 }
353
354 return event::Status::Captured;
355 }
356 }
357 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
358 | Event::Touch(touch::Event::FingerLifted { .. })
359 | Event::Touch(touch::Event::FingerLost { .. }) => {
360 if is_dragging {
361 if let Some(on_release) = self.on_release.clone() {
362 shell.publish(on_release);
363 }
364 state.is_dragging = false;
365
366 return event::Status::Captured;
367 }
368 }
369 Event::Mouse(mouse::Event::CursorMoved { .. })
370 | Event::Touch(touch::Event::FingerMoved { .. }) => {
371 if is_dragging {
372 let _ = cursor.position().and_then(locate).map(change);
373
374 return event::Status::Captured;
375 }
376 }
377 Event::Mouse(mouse::Event::WheelScrolled { delta })
378 if state.keyboard_modifiers.control() =>
379 {
380 if cursor.is_over(layout.bounds()) {
381 let delta = match delta {
382 mouse::ScrollDelta::Lines { x: _, y } => y,
383 mouse::ScrollDelta::Pixels { x: _, y } => y,
384 };
385
386 if delta < 0.0 {
387 let _ = decrement(current_value).map(change);
388 } else {
389 let _ = increment(current_value).map(change);
390 }
391
392 return event::Status::Captured;
393 }
394 }
395 Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
396 if cursor.is_over(layout.bounds()) {
397 match key {
398 Key::Named(key::Named::ArrowUp) => {
399 let _ = increment(current_value).map(change);
400 }
401 Key::Named(key::Named::ArrowDown) => {
402 let _ = decrement(current_value).map(change);
403 }
404 _ => (),
405 }
406
407 return event::Status::Captured;
408 }
409 }
410 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
411 state.keyboard_modifiers = modifiers;
412 }
413 _ => {}
414 }
415
416 event::Status::Ignored
417 }
418
419 fn draw(
420 &self,
421 tree: &Tree,
422 renderer: &mut Renderer,
423 theme: &Theme,
424 _style: &renderer::Style,
425 layout: Layout<'_>,
426 cursor: mouse::Cursor,
427 _viewport: &Rectangle,
428 ) {
429 let state = tree.state.downcast_ref::<State>();
430 let bounds = layout.bounds();
431 let is_mouse_over = cursor.is_over(bounds);
432
433 let style = theme.style(
434 &self.class,
435 if state.is_dragging {
436 Status::Dragged
437 } else if is_mouse_over {
438 Status::Hovered
439 } else {
440 Status::Active
441 },
442 );
443
444 let (handle_width, handle_height, handle_border_radius) =
445 match style.handle.shape {
446 HandleShape::Circle { radius } => {
447 (radius * 2.0, radius * 2.0, radius.into())
448 }
449 HandleShape::Rectangle {
450 width,
451 border_radius,
452 height,
453 } => (f32::from(width), f32::from(height), border_radius),
454 };
455
456 let value = self.value.into() as f32;
457 let (range_start, range_end) = {
458 let (start, end) = self.range.clone().into_inner();
459
460 (start.into() as f32, end.into() as f32)
461 };
462
463 let offset = if range_start >= range_end {
464 0.0
465 } else {
466 (bounds.height - handle_width) * (value - range_end)
467 / (range_start - range_end)
468 };
469
470 let rail_x = bounds.x + bounds.width / 2.0;
471
472 renderer.fill_quad(
473 renderer::Quad {
474 bounds: Rectangle {
475 x: rail_x - style.rail.width / 2.0,
476 y: bounds.y,
477 width: style.rail.width,
478 height: offset + handle_width / 2.0,
479 },
480 border: style.rail.border,
481 ..renderer::Quad::default()
482 },
483 style.rail.backgrounds.1,
484 );
485
486 renderer.fill_quad(
487 renderer::Quad {
488 bounds: Rectangle {
489 x: rail_x - style.rail.width / 2.0,
490 y: bounds.y + offset + handle_width / 2.0,
491 width: style.rail.width,
492 height: bounds.height - offset - handle_width / 2.0,
493 },
494 border: style.rail.border,
495 ..renderer::Quad::default()
496 },
497 style.rail.backgrounds.0,
498 );
499
500 renderer.fill_quad(
501 renderer::Quad {
502 bounds: Rectangle {
503 x: rail_x - handle_height / 2.0,
504 y: bounds.y + offset,
505 width: handle_height,
506 height: handle_width,
507 },
508 border: Border {
509 radius: handle_border_radius,
510 width: style.handle.border_width,
511 color: style.handle.border_color,
512 },
513 ..renderer::Quad::default()
514 },
515 style.handle.background,
516 );
517 }
518
519 fn mouse_interaction(
520 &self,
521 tree: &Tree,
522 layout: Layout<'_>,
523 cursor: mouse::Cursor,
524 _viewport: &Rectangle,
525 _renderer: &Renderer,
526 ) -> mouse::Interaction {
527 let state = tree.state.downcast_ref::<State>();
528 let bounds = layout.bounds();
529 let is_mouse_over = cursor.is_over(bounds);
530
531 if state.is_dragging {
532 mouse::Interaction::Grabbing
533 } else if is_mouse_over {
534 mouse::Interaction::Grab
535 } else {
536 mouse::Interaction::default()
537 }
538 }
539}
540
541impl<'a, T, Message, Theme, Renderer>
542 From<VerticalSlider<'a, T, Message, Theme>>
543 for Element<'a, Message, Theme, Renderer>
544where
545 T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
546 Message: Clone + 'a,
547 Theme: Catalog + 'a,
548 Renderer: core::Renderer + 'a,
549{
550 fn from(
551 slider: VerticalSlider<'a, T, Message, Theme>,
552 ) -> Element<'a, Message, Theme, Renderer> {
553 Element::new(slider)
554 }
555}
556
557#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
558struct State {
559 is_dragging: bool,
560 keyboard_modifiers: keyboard::Modifiers,
561}