iced_native/widget/
checkbox.rs

1//! Show toggle controls using checkboxes.
2use crate::alignment;
3use crate::event::{self, Event};
4use crate::layout;
5use crate::mouse;
6use crate::renderer;
7use crate::text;
8use crate::touch;
9use crate::widget::{self, Row, Text, Tree};
10use crate::{
11    Alignment, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle,
12    Shell, Widget,
13};
14
15pub use iced_style::checkbox::{Appearance, StyleSheet};
16
17/// A box that can be checked.
18///
19/// # Example
20///
21/// ```
22/// # type Checkbox<'a, Message> = iced_native::widget::Checkbox<'a, Message, iced_native::renderer::Null>;
23/// #
24/// pub enum Message {
25///     CheckboxToggled(bool),
26/// }
27///
28/// let is_checked = true;
29///
30/// Checkbox::new("Toggle me!", is_checked, Message::CheckboxToggled);
31/// ```
32///
33/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true)
34#[allow(missing_debug_implementations)]
35pub struct Checkbox<'a, Message, Renderer>
36where
37    Renderer: text::Renderer,
38    Renderer::Theme: StyleSheet + widget::text::StyleSheet,
39{
40    is_checked: bool,
41    on_toggle: Box<dyn Fn(bool) -> Message + 'a>,
42    label: String,
43    width: Length,
44    size: f32,
45    spacing: f32,
46    text_size: Option<f32>,
47    font: Renderer::Font,
48    icon: Icon<Renderer::Font>,
49    style: <Renderer::Theme as StyleSheet>::Style,
50}
51
52impl<'a, Message, Renderer> Checkbox<'a, Message, Renderer>
53where
54    Renderer: text::Renderer,
55    Renderer::Theme: StyleSheet + widget::text::StyleSheet,
56{
57    /// The default size of a [`Checkbox`].
58    const DEFAULT_SIZE: f32 = 20.0;
59
60    /// The default spacing of a [`Checkbox`].
61    const DEFAULT_SPACING: f32 = 15.0;
62
63    /// Creates a new [`Checkbox`].
64    ///
65    /// It expects:
66    ///   * a boolean describing whether the [`Checkbox`] is checked or not
67    ///   * the label of the [`Checkbox`]
68    ///   * a function that will be called when the [`Checkbox`] is toggled. It
69    ///     will receive the new state of the [`Checkbox`] and must produce a
70    ///     `Message`.
71    pub fn new<F>(label: impl Into<String>, is_checked: bool, f: F) -> Self
72    where
73        F: 'a + Fn(bool) -> Message,
74    {
75        Checkbox {
76            is_checked,
77            on_toggle: Box::new(f),
78            label: label.into(),
79            width: Length::Shrink,
80            size: Self::DEFAULT_SIZE,
81            spacing: Self::DEFAULT_SPACING,
82            text_size: None,
83            font: Renderer::Font::default(),
84            icon: Icon {
85                font: Renderer::ICON_FONT,
86                code_point: Renderer::CHECKMARK_ICON,
87                size: None,
88            },
89            style: Default::default(),
90        }
91    }
92
93    /// Sets the size of the [`Checkbox`].
94    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
95        self.size = size.into().0;
96        self
97    }
98
99    /// Sets the width of the [`Checkbox`].
100    pub fn width(mut self, width: impl Into<Length>) -> Self {
101        self.width = width.into();
102        self
103    }
104
105    /// Sets the spacing between the [`Checkbox`] and the text.
106    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
107        self.spacing = spacing.into().0;
108        self
109    }
110
111    /// Sets the text size of the [`Checkbox`].
112    pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
113        self.text_size = Some(text_size.into().0);
114        self
115    }
116
117    /// Sets the [`Font`] of the text of the [`Checkbox`].
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 [`Icon`] of the [`Checkbox`].
126    pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
127        self.icon = icon;
128        self
129    }
130
131    /// Sets the style of the [`Checkbox`].
132    pub fn style(
133        mut self,
134        style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
135    ) -> Self {
136        self.style = style.into();
137        self
138    }
139}
140
141impl<'a, Message, Renderer> Widget<Message, Renderer>
142    for Checkbox<'a, Message, Renderer>
143where
144    Renderer: text::Renderer,
145    Renderer::Theme: StyleSheet + widget::text::StyleSheet,
146{
147    fn width(&self) -> Length {
148        self.width
149    }
150
151    fn height(&self) -> Length {
152        Length::Shrink
153    }
154
155    fn layout(
156        &self,
157        renderer: &Renderer,
158        limits: &layout::Limits,
159    ) -> layout::Node {
160        Row::<(), Renderer>::new()
161            .width(self.width)
162            .spacing(self.spacing)
163            .align_items(Alignment::Center)
164            .push(Row::new().width(self.size).height(self.size))
165            .push(
166                Text::new(&self.label)
167                    .font(self.font.clone())
168                    .width(self.width)
169                    .size(
170                        self.text_size
171                            .unwrap_or_else(|| renderer.default_size()),
172                    ),
173            )
174            .layout(renderer, limits)
175    }
176
177    fn on_event(
178        &mut self,
179        _tree: &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            | Event::Touch(touch::Event::FingerPressed { .. }) => {
190                let mouse_over = layout.bounds().contains(cursor_position);
191
192                if mouse_over {
193                    shell.publish((self.on_toggle)(!self.is_checked));
194
195                    return event::Status::Captured;
196                }
197            }
198            _ => {}
199        }
200
201        event::Status::Ignored
202    }
203
204    fn mouse_interaction(
205        &self,
206        _tree: &Tree,
207        layout: Layout<'_>,
208        cursor_position: Point,
209        _viewport: &Rectangle,
210        _renderer: &Renderer,
211    ) -> mouse::Interaction {
212        if layout.bounds().contains(cursor_position) {
213            mouse::Interaction::Pointer
214        } else {
215            mouse::Interaction::default()
216        }
217    }
218
219    fn draw(
220        &self,
221        _tree: &Tree,
222        renderer: &mut Renderer,
223        theme: &Renderer::Theme,
224        style: &renderer::Style,
225        layout: Layout<'_>,
226        cursor_position: Point,
227        _viewport: &Rectangle,
228    ) {
229        let bounds = layout.bounds();
230        let is_mouse_over = bounds.contains(cursor_position);
231
232        let mut children = layout.children();
233
234        let custom_style = if is_mouse_over {
235            theme.hovered(&self.style, self.is_checked)
236        } else {
237            theme.active(&self.style, self.is_checked)
238        };
239
240        {
241            let layout = children.next().unwrap();
242            let bounds = layout.bounds();
243
244            renderer.fill_quad(
245                renderer::Quad {
246                    bounds,
247                    border_radius: custom_style.border_radius.into(),
248                    border_width: custom_style.border_width,
249                    border_color: custom_style.border_color,
250                },
251                custom_style.background,
252            );
253
254            let Icon {
255                font,
256                code_point,
257                size,
258            } = &self.icon;
259            let size = size.map(f32::from).unwrap_or(bounds.height * 0.7);
260
261            if self.is_checked {
262                renderer.fill_text(text::Text {
263                    content: &code_point.to_string(),
264                    font: font.clone(),
265                    size,
266                    bounds: Rectangle {
267                        x: bounds.center_x(),
268                        y: bounds.center_y(),
269                        ..bounds
270                    },
271                    color: custom_style.icon_color,
272                    horizontal_alignment: alignment::Horizontal::Center,
273                    vertical_alignment: alignment::Vertical::Center,
274                });
275            }
276        }
277
278        {
279            let label_layout = children.next().unwrap();
280
281            widget::text::draw(
282                renderer,
283                style,
284                label_layout,
285                &self.label,
286                self.text_size,
287                self.font.clone(),
288                widget::text::Appearance {
289                    color: custom_style.text_color,
290                },
291                alignment::Horizontal::Left,
292                alignment::Vertical::Center,
293            );
294        }
295    }
296}
297
298impl<'a, Message, Renderer> From<Checkbox<'a, Message, Renderer>>
299    for Element<'a, Message, Renderer>
300where
301    Message: 'a,
302    Renderer: 'a + text::Renderer,
303    Renderer::Theme: StyleSheet + widget::text::StyleSheet,
304{
305    fn from(
306        checkbox: Checkbox<'a, Message, Renderer>,
307    ) -> Element<'a, Message, Renderer> {
308        Element::new(checkbox)
309    }
310}
311
312/// The icon in a [`Checkbox`].
313#[derive(Debug, Clone, PartialEq)]
314pub struct Icon<Font> {
315    /// Font that will be used to display the `code_point`,
316    pub font: Font,
317    /// The unicode code point that will be used as the icon.
318    pub code_point: char,
319    /// Font size of the content.
320    pub size: Option<f32>,
321}