cosmic/widget/
toggler.rs

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