iced_native/widget/
toggler.rs

1//! Show toggle controls using togglers.
2use crate::alignment;
3use crate::event;
4use crate::layout;
5use crate::mouse;
6use crate::renderer;
7use crate::text;
8use crate::widget::{self, Row, Text, Tree};
9use crate::{
10    Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point,
11    Rectangle, Shell, Widget,
12};
13
14pub use iced_style::toggler::{Appearance, StyleSheet};
15
16/// A toggler widget.
17///
18/// # Example
19///
20/// ```
21/// # type Toggler<'a, Message> = iced_native::widget::Toggler<'a, Message, iced_native::renderer::Null>;
22/// #
23/// pub enum Message {
24///     TogglerToggled(bool),
25/// }
26///
27/// let is_toggled = true;
28///
29/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b));
30/// ```
31#[allow(missing_debug_implementations)]
32pub struct Toggler<'a, Message, Renderer>
33where
34    Renderer: text::Renderer,
35    Renderer::Theme: StyleSheet,
36{
37    is_toggled: bool,
38    on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
39    label: Option<String>,
40    width: Length,
41    size: f32,
42    text_size: Option<f32>,
43    text_alignment: alignment::Horizontal,
44    spacing: f32,
45    font: Renderer::Font,
46    style: <Renderer::Theme as StyleSheet>::Style,
47}
48
49impl<'a, Message, Renderer> Toggler<'a, Message, Renderer>
50where
51    Renderer: text::Renderer,
52    Renderer::Theme: StyleSheet,
53{
54    /// The default size of a [`Toggler`].
55    pub const DEFAULT_SIZE: f32 = 20.0;
56
57    /// Creates a new [`Toggler`].
58    ///
59    /// It expects:
60    ///   * a boolean describing whether the [`Toggler`] is checked or not
61    ///   * An optional label for the [`Toggler`]
62    ///   * a function that will be called when the [`Toggler`] is toggled. It
63    ///     will receive the new state of the [`Toggler`] and must produce a
64    ///     `Message`.
65    pub fn new<F>(
66        label: impl Into<Option<String>>,
67        is_toggled: bool,
68        f: F,
69    ) -> Self
70    where
71        F: 'a + Fn(bool) -> Message,
72    {
73        Toggler {
74            is_toggled,
75            on_toggle: Box::new(f),
76            label: label.into(),
77            width: Length::Fill,
78            size: Self::DEFAULT_SIZE,
79            text_size: None,
80            text_alignment: alignment::Horizontal::Left,
81            spacing: 0.0,
82            font: Renderer::Font::default(),
83            style: Default::default(),
84        }
85    }
86
87    /// Sets the size of the [`Toggler`].
88    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
89        self.size = size.into().0;
90        self
91    }
92
93    /// Sets the width of the [`Toggler`].
94    pub fn width(mut self, width: impl Into<Length>) -> Self {
95        self.width = width.into();
96        self
97    }
98
99    /// Sets the text size o the [`Toggler`].
100    pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
101        self.text_size = Some(text_size.into().0);
102        self
103    }
104
105    /// Sets the horizontal alignment of the text of the [`Toggler`]
106    pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self {
107        self.text_alignment = alignment;
108        self
109    }
110
111    /// Sets the spacing between the [`Toggler`] and the text.
112    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
113        self.spacing = spacing.into().0;
114        self
115    }
116
117    /// Sets the [`Font`] of the text of the [`Toggler`]
118    ///
119    /// [`Font`]: crate::text::Renderer::Font
120    pub fn font(mut self, font: Renderer::Font) -> Self {
121        self.font = font;
122        self
123    }
124
125    /// Sets the style of the [`Toggler`].
126    pub fn style(
127        mut self,
128        style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
129    ) -> Self {
130        self.style = style.into();
131        self
132    }
133}
134
135impl<'a, Message, Renderer> Widget<Message, Renderer>
136    for Toggler<'a, Message, Renderer>
137where
138    Renderer: text::Renderer,
139    Renderer::Theme: StyleSheet + widget::text::StyleSheet,
140{
141    fn width(&self) -> Length {
142        self.width
143    }
144
145    fn height(&self) -> Length {
146        Length::Shrink
147    }
148
149    fn layout(
150        &self,
151        renderer: &Renderer,
152        limits: &layout::Limits,
153    ) -> layout::Node {
154        let mut row = Row::<(), Renderer>::new()
155            .width(self.width)
156            .spacing(self.spacing)
157            .align_items(Alignment::Center);
158
159        if let Some(label) = &self.label {
160            row = row.push(
161                Text::new(label)
162                    .horizontal_alignment(self.text_alignment)
163                    .font(self.font.clone())
164                    .width(self.width)
165                    .size(
166                        self.text_size
167                            .unwrap_or_else(|| renderer.default_size()),
168                    ),
169            );
170        }
171
172        row = row.push(Row::new().width(2.0 * self.size).height(self.size));
173
174        row.layout(renderer, limits)
175    }
176
177    fn on_event(
178        &mut self,
179        _state: &mut Tree,
180        event: Event,
181        layout: Layout<'_>,
182        cursor_position: Point,
183        _renderer: &Renderer,
184        _clipboard: &mut dyn Clipboard,
185        shell: &mut Shell<'_, Message>,
186    ) -> event::Status {
187        match event {
188            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
189                let mouse_over = layout.bounds().contains(cursor_position);
190
191                if mouse_over {
192                    shell.publish((self.on_toggle)(!self.is_toggled));
193
194                    event::Status::Captured
195                } else {
196                    event::Status::Ignored
197                }
198            }
199            _ => event::Status::Ignored,
200        }
201    }
202
203    fn mouse_interaction(
204        &self,
205        _state: &Tree,
206        layout: Layout<'_>,
207        cursor_position: Point,
208        _viewport: &Rectangle,
209        _renderer: &Renderer,
210    ) -> mouse::Interaction {
211        if layout.bounds().contains(cursor_position) {
212            mouse::Interaction::Pointer
213        } else {
214            mouse::Interaction::default()
215        }
216    }
217
218    fn draw(
219        &self,
220        _state: &Tree,
221        renderer: &mut Renderer,
222        theme: &Renderer::Theme,
223        style: &renderer::Style,
224        layout: Layout<'_>,
225        cursor_position: Point,
226        _viewport: &Rectangle,
227    ) {
228        /// Makes sure that the border radius of the toggler looks good at every size.
229        const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0;
230
231        /// The space ratio between the background Quad and the Toggler bounds, and
232        /// between the background Quad and foreground Quad.
233        const SPACE_RATIO: f32 = 0.05;
234
235        let mut children = layout.children();
236
237        if let Some(label) = &self.label {
238            let label_layout = children.next().unwrap();
239
240            crate::widget::text::draw(
241                renderer,
242                style,
243                label_layout,
244                label,
245                self.text_size,
246                self.font.clone(),
247                Default::default(),
248                self.text_alignment,
249                alignment::Vertical::Center,
250            );
251        }
252
253        let toggler_layout = children.next().unwrap();
254        let bounds = toggler_layout.bounds();
255
256        let is_mouse_over = bounds.contains(cursor_position);
257
258        let style = if is_mouse_over {
259            theme.hovered(&self.style, self.is_toggled)
260        } else {
261            theme.active(&self.style, self.is_toggled)
262        };
263
264        let border_radius = bounds.height / BORDER_RADIUS_RATIO;
265        let space = SPACE_RATIO * bounds.height;
266
267        let toggler_background_bounds = Rectangle {
268            x: bounds.x + space,
269            y: bounds.y + space,
270            width: bounds.width - (2.0 * space),
271            height: bounds.height - (2.0 * space),
272        };
273
274        renderer.fill_quad(
275            renderer::Quad {
276                bounds: toggler_background_bounds,
277                border_radius: border_radius.into(),
278                border_width: 1.0,
279                border_color: style
280                    .background_border
281                    .unwrap_or(style.background),
282            },
283            style.background,
284        );
285
286        let toggler_foreground_bounds = Rectangle {
287            x: bounds.x
288                + if self.is_toggled {
289                    bounds.width - 2.0 * space - (bounds.height - (4.0 * space))
290                } else {
291                    2.0 * space
292                },
293            y: bounds.y + (2.0 * space),
294            width: bounds.height - (4.0 * space),
295            height: bounds.height - (4.0 * space),
296        };
297
298        renderer.fill_quad(
299            renderer::Quad {
300                bounds: toggler_foreground_bounds,
301                border_radius: border_radius.into(),
302                border_width: 1.0,
303                border_color: style
304                    .foreground_border
305                    .unwrap_or(style.foreground),
306            },
307            style.foreground,
308        );
309    }
310}
311
312impl<'a, Message, Renderer> From<Toggler<'a, Message, Renderer>>
313    for Element<'a, Message, Renderer>
314where
315    Message: 'a,
316    Renderer: 'a + text::Renderer,
317    Renderer::Theme: StyleSheet + widget::text::StyleSheet,
318{
319    fn from(
320        toggler: Toggler<'a, Message, Renderer>,
321    ) -> Element<'a, Message, Renderer> {
322        Element::new(toggler)
323    }
324}