Skip to main content

cosmic/widget/
toggler.rs

1//! Show toggle controls using togglers.
2
3use std::time::{Duration, Instant};
4
5use crate::{Element, anim};
6use iced_core::renderer::{self, Renderer};
7use iced_core::widget::{self, Tree, tree};
8use iced_core::{
9    Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment,
10    event, layout, mouse, text, touch, window,
11};
12use iced_widget::Id;
13use iced_widget::toggler::Status;
14
15pub use iced_widget::toggler::{Catalog, Style};
16
17pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> {
18    Toggler::new(is_checked)
19}
20/// A toggler widget.
21#[allow(missing_debug_implementations)]
22pub struct Toggler<'a, Message> {
23    id: Id,
24    is_toggled: bool,
25    on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
26    label: Option<String>,
27    width: Length,
28    size: f32,
29    text_size: Option<f32>,
30    text_line_height: text::LineHeight,
31    text_alignment: text::Alignment,
32    text_shaping: text::Shaping,
33    spacing: f32,
34    font: Option<crate::font::Font>,
35    duration: Duration,
36    ellipsize: text::Ellipsize,
37}
38
39impl<'a, Message> Toggler<'a, Message> {
40    /// The default size of a [`Toggler`].
41    pub const DEFAULT_SIZE: f32 = 24.0;
42
43    /// Creates a new [`Toggler`].
44    ///
45    /// It expects:
46    ///   * a boolean describing whether the [`Toggler`] is checked or not
47    ///   * An optional label for the [`Toggler`]
48    ///   * a function that will be called when the [`Toggler`] is toggled. It
49    ///     will receive the new state of the [`Toggler`] and must produce a
50    ///     `Message`.
51    pub fn new(is_toggled: bool) -> Self {
52        Toggler {
53            id: Id::unique(),
54            is_toggled,
55            on_toggle: None,
56            label: None,
57            width: Length::Shrink,
58            size: Self::DEFAULT_SIZE,
59            text_size: None,
60            text_line_height: text::LineHeight::default(),
61            text_alignment: text::Alignment::Left,
62            text_shaping: text::Shaping::Advanced,
63            spacing: 0.0,
64            font: None,
65            duration: Duration::from_millis(200),
66            ellipsize: text::Ellipsize::None,
67        }
68    }
69
70    /// Sets the size of the [`Toggler`].
71    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
72        self.size = size.into().0;
73        self
74    }
75
76    /// Sets the width of the [`Toggler`].
77    pub fn width(mut self, width: impl Into<Length>) -> Self {
78        self.width = width.into();
79        self
80    }
81
82    /// Sets the text size o the [`Toggler`].
83    pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
84        self.text_size = Some(text_size.into().0);
85        self
86    }
87
88    /// Sets the text [`LineHeight`] of the [`Toggler`].
89    pub fn text_line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
90        self.text_line_height = line_height.into();
91        self
92    }
93
94    /// Sets the horizontal alignment of the text of the [`Toggler`]
95    pub fn text_alignment(mut self, alignment: text::Alignment) -> Self {
96        self.text_alignment = alignment;
97        self
98    }
99
100    /// Sets the [`text::Shaping`] strategy of the [`Toggler`].
101    pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
102        self.text_shaping = shaping;
103        self
104    }
105
106    /// Sets the spacing between the [`Toggler`] and the text.
107    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
108        self.spacing = spacing.into().0;
109        self
110    }
111
112    /// Sets the [`text::Ellipsize`] strategy of the [`Toggler`].
113    pub fn ellipsize(mut self, ellipsize: text::Ellipsize) -> Self {
114        self.ellipsize = ellipsize;
115        self
116    }
117
118    /// Sets the [`Font`] of the text of the [`Toggler`]
119    ///
120    /// [`Font`]: cosmic::iced::text::Renderer::Font
121    pub fn font(mut self, font: impl Into<crate::font::Font>) -> Self {
122        self.font = Some(font.into());
123        self
124    }
125
126    pub fn id(mut self, id: Id) -> Self {
127        self.id = id;
128        self
129    }
130
131    pub fn duration(mut self, dur: Duration) -> Self {
132        self.duration = dur;
133        self
134    }
135
136    pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self {
137        self.on_toggle = Some(Box::new(on_toggle));
138        self
139    }
140
141    pub fn on_toggle_maybe(mut self, on_toggle: Option<impl Fn(bool) -> Message + 'a>) -> Self {
142        self.on_toggle = on_toggle.map(|t| Box::new(t) as _);
143        self
144    }
145
146    /// Sets the label of the [`Button`].
147    pub fn label(mut self, label: impl Into<Option<String>>) -> Self {
148        self.label = label.into();
149        self
150    }
151}
152
153impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for Toggler<'a, Message> {
154    fn size(&self) -> Size<Length> {
155        Size::new(self.width, Length::Shrink)
156    }
157
158    fn tag(&self) -> tree::Tag {
159        tree::Tag::of::<State>()
160    }
161
162    fn state(&self) -> tree::State {
163        tree::State::new(State {
164            prev_toggled: self.is_toggled,
165            ..State::default()
166        })
167    }
168
169    fn id(&self) -> Option<Id> {
170        Some(self.id.clone())
171    }
172
173    fn set_id(&mut self, id: Id) {
174        self.id = id;
175    }
176
177    fn layout(
178        &mut self,
179        tree: &mut Tree,
180        renderer: &crate::Renderer,
181        limits: &layout::Limits,
182    ) -> layout::Node {
183        let limits = limits.width(self.width);
184
185        let res = next_to_each_other(
186            &limits,
187            self.spacing,
188            |limits| {
189                if let Some(label) = self.label.as_deref() {
190                    let state = tree.state.downcast_mut::<State>();
191                    let node = iced_core::widget::text::layout(
192                        &mut state.text,
193                        renderer,
194                        limits,
195                        label,
196                        widget::text::Format {
197                            width: self.width,
198                            height: Length::Shrink,
199                            line_height: self.text_line_height,
200                            size: self.text_size.map(iced::Pixels),
201                            font: self.font,
202                            align_x: self.text_alignment,
203                            align_y: alignment::Vertical::Top,
204                            shaping: self.text_shaping,
205                            wrapping: iced_core::text::Wrapping::default(),
206                            ellipsize: self.ellipsize,
207                        },
208                    );
209                    match self.width {
210                        Length::Fill => {
211                            let size = node.size();
212                            layout::Node::with_children(
213                                Size::new(limits.width(Length::Fill).max().width, size.height),
214                                vec![node],
215                            )
216                        }
217                        _ => node,
218                    }
219                } else {
220                    layout::Node::new(iced_core::Size::ZERO)
221                }
222            },
223            |_| layout::Node::new(Size::new(48., 24.)),
224        );
225        res
226    }
227
228    fn update(
229        &mut self,
230        tree: &mut Tree,
231        event: &Event,
232        layout: Layout<'_>,
233        cursor_position: mouse::Cursor,
234        _renderer: &crate::Renderer,
235        _clipboard: &mut dyn Clipboard,
236        shell: &mut Shell<'_, Message>,
237        _viewport: &Rectangle,
238    ) {
239        let Some(on_toggle) = self.on_toggle.as_ref() else {
240            return;
241        };
242        let state = tree.state.downcast_mut::<State>();
243
244        // animate external changes
245        if state.prev_toggled != self.is_toggled {
246            state.anim.changed(self.duration);
247            shell.request_redraw();
248            state.prev_toggled = self.is_toggled;
249        }
250
251        match event {
252            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
253            | Event::Touch(touch::Event::FingerLifted { .. }) => {
254                let mouse_over = cursor_position.is_over(layout.bounds());
255
256                if mouse_over {
257                    shell.publish((on_toggle)(!self.is_toggled));
258                    state.anim.changed(self.duration);
259                    state.prev_toggled = !self.is_toggled;
260                    shell.capture_event();
261                }
262            }
263            Event::Window(window::Event::RedrawRequested(now)) => {
264                state.anim.anim_done(self.duration);
265                if state.anim.last_change.is_some() {
266                    shell.request_redraw();
267                }
268            }
269            Event::Mouse(mouse::Event::CursorMoved { .. }) => {
270                let toggler_bounds = layout
271                    .children()
272                    .nth(1)
273                    .map_or_else(|| layout.bounds(), |l| l.bounds());
274                let hovered = cursor_position.is_over(toggler_bounds);
275                if state.hovered != hovered {
276                    state.hovered = hovered;
277                    shell.request_redraw();
278                }
279            }
280            _ => {}
281        }
282    }
283
284    fn mouse_interaction(
285        &self,
286        _state: &Tree,
287        layout: Layout<'_>,
288        cursor_position: mouse::Cursor,
289        _viewport: &Rectangle,
290        _renderer: &crate::Renderer,
291    ) -> mouse::Interaction {
292        if cursor_position.is_over(layout.bounds()) {
293            mouse::Interaction::Pointer
294        } else {
295            mouse::Interaction::default()
296        }
297    }
298
299    fn draw(
300        &self,
301        tree: &Tree,
302        renderer: &mut crate::Renderer,
303        theme: &crate::Theme,
304        style: &renderer::Style,
305        layout: Layout<'_>,
306        cursor_position: mouse::Cursor,
307        viewport: &Rectangle,
308    ) {
309        let state = tree.state.downcast_ref::<State>();
310
311        let mut children = layout.children();
312        let label_layout = children.next().unwrap();
313
314        if let Some(_label) = &self.label {
315            let state: &State = tree.state.downcast_ref();
316            iced_widget::text::draw(
317                renderer,
318                style,
319                label_layout.bounds(),
320                state.text.raw(),
321                iced_widget::text::Style::default(),
322                viewport,
323            );
324        }
325
326        let toggler_layout = children.next().unwrap();
327        let bounds = toggler_layout.bounds();
328
329        let is_mouse_over = cursor_position.is_over(bounds);
330
331        // let style = blend_appearances(
332        //     theme.style(
333        //         &(),
334        //         if is_mouse_over {
335        //             Status::Hovered { is_toggled: false }
336        //         } else {
337        //             Status::Active { is_toggled: false }
338        //         },
339        //     ),
340        //     theme.style(
341        //         &(),
342        //         if is_mouse_over {
343        //             Status::Hovered { is_toggled: true }
344        //         } else {
345        //             Status::Active { is_toggled: true }
346        //         },
347        //     ),
348        //     percent,
349        // );
350
351        let style = theme.style(
352            &(),
353            if is_mouse_over {
354                Status::Hovered {
355                    is_toggled: self.is_toggled,
356                }
357            } else {
358                Status::Active {
359                    is_toggled: self.is_toggled,
360                }
361            },
362        );
363
364        let space = style.handle_margin;
365
366        let toggler_background_bounds = Rectangle {
367            x: bounds.x,
368            y: bounds.y,
369            width: bounds.width,
370            height: bounds.height,
371        };
372
373        renderer.fill_quad(
374            renderer::Quad {
375                bounds: toggler_background_bounds,
376                border: Border {
377                    radius: style.border_radius,
378                    ..Default::default()
379                },
380                ..renderer::Quad::default()
381            },
382            style.background,
383        );
384        let mut t = state.anim.t(self.duration, self.is_toggled);
385
386        let toggler_foreground_bounds = Rectangle {
387            x: bounds.x
388                + anim::slerp(
389                    space,
390                    bounds.width - space - (bounds.height - (2.0 * space)),
391                    t,
392                ),
393
394            y: bounds.y + space,
395            width: bounds.height - (2.0 * space),
396            height: bounds.height - (2.0 * space),
397        };
398
399        renderer.fill_quad(
400            renderer::Quad {
401                bounds: toggler_foreground_bounds,
402                border: Border {
403                    radius: style.handle_radius,
404                    ..Default::default()
405                },
406                ..renderer::Quad::default()
407            },
408            style.foreground,
409        );
410    }
411}
412
413impl<'a, Message: 'static> From<Toggler<'a, Message>> for Element<'a, Message> {
414    fn from(toggler: Toggler<'a, Message>) -> Element<'a, Message> {
415        Element::new(toggler)
416    }
417}
418
419/// Produces a [`Node`] with two children nodes one right next to each other.
420pub fn next_to_each_other(
421    limits: &iced::Limits,
422    spacing: f32,
423    left: impl FnOnce(&iced::Limits) -> iced_core::layout::Node,
424    right: impl FnOnce(&iced::Limits) -> iced_core::layout::Node,
425) -> iced_core::layout::Node {
426    let mut right_node = right(limits);
427    let right_size = right_node.size();
428
429    let left_limits = limits.shrink(Size::new(right_size.width + spacing, 0.0));
430    let mut left_node = left(&left_limits);
431    let left_size = left_node.size();
432
433    let (left_y, right_y) = if left_size.height > right_size.height {
434        (0.0, (left_size.height - right_size.height) / 2.0)
435    } else {
436        ((right_size.height - left_size.height) / 2.0, 0.0)
437    };
438
439    left_node = left_node.move_to(iced::Point::new(0.0, left_y));
440    right_node = right_node.move_to(iced::Point::new(left_size.width + spacing, right_y));
441
442    iced_core::layout::Node::with_children(
443        Size::new(
444            left_size.width + spacing + right_size.width,
445            left_size.height.max(right_size.height),
446        ),
447        vec![left_node, right_node],
448    )
449}
450
451#[derive(Debug, Default)]
452pub struct State {
453    text: widget::text::State<<crate::Renderer as iced_core::text::Renderer>::Paragraph>,
454    anim: anim::State,
455    prev_toggled: bool,
456    hovered: bool,
457}