1#[cfg(feature = "a11y")]
35use std::borrow::Cow;
36
37use iced_runtime::core::border::Radius;
38
39use crate::core::alignment;
40use crate::core::event;
41use crate::core::layout;
42use crate::core::mouse;
43use crate::core::renderer;
44use crate::core::text;
45use crate::core::touch;
46use crate::core::widget::tree::{self, Tree};
47use crate::core::widget::{self, Id};
48use crate::core::{
49 id, Border, Clipboard, Color, Element, Event, Layout, Length, Pixels,
50 Rectangle, Shell, Size, Theme, Widget,
51};
52
53#[allow(missing_debug_implementations)]
86pub struct Toggler<
87 'a,
88 Message,
89 Theme = crate::Theme,
90 Renderer = crate::Renderer,
91> where
92 Theme: Catalog,
93 Renderer: text::Renderer,
94{
95 id: Id,
96 label_id: Option<Id>,
97 #[cfg(feature = "a11y")]
98 name: Option<Cow<'a, str>>,
99 #[cfg(feature = "a11y")]
100 description: Option<iced_accessibility::Description<'a>>,
101 #[cfg(feature = "a11y")]
102 labeled_by_widget: Option<Vec<iced_accessibility::accesskit::NodeId>>,
103 is_toggled: bool,
104 on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>,
105 label: Option<text::Fragment<'a>>,
106 width: Length,
107 size: f32,
108 text_size: Option<Pixels>,
109 text_line_height: text::LineHeight,
110 text_alignment: alignment::Horizontal,
111 text_shaping: text::Shaping,
112 text_wrapping: text::Wrapping,
113 spacing: f32,
114 font: Option<Renderer::Font>,
115 class: Theme::Class<'a>,
116}
117
118impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
119where
120 Theme: Catalog,
121 Renderer: text::Renderer,
122{
123 pub const DEFAULT_SIZE: f32 = 16.0;
125
126 pub fn new(is_toggled: bool) -> Self {
132 Toggler {
133 id: Id::unique(),
134 label_id: None,
135 #[cfg(feature = "a11y")]
136 name: None,
137 #[cfg(feature = "a11y")]
138 description: None,
139 #[cfg(feature = "a11y")]
140 labeled_by_widget: None,
141 is_toggled,
142 on_toggle: None,
143 label: None,
144 width: Length::Shrink,
145 size: Self::DEFAULT_SIZE,
146 text_size: None,
147 text_line_height: text::LineHeight::default(),
148 text_alignment: alignment::Horizontal::Left,
149 text_wrapping: text::Wrapping::default(),
150 spacing: Self::DEFAULT_SIZE / 2.0,
151 text_shaping: text::Shaping::Advanced,
152 font: None,
153 class: Theme::default(),
154 }
155 }
156
157 pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self {
159 self.label = Some(label.into_fragment());
160 self.label_id = Some(Id::unique());
161 self
162 }
163
164 pub fn on_toggle(
169 mut self,
170 on_toggle: impl Fn(bool) -> Message + 'a,
171 ) -> Self {
172 self.on_toggle = Some(Box::new(on_toggle));
173 self
174 }
175
176 pub fn on_toggle_maybe(
181 mut self,
182 on_toggle: Option<impl Fn(bool) -> Message + 'a>,
183 ) -> Self {
184 self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _);
185 self
186 }
187
188 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
190 self.size = size.into().0;
191 self
192 }
193
194 pub fn width(mut self, width: impl Into<Length>) -> Self {
196 self.width = width.into();
197 self
198 }
199
200 pub fn text_size(mut self, text_size: impl Into<Pixels>) -> Self {
202 self.text_size = Some(text_size.into());
203 self
204 }
205
206 pub fn text_line_height(
208 mut self,
209 line_height: impl Into<text::LineHeight>,
210 ) -> Self {
211 self.text_line_height = line_height.into();
212 self
213 }
214
215 pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self {
217 self.text_alignment = alignment;
218 self
219 }
220
221 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
223 self.text_shaping = shaping;
224 self
225 }
226
227 pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self {
229 self.text_wrapping = wrapping;
230 self
231 }
232
233 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
235 self.spacing = spacing.into().0;
236 self
237 }
238
239 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
243 self.font = Some(font.into());
244 self
245 }
246
247 #[must_use]
249 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
250 where
251 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
252 {
253 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
254 self
255 }
256
257 #[cfg(feature = "advanced")]
259 #[must_use]
260 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
261 self.class = class.into();
262 self
263 }
264
265 #[cfg(feature = "a11y")]
266 pub fn name(mut self, name: impl Into<Cow<'a, str>>) -> Self {
268 self.name = Some(name.into());
269 self
270 }
271
272 #[cfg(feature = "a11y")]
273 pub fn description_widget<T: iced_accessibility::Describes>(
275 mut self,
276 description: &T,
277 ) -> Self {
278 self.description = Some(iced_accessibility::Description::Id(
279 description.description(),
280 ));
281 self
282 }
283
284 #[cfg(feature = "a11y")]
285 pub fn description(mut self, description: impl Into<Cow<'a, str>>) -> Self {
287 self.description =
288 Some(iced_accessibility::Description::Text(description.into()));
289 self
290 }
291
292 #[cfg(feature = "a11y")]
293 pub fn labeled_by_widget(
295 mut self,
296 label: &dyn iced_accessibility::Labels,
297 ) -> Self {
298 self.labeled_by_widget =
299 Some(label.label().into_iter().map(|l| l.into()).collect());
300 self
301 }
302}
303
304impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
305 for Toggler<'a, Message, Theme, Renderer>
306where
307 Theme: Catalog,
308 Renderer: text::Renderer,
309{
310 fn tag(&self) -> tree::Tag {
311 tree::Tag::of::<widget::text::State<Renderer::Paragraph>>()
312 }
313
314 fn state(&self) -> tree::State {
315 tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
316 }
317
318 fn size(&self) -> Size<Length> {
319 Size {
320 width: self.width,
321 height: Length::Shrink,
322 }
323 }
324
325 fn layout(
326 &self,
327 tree: &mut Tree,
328 renderer: &Renderer,
329 limits: &layout::Limits,
330 ) -> layout::Node {
331 let limits = limits.width(self.width);
332
333 layout::next_to_each_other(
334 &limits,
335 self.spacing,
336 |_| layout::Node::new(crate::core::Size::new(48., 24.)),
337 |limits| {
338 if let Some(label) = self.label.as_deref() {
339 let state = tree
340 .state
341 .downcast_mut::<widget::text::State<Renderer::Paragraph>>();
342
343 widget::text::layout(
344 state,
345 renderer,
346 limits,
347 self.width,
348 Length::Shrink,
349 label,
350 self.text_line_height,
351 self.text_size,
352 self.font,
353 self.text_alignment,
354 alignment::Vertical::Top,
355 self.text_shaping,
356 self.text_wrapping,
357 )
358 } else {
359 layout::Node::new(crate::core::Size::ZERO)
360 }
361 },
362 )
363 }
364
365 fn on_event(
366 &mut self,
367 _state: &mut Tree,
368 event: Event,
369 layout: Layout<'_>,
370 cursor: mouse::Cursor,
371 _renderer: &Renderer,
372 _clipboard: &mut dyn Clipboard,
373 shell: &mut Shell<'_, Message>,
374 _viewport: &Rectangle,
375 ) -> event::Status {
376 let Some(on_toggle) = &self.on_toggle else {
377 return event::Status::Ignored;
378 };
379
380 match event {
381 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
382 | Event::Touch(touch::Event::FingerPressed { .. }) => {
383 let mouse_over = cursor.is_over(layout.bounds());
384
385 if mouse_over {
386 shell.publish(on_toggle(!self.is_toggled));
387
388 event::Status::Captured
389 } else {
390 event::Status::Ignored
391 }
392 }
393 _ => event::Status::Ignored,
394 }
395 }
396
397 fn mouse_interaction(
398 &self,
399 _state: &Tree,
400 layout: Layout<'_>,
401 cursor: mouse::Cursor,
402 _viewport: &Rectangle,
403 _renderer: &Renderer,
404 ) -> mouse::Interaction {
405 if cursor.is_over(layout.bounds()) {
406 if self.on_toggle.is_some() {
407 mouse::Interaction::Pointer
408 } else {
409 mouse::Interaction::NotAllowed
410 }
411 } else {
412 mouse::Interaction::default()
413 }
414 }
415
416 fn draw(
417 &self,
418 tree: &Tree,
419 renderer: &mut Renderer,
420 theme: &Theme,
421 style: &renderer::Style,
422 layout: Layout<'_>,
423 cursor: mouse::Cursor,
424 viewport: &Rectangle,
425 ) {
426 let mut children = layout.children();
427 let toggler_layout = children.next().unwrap();
428
429 if self.label.is_some() {
430 let label_layout = children.next().unwrap();
431 let state: &widget::text::State<Renderer::Paragraph> =
432 tree.state.downcast_ref();
433
434 crate::text::draw(
435 renderer,
436 style,
437 label_layout,
438 state.0.raw(),
439 crate::text::Style::default(),
440 viewport,
441 );
442 }
443
444 let bounds = toggler_layout.bounds();
445 let is_mouse_over = cursor.is_over(layout.bounds());
446
447 let status = if self.on_toggle.is_none() {
448 Status::Disabled
449 } else if is_mouse_over {
450 Status::Hovered {
451 is_toggled: self.is_toggled,
452 }
453 } else {
454 Status::Active {
455 is_toggled: self.is_toggled,
456 }
457 };
458
459 let style = theme.style(&self.class, status);
460
461 let space = style.handle_margin;
462
463 let toggler_background_bounds = Rectangle {
464 x: bounds.x,
465 y: bounds.y,
466 width: bounds.width,
467 height: bounds.height,
468 };
469
470 renderer.fill_quad(
471 renderer::Quad {
472 bounds: toggler_background_bounds,
473 border: Border {
474 radius: style.border_radius,
475 width: style.background_border_width,
476 color: style.background_border_color,
477 },
478 ..renderer::Quad::default()
479 },
480 style.background,
481 );
482
483 let toggler_foreground_bounds = Rectangle {
484 x: bounds.x
485 + if self.is_toggled {
486 bounds.width - space - (bounds.height - (2.0 * space))
487 } else {
488 space
489 },
490 y: bounds.y + space,
491 width: bounds.height - (2.0 * space),
492 height: bounds.height - (2.0 * space),
493 };
494
495 renderer.fill_quad(
496 renderer::Quad {
497 bounds: toggler_foreground_bounds,
498 border: Border {
499 radius: style.handle_radius,
500 width: style.foreground_border_width,
501 color: style.foreground_border_color,
502 },
503 ..renderer::Quad::default()
504 },
505 style.foreground,
506 );
507 }
508
509 #[cfg(feature = "a11y")]
510 fn a11y_nodes(
512 &self,
513 layout: Layout<'_>,
514 _state: &Tree,
515 cursor: mouse::Cursor,
516 ) -> iced_accessibility::A11yTree {
517 use iced_accessibility::{
518 accesskit::{Action, NodeBuilder, NodeId, Rect, Role},
519 A11yNode, A11yTree,
520 };
521
522 let bounds = layout.bounds();
523 let is_hovered = cursor.is_over(bounds);
524 let Rectangle {
525 x,
526 y,
527 width,
528 height,
529 } = bounds;
530
531 let bounds = Rect::new(
532 x as f64,
533 y as f64,
534 (x + width) as f64,
535 (y + height) as f64,
536 );
537
538 let mut node = NodeBuilder::new(Role::Switch);
539 node.add_action(Action::Focus);
540 node.add_action(Action::Default);
541 node.set_bounds(bounds);
542 if let Some(name) = self.name.as_ref() {
543 node.set_name(name.clone());
544 }
545 match self.description.as_ref() {
546 Some(iced_accessibility::Description::Id(id)) => {
547 node.set_described_by(
548 id.iter()
549 .cloned()
550 .map(|id| NodeId::from(id))
551 .collect::<Vec<_>>(),
552 );
553 }
554 Some(iced_accessibility::Description::Text(text)) => {
555 node.set_description(text.clone());
556 }
557 None => {}
558 }
559 node.set_selected(self.is_toggled);
560 if is_hovered {
561 node.set_hovered();
562 }
563 node.add_action(Action::Default);
564 if let Some(label) = self.label.as_ref() {
565 let mut label_node = NodeBuilder::new(Role::Label);
566
567 label_node.set_name(label.clone());
568 label_node.set_bounds(bounds);
570
571 A11yTree::node_with_child_tree(
572 A11yNode::new(node, self.id.clone()),
573 A11yTree::leaf(label_node, self.label_id.clone().unwrap()),
574 )
575 } else {
576 if let Some(labeled_by_widget) = self.labeled_by_widget.as_ref() {
577 node.set_labelled_by(labeled_by_widget.clone());
578 }
579 A11yTree::leaf(node, self.id.clone())
580 }
581 }
582
583 fn id(&self) -> Option<Id> {
584 if self.label.is_some() {
585 Some(Id(iced_runtime::core::id::Internal::Set(vec![
586 self.id.0.clone(),
587 self.label_id.clone().unwrap().0,
588 ])))
589 } else {
590 Some(self.id.clone())
591 }
592 }
593
594 fn set_id(&mut self, id: Id) {
595 if let Id(id::Internal::Set(list)) = id {
596 if list.len() == 2 && self.label.is_some() {
597 self.id.0 = list[0].clone();
598 self.label_id = Some(Id(list[1].clone()));
599 }
600 } else if self.label.is_none() {
601 self.id = id;
602 }
603 }
604}
605
606impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
607 for Element<'a, Message, Theme, Renderer>
608where
609 Message: 'a,
610 Theme: Catalog + 'a,
611 Renderer: text::Renderer + 'a,
612{
613 fn from(
614 toggler: Toggler<'a, Message, Theme, Renderer>,
615 ) -> Element<'a, Message, Theme, Renderer> {
616 Element::new(toggler)
617 }
618}
619
620#[derive(Debug, Clone, Copy, PartialEq, Eq)]
622pub enum Status {
623 Active {
625 is_toggled: bool,
627 },
628 Hovered {
630 is_toggled: bool,
632 },
633 Disabled,
635}
636
637#[derive(Debug, Clone, Copy, PartialEq)]
639pub struct Style {
640 pub background: Color,
642 pub background_border_width: f32,
644 pub background_border_color: Color,
646 pub foreground: Color,
648 pub foreground_border_width: f32,
650 pub foreground_border_color: Color,
652 pub border_radius: Radius,
654 pub handle_radius: Radius,
656 pub handle_margin: f32,
658}
659
660pub trait Catalog: Sized {
662 type Class<'a>;
664
665 fn default<'a>() -> Self::Class<'a>;
667
668 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
670}
671
672pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
676
677impl Catalog for Theme {
678 type Class<'a> = StyleFn<'a, Self>;
679
680 fn default<'a>() -> Self::Class<'a> {
681 Box::new(default)
682 }
683
684 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
685 class(self, status)
686 }
687}
688
689pub fn default(theme: &Theme, status: Status) -> Style {
691 let palette = theme.extended_palette();
692
693 let background = match status {
694 Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
695 if is_toggled {
696 palette.primary.strong.color
697 } else {
698 palette.background.strong.color
699 }
700 }
701 Status::Disabled => palette.background.weak.color,
702 };
703
704 let foreground = match status {
705 Status::Active { is_toggled } => {
706 if is_toggled {
707 palette.primary.strong.text
708 } else {
709 palette.background.base.color
710 }
711 }
712 Status::Hovered { is_toggled } => {
713 if is_toggled {
714 Color {
715 a: 0.5,
716 ..palette.primary.strong.text
717 }
718 } else {
719 palette.background.weak.color
720 }
721 }
722 Status::Disabled => palette.background.base.color,
723 };
724
725 Style {
726 background,
727 foreground,
728 foreground_border_width: 0.0,
729 foreground_border_color: Color::TRANSPARENT,
730 background_border_width: 0.0,
731 background_border_color: Color::TRANSPARENT,
732 border_radius: Radius::from(8.0),
733 handle_radius: Radius::from(8.0),
734 handle_margin: 2.0,
735 }
736}