1use crate::core::alignment;
34use crate::core::border;
35use crate::core::keyboard;
36use crate::core::keyboard::key;
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::operation::accessible::{Accessible, Role};
45use crate::core::widget::operation::focusable::{self, Focusable};
46use crate::core::widget::tree::{self, Tree};
47use crate::core::window;
48use crate::core::{
49 Background, Border, Color, Element, Event, Layout, Length, Pixels, Rectangle, Shadow, Shell,
50 Size, Theme, Widget,
51};
52
53pub struct Toggler<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
86where
87 Theme: Catalog,
88 Renderer: text::Renderer,
89{
90 is_toggled: bool,
91 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
92 label: Option<text::Fragment<'a>>,
93 width: Length,
94 size: f32,
95 text_size: Option<Pixels>,
96 line_height: text::LineHeight,
97 alignment: text::Alignment,
98 text_shaping: text::Shaping,
99 wrapping: text::Wrapping,
100 spacing: f32,
101 font: Option<Renderer::Font>,
102 class: Theme::Class<'a>,
103 last_status: Option<Status>,
104}
105
106impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
107where
108 Theme: Catalog,
109 Renderer: text::Renderer,
110{
111 pub const DEFAULT_SIZE: f32 = 16.0;
113
114 pub fn new(is_toggled: bool) -> Self {
123 Toggler {
124 is_toggled,
125 on_toggle: None,
126 label: None,
127 width: Length::Shrink,
128 size: Self::DEFAULT_SIZE,
129 text_size: None,
130 line_height: text::LineHeight::default(),
131 alignment: text::Alignment::Default,
132 text_shaping: text::Shaping::default(),
133 wrapping: text::Wrapping::default(),
134 spacing: Self::DEFAULT_SIZE / 2.0,
135 font: None,
136 class: Theme::default(),
137 last_status: None,
138 }
139 }
140
141 pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
143 self.label = Some(label.into_fragment());
144 self
145 }
146
147 pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self {
152 self.on_toggle = Some(Box::new(on_toggle));
153 self
154 }
155
156 pub fn on_toggle_maybe(mut self, on_toggle: Option<impl Fn(bool) -> Message + 'a>) -> Self {
161 self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
162 self
163 }
164
165 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
167 self.size = size.into().0;
168 self
169 }
170
171 pub fn width(mut self, width: impl Into<Length>) -> Self {
173 self.width = width.into();
174 self
175 }
176
177 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
179 self.text_size = Some(text_size.into());
180 self
181 }
182
183 pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
185 self.line_height = line_height.into();
186 self
187 }
188
189 pub fn alignment(mut self, alignment: impl Into<text::Alignment>) -> Self {
191 self.alignment = alignment.into();
192 self
193 }
194
195 pub fn shaping(mut self, shaping: text::Shaping) -> Self {
197 self.text_shaping = shaping;
198 self
199 }
200
201 pub fn wrapping(mut self, wrapping: text::Wrapping) -> Self {
203 self.wrapping = wrapping;
204 self
205 }
206
207 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
209 self.spacing = spacing.into().0;
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 #[must_use]
223 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
224 where
225 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
226 {
227 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
228 self
229 }
230
231 #[cfg(feature = "advanced")]
233 #[must_use]
234 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
235 self.class = class.into();
236 self
237 }
238}
239
240#[derive(Debug, Clone, Default)]
241struct State<P: text::Paragraph> {
242 is_focused: bool,
243 focus_visible: bool,
244 label: widget::text::State<P>,
245}
246
247impl<P: text::Paragraph> focusable::Focusable for State<P> {
248 fn is_focused(&self) -> bool {
249 self.is_focused
250 }
251
252 fn focus(&mut self) {
253 self.is_focused = true;
254 self.focus_visible = true;
255 }
256
257 fn unfocus(&mut self) {
258 self.is_focused = false;
259 self.focus_visible = false;
260 }
261}
262
263impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
264 for Toggler<'_, Message, Theme, Renderer>
265where
266 Theme: Catalog,
267 Renderer: text::Renderer,
268{
269 fn tag(&self) -> tree::Tag {
270 tree::Tag::of::<State<Renderer::Paragraph>>()
271 }
272
273 fn state(&self) -> tree::State {
274 tree::State::new(State::<Renderer::Paragraph>::default())
275 }
276
277 fn size(&self) -> Size<Length> {
278 Size {
279 width: self.width,
280 height: Length::Shrink,
281 }
282 }
283
284 fn layout(
285 &mut self,
286 tree: &mut Tree,
287 renderer: &Renderer,
288 limits: &layout::Limits,
289 ) -> layout::Node {
290 let limits = limits.width(self.width);
291
292 layout::next_to_each_other(
293 &limits,
294 if self.label.is_some() {
295 self.spacing
296 } else {
297 0.0
298 },
299 |_| {
300 let size = if renderer::CRISP {
301 let scale_factor = renderer.scale_factor().unwrap_or(1.0);
302
303 (self.size * scale_factor).round() / scale_factor
304 } else {
305 self.size
306 };
307
308 layout::Node::new(Size::new(2.0 * size, size))
309 },
310 |limits| {
311 if let Some(label) = self.label.as_deref() {
312 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
313
314 widget::text::layout(
315 &mut state.label,
316 renderer,
317 limits,
318 label,
319 widget::text::Format {
320 width: self.width,
321 height: Length::Shrink,
322 line_height: self.line_height,
323 size: self.text_size,
324 font: self.font,
325 align_x: self.alignment,
326 align_y: alignment::Vertical::Top,
327 shaping: self.text_shaping,
328 wrapping: self.wrapping,
329 ellipsis: text::Ellipsis::None,
330 },
331 )
332 } else {
333 layout::Node::new(Size::ZERO)
334 }
335 },
336 )
337 }
338
339 fn operate(
340 &mut self,
341 tree: &mut Tree,
342 layout: Layout<'_>,
343 _renderer: &Renderer,
344 operation: &mut dyn widget::Operation,
345 ) {
346 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
347
348 operation.accessible(
349 None,
350 layout.bounds(),
351 &Accessible {
352 role: Role::Switch,
353 label: self.label.as_deref(),
354 toggled: Some(self.is_toggled),
355 disabled: self.on_toggle.is_none(),
356 ..Accessible::default()
357 },
358 );
359
360 if self.on_toggle.is_some() {
361 operation.focusable(None, layout.bounds(), state);
362 } else {
363 state.unfocus();
364 }
365 }
366
367 fn update(
368 &mut self,
369 tree: &mut Tree,
370 event: &Event,
371 layout: Layout<'_>,
372 cursor: mouse::Cursor,
373 _renderer: &Renderer,
374 shell: &mut Shell<'_, Message>,
375 _viewport: &Rectangle,
376 ) {
377 let Some(on_toggle) = &self.on_toggle else {
378 return;
379 };
380
381 match event {
382 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
383 | Event::Touch(touch::Event::FingerPressed { .. }) => {
384 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
385
386 if cursor.is_over(layout.bounds()) {
387 state.is_focused = true;
388 state.focus_visible = false;
389
390 shell.publish(on_toggle(!self.is_toggled));
391 shell.capture_event();
392 } else {
393 state.is_focused = false;
394 state.focus_visible = false;
395 }
396 }
397 Event::Keyboard(keyboard::Event::KeyPressed {
398 key: keyboard::Key::Named(key::Named::Space),
399 ..
400 }) => {
401 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
402
403 if state.is_focused {
404 shell.publish(on_toggle(!self.is_toggled));
405 shell.capture_event();
406 }
407 }
408 Event::Keyboard(keyboard::Event::KeyPressed {
409 key: keyboard::Key::Named(key::Named::Escape),
410 ..
411 }) => {
412 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
413 if state.is_focused {
414 state.is_focused = false;
415 state.focus_visible = false;
416 shell.capture_event();
417 }
418 }
419 _ => {}
420 }
421
422 let current_status = if self.on_toggle.is_none() {
423 Status::Disabled {
424 is_toggled: self.is_toggled,
425 }
426 } else {
427 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
428
429 if state.focus_visible {
430 Status::Focused {
431 is_toggled: self.is_toggled,
432 }
433 } else if cursor.is_over(layout.bounds()) {
434 Status::Hovered {
435 is_toggled: self.is_toggled,
436 }
437 } else {
438 Status::Active {
439 is_toggled: self.is_toggled,
440 }
441 }
442 };
443
444 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
445 self.last_status = Some(current_status);
446 } else if self
447 .last_status
448 .is_some_and(|status| status != current_status)
449 {
450 shell.request_redraw();
451 }
452 }
453
454 fn mouse_interaction(
455 &self,
456 _tree: &Tree,
457 layout: Layout<'_>,
458 cursor: mouse::Cursor,
459 _viewport: &Rectangle,
460 _renderer: &Renderer,
461 ) -> mouse::Interaction {
462 if cursor.is_over(layout.bounds()) {
463 if self.on_toggle.is_some() {
464 mouse::Interaction::Pointer
465 } else {
466 mouse::Interaction::NotAllowed
467 }
468 } else {
469 mouse::Interaction::default()
470 }
471 }
472
473 fn draw(
474 &self,
475 tree: &Tree,
476 renderer: &mut Renderer,
477 theme: &Theme,
478 defaults: &renderer::Style,
479 layout: Layout<'_>,
480 _cursor: mouse::Cursor,
481 viewport: &Rectangle,
482 ) {
483 let mut children = layout.children();
484 let toggler_layout = children.next().unwrap();
485
486 let style = theme.style(
487 &self.class,
488 self.last_status.unwrap_or(Status::Disabled {
489 is_toggled: self.is_toggled,
490 }),
491 );
492
493 if self.label.is_some() {
494 let label_layout = children.next().unwrap();
495 let state: &State<Renderer::Paragraph> = tree.state.downcast_ref();
496
497 crate::text::draw(
498 renderer,
499 defaults,
500 label_layout.bounds(),
501 state.label.raw(),
502 crate::text::Style {
503 color: style.text_color,
504 },
505 viewport,
506 );
507 }
508
509 let scale_factor = renderer.scale_factor().unwrap_or(1.0);
510 let bounds = toggler_layout.bounds();
511
512 let border_radius = style
513 .border_radius
514 .unwrap_or_else(|| border::Radius::new(bounds.height / 2.0));
515
516 renderer.fill_quad(
517 renderer::Quad {
518 bounds,
519 border: Border {
520 radius: border_radius,
521 width: style.background_border_width,
522 color: style.background_border_color,
523 },
524 shadow: style.shadow,
525 ..renderer::Quad::default()
526 },
527 style.background,
528 );
529
530 let toggle_bounds = {
531 let bounds = if renderer::CRISP {
533 (bounds * scale_factor).round()
534 } else {
535 bounds
536 };
537
538 let padding = (style.padding_ratio * bounds.height).round();
539
540 Rectangle {
541 x: bounds.x
542 + if self.is_toggled {
543 bounds.width - bounds.height + padding
544 } else {
545 padding
546 },
547 y: bounds.y + padding,
548 width: bounds.height - (2.0 * padding),
549 height: bounds.height - (2.0 * padding),
550 } * (1.0 / scale_factor)
551 };
552
553 renderer.fill_quad(
554 renderer::Quad {
555 bounds: toggle_bounds,
556 border: Border {
557 radius: border_radius,
558 width: style.foreground_border_width,
559 color: style.foreground_border_color,
560 },
561 ..renderer::Quad::default()
562 },
563 style.foreground,
564 );
565 }
566}
567
568impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
569 for Element<'a, Message, Theme, Renderer>
570where
571 Message: 'a,
572 Theme: Catalog + 'a,
573 Renderer: text::Renderer + 'a,
574{
575 fn from(
576 toggler: Toggler<'a, Message, Theme, Renderer>,
577 ) -> Element<'a, Message, Theme, Renderer> {
578 Element::new(toggler)
579 }
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq)]
584pub enum Status {
585 Active {
587 is_toggled: bool,
589 },
590 Hovered {
592 is_toggled: bool,
594 },
595 Focused {
597 is_toggled: bool,
599 },
600 Disabled {
602 is_toggled: bool,
604 },
605}
606
607#[derive(Debug, Clone, Copy, PartialEq)]
609pub struct Style {
610 pub background: Background,
612 pub background_border_width: f32,
614 pub background_border_color: Color,
616 pub foreground: Background,
618 pub foreground_border_width: f32,
620 pub foreground_border_color: Color,
622 pub text_color: Option<Color>,
624 pub border_radius: Option<border::Radius>,
628 pub padding_ratio: f32,
630 pub shadow: Shadow,
632}
633
634pub trait Catalog: Sized {
636 type Class<'a>;
638
639 fn default<'a>() -> Self::Class<'a>;
641
642 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
644}
645
646pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
650
651impl Catalog for Theme {
652 type Class<'a> = StyleFn<'a, Self>;
653
654 fn default<'a>() -> Self::Class<'a> {
655 Box::new(default)
656 }
657
658 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
659 class(self, status)
660 }
661}
662
663pub fn default(theme: &Theme, status: Status) -> Style {
665 let palette = theme.palette();
666
667 let background = match status {
668 Status::Active { is_toggled }
669 | Status::Hovered { is_toggled }
670 | Status::Focused { is_toggled } => {
671 if is_toggled {
672 palette.primary.base.color
673 } else {
674 palette.background.strong.color
675 }
676 }
677 Status::Disabled { is_toggled } => {
678 if is_toggled {
679 palette.background.strong.color
680 } else {
681 palette.background.weak.color
682 }
683 }
684 };
685
686 let foreground = match status {
687 Status::Active { is_toggled } | Status::Focused { is_toggled } => {
688 if is_toggled {
689 palette.primary.base.text
690 } else {
691 palette.background.base.color
692 }
693 }
694 Status::Hovered { is_toggled } => {
695 if is_toggled {
696 Color {
697 a: 0.5,
698 ..palette.primary.base.text
699 }
700 } else {
701 palette.background.weak.color
702 }
703 }
704 Status::Disabled { .. } => palette.background.weakest.color,
705 };
706
707 let page_bg = palette.background.base.color;
708 let accent = palette.primary.strong.color;
709
710 let (background_border_width, background_border_color) = match status {
711 Status::Focused { is_toggled } => {
712 let widget_bg = if is_toggled {
713 palette.primary.base.color
714 } else {
715 palette.background.strong.color
716 };
717 (2.0, palette::focus_border_color(widget_bg, accent, page_bg))
718 }
719 _ => (0.0, Color::TRANSPARENT),
720 };
721
722 let shadow = match status {
723 Status::Focused { .. } => palette::focus_shadow(accent, page_bg),
724 _ => Shadow::default(),
725 };
726
727 Style {
728 background: background.into(),
729 foreground: foreground.into(),
730 foreground_border_width: 0.0,
731 foreground_border_color: Color::TRANSPARENT,
732 background_border_width,
733 background_border_color,
734 text_color: None,
735 border_radius: None,
736 padding_ratio: 0.1,
737 shadow,
738 }
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744 use crate::core::widget::operation::focusable::Focusable;
745
746 type TestState = State<()>;
747
748 #[test]
749 fn focusable_trait() {
750 let mut state = TestState::default();
751 assert!(!state.is_focused());
752 assert!(!state.focus_visible);
753 state.focus();
754 assert!(state.is_focused());
755 assert!(state.focus_visible);
756 state.unfocus();
757 assert!(!state.is_focused());
758 assert!(!state.focus_visible);
759 }
760}