egui/widgets/
checkbox.rs

1use emath::Rect;
2
3use crate::{
4    Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui, Vec2, Widget,
5    WidgetInfo, WidgetType, epaint, pos2, widget_style::CheckboxStyle,
6};
7
8// TODO(emilk): allow checkbox without a text label
9/// Boolean on/off control with text label.
10///
11/// Usually you'd use [`Ui::checkbox`] instead.
12///
13/// ```
14/// # egui::__run_test_ui(|ui| {
15/// # let mut my_bool = true;
16/// // These are equivalent:
17/// ui.checkbox(&mut my_bool, "Checked");
18/// ui.add(egui::Checkbox::new(&mut my_bool, "Checked"));
19/// # });
20/// ```
21#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
22pub struct Checkbox<'a> {
23    checked: &'a mut bool,
24    atoms: Atoms<'a>,
25    indeterminate: bool,
26}
27
28impl<'a> Checkbox<'a> {
29    pub fn new(checked: &'a mut bool, atoms: impl IntoAtoms<'a>) -> Self {
30        Checkbox {
31            checked,
32            atoms: atoms.into_atoms(),
33            indeterminate: false,
34        }
35    }
36
37    pub fn without_text(checked: &'a mut bool) -> Self {
38        Self::new(checked, ())
39    }
40
41    /// Display an indeterminate state (neither checked nor unchecked)
42    ///
43    /// This only affects the checkbox's appearance. It will still toggle its boolean value when
44    /// clicked.
45    #[inline]
46    pub fn indeterminate(mut self, indeterminate: bool) -> Self {
47        self.indeterminate = indeterminate;
48        self
49    }
50}
51
52impl Widget for Checkbox<'_> {
53    fn ui(self, ui: &mut Ui) -> Response {
54        let Checkbox {
55            checked,
56            mut atoms,
57            indeterminate,
58        } = self;
59
60        // Get the widget style by reading the response from the previous pass
61        let id = ui.next_auto_id();
62        let response: Option<Response> = ui.ctx().read_response(id);
63        let state = response.map(|r| r.widget_state()).unwrap_or_default();
64
65        let CheckboxStyle {
66            check_size,
67            checkbox_frame,
68            checkbox_size,
69            frame,
70            check_stroke,
71            text_style,
72        } = ui.style().checkbox_style(state);
73
74        let mut min_size = Vec2::splat(ui.spacing().interact_size.y);
75        min_size.y = min_size.y.at_least(checkbox_size);
76
77        // In order to center the checkbox based on min_size we set the icon height to at least min_size.y
78        let mut icon_size = Vec2::splat(checkbox_size);
79        icon_size.y = icon_size.y.at_least(min_size.y);
80        let rect_id = Id::new("egui::checkbox");
81        atoms.push_left(Atom::custom(rect_id, icon_size));
82
83        let text = atoms.text().map(String::from);
84
85        let mut prepared = AtomLayout::new(atoms)
86            .sense(Sense::click())
87            .min_size(min_size)
88            .frame(frame)
89            .allocate(ui);
90
91        if prepared.response.clicked() {
92            *checked = !*checked;
93            prepared.response.mark_changed();
94        }
95        prepared.response.widget_info(|| {
96            if indeterminate {
97                WidgetInfo::labeled(
98                    WidgetType::Checkbox,
99                    ui.is_enabled(),
100                    text.as_deref().unwrap_or(""),
101                )
102            } else {
103                WidgetInfo::selected(
104                    WidgetType::Checkbox,
105                    ui.is_enabled(),
106                    *checked,
107                    text.as_deref().unwrap_or(""),
108                )
109            }
110        });
111
112        if ui.is_rect_visible(prepared.response.rect) {
113            prepared.fallback_text_color = text_style.color;
114            let response = prepared.paint(ui);
115
116            if let Some(rect) = response.rect(rect_id) {
117                let big_icon_rect = Rect::from_center_size(
118                    pos2(rect.left() + checkbox_size / 2.0, rect.center().y),
119                    Vec2::splat(checkbox_size),
120                );
121                let small_icon_rect =
122                    Rect::from_center_size(big_icon_rect.center(), Vec2::splat(check_size));
123                ui.painter().add(epaint::RectShape::new(
124                    big_icon_rect.expand(checkbox_frame.inner_margin.left.into()),
125                    checkbox_frame.corner_radius,
126                    checkbox_frame.fill,
127                    checkbox_frame.stroke,
128                    epaint::StrokeKind::Inside,
129                ));
130
131                if indeterminate {
132                    // Horizontal line:
133                    ui.painter().add(Shape::hline(
134                        small_icon_rect.x_range(),
135                        small_icon_rect.center().y,
136                        check_stroke,
137                    ));
138                } else if *checked {
139                    // Check mark:
140                    ui.painter().add(Shape::line(
141                        vec![
142                            pos2(small_icon_rect.left(), small_icon_rect.center().y),
143                            pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
144                            pos2(small_icon_rect.right(), small_icon_rect.top()),
145                        ],
146                        check_stroke,
147                    ));
148                }
149            }
150            response.response
151        } else {
152            prepared.response
153        }
154    }
155}