1use iced::border::Radius;
12use iced::keyboard::key::Named;
13use iced::widget::{self, button::Status, container, row};
14use iced::{Background, Border, Element, Length, 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, mix, 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 width: Length,
325 height: Option<f32>,
326 on_event: Option<Box<dyn Fn(Event<Id>) -> Message + 'a>>,
327}
328
329pub fn toggle_group<Id, Message>(state: &State<Id>) -> ToggleGroup<'_, Id, Message> {
332 ToggleGroup {
333 state,
334 variant: Variant::default(),
335 size: Size::default(),
336 width: Length::Shrink,
337 height: None,
338 on_event: None,
339 }
340}
341
342impl<Id: std::fmt::Debug, Message> std::fmt::Debug for ToggleGroup<'_, Id, Message> {
343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 f.debug_struct("ToggleGroup")
345 .field("state", self.state)
346 .field("variant", &self.variant)
347 .field("size", &self.size)
348 .field("width", &self.width)
349 .field("height", &self.height)
350 .field("enabled", &self.on_event.is_some())
351 .finish()
352 }
353}
354
355impl<'a, Id, Message> ToggleGroup<'a, Id, Message> {
356 pub fn variant(mut self, variant: Variant) -> Self {
357 self.variant = variant;
358 self
359 }
360
361 pub fn size(mut self, size: Size) -> Self {
362 self.size = size;
363 self
364 }
365
366 pub fn width(mut self, width: impl Into<Length>) -> Self {
370 self.width = width.into();
371 self
372 }
373
374 pub fn height(mut self, height: f32) -> Self {
376 self.height = Some(height);
377 self
378 }
379
380 pub fn on_event(mut self, on_event: impl Fn(Event<Id>) -> Message + 'a) -> Self {
381 self.on_event = Some(Box::new(on_event));
382 self
383 }
384}
385
386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
388pub enum Position {
389 Only,
390 First,
391 Middle,
392 Last,
393}
394
395impl Position {
396 fn of(index: usize, len: usize) -> Self {
397 match (index, len) {
398 (_, 1) => Position::Only,
399 (0, _) => Position::First,
400 (index, len) if index + 1 == len => Position::Last,
401 _ => Position::Middle,
402 }
403 }
404}
405
406impl<'a, Id, Message> From<ToggleGroup<'a, Id, Message>> for Element<'a, Message>
407where
408 Id: Copy + PartialEq + 'a,
409 Message: Clone + 'a,
410{
411 fn from(group: ToggleGroup<'a, Id, Message>) -> Self {
412 let ToggleGroup {
413 state,
414 variant,
415 size,
416 width,
417 height,
418 on_event,
419 } = group;
420 let len = state.items.len();
421 let sized = toggle::Metrics {
422 height: height.unwrap_or(size.metrics().height),
423 ..size.metrics()
424 };
425 let shrink = width == Length::Shrink;
426 let item_width = if shrink {
427 Length::Shrink
428 } else {
429 Length::FillPortion(1)
430 };
431 let metrics = match variant {
434 Variant::Default => sized,
435 Variant::Outline => toggle::Metrics {
436 height: sized.height - 2.0 * BORDER,
437 ..sized
438 },
439 };
440
441 let items = state.items.iter().enumerate().map(|(index, entry)| {
442 let pressed = state.selected[index];
443 let message = on_event
444 .as_ref()
445 .filter(|_| !entry.disabled)
446 .map(|on_event| on_event(Event::Toggle(entry.id)));
447 let position = Position::of(index, len);
448 toggle::button(
449 entry.label.as_str(),
450 entry.icon,
451 pressed,
452 metrics,
453 item_width,
454 message,
455 move |tokens, status| item_style(tokens, variant, position, pressed, status),
456 )
457 });
458
459 let enabled = on_event.is_some();
460 match variant {
464 Variant::Default if shrink => row(items.map(natural)).spacing(space::XS).wrap().into(),
465 Variant::Default => row(items).spacing(space::XS).width(width).into(),
466 Variant::Outline => {
467 let mut segments = row![].width(if shrink { Length::Shrink } else { Length::Fill });
468 for (index, item) in items.enumerate() {
469 if index > 0 {
470 segments = segments.push(divider(metrics.height, enabled));
471 }
472 segments = segments.push(item);
473 }
474 let group = container(segments)
475 .padding(BORDER)
476 .width(width)
477 .style(move |theme: &Theme| group_style(&Tokens::of(theme), variant, enabled));
478 if shrink { natural(group) } else { group.into() }
479 }
480 }
481 }
482}
483
484fn divider<'a, Message: 'a>(height: f32, enabled: bool) -> Element<'a, Message> {
487 container(widget::space())
488 .width(BORDER)
489 .height(height)
490 .style(move |theme: &Theme| divider_style(&Tokens::of(theme), enabled))
491 .into()
492}
493
494pub fn divider_style(tokens: &Tokens, enabled: bool) -> container::Style {
496 let colour = if enabled {
497 tokens.border
498 } else {
499 fade(tokens.border, 0.5)
500 };
501 container::Style {
502 background: Some(Background::Color(colour)),
503 ..container::Style::default()
504 }
505}
506
507const BORDER: f32 = 1.0;
509
510pub fn item_style(
513 tokens: &Tokens,
514 variant: Variant,
515 position: Position,
516 pressed: bool,
517 status: Status,
518) -> widget::button::Style {
519 let base = toggle::style(tokens, toggle::Variant::Default, pressed, status);
520 if variant == Variant::Default {
521 return base;
522 }
523 let outer = radius::MD - BORDER;
526 let radius = match position {
527 Position::Only => Radius::from(outer),
528 Position::First => Radius::default().left(outer),
529 Position::Middle => Radius::default(),
530 Position::Last => Radius::default().right(outer),
531 };
532 widget::button::Style {
533 background: selected_background(tokens, pressed, status).or(base.background),
534 border: Border {
535 radius,
536 ..base.border
537 },
538 ..base
539 }
540}
541
542fn selected_background(tokens: &Tokens, pressed: bool, status: Status) -> Option<Background> {
546 if !pressed {
547 return None;
548 }
549 let amount = match status {
550 Status::Hovered | Status::Pressed => 0.18,
551 Status::Active => 0.1,
552 Status::Disabled => return None,
553 };
554 Some(Background::Color(mix(
555 tokens.accent,
556 tokens.foreground,
557 amount,
558 )))
559}
560
561pub fn group_style(tokens: &Tokens, variant: Variant, enabled: bool) -> container::Style {
564 match variant {
565 Variant::Default => container::Style::default(),
566 Variant::Outline => container::Style {
567 background: Some(Background::Color(tokens.background)),
568 border: Border {
569 color: if enabled {
570 tokens.border
571 } else {
572 fade(tokens.border, 0.5)
573 },
574 width: BORDER,
575 radius: radius::MD.into(),
576 },
577 ..container::Style::default()
578 },
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use crate::theme::{dark, light};
586
587 fn items() -> [Item<u8>; 4] {
588 [
589 item(0, "A"),
590 item(1, "B").disabled(true),
591 item(2, "C"),
592 item(3, "D"),
593 ]
594 }
595
596 #[test]
597 fn groups_start_with_nothing_on() {
598 for state in [State::single(items()), State::multiple(items())] {
599 assert!(state.selected().is_empty());
600 assert_eq!(state.selected_one(), None);
601 assert!(!state.is_required());
602 }
603 assert_eq!(State::single(items()).mode(), Mode::Single);
604 assert_eq!(State::multiple(items()).mode(), Mode::Multiple);
605 }
606
607 #[test]
608 fn single_mode_moves_the_selection_and_can_clear_it() {
609 let mut state = State::single(items());
610 assert_eq!(state.update(Event::Toggle(0)), Some(vec![0]));
611 assert_eq!(state.update(Event::Toggle(3)), Some(vec![3]));
612 assert!(!state.is_selected(0));
613 assert_eq!(state.update(Event::Toggle(3)), Some(vec![]));
614 assert_eq!(state.selected_one(), None);
615 }
616
617 #[test]
618 fn required_single_mode_never_empties() {
619 let mut state = State::single(items()).required(true);
620 assert_eq!(state.selected(), vec![0]);
621 assert_eq!(state.update(Event::Toggle(0)), None);
622 assert_eq!(state.update(Event::Toggle(2)), Some(vec![2]));
623 assert_eq!(state.update(Event::Toggle(2)), None);
624
625 let kept = State::single(items()).with_selected([3]).required(true);
626 assert_eq!(kept.selected(), vec![3]);
627 let replaced = State::single(items()).required(true).with_selected([3]);
628 assert_eq!(replaced.selected(), vec![3]);
629 }
630
631 #[test]
632 fn multiple_mode_switches_items_independently() {
633 let mut state = State::multiple(items());
634 assert_eq!(state.update(Event::Toggle(0)), Some(vec![0]));
635 assert_eq!(state.update(Event::Toggle(3)), Some(vec![0, 3]));
636 assert_eq!(state.update(Event::Toggle(0)), Some(vec![3]));
637 assert_eq!(state.selected_one(), Some(3));
638 }
639
640 #[test]
641 fn disabled_and_unknown_items_do_not_change() {
642 for mut state in [State::single(items()), State::multiple(items())] {
643 assert_eq!(state.update(Event::Toggle(1)), None);
644 assert_eq!(state.update(Event::Toggle(9)), None);
645 assert!(state.selected().is_empty());
646 }
647 }
648
649 #[test]
650 fn with_selected_skips_disabled_and_keeps_one_in_single_mode() {
651 let single = State::single(items()).with_selected([1, 3, 0]);
652 assert_eq!(single.selected(), vec![3]);
653 let multiple = State::multiple(items()).with_selected([3, 1, 0]);
654 assert_eq!(multiple.selected(), vec![0, 3]);
655 }
656
657 #[test]
658 fn next_and_previous_skip_disabled_and_wrap() {
659 let mut state = State::single(items()).with_selected([0]);
660 assert_eq!(state.update(Event::Next), Some(vec![2]));
661 assert_eq!(state.update(Event::Next), Some(vec![3]));
662 assert_eq!(state.update(Event::Next), Some(vec![0]));
663 assert_eq!(state.update(Event::Previous), Some(vec![3]));
664 assert_eq!(state.update(Event::Previous), Some(vec![2]));
665 assert_eq!(state.update(Event::Previous), Some(vec![0]));
666 }
667
668 #[test]
669 fn next_from_nothing_picks_the_first_and_previous_the_last() {
670 let mut forward = State::single(items());
671 assert_eq!(forward.update(Event::Next), Some(vec![0]));
672 let mut backward = State::single(items());
673 assert_eq!(backward.update(Event::Previous), Some(vec![3]));
674 }
675
676 #[test]
677 fn stepping_does_nothing_in_multiple_mode_or_without_enabled_items() {
678 let mut multiple = State::multiple(items());
679 assert_eq!(multiple.update(Event::Next), None);
680 let mut empty: State<u8> = State::single([]);
681 assert_eq!(empty.update(Event::Next), None);
682 let mut disabled = State::single([item(0, "A").disabled(true)]);
683 assert_eq!(disabled.update(Event::Previous), None);
684 let mut lonely = State::single([item(0, "A")]).with_selected([0]);
685 assert_eq!(lonely.update(Event::Next), None);
686 }
687
688 #[test]
689 fn set_disabled_keeps_the_selection() {
690 let mut state = State::multiple(items()).with_selected([0]);
691 state.set_disabled(0, true);
692 assert!(state.is_selected(0));
693 assert_eq!(state.update(Event::Toggle(0)), None);
694 state.set_disabled(1, false);
695 assert_eq!(state.update(Event::Toggle(1)), Some(vec![0, 1]));
696 }
697
698 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
699 let chord: Chord = chord.parse().unwrap();
700 keymap.resolve(chord.key(), chord.modifiers())
701 }
702
703 #[test]
704 fn default_keymap_covers_every_action() {
705 let keymap = default_keymap();
706 assert_eq!(press(&keymap, "ArrowRight"), Some(Action::Next));
707 assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::Previous));
708 assert_eq!(press(&keymap, "Tab"), None);
709 for action in <Action as keys::Action>::ALL {
710 assert!(!keymap.chords(action).is_empty(), "{action:?}");
711 }
712 assert_eq!(Keymap::<Action>::defaults(), keymap);
713 }
714
715 #[test]
716 fn keymap_overrides_and_unbinding() {
717 let keymap = default_keymap()
718 .unbind(&Chord::named(Named::ArrowRight))
719 .bind("Alt+ArrowDown".parse().unwrap(), Action::Next);
720 assert_eq!(press(&keymap, "ArrowRight"), None);
721 assert_eq!(press(&keymap, "Alt+ArrowDown"), Some(Action::Next));
722 assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::Previous));
723 }
724
725 #[test]
726 fn actions_map_to_events_in_single_mode_only() {
727 let mut single = State::single(items()).with_selected([0]);
728 assert_eq!(Action::Next.event(&single), Some(Event::Next));
729 assert_eq!(Action::Previous.event(&single), Some(Event::Previous));
730 let event = Action::Next.event(&single).unwrap();
731 assert_eq!(single.update(event), Some(vec![2]));
732
733 let multiple = State::multiple(items());
734 assert_eq!(Action::Next.event(&multiple), None);
735 assert_eq!(Action::Previous.event(&multiple), None);
736 }
737
738 #[test]
739 fn positions_follow_the_item_index() {
740 assert_eq!(Position::of(0, 1), Position::Only);
741 assert_eq!(Position::of(0, 3), Position::First);
742 assert_eq!(Position::of(1, 3), Position::Middle);
743 assert_eq!(Position::of(2, 3), Position::Last);
744 }
745
746 #[test]
747 fn outlined_items_round_only_their_outer_corners() {
748 let tokens = Tokens::of(&light());
749 let corners = |position| {
750 item_style(&tokens, Variant::Outline, position, true, Status::Active)
751 .border
752 .radius
753 };
754 let outer = radius::MD - 1.0;
755 assert_eq!(corners(Position::Only), Radius::from(outer));
756 assert_eq!(corners(Position::Middle), Radius::default());
757 let first = corners(Position::First);
758 assert_eq!((first.top_left, first.top_right), (outer, 0.0));
759 let last = corners(Position::Last);
760 assert_eq!((last.top_left, last.bottom_right), (0.0, outer));
761 }
762
763 #[test]
764 fn items_never_draw_their_own_border() {
765 for theme in [light(), dark()] {
766 let tokens = Tokens::of(&theme);
767 for variant in Variant::ALL {
768 for pressed in [false, true] {
769 let style =
770 item_style(&tokens, variant, Position::Middle, pressed, Status::Hovered);
771 assert_eq!(style.border.width, 0.0);
772 }
773 }
774 }
775 }
776
777 #[test]
778 fn pressed_items_match_a_pressed_toggle() {
779 for theme in [light(), dark()] {
780 let tokens = Tokens::of(&theme);
781 let item = item_style(
782 &tokens,
783 Variant::Default,
784 Position::Only,
785 true,
786 Status::Active,
787 );
788 let toggle = toggle::style(&tokens, toggle::Variant::Default, true, Status::Active);
789 assert_eq!(item, toggle);
790 assert_eq!(item.background, Some(Background::Color(tokens.accent)));
791 }
792 }
793
794 #[test]
795 fn a_hovered_item_never_looks_selected() {
796 for theme in [light(), dark()] {
797 let tokens = Tokens::of(&theme);
798 for variant in Variant::ALL {
799 let hovered =
800 item_style(&tokens, variant, Position::Middle, false, Status::Hovered);
801 let selected = item_style(&tokens, variant, Position::Middle, true, Status::Active);
802 assert_ne!(hovered.background, selected.background, "{variant:?}");
803 assert_ne!(hovered.text_color, selected.text_color, "{variant:?}");
804 }
805 }
806 }
807
808 #[test]
809 fn only_the_outline_group_has_a_border_and_it_fades_when_disabled() {
810 let tokens = Tokens::of(&dark());
811 assert_eq!(
812 group_style(&tokens, Variant::Default, true).border.width,
813 0.0
814 );
815 let enabled = group_style(&tokens, Variant::Outline, true);
816 let disabled = group_style(&tokens, Variant::Outline, false);
817 assert_eq!(enabled.border.width, 1.0);
818 assert_eq!(enabled.border.color, tokens.border);
819 assert!(disabled.border.color.a < enabled.border.color.a);
820 }
821
822 fn lightness(background: Option<Background>) -> f32 {
823 match background {
824 Some(Background::Color(colour)) => (colour.r + colour.g + colour.b) / 3.0,
825 _ => 0.0,
826 }
827 }
828
829 #[test]
830 fn an_outlined_selected_item_stands_out_more_than_the_plain_accent() {
831 for theme in [light(), dark()] {
832 let tokens = Tokens::of(&theme);
833 let selected = item_style(
834 &tokens,
835 Variant::Outline,
836 Position::First,
837 true,
838 Status::Active,
839 );
840 let plain = toggle::style(&tokens, toggle::Variant::Default, true, Status::Active);
841 let base = (tokens.background.r + tokens.background.g + tokens.background.b) / 3.0;
842 assert!(
843 (lightness(selected.background) - base).abs()
844 > (lightness(plain.background) - base).abs(),
845 "{theme:?}"
846 );
847 let hovered = item_style(
848 &tokens,
849 Variant::Outline,
850 Position::First,
851 true,
852 Status::Hovered,
853 );
854 assert_ne!(hovered.background, selected.background);
855 }
856 }
857
858 #[test]
859 fn dividers_use_the_border_and_fade_when_disabled() {
860 for theme in [light(), dark()] {
861 let tokens = Tokens::of(&theme);
862 let enabled = divider_style(&tokens, true);
863 assert_eq!(enabled.background, Some(Background::Color(tokens.border)));
864 let disabled = divider_style(&tokens, false);
865 assert_eq!(
866 disabled.background,
867 Some(Background::Color(fade(tokens.border, 0.5)))
868 );
869 }
870 }
871}