egui/widgets/
checkbox.rs

1use crate::{
2    epaint, pos2, vec2, NumExt, Response, Sense, Shape, TextStyle, Ui, Vec2, Widget, WidgetInfo,
3    WidgetText, WidgetType,
4};
5
6// TODO(emilk): allow checkbox without a text label
7/// Boolean on/off control with text label.
8///
9/// Usually you'd use [`Ui::checkbox`] instead.
10///
11/// ```
12/// # egui::__run_test_ui(|ui| {
13/// # let mut my_bool = true;
14/// // These are equivalent:
15/// ui.checkbox(&mut my_bool, "Checked");
16/// ui.add(egui::Checkbox::new(&mut my_bool, "Checked"));
17/// # });
18/// ```
19#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
20pub struct Checkbox<'a> {
21    checked: &'a mut bool,
22    text: WidgetText,
23    indeterminate: bool,
24}
25
26impl<'a> Checkbox<'a> {
27    pub fn new(checked: &'a mut bool, text: impl Into<WidgetText>) -> Self {
28        Checkbox {
29            checked,
30            text: text.into(),
31            indeterminate: false,
32        }
33    }
34
35    pub fn without_text(checked: &'a mut bool) -> Self {
36        Self::new(checked, WidgetText::default())
37    }
38
39    /// Display an indeterminate state (neither checked nor unchecked)
40    ///
41    /// This only affects the checkbox's appearance. It will still toggle its boolean value when
42    /// clicked.
43    #[inline]
44    pub fn indeterminate(mut self, indeterminate: bool) -> Self {
45        self.indeterminate = indeterminate;
46        self
47    }
48}
49
50impl Widget for Checkbox<'_> {
51    fn ui(self, ui: &mut Ui) -> Response {
52        let Checkbox {
53            checked,
54            text,
55            indeterminate,
56        } = self;
57
58        let spacing = &ui.spacing();
59        let icon_width = spacing.icon_width;
60        let icon_spacing = spacing.icon_spacing;
61
62        let (galley, mut desired_size) = if text.is_empty() {
63            (None, vec2(icon_width, 0.0))
64        } else {
65            let total_extra = vec2(icon_width + icon_spacing, 0.0);
66
67            let wrap_width = ui.available_width() - total_extra.x;
68            let galley = text.into_galley(ui, None, wrap_width, TextStyle::Button);
69
70            let mut desired_size = total_extra + galley.size();
71            desired_size = desired_size.at_least(spacing.interact_size);
72
73            (Some(galley), desired_size)
74        };
75
76        desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
77        desired_size.y = desired_size.y.max(icon_width);
78        let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
79
80        if response.clicked() {
81            *checked = !*checked;
82            response.mark_changed();
83        }
84        response.widget_info(|| {
85            if indeterminate {
86                WidgetInfo::labeled(
87                    WidgetType::Checkbox,
88                    ui.is_enabled(),
89                    galley.as_ref().map_or("", |x| x.text()),
90                )
91            } else {
92                WidgetInfo::selected(
93                    WidgetType::Checkbox,
94                    ui.is_enabled(),
95                    *checked,
96                    galley.as_ref().map_or("", |x| x.text()),
97                )
98            }
99        });
100
101        if ui.is_rect_visible(rect) {
102            // let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
103            let visuals = ui.style().interact(&response);
104            let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
105            ui.painter().add(epaint::RectShape::new(
106                big_icon_rect.expand(visuals.expansion),
107                visuals.corner_radius,
108                visuals.bg_fill,
109                visuals.bg_stroke,
110                epaint::StrokeKind::Inside,
111            ));
112
113            if indeterminate {
114                // Horizontal line:
115                ui.painter().add(Shape::hline(
116                    small_icon_rect.x_range(),
117                    small_icon_rect.center().y,
118                    visuals.fg_stroke,
119                ));
120            } else if *checked {
121                // Check mark:
122                ui.painter().add(Shape::line(
123                    vec![
124                        pos2(small_icon_rect.left(), small_icon_rect.center().y),
125                        pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
126                        pos2(small_icon_rect.right(), small_icon_rect.top()),
127                    ],
128                    visuals.fg_stroke,
129                ));
130            }
131            if let Some(galley) = galley {
132                let text_pos = pos2(
133                    rect.min.x + icon_width + icon_spacing,
134                    rect.center().y - 0.5 * galley.size().y,
135                );
136                ui.painter().galley(text_pos, galley, visuals.text_color());
137            }
138        }
139
140        response
141    }
142}