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 },
428 bounds.center(),
429 style.icon_color,
430 *viewport,
431 );
432 }
433 }
434
435 if self.label.is_none() {
436 return;
437 }
438
439 {
440 let label_layout = children.next().unwrap();
441 let state: &widget::text::State<Renderer::Paragraph> = tree.state.downcast_ref();
442
443 crate::text::draw(
444 renderer,
445 defaults,
446 label_layout.bounds(),
447 state.raw(),
448 crate::text::Style {
449 color: style.text_color,
450 },
451 viewport,
452 );
453 }
454 }
455
456 fn operate(
457 &mut self,
458 _tree: &mut Tree,
459 layout: Layout<'_>,
460 _renderer: &Renderer,
461 operation: &mut dyn widget::Operation,
462 ) {
463 if let Some(label) = self.label.as_deref() {
464 operation.text(None, layout.bounds(), label);
465 }
466 }
467}
468
469impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
470 for Element<'a, Message, Theme, Renderer>
471where
472 Message: 'a,
473 Theme: 'a + Catalog,
474 Renderer: 'a + text::Renderer,
475{
476 fn from(
477 checkbox: Checkbox<'a, Message, Theme, Renderer>,
478 ) -> Element<'a, Message, Theme, Renderer> {
479 Element::new(checkbox)
480 }
481}
482
483#[derive(Debug, Clone, PartialEq)]
485pub struct Icon<Font> {
486 pub font: Font,
488 pub code_point: char,
490 pub size: Option<Pixels>,
492 pub line_height: text::LineHeight,
494 pub shaping: text::Shaping,
496}
497
498#[derive(Debug, Clone, Copy, PartialEq, Eq)]
500pub enum Status {
501 Active {
503 is_checked: bool,
505 },
506 Hovered {
508 is_checked: bool,
510 },
511 Disabled {
513 is_checked: bool,
515 },
516}
517
518#[derive(Debug, Clone, Copy, PartialEq)]
520pub struct Style {
521 pub background: Background,
523 pub icon_color: Color,
525 pub border: Border,
527 pub text_color: Option<Color>,
529}
530
531pub trait Catalog: Sized {
533 type Class<'a>;
535
536 fn default<'a>() -> Self::Class<'a>;
538
539 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
541}
542
543pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
547
548impl Catalog for Theme {
549 type Class<'a> = StyleFn<'a, Self>;
550
551 fn default<'a>() -> Self::Class<'a> {
552 Box::new(primary)
553 }
554
555 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
556 class(self, status)
557 }
558}
559
560pub fn primary(theme: &Theme, status: Status) -> Style {
562 let palette = theme.extended_palette();
563
564 match status {
565 Status::Active { is_checked } => styled(
566 palette.background.strong.color,
567 palette.background.base,
568 palette.primary.base.text,
569 palette.primary.base,
570 is_checked,
571 ),
572 Status::Hovered { is_checked } => styled(
573 palette.background.strong.color,
574 palette.background.weak,
575 palette.primary.base.text,
576 palette.primary.strong,
577 is_checked,
578 ),
579 Status::Disabled { is_checked } => styled(
580 palette.background.weak.color,
581 palette.background.weaker,
582 palette.primary.base.text,
583 palette.background.strong,
584 is_checked,
585 ),
586 }
587}
588
589pub fn secondary(theme: &Theme, status: Status) -> Style {
591 let palette = theme.extended_palette();
592
593 match status {
594 Status::Active { is_checked } => styled(
595 palette.background.strong.color,
596 palette.background.base,
597 palette.background.base.text,
598 palette.background.strong,
599 is_checked,
600 ),
601 Status::Hovered { is_checked } => styled(
602 palette.background.strong.color,
603 palette.background.weak,
604 palette.background.base.text,
605 palette.background.strong,
606 is_checked,
607 ),
608 Status::Disabled { is_checked } => styled(
609 palette.background.weak.color,
610 palette.background.weak,
611 palette.background.base.text,
612 palette.background.weak,
613 is_checked,
614 ),
615 }
616}
617
618pub fn success(theme: &Theme, status: Status) -> Style {
620 let palette = theme.extended_palette();
621
622 match status {
623 Status::Active { is_checked } => styled(
624 palette.background.weak.color,
625 palette.background.base,
626 palette.success.base.text,
627 palette.success.base,
628 is_checked,
629 ),
630 Status::Hovered { is_checked } => styled(
631 palette.background.strong.color,
632 palette.background.weak,
633 palette.success.base.text,
634 palette.success.strong,
635 is_checked,
636 ),
637 Status::Disabled { is_checked } => styled(
638 palette.background.weak.color,
639 palette.background.weak,
640 palette.success.base.text,
641 palette.success.weak,
642 is_checked,
643 ),
644 }
645}
646
647pub fn danger(theme: &Theme, status: Status) -> Style {
649 let palette = theme.extended_palette();
650
651 match status {
652 Status::Active { is_checked } => styled(
653 palette.background.strong.color,
654 palette.background.base,
655 palette.danger.base.text,
656 palette.danger.base,
657 is_checked,
658 ),
659 Status::Hovered { is_checked } => styled(
660 palette.background.strong.color,
661 palette.background.weak,
662 palette.danger.base.text,
663 palette.danger.strong,
664 is_checked,
665 ),
666 Status::Disabled { is_checked } => styled(
667 palette.background.weak.color,
668 palette.background.weak,
669 palette.danger.base.text,
670 palette.danger.weak,
671 is_checked,
672 ),
673 }
674}
675
676fn styled(
677 border_color: Color,
678 base: palette::Pair,
679 icon_color: Color,
680 accent: palette::Pair,
681 is_checked: bool,
682) -> Style {
683 let (background, border) = if is_checked {
684 (accent, accent.color)
685 } else {
686 (base, border_color)
687 };
688
689 Style {
690 background: Background::Color(background.color),
691 icon_color,
692 border: Border {
693 radius: 2.0.into(),
694 width: 1.0,
695 color: border,
696 },
697 text_color: None,
698 }
699}