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.label,
291 widget::text::Format {
292 width: self.width,
293 height: Length::Shrink,
294 line_height: self.text_line_height,
295 size: self.text_size,
296 font: self.font,
297 align_x: text::Alignment::Default,
298 align_y: alignment::Vertical::Top,
299 shaping: self.text_shaping,
300 wrapping: self.text_wrapping,
301 },
302 )
303 },
304 )
305 }
306
307 fn update(
308 &mut self,
309 _tree: &mut Tree,
310 event: &Event,
311 layout: Layout<'_>,
312 cursor: mouse::Cursor,
313 _renderer: &Renderer,
314 _clipboard: &mut dyn Clipboard,
315 shell: &mut Shell<'_, Message>,
316 _viewport: &Rectangle,
317 ) {
318 match event {
319 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
320 | Event::Touch(touch::Event::FingerPressed { .. }) => {
321 let mouse_over = cursor.is_over(layout.bounds());
322
323 if mouse_over {
324 if let Some(on_toggle) = &self.on_toggle {
325 shell.publish((on_toggle)(!self.is_checked));
326 shell.capture_event();
327 }
328 }
329 }
330 _ => {}
331 }
332
333 let current_status = {
334 let is_mouse_over = cursor.is_over(layout.bounds());
335 let is_disabled = self.on_toggle.is_none();
336 let is_checked = self.is_checked;
337
338 if is_disabled {
339 Status::Disabled { is_checked }
340 } else if is_mouse_over {
341 Status::Hovered { is_checked }
342 } else {
343 Status::Active { is_checked }
344 }
345 };
346
347 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
348 self.last_status = Some(current_status);
349 } else if self
350 .last_status
351 .is_some_and(|status| status != current_status)
352 {
353 shell.request_redraw();
354 }
355 }
356
357 fn mouse_interaction(
358 &self,
359 _tree: &Tree,
360 layout: Layout<'_>,
361 cursor: mouse::Cursor,
362 _viewport: &Rectangle,
363 _renderer: &Renderer,
364 ) -> mouse::Interaction {
365 if cursor.is_over(layout.bounds()) && self.on_toggle.is_some() {
366 mouse::Interaction::Pointer
367 } else {
368 mouse::Interaction::default()
369 }
370 }
371
372 fn draw(
373 &self,
374 tree: &Tree,
375 renderer: &mut Renderer,
376 theme: &Theme,
377 defaults: &renderer::Style,
378 layout: Layout<'_>,
379 _cursor: mouse::Cursor,
380 viewport: &Rectangle,
381 ) {
382 let mut children = layout.children();
383
384 let style = theme.style(
385 &self.class,
386 self.last_status.unwrap_or(Status::Disabled {
387 is_checked: self.is_checked,
388 }),
389 );
390
391 {
392 let layout = children.next().unwrap();
393 let bounds = layout.bounds();
394
395 renderer.fill_quad(
396 renderer::Quad {
397 bounds,
398 border: style.border,
399 ..renderer::Quad::default()
400 },
401 style.background,
402 );
403
404 let Icon {
405 font,
406 code_point,
407 size,
408 line_height,
409 shaping,
410 } = &self.icon;
411 let size = size.unwrap_or(Pixels(bounds.height * 0.7));
412
413 if self.is_checked {
414 renderer.fill_text(
415 text::Text {
416 content: code_point.to_string(),
417 font: *font,
418 size,
419 line_height: *line_height,
420 bounds: bounds.size(),
421 align_x: text::Alignment::Center,
422 align_y: alignment::Vertical::Center,
423 shaping: *shaping,
424 wrapping: text::Wrapping::default(),
425 },
426 bounds.center(),
427 style.icon_color,
428 *viewport,
429 );
430 }
431 }
432
433 {
434 let label_layout = children.next().unwrap();
435 let state: &widget::text::State<Renderer::Paragraph> =
436 tree.state.downcast_ref();
437
438 crate::text::draw(
439 renderer,
440 defaults,
441 label_layout.bounds(),
442 state.raw(),
443 crate::text::Style {
444 color: style.text_color,
445 },
446 viewport,
447 );
448 }
449 }
450
451 fn operate(
452 &self,
453 _state: &mut Tree,
454 layout: Layout<'_>,
455 _renderer: &Renderer,
456 operation: &mut dyn widget::Operation,
457 ) {
458 operation.text(None, layout.bounds(), &self.label);
459 }
460}
461
462impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
463 for Element<'a, Message, Theme, Renderer>
464where
465 Message: 'a,
466 Theme: 'a + Catalog,
467 Renderer: 'a + text::Renderer,
468{
469 fn from(
470 checkbox: Checkbox<'a, Message, Theme, Renderer>,
471 ) -> Element<'a, Message, Theme, Renderer> {
472 Element::new(checkbox)
473 }
474}
475
476#[derive(Debug, Clone, PartialEq)]
478pub struct Icon<Font> {
479 pub font: Font,
481 pub code_point: char,
483 pub size: Option<Pixels>,
485 pub line_height: text::LineHeight,
487 pub shaping: text::Shaping,
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq)]
493pub enum Status {
494 Active {
496 is_checked: bool,
498 },
499 Hovered {
501 is_checked: bool,
503 },
504 Disabled {
506 is_checked: bool,
508 },
509}
510
511#[derive(Debug, Clone, Copy, PartialEq)]
513pub struct Style {
514 pub background: Background,
516 pub icon_color: Color,
518 pub border: Border,
520 pub text_color: Option<Color>,
522}
523
524pub trait Catalog: Sized {
526 type Class<'a>;
528
529 fn default<'a>() -> Self::Class<'a>;
531
532 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
534}
535
536pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
540
541impl Catalog for Theme {
542 type Class<'a> = StyleFn<'a, Self>;
543
544 fn default<'a>() -> Self::Class<'a> {
545 Box::new(primary)
546 }
547
548 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
549 class(self, status)
550 }
551}
552
553pub fn primary(theme: &Theme, status: Status) -> Style {
555 let palette = theme.extended_palette();
556
557 match status {
558 Status::Active { is_checked } => styled(
559 palette.primary.strong.text,
560 palette.background.strongest.color,
561 palette.background.base,
562 palette.primary.base,
563 is_checked,
564 ),
565 Status::Hovered { is_checked } => styled(
566 palette.primary.strong.text,
567 palette.background.strongest.color,
568 palette.background.weak,
569 palette.primary.strong,
570 is_checked,
571 ),
572 Status::Disabled { is_checked } => styled(
573 palette.primary.strong.text,
574 palette.background.weak.color,
575 palette.background.weak,
576 palette.background.strong,
577 is_checked,
578 ),
579 }
580}
581
582pub fn secondary(theme: &Theme, status: Status) -> Style {
584 let palette = theme.extended_palette();
585
586 match status {
587 Status::Active { is_checked } => styled(
588 palette.background.base.text,
589 palette.background.strongest.color,
590 palette.background.base,
591 palette.background.strong,
592 is_checked,
593 ),
594 Status::Hovered { is_checked } => styled(
595 palette.background.base.text,
596 palette.background.strongest.color,
597 palette.background.weak,
598 palette.background.strong,
599 is_checked,
600 ),
601 Status::Disabled { is_checked } => styled(
602 palette.background.strong.color,
603 palette.background.weak.color,
604 palette.background.weak,
605 palette.background.weak,
606 is_checked,
607 ),
608 }
609}
610
611pub fn success(theme: &Theme, status: Status) -> Style {
613 let palette = theme.extended_palette();
614
615 match status {
616 Status::Active { is_checked } => styled(
617 palette.success.base.text,
618 palette.background.weak.color,
619 palette.background.base,
620 palette.success.base,
621 is_checked,
622 ),
623 Status::Hovered { is_checked } => styled(
624 palette.success.base.text,
625 palette.background.strongest.color,
626 palette.background.weak,
627 palette.success.strong,
628 is_checked,
629 ),
630 Status::Disabled { is_checked } => styled(
631 palette.success.base.text,
632 palette.background.weak.color,
633 palette.background.weak,
634 palette.success.weak,
635 is_checked,
636 ),
637 }
638}
639
640pub fn danger(theme: &Theme, status: Status) -> Style {
642 let palette = theme.extended_palette();
643
644 match status {
645 Status::Active { is_checked } => styled(
646 palette.danger.base.text,
647 palette.background.strongest.color,
648 palette.background.base,
649 palette.danger.base,
650 is_checked,
651 ),
652 Status::Hovered { is_checked } => styled(
653 palette.danger.base.text,
654 palette.background.strongest.color,
655 palette.background.weak,
656 palette.danger.strong,
657 is_checked,
658 ),
659 Status::Disabled { is_checked } => styled(
660 palette.danger.base.text,
661 palette.background.weak.color,
662 palette.background.weak,
663 palette.danger.weak,
664 is_checked,
665 ),
666 }
667}
668
669fn styled(
670 icon_color: Color,
671 border_color: Color,
672 base: palette::Pair,
673 accent: palette::Pair,
674 is_checked: bool,
675) -> Style {
676 Style {
677 background: Background::Color(if is_checked {
678 accent.color
679 } else {
680 base.color
681 }),
682 icon_color,
683 border: Border {
684 radius: 2.0.into(),
685 width: 1.0,
686 color: if is_checked {
687 accent.color
688 } else {
689 border_color
690 },
691 },
692 text_color: None,
693 }
694}