egui_components/
checkbox.rs1use 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 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}