Skip to main content

egui_components/
checkbox.rs

1//! `Checkbox` widget.
2
3use egui::{
4    pos2, vec2, Color32, FontId, Response, Sense, Stroke, Ui, Widget, WidgetText,
5};
6use egui_components_theme::Theme;
7
8pub struct Checkbox<'a> {
9    checked: &'a mut bool,
10    label: Option<WidgetText>,
11    disabled: bool,
12}
13
14impl<'a> Checkbox<'a> {
15    pub fn new(checked: &'a mut bool, label: impl Into<WidgetText>) -> Self {
16        Self {
17            checked,
18            label: Some(label.into()),
19            disabled: false,
20        }
21    }
22
23    pub fn without_label(checked: &'a mut bool) -> Self {
24        Self { checked, label: None, disabled: false }
25    }
26
27    pub fn disabled(mut self, d: bool) -> Self {
28        self.disabled = d;
29        self
30    }
31}
32
33impl<'a> Widget for Checkbox<'a> {
34    fn ui(self, ui: &mut Ui) -> Response {
35        let theme = Theme::get(ui.ctx());
36        let m = theme.metrics;
37        let c = theme.colors;
38        let box_size = m.checkbox_size;
39        let label_font = FontId::proportional(m.font_size_md);
40
41        let galley = self.label.as_ref().map(|t| {
42            t.clone().into_galley(
43                ui,
44                Some(egui::TextWrapMode::Extend),
45                f32::INFINITY,
46                label_font,
47            )
48        });
49        let label_w = galley.as_ref().map(|g| g.size().x).unwrap_or(0.0);
50        let label_h = galley.as_ref().map(|g| g.size().y).unwrap_or(0.0);
51        let gap = if galley.is_some() { 8.0 } else { 0.0 };
52
53        let total = vec2(box_size + gap + label_w, box_size.max(label_h));
54        let sense = if self.disabled { Sense::hover() } else { Sense::click() };
55        let (rect, mut response) = ui.allocate_exact_size(total, sense);
56
57        if response.clicked() && !self.disabled {
58            *self.checked = !*self.checked;
59            response.mark_changed();
60        }
61
62        if ui.is_rect_visible(rect) {
63            let box_rect = egui::Rect::from_min_size(
64                pos2(rect.left(), rect.center().y - box_size * 0.5),
65                vec2(box_size, box_size),
66            );
67            let painter = ui.painter();
68            let radius = theme.corner_sm();
69
70            let (fill, border) = if *self.checked {
71                (c.primary_background, c.primary_background)
72            } else if response.hovered() && !self.disabled {
73                (c.background, c.foreground)
74            } else {
75                (c.background, c.border)
76            };
77
78            let fill = if self.disabled { fade(fill) } else { fill };
79            let border = if self.disabled { fade(border) } else { border };
80
81            painter.rect(
82                box_rect,
83                radius,
84                fill,
85                Stroke::new(1.5, border),
86                egui::StrokeKind::Inside,
87            );
88
89            if *self.checked {
90                // Draw a check mark
91                let check_color = if self.disabled { fade(c.primary_foreground) } else { c.primary_foreground };
92                let stroke = Stroke::new(2.0, check_color);
93                let inset = box_size * 0.22;
94                let l = box_rect.left() + inset;
95                let r = box_rect.right() - inset;
96                let t = box_rect.top() + inset;
97                let b = box_rect.bottom() - inset;
98                let mid_x = l + (r - l) * 0.38;
99                let mid_y = b - (b - t) * 0.15;
100                let start = pos2(l, t + (b - t) * 0.55);
101                let mid = pos2(mid_x, mid_y);
102                let end = pos2(r, t + (b - t) * 0.15);
103                painter.line_segment([start, mid], stroke);
104                painter.line_segment([mid, end], stroke);
105            }
106
107            if response.has_focus() {
108                painter.rect_stroke(
109                    box_rect.expand(2.5),
110                    theme.corner(),
111                    theme.focus_ring(),
112                    egui::StrokeKind::Outside,
113                );
114            }
115
116            if let Some(galley) = galley {
117                let text_color = if self.disabled { fade(c.foreground) } else { c.foreground };
118                let pos = pos2(
119                    box_rect.right() + gap,
120                    rect.center().y - galley.size().y * 0.5,
121                );
122                painter.galley_with_override_text_color(pos, galley, text_color);
123            }
124
125            if !self.disabled && response.hovered() {
126                ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
127            }
128        }
129
130        response
131    }
132}
133
134fn fade(c: Color32) -> Color32 {
135    egui_components_theme::mix(c, Color32::from_rgba_unmultiplied(0, 0, 0, 0), 0.5)
136}