Skip to main content

elegance/
checkbox.rs

1//! A checkbox with a sky accent.
2
3use egui::{
4    vec2, Color32, CornerRadius, FontSelection, Response, Sense, Stroke, Ui, Vec2, Widget,
5    WidgetInfo, WidgetText, WidgetType,
6};
7
8use crate::theme::Theme;
9
10/// A styled checkbox.
11///
12/// ```no_run
13/// # use elegance::Checkbox;
14/// # egui::__run_test_ui(|ui| {
15/// let mut enabled = false;
16/// ui.add(Checkbox::new(&mut enabled, "Enable feature"));
17/// # });
18/// ```
19#[must_use = "Add this widget with `ui.add(...)`."]
20pub struct Checkbox<'a> {
21    checked: &'a mut bool,
22    label: WidgetText,
23}
24
25impl<'a> std::fmt::Debug for Checkbox<'a> {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.debug_struct("Checkbox")
28            .field("checked", &*self.checked)
29            .field("label", &self.label.text())
30            .finish()
31    }
32}
33
34impl<'a> Checkbox<'a> {
35    /// Create a checkbox bound to `checked` with the given label.
36    pub fn new(checked: &'a mut bool, label: impl Into<WidgetText>) -> Self {
37        Self {
38            checked,
39            label: label.into(),
40        }
41    }
42}
43
44impl<'a> Widget for Checkbox<'a> {
45    fn ui(self, ui: &mut Ui) -> Response {
46        let theme = Theme::current(ui.ctx());
47        let p = &theme.palette;
48        let t = &theme.typography;
49
50        let box_size = 14.0;
51        let gap = 6.0;
52
53        let galley = egui::WidgetText::from(
54            egui::RichText::new(self.label.text())
55                .color(p.text)
56                .size(t.body),
57        )
58        .into_galley(
59            ui,
60            Some(egui::TextWrapMode::Extend),
61            ui.available_width(),
62            FontSelection::FontId(egui::FontId::proportional(t.body)),
63        );
64
65        let text_size = galley.size();
66        let desired = vec2(box_size + gap + text_size.x, box_size.max(text_size.y));
67        let (rect, mut response) = ui.allocate_exact_size(desired, Sense::click());
68
69        if response.clicked() {
70            *self.checked = !*self.checked;
71            response.mark_changed();
72        }
73
74        if ui.is_rect_visible(rect) {
75            let checked = *self.checked;
76            let is_hovered = response.hovered();
77
78            let box_rect = egui::Rect::from_min_size(
79                egui::pos2(rect.min.x, rect.center().y - box_size * 0.5),
80                Vec2::splat(box_size),
81            );
82
83            let (fill, stroke) = if checked {
84                (p.sky, Stroke::new(1.0, p.sky))
85            } else if is_hovered {
86                (p.input_bg, Stroke::new(1.0, p.sky))
87            } else {
88                (p.input_bg, Stroke::new(1.0, p.border))
89            };
90
91            ui.painter().rect(
92                box_rect,
93                CornerRadius::same(3),
94                fill,
95                stroke,
96                egui::StrokeKind::Inside,
97            );
98
99            if checked {
100                // Draw a crisp checkmark.
101                let m = box_rect.min;
102                let s = box_size;
103                let a = egui::pos2(m.x + s * 0.22, m.y + s * 0.52);
104                let b = egui::pos2(m.x + s * 0.44, m.y + s * 0.72);
105                let c = egui::pos2(m.x + s * 0.78, m.y + s * 0.30);
106                let stroke = Stroke::new(1.6, Color32::WHITE);
107                ui.painter().line_segment([a, b], stroke);
108                ui.painter().line_segment([b, c], stroke);
109            }
110
111            let text_pos = egui::pos2(box_rect.max.x + gap, rect.center().y - text_size.y * 0.5);
112            ui.painter().galley(text_pos, galley, p.text);
113        }
114
115        response.widget_info(|| WidgetInfo::labeled(WidgetType::Checkbox, true, self.label.text()));
116        response
117    }
118}