Skip to main content

iced_cube/primitives/
toggle_group.rs

1//! A row of toggles where one, or several, can be on.
2//!
3//! [`State`] owns the items and the selection. In [`Mode::Single`] pressing
4//! an item turns the others off, like a segmented control; in
5//! [`Mode::Multiple`] each item switches on and off by itself, like the
6//! formatting buttons of a text editor. Disabled items are skipped.
7//!
8//! Keyboard shortcuts resolve through a [`Keymap`] of [`Action`]s; see
9//! [`default_keymap`] and [`crate::keys`].
10
11use 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/// Whether one or several items can be on.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
24pub enum Mode {
25    /// At most one item is on. Pressing another moves the selection.
26    #[default]
27    Single,
28    /// Every item switches on and off by itself.
29    Multiple,
30}
31
32impl Mode {
33    pub const ALL: [Mode; 2] = [Mode::Single, Mode::Multiple];
34}
35
36/// How the group is drawn.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
38pub enum Variant {
39    /// Separate toggles with a small gap, transparent until hovered.
40    #[default]
41    Default,
42    /// Attached segments inside one border.
43    Outline,
44}
45
46impl Variant {
47    pub const ALL: [Variant; 2] = [Variant::Default, Variant::Outline];
48}
49
50/// One item in a [`State`].
51#[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
59/// Creates an enabled item.
60pub 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/// Changes to the selection.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum Event<Id> {
84    /// Switches an item on or off. In single mode, switching one on turns
85    /// the others off.
86    Toggle(Id),
87    /// Single mode: moves the selection to the next enabled item, wrapping.
88    Next,
89    /// Single mode: moves the selection to the previous enabled item,
90    /// wrapping.
91    Previous,
92}
93
94/// The items and which of them are on.
95#[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    /// A group where at most one item is on, starting with none.
105    pub fn single(items: impl IntoIterator<Item = Item<Id>>) -> Self {
106        Self::new(items, Mode::Single)
107    }
108
109    /// A group where every item switches on and off by itself.
110    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    /// Switches these items on, skipping unknown and disabled ones. In
126    /// single mode the first that applies replaces the selection.
127    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    /// Keeps the last item in a single-mode group on, so pressing it again
146    /// does nothing rather than leaving the group empty. When nothing is on
147    /// yet, the first enabled item is switched on.
148    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    /// The ids that are on, in item order.
173    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    /// The first id that is on, which in single mode is the only one.
182    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    /// Enables or disables an item. Whether it is on stays as it was.
192    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    /// Applies an event and returns the ids that are on afterwards, if
199    /// anything changed.
200    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    /// Moves a single-mode selection to the nearest enabled item in one
226    /// direction, wrapping round.
227    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/// What a toggle group keyboard shortcut does.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
260pub enum Action {
261    Next,
262    Previous,
263}
264
265impl Action {
266    /// The [`Event`] this action sends to `state`. Only single-mode groups
267    /// move with the keyboard, so a multiple-mode group gets `None`.
268    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
303/// The default toggle group shortcuts:
304///
305/// | Keys | Action |
306/// | --- | --- |
307/// | `ArrowRight` | [`Action::Next`] |
308/// | `ArrowLeft` | [`Action::Previous`] |
309///
310/// These are the keys tabs use, since a single-choice group switches views
311/// the same way. Shortcuts are app-wide, so an app showing tabs, a slider
312/// or a second group alongside should rebind one of them.
313pub 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
319/// A toggle group builder. Convert it into an [`Element`] to render.
320pub struct ToggleGroup<'a, Id, Message> {
321    state: &'a State<Id>,
322    variant: Variant,
323    size: Size,
324    width: Length,
325    on_event: Option<Box<dyn Fn(Event<Id>) -> Message + 'a>>,
326}
327
328/// Renders the items of `state`. Without
329/// [`on_event`](ToggleGroup::on_event) every item renders disabled.
330pub fn toggle_group<Id, Message>(state: &State<Id>) -> ToggleGroup<'_, Id, Message> {
331    ToggleGroup {
332        state,
333        variant: Variant::default(),
334        size: Size::default(),
335        width: Length::Shrink,
336        on_event: None,
337    }
338}
339
340impl<Id: std::fmt::Debug, Message> std::fmt::Debug for ToggleGroup<'_, Id, Message> {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        f.debug_struct("ToggleGroup")
343            .field("state", self.state)
344            .field("variant", &self.variant)
345            .field("size", &self.size)
346            .field("width", &self.width)
347            .field("enabled", &self.on_event.is_some())
348            .finish()
349    }
350}
351
352impl<'a, Id, Message> ToggleGroup<'a, Id, Message> {
353    pub fn variant(mut self, variant: Variant) -> Self {
354        self.variant = variant;
355        self
356    }
357
358    pub fn size(mut self, size: Size) -> Self {
359        self.size = size;
360        self
361    }
362
363    /// The group's width. The default shrinks to the items; any other
364    /// width, such as `Length::Fill`, shares it equally between the items
365    /// with their content centred, like a segmented control.
366    pub fn width(mut self, width: impl Into<Length>) -> Self {
367        self.width = width.into();
368        self
369    }
370
371    pub fn on_event(mut self, on_event: impl Fn(Event<Id>) -> Message + 'a) -> Self {
372        self.on_event = Some(Box::new(on_event));
373        self
374    }
375}
376
377/// Where an item sits in an outlined group, which decides its corners.
378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
379pub enum Position {
380    Only,
381    First,
382    Middle,
383    Last,
384}
385
386impl Position {
387    fn of(index: usize, len: usize) -> Self {
388        match (index, len) {
389            (_, 1) => Position::Only,
390            (0, _) => Position::First,
391            (index, len) if index + 1 == len => Position::Last,
392            _ => Position::Middle,
393        }
394    }
395}
396
397impl<'a, Id, Message> From<ToggleGroup<'a, Id, Message>> for Element<'a, Message>
398where
399    Id: Copy + PartialEq + 'a,
400    Message: Clone + 'a,
401{
402    fn from(group: ToggleGroup<'a, Id, Message>) -> Self {
403        let ToggleGroup {
404            state,
405            variant,
406            size,
407            width,
408            on_event,
409        } = group;
410        let len = state.items.len();
411        let shrink = width == Length::Shrink;
412        let item_width = if shrink {
413            Length::Shrink
414        } else {
415            Length::FillPortion(1)
416        };
417        // An outlined group draws its border outside the items, so they
418        // shrink by the border to keep the group as tall as a button.
419        let metrics = match variant {
420            Variant::Default => size.metrics(),
421            Variant::Outline => toggle::Metrics {
422                height: size.metrics().height - 2.0 * BORDER,
423                ..size.metrics()
424            },
425        };
426
427        let items = state.items.iter().enumerate().map(|(index, entry)| {
428            let pressed = state.selected[index];
429            let message = on_event
430                .as_ref()
431                .filter(|_| !entry.disabled)
432                .map(|on_event| on_event(Event::Toggle(entry.id)));
433            let position = Position::of(index, len);
434            toggle::button(
435                entry.label.as_str(),
436                entry.icon,
437                pressed,
438                metrics,
439                item_width,
440                message,
441                move |tokens, status| item_style(tokens, variant, position, pressed, status),
442            )
443        });
444
445        let enabled = on_event.is_some();
446        // Separate toggles wrap onto new lines on a narrow screen; attached
447        // segments stay in one row, so keep outlined groups to a few short
448        // items.
449        match variant {
450            Variant::Default if shrink => row(items.map(natural)).spacing(space::XS).wrap().into(),
451            Variant::Default => row(items).spacing(space::XS).width(width).into(),
452            Variant::Outline => {
453                let mut segments = row![].width(if shrink { Length::Shrink } else { Length::Fill });
454                for (index, item) in items.enumerate() {
455                    if index > 0 {
456                        segments = segments.push(divider(metrics.height, enabled));
457                    }
458                    segments = segments.push(item);
459                }
460                let group = container(segments)
461                    .padding(BORDER)
462                    .width(width)
463                    .style(move |theme: &Theme| group_style(&Tokens::of(theme), variant, enabled));
464                if shrink { natural(group) } else { group.into() }
465            }
466        }
467    }
468}
469
470/// The line between two segments of an outlined group, so every item reads
471/// as outlined whether or not it is on.
472fn divider<'a, Message: 'a>(height: f32, enabled: bool) -> Element<'a, Message> {
473    container(widget::space())
474        .width(BORDER)
475        .height(height)
476        .style(move |theme: &Theme| divider_style(&Tokens::of(theme), enabled))
477        .into()
478}
479
480/// The line between segments: the group's border colour.
481pub fn divider_style(tokens: &Tokens, enabled: bool) -> container::Style {
482    let colour = if enabled {
483        tokens.border
484    } else {
485        fade(tokens.border, 0.5)
486    };
487    container::Style {
488        background: Some(Background::Color(colour)),
489        ..container::Style::default()
490    }
491}
492
493/// Width of an outlined group's border.
494const BORDER: f32 = 1.0;
495
496/// The style of one item: a toggle, with square inner corners and no gap
497/// when the group is outlined.
498pub fn item_style(
499    tokens: &Tokens,
500    variant: Variant,
501    position: Position,
502    pressed: bool,
503    status: Status,
504) -> widget::button::Style {
505    let base = toggle::style(tokens, toggle::Variant::Default, pressed, status);
506    if variant == Variant::Default {
507        return base;
508    }
509    // The items sit inside the group's border, so their outer corners are
510    // rounded by the border less than the group's.
511    let outer = radius::MD - BORDER;
512    let radius = match position {
513        Position::Only => Radius::from(outer),
514        Position::First => Radius::default().left(outer),
515        Position::Middle => Radius::default(),
516        Position::Last => Radius::default().right(outer),
517    };
518    widget::button::Style {
519        background: selected_background(tokens, pressed, status).or(base.background),
520        border: Border {
521            radius,
522            ..base.border
523        },
524        ..base
525    }
526}
527
528/// An outlined segment that is on takes the accent pulled towards the
529/// foreground, so it stands out from its neighbours in dark themes too,
530/// where the plain accent is close to the background.
531fn selected_background(tokens: &Tokens, pressed: bool, status: Status) -> Option<Background> {
532    if !pressed {
533        return None;
534    }
535    let amount = match status {
536        Status::Hovered | Status::Pressed => 0.18,
537        Status::Active => 0.1,
538        Status::Disabled => return None,
539    };
540    Some(Background::Color(mix(
541        tokens.accent,
542        tokens.foreground,
543        amount,
544    )))
545}
546
547/// The container around the items: nothing for separate toggles, one
548/// border for an outlined group.
549pub fn group_style(tokens: &Tokens, variant: Variant, enabled: bool) -> container::Style {
550    match variant {
551        Variant::Default => container::Style::default(),
552        Variant::Outline => container::Style {
553            background: Some(Background::Color(tokens.background)),
554            border: Border {
555                color: if enabled {
556                    tokens.border
557                } else {
558                    fade(tokens.border, 0.5)
559                },
560                width: BORDER,
561                radius: radius::MD.into(),
562            },
563            ..container::Style::default()
564        },
565    }
566}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571    use crate::theme::{dark, light};
572
573    fn items() -> [Item<u8>; 4] {
574        [
575            item(0, "A"),
576            item(1, "B").disabled(true),
577            item(2, "C"),
578            item(3, "D"),
579        ]
580    }
581
582    #[test]
583    fn groups_start_with_nothing_on() {
584        for state in [State::single(items()), State::multiple(items())] {
585            assert!(state.selected().is_empty());
586            assert_eq!(state.selected_one(), None);
587            assert!(!state.is_required());
588        }
589        assert_eq!(State::single(items()).mode(), Mode::Single);
590        assert_eq!(State::multiple(items()).mode(), Mode::Multiple);
591    }
592
593    #[test]
594    fn single_mode_moves_the_selection_and_can_clear_it() {
595        let mut state = State::single(items());
596        assert_eq!(state.update(Event::Toggle(0)), Some(vec![0]));
597        assert_eq!(state.update(Event::Toggle(3)), Some(vec![3]));
598        assert!(!state.is_selected(0));
599        assert_eq!(state.update(Event::Toggle(3)), Some(vec![]));
600        assert_eq!(state.selected_one(), None);
601    }
602
603    #[test]
604    fn required_single_mode_never_empties() {
605        let mut state = State::single(items()).required(true);
606        assert_eq!(state.selected(), vec![0]);
607        assert_eq!(state.update(Event::Toggle(0)), None);
608        assert_eq!(state.update(Event::Toggle(2)), Some(vec![2]));
609        assert_eq!(state.update(Event::Toggle(2)), None);
610
611        let kept = State::single(items()).with_selected([3]).required(true);
612        assert_eq!(kept.selected(), vec![3]);
613        let replaced = State::single(items()).required(true).with_selected([3]);
614        assert_eq!(replaced.selected(), vec![3]);
615    }
616
617    #[test]
618    fn multiple_mode_switches_items_independently() {
619        let mut state = State::multiple(items());
620        assert_eq!(state.update(Event::Toggle(0)), Some(vec![0]));
621        assert_eq!(state.update(Event::Toggle(3)), Some(vec![0, 3]));
622        assert_eq!(state.update(Event::Toggle(0)), Some(vec![3]));
623        assert_eq!(state.selected_one(), Some(3));
624    }
625
626    #[test]
627    fn disabled_and_unknown_items_do_not_change() {
628        for mut state in [State::single(items()), State::multiple(items())] {
629            assert_eq!(state.update(Event::Toggle(1)), None);
630            assert_eq!(state.update(Event::Toggle(9)), None);
631            assert!(state.selected().is_empty());
632        }
633    }
634
635    #[test]
636    fn with_selected_skips_disabled_and_keeps_one_in_single_mode() {
637        let single = State::single(items()).with_selected([1, 3, 0]);
638        assert_eq!(single.selected(), vec![3]);
639        let multiple = State::multiple(items()).with_selected([3, 1, 0]);
640        assert_eq!(multiple.selected(), vec![0, 3]);
641    }
642
643    #[test]
644    fn next_and_previous_skip_disabled_and_wrap() {
645        let mut state = State::single(items()).with_selected([0]);
646        assert_eq!(state.update(Event::Next), Some(vec![2]));
647        assert_eq!(state.update(Event::Next), Some(vec![3]));
648        assert_eq!(state.update(Event::Next), Some(vec![0]));
649        assert_eq!(state.update(Event::Previous), Some(vec![3]));
650        assert_eq!(state.update(Event::Previous), Some(vec![2]));
651        assert_eq!(state.update(Event::Previous), Some(vec![0]));
652    }
653
654    #[test]
655    fn next_from_nothing_picks_the_first_and_previous_the_last() {
656        let mut forward = State::single(items());
657        assert_eq!(forward.update(Event::Next), Some(vec![0]));
658        let mut backward = State::single(items());
659        assert_eq!(backward.update(Event::Previous), Some(vec![3]));
660    }
661
662    #[test]
663    fn stepping_does_nothing_in_multiple_mode_or_without_enabled_items() {
664        let mut multiple = State::multiple(items());
665        assert_eq!(multiple.update(Event::Next), None);
666        let mut empty: State<u8> = State::single([]);
667        assert_eq!(empty.update(Event::Next), None);
668        let mut disabled = State::single([item(0, "A").disabled(true)]);
669        assert_eq!(disabled.update(Event::Previous), None);
670        let mut lonely = State::single([item(0, "A")]).with_selected([0]);
671        assert_eq!(lonely.update(Event::Next), None);
672    }
673
674    #[test]
675    fn set_disabled_keeps_the_selection() {
676        let mut state = State::multiple(items()).with_selected([0]);
677        state.set_disabled(0, true);
678        assert!(state.is_selected(0));
679        assert_eq!(state.update(Event::Toggle(0)), None);
680        state.set_disabled(1, false);
681        assert_eq!(state.update(Event::Toggle(1)), Some(vec![0, 1]));
682    }
683
684    fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
685        let chord: Chord = chord.parse().unwrap();
686        keymap.resolve(chord.key(), chord.modifiers())
687    }
688
689    #[test]
690    fn default_keymap_covers_every_action() {
691        let keymap = default_keymap();
692        assert_eq!(press(&keymap, "ArrowRight"), Some(Action::Next));
693        assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::Previous));
694        assert_eq!(press(&keymap, "Tab"), None);
695        for action in <Action as keys::Action>::ALL {
696            assert!(!keymap.chords(action).is_empty(), "{action:?}");
697        }
698        assert_eq!(Keymap::<Action>::defaults(), keymap);
699    }
700
701    #[test]
702    fn keymap_overrides_and_unbinding() {
703        let keymap = default_keymap()
704            .unbind(&Chord::named(Named::ArrowRight))
705            .bind("Alt+ArrowDown".parse().unwrap(), Action::Next);
706        assert_eq!(press(&keymap, "ArrowRight"), None);
707        assert_eq!(press(&keymap, "Alt+ArrowDown"), Some(Action::Next));
708        assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::Previous));
709    }
710
711    #[test]
712    fn actions_map_to_events_in_single_mode_only() {
713        let mut single = State::single(items()).with_selected([0]);
714        assert_eq!(Action::Next.event(&single), Some(Event::Next));
715        assert_eq!(Action::Previous.event(&single), Some(Event::Previous));
716        let event = Action::Next.event(&single).unwrap();
717        assert_eq!(single.update(event), Some(vec![2]));
718
719        let multiple = State::multiple(items());
720        assert_eq!(Action::Next.event(&multiple), None);
721        assert_eq!(Action::Previous.event(&multiple), None);
722    }
723
724    #[test]
725    fn positions_follow_the_item_index() {
726        assert_eq!(Position::of(0, 1), Position::Only);
727        assert_eq!(Position::of(0, 3), Position::First);
728        assert_eq!(Position::of(1, 3), Position::Middle);
729        assert_eq!(Position::of(2, 3), Position::Last);
730    }
731
732    #[test]
733    fn outlined_items_round_only_their_outer_corners() {
734        let tokens = Tokens::of(&light());
735        let corners = |position| {
736            item_style(&tokens, Variant::Outline, position, true, Status::Active)
737                .border
738                .radius
739        };
740        let outer = radius::MD - 1.0;
741        assert_eq!(corners(Position::Only), Radius::from(outer));
742        assert_eq!(corners(Position::Middle), Radius::default());
743        let first = corners(Position::First);
744        assert_eq!((first.top_left, first.top_right), (outer, 0.0));
745        let last = corners(Position::Last);
746        assert_eq!((last.top_left, last.bottom_right), (0.0, outer));
747    }
748
749    #[test]
750    fn items_never_draw_their_own_border() {
751        for theme in [light(), dark()] {
752            let tokens = Tokens::of(&theme);
753            for variant in Variant::ALL {
754                for pressed in [false, true] {
755                    let style =
756                        item_style(&tokens, variant, Position::Middle, pressed, Status::Hovered);
757                    assert_eq!(style.border.width, 0.0);
758                }
759            }
760        }
761    }
762
763    #[test]
764    fn pressed_items_match_a_pressed_toggle() {
765        for theme in [light(), dark()] {
766            let tokens = Tokens::of(&theme);
767            let item = item_style(
768                &tokens,
769                Variant::Default,
770                Position::Only,
771                true,
772                Status::Active,
773            );
774            let toggle = toggle::style(&tokens, toggle::Variant::Default, true, Status::Active);
775            assert_eq!(item, toggle);
776            assert_eq!(item.background, Some(Background::Color(tokens.accent)));
777        }
778    }
779
780    #[test]
781    fn a_hovered_item_never_looks_selected() {
782        for theme in [light(), dark()] {
783            let tokens = Tokens::of(&theme);
784            for variant in Variant::ALL {
785                let hovered =
786                    item_style(&tokens, variant, Position::Middle, false, Status::Hovered);
787                let selected = item_style(&tokens, variant, Position::Middle, true, Status::Active);
788                assert_ne!(hovered.background, selected.background, "{variant:?}");
789                assert_ne!(hovered.text_color, selected.text_color, "{variant:?}");
790            }
791        }
792    }
793
794    #[test]
795    fn only_the_outline_group_has_a_border_and_it_fades_when_disabled() {
796        let tokens = Tokens::of(&dark());
797        assert_eq!(
798            group_style(&tokens, Variant::Default, true).border.width,
799            0.0
800        );
801        let enabled = group_style(&tokens, Variant::Outline, true);
802        let disabled = group_style(&tokens, Variant::Outline, false);
803        assert_eq!(enabled.border.width, 1.0);
804        assert_eq!(enabled.border.color, tokens.border);
805        assert!(disabled.border.color.a < enabled.border.color.a);
806    }
807
808    fn lightness(background: Option<Background>) -> f32 {
809        match background {
810            Some(Background::Color(colour)) => (colour.r + colour.g + colour.b) / 3.0,
811            _ => 0.0,
812        }
813    }
814
815    #[test]
816    fn an_outlined_selected_item_stands_out_more_than_the_plain_accent() {
817        for theme in [light(), dark()] {
818            let tokens = Tokens::of(&theme);
819            let selected = item_style(
820                &tokens,
821                Variant::Outline,
822                Position::First,
823                true,
824                Status::Active,
825            );
826            let plain = toggle::style(&tokens, toggle::Variant::Default, true, Status::Active);
827            let base = (tokens.background.r + tokens.background.g + tokens.background.b) / 3.0;
828            assert!(
829                (lightness(selected.background) - base).abs()
830                    > (lightness(plain.background) - base).abs(),
831                "{theme:?}"
832            );
833            let hovered = item_style(
834                &tokens,
835                Variant::Outline,
836                Position::First,
837                true,
838                Status::Hovered,
839            );
840            assert_ne!(hovered.background, selected.background);
841        }
842    }
843
844    #[test]
845    fn dividers_use_the_border_and_fade_when_disabled() {
846        for theme in [light(), dark()] {
847            let tokens = Tokens::of(&theme);
848            let enabled = divider_style(&tokens, true);
849            assert_eq!(enabled.background, Some(Background::Color(tokens.border)));
850            let disabled = divider_style(&tokens, false);
851            assert_eq!(
852                disabled.background,
853                Some(Background::Color(fade(tokens.border, 0.5)))
854            );
855        }
856    }
857}