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