Skip to main content

egui_material3/
checkbox.rs

1use crate::get_global_color;
2use eframe::egui::{self, Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget};
3
4/// Material Design checkbox component following Material Design 3 specifications
5///
6/// Provides a checkbox with three states: checked, unchecked, and indeterminate.
7/// Follows Material Design guidelines for colors, sizing, and interaction states.
8///
9/// ## Usage Examples
10/// ```rust
11/// # egui::__run_test_ui(|ui| {
12/// let mut checked = false;
13///
14/// // Basic checkbox
15/// ui.add(MaterialCheckbox::new(&mut checked, "Accept terms"));
16///
17/// // Checkbox with indeterminate state
18/// let mut partial_checked = false;
19/// ui.add(MaterialCheckbox::new(&mut partial_checked, "Select all")
20///     .indeterminate(true));
21///
22/// // Disabled checkbox
23/// let mut disabled_checked = false;  
24/// ui.add(MaterialCheckbox::new(&mut disabled_checked, "Disabled option")
25///     .enabled(false));
26/// # });
27/// ```
28///
29/// ## Material Design Spec
30/// - Size: 18x18dp checkbox with 40x40dp touch target
31/// - Colors: Primary color when checked, outline when unchecked
32/// - Animation: 150ms cubic-bezier transition
33/// - States: Normal, hover, focus, pressed, disabled, error
34pub struct MaterialCheckbox<'a> {
35    /// Mutable reference to the checked state
36    checked: &'a mut bool,
37    /// Text label displayed next to the checkbox
38    text: String,
39    /// Whether the checkbox is in indeterminate state (partially checked)
40    indeterminate: bool,
41    /// Whether the checkbox is interactive (enabled/disabled)
42    enabled: bool,
43    /// Whether to show error state styling
44    is_error: bool,
45    /// Custom check mark color (overrides theme)
46    check_color: Option<Color32>,
47    /// Custom fill color when checked (overrides theme)
48    fill_color: Option<Color32>,
49    /// Custom border width (default: 2.0)
50    border_width: f32,
51}
52
53impl<'a> MaterialCheckbox<'a> {
54    /// Create a new Material Design checkbox
55    ///
56    /// ## Parameters
57    /// - `checked`: Mutable reference to boolean state
58    /// - `text`: Label text displayed next to checkbox
59    ///
60    /// ## Returns
61    /// A new MaterialCheckbox instance with default settings
62    pub fn new(checked: &'a mut bool, text: impl Into<String>) -> Self {
63        Self {
64            checked,
65            text: text.into(),
66            indeterminate: false,
67            enabled: true,
68            is_error: false,
69            check_color: None,
70            fill_color: None,
71            border_width: 2.0,
72        }
73    }
74
75    /// Set the indeterminate state of the checkbox
76    ///
77    /// Indeterminate checkboxes are used when the checkbox represents
78    /// a collection of items where some, but not all, are selected.
79    ///
80    /// ## Parameters  
81    /// - `indeterminate`: True for indeterminate state, false for normal
82    pub fn indeterminate(mut self, indeterminate: bool) -> Self {
83        self.indeterminate = indeterminate;
84        self
85    }
86
87    /// Set whether the checkbox is enabled or disabled
88    ///
89    /// Disabled checkboxes cannot be interacted with and are visually dimmed.
90    ///
91    /// ## Parameters
92    /// - `enabled`: True for interactive, false for disabled
93    pub fn enabled(mut self, enabled: bool) -> Self {
94        self.enabled = enabled;
95        self
96    }
97
98    /// Set whether the checkbox should display in error state
99    ///
100    /// Error state checkboxes use error color from the theme to indicate
101    /// validation failure or invalid selection.
102    ///
103    /// ## Parameters
104    /// - `is_error`: True for error state styling
105    pub fn is_error(mut self, is_error: bool) -> Self {
106        self.is_error = is_error;
107        self
108    }
109
110    /// Set custom check mark color
111    ///
112    /// Overrides the default check mark color from the theme.
113    ///
114    /// ## Parameters
115    /// - `color`: Custom color for the check mark
116    pub fn check_color(mut self, color: Color32) -> Self {
117        self.check_color = Some(color);
118        self
119    }
120
121    /// Set custom fill color when checked
122    ///
123    /// Overrides the default fill color from the theme.
124    ///
125    /// ## Parameters
126    /// - `color`: Custom fill color when checkbox is checked
127    pub fn fill_color(mut self, color: Color32) -> Self {
128        self.fill_color = Some(color);
129        self
130    }
131
132    /// Set custom border width
133    ///
134    /// ## Parameters
135    /// - `width`: Border width in pixels (default: 2.0)
136    pub fn border_width(mut self, width: f32) -> Self {
137        self.border_width = width;
138        self
139    }
140}
141
142impl<'a> Widget for MaterialCheckbox<'a> {
143    fn ui(self, ui: &mut Ui) -> Response {
144        let desired_size = Vec2::new(ui.available_width().min(300.0), 24.0);
145
146        let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
147
148        if response.clicked() && self.enabled {
149            if self.indeterminate {
150                *self.checked = true;
151            } else {
152                *self.checked = !*self.checked;
153            }
154            response.mark_changed();
155        }
156
157        let visuals = ui.style().interact(&response);
158        let checkbox_size = 18.0;
159        let checkbox_rect = Rect::from_min_size(
160            Pos2::new(rect.min.x, rect.center().y - checkbox_size / 2.0),
161            Vec2::splat(checkbox_size),
162        );
163
164        // Material Design colors
165        let primary_color = self.fill_color.unwrap_or_else(|| get_global_color("primary"));
166        let error_color = get_global_color("error");
167        let on_error = get_global_color("onError");
168        let on_surface = get_global_color("onSurface");
169        let on_surface_variant = get_global_color("onSurfaceVariant");
170        let surface_variant = get_global_color("surfaceVariant");
171        let outline = get_global_color("outline");
172        let on_primary = self.check_color.unwrap_or_else(|| get_global_color("onPrimary"));
173
174        // Determine colors based on state
175        let (bg_color, border_color, check_color, border_width) = if !self.enabled {
176            // Material Design disabled state: onSurface with 38% opacity
177            let disabled_color = on_surface.gamma_multiply(0.38);
178            if *self.checked || self.indeterminate {
179                (disabled_color, Color32::TRANSPARENT, on_surface.gamma_multiply(0.38), 0.0)
180            } else {
181                (Color32::TRANSPARENT, disabled_color, disabled_color, self.border_width)
182            }
183        } else if self.is_error {
184            // Error state styling
185            if *self.checked || self.indeterminate {
186                (error_color, Color32::TRANSPARENT, on_error, 0.0)
187            } else if response.hovered() {
188                (Color32::TRANSPARENT, error_color, on_surface, self.border_width)
189            } else {
190                (Color32::TRANSPARENT, error_color, on_surface, self.border_width)
191            }
192        } else if *self.checked || self.indeterminate {
193            // Checked/indeterminate state
194            (primary_color, Color32::TRANSPARENT, on_primary, 0.0)
195        } else if response.hovered() {
196            // Hover state for unchecked
197            (Color32::TRANSPARENT, on_surface, on_surface, self.border_width)
198        } else {
199            // Default unchecked state
200            (Color32::TRANSPARENT, on_surface_variant, on_surface, self.border_width)
201        };
202
203        // Draw checkbox background
204        ui.painter().rect_filled(checkbox_rect, 2.0, bg_color);
205
206        // Draw checkbox border (only for unchecked or when needed)
207        if border_width > 0.0 {
208            ui.painter().rect_stroke(
209                checkbox_rect,
210                2.0,
211                Stroke::new(border_width, border_color),
212                egui::epaint::StrokeKind::Outside,
213            );
214        }
215
216        // Draw checkmark or indeterminate mark
217        if *self.checked && !self.indeterminate {
218            // Draw checkmark
219            let center = checkbox_rect.center();
220            let checkmark_size = checkbox_size * 0.6;
221
222            let start = Pos2::new(center.x - checkmark_size * 0.3, center.y);
223            let middle = Pos2::new(
224                center.x - checkmark_size * 0.1,
225                center.y + checkmark_size * 0.2,
226            );
227            let end = Pos2::new(
228                center.x + checkmark_size * 0.3,
229                center.y - checkmark_size * 0.2,
230            );
231
232            ui.painter()
233                .line_segment([start, middle], Stroke::new(2.0, check_color));
234            ui.painter()
235                .line_segment([middle, end], Stroke::new(2.0, check_color));
236        } else if self.indeterminate {
237            // Draw indeterminate mark (horizontal line)
238            let center = checkbox_rect.center();
239            let line_width = checkbox_size * 0.5;
240            let start = Pos2::new(center.x - line_width / 2.0, center.y);
241            let end = Pos2::new(center.x + line_width / 2.0, center.y);
242
243            ui.painter()
244                .line_segment([start, end], Stroke::new(2.0, check_color));
245        }
246
247        // Draw label text
248        if !self.text.is_empty() {
249            let text_pos = Pos2::new(checkbox_rect.max.x + 8.0, rect.center().y);
250
251            let text_color = if self.enabled {
252                on_surface
253            } else {
254                on_surface.gamma_multiply(0.38)
255            };
256
257            ui.painter().text(
258                text_pos,
259                egui::Align2::LEFT_CENTER,
260                &self.text,
261                egui::FontId::default(),
262                text_color,
263            );
264        }
265
266        // Add state overlay effect (hover/focus/pressed)
267        if self.enabled {
268            let overlay_rect = Rect::from_center_size(checkbox_rect.center(), Vec2::splat(40.0));
269            let overlay_color = if response.is_pointer_button_down_on() {
270                // Pressed state: 10% opacity
271                if self.is_error {
272                    Color32::from_rgba_premultiplied(
273                        error_color.r(),
274                        error_color.g(),
275                        error_color.b(),
276                        25,
277                    )
278                } else if *self.checked || self.indeterminate {
279                    Color32::from_rgba_premultiplied(
280                        primary_color.r(),
281                        primary_color.g(),
282                        primary_color.b(),
283                        25,
284                    )
285                } else {
286                    Color32::from_rgba_premultiplied(
287                        on_surface.r(),
288                        on_surface.g(),
289                        on_surface.b(),
290                        25,
291                    )
292                }
293            } else if response.hovered() {
294                // Hover state: 8% opacity
295                if self.is_error {
296                    Color32::from_rgba_premultiplied(
297                        error_color.r(),
298                        error_color.g(),
299                        error_color.b(),
300                        20,
301                    )
302                } else if *self.checked || self.indeterminate {
303                    Color32::from_rgba_premultiplied(
304                        primary_color.r(),
305                        primary_color.g(),
306                        primary_color.b(),
307                        20,
308                    )
309                } else {
310                    Color32::from_rgba_premultiplied(
311                        on_surface.r(),
312                        on_surface.g(),
313                        on_surface.b(),
314                        20,
315                    )
316                }
317            } else if response.has_focus() {
318                // Focus state: 10% opacity
319                if self.is_error {
320                    Color32::from_rgba_premultiplied(
321                        error_color.r(),
322                        error_color.g(),
323                        error_color.b(),
324                        25,
325                    )
326                } else if *self.checked || self.indeterminate {
327                    Color32::from_rgba_premultiplied(
328                        primary_color.r(),
329                        primary_color.g(),
330                        primary_color.b(),
331                        25,
332                    )
333                } else {
334                    Color32::from_rgba_premultiplied(
335                        on_surface.r(),
336                        on_surface.g(),
337                        on_surface.b(),
338                        25,
339                    )
340                }
341            } else {
342                Color32::TRANSPARENT
343            };
344
345            if overlay_color != Color32::TRANSPARENT {
346                ui.painter().circle_filled(
347                    overlay_rect.center(),
348                    overlay_rect.width() / 2.0,
349                    overlay_color,
350                );
351            }
352        }
353
354        response
355    }
356}
357
358pub fn checkbox(checked: &mut bool, text: impl Into<String>) -> MaterialCheckbox<'_> {
359    MaterialCheckbox::new(checked, text)
360}