1use std::ops::RangeInclusive;
32
33pub use crate::slider::{Catalog, Handle, HandleShape, Status, Style, StyleFn, default};
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::touch;
42use crate::core::widget::tree::{self, Tree};
43use crate::core::window;
44use crate::core::{
45 self, Clipboard, Element, Event, Length, Pixels, Point, Rectangle, Shell, Size, Widget,
46};
47
48pub struct VerticalSlider<'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: f32,
96 height: Length,
97 class: Theme::Class<'a>,
98 status: Option<Status>,
99}
100
101impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme>
102where
103 T: Copy + From<u8> + std::cmp::PartialOrd,
104 Message: Clone,
105 Theme: Catalog,
106{
107 pub const DEFAULT_WIDTH: f32 = 16.0;
109
110 pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
119 where
120 F: 'a + Fn(T) -> Message,
121 {
122 let value = if value >= *range.start() {
123 value
124 } else {
125 *range.start()
126 };
127
128 let value = if value <= *range.end() {
129 value
130 } else {
131 *range.end()
132 };
133
134 VerticalSlider {
135 value,
136 default: None,
137 range,
138 step: T::from(1),
139 shift_step: None,
140 on_change: Box::new(on_change),
141 on_release: None,
142 width: Self::DEFAULT_WIDTH,
143 height: Length::Fill,
144 class: Theme::default(),
145 status: None,
146 }
147 }
148
149 pub fn default(mut self, default: impl Into<T>) -> Self {
153 self.default = Some(default.into());
154 self
155 }
156
157 pub fn on_release(mut self, on_release: Message) -> Self {
164 self.on_release = Some(on_release);
165 self
166 }
167
168 pub fn width(mut self, width: impl Into<Pixels>) -> Self {
170 self.width = width.into().0;
171 self
172 }
173
174 pub fn height(mut self, height: impl Into<Length>) -> Self {
176 self.height = height.into();
177 self
178 }
179
180 pub fn step(mut self, step: T) -> Self {
182 self.step = step;
183 self
184 }
185
186 pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
190 self.shift_step = Some(shift_step.into());
191 self
192 }
193
194 #[must_use]
196 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
197 where
198 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
199 {
200 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
201 self
202 }
203
204 #[cfg(feature = "advanced")]
206 #[must_use]
207 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
208 self.class = class.into();
209 self
210 }
211}
212
213impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
214 for VerticalSlider<'_, T, Message, Theme>
215where
216 T: Copy + Into<f64> + num_traits::FromPrimitive,
217 Message: Clone,
218 Theme: Catalog,
219 Renderer: core::Renderer,
220{
221 fn tag(&self) -> tree::Tag {
222 tree::Tag::of::<State>()
223 }
224
225 fn state(&self) -> tree::State {
226 tree::State::new(State::default())
227 }
228
229 fn size(&self) -> Size<Length> {
230 Size {
231 width: Length::Shrink,
232 height: self.height,
233 }
234 }
235
236 fn layout(
237 &mut self,
238 _tree: &mut Tree,
239 _renderer: &Renderer,
240 limits: &layout::Limits,
241 ) -> layout::Node {
242 layout::atomic(limits, self.width, self.height)
243 }
244
245 fn update(
246 &mut self,
247 tree: &mut Tree,
248 event: &Event,
249 layout: Layout<'_>,
250 cursor: mouse::Cursor,
251 _renderer: &Renderer,
252 _clipboard: &mut dyn Clipboard,
253 shell: &mut Shell<'_, Message>,
254 _viewport: &Rectangle,
255 ) {
256 let state = tree.state.downcast_mut::<State>();
257 let is_dragging = state.is_dragging;
258 let current_value = self.value;
259
260 let locate = |cursor_position: Point| -> Option<T> {
261 let bounds = layout.bounds();
262
263 if cursor_position.y >= bounds.y + bounds.height {
264 Some(*self.range.start())
265 } else if cursor_position.y <= bounds.y {
266 Some(*self.range.end())
267 } else {
268 let step = if state.keyboard_modifiers.shift() {
269 self.shift_step.unwrap_or(self.step)
270 } else {
271 self.step
272 }
273 .into();
274
275 let start = (*self.range.start()).into();
276 let end = (*self.range.end()).into();
277
278 let percent =
279 1.0 - f64::from(cursor_position.y - bounds.y) / f64::from(bounds.height);
280
281 let steps = (percent * (end - start) / step).round();
282 let value = steps * step + start;
283
284 T::from_f64(value.min(end))
285 }
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) = cursor.position_over(layout.bounds()) {
336 if state.keyboard_modifiers.control() || state.keyboard_modifiers.command() {
337 let _ = self.default.map(change);
338 state.is_dragging = false;
339 } else {
340 let _ = locate(cursor_position).map(change);
341 state.is_dragging = true;
342 }
343
344 shell.capture_event();
345 }
346 }
347 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
348 | Event::Touch(touch::Event::FingerLifted { .. })
349 | Event::Touch(touch::Event::FingerLost { .. }) => {
350 if is_dragging {
351 if let Some(on_release) = self.on_release.clone() {
352 shell.publish(on_release);
353 }
354 state.is_dragging = false;
355 }
356 }
357 Event::Mouse(mouse::Event::CursorMoved { .. })
358 | Event::Touch(touch::Event::FingerMoved { .. }) => {
359 if is_dragging {
360 let _ = cursor.land().position().and_then(locate).map(change);
361
362 shell.capture_event();
363 }
364 }
365 Event::Mouse(mouse::Event::WheelScrolled { delta })
366 if state.keyboard_modifiers.control() =>
367 {
368 if cursor.is_over(layout.bounds()) {
369 let delta = match *delta {
370 mouse::ScrollDelta::Lines { x: _, y } => y,
371 mouse::ScrollDelta::Pixels { x: _, y } => y,
372 };
373
374 if delta < 0.0 {
375 let _ = decrement(current_value).map(change);
376 } else {
377 let _ = increment(current_value).map(change);
378 }
379
380 shell.capture_event();
381 }
382 }
383 Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
384 if cursor.is_over(layout.bounds()) {
385 match key {
386 Key::Named(key::Named::ArrowUp) => {
387 let _ = increment(current_value).map(change);
388 shell.capture_event();
389 }
390 Key::Named(key::Named::ArrowDown) => {
391 let _ = decrement(current_value).map(change);
392 shell.capture_event();
393 }
394 _ => (),
395 }
396 }
397 }
398 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
399 state.keyboard_modifiers = *modifiers;
400 }
401 _ => {}
402 }
403
404 let current_status = if state.is_dragging {
405 Status::Dragged
406 } else if cursor.is_over(layout.bounds()) {
407 Status::Hovered
408 } else {
409 Status::Active
410 };
411
412 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
413 self.status = Some(current_status);
414 } else if self.status.is_some_and(|status| status != current_status) {
415 shell.request_redraw();
416 }
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 bounds = layout.bounds();
430
431 let style = theme.style(&self.class, self.status.unwrap_or(Status::Active));
432
433 let (handle_width, handle_height, handle_border_radius) = match style.handle.shape {
434 HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius.into()),
435 HandleShape::Rectangle {
436 width,
437 border_radius,
438 } => (f32::from(width), bounds.width, border_radius),
439 };
440
441 let value = self.value.into() as f32;
442 let (range_start, range_end) = {
443 let (start, end) = self.range.clone().into_inner();
444
445 (start.into() as f32, end.into() as f32)
446 };
447
448 let offset = if range_start >= range_end {
449 0.0
450 } else {
451 (bounds.height - handle_width) * (value - range_end) / (range_start - range_end)
452 };
453
454 let rail_x = bounds.x + bounds.width / 2.0;
455
456 renderer.fill_quad(
457 renderer::Quad {
458 bounds: Rectangle {
459 x: rail_x - style.rail.width / 2.0,
460 y: bounds.y,
461 width: style.rail.width,
462 height: offset + handle_width / 2.0,
463 },
464 border: style.rail.border,
465 ..renderer::Quad::default()
466 },
467 style.rail.backgrounds.1,
468 );
469
470 renderer.fill_quad(
471 renderer::Quad {
472 bounds: Rectangle {
473 x: rail_x - style.rail.width / 2.0,
474 y: bounds.y + offset + handle_width / 2.0,
475 width: style.rail.width,
476 height: bounds.height - offset - handle_width / 2.0,
477 },
478 border: style.rail.border,
479 ..renderer::Quad::default()
480 },
481 style.rail.backgrounds.0,
482 );
483
484 renderer.fill_quad(
485 renderer::Quad {
486 bounds: Rectangle {
487 x: rail_x - handle_height / 2.0,
488 y: bounds.y + offset,
489 width: handle_height,
490 height: handle_width,
491 },
492 border: Border {
493 radius: handle_border_radius,
494 width: style.handle.border_width,
495 color: style.handle.border_color,
496 },
497 ..renderer::Quad::default()
498 },
499 style.handle.background,
500 );
501 }
502
503 fn mouse_interaction(
504 &self,
505 tree: &Tree,
506 layout: Layout<'_>,
507 cursor: mouse::Cursor,
508 _viewport: &Rectangle,
509 _renderer: &Renderer,
510 ) -> mouse::Interaction {
511 let state = tree.state.downcast_ref::<State>();
512
513 if state.is_dragging {
514 if cfg!(target_os = "windows") {
517 mouse::Interaction::Pointer
518 } else {
519 mouse::Interaction::Grabbing
520 }
521 } else if cursor.is_over(layout.bounds()) {
522 if cfg!(target_os = "windows") {
523 mouse::Interaction::Pointer
524 } else {
525 mouse::Interaction::Grab
526 }
527 } else {
528 mouse::Interaction::default()
529 }
530 }
531}
532
533impl<'a, T, Message, Theme, Renderer> From<VerticalSlider<'a, T, Message, Theme>>
534 for Element<'a, Message, Theme, Renderer>
535where
536 T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
537 Message: Clone + 'a,
538 Theme: Catalog + 'a,
539 Renderer: core::Renderer + 'a,
540{
541 fn from(
542 slider: VerticalSlider<'a, T, Message, Theme>,
543 ) -> Element<'a, Message, Theme, Renderer> {
544 Element::new(slider)
545 }
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
549struct State {
550 is_dragging: bool,
551 keyboard_modifiers: keyboard::Modifiers,
552}