1use crate::get_global_color;
2use egui::{self, Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget};
3
4pub struct MaterialCheckbox<'a> {
35 checked: &'a mut bool,
37 text: String,
39 indeterminate: bool,
41 enabled: bool,
43 is_error: bool,
45 check_color: Option<Color32>,
47 fill_color: Option<Color32>,
49 border_width: f32,
51}
52
53impl<'a> MaterialCheckbox<'a> {
54 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 pub fn indeterminate(mut self, indeterminate: bool) -> Self {
83 self.indeterminate = indeterminate;
84 self
85 }
86
87 pub fn enabled(mut self, enabled: bool) -> Self {
94 self.enabled = enabled;
95 self
96 }
97
98 pub fn is_error(mut self, is_error: bool) -> Self {
106 self.is_error = is_error;
107 self
108 }
109
110 pub fn check_color(mut self, color: Color32) -> Self {
117 self.check_color = Some(color);
118 self
119 }
120
121 pub fn fill_color(mut self, color: Color32) -> Self {
128 self.fill_color = Some(color);
129 self
130 }
131
132 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 checkbox_size = 18.0;
145 let spacing = 4.0;
146
147 let text_width = if !self.text.is_empty() {
149 let font_id = ui.style().text_styles.get(&egui::TextStyle::Body)
150 .cloned()
151 .unwrap_or_else(|| egui::FontId::default());
152 let galley = ui.painter().layout_no_wrap(self.text.clone(), font_id, egui::Color32::WHITE);
153 galley.size().x
154 } else {
155 0.0
156 };
157
158 let desired_width = checkbox_size + spacing + text_width;
159 let desired_size = Vec2::new(desired_width, 24.0);
160
161 let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
162
163 if response.clicked() && self.enabled {
164 if self.indeterminate {
165 *self.checked = true;
166 } else {
167 *self.checked = !*self.checked;
168 }
169 response.mark_changed();
170 }
171
172 let _visuals = ui.style().interact(&response);
173 let checkbox_rect = Rect::from_min_size(
174 Pos2::new(rect.min.x, rect.center().y - checkbox_size / 2.0),
175 Vec2::splat(checkbox_size),
176 );
177
178 let primary_color = self.fill_color.unwrap_or_else(|| get_global_color("primary"));
180 let error_color = get_global_color("error");
181 let on_error = get_global_color("onError");
182 let on_surface = get_global_color("onSurface");
183 let on_surface_variant = get_global_color("onSurfaceVariant");
184 let _surface_variant = get_global_color("surfaceVariant");
185 let _outline = get_global_color("outline");
186 let on_primary = self.check_color.unwrap_or_else(|| get_global_color("onPrimary"));
187
188 let (bg_color, border_color, check_color, border_width) = if !self.enabled {
190 let disabled_color = on_surface.gamma_multiply(0.38);
192 if *self.checked || self.indeterminate {
193 (disabled_color, Color32::TRANSPARENT, on_surface.gamma_multiply(0.38), 0.0)
194 } else {
195 (Color32::TRANSPARENT, disabled_color, disabled_color, self.border_width)
196 }
197 } else if self.is_error {
198 if *self.checked || self.indeterminate {
200 (error_color, Color32::TRANSPARENT, on_error, 0.0)
201 } else if response.hovered() {
202 (Color32::TRANSPARENT, error_color, on_surface, self.border_width)
203 } else {
204 (Color32::TRANSPARENT, error_color, on_surface, self.border_width)
205 }
206 } else if *self.checked || self.indeterminate {
207 (primary_color, Color32::TRANSPARENT, on_primary, 0.0)
209 } else if response.hovered() {
210 (Color32::TRANSPARENT, on_surface, on_surface, self.border_width)
212 } else {
213 (Color32::TRANSPARENT, on_surface_variant, on_surface, self.border_width)
215 };
216
217 ui.painter().rect_filled(checkbox_rect, 2.0, bg_color);
219
220 if border_width > 0.0 {
222 ui.painter().rect_stroke(
223 checkbox_rect,
224 2.0,
225 Stroke::new(border_width, border_color),
226 egui::epaint::StrokeKind::Outside,
227 );
228 }
229
230 if *self.checked && !self.indeterminate {
232 let center = checkbox_rect.center();
234 let checkmark_size = checkbox_size * 0.6;
235
236 let start = Pos2::new(center.x - checkmark_size * 0.3, center.y);
237 let middle = Pos2::new(
238 center.x - checkmark_size * 0.1,
239 center.y + checkmark_size * 0.2,
240 );
241 let end = Pos2::new(
242 center.x + checkmark_size * 0.3,
243 center.y - checkmark_size * 0.2,
244 );
245
246 ui.painter()
247 .line_segment([start, middle], Stroke::new(2.0, check_color));
248 ui.painter()
249 .line_segment([middle, end], Stroke::new(2.0, check_color));
250 } else if self.indeterminate {
251 let center = checkbox_rect.center();
253 let line_width = checkbox_size * 0.5;
254 let start = Pos2::new(center.x - line_width / 2.0, center.y);
255 let end = Pos2::new(center.x + line_width / 2.0, center.y);
256
257 ui.painter()
258 .line_segment([start, end], Stroke::new(2.0, check_color));
259 }
260
261 if !self.text.is_empty() {
263 let text_pos = Pos2::new(checkbox_rect.max.x + 4.0, rect.center().y);
264
265 let text_color = if self.enabled {
266 on_surface
267 } else {
268 on_surface.gamma_multiply(0.38)
269 };
270
271 ui.painter().text(
272 text_pos,
273 egui::Align2::LEFT_CENTER,
274 &self.text,
275 egui::FontId::default(),
276 text_color,
277 );
278 }
279
280 if self.enabled {
282 let overlay_rect = Rect::from_center_size(checkbox_rect.center(), Vec2::splat(40.0));
283 let overlay_color = if response.is_pointer_button_down_on() {
284 if self.is_error {
286 Color32::from_rgba_premultiplied(
287 error_color.r(),
288 error_color.g(),
289 error_color.b(),
290 25,
291 )
292 } else if *self.checked || self.indeterminate {
293 Color32::from_rgba_premultiplied(
294 primary_color.r(),
295 primary_color.g(),
296 primary_color.b(),
297 25,
298 )
299 } else {
300 Color32::from_rgba_premultiplied(
301 on_surface.r(),
302 on_surface.g(),
303 on_surface.b(),
304 25,
305 )
306 }
307 } else if response.hovered() {
308 if self.is_error {
310 Color32::from_rgba_premultiplied(
311 error_color.r(),
312 error_color.g(),
313 error_color.b(),
314 20,
315 )
316 } else if *self.checked || self.indeterminate {
317 Color32::from_rgba_premultiplied(
318 primary_color.r(),
319 primary_color.g(),
320 primary_color.b(),
321 20,
322 )
323 } else {
324 Color32::from_rgba_premultiplied(
325 on_surface.r(),
326 on_surface.g(),
327 on_surface.b(),
328 20,
329 )
330 }
331 } else if response.has_focus() {
332 if self.is_error {
334 Color32::from_rgba_premultiplied(
335 error_color.r(),
336 error_color.g(),
337 error_color.b(),
338 25,
339 )
340 } else if *self.checked || self.indeterminate {
341 Color32::from_rgba_premultiplied(
342 primary_color.r(),
343 primary_color.g(),
344 primary_color.b(),
345 25,
346 )
347 } else {
348 Color32::from_rgba_premultiplied(
349 on_surface.r(),
350 on_surface.g(),
351 on_surface.b(),
352 25,
353 )
354 }
355 } else {
356 Color32::TRANSPARENT
357 };
358
359 if overlay_color != Color32::TRANSPARENT {
360 ui.painter().circle_filled(
361 overlay_rect.center(),
362 overlay_rect.width() / 2.0,
363 overlay_color,
364 );
365 }
366 }
367
368 response
369 }
370}
371
372pub fn checkbox(checked: &mut bool, text: impl Into<String>) -> MaterialCheckbox<'_> {
373 MaterialCheckbox::new(checked, text)
374}