1use super::*;
4use iced_widget::core::keyboard;
5use iced_widget::core::keyboard::key::{self, Key};
6use iced_widget::slider::{Catalog as SliderCatalog, HandleShape, Status, Style, StyleFn};
7use num_traits::FromPrimitive;
8use std::fmt;
9use std::ops::RangeInclusive;
10
11pub struct Slider<'a, T, Message> {
13 range: RangeInclusive<T>,
14 step: T,
15 shift_step: Option<T>,
16 value: T,
17 default: Option<T>,
18 on_change: Box<dyn Fn(T) -> Message + 'a>,
19 on_release: Option<Message>,
20 width: Length,
21 height: f32,
22 class: StyleFn<'a, Theme>,
23 status: Option<Status>,
24}
25
26impl<T, Message> fmt::Debug for Slider<'_, T, Message>
27where
28 T: fmt::Debug,
29{
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 f.debug_struct("Slider")
32 .field("range", &self.range)
33 .field("step", &self.step)
34 .field("shift_step", &self.shift_step)
35 .field("value", &self.value)
36 .field("default", &self.default)
37 .field("width", &self.width)
38 .field("height", &self.height)
39 .field("status", &self.status)
40 .finish_non_exhaustive()
41 }
42}
43
44impl<'a, T, Message> Slider<'a, T, Message>
45where
46 T: Copy + From<u8> + PartialOrd,
47 Message: Clone + 'a,
48{
49 fn new(range: RangeInclusive<T>, value: T, on_change: impl Fn(T) -> Message + 'a) -> Self {
50 let value = clamped_value(&range, value);
51
52 Self {
53 range,
54 step: T::from(1),
55 shift_step: None,
56 value,
57 default: None,
58 on_change: Box::new(on_change),
59 on_release: None,
60 width: Length::Fill,
61 height: tokens::component::slider::STATE_LAYER_SIZE,
62 class: Box::new(slider_style::default),
63 status: None,
64 }
65 }
66
67 pub fn default(mut self, default: impl Into<T>) -> Self {
69 self.default = Some(default.into());
70 self
71 }
72
73 pub fn on_release(mut self, on_release: Message) -> Self {
75 self.on_release = Some(on_release);
76 self
77 }
78
79 pub fn width(mut self, width: impl Into<Length>) -> Self {
81 self.width = width.into();
82 self
83 }
84
85 pub fn height(mut self, height: impl Into<Pixels>) -> Self {
87 self.height = height.into().0;
88 self
89 }
90
91 pub fn step(mut self, step: impl Into<T>) -> Self {
93 self.step = step.into();
94 self
95 }
96
97 pub fn shift_step(mut self, shift_step: impl Into<T>) -> Self {
99 self.shift_step = Some(shift_step.into());
100 self
101 }
102
103 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self {
105 self.class = Box::new(style);
106 self
107 }
108}
109
110pub fn continuous<'a, T, Message>(
111 range: RangeInclusive<T>,
112 value: T,
113 on_change: impl Fn(T) -> Message + 'a,
114) -> Slider<'a, T, Message>
115where
116 T: Copy + From<u8> + PartialOrd,
117 Message: Clone + 'a,
118{
119 Slider::new(range, value, on_change)
120}
121
122impl<'a, T, Message, Renderer> Widget<Message, Theme, Renderer> for Slider<'a, T, Message>
123where
124 T: Copy + Into<f64> + FromPrimitive,
125 Message: Clone,
126 Renderer: iced_widget::core::Renderer,
127{
128 fn tag(&self) -> tree::Tag {
129 tree::Tag::of::<State>()
130 }
131
132 fn state(&self) -> tree::State {
133 tree::State::new(State::default())
134 }
135
136 fn size(&self) -> Size<Length> {
137 Size {
138 width: self.width,
139 height: Length::Shrink,
140 }
141 }
142
143 fn layout(
144 &mut self,
145 _tree: &mut Tree,
146 _renderer: &Renderer,
147 limits: &layout::Limits,
148 ) -> layout::Node {
149 layout::atomic(limits, self.width, self.height)
150 }
151
152 fn update(
153 &mut self,
154 tree: &mut Tree,
155 event: &Event,
156 layout: Layout<'_>,
157 cursor: mouse::Cursor,
158 _renderer: &Renderer,
159 _clipboard: &mut dyn Clipboard,
160 shell: &mut Shell<'_, Message>,
161 _viewport: &Rectangle,
162 ) {
163 let state = tree.state.downcast_mut::<State>();
164
165 let mut update = || {
166 let current_value = self.value;
167
168 let locate = |cursor_position: Point| -> Option<T> {
169 let bounds = layout.bounds();
170
171 if cursor_position.x <= bounds.x {
172 Some(*self.range.start())
173 } else if cursor_position.x >= bounds.x + bounds.width {
174 Some(*self.range.end())
175 } else {
176 let step = if state.keyboard_modifiers.shift() {
177 self.shift_step.unwrap_or(self.step)
178 } else {
179 self.step
180 }
181 .into();
182
183 let start = (*self.range.start()).into();
184 let end = (*self.range.end()).into();
185 let percent = f64::from(cursor_position.x - bounds.x) / f64::from(bounds.width);
186 let steps = (percent * (end - start) / step).round();
187 let value = steps * step + start;
188
189 T::from_f64(value.min(end))
190 }
191 };
192
193 let increment = |value: T| -> Option<T> {
194 let step = if state.keyboard_modifiers.shift() {
195 self.shift_step.unwrap_or(self.step)
196 } else {
197 self.step
198 }
199 .into();
200
201 let steps = (value.into() / step).round();
202 let new_value = step * (steps + 1.0);
203
204 if new_value > (*self.range.end()).into() {
205 return Some(*self.range.end());
206 }
207
208 T::from_f64(new_value)
209 };
210
211 let decrement = |value: T| -> Option<T> {
212 let step = if state.keyboard_modifiers.shift() {
213 self.shift_step.unwrap_or(self.step)
214 } else {
215 self.step
216 }
217 .into();
218
219 let steps = (value.into() / step).round();
220 let new_value = step * (steps - 1.0);
221
222 if new_value < (*self.range.start()).into() {
223 return Some(*self.range.start());
224 }
225
226 T::from_f64(new_value)
227 };
228
229 let mut change = |new_value: T| {
230 if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
231 shell.publish((self.on_change)(new_value));
232 self.value = new_value;
233 }
234 };
235
236 match &event {
237 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
238 | Event::Touch(touch::Event::FingerPressed { .. }) => {
239 if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
240 if state.keyboard_modifiers.command() {
241 if let Some(default) = self.default {
242 change(default);
243 }
244
245 state.is_dragging = false;
246 } else {
247 if let Some(value) = locate(cursor_position) {
248 change(value);
249 }
250
251 state.is_dragging = true;
252 }
253
254 shell.capture_event();
255 }
256 }
257 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
258 | Event::Touch(touch::Event::FingerLifted { .. })
259 | Event::Touch(touch::Event::FingerLost { .. }) => {
260 if state.is_dragging {
261 if let Some(on_release) = self.on_release.clone() {
262 shell.publish(on_release);
263 }
264
265 state.is_dragging = false;
266 }
267 }
268 Event::Mouse(mouse::Event::CursorMoved { .. })
269 | Event::Touch(touch::Event::FingerMoved { .. }) => {
270 if state.is_dragging {
271 if let Some(value) = cursor.land().position().and_then(locate) {
272 change(value);
273 }
274
275 shell.capture_event();
276 }
277 }
278 Event::Mouse(mouse::Event::WheelScrolled { delta })
279 if state.keyboard_modifiers.control() =>
280 {
281 if cursor.is_over(layout.bounds()) {
282 let delta = match delta {
283 mouse::ScrollDelta::Lines { x: _, y } => y,
284 mouse::ScrollDelta::Pixels { x: _, y } => y,
285 };
286
287 if *delta < 0.0 {
288 if let Some(value) = decrement(current_value) {
289 change(value);
290 }
291 } else if let Some(value) = increment(current_value) {
292 change(value);
293 }
294
295 shell.capture_event();
296 }
297 }
298 Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
299 if cursor.is_over(layout.bounds()) {
300 match key {
301 Key::Named(key::Named::ArrowUp) => {
302 if let Some(value) = increment(current_value) {
303 change(value);
304 }
305
306 shell.capture_event();
307 }
308 Key::Named(key::Named::ArrowDown) => {
309 if let Some(value) = decrement(current_value) {
310 change(value);
311 }
312
313 shell.capture_event();
314 }
315 _ => (),
316 }
317 }
318 }
319 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
320 state.keyboard_modifiers = *modifiers;
321 }
322 _ => {}
323 }
324 };
325
326 update();
327
328 let current_status = if state.is_dragging {
329 Status::Dragged
330 } else if cursor.is_over(layout.bounds()) {
331 Status::Hovered
332 } else {
333 Status::Active
334 };
335
336 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
337 self.status = Some(current_status);
338 } else if self.status.is_some_and(|status| status != current_status) {
339 shell.request_redraw();
340 }
341 }
342
343 fn draw(
344 &self,
345 _tree: &Tree,
346 renderer: &mut Renderer,
347 theme: &Theme,
348 _style: &renderer::Style,
349 layout: Layout<'_>,
350 _cursor: mouse::Cursor,
351 _viewport: &Rectangle,
352 ) {
353 let bounds = layout.bounds();
354 let style = theme.style(&self.class, self.status.unwrap_or(Status::Active));
355 let (handle_width, handle_height, handle_border_radius) = match style.handle.shape {
356 HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius.into()),
357 HandleShape::Rectangle {
358 width,
359 border_radius,
360 } => (f32::from(width), bounds.height, border_radius),
361 };
362 let thumb_bounds =
363 thumb_bounds_with_size(bounds, self.value, &self.range, handle_width, handle_height);
364 let rail_y = bounds.y + bounds.height / 2.0;
365
366 renderer.fill_quad(
367 renderer::Quad {
368 bounds: Rectangle {
369 x: bounds.x,
370 y: rail_y - style.rail.width / 2.0,
371 width: thumb_bounds.x - bounds.x + handle_width / 2.0,
372 height: style.rail.width,
373 },
374 border: style.rail.border,
375 ..renderer::Quad::default()
376 },
377 style.rail.backgrounds.0,
378 );
379
380 renderer.fill_quad(
381 renderer::Quad {
382 bounds: Rectangle {
383 x: thumb_bounds.x + handle_width / 2.0,
384 y: rail_y - style.rail.width / 2.0,
385 width: bounds.width - (thumb_bounds.x - bounds.x) - handle_width / 2.0,
386 height: style.rail.width,
387 },
388 border: style.rail.border,
389 ..renderer::Quad::default()
390 },
391 style.rail.backgrounds.1,
392 );
393
394 renderer.fill_quad(
395 renderer::Quad {
396 bounds: thumb_bounds,
397 border: Border {
398 radius: handle_border_radius,
399 width: style.handle.border_width,
400 color: style.handle.border_color,
401 },
402 shadow: thumb_shadow(theme),
403 ..renderer::Quad::default()
404 },
405 style.handle.background,
406 );
407 }
408
409 fn mouse_interaction(
410 &self,
411 tree: &Tree,
412 layout: Layout<'_>,
413 cursor: mouse::Cursor,
414 _viewport: &Rectangle,
415 _renderer: &Renderer,
416 ) -> mouse::Interaction {
417 let state = tree.state.downcast_ref::<State>();
418
419 if state.is_dragging {
420 if cfg!(target_os = "windows") {
421 mouse::Interaction::Pointer
422 } else {
423 mouse::Interaction::Grabbing
424 }
425 } else if cursor.is_over(layout.bounds()) {
426 if cfg!(target_os = "windows") {
427 mouse::Interaction::Pointer
428 } else {
429 mouse::Interaction::Grab
430 }
431 } else {
432 mouse::Interaction::default()
433 }
434 }
435}
436
437impl<'a, T, Message, Renderer> From<Slider<'a, T, Message>>
438 for Element<'a, Message, Theme, Renderer>
439where
440 T: Copy + Into<f64> + FromPrimitive + 'a,
441 Message: Clone + 'a,
442 Renderer: iced_widget::core::Renderer + 'a,
443{
444 fn from(slider: Slider<'a, T, Message>) -> Self {
445 Element::new(slider)
446 }
447}
448
449fn clamped_value<T>(range: &RangeInclusive<T>, value: T) -> T
450where
451 T: Copy + PartialOrd,
452{
453 if value < *range.start() {
454 *range.start()
455 } else if value > *range.end() {
456 *range.end()
457 } else {
458 value
459 }
460}
461
462fn thumb_bounds_with_size<T>(
463 bounds: Rectangle,
464 value: T,
465 range: &RangeInclusive<T>,
466 handle_width: f32,
467 handle_height: f32,
468) -> Rectangle
469where
470 T: Copy + Into<f64>,
471{
472 let range_start = (*range.start()).into() as f32;
473 let range_end = (*range.end()).into() as f32;
474 let offset = if range_start >= range_end {
475 0.0
476 } else {
477 let value = (value.into() as f32).clamp(range_start, range_end);
478
479 (bounds.width - handle_width) * (value - range_start) / (range_end - range_start)
480 };
481 let rail_y = bounds.y + bounds.height / 2.0;
482
483 Rectangle {
484 x: bounds.x + offset,
485 y: rail_y - handle_height / 2.0,
486 width: handle_width,
487 height: handle_height,
488 }
489}
490
491fn thumb_shadow(theme: &Theme) -> iced_widget::core::Shadow {
492 crate::utils::shadow_from_elevation(
493 tokens::component::slider::HANDLE_ELEVATION,
494 theme.colors().shadow,
495 )
496}
497
498#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
499struct State {
500 is_dragging: bool,
501 keyboard_modifiers: keyboard::Modifiers,
502}
503
504#[cfg(test)]
505#[path = "../../../tests/widget/component/slider.rs"]
506mod tests;