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    height: Option<f32>,
326    on_event: Option<Box<dyn Fn(Event<Id>) -> Message + 'a>>,
327}
328
329/// Renders the items of `state`. Without
330/// [`on_event`](ToggleGroup::on_event) every item renders disabled.
331pub 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    /// The group's width. The default shrinks to the items; any other
367    /// width, such as `Length::Fill`, shares it equally between the items
368    /// with their content centred, like a segmented control.
369    pub fn width(mut self, width: impl Into<Length>) -> Self {
370        self.width = width.into();
371        self
372    }
373
374    /// The group's height, overriding the size's. A touch screen wants 48.
375    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/// Where an item sits in an outlined group, which decides its corners.
387#[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        // An outlined group draws its border outside the items, so they
432        // shrink by the border to keep the group as tall as a button.
433        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        // Separate toggles wrap onto new lines on a narrow screen; attached
461        // segments stay in one row, so keep outlined groups to a few short
462        // items.
463        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
484/// The line between two segments of an outlined group, so every item reads
485/// as outlined whether or not it is on.
486fn 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
494/// The line between segments: the group's border colour.
495pub 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
507/// Width of an outlined group's border.
508const BORDER: f32 = 1.0;
509
510/// The style of one item: a toggle, with square inner corners and no gap
511/// when the group is outlined.
512pub 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    // The items sit inside the group's border, so their outer corners are
524    // rounded by the border less than the group's.
525    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
542/// An outlined segment that is on takes the accent pulled towards the
543/// foreground, so it stands out from its neighbours in dark themes too,
544/// where the plain accent is close to the background.
545fn 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
561/// The container around the items: nothing for separate toggles, one
562/// border for an outlined group.
563pub 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}