1use crate::core::alignment;
34use crate::core::layout;
35use crate::core::mouse;
36use crate::core::renderer;
37use crate::core::text;
38use crate::core::theme::palette;
39use crate::core::touch;
40use crate::core::widget;
41use crate::core::widget::tree::{self, Tree};
42use crate::core::window;
43use crate::core::{
44 Background, Border, Clipboard, Color, Element, Event, Layout, Length,
45 Pixels, Rectangle, Shell, Size, Theme, Widget,
46};
47
48#[allow(missing_debug_implementations)]
81pub struct Checkbox<
82 'a,
83 Message,
84 Theme = crate::Theme,
85 Renderer = crate::Renderer,
86> where
87 Renderer: text::Renderer,
88 Theme: Catalog,
89{
90 is_checked: bool,
91 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
92 label: String,
93 width: Length,
94 size: f32,
95 spacing: f32,
96 text_size: Option<Pixels>,
97 text_line_height: text::LineHeight,
98 text_shaping: text::Shaping,
99 text_wrapping: text::Wrapping,
100 font: Option<Renderer::Font>,
101 icon: Icon<Renderer::Font>,
102 class: Theme::Class<'a>,
103 last_status: Option<Status>,
104}
105
106impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer>
107where
108 Renderer: text::Renderer,
109 Theme: Catalog,
110{
111 const DEFAULT_SIZE: f32 = 16.0;
113
114 const DEFAULT_SPACING: f32 = 8.0;
116
117 pub fn new(label: impl Into<String>, is_checked: bool) -> Self {
123 Checkbox {
124 is_checked,
125 on_toggle: None,
126 label: label.into(),
127 width: Length::Shrink,
128 size: Self::DEFAULT_SIZE,
129 spacing: Self::DEFAULT_SPACING,
130 text_size: None,
131 text_line_height: text::LineHeight::default(),
132 text_shaping: text::Shaping::default(),
133 text_wrapping: text::Wrapping::default(),
134 font: None,
135 icon: Icon {
136 font: Renderer::ICON_FONT,
137 code_point: Renderer::CHECKMARK_ICON,
138 size: None,
139 line_height: text::LineHeight::default(),
140 shaping: text::Shaping::Basic,
141 },
142 class: Theme::default(),
143 last_status: None,
144 }
145 }
146
147 pub fn on_toggle<F>(mut self, f: F) -> Self
153 where
154 F: 'a + Fn(bool) -> Message,
155 {
156 self.on_toggle = Some(Box::new(f));
157 self
158 }
159
160 pub fn on_toggle_maybe<F>(mut self, f: Option<F>) -> Self
165 where
166 F: Fn(bool) -> Message + 'a,
167 {
168 self.on_toggle = f.map(|f| Box::new(f) as _);
169 self
170 }
171
172 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
174 self.size = size.into().0;
175 self
176 }
177
178 pub fn width(mut self, width: impl Into<Length>) -> Self {
180 self.width = width.into();
181 self
182 }
183
184 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
186 self.spacing = spacing.into().0;
187 self
188 }
189
190 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
192 self.text_size = Some(text_size.into());
193 self
194 }
195
196 pub fn text_line_height(
198 mut self,
199 line_height: impl Into<text::LineHeight>,
200 ) -> Self {
201 self.text_line_height = line_height.into();
202 self
203 }
204
205 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
207 self.text_shaping = shaping;
208 self
209 }
210
211 pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
213 self.text_wrapping = wrapping;
214 self
215 }
216
217 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
221 self.font = Some(font.into());
222 self
223 }
224
225 pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
227 self.icon = icon;
228 self
229 }
230
231 #[must_use]
233 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
234 where
235 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
236 {
237 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
238 self
239 }
240
241 #[cfg(feature = "advanced")]
243 #[must_use]
244 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
245 self.class = class.into();
246 self
247 }
248}
249
250impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
251 for Checkbox<'_, Message, Theme, Renderer>
252where
253 Renderer: text::Renderer,
254 Theme: Catalog,
255{
256 fn tag(&self) -> tree::Tag {
257 tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
258 }
259
260 fn state(&self) -> tree::State {
261 tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
262 }
263
264 fn size(&self) -> Size<Length> {
265 Size {
266 width: self.width,
267 height: Length::Shrink,
268 }
269 }
270
271 fn layout(
272 &self,
273 tree: &mut Tree,
274 renderer: &Renderer,
275 limits: &layout::Limits,
276 ) -> layout::Node {
277 layout::next_to_each_other(
278 &limits.width(self.width),
279 self.spacing,
280 |_| layout::Node::new(Size::new(self.size, self.size)),
281 |limits| {
282 let state = tree
283 .state
284 .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
285
286 widget::text::layout(
287 state,
288 renderer,
289 limits,
290 self.width,
291 Length::Shrink,
292 &self.label,
293 self.text_line_height,
294 self.text_size,
295 self.font,
296 text::Alignment::Default,
297 alignment::Vertical::Top,
298 self.text_shaping,
299 self.text_wrapping,
300 )
301 },
302 )
303 }
304
305 fn update(
306 &mut self,
307 _tree: &mut Tree,
308 event: &Event,
309 layout: Layout<'_>,
310 cursor: mouse::Cursor,
311 _renderer: &Renderer,
312 _clipboard: &mut dyn Clipboard,
313 shell: &mut Shell<'_, Message>,
314 _viewport: &Rectangle,
315 ) {
316 match event {
317 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
318 | Event::Touch(touch::Event::FingerPressed { .. }) => {
319 let mouse_over = cursor.is_over(layout.bounds());
320
321 if mouse_over {
322 if let Some(on_toggle) = &self.on_toggle {
323 shell.publish((on_toggle)(!self.is_checked));
324 shell.capture_event();
325 }
326 }
327 }
328 _ => {}
329 }
330
331 let current_status = {
332 let is_mouse_over = cursor.is_over(layout.bounds());
333 let is_disabled = self.on_toggle.is_none();
334 let is_checked = self.is_checked;
335
336 if is_disabled {
337 Status::Disabled { is_checked }
338 } else if is_mouse_over {
339 Status::Hovered { is_checked }
340 } else {
341 Status::Active { is_checked }
342 }
343 };
344
345 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
346 self.last_status = Some(current_status);
347 } else if self
348 .last_status
349 .is_some_and(|status| status != current_status)
350 {
351 shell.request_redraw();
352 }
353 }
354
355 fn mouse_interaction(
356 &self,
357 _tree: &Tree,
358 layout: Layout<'_>,
359 cursor: mouse::Cursor,
360 _viewport: &Rectangle,
361 _renderer: &Renderer,
362 ) -> mouse::Interaction {
363 if cursor.is_over(layout.bounds()) && self.on_toggle.is_some() {
364 mouse::Interaction::Pointer
365 } else {
366 mouse::Interaction::default()
367 }
368 }
369
370 fn draw(
371 &self,
372 tree: &Tree,
373 renderer: &mut Renderer,
374 theme: &Theme,
375 defaults: &renderer::Style,
376 layout: Layout<'_>,
377 _cursor: mouse::Cursor,
378 viewport: &Rectangle,
379 ) {
380 let mut children = layout.children();
381
382 let style = theme.style(
383 &self.class,
384 self.last_status.unwrap_or(Status::Disabled {
385 is_checked: self.is_checked,
386 }),
387 );
388
389 {
390 let layout = children.next().unwrap();
391 let bounds = layout.bounds();
392
393 renderer.fill_quad(
394 renderer::Quad {
395 bounds,
396 border: style.border,
397 ..renderer::Quad::default()
398 },
399 style.background,
400 );
401
402 let Icon {
403 font,
404 code_point,
405 size,
406 line_height,
407 shaping,
408 } = &self.icon;
409 let size = size.unwrap_or(Pixels(bounds.height * 0.7));
410
411 if self.is_checked {
412 renderer.fill_text(
413 text::Text {
414 content: code_point.to_string(),
415 font: *font,
416 size,
417 line_height: *line_height,
418 bounds: bounds.size(),
419 align_x: text::Alignment::Center,
420 align_y: alignment::Vertical::Center,
421 shaping: *shaping,
422 wrapping: text::Wrapping::default(),
423 },
424 bounds.center(),
425 style.icon_color,
426 *viewport,
427 );
428 }
429 }
430
431 {
432 let label_layout = children.next().unwrap();
433 let state: &widget::text::State<Renderer::Paragraph> =
434 tree.state.downcast_ref();
435
436 crate::text::draw(
437 renderer,
438 defaults,
439 label_layout,
440 state.0.raw(),
441 crate::text::Style {
442 color: style.text_color,
443 },
444 viewport,
445 );
446 }
447 }
448
449 fn operate(
450 &self,
451 _state: &mut Tree,
452 layout: Layout<'_>,
453 _renderer: &Renderer,
454 operation: &mut dyn widget::Operation,
455 ) {
456 operation.text(None, layout.bounds(), &self.label);
457 }
458}
459
460impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
461 for Element<'a, Message, Theme, Renderer>
462where
463 Message: 'a,
464 Theme: 'a + Catalog,
465 Renderer: 'a + text::Renderer,
466{
467 fn from(
468 checkbox: Checkbox<'a, Message, Theme, Renderer>,
469 ) -> Element<'a, Message, Theme, Renderer> {
470 Element::new(checkbox)
471 }
472}
473
474#[derive(Debug, Clone, PartialEq)]
476pub struct Icon<Font> {
477 pub font: Font,
479 pub code_point: char,
481 pub size: Option<Pixels>,
483 pub line_height: text::LineHeight,
485 pub shaping: text::Shaping,
487}
488
489#[derive(Debug, Clone, Copy, PartialEq, Eq)]
491pub enum Status {
492 Active {
494 is_checked: bool,
496 },
497 Hovered {
499 is_checked: bool,
501 },
502 Disabled {
504 is_checked: bool,
506 },
507}
508
509#[derive(Debug, Clone, Copy, PartialEq)]
511pub struct Style {
512 pub background: Background,
514 pub icon_color: Color,
516 pub border: Border,
518 pub text_color: Option<Color>,
520}
521
522pub trait Catalog: Sized {
524 type Class<'a>;
526
527 fn default<'a>() -> Self::Class<'a>;
529
530 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
532}
533
534pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
538
539impl Catalog for Theme {
540 type Class<'a> = StyleFn<'a, Self>;
541
542 fn default<'a>() -> Self::Class<'a> {
543 Box::new(primary)
544 }
545
546 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
547 class(self, status)
548 }
549}
550
551pub fn primary(theme: &Theme, status: Status) -> Style {
553 let palette = theme.extended_palette();
554
555 match status {
556 Status::Active { is_checked } => styled(
557 palette.primary.strong.text,
558 palette.background.strongest.color,
559 palette.background.base,
560 palette.primary.base,
561 is_checked,
562 ),
563 Status::Hovered { is_checked } => styled(
564 palette.primary.strong.text,
565 palette.background.strongest.color,
566 palette.background.weak,
567 palette.primary.strong,
568 is_checked,
569 ),
570 Status::Disabled { is_checked } => styled(
571 palette.primary.strong.text,
572 palette.background.weak.color,
573 palette.background.weak,
574 palette.background.strong,
575 is_checked,
576 ),
577 }
578}
579
580pub fn secondary(theme: &Theme, status: Status) -> Style {
582 let palette = theme.extended_palette();
583
584 match status {
585 Status::Active { is_checked } => styled(
586 palette.background.base.text,
587 palette.background.strongest.color,
588 palette.background.base,
589 palette.background.strong,
590 is_checked,
591 ),
592 Status::Hovered { is_checked } => styled(
593 palette.background.base.text,
594 palette.background.strongest.color,
595 palette.background.weak,
596 palette.background.strong,
597 is_checked,
598 ),
599 Status::Disabled { is_checked } => styled(
600 palette.background.strong.color,
601 palette.background.weak.color,
602 palette.background.weak,
603 palette.background.weak,
604 is_checked,
605 ),
606 }
607}
608
609pub fn success(theme: &Theme, status: Status) -> Style {
611 let palette = theme.extended_palette();
612
613 match status {
614 Status::Active { is_checked } => styled(
615 palette.success.base.text,
616 palette.background.weak.color,
617 palette.background.base,
618 palette.success.base,
619 is_checked,
620 ),
621 Status::Hovered { is_checked } => styled(
622 palette.success.base.text,
623 palette.background.strongest.color,
624 palette.background.weak,
625 palette.success.strong,
626 is_checked,
627 ),
628 Status::Disabled { is_checked } => styled(
629 palette.success.base.text,
630 palette.background.weak.color,
631 palette.background.weak,
632 palette.success.weak,
633 is_checked,
634 ),
635 }
636}
637
638pub fn danger(theme: &Theme, status: Status) -> Style {
640 let palette = theme.extended_palette();
641
642 match status {
643 Status::Active { is_checked } => styled(
644 palette.danger.base.text,
645 palette.background.strongest.color,
646 palette.background.base,
647 palette.danger.base,
648 is_checked,
649 ),
650 Status::Hovered { is_checked } => styled(
651 palette.danger.base.text,
652 palette.background.strongest.color,
653 palette.background.weak,
654 palette.danger.strong,
655 is_checked,
656 ),
657 Status::Disabled { is_checked } => styled(
658 palette.danger.base.text,
659 palette.background.weak.color,
660 palette.background.weak,
661 palette.danger.weak,
662 is_checked,
663 ),
664 }
665}
666
667fn styled(
668 icon_color: Color,
669 border_color: Color,
670 base: palette::Pair,
671 accent: palette::Pair,
672 is_checked: bool,
673) -> Style {
674 Style {
675 background: Background::Color(if is_checked {
676 accent.color
677 } else {
678 base.color
679 }),
680 icon_color,
681 border: Border {
682 radius: 2.0.into(),
683 width: 1.0,
684 color: if is_checked {
685 accent.color
686 } else {
687 border_color
688 },
689 },
690 text_color: None,
691 }
692}