1use iced::border::Radius;
12use iced::keyboard::key::Named;
13use iced::widget::{self, button::Status, container, row};
14use iced::{Background, Border, Element, Theme};
15
16use crate::icon::Glyph;
17use crate::keys::{self, Chord, Keymap};
18use crate::natural::natural;
19use crate::primitives::toggle::{self, Size};
20use crate::theme::{Tokens, fade, radius, space};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
24pub enum Mode {
25 #[default]
27 Single,
28 Multiple,
30}
31
32impl Mode {
33 pub const ALL: [Mode; 2] = [Mode::Single, Mode::Multiple];
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
38pub enum Variant {
39 #[default]
41 Default,
42 Outline,
44}
45
46impl Variant {
47 pub const ALL: [Variant; 2] = [Variant::Default, Variant::Outline];
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub struct Item<Id> {
53 pub id: Id,
54 pub label: String,
55 pub icon: Option<Glyph>,
56 pub disabled: bool,
57}
58
59pub fn item<Id>(id: Id, label: impl Into<String>) -> Item<Id> {
61 Item {
62 id,
63 label: label.into(),
64 icon: None,
65 disabled: false,
66 }
67}
68
69impl<Id> Item<Id> {
70 pub fn icon(mut self, glyph: Glyph) -> Self {
71 self.icon = Some(glyph);
72 self
73 }
74
75 pub fn disabled(mut self, disabled: bool) -> Self {
76 self.disabled = disabled;
77 self
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum Event<Id> {
84 Toggle(Id),
87 Next,
89 Previous,
92}
93
94#[derive(Debug, Clone, PartialEq)]
96pub struct State<Id> {
97 items: Vec<Item<Id>>,
98 mode: Mode,
99 required: bool,
100 selected: Vec<bool>,
101}
102
103impl<Id: Copy + PartialEq> State<Id> {
104 pub fn single(items: impl IntoIterator<Item = Item<Id>>) -> Self {
106 Self::new(items, Mode::Single)
107 }
108
109 pub fn multiple(items: impl IntoIterator<Item = Item<Id>>) -> Self {
111 Self::new(items, Mode::Multiple)
112 }
113
114 fn new(items: impl IntoIterator<Item = Item<Id>>, mode: Mode) -> Self {
115 let items: Vec<_> = items.into_iter().collect();
116 let selected = vec![false; items.len()];
117 Self {
118 items,
119 mode,
120 required: false,
121 selected,
122 }
123 }
124
125 pub fn with_selected(mut self, ids: impl IntoIterator<Item = Id>) -> Self {
128 let mut replaced = false;
129 for id in ids {
130 let Some(index) = self.enabled_index(id) else {
131 continue;
132 };
133 if self.mode == Mode::Single {
134 if replaced {
135 break;
136 }
137 self.selected.fill(false);
138 replaced = true;
139 }
140 self.selected[index] = true;
141 }
142 self
143 }
144
145 pub fn required(mut self, required: bool) -> Self {
149 self.required = required;
150 if required
151 && self.mode == Mode::Single
152 && !self.selected.contains(&true)
153 && let Some(index) = self.items.iter().position(|item| !item.disabled)
154 {
155 self.selected[index] = true;
156 }
157 self
158 }
159
160 pub fn mode(&self) -> Mode {
161 self.mode
162 }
163
164 pub fn is_required(&self) -> bool {
165 self.required
166 }
167
168 pub fn items(&self) -> &[Item<Id>] {
169 &self.items
170 }
171
172 pub fn selected(&self) -> Vec<Id> {
174 self.items
175 .iter()
176 .zip(&self.selected)
177 .filter_map(|(item, on)| on.then_some(item.id))
178 .collect()
179 }
180
181 pub fn selected_one(&self) -> Option<Id> {
183 let index = self.selected.iter().position(|on| *on)?;
184 Some(self.items[index].id)
185 }
186
187 pub fn is_selected(&self, id: Id) -> bool {
188 self.index(id).is_some_and(|index| self.selected[index])
189 }
190
191 pub fn set_disabled(&mut self, id: Id, disabled: bool) {
193 if let Some(item) = self.items.iter_mut().find(|item| item.id == id) {
194 item.disabled = disabled;
195 }
196 }
197
198 pub fn update(&mut self, event: Event<Id>) -> Option<Vec<Id>> {
201 let changed = match event {
202 Event::Toggle(id) => self.toggle(id),
203 Event::Next => self.step(true),
204 Event::Previous => self.step(false),
205 };
206 changed.then(|| self.selected())
207 }
208
209 fn toggle(&mut self, id: Id) -> bool {
210 let Some(index) = self.enabled_index(id) else {
211 return false;
212 };
213 let on = self.selected[index];
214 match self.mode {
215 Mode::Multiple => self.selected[index] = !on,
216 Mode::Single if on && self.required => return false,
217 Mode::Single => {
218 self.selected.fill(false);
219 self.selected[index] = !on;
220 }
221 }
222 true
223 }
224
225 fn step(&mut self, forward: bool) -> bool {
228 let len = self.items.len();
229 if self.mode != Mode::Single || len == 0 {
230 return false;
231 }
232 let current = self.selected.iter().position(|on| *on);
233 let start = current.unwrap_or(if forward { len - 1 } else { 0 });
234 let offset = if forward { 1 } else { len - 1 };
235 let Some(target) = (1..=len)
236 .map(|n| (start + offset * n) % len)
237 .find(|&index| !self.items[index].disabled)
238 else {
239 return false;
240 };
241 if current == Some(target) {
242 return false;
243 }
244 self.selected.fill(false);
245 self.selected[target] = true;
246 true
247 }
248
249 fn index(&self, id: Id) -> Option<usize> {
250 self.items.iter().position(|item| item.id == id)
251 }
252
253 fn enabled_index(&self, id: Id) -> Option<usize> {
254 self.index(id).filter(|&index| !self.items[index].disabled)
255 }
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
260pub enum Action {
261 Next,
262 Previous,
263}
264
265impl Action {
266 pub fn event<Id: Copy + PartialEq>(self, state: &State<Id>) -> Option<Event<Id>> {
269 if state.mode != Mode::Single {
270 return None;
271 }
272 Some(match self {
273 Action::Next => Event::Next,
274 Action::Previous => Event::Previous,
275 })
276 }
277}
278
279impl keys::Action for Action {
280 const ALL: &'static [Self] = &[Action::Next, Action::Previous];
281
282 fn defaults() -> Keymap<Self> {
283 default_keymap()
284 }
285
286 fn name(self) -> &'static str {
287 match self {
288 Action::Next => "Next",
289 Action::Previous => "Previous",
290 }
291 }
292
293 fn description(self) -> &'static str {
294 match self {
295 Action::Next => "Turns on the next enabled item of a single-choice group, wrapping.",
296 Action::Previous => {
297 "Turns on the previous enabled item of a single-choice group, wrapping."
298 }
299 }
300 }
301}
302
303pub fn default_keymap() -> Keymap<Action> {
314 Keymap::new()
315 .bind(Chord::named(Named::ArrowRight), Action::Next)
316 .bind(Chord::named(Named::ArrowLeft), Action::Previous)
317}
318
319pub struct ToggleGroup<'a, Id, Message> {
321 state: &'a State<Id>,
322 variant: Variant,
323 size: Size,
324 on_event: Option<Box<dyn Fn(Event<Id>) -> Message + 'a>>,
325}
326
327pub fn toggle_group<Id, Message>(state: &State<Id>) -> ToggleGroup<'_, Id, Message> {
330 ToggleGroup {
331 state,
332 variant: Variant::default(),
333 size: Size::default(),
334 on_event: None,
335 }
336}
337
338impl<Id: std::fmt::Debug, Message> std::fmt::Debug for ToggleGroup<'_, Id, Message> {
339 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
340 f.debug_struct("ToggleGroup")
341 .field("state", self.state)
342 .field("variant", &self.variant)
343 .field("size", &self.size)
344 .field("enabled", &self.on_event.is_some())
345 .finish()
346 }
347}
348
349impl<'a, Id, Message> ToggleGroup<'a, Id, Message> {
350 pub fn variant(mut self, variant: Variant) -> Self {
351 self.variant = variant;
352 self
353 }
354
355 pub fn size(mut self, size: Size) -> Self {
356 self.size = size;
357 self
358 }
359
360 pub fn on_event(mut self, on_event: impl Fn(Event<Id>) -> Message + 'a) -> Self {
361 self.on_event = Some(Box::new(on_event));
362 self
363 }
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
368pub enum Position {
369 Only,
370 First,
371 Middle,
372 Last,
373}
374
375impl Position {
376 fn of(index: usize, len: usize) -> Self {
377 match (index, len) {
378 (_, 1) => Position::Only,
379 (0, _) => Position::First,
380 (index, len) if index + 1 == len => Position::Last,
381 _ => Position::Middle,
382 }
383 }
384}
385
386impl<'a, Id, Message> From<ToggleGroup<'a, Id, Message>> for Element<'a, Message>
387where
388 Id: Copy + PartialEq + 'a,
389 Message: Clone + 'a,
390{
391 fn from(group: ToggleGroup<'a, Id, Message>) -> Self {
392 let ToggleGroup {
393 state,
394 variant,
395 size,
396 on_event,
397 } = group;
398 let len = state.items.len();
399 let metrics = match variant {
402 Variant::Default => size.metrics(),
403 Variant::Outline => toggle::Metrics {
404 height: size.metrics().height - 2.0 * BORDER,
405 ..size.metrics()
406 },
407 };
408
409 let items = state.items.iter().enumerate().map(|(index, entry)| {
410 let pressed = state.selected[index];
411 let message = on_event
412 .as_ref()
413 .filter(|_| !entry.disabled)
414 .map(|on_event| on_event(Event::Toggle(entry.id)));
415 let position = Position::of(index, len);
416 toggle::button(
417 entry.label.as_str(),
418 entry.icon,
419 pressed,
420 metrics,
421 message,
422 move |tokens, status| item_style(tokens, variant, position, pressed, status),
423 )
424 });
425
426 let enabled = on_event.is_some();
427 match variant {
431 Variant::Default => row(items.map(natural)).spacing(space::XS).wrap().into(),
432 Variant::Outline => natural(
433 container(row(items))
434 .padding(BORDER)
435 .style(move |theme: &Theme| group_style(&Tokens::of(theme), variant, enabled)),
436 ),
437 }
438 }
439}
440
441const BORDER: f32 = 1.0;
443
444pub fn item_style(
447 tokens: &Tokens,
448 variant: Variant,
449 position: Position,
450 pressed: bool,
451 status: Status,
452) -> widget::button::Style {
453 let base = toggle::style(tokens, toggle::Variant::Default, pressed, status);
454 if variant == Variant::Default {
455 return base;
456 }
457 let outer = radius::MD - BORDER;
460 let radius = match position {
461 Position::Only => Radius::from(outer),
462 Position::First => Radius::default().left(outer),
463 Position::Middle => Radius::default(),
464 Position::Last => Radius::default().right(outer),
465 };
466 widget::button::Style {
467 border: Border {
468 radius,
469 ..base.border
470 },
471 ..base
472 }
473}
474
475pub fn group_style(tokens: &Tokens, variant: Variant, enabled: bool) -> container::Style {
478 match variant {
479 Variant::Default => container::Style::default(),
480 Variant::Outline => container::Style {
481 background: Some(Background::Color(tokens.background)),
482 border: Border {
483 color: if enabled {
484 tokens.border
485 } else {
486 fade(tokens.border, 0.5)
487 },
488 width: BORDER,
489 radius: radius::MD.into(),
490 },
491 ..container::Style::default()
492 },
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use crate::theme::{dark, light};
500
501 fn items() -> [Item<u8>; 4] {
502 [
503 item(0, "A"),
504 item(1, "B").disabled(true),
505 item(2, "C"),
506 item(3, "D"),
507 ]
508 }
509
510 #[test]
511 fn groups_start_with_nothing_on() {
512 for state in [State::single(items()), State::multiple(items())] {
513 assert!(state.selected().is_empty());
514 assert_eq!(state.selected_one(), None);
515 assert!(!state.is_required());
516 }
517 assert_eq!(State::single(items()).mode(), Mode::Single);
518 assert_eq!(State::multiple(items()).mode(), Mode::Multiple);
519 }
520
521 #[test]
522 fn single_mode_moves_the_selection_and_can_clear_it() {
523 let mut state = State::single(items());
524 assert_eq!(state.update(Event::Toggle(0)), Some(vec![0]));
525 assert_eq!(state.update(Event::Toggle(3)), Some(vec![3]));
526 assert!(!state.is_selected(0));
527 assert_eq!(state.update(Event::Toggle(3)), Some(vec![]));
528 assert_eq!(state.selected_one(), None);
529 }
530
531 #[test]
532 fn required_single_mode_never_empties() {
533 let mut state = State::single(items()).required(true);
534 assert_eq!(state.selected(), vec![0]);
535 assert_eq!(state.update(Event::Toggle(0)), None);
536 assert_eq!(state.update(Event::Toggle(2)), Some(vec![2]));
537 assert_eq!(state.update(Event::Toggle(2)), None);
538
539 let kept = State::single(items()).with_selected([3]).required(true);
540 assert_eq!(kept.selected(), vec![3]);
541 let replaced = State::single(items()).required(true).with_selected([3]);
542 assert_eq!(replaced.selected(), vec![3]);
543 }
544
545 #[test]
546 fn multiple_mode_switches_items_independently() {
547 let mut state = State::multiple(items());
548 assert_eq!(state.update(Event::Toggle(0)), Some(vec![0]));
549 assert_eq!(state.update(Event::Toggle(3)), Some(vec![0, 3]));
550 assert_eq!(state.update(Event::Toggle(0)), Some(vec![3]));
551 assert_eq!(state.selected_one(), Some(3));
552 }
553
554 #[test]
555 fn disabled_and_unknown_items_do_not_change() {
556 for mut state in [State::single(items()), State::multiple(items())] {
557 assert_eq!(state.update(Event::Toggle(1)), None);
558 assert_eq!(state.update(Event::Toggle(9)), None);
559 assert!(state.selected().is_empty());
560 }
561 }
562
563 #[test]
564 fn with_selected_skips_disabled_and_keeps_one_in_single_mode() {
565 let single = State::single(items()).with_selected([1, 3, 0]);
566 assert_eq!(single.selected(), vec![3]);
567 let multiple = State::multiple(items()).with_selected([3, 1, 0]);
568 assert_eq!(multiple.selected(), vec![0, 3]);
569 }
570
571 #[test]
572 fn next_and_previous_skip_disabled_and_wrap() {
573 let mut state = State::single(items()).with_selected([0]);
574 assert_eq!(state.update(Event::Next), Some(vec![2]));
575 assert_eq!(state.update(Event::Next), Some(vec![3]));
576 assert_eq!(state.update(Event::Next), Some(vec![0]));
577 assert_eq!(state.update(Event::Previous), Some(vec![3]));
578 assert_eq!(state.update(Event::Previous), Some(vec![2]));
579 assert_eq!(state.update(Event::Previous), Some(vec![0]));
580 }
581
582 #[test]
583 fn next_from_nothing_picks_the_first_and_previous_the_last() {
584 let mut forward = State::single(items());
585 assert_eq!(forward.update(Event::Next), Some(vec![0]));
586 let mut backward = State::single(items());
587 assert_eq!(backward.update(Event::Previous), Some(vec![3]));
588 }
589
590 #[test]
591 fn stepping_does_nothing_in_multiple_mode_or_without_enabled_items() {
592 let mut multiple = State::multiple(items());
593 assert_eq!(multiple.update(Event::Next), None);
594 let mut empty: State<u8> = State::single([]);
595 assert_eq!(empty.update(Event::Next), None);
596 let mut disabled = State::single([item(0, "A").disabled(true)]);
597 assert_eq!(disabled.update(Event::Previous), None);
598 let mut lonely = State::single([item(0, "A")]).with_selected([0]);
599 assert_eq!(lonely.update(Event::Next), None);
600 }
601
602 #[test]
603 fn set_disabled_keeps_the_selection() {
604 let mut state = State::multiple(items()).with_selected([0]);
605 state.set_disabled(0, true);
606 assert!(state.is_selected(0));
607 assert_eq!(state.update(Event::Toggle(0)), None);
608 state.set_disabled(1, false);
609 assert_eq!(state.update(Event::Toggle(1)), Some(vec![0, 1]));
610 }
611
612 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
613 let chord: Chord = chord.parse().unwrap();
614 keymap.resolve(chord.key(), chord.modifiers())
615 }
616
617 #[test]
618 fn default_keymap_covers_every_action() {
619 let keymap = default_keymap();
620 assert_eq!(press(&keymap, "ArrowRight"), Some(Action::Next));
621 assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::Previous));
622 assert_eq!(press(&keymap, "Tab"), None);
623 for action in <Action as keys::Action>::ALL {
624 assert!(!keymap.chords(action).is_empty(), "{action:?}");
625 }
626 assert_eq!(Keymap::<Action>::defaults(), keymap);
627 }
628
629 #[test]
630 fn keymap_overrides_and_unbinding() {
631 let keymap = default_keymap()
632 .unbind(&Chord::named(Named::ArrowRight))
633 .bind("Alt+ArrowDown".parse().unwrap(), Action::Next);
634 assert_eq!(press(&keymap, "ArrowRight"), None);
635 assert_eq!(press(&keymap, "Alt+ArrowDown"), Some(Action::Next));
636 assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::Previous));
637 }
638
639 #[test]
640 fn actions_map_to_events_in_single_mode_only() {
641 let mut single = State::single(items()).with_selected([0]);
642 assert_eq!(Action::Next.event(&single), Some(Event::Next));
643 assert_eq!(Action::Previous.event(&single), Some(Event::Previous));
644 let event = Action::Next.event(&single).unwrap();
645 assert_eq!(single.update(event), Some(vec![2]));
646
647 let multiple = State::multiple(items());
648 assert_eq!(Action::Next.event(&multiple), None);
649 assert_eq!(Action::Previous.event(&multiple), None);
650 }
651
652 #[test]
653 fn positions_follow_the_item_index() {
654 assert_eq!(Position::of(0, 1), Position::Only);
655 assert_eq!(Position::of(0, 3), Position::First);
656 assert_eq!(Position::of(1, 3), Position::Middle);
657 assert_eq!(Position::of(2, 3), Position::Last);
658 }
659
660 #[test]
661 fn outlined_items_round_only_their_outer_corners() {
662 let tokens = Tokens::of(&light());
663 let corners = |position| {
664 item_style(&tokens, Variant::Outline, position, true, Status::Active)
665 .border
666 .radius
667 };
668 let outer = radius::MD - 1.0;
669 assert_eq!(corners(Position::Only), Radius::from(outer));
670 assert_eq!(corners(Position::Middle), Radius::default());
671 let first = corners(Position::First);
672 assert_eq!((first.top_left, first.top_right), (outer, 0.0));
673 let last = corners(Position::Last);
674 assert_eq!((last.top_left, last.bottom_right), (0.0, outer));
675 }
676
677 #[test]
678 fn items_never_draw_their_own_border() {
679 for theme in [light(), dark()] {
680 let tokens = Tokens::of(&theme);
681 for variant in Variant::ALL {
682 for pressed in [false, true] {
683 let style =
684 item_style(&tokens, variant, Position::Middle, pressed, Status::Hovered);
685 assert_eq!(style.border.width, 0.0);
686 }
687 }
688 }
689 }
690
691 #[test]
692 fn pressed_items_match_a_pressed_toggle() {
693 for theme in [light(), dark()] {
694 let tokens = Tokens::of(&theme);
695 let item = item_style(
696 &tokens,
697 Variant::Default,
698 Position::Only,
699 true,
700 Status::Active,
701 );
702 let toggle = toggle::style(&tokens, toggle::Variant::Default, true, Status::Active);
703 assert_eq!(item, toggle);
704 assert_eq!(item.background, Some(Background::Color(tokens.accent)));
705 }
706 }
707
708 #[test]
709 fn a_hovered_item_never_looks_selected() {
710 for theme in [light(), dark()] {
711 let tokens = Tokens::of(&theme);
712 for variant in Variant::ALL {
713 let hovered =
714 item_style(&tokens, variant, Position::Middle, false, Status::Hovered);
715 let selected = item_style(&tokens, variant, Position::Middle, true, Status::Active);
716 assert_ne!(hovered.background, selected.background, "{variant:?}");
717 assert_ne!(hovered.text_color, selected.text_color, "{variant:?}");
718 }
719 }
720 }
721
722 #[test]
723 fn only_the_outline_group_has_a_border_and_it_fades_when_disabled() {
724 let tokens = Tokens::of(&dark());
725 assert_eq!(
726 group_style(&tokens, Variant::Default, true).border.width,
727 0.0
728 );
729 let enabled = group_style(&tokens, Variant::Outline, true);
730 let disabled = group_style(&tokens, Variant::Outline, false);
731 assert_eq!(enabled.border.width, 1.0);
732 assert_eq!(enabled.border.color, tokens.border);
733 assert!(disabled.border.color.a < enabled.border.color.a);
734 }
735}