Skip to main content

ui/components/
toggle_group.rs

1use gpui::ElementId;
2use std::rc::Rc;
3
4use crate::prelude::*;
5
6/// Selection mode for a [`ToggleGroup`].
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum ToggleGroupMode {
9    /// Exactly one item may be selected at a time.
10    #[default]
11    Single,
12    /// Multiple items may be selected simultaneously.
13    Multiple,
14}
15
16/// Configuration for one segment in a [`ToggleGroup`].
17#[derive(Clone)]
18pub struct ToggleGroupItem {
19    label: SharedString,
20    icon: Option<IconName>,
21}
22
23impl ToggleGroupItem {
24    pub fn new(label: impl Into<SharedString>) -> Self {
25        Self {
26            label: label.into(),
27            icon: None,
28        }
29    }
30
31    pub fn icon(mut self, icon: IconName) -> Self {
32        self.icon = Some(icon);
33        self
34    }
35}
36
37/// A connected row of toggle buttons supporting single or multiple selection.
38///
39/// Selection state is caller-managed: pass the active indices via
40/// [`.selected()`](ToggleGroup::selected) and update them in
41/// [`.on_change()`](ToggleGroup::on_change).
42#[derive(IntoElement, RegisterComponent)]
43pub struct ToggleGroup {
44    id: ElementId,
45    items: Vec<ToggleGroupItem>,
46    mode: ToggleGroupMode,
47    selected: Vec<usize>,
48    disabled: bool,
49    on_change: Option<Rc<dyn Fn(Vec<usize>, &mut Window, &mut App) + 'static>>,
50}
51
52impl ToggleGroup {
53    pub fn new(id: impl Into<ElementId>, items: impl IntoIterator<Item = ToggleGroupItem>) -> Self {
54        Self {
55            id: id.into(),
56            items: items.into_iter().collect(),
57            mode: ToggleGroupMode::default(),
58            selected: Vec::new(),
59            disabled: false,
60            on_change: None,
61        }
62    }
63
64    pub fn mode(mut self, mode: ToggleGroupMode) -> Self {
65        self.mode = mode;
66        self
67    }
68
69    /// Sets the indices of currently selected items.
70    pub fn selected(mut self, indices: impl IntoIterator<Item = usize>) -> Self {
71        self.selected = indices.into_iter().collect();
72        self
73    }
74
75    pub fn disabled(mut self, disabled: bool) -> Self {
76        self.disabled = disabled;
77        self
78    }
79
80    /// Binds a handler called with the group's *next* selection after a
81    /// click, computed according to [`ToggleGroupMode`]: in `Single` mode
82    /// the clicked index replaces the selection (`[index]`); in `Multiple`
83    /// mode the clicked index is toggled within the current selection. The
84    /// group is controlled — the caller must feed the reported selection
85    /// back in via [`.selected()`](ToggleGroup::selected) for it to render.
86    pub fn on_change(
87        mut self,
88        handler: impl Fn(Vec<usize>, &mut Window, &mut App) + 'static,
89    ) -> Self {
90        self.on_change = Some(Rc::new(handler));
91        self
92    }
93}
94
95impl RenderOnce for ToggleGroup {
96    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
97        let selected = self.selected;
98        let disabled = self.disabled;
99        let mode = self.mode;
100        let group_id = self.id.clone();
101        let on_change = self.on_change;
102
103        let mut row = h_flex()
104            .id(self.id)
105            .gap_1()
106            .p(px(2.))
107            .rounded_md()
108            .bg(semantic::muted_bg(cx));
109
110        for (i, item) in self.items.into_iter().enumerate() {
111            let is_selected = selected.contains(&i);
112            let on_change = on_change.clone();
113            let current_selected = selected.clone();
114
115            let mut cell = h_flex()
116                .id((group_id.clone(), i.to_string()))
117                .flex_1()
118                .justify_center()
119                .items_center()
120                .gap_1()
121                .px_3()
122                .py_1p5()
123                .rounded_sm()
124                .when(is_selected, |this| {
125                    this.bg(semantic::accent_bg(cx))
126                        .text_color(semantic::accent_fg(cx))
127                })
128                .when(!is_selected && !disabled, |this| {
129                    this.hover(|style| style.bg(semantic::hover_bg(cx)))
130                })
131                .when(disabled, |this| this.opacity(0.5))
132                .children(item.icon.map(|icon| {
133                    Icon::new(icon)
134                        .size(IconSize::Small)
135                        .color(if is_selected {
136                            Color::Accent
137                        } else {
138                            Color::Muted
139                        })
140                        .into_any_element()
141                }))
142                .child(
143                    Label::new(item.label)
144                        .size(LabelSize::Small)
145                        .color(if is_selected {
146                            Color::Accent
147                        } else {
148                            Color::Default
149                        }),
150                );
151
152            if !disabled {
153                cell = cell.cursor_pointer().on_click(move |_, window, cx| {
154                    if let Some(handler) = &on_change {
155                        let next = match mode {
156                            ToggleGroupMode::Single => vec![i],
157                            ToggleGroupMode::Multiple => {
158                                let mut next = current_selected.clone();
159                                if let Some(pos) = next.iter().position(|&x| x == i) {
160                                    next.remove(pos);
161                                } else {
162                                    next.push(i);
163                                }
164                                next
165                            }
166                        };
167                        handler(next, window, cx);
168                    }
169                });
170            }
171
172            row = row.child(cell);
173        }
174
175        row
176    }
177}
178
179impl Component for ToggleGroup {
180    fn scope() -> ComponentScope {
181        ComponentScope::Input
182    }
183
184    fn description() -> Option<&'static str> {
185        Some("A row of toggle buttons supporting single or multiple selection.")
186    }
187
188    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
189        Some(
190            v_flex()
191                .gap_4()
192                .child(
193                    ToggleGroup::new(
194                        "toggle-group-single",
195                        [
196                            ToggleGroupItem::new("List"),
197                            ToggleGroupItem::new("Grid"),
198                            ToggleGroupItem::new("Board"),
199                        ],
200                    )
201                    .selected([1])
202                    .into_any_element(),
203                )
204                .child(
205                    ToggleGroup::new(
206                        "toggle-group-multiple",
207                        [
208                            ToggleGroupItem::new("Bold"),
209                            ToggleGroupItem::new("Italic"),
210                            ToggleGroupItem::new("Underline"),
211                        ],
212                    )
213                    .mode(ToggleGroupMode::Multiple)
214                    .selected([0, 2])
215                    .into_any_element(),
216                )
217                .child(
218                    ToggleGroup::new(
219                        "toggle-group-icons",
220                        [
221                            ToggleGroupItem::new("Left").icon(IconName::ArrowLeft),
222                            ToggleGroupItem::new("Center").icon(IconName::Dash),
223                            ToggleGroupItem::new("Right").icon(IconName::ArrowRight),
224                        ],
225                    )
226                    .selected([0])
227                    .disabled(true)
228                    .into_any_element(),
229                )
230                .into_any_element(),
231        )
232    }
233}