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