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 {
165            prev_toggled: self.is_toggled,
166            ..State::default()
167        })
168    }
169
170    fn id(&self) -> Option<Id> {
171        Some(self.id.clone())
172    }
173
174    fn set_id(&mut self, id: Id) {
175        self.id = id;
176    }
177
178    fn layout(
179        &mut self,
180        tree: &mut Tree,
181        renderer: &crate::Renderer,
182        limits: &layout::Limits,
183    ) -> layout::Node {
184        let limits = limits.width(self.width);
185
186        let res = next_to_each_other(
187            &limits,
188            self.spacing,
189            |limits| {
190                if let Some(label) = self.label.as_deref() {
191                    let state = tree.state.downcast_mut::<State>();
192                    let node = iced_core::widget::text::layout(
193                        &mut state.text,
194                        renderer,
195                        limits,
196                        label,
197                        widget::text::Format {
198                            width: self.width,
199                            height: Length::Shrink,
200                            line_height: self.text_line_height,
201                            size: self.text_size.map(iced::Pixels),
202                            font: self.font,
203                            align_x: self.text_alignment,
204                            align_y: alignment::Vertical::Top,
205                            shaping: self.text_shaping,
206                            wrapping: iced_core::text::Wrapping::default(),
207                            ellipsize: self.ellipsize,
208                        },
209                    );
210                    match self.width {
211                        Length::Fill => {
212                            let size = node.size();
213                            layout::Node::with_children(
214                                Size::new(limits.width(Length::Fill).max().width, size.height),
215                                vec![node],
216                            )
217                        }
218                        _ => node,
219                    }
220                } else {
221                    layout::Node::new(iced_core::Size::ZERO)
222                }
223            },
224            |_| layout::Node::new(Size::new(48., 24.)),
225        );
226        res
227    }
228
229    fn update(
230        &mut self,
231        tree: &mut Tree,
232        event: &Event,
233        layout: Layout<'_>,
234        cursor_position: mouse::Cursor,
235        _renderer: &crate::Renderer,
236        _clipboard: &mut dyn Clipboard,
237        shell: &mut Shell<'_, Message>,
238        _viewport: &Rectangle,
239    ) {
240        let Some(on_toggle) = self.on_toggle.as_ref() else {
241            return;
242        };
243        let state = tree.state.downcast_mut::<State>();
244
245        // animate external changes
246        if state.prev_toggled != self.is_toggled {
247            state.anim.changed(self.duration);
248            shell.request_redraw();
249            state.prev_toggled = self.is_toggled;
250        }
251
252        match event {
253            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
254            | Event::Touch(touch::Event::FingerPressed { .. }) => {
255                let mouse_over = cursor_position.is_over(layout.bounds());
256
257                if mouse_over {
258                    shell.publish((on_toggle)(!self.is_toggled));
259                    state.anim.changed(self.duration);
260                    state.prev_toggled = !self.is_toggled;
261                    shell.capture_event();
262                }
263            }
264            Event::Window(window::Event::RedrawRequested(now)) => {
265                state.anim.anim_done(self.duration);
266                if state.anim.last_change.is_some() {
267                    shell.request_redraw();
268                }
269            }
270            _ => {}
271        }
272    }
273
274    fn mouse_interaction(
275        &self,
276        _state: &Tree,
277        layout: Layout<'_>,
278        cursor_position: mouse::Cursor,
279        _viewport: &Rectangle,
280        _renderer: &crate::Renderer,
281    ) -> mouse::Interaction {
282        if cursor_position.is_over(layout.bounds()) {
283            mouse::Interaction::Pointer
284        } else {
285            mouse::Interaction::default()
286        }
287    }
288
289    fn draw(
290        &self,
291        tree: &Tree,
292        renderer: &mut crate::Renderer,
293        theme: &crate::Theme,
294        style: &renderer::Style,
295        layout: Layout<'_>,
296        cursor_position: mouse::Cursor,
297        viewport: &Rectangle,
298    ) {
299        let state = tree.state.downcast_ref::<State>();
300
301        let mut children = layout.children();
302        let label_layout = children.next().unwrap();
303
304        if let Some(_label) = &self.label {
305            let state: &State = tree.state.downcast_ref();
306            iced_widget::text::draw(
307                renderer,
308                style,
309                label_layout.bounds(),
310                state.text.raw(),
311                iced_widget::text::Style::default(),
312                viewport,
313            );
314        }
315
316        let toggler_layout = children.next().unwrap();
317        let bounds = toggler_layout.bounds();
318
319        let is_mouse_over = cursor_position.is_over(bounds);
320
321        // let style = blend_appearances(
322        //     theme.style(
323        //         &(),
324        //         if is_mouse_over {
325        //             Status::Hovered { is_toggled: false }
326        //         } else {
327        //             Status::Active { is_toggled: false }
328        //         },
329        //     ),
330        //     theme.style(
331        //         &(),
332        //         if is_mouse_over {
333        //             Status::Hovered { is_toggled: true }
334        //         } else {
335        //             Status::Active { is_toggled: true }
336        //         },
337        //     ),
338        //     percent,
339        // );
340
341        let style = theme.style(
342            &(),
343            if is_mouse_over {
344                Status::Hovered {
345                    is_toggled: self.is_toggled,
346                }
347            } else {
348                Status::Active {
349                    is_toggled: self.is_toggled,
350                }
351            },
352        );
353
354        let space = style.handle_margin;
355
356        let toggler_background_bounds = Rectangle {
357            x: bounds.x,
358            y: bounds.y,
359            width: bounds.width,
360            height: bounds.height,
361        };
362
363        renderer.fill_quad(
364            renderer::Quad {
365                bounds: toggler_background_bounds,
366                border: Border {
367                    radius: style.border_radius,
368                    ..Default::default()
369                },
370                ..renderer::Quad::default()
371            },
372            style.background,
373        );
374        let mut t = state.anim.t(self.duration, self.is_toggled);
375
376        let toggler_foreground_bounds = Rectangle {
377            x: bounds.x
378                + anim::slerp(
379                    space,
380                    bounds.width - space - (bounds.height - (2.0 * space)),
381                    t,
382                ),
383
384            y: bounds.y + space,
385            width: bounds.height - (2.0 * space),
386            height: bounds.height - (2.0 * space),
387        };
388
389        renderer.fill_quad(
390            renderer::Quad {
391                bounds: toggler_foreground_bounds,
392                border: Border {
393                    radius: style.handle_radius,
394                    ..Default::default()
395                },
396                ..renderer::Quad::default()
397            },
398            style.foreground,
399        );
400    }
401}
402
403impl<'a, Message: 'static> From<Toggler<'a, Message>> for Element<'a, Message> {
404    fn from(toggler: Toggler<'a, Message>) -> Element<'a, Message> {
405        Element::new(toggler)
406    }
407}
408
409/// Produces a [`Node`] with two children nodes one right next to each other.
410pub fn next_to_each_other(
411    limits: &iced::Limits,
412    spacing: f32,
413    left: impl FnOnce(&iced::Limits) -> iced_core::layout::Node,
414    right: impl FnOnce(&iced::Limits) -> iced_core::layout::Node,
415) -> iced_core::layout::Node {
416    let mut right_node = right(limits);
417    let right_size = right_node.size();
418
419    let left_limits = limits.shrink(Size::new(right_size.width + spacing, 0.0));
420    let mut left_node = left(&left_limits);
421    let left_size = left_node.size();
422
423    let (left_y, right_y) = if left_size.height > right_size.height {
424        (0.0, (left_size.height - right_size.height) / 2.0)
425    } else {
426        ((right_size.height - left_size.height) / 2.0, 0.0)
427    };
428
429    left_node = left_node.move_to(iced::Point::new(0.0, left_y));
430    right_node = right_node.move_to(iced::Point::new(left_size.width + spacing, right_y));
431
432    iced_core::layout::Node::with_children(
433        Size::new(
434            left_size.width + spacing + right_size.width,
435            left_size.height.max(right_size.height),
436        ),
437        vec![left_node, right_node],
438    )
439}
440
441#[derive(Debug, Default)]
442pub struct State {
443    text: widget::text::State<<crate::Renderer as iced_core::text::Renderer>::Paragraph>,
444    anim: anim::State,
445    prev_toggled: bool,
446}