1use std::{cell::Cell, rc::Rc};
2
3use gpui::{
4 div, prelude::FluentBuilder as _, AnyElement, App, ElementId, InteractiveElement, IntoElement,
5 ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
6 Window,
7};
8use smallvec::{smallvec, SmallVec};
9
10use crate::{h_flex, ActiveTheme, Disableable, Icon, Sizable, Size, StyledExt};
11
12#[derive(Default, Copy, Debug, Clone, PartialEq, Eq, Hash)]
13pub enum ToggleVariant {
14 #[default]
15 Ghost,
16 Outline,
17}
18
19pub trait ToggleVariants: Sized {
20 fn with_variant(self, variant: ToggleVariant) -> Self;
21 fn ghost(self) -> Self {
22 self.with_variant(ToggleVariant::Ghost)
23 }
24 fn outline(self) -> Self {
25 self.with_variant(ToggleVariant::Outline)
26 }
27}
28
29#[derive(IntoElement)]
30pub struct Toggle {
31 style: StyleRefinement,
32 checked: bool,
33 size: Size,
34 variant: ToggleVariant,
35 disabled: bool,
36 children: SmallVec<[AnyElement; 1]>,
37}
38
39#[derive(IntoElement)]
40pub struct InteractiveToggle {
41 id: ElementId,
42 toggle: Toggle,
43 on_change: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
44}
45
46impl Toggle {
47 fn new() -> Self {
48 Self {
49 style: StyleRefinement::default(),
50 checked: false,
51 size: Size::default(),
52 variant: ToggleVariant::default(),
53 disabled: false,
54 children: smallvec![],
55 }
56 }
57
58 pub fn label(label: impl Into<SharedString>) -> Self {
59 Self::new().child(label.into())
60 }
61
62 pub fn icon(icon: impl Into<Icon>) -> Self {
63 Self::new().child(icon.into())
64 }
65
66 pub fn id(self, id: impl Into<ElementId>) -> InteractiveToggle {
67 InteractiveToggle {
68 id: id.into(),
69 toggle: self,
70 on_change: None,
71 }
72 }
73
74 pub fn checked(mut self, checked: bool) -> Self {
75 self.checked = checked;
76 self
77 }
78}
79
80impl ToggleVariants for Toggle {
81 fn with_variant(mut self, variant: ToggleVariant) -> Self {
82 self.variant = variant;
83 self
84 }
85}
86
87impl Disableable for Toggle {
88 fn disabled(mut self, disabled: bool) -> Self {
89 self.disabled = disabled;
90 self
91 }
92}
93
94impl Sizable for Toggle {
95 fn with_size(mut self, size: impl Into<Size>) -> Self {
96 self.size = size.into();
97 self
98 }
99}
100
101impl Styled for Toggle {
102 fn style(&mut self) -> &mut StyleRefinement {
103 &mut self.style
104 }
105}
106
107impl ParentElement for Toggle {
108 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
109 self.children.extend(elements);
110 }
111}
112
113impl RenderOnce for Toggle {
114 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
115 let checked = self.checked;
116 let disabled = self.disabled;
117 let hoverable = !disabled && !checked;
118
119 div()
120 .flex()
121 .flex_row()
122 .items_center()
123 .justify_center()
124 .map(|this| match self.size {
125 Size::XSmall => this.min_w_5().h_5().px_0p5().text_xs(),
126 Size::Small => this.min_w_6().h_6().px_1().text_sm(),
127 Size::Large => this.min_w_9().h_9().px_3().text_lg(),
128 _ => this.min_w_8().h_8().px_2(),
129 })
130 .rounded(cx.theme().radius)
131 .when(self.variant == ToggleVariant::Outline, |this| {
132 this.border_1()
133 .border_color(cx.theme().border)
134 .bg(cx.theme().background)
135 .when(cx.theme().shadow, |this| this.shadow_xs())
136 })
137 .when(hoverable, |this| {
138 this.hover(|this| {
139 this.bg(cx.theme().accent)
140 .text_color(cx.theme().accent_foreground)
141 })
142 })
143 .when(checked, |this| {
144 this.bg(cx.theme().accent)
145 .text_color(cx.theme().accent_foreground)
146 })
147 .refine_style(&self.style)
148 .children(self.children)
149 }
150}
151
152impl InteractiveToggle {
153 pub fn on_change(mut self, on_change: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
157 self.on_change = Some(Box::new(on_change));
158 self
159 }
160
161 pub fn checked(mut self, checked: bool) -> Self {
162 self.toggle.checked = checked;
163 self
164 }
165}
166
167impl Sizable for InteractiveToggle {
168 fn with_size(mut self, size: impl Into<Size>) -> Self {
169 self.toggle = self.toggle.with_size(size);
170 self
171 }
172}
173
174impl ToggleVariants for InteractiveToggle {
175 fn with_variant(mut self, variant: ToggleVariant) -> Self {
176 self.toggle.variant = variant;
177 self
178 }
179}
180
181impl Disableable for InteractiveToggle {
182 fn disabled(mut self, disabled: bool) -> Self {
183 self.toggle.disabled = disabled;
184 self
185 }
186}
187
188impl ParentElement for InteractiveToggle {
189 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
190 self.toggle.extend(elements);
191 }
192}
193
194impl RenderOnce for InteractiveToggle {
195 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
196 let checked = self.toggle.checked;
197 let disabled = self.toggle.disabled;
198
199 div()
200 .id(self.id)
201 .child(self.toggle)
202 .when(!disabled, |this| {
203 this.when_some(self.on_change, |this, on_change| {
204 this.on_click(move |_, window, cx| on_change(&!checked, window, cx))
205 })
206 })
207 }
208}
209
210#[derive(IntoElement)]
211pub struct ToggleGroup {
212 id: ElementId,
213 style: StyleRefinement,
214 size: Size,
215 variant: ToggleVariant,
216 disabled: bool,
217 items: Vec<Toggle>,
218 on_change: Option<Rc<dyn Fn(&Vec<bool>, &mut Window, &mut App) + 'static>>,
219}
220
221impl ToggleGroup {
222 pub fn new(id: impl Into<ElementId>) -> Self {
223 Self {
224 id: id.into(),
225 style: StyleRefinement::default(),
226 size: Size::default(),
227 variant: ToggleVariant::default(),
228 disabled: false,
229 items: Vec::new(),
230 on_change: None,
231 }
232 }
233
234 pub fn child(mut self, toggle: impl Into<Toggle>) -> Self {
236 self.items.push(toggle.into());
237 self
238 }
239
240 pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Toggle>>) -> Self {
242 self.items.extend(children.into_iter().map(Into::into));
243 self
244 }
245
246 pub fn on_change(
250 mut self,
251 on_change: impl Fn(&Vec<bool>, &mut Window, &mut App) + 'static,
252 ) -> Self {
253 self.on_change = Some(Rc::new(on_change));
254 self
255 }
256}
257
258impl Sizable for ToggleGroup {
259 fn with_size(mut self, size: impl Into<Size>) -> Self {
260 self.size = size.into();
261 self
262 }
263}
264
265impl ToggleVariants for ToggleGroup {
266 fn with_variant(mut self, variant: ToggleVariant) -> Self {
267 self.variant = variant;
268 self
269 }
270}
271
272impl Disableable for ToggleGroup {
273 fn disabled(mut self, disabled: bool) -> Self {
274 self.disabled = disabled;
275 self
276 }
277}
278
279impl Styled for ToggleGroup {
280 fn style(&mut self) -> &mut StyleRefinement {
281 &mut self.style
282 }
283}
284
285impl RenderOnce for ToggleGroup {
286 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
287 let disabled = self.disabled;
288 let checks = self
289 .items
290 .iter()
291 .map(|item| item.checked)
292 .collect::<Vec<bool>>();
293 let state = Rc::new(Cell::new(None));
294
295 h_flex()
296 .id(self.id)
297 .gap_1()
298 .refine_style(&self.style)
299 .children(self.items.into_iter().enumerate().map({
300 |(ix, item)| {
301 let state = state.clone();
302 item.disabled(disabled)
303 .id(ix)
304 .with_size(self.size)
305 .with_variant(self.variant)
306 .on_change(move |_, _, _| {
307 state.set(Some(ix));
308 })
309 }
310 }))
311 .when(!disabled, |this| {
312 this.when_some(self.on_change, |this, on_change| {
313 this.on_click(move |_, window, cx| {
314 if let Some(ix) = state.get() {
315 let mut checks = checks.clone();
316 checks[ix] = !checks[ix];
317 on_change(&checks, window, cx);
318 }
319 })
320 })
321 })
322 }
323}