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, 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/// 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    on_event: Option<Box<dyn Fn(Event<Id>) -> Message + 'a>>,
325}
326
327/// Renders the items of `state`. Without
328/// [`on_event`](ToggleGroup::on_event) every item renders disabled.
329pub 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/// Where an item sits in an outlined group, which decides its corners.
367#[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        // An outlined group draws its border outside the items, so they
400        // shrink by the border to keep the group as tall as a button.
401        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        // Separate toggles wrap onto new lines on a narrow screen; attached
428        // segments stay in one row, so keep outlined groups to a few short
429        // items.
430        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
441/// Width of an outlined group's border.
442const BORDER: f32 = 1.0;
443
444/// The style of one item: a toggle, with square inner corners and no gap
445/// when the group is outlined.
446pub 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    // The items sit inside the group's border, so their outer corners are
458    // rounded by the border less than the group's.
459    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
475/// The container around the items: nothing for separate toggles, one
476/// border for an outlined group.
477pub 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}