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;
22 fn ghost(self) -> Self {
24 self.with_variant(ToggleVariant::Ghost)
25 }
26 fn outline(self) -> Self {
28 self.with_variant(ToggleVariant::Outline)
29 }
30}
31
32#[derive(IntoElement)]
33pub struct Toggle {
34 id: ElementId,
35 style: StyleRefinement,
36 checked: bool,
37 size: Size,
38 variant: ToggleVariant,
39 disabled: bool,
40 children: SmallVec<[AnyElement; 1]>,
41 on_click: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
42}
43
44impl Toggle {
45 pub fn new(id: impl Into<ElementId>) -> Self {
47 Self {
48 id: id.into(),
49 style: StyleRefinement::default(),
50 checked: false,
51 size: Size::default(),
52 variant: ToggleVariant::default(),
53 disabled: false,
54 children: smallvec![],
55 on_click: None,
56 }
57 }
58
59 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
61 let label: SharedString = label.into();
62 self.children.push(label.into_any_element());
63 self
64 }
65
66 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
68 let icon: Icon = icon.into();
69 self.children.push(icon.into());
70 self
71 }
72
73 pub fn checked(mut self, checked: bool) -> Self {
75 self.checked = checked;
76 self
77 }
78
79 pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
83 self.on_click = Some(Box::new(handler));
84 self
85 }
86}
87
88impl ToggleVariants for Toggle {
89 fn with_variant(mut self, variant: ToggleVariant) -> Self {
90 self.variant = variant;
91 self
92 }
93}
94
95impl ParentElement for Toggle {
96 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
97 self.children.extend(elements);
98 }
99}
100
101impl Disableable for Toggle {
102 fn disabled(mut self, disabled: bool) -> Self {
103 self.disabled = disabled;
104 self
105 }
106}
107
108impl Sizable for Toggle {
109 fn with_size(mut self, size: impl Into<Size>) -> Self {
110 self.size = size.into();
111 self
112 }
113}
114
115impl Styled for Toggle {
116 fn style(&mut self) -> &mut StyleRefinement {
117 &mut self.style
118 }
119}
120
121impl RenderOnce for Toggle {
122 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
123 let checked = self.checked;
124 let disabled = self.disabled;
125 let hoverable = !disabled && !checked;
126
127 div()
128 .id(self.id)
129 .flex()
130 .flex_row()
131 .items_center()
132 .justify_center()
133 .map(|this| match self.size {
134 Size::XSmall => this.min_w_5().h_5().px_0p5().text_xs(),
135 Size::Small => this.min_w_6().h_6().px_1().text_sm(),
136 Size::Large => this.min_w_9().h_9().px_3().text_lg(),
137 _ => this.min_w_8().h_8().px_2(),
138 })
139 .rounded(cx.theme().radius)
140 .when(self.variant == ToggleVariant::Outline, |this| {
141 this.border_1()
142 .border_color(cx.theme().border)
143 .bg(cx.theme().background)
144 .when(cx.theme().shadow, |this| this.shadow_xs())
145 })
146 .when(hoverable, |this| {
147 this.hover(|this| {
148 this.bg(cx.theme().accent)
149 .text_color(cx.theme().accent_foreground)
150 })
151 })
152 .when(checked, |this| {
153 this.bg(cx.theme().accent)
154 .text_color(cx.theme().accent_foreground)
155 })
156 .refine_style(&self.style)
157 .children(self.children)
158 .when(!disabled, |this| {
159 this.when_some(self.on_click, |this, on_click| {
160 this.on_click(move |_, window, cx| on_click(&!checked, window, cx))
161 })
162 })
163 }
164}
165
166#[derive(IntoElement)]
168pub struct ToggleGroup {
169 id: ElementId,
170 style: StyleRefinement,
171 size: Size,
172 variant: ToggleVariant,
173 disabled: bool,
174 items: Vec<Toggle>,
175 on_click: Option<Rc<dyn Fn(&Vec<bool>, &mut Window, &mut App) + 'static>>,
176}
177
178impl ToggleGroup {
179 pub fn new(id: impl Into<ElementId>) -> Self {
181 Self {
182 id: id.into(),
183 style: StyleRefinement::default(),
184 size: Size::default(),
185 variant: ToggleVariant::default(),
186 disabled: false,
187 items: Vec::new(),
188 on_click: None,
189 }
190 }
191
192 pub fn child(mut self, toggle: impl Into<Toggle>) -> Self {
194 self.items.push(toggle.into());
195 self
196 }
197
198 pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Toggle>>) -> Self {
200 self.items.extend(children.into_iter().map(Into::into));
201 self
202 }
203
204 pub fn on_click(
208 mut self,
209 on_click: impl Fn(&Vec<bool>, &mut Window, &mut App) + 'static,
210 ) -> Self {
211 self.on_click = Some(Rc::new(on_click));
212 self
213 }
214}
215
216impl Sizable for ToggleGroup {
217 fn with_size(mut self, size: impl Into<Size>) -> Self {
218 self.size = size.into();
219 self
220 }
221}
222
223impl ToggleVariants for ToggleGroup {
224 fn with_variant(mut self, variant: ToggleVariant) -> Self {
225 self.variant = variant;
226 self
227 }
228}
229
230impl Disableable for ToggleGroup {
231 fn disabled(mut self, disabled: bool) -> Self {
232 self.disabled = disabled;
233 self
234 }
235}
236
237impl Styled for ToggleGroup {
238 fn style(&mut self) -> &mut StyleRefinement {
239 &mut self.style
240 }
241}
242
243impl RenderOnce for ToggleGroup {
244 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
245 let disabled = self.disabled;
246 let checks = self
247 .items
248 .iter()
249 .map(|item| item.checked)
250 .collect::<Vec<bool>>();
251 let state = Rc::new(Cell::new(None));
252
253 h_flex()
254 .id(self.id)
255 .gap_1()
256 .refine_style(&self.style)
257 .children(self.items.into_iter().enumerate().map({
258 |(ix, item)| {
259 let state = state.clone();
260 item.disabled(disabled)
261 .with_size(self.size)
262 .with_variant(self.variant)
263 .on_click(move |_, _, _| {
264 state.set(Some(ix));
265 })
266 }
267 }))
268 .when(!disabled, |this| {
269 this.when_some(self.on_click, |this, on_click| {
270 this.on_click(move |_, window, cx| {
271 if let Some(ix) = state.get() {
272 let mut checks = checks.clone();
273 checks[ix] = !checks[ix];
274 on_click(&checks, window, cx);
275 }
276 })
277 })
278 })
279 }
280}