egui_material3/
checkbox.rs

1use eframe::egui::{self, Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget};
2use crate::get_global_color;
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
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}
44
45impl<'a> MaterialCheckbox<'a> {
46    /// Create a new Material Design checkbox
47    /// 
48    /// ## Parameters
49    /// - `checked`: Mutable reference to boolean state
50    /// - `text`: Label text displayed next to checkbox
51    /// 
52    /// ## Returns
53    /// A new MaterialCheckbox instance with default settings
54    pub fn new(checked: &'a mut bool, text: impl Into<String>) -> Self {
55        Self {
56            checked,
57            text: text.into(),
58            indeterminate: false,
59            enabled: true,
60        }
61    }
62
63    /// Set the indeterminate state of the checkbox
64    /// 
65    /// Indeterminate checkboxes are used when the checkbox represents
66    /// a collection of items where some, but not all, are selected.
67    /// 
68    /// ## Parameters  
69    /// - `indeterminate`: True for indeterminate state, false for normal
70    pub fn indeterminate(mut self, indeterminate: bool) -> Self {
71        self.indeterminate = indeterminate;
72        self
73    }
74
75    /// Set whether the checkbox is enabled or disabled
76    /// 
77    /// Disabled checkboxes cannot be interacted with and are visually dimmed.
78    /// 
79    /// ## Parameters
80    /// - `enabled`: True for interactive, false for disabled
81    pub fn enabled(mut self, enabled: bool) -> Self {
82        self.enabled = enabled;
83        self
84    }
85}
86
87impl<'a> Widget for MaterialCheckbox<'a> {
88    fn ui(self, ui: &mut Ui) -> Response {
89        let desired_size = Vec2::new(
90            ui.available_width().min(300.0),
91            24.0,
92        );
93
94        let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
95
96        if response.clicked() && self.enabled {
97            if self.indeterminate {
98                *self.checked = true;
99            } else {
100                *self.checked = !*self.checked;
101            }
102            response.mark_changed();
103        }
104
105        let _visuals = ui.style().interact(&response);
106        let checkbox_size = 18.0;
107        let checkbox_rect = Rect::from_min_size(
108            Pos2::new(rect.min.x, rect.center().y - checkbox_size / 2.0),
109            Vec2::splat(checkbox_size),
110        );
111
112        // Material Design colors
113        let primary_color = get_global_color("primary");
114        let on_surface = get_global_color("onSurface");
115        let surface_variant = get_global_color("surfaceVariant");
116        let outline = get_global_color("outline");
117
118        let (bg_color, border_color, check_color) = if !self.enabled {
119            // Material Design disabled state: onSurface with 38% opacity
120            let disabled_color = on_surface.gamma_multiply(0.38);
121            (
122                Color32::TRANSPARENT,
123                disabled_color,
124                disabled_color,
125            )
126        } else if *self.checked || self.indeterminate {
127            (primary_color, primary_color, get_global_color("onPrimary"))
128        } else if response.hovered() {
129            (surface_variant, outline, on_surface)
130        } else {
131            (Color32::TRANSPARENT, outline, on_surface)
132        };
133
134        // Draw checkbox background
135        ui.painter().rect_filled(
136            checkbox_rect,
137            2.0,
138            bg_color,
139        );
140
141        // Draw checkbox border
142        ui.painter().rect_stroke(
143            checkbox_rect,
144            2.0,
145            Stroke::new(2.0, border_color),
146            egui::epaint::StrokeKind::Outside,
147        );
148
149        // Draw checkmark or indeterminate mark
150        if *self.checked && !self.indeterminate {
151            // Draw checkmark
152            let center = checkbox_rect.center();
153            let checkmark_size = checkbox_size * 0.6;
154            
155            let start = Pos2::new(
156                center.x - checkmark_size * 0.3,
157                center.y,
158            );
159            let middle = Pos2::new(
160                center.x - checkmark_size * 0.1,
161                center.y + checkmark_size * 0.2,
162            );
163            let end = Pos2::new(
164                center.x + checkmark_size * 0.3,
165                center.y - checkmark_size * 0.2,
166            );
167
168            ui.painter().line_segment([start, middle], Stroke::new(2.0, check_color));
169            ui.painter().line_segment([middle, end], Stroke::new(2.0, check_color));
170        } else if self.indeterminate {
171            // Draw indeterminate mark (horizontal line)
172            let center = checkbox_rect.center();
173            let line_width = checkbox_size * 0.5;
174            let start = Pos2::new(center.x - line_width / 2.0, center.y);
175            let end = Pos2::new(center.x + line_width / 2.0, center.y);
176            
177            ui.painter().line_segment([start, end], Stroke::new(2.0, check_color));
178        }
179
180        // Draw label text
181        if !self.text.is_empty() {
182            let text_pos = Pos2::new(
183                checkbox_rect.max.x + 8.0,
184                rect.center().y,
185            );
186            
187            let text_color = if self.enabled { 
188                on_surface 
189            } else { 
190                on_surface.gamma_multiply(0.38)
191            };
192
193            ui.painter().text(
194                text_pos,
195                egui::Align2::LEFT_CENTER,
196                &self.text,
197                egui::FontId::default(),
198                text_color,
199            );
200        }
201
202        // Add ripple effect on hover/click
203        if response.hovered() && self.enabled {
204            let ripple_rect = Rect::from_center_size(checkbox_rect.center(), Vec2::splat(40.0));
205            let ripple_color = if *self.checked || self.indeterminate {
206                Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 20)
207            } else {
208                Color32::from_rgba_premultiplied(on_surface.r(), on_surface.g(), on_surface.b(), 20)
209            };
210            
211            ui.painter().circle_filled(
212                ripple_rect.center(),
213                ripple_rect.width() / 2.0,
214                ripple_color,
215            );
216        }
217
218        response
219    }
220}
221
222pub fn checkbox(checked: &mut bool, text: impl Into<String>) -> MaterialCheckbox<'_> {
223    MaterialCheckbox::new(checked, text)
224}