Skip to main content

gpui_component/button/
toggle.rs

1use std::{cell::Cell, rc::Rc, time::Duration};
2
3use gpui::{
4    AnyElement, App, Axis, Corners, Edges, ElementId, InteractiveElement, IntoElement,
5    ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
6    Window, prelude::FluentBuilder as _, relative,
7};
8use gpui_base::{Toggle as BaseToggle, ToggleGroup as BaseToggleGroup};
9use smallvec::{SmallVec, smallvec};
10
11use crate::{ActiveTheme, Disableable, Icon, Sizable, Size, StyledExt, tooltip::ComponentTooltip};
12
13#[derive(Default, Copy, Debug, Clone, PartialEq, Eq, Hash)]
14pub enum ToggleVariant {
15    #[default]
16    Ghost,
17    Outline,
18}
19
20pub trait ToggleVariants: Sized {
21    /// Set the variant of the toggle.
22    fn with_variant(self, variant: ToggleVariant) -> Self;
23    /// Set the variant to ghost.
24    fn ghost(self) -> Self {
25        self.with_variant(ToggleVariant::Ghost)
26    }
27    /// Set the variant to outline.
28    fn outline(self) -> Self {
29        self.with_variant(ToggleVariant::Outline)
30    }
31}
32
33#[derive(IntoElement)]
34pub struct Toggle {
35    id: ElementId,
36    style: StyleRefinement,
37    checked: bool,
38    size: Size,
39    variant: ToggleVariant,
40    disabled: bool,
41    border_corners: Corners<bool>,
42    border_edges: Edges<bool>,
43    children: SmallVec<[AnyElement; 1]>,
44    on_click: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
45    tooltip: ComponentTooltip,
46}
47
48impl Toggle {
49    /// Create a new Toggle element.
50    pub fn new(id: impl Into<ElementId>) -> Self {
51        Self {
52            id: id.into(),
53            style: StyleRefinement::default(),
54            checked: false,
55            size: Size::default(),
56            variant: ToggleVariant::default(),
57            disabled: false,
58            border_corners: Corners {
59                top_left: true,
60                top_right: true,
61                bottom_left: true,
62                bottom_right: true,
63            },
64            border_edges: Edges::all(true),
65            children: smallvec![],
66            on_click: None,
67            tooltip: ComponentTooltip::default(),
68        }
69    }
70
71    /// Set tooltip text for the toggle.
72    pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
73        self.tooltip.text = Some((tooltip.into(), None));
74        self
75    }
76
77    /// Overrides how long the pointer must rest before the tooltip shows.
78    ///
79    /// Defaults to [`gpui_base::TooltipDefaults::show_delay`].
80    pub fn tooltip_show_delay(mut self, delay: Duration) -> Self {
81        self.tooltip.show_delay = Some(delay);
82        self
83    }
84
85    /// Add a label to the toggle.
86    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
87        let label: SharedString = label.into();
88        self.children.push(label.into_any_element());
89        self
90    }
91
92    /// Add icon to the toggle.
93    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
94        let icon: Icon = icon.into();
95        self.children.push(icon.into());
96        self
97    }
98
99    /// Set the checked state of the toggle, default: false
100    pub fn checked(mut self, checked: bool) -> Self {
101        self.checked = checked;
102        self
103    }
104
105    /// Set the callback to be called when the toggle is clicked.
106    ///
107    /// The `&bool` parameter represents the new checked state of the toggle.
108    pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
109        self.on_click = Some(Box::new(handler));
110        self
111    }
112
113    pub(crate) fn border_corners(mut self, corners: impl Into<Corners<bool>>) -> Self {
114        self.border_corners = corners.into();
115        self
116    }
117
118    pub(crate) fn border_edges(mut self, edges: impl Into<Edges<bool>>) -> Self {
119        self.border_edges = edges.into();
120        self
121    }
122}
123
124impl ToggleVariants for Toggle {
125    fn with_variant(mut self, variant: ToggleVariant) -> Self {
126        self.variant = variant;
127        self
128    }
129}
130
131impl ParentElement for Toggle {
132    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
133        self.children.extend(elements);
134    }
135}
136
137impl Disableable for Toggle {
138    fn disabled(mut self, disabled: bool) -> Self {
139        self.disabled = disabled;
140        self
141    }
142}
143
144impl Sizable for Toggle {
145    fn with_size(mut self, size: impl Into<Size>) -> Self {
146        self.size = size.into();
147        self
148    }
149}
150
151impl Styled for Toggle {
152    fn style(&mut self) -> &mut StyleRefinement {
153        &mut self.style
154    }
155}
156
157impl RenderOnce for Toggle {
158    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
159        let checked = self.checked;
160        let disabled = self.disabled;
161        let hoverable = !disabled && !checked;
162        let rounding = cx.theme().radius;
163        let pressed_background = cx.theme().tokens.accent;
164        let pressed_foreground = cx.theme().accent_foreground;
165        let instance_style = self.style.clone();
166
167        BaseToggle::new(self.id)
168            .pressed(checked)
169            .disabled(disabled)
170            .when_some(
171                self.tooltip.text.as_ref().map(|(text, _)| text.clone()),
172                |this, label| this.accessibility_label(label),
173            )
174            .when_some(self.on_click, |this, on_click| {
175                this.on_change(move |next, _, window, cx| on_click(&next, window, cx))
176            })
177            .flex()
178            .flex_row()
179            .line_height(relative(1.25))
180            .items_center()
181            .justify_center()
182            .map(|this| match self.size {
183                Size::XSmall => this.min_w_5().h_5().px_0p5().text_xs(),
184                Size::Small => this.min_w_6().h_6().px_1().text_sm(),
185                Size::Large => this.min_w_9().h_9().px_3().text_base(),
186                _ => this.min_w_8().h_8().px_2().text_sm(),
187            })
188            .when(self.border_corners.top_left, |this| {
189                this.rounded_tl(rounding)
190            })
191            .when(self.border_corners.top_right, |this| {
192                this.rounded_tr(rounding)
193            })
194            .when(self.border_corners.bottom_left, |this| {
195                this.rounded_bl(rounding)
196            })
197            .when(self.border_corners.bottom_right, |this| {
198                this.rounded_br(rounding)
199            })
200            .when(self.variant == ToggleVariant::Outline, |this| {
201                this.when(self.border_edges.left, |this| this.border_l_1())
202                    .when(self.border_edges.right, |this| this.border_r_1())
203                    .when(self.border_edges.top, |this| this.border_t_1())
204                    .when(self.border_edges.bottom, |this| this.border_b_1())
205                    .border_color(cx.theme().border)
206                    .bg(cx.theme().tokens.background)
207            })
208            // Always register the hover style: GPUI only refreshes the cached
209            // hover state while one is present. If a checked toggle skipped it,
210            // the stale state would paint its label in the hover color once
211            // something else unchecks it while the pointer rests elsewhere.
212            .hover(|this| {
213                if !hoverable {
214                    return this;
215                }
216                this.bg(cx.theme().tokens.accent)
217                    .text_color(cx.theme().accent_foreground)
218            })
219            .styles(|styles| {
220                styles.pressed(|style| {
221                    style
222                        .bg(pressed_background)
223                        .text_color(pressed_foreground)
224                        .refine_style(&instance_style)
225                })
226            })
227            .refine_style(&self.style)
228            .children(self.children)
229            .map(|this| self.tooltip.apply(this))
230    }
231}
232
233/// A group of toggles.
234#[derive(IntoElement)]
235pub struct ToggleGroup {
236    id: ElementId,
237    style: StyleRefinement,
238    size: Size,
239    variant: ToggleVariant,
240    disabled: bool,
241    segmented: bool,
242    single: bool,
243    items: Vec<Toggle>,
244    on_click: Option<Rc<dyn Fn(&Vec<bool>, &mut Window, &mut App) + 'static>>,
245}
246
247impl ToggleGroup {
248    /// Create a new ToggleGroup element.
249    pub fn new(id: impl Into<ElementId>) -> Self {
250        Self {
251            id: id.into(),
252            style: StyleRefinement::default(),
253            size: Size::default(),
254            variant: ToggleVariant::default(),
255            disabled: false,
256            segmented: false,
257            single: false,
258            items: Vec::new(),
259            on_click: None,
260        }
261    }
262
263    /// Add a child [`Toggle`] to the group.
264    pub fn child(mut self, toggle: impl Into<Toggle>) -> Self {
265        self.items.push(toggle.into());
266        self
267    }
268
269    /// Add multiple [`Toggle`]s to the group.
270    pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Toggle>>) -> Self {
271        self.items.extend(children.into_iter().map(Into::into));
272        self
273    }
274
275    /// Set the callback to be called when the toggle group changes.
276    ///
277    /// The `&Vec<bool>` parameter represents the new check state of each [`Toggle`] in the group.
278    /// In a [`single`](Self::single) group at most one of them is `true`.
279    pub fn on_click(
280        mut self,
281        on_click: impl Fn(&Vec<bool>, &mut Window, &mut App) + 'static,
282    ) -> Self {
283        self.on_click = Some(Rc::new(on_click));
284        self
285    }
286
287    /// Render the group as a connected segmented control.
288    ///
289    /// This only changes the look: it removes the default gap and joins
290    /// adjacent item borders into a single segmented outline. Pair it with
291    /// [`single`](Self::single) for an exclusive choice.
292    pub fn segmented(mut self) -> Self {
293        self.segmented = true;
294        self
295    }
296
297    /// Let at most one item be checked at a time.
298    ///
299    /// A click on an unchecked item checks it and clears the others. A click
300    /// on the checked item clears it, so the group can also hold no selection.
301    /// The caller keeps at most one item `checked`; the group does not
302    /// reconcile a state in which several are.
303    pub fn single(mut self) -> Self {
304        self.single = true;
305        self
306    }
307}
308
309impl Sizable for ToggleGroup {
310    fn with_size(mut self, size: impl Into<Size>) -> Self {
311        self.size = size.into();
312        self
313    }
314}
315
316impl ToggleVariants for ToggleGroup {
317    fn with_variant(mut self, variant: ToggleVariant) -> Self {
318        self.variant = variant;
319        self
320    }
321}
322
323impl Disableable for ToggleGroup {
324    fn disabled(mut self, disabled: bool) -> Self {
325        self.disabled = disabled;
326        self
327    }
328}
329
330impl Styled for ToggleGroup {
331    fn style(&mut self) -> &mut StyleRefinement {
332        &mut self.style
333    }
334}
335
336impl RenderOnce for ToggleGroup {
337    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
338        let disabled = self.disabled;
339        let single = self.single;
340        let items_len = self.items.len();
341        let checks = self
342            .items
343            .iter()
344            .map(|item| item.checked)
345            .collect::<Vec<bool>>();
346        let clicked_index = Rc::new(Cell::new(None));
347
348        BaseToggleGroup::new(self.id)
349            .axis(Axis::Horizontal)
350            .child(
351                crate::h_flex()
352                    .items_center()
353                    .when(!self.segmented, |this| this.gap_2())
354                    .refine_style(&self.style)
355                    .children(self.items.into_iter().enumerate().map({
356                        {
357                            let clicked_index = clicked_index.clone();
358                            move |(ix, item)| {
359                                let item = if !self.segmented || items_len == 1 {
360                                    item
361                                } else if ix == 0 {
362                                    item.border_corners(Corners {
363                                        top_left: true,
364                                        top_right: false,
365                                        bottom_left: true,
366                                        bottom_right: false,
367                                    })
368                                    .border_edges(Edges {
369                                        left: true,
370                                        top: true,
371                                        right: true,
372                                        bottom: true,
373                                    })
374                                } else if ix == items_len - 1 {
375                                    item.border_corners(Corners {
376                                        top_left: false,
377                                        top_right: true,
378                                        bottom_left: false,
379                                        bottom_right: true,
380                                    })
381                                    .border_edges(Edges {
382                                        left: false,
383                                        top: true,
384                                        right: true,
385                                        bottom: true,
386                                    })
387                                } else {
388                                    item.border_corners(Corners {
389                                        top_left: false,
390                                        top_right: false,
391                                        bottom_left: false,
392                                        bottom_right: false,
393                                    })
394                                    .border_edges(Edges {
395                                        left: false,
396                                        top: true,
397                                        right: true,
398                                        bottom: true,
399                                    })
400                                };
401
402                                let effective_disabled = disabled || item.disabled;
403                                let clicked_index = clicked_index.clone();
404                                item.disabled(effective_disabled)
405                                    .with_size(self.size)
406                                    .with_variant(self.variant)
407                                    .on_click(move |_, _, cx| {
408                                        clicked_index.set(Some(ix));
409                                        cx.propagate();
410                                    })
411                            }
412                        }
413                    })),
414            )
415            .when_some(
416                (!disabled).then_some(self.on_click).flatten(),
417                |this, on_click| {
418                    this.on_click(move |_, window, cx| {
419                        let Some(ix) = clicked_index.take() else {
420                            return;
421                        };
422                        // A single group clears every other item; clicking its
423                        // checked item still flips that one off.
424                        let mut next = if single {
425                            vec![false; checks.len()]
426                        } else {
427                            checks.clone()
428                        };
429                        next[ix] = !checks[ix];
430                        on_click(&next, window, cx);
431                    })
432                },
433            )
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440    use crate::{IconName, Theme, h_flex};
441    use gpui::{
442        Context, Hsla, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, Render,
443        StatefulInteractiveElement, TestAppContext, VisualTestContext, div, point, px,
444    };
445    use std::cell::{Cell, RefCell};
446
447    struct ToggleHarness {
448        disabled: bool,
449        changes: Rc<RefCell<Vec<bool>>>,
450        parent_clicks: Rc<Cell<usize>>,
451    }
452
453    impl Render for ToggleHarness {
454        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
455            let changes = self.changes.clone();
456            let parent_clicks = self.parent_clicks.clone();
457            h_flex()
458                .id("toggle-parent")
459                .tab_group()
460                .size(px(100.))
461                .on_click(move |_, _, _| parent_clicks.set(parent_clicks.get() + 1))
462                .child(
463                    Toggle::new("toggle")
464                        .label("Bold")
465                        .disabled(self.disabled)
466                        .size_full()
467                        .on_click(move |next, _, _| changes.borrow_mut().push(*next)),
468                )
469        }
470    }
471
472    fn harness(
473        cx: &mut TestAppContext,
474        disabled: bool,
475    ) -> (
476        &mut VisualTestContext,
477        Rc<RefCell<Vec<bool>>>,
478        Rc<Cell<usize>>,
479    ) {
480        cx.update(crate::init);
481        let changes = Rc::new(RefCell::new(Vec::new()));
482        let parent_clicks = Rc::new(Cell::new(0));
483        let (_, cx) = cx.add_window_view({
484            let changes = changes.clone();
485            let parent_clicks = parent_clicks.clone();
486            move |_, _| ToggleHarness {
487                disabled,
488                changes,
489                parent_clicks,
490            }
491        });
492        cx.update(|window, cx| window.draw(cx).clear(cx));
493        (cx, changes, parent_clicks)
494    }
495
496    fn activate_key(cx: &mut VisualTestContext, key: &str) {
497        let keystroke =
498            cx.update(|_, cx| Keystroke::parse_with_os(key, cx.platform_info().os).unwrap());
499        cx.simulate_event(KeyDownEvent {
500            keystroke: keystroke.clone(),
501            is_held: false,
502            prefer_character_input: false,
503        });
504        cx.simulate_event(KeyUpEvent { keystroke });
505    }
506
507    #[gpui::test]
508    fn canonical_pointer_activation_fires_once_and_focuses(cx: &mut TestAppContext) {
509        let (cx, changes, _) = harness(cx, false);
510        cx.simulate_click(point(px(10.), px(10.)), Modifiers::default());
511        assert_eq!(changes.borrow().as_slice(), &[true]);
512        cx.update(|window, cx| assert!(window.focused(cx).is_some()));
513    }
514
515    #[gpui::test]
516    fn canonical_toggle_supports_tab_enter_and_space(cx: &mut TestAppContext) {
517        let (cx, changes, _) = harness(cx, false);
518        cx.update(|window, cx| window.focus_next(cx));
519        cx.update(|window, cx| assert!(window.focused(cx).is_some()));
520        activate_key(cx, "enter");
521        activate_key(cx, "space");
522        assert_eq!(changes.borrow().as_slice(), &[true, true]);
523    }
524
525    #[gpui::test]
526    fn canonical_disabled_toggle_is_inert_and_blocks_parent(cx: &mut TestAppContext) {
527        let (cx, changes, parent_clicks) = harness(cx, true);
528        cx.simulate_click(point(px(10.), px(10.)), Modifiers::default());
529        assert!(changes.borrow().is_empty());
530        assert_eq!(parent_clicks.get(), 0);
531    }
532
533    struct ToggleGroupHarness {
534        install_group_callback: bool,
535        child_clicks: Rc<Cell<usize>>,
536        group_changes: Rc<RefCell<Vec<Vec<bool>>>>,
537    }
538
539    impl Render for ToggleGroupHarness {
540        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
541            let child_clicks = self.child_clicks.clone();
542            let mut group = ToggleGroup::new("toggle-group")
543                .w(px(120.))
544                .child(
545                    Toggle::new("one")
546                        .label("One")
547                        .checked(true)
548                        .on_click(move |_, _, _| child_clicks.set(child_clicks.get() + 1)),
549                )
550                .child(Toggle::new("two").label("Two"));
551            if self.install_group_callback {
552                let changes = self.group_changes.clone();
553                group = group.on_click(move |next, _, _| changes.borrow_mut().push(next.clone()));
554            }
555            group
556        }
557    }
558
559    fn toggle_group_harness(
560        cx: &mut TestAppContext,
561        install_group_callback: bool,
562    ) -> (
563        &mut VisualTestContext,
564        Rc<Cell<usize>>,
565        Rc<RefCell<Vec<Vec<bool>>>>,
566    ) {
567        cx.update(crate::init);
568        let child_clicks = Rc::new(Cell::new(0));
569        let group_changes = Rc::new(RefCell::new(Vec::new()));
570        let (_, cx) = cx.add_window_view({
571            let child_clicks = child_clicks.clone();
572            let group_changes = group_changes.clone();
573            move |_, _| ToggleGroupHarness {
574                install_group_callback,
575                child_clicks,
576                group_changes,
577            }
578        });
579        cx.update(|window, cx| window.draw(cx).clear(cx));
580        (cx, child_clicks, group_changes)
581    }
582
583    #[gpui::test]
584    fn legacy_toggle_group_overrides_child_callback_even_without_group_callback(
585        cx: &mut TestAppContext,
586    ) {
587        let (cx, child_clicks, changes) = toggle_group_harness(cx, false);
588        cx.simulate_click(point(px(10.), px(10.)), Modifiers::default());
589        assert_eq!(child_clicks.get(), 0);
590        assert!(changes.borrow().is_empty());
591    }
592
593    #[gpui::test]
594    fn legacy_toggle_group_pointer_flips_only_the_clicked_rendered_value(cx: &mut TestAppContext) {
595        let (cx, child_clicks, changes) = toggle_group_harness(cx, true);
596        cx.simulate_click(point(px(10.), px(10.)), Modifiers::default());
597        assert_eq!(child_clicks.get(), 0);
598        assert_eq!(changes.borrow().as_slice(), &[vec![false, false]]);
599    }
600
601    #[gpui::test]
602    fn legacy_toggle_group_keyboard_click_does_not_reach_the_group_callback(
603        cx: &mut TestAppContext,
604    ) {
605        let (cx, child_clicks, changes) = toggle_group_harness(cx, true);
606        cx.update(|window, cx| window.focus_next(cx));
607        activate_key(cx, "enter");
608        assert_eq!(child_clicks.get(), 0);
609        assert!(changes.borrow().is_empty());
610    }
611
612    /// Three 60 px items with no gap, so the pointer reaches each of them at
613    /// a known x: the first is checked.
614    struct SelectionHarness {
615        single: bool,
616        changes: Rc<RefCell<Vec<Vec<bool>>>>,
617    }
618
619    impl Render for SelectionHarness {
620        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
621            let changes = self.changes.clone();
622            ToggleGroup::new("selection")
623                .segmented()
624                .when(self.single, ToggleGroup::single)
625                .child(Toggle::new("one").label("One").w(px(60.)).checked(true))
626                .child(Toggle::new("two").label("Two").w(px(60.)))
627                .child(Toggle::new("three").label("Three").w(px(60.)))
628                .on_click(move |next, _, _| changes.borrow_mut().push(next.clone()))
629        }
630    }
631
632    fn selection_harness(
633        cx: &mut TestAppContext,
634        single: bool,
635    ) -> (&mut VisualTestContext, Rc<RefCell<Vec<Vec<bool>>>>) {
636        cx.update(crate::init);
637        let changes = Rc::new(RefCell::new(Vec::new()));
638        let (_, cx) = cx.add_window_view({
639            let changes = changes.clone();
640            move |_, _| SelectionHarness { single, changes }
641        });
642        cx.update(|window, cx| window.draw(cx).clear(cx));
643        (cx, changes)
644    }
645
646    #[gpui::test]
647    fn single_toggle_group_moves_the_check_to_the_clicked_item(cx: &mut TestAppContext) {
648        let (cx, changes) = selection_harness(cx, true);
649        cx.simulate_click(point(px(90.), px(16.)), Modifiers::default());
650        assert_eq!(changes.borrow().as_slice(), &[vec![false, true, false]]);
651    }
652
653    #[gpui::test]
654    fn single_toggle_group_clears_the_checked_item_when_it_is_clicked(cx: &mut TestAppContext) {
655        let (cx, changes) = selection_harness(cx, true);
656        cx.simulate_click(point(px(30.), px(16.)), Modifiers::default());
657        assert_eq!(changes.borrow().as_slice(), &[vec![false, false, false]]);
658    }
659
660    #[gpui::test]
661    fn multi_toggle_group_keeps_the_other_checked_items(cx: &mut TestAppContext) {
662        let (cx, changes) = selection_harness(cx, false);
663        cx.simulate_click(point(px(90.), px(16.)), Modifiers::default());
664        assert_eq!(changes.borrow().as_slice(), &[vec![true, true, false]]);
665    }
666
667    /// Records the text color the toggle hands down to its content.
668    #[derive(IntoElement)]
669    struct ContentColor(Rc<Cell<Option<Hsla>>>);
670
671    impl RenderOnce for ContentColor {
672        fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
673            self.0.set(Some(window.text_style().color));
674            div().w(px(40.)).h(px(16.))
675        }
676    }
677
678    /// A single group of two 60 px items that owns its selection; the first
679    /// item reports the text color it passes to its content.
680    struct ContentColorHarness {
681        checked: Option<usize>,
682        color: Rc<Cell<Option<Hsla>>>,
683    }
684
685    impl Render for ContentColorHarness {
686        fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
687            ToggleGroup::new("selection")
688                .segmented()
689                .single()
690                .child(
691                    Toggle::new("one")
692                        .w(px(60.))
693                        .checked(self.checked == Some(0))
694                        .child(ContentColor(self.color.clone())),
695                )
696                .child(
697                    Toggle::new("two")
698                        .label("Two")
699                        .w(px(60.))
700                        .checked(self.checked == Some(1)),
701                )
702                .on_click(cx.listener(|this, next: &Vec<bool>, _, cx| {
703                    this.checked = next.iter().position(|checked| *checked);
704                    cx.notify();
705                }))
706        }
707    }
708
709    #[gpui::test]
710    fn an_item_unchecked_from_elsewhere_drops_its_hover_color(cx: &mut TestAppContext) {
711        let (foreground, accent_foreground) = cx.update(|cx| {
712            crate::init(cx);
713            let theme = Theme::global_mut(cx);
714            theme.foreground = gpui::black();
715            theme.accent_foreground = gpui::white();
716            (theme.foreground, theme.accent_foreground)
717        });
718        let color = Rc::new(Cell::new(None));
719        let (_, cx) = cx.add_window_view({
720            let color = color.clone();
721            move |_, _| ContentColorHarness {
722                checked: None,
723                color,
724            }
725        });
726        let draw = |cx: &mut VisualTestContext| cx.update(|window, cx| window.draw(cx).clear(cx));
727        draw(cx);
728        assert_eq!(color.get(), Some(foreground));
729
730        // Hover the first item and check it, then rest the pointer on the second.
731        cx.simulate_mouse_move(point(px(30.), px(16.)), None, Modifiers::default());
732        draw(cx);
733        assert_eq!(color.get(), Some(accent_foreground));
734        cx.simulate_click(point(px(30.), px(16.)), Modifiers::default());
735        draw(cx);
736        assert_eq!(color.get(), Some(accent_foreground));
737        cx.simulate_mouse_move(point(px(90.), px(16.)), None, Modifiers::default());
738        draw(cx);
739        assert_eq!(color.get(), Some(accent_foreground));
740
741        // Checking the second item unchecks the first while the pointer is
742        // elsewhere, so its content goes back to the plain foreground.
743        cx.simulate_click(point(px(90.), px(16.)), Modifiers::default());
744        draw(cx);
745        assert_eq!(color.get(), Some(foreground));
746    }
747
748    #[test]
749    fn instance_style_remains_the_final_visual_override() {
750        let toggle = Toggle::new("styled").checked(true).opacity(0.37);
751        assert_eq!(toggle.style.opacity, Some(0.37));
752    }
753
754    #[gpui::test]
755    fn test_toggle_builder(_cx: &mut gpui::TestAppContext) {
756        let toggle = Toggle::new("complex-toggle")
757            .label("Enable Feature")
758            .icon(IconName::Check)
759            .checked(true)
760            .outline()
761            .large()
762            .disabled(false)
763            .on_click(|_, _, _| {});
764
765        assert_eq!(toggle.children.len(), 2); // label + icon
766        assert!(toggle.checked);
767        assert_eq!(toggle.variant, ToggleVariant::Outline);
768        assert_eq!(toggle.size, Size::Large);
769        assert!(!toggle.disabled);
770        assert!(toggle.on_click.is_some());
771    }
772
773    #[gpui::test]
774    fn test_toggle_group_builder(_cx: &mut gpui::TestAppContext) {
775        let group = ToggleGroup::new("complex-group")
776            .child(Toggle::new("toggle1").label("Option 1"))
777            .child(Toggle::new("toggle2").label("Option 2").checked(true))
778            .child(Toggle::new("toggle3").label("Option 3"))
779            .outline()
780            .large()
781            .segmented()
782            .single()
783            .disabled(false)
784            .on_click(|_, _, _| {});
785
786        assert_eq!(group.items.len(), 3);
787        assert_eq!(group.variant, ToggleVariant::Outline);
788        assert_eq!(group.size, Size::Large);
789        assert!(group.segmented);
790        assert!(group.single);
791        assert!(!group.disabled);
792        assert!(group.on_click.is_some());
793    }
794}