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