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