1use crate::get_global_color;
2use eframe::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 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 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 let (bg_color, border_color, check_color, border_width) = if !self.enabled {
176 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 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 (primary_color, Color32::TRANSPARENT, on_primary, 0.0)
195 } else if response.hovered() {
196 (Color32::TRANSPARENT, on_surface, on_surface, self.border_width)
198 } else {
199 (Color32::TRANSPARENT, on_surface_variant, on_surface, self.border_width)
201 };
202
203 ui.painter().rect_filled(checkbox_rect, 2.0, bg_color);
205
206 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 if *self.checked && !self.indeterminate {
218 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 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 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 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 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 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 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}