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::ButtonPressed(mouse::Button::Left))
253            | Event::Touch(touch::Event::FingerPressed { .. }) => {
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            _ => {}
270        }
271    }
272
273    fn mouse_interaction(
274        &self,
275        _state: &Tree,
276        layout: Layout<'_>,
277        cursor_position: mouse::Cursor,
278        _viewport: &Rectangle,
279        _renderer: &crate::Renderer,
280    ) -> mouse::Interaction {
281        if cursor_position.is_over(layout.bounds()) {
282            mouse::Interaction::Pointer
283        } else {
284            mouse::Interaction::default()
285        }
286    }
287
288    fn draw(
289        &self,
290        tree: &Tree,
291        renderer: &mut crate::Renderer,
292        theme: &crate::Theme,
293        style: &renderer::Style,
294        layout: Layout<'_>,
295        cursor_position: mouse::Cursor,
296        viewport: &Rectangle,
297    ) {
298        let state = tree.state.downcast_ref::<State>();
299
300        let mut children = layout.children();
301        let label_layout = children.next().unwrap();
302
303        if let Some(_label) = &self.label {
304            let state: &State = tree.state.downcast_ref();
305            iced_widget::text::draw(
306                renderer,
307                style,
308                label_layout.bounds(),
309                state.text.raw(),
310                iced_widget::text::Style::default(),
311                viewport,
312            );
313        }
314
315        let toggler_layout = children.next().unwrap();
316        let bounds = toggler_layout.bounds();
317
318        let is_mouse_over = cursor_position.is_over(bounds);
319
320        // let style = blend_appearances(
321        //     theme.style(
322        //         &(),
323        //         if is_mouse_over {
324        //             Status::Hovered { is_toggled: false }
325        //         } else {
326        //             Status::Active { is_toggled: false }
327        //         },
328        //     ),
329        //     theme.style(
330        //         &(),
331        //         if is_mouse_over {
332        //             Status::Hovered { is_toggled: true }
333        //         } else {
334        //             Status::Active { is_toggled: true }
335        //         },
336        //     ),
337        //     percent,
338        // );
339
340        let style = theme.style(
341            &(),
342            if is_mouse_over {
343                Status::Hovered {
344                    is_toggled: self.is_toggled,
345                }
346            } else {
347                Status::Active {
348                    is_toggled: self.is_toggled,
349                }
350            },
351        );
352
353        let space = style.handle_margin;
354
355        let toggler_background_bounds = Rectangle {
356            x: bounds.x,
357            y: bounds.y,
358            width: bounds.width,
359            height: bounds.height,
360        };
361
362        renderer.fill_quad(
363            renderer::Quad {
364                bounds: toggler_background_bounds,
365                border: Border {
366                    radius: style.border_radius,
367                    ..Default::default()
368                },
369                ..renderer::Quad::default()
370            },
371            style.background,
372        );
373        let mut t = state.anim.t(self.duration, self.is_toggled);
374
375        let toggler_foreground_bounds = Rectangle {
376            x: bounds.x
377                + anim::slerp(
378                    space,
379                    bounds.width - space - (bounds.height - (2.0 * space)),
380                    t,
381                ),
382
383            y: bounds.y + space,
384            width: bounds.height - (2.0 * space),
385            height: bounds.height - (2.0 * space),
386        };
387
388        renderer.fill_quad(
389            renderer::Quad {
390                bounds: toggler_foreground_bounds,
391                border: Border {
392                    radius: style.handle_radius,
393                    ..Default::default()
394                },
395                ..renderer::Quad::default()
396            },
397            style.foreground,
398        );
399    }
400}
401
402impl<'a, Message: 'static> From<Toggler<'a, Message>> for Element<'a, Message> {
403    fn from(toggler: Toggler<'a, Message>) -> Element<'a, Message> {
404        Element::new(toggler)
405    }
406}
407
408/// Produces a [`Node`] with two children nodes one right next to each other.
409pub fn next_to_each_other(
410    limits: &iced::Limits,
411    spacing: f32,
412    left: impl FnOnce(&iced::Limits) -> iced_core::layout::Node,
413    right: impl FnOnce(&iced::Limits) -> iced_core::layout::Node,
414) -> iced_core::layout::Node {
415    let mut right_node = right(limits);
416    let right_size = right_node.size();
417
418    let left_limits = limits.shrink(Size::new(right_size.width + spacing, 0.0));
419    let mut left_node = left(&left_limits);
420    let left_size = left_node.size();
421
422    let (left_y, right_y) = if left_size.height > right_size.height {
423        (0.0, (left_size.height - right_size.height) / 2.0)
424    } else {
425        ((right_size.height - left_size.height) / 2.0, 0.0)
426    };
427
428    left_node = left_node.move_to(iced::Point::new(0.0, left_y));
429    right_node = right_node.move_to(iced::Point::new(left_size.width + spacing, right_y));
430
431    iced_core::layout::Node::with_children(
432        Size::new(
433            left_size.width + spacing + right_size.width,
434            left_size.height.max(right_size.height),
435        ),
436        vec![left_node, right_node],
437    )
438}
439
440#[derive(Debug, Default)]
441pub struct State {
442    text: widget::text::State<<crate::Renderer as iced_core::text::Renderer>::Paragraph>,
443    anim: anim::State,
444    prev_toggled: bool,
445}