iced_aw/widget/
slide_bar.rs

1//! A dummy widget that draws a quad
2//!
3//! *This API requires the following crate features to be activated: `quad`*
4
5use iced_core::{
6    Border, Clipboard, Color, Element, Event, Layout, Length, Point, Rectangle, Shadow, Shell,
7    Size, Widget,
8    layout::{Limits, Node},
9    mouse::{self, Cursor},
10    renderer, touch,
11    widget::tree::{self, Tree},
12};
13
14use std::ops::RangeInclusive;
15
16/// Constant Default height of `SlideBar`.
17pub const DEFAULT_HEIGHT: f32 = 30.0;
18
19/// A widget that draws a `SlideBar`
20#[allow(missing_debug_implementations)]
21pub struct SlideBar<'a, T, Message>
22where
23    Message: Clone,
24{
25    /// Width of the bar
26    pub width: Length,
27    /// Height of the bar
28    pub height: Option<Length>,
29    /// Color of the bar
30    pub color: Color,
31    /// Background color of the bar
32    pub background: Option<Color>,
33    /// Border radius of the bar
34    pub border_radius: f32,
35    /// Border width of the bar
36    pub border_width: f32,
37    /// Border color of the bar
38    pub border_color: Color,
39    /// value Range
40    pub range: RangeInclusive<T>,
41    /// smallest value within moveable limitations.
42    step: T,
43    /// Value of the bar
44    value: T,
45    /// Change event of the bar when a value is modified
46    on_change: Box<dyn Fn(T) -> Message + 'a>,
47    /// Release event when the mouse is released.
48    on_release: Option<Message>,
49}
50
51impl<'a, T, Message> SlideBar<'a, T, Message>
52where
53    T: Copy + From<u8> + std::cmp::PartialOrd,
54    Message: Clone,
55{
56    /// Creates a new [`SlideBar`].
57    ///
58    /// It expects:
59    ///   * an inclusive range of possible values
60    ///   * the current value of the [`SlideBar`]
61    ///   * a function that will be called when the [`SlideBar`] is dragged.
62    ///   * the new value of the [`SlideBar`] and must produce a `Message`.
63    ///
64    pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
65    where
66        F: 'a + Fn(T) -> Message,
67    {
68        let value = if value >= *range.start() {
69            value
70        } else {
71            *range.start()
72        };
73
74        let value = if value <= *range.end() {
75            value
76        } else {
77            *range.end()
78        };
79
80        Self {
81            width: Length::Fill,
82            height: None,
83            color: Color::from([0.5; 3]),
84            background: None,
85            border_radius: 5.0,
86            border_width: 1.0,
87            border_color: Color::BLACK,
88            step: T::from(1),
89            value,
90            range,
91            on_change: Box::new(on_change),
92            on_release: None,
93        }
94    }
95
96    /// Sets the release message of the [`SlideBar`].
97    /// This is called when the mouse is released from the slider.
98    ///
99    /// Typically, the user's interaction with the slider is finished when this message is produced.
100    /// This is useful if you need to spawn a long-running task from the slider's result, where
101    /// the default `on_change` message could create too many events.
102    #[must_use]
103    pub fn on_release(mut self, on_release: Message) -> Self {
104        self.on_release = Some(on_release);
105        self
106    }
107
108    /// Sets the width of the [`SlideBar`].
109    #[must_use]
110    pub fn width(mut self, width: impl Into<Length>) -> Self {
111        self.width = width.into();
112        self
113    }
114
115    /// Sets the height of the [`SlideBar`].
116    #[must_use]
117    pub fn height(mut self, height: Option<Length>) -> Self {
118        self.height = height;
119        self
120    }
121
122    /// Sets the step size of the [`SlideBar`].
123    #[must_use]
124    pub fn step(mut self, step: impl Into<T>) -> Self {
125        self.step = step.into();
126        self
127    }
128}
129
130impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for SlideBar<'_, T, Message>
131where
132    T: Copy + Into<f64> + num_traits::FromPrimitive,
133    Message: Clone,
134    Renderer: renderer::Renderer,
135{
136    fn tag(&self) -> tree::Tag {
137        tree::Tag::of::<State>()
138    }
139
140    fn state(&self) -> tree::State {
141        tree::State::new(State::new())
142    }
143
144    fn size(&self) -> Size<Length> {
145        Size {
146            width: self.width,
147            height: self.height.unwrap_or(Length::Fixed(DEFAULT_HEIGHT)),
148        }
149    }
150
151    fn layout(&mut self, _tree: &mut Tree, _renderer: &Renderer, limits: &Limits) -> Node {
152        let limits = limits
153            .width(self.width)
154            .height(self.height.unwrap_or(Length::Fixed(DEFAULT_HEIGHT)));
155
156        let size = limits.resolve(
157            self.width,
158            self.height.unwrap_or(Length::Fixed(DEFAULT_HEIGHT)),
159            Size::ZERO,
160        );
161
162        Node::new(size)
163    }
164
165    fn update(
166        &mut self,
167        tree: &mut Tree,
168        event: &Event,
169        layout: Layout<'_>,
170        cursor: mouse::Cursor,
171        _renderer: &Renderer,
172        _clipboard: &mut dyn Clipboard,
173        shell: &mut Shell<'_, Message>,
174        _viewport: &Rectangle,
175    ) {
176        update(
177            event,
178            layout,
179            cursor,
180            shell,
181            tree.state.downcast_mut::<State>(),
182            &mut self.value,
183            &self.range,
184            self.step,
185            self.on_change.as_ref(),
186            &self.on_release,
187        );
188    }
189
190    fn draw(
191        &self,
192        _tree: &Tree,
193        renderer: &mut Renderer,
194        _theme: &Theme,
195        _style: &renderer::Style,
196        layout: Layout<'_>,
197        _cursor: Cursor,
198        viewport: &Rectangle,
199    ) {
200        draw(renderer, layout, viewport, self);
201    }
202}
203
204/// Processes an [`Event`] and updates the [`State`] of a [`SlideBar`]
205/// accordingly.
206#[allow(clippy::too_many_arguments)]
207pub fn update<Message, T>(
208    event: &Event,
209    layout: Layout<'_>,
210    cursor: mouse::Cursor,
211    shell: &mut Shell<'_, Message>,
212    state: &mut State,
213    value: &mut T,
214    range: &RangeInclusive<T>,
215    step: T,
216    on_change: &dyn Fn(T) -> Message,
217    on_release: &Option<Message>,
218) where
219    T: Copy + Into<f64> + num_traits::FromPrimitive,
220    Message: Clone,
221{
222    let is_dragging = state.is_dragging;
223
224    let mut change = |cursor_position: Point| {
225        let bounds = layout.bounds();
226        let new_value = if cursor_position.x <= bounds.x {
227            *range.start()
228        } else if cursor_position.x >= bounds.x + bounds.width {
229            *range.end()
230        } else {
231            let step = step.into();
232            let start = (*range.start()).into();
233            let end = (*range.end()).into();
234
235            let percent = f64::from(cursor_position.x - bounds.x) / f64::from(bounds.width);
236
237            let steps = (percent * (end - start) / step).round();
238            let value = steps * step + start;
239
240            if let Some(value) = T::from_f64(value) {
241                value
242            } else {
243                return;
244            }
245        };
246
247        if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
248            shell.publish((on_change)(new_value));
249
250            *value = new_value;
251        }
252    };
253
254    match event {
255        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
256        | Event::Touch(touch::Event::FingerPressed { .. }) => {
257            if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
258                change(cursor_position);
259                state.is_dragging = true;
260
261                shell.capture_event();
262            }
263        }
264        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
265        | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => {
266            if is_dragging {
267                if let Some(on_release) = on_release.clone() {
268                    shell.publish(on_release);
269                }
270                state.is_dragging = false;
271
272                shell.capture_event();
273            }
274        }
275        Event::Mouse(mouse::Event::CursorMoved { .. })
276        | Event::Touch(touch::Event::FingerMoved { .. }) => {
277            if is_dragging {
278                let _ = cursor.position().map(change);
279
280                shell.capture_event();
281            }
282        }
283        _ => {}
284    }
285}
286
287/// Draws a [`SlideBar`].
288pub fn draw<T, R, Message>(
289    renderer: &mut R,
290    layout: Layout<'_>,
291    viewport: &Rectangle,
292    slider: &SlideBar<T, Message>,
293) where
294    T: Into<f64> + Copy,
295    Message: Clone,
296    R: renderer::Renderer,
297{
298    let bounds = layout.bounds();
299    let value = slider.value.into() as f32;
300    let (range_start, range_end) = {
301        let (start, end) = slider.range.clone().into_inner();
302
303        (start.into() as f32, end.into() as f32)
304    };
305
306    let active_progress_bounds = if range_start >= range_end {
307        Rectangle {
308            width: 0.0,
309            ..bounds
310        }
311    } else {
312        Rectangle {
313            width: bounds.width * (value - range_start) / (range_end - range_start),
314            ..bounds
315        }
316    };
317
318    let background = slider.background.unwrap_or_else(|| Color::from([1.0; 3]));
319
320    if bounds.intersects(viewport) {
321        renderer.fill_quad(
322            renderer::Quad {
323                bounds,
324                border: Border {
325                    radius: slider.border_radius.into(),
326                    width: slider.border_width,
327                    color: slider.border_color,
328                },
329                shadow: Shadow::default(),
330                ..Default::default()
331            },
332            background,
333        );
334    }
335
336    if active_progress_bounds.intersects(viewport) {
337        renderer.fill_quad(
338            renderer::Quad {
339                bounds: active_progress_bounds,
340                border: Border {
341                    radius: slider.border_radius.into(),
342                    width: 0.0,
343                    color: Color::TRANSPARENT,
344                },
345                shadow: Shadow::default(),
346                ..Default::default()
347            },
348            slider.color,
349        );
350    }
351}
352
353impl<'a, T, Message, Theme, Renderer> From<SlideBar<'a, T, Message>>
354    for Element<'a, Message, Theme, Renderer>
355where
356    T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
357    Renderer: 'a + renderer::Renderer,
358    Message: 'a + Clone,
359    Theme: 'a,
360{
361    fn from(value: SlideBar<'a, T, Message>) -> Self {
362        Self::new(value)
363    }
364}
365
366/// The local state of a [`SlideBar`].
367#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
368pub struct State {
369    is_dragging: bool,
370}
371
372impl State {
373    /// Creates a new [`State`].
374    #[must_use]
375    pub fn new() -> Self {
376        Self::default()
377    }
378}