1use std::marker::PhantomData;
35
36use crate::core::alignment;
37use crate::core::layout;
38use crate::core::mouse;
39use crate::core::renderer;
40use crate::core::text;
41use crate::core::theme::palette;
42use crate::core::touch;
43use crate::core::widget;
44use crate::core::widget::tree::{self, Tree};
45use crate::core::window;
46use crate::core::{
47 Background, Border, Color, Element, Event, Font, Layout, Length, Pixels, Rectangle, Shell,
48 Size, Theme, Widget,
49};
50
51pub struct Checkbox<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
85where
86 Theme: Catalog,
87 Renderer: text::Renderer,
88{
89 is_checked: bool,
90 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
91 label: Option<text::Fragment<'a>>,
92 width: Length,
93 size: f32,
94 spacing: f32,
95 text_size: Option<Pixels>,
96 line_height: Option<text::LineHeight>,
97 shaping: text::Shaping,
98 wrapping: text::Wrapping,
99 font: Option<Font>,
100 icon: Icon,
101 class: Theme::Class<'a>,
102 last_status: Option<Status>,
103 renderer_: PhantomData<Renderer>,
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 pub fn new(is_checked: bool) -> Self {
119 Checkbox {
120 is_checked,
121 on_toggle: None,
122 label: None,
123 width: Length::Shrink,
124 size: Self::DEFAULT_SIZE,
125 spacing: Self::DEFAULT_SIZE / 2.0,
126 text_size: None,
127 line_height: None,
128 shaping: text::Shaping::default(),
129 wrapping: text::Wrapping::default(),
130 font: None,
131 icon: Icon {
132 font: Renderer::ICON_FONT,
133 code_point: Renderer::CHECKMARK_ICON,
134 size: None,
135 line_height: None,
136 shaping: text::Shaping::Basic,
137 },
138 class: Theme::default(),
139 last_status: None,
140 renderer_: PhantomData,
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 line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
201 self.line_height = Some(line_height.into());
202 self
203 }
204
205 pub fn shaping(mut self, shaping: text::Shaping) -> Self {
207 self.shaping = shaping;
208 self
209 }
210
211 pub fn wrapping(mut self, wrapping: text::Wrapping) -> Self {
213 self.wrapping = wrapping;
214 self
215 }
216
217 pub fn font(mut self, font: impl Into<Font>) -> Self {
221 self.font = Some(font.into());
222 self
223 }
224
225 pub fn icon(mut self, icon: Icon) -> 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 &mut 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 if self.label.is_some() {
280 self.spacing
281 } else {
282 0.0
283 },
284 |_| layout::Node::new(Size::new(self.size, self.size)),
285 |limits| {
286 if let Some(label) = self.label.as_deref() {
287 let state = tree
288 .state
289 .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
290
291 widget::text::layout(
292 state,
293 renderer,
294 limits,
295 label,
296 widget::text::Format {
297 width: self.width,
298 height: Length::Shrink,
299 line_height: self.line_height,
300 size: self.text_size,
301 font: self.font,
302 align_x: text::Alignment::Default,
303 align_y: alignment::Vertical::Top,
304 shaping: self.shaping,
305 wrapping: self.wrapping,
306 ellipsis: text::Ellipsis::None,
307 },
308 )
309 } else {
310 layout::Node::new(Size::ZERO)
311 }
312 },
313 )
314 }
315
316 fn update(
317 &mut self,
318 _tree: &mut Tree,
319 event: &Event,
320 layout: Layout<'_>,
321 cursor: mouse::Cursor,
322 _renderer: &Renderer,
323 shell: &mut Shell<'_, Message>,
324 _viewport: &Rectangle,
325 ) {
326 match event {
327 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
328 | Event::Touch(touch::Event::FingerPressed { .. }) => {
329 let mouse_over = cursor.is_over(layout.bounds());
330
331 if mouse_over && let Some(on_toggle) = &self.on_toggle {
332 shell.publish((on_toggle)(!self.is_checked));
333 shell.capture_event();
334 }
335 }
336 _ => {}
337 }
338
339 let current_status = {
340 let is_mouse_over = cursor.is_over(layout.bounds());
341 let is_disabled = self.on_toggle.is_none();
342 let is_checked = self.is_checked;
343
344 if is_disabled {
345 Status::Disabled { is_checked }
346 } else if is_mouse_over {
347 Status::Hovered { is_checked }
348 } else {
349 Status::Active { is_checked }
350 }
351 };
352
353 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
354 self.last_status = Some(current_status);
355 } else if self
356 .last_status
357 .is_some_and(|status| status != current_status)
358 {
359 shell.request_redraw();
360 }
361 }
362
363 fn mouse_interaction(
364 &self,
365 _tree: &Tree,
366 layout: Layout<'_>,
367 cursor: mouse::Cursor,
368 _viewport: &Rectangle,
369 _renderer: &Renderer,
370 ) -> mouse::Interaction {
371 if cursor.is_over(layout.bounds()) && self.on_toggle.is_some() {
372 mouse::Interaction::Pointer
373 } else {
374 mouse::Interaction::default()
375 }
376 }
377
378 fn draw(
379 &self,
380 tree: &Tree,
381 renderer: &mut Renderer,
382 theme: &Theme,
383 defaults: &renderer::Style,
384 layout: Layout<'_>,
385 _cursor: mouse::Cursor,
386 viewport: &Rectangle,
387 ) {
388 let mut children = layout.children();
389
390 let style = theme.style(
391 &self.class,
392 self.last_status.unwrap_or(Status::Disabled {
393 is_checked: self.is_checked,
394 }),
395 );
396
397 {
398 let layout = children.next().unwrap();
399 let bounds = layout.bounds();
400
401 renderer.fill_quad(
402 renderer::Quad {
403 bounds,
404 border: style.border,
405 ..renderer::Quad::default()
406 },
407 style.background,
408 );
409
410 let Icon {
411 font,
412 code_point,
413 size,
414 line_height,
415 shaping,
416 } = &self.icon;
417 let size = size.unwrap_or(Pixels(bounds.height * 0.7));
418 let line_height = line_height.unwrap_or_else(|| renderer.line_height());
419
420 if self.is_checked {
421 renderer.fill_text(
422 text::Text {
423 content: code_point.to_string(),
424 font: *font,
425 size,
426 line_height,
427 bounds: bounds.size(),
428 align_x: text::Alignment::Center,
429 align_y: alignment::Vertical::Center,
430 shaping: *shaping,
431 wrapping: text::Wrapping::default(),
432 ellipsis: text::Ellipsis::default(),
433 hint_factor: None,
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> = tree.state.downcast_ref();
449
450 crate::text::draw(
451 renderer,
452 defaults,
453 label_layout.bounds(),
454 state.raw(),
455 crate::text::Style {
456 color: style.text_color,
457 },
458 viewport,
459 );
460 }
461 }
462
463 fn operate(
464 &mut self,
465 _tree: &mut Tree,
466 layout: Layout<'_>,
467 _renderer: &Renderer,
468 operation: &mut dyn widget::Operation,
469 ) {
470 if let Some(label) = self.label.as_deref() {
471 operation.text(None, layout.bounds(), label);
472 }
473 }
474}
475
476impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
477 for Element<'a, Message, Theme, Renderer>
478where
479 Message: 'a,
480 Theme: 'a + Catalog,
481 Renderer: 'a + text::Renderer,
482{
483 fn from(
484 checkbox: Checkbox<'a, Message, Theme, Renderer>,
485 ) -> Element<'a, Message, Theme, Renderer> {
486 Element::new(checkbox)
487 }
488}
489
490#[derive(Debug, Clone, PartialEq)]
492pub struct Icon {
493 pub font: Font,
495 pub code_point: char,
497 pub size: Option<Pixels>,
499 pub line_height: Option<text::LineHeight>,
501 pub shaping: text::Shaping,
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
507pub enum Status {
508 Active {
510 is_checked: bool,
512 },
513 Hovered {
515 is_checked: bool,
517 },
518 Disabled {
520 is_checked: bool,
522 },
523}
524
525#[derive(Debug, Clone, Copy, PartialEq)]
527pub struct Style {
528 pub background: Background,
530 pub icon_color: Color,
532 pub border: Border,
534 pub text_color: Option<Color>,
536}
537
538pub trait Catalog: Sized {
540 type Class<'a>;
542
543 fn default<'a>() -> Self::Class<'a>;
545
546 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
548}
549
550pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
554
555impl Catalog for Theme {
556 type Class<'a> = StyleFn<'a, Self>;
557
558 fn default<'a>() -> Self::Class<'a> {
559 Box::new(primary)
560 }
561
562 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
563 class(self, status)
564 }
565}
566
567pub fn primary(theme: &Theme, status: Status) -> Style {
569 let palette = theme.palette();
570
571 match status {
572 Status::Active { is_checked } => styled(
573 palette.background.strong.color,
574 palette.background.base,
575 palette.primary.base.text,
576 palette.primary.base,
577 is_checked,
578 ),
579 Status::Hovered { is_checked } => styled(
580 palette.background.strong.color,
581 palette.background.weak,
582 palette.primary.base.text,
583 palette.primary.strong,
584 is_checked,
585 ),
586 Status::Disabled { is_checked } => styled(
587 palette.background.weak.color,
588 palette.background.weaker,
589 palette.primary.base.text,
590 palette.background.strong,
591 is_checked,
592 ),
593 }
594}
595
596pub fn secondary(theme: &Theme, status: Status) -> Style {
598 let palette = theme.palette();
599
600 match status {
601 Status::Active { is_checked } => styled(
602 palette.background.strong.color,
603 palette.background.base,
604 palette.background.base.text,
605 palette.background.strong,
606 is_checked,
607 ),
608 Status::Hovered { is_checked } => styled(
609 palette.background.strong.color,
610 palette.background.weak,
611 palette.background.base.text,
612 palette.background.strong,
613 is_checked,
614 ),
615 Status::Disabled { is_checked } => styled(
616 palette.background.weak.color,
617 palette.background.weak,
618 palette.background.base.text,
619 palette.background.weak,
620 is_checked,
621 ),
622 }
623}
624
625pub fn success(theme: &Theme, status: Status) -> Style {
627 let palette = theme.palette();
628
629 match status {
630 Status::Active { is_checked } => styled(
631 palette.background.weak.color,
632 palette.background.base,
633 palette.success.base.text,
634 palette.success.base,
635 is_checked,
636 ),
637 Status::Hovered { is_checked } => styled(
638 palette.background.strong.color,
639 palette.background.weak,
640 palette.success.base.text,
641 palette.success.strong,
642 is_checked,
643 ),
644 Status::Disabled { is_checked } => styled(
645 palette.background.weak.color,
646 palette.background.weak,
647 palette.success.base.text,
648 palette.success.weak,
649 is_checked,
650 ),
651 }
652}
653
654pub fn danger(theme: &Theme, status: Status) -> Style {
656 let palette = theme.palette();
657
658 match status {
659 Status::Active { is_checked } => styled(
660 palette.background.strong.color,
661 palette.background.base,
662 palette.danger.base.text,
663 palette.danger.base,
664 is_checked,
665 ),
666 Status::Hovered { is_checked } => styled(
667 palette.background.strong.color,
668 palette.background.weak,
669 palette.danger.base.text,
670 palette.danger.strong,
671 is_checked,
672 ),
673 Status::Disabled { is_checked } => styled(
674 palette.background.weak.color,
675 palette.background.weak,
676 palette.danger.base.text,
677 palette.danger.weak,
678 is_checked,
679 ),
680 }
681}
682
683fn styled(
684 border_color: Color,
685 base: palette::Pair,
686 icon_color: Color,
687 accent: palette::Pair,
688 is_checked: bool,
689) -> Style {
690 let (background, border) = if is_checked {
691 (accent, accent.color)
692 } else {
693 (base, border_color)
694 };
695
696 Style {
697 background: Background::Color(background.color),
698 icon_color,
699 border: Border {
700 radius: 2.0.into(),
701 width: 1.0,
702 color: border,
703 },
704 text_color: None,
705 }
706}