1#![allow(missing_docs)]
2
3use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
4use std::sync::Arc;
5
6use crate::prelude::*;
7use crate::utils::is_light;
8use crate::{Color, Icon, IconName, ToggleState};
9
10/// Creates a new checkbox
11pub fn checkbox(id: impl Into<ElementId>, toggle_state: ToggleState) -> Checkbox {
12 Checkbox::new(id, toggle_state)
13}
14
15/// Creates a new switch
16pub fn switch(id: impl Into<ElementId>, toggle_state: ToggleState) -> Switch {
17 Switch::new(id, toggle_state)
18}
19
20/// # Checkbox
21///
22/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
23/// Each checkbox works independently from other checkboxes in the list,
24/// therefore checking an additional box does not affect any other selections.
25#[derive(IntoElement)]
26pub struct Checkbox {
27 id: ElementId,
28 toggle_state: ToggleState,
29 disabled: bool,
30 on_click: Option<Box<dyn Fn(&ToggleState, &mut WindowContext) + 'static>>,
31}
32
33impl Checkbox {
34 pub fn new(id: impl Into<ElementId>, checked: ToggleState) -> Self {
35 Self {
36 id: id.into(),
37 toggle_state: checked,
38 disabled: false,
39 on_click: None,
40 }
41 }
42
43 pub fn disabled(mut self, disabled: bool) -> Self {
44 self.disabled = disabled;
45 self
46 }
47
48 pub fn on_click(
49 mut self,
50 handler: impl Fn(&ToggleState, &mut WindowContext) + 'static,
51 ) -> Self {
52 self.on_click = Some(Box::new(handler));
53 self
54 }
55}
56
57impl RenderOnce for Checkbox {
58 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
59 let group_id = format!("checkbox_group_{:?}", self.id);
60
61 let icon = match self.toggle_state {
62 ToggleState::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
63 if self.disabled {
64 Color::Disabled
65 } else {
66 Color::Selected
67 },
68 )),
69 ToggleState::Indeterminate => Some(
70 Icon::new(IconName::Dash)
71 .size(IconSize::Small)
72 .color(if self.disabled {
73 Color::Disabled
74 } else {
75 Color::Selected
76 }),
77 ),
78 ToggleState::Unselected => None,
79 };
80
81 let selected = self.toggle_state == ToggleState::Selected
82 || self.toggle_state == ToggleState::Indeterminate;
83
84 let (bg_color, border_color) = match (self.disabled, selected) {
85 (true, _) => (
86 cx.theme().colors().ghost_element_disabled,
87 cx.theme().colors().border_disabled,
88 ),
89 (false, true) => (
90 cx.theme().colors().element_selected,
91 cx.theme().colors().border,
92 ),
93 (false, false) => (
94 cx.theme().colors().element_background,
95 cx.theme().colors().border,
96 ),
97 };
98
99 h_flex()
100 .id(self.id)
101 .justify_center()
102 .items_center()
103 .size(DynamicSpacing::Base20.rems(cx))
104 .group(group_id.clone())
105 .child(
106 div()
107 .flex()
108 .flex_none()
109 .justify_center()
110 .items_center()
111 .m(DynamicSpacing::Base04.px(cx))
112 .size(DynamicSpacing::Base16.rems(cx))
113 .rounded_sm()
114 .bg(bg_color)
115 .border_1()
116 .border_color(border_color)
117 .when(!self.disabled, |this| {
118 this.group_hover(group_id.clone(), |el| {
119 el.bg(cx.theme().colors().element_hover)
120 })
121 })
122 .children(icon),
123 )
124 .when_some(
125 self.on_click.filter(|_| !self.disabled),
126 |this, on_click| {
127 this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx))
128 },
129 )
130 }
131}
132
133/// A [`Checkbox`] that has a [`Label`].
134#[derive(IntoElement)]
135pub struct CheckboxWithLabel {
136 id: ElementId,
137 label: Label,
138 checked: ToggleState,
139 on_click: Arc<dyn Fn(&ToggleState, &mut WindowContext) + 'static>,
140}
141
142impl CheckboxWithLabel {
143 pub fn new(
144 id: impl Into<ElementId>,
145 label: Label,
146 checked: ToggleState,
147 on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static,
148 ) -> Self {
149 Self {
150 id: id.into(),
151 label,
152 checked,
153 on_click: Arc::new(on_click),
154 }
155 }
156}
157
158impl RenderOnce for CheckboxWithLabel {
159 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
160 h_flex()
161 .gap(DynamicSpacing::Base08.rems(cx))
162 .child(Checkbox::new(self.id.clone(), self.checked).on_click({
163 let on_click = self.on_click.clone();
164 move |checked, cx| {
165 (on_click)(checked, cx);
166 }
167 }))
168 .child(
169 div()
170 .id(SharedString::from(format!("{}-label", self.id)))
171 .on_click(move |_event, cx| {
172 (self.on_click)(&self.checked.inverse(), cx);
173 })
174 .child(self.label),
175 )
176 }
177}
178
179/// # Switch
180///
181/// Switches are used to represent opposite states, such as enabled or disabled.
182#[derive(IntoElement)]
183pub struct Switch {
184 id: ElementId,
185 toggle_state: ToggleState,
186 disabled: bool,
187 on_click: Option<Box<dyn Fn(&ToggleState, &mut WindowContext) + 'static>>,
188}
189
190impl Switch {
191 pub fn new(id: impl Into<ElementId>, state: ToggleState) -> Self {
192 Self {
193 id: id.into(),
194 toggle_state: state,
195 disabled: false,
196 on_click: None,
197 }
198 }
199
200 pub fn disabled(mut self, disabled: bool) -> Self {
201 self.disabled = disabled;
202 self
203 }
204
205 pub fn on_click(
206 mut self,
207 handler: impl Fn(&ToggleState, &mut WindowContext) + 'static,
208 ) -> Self {
209 self.on_click = Some(Box::new(handler));
210 self
211 }
212}
213
214impl RenderOnce for Switch {
215 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
216 let is_on = self.toggle_state == ToggleState::Selected;
217 let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 };
218 let base_color = cx.theme().colors().text;
219
220 let bg_color = if is_on {
221 cx.theme()
222 .colors()
223 .element_background
224 .blend(base_color.opacity(0.08))
225 } else {
226 cx.theme().colors().element_background
227 };
228 let thumb_color = base_color.opacity(0.8);
229 let thumb_hover_color = base_color;
230 let border_color = cx.theme().colors().border_variant;
231 // Lighter themes need higher contrast borders
232 let border_hover_color = if is_on {
233 border_color.blend(base_color.opacity(0.16 * adjust_ratio))
234 } else {
235 border_color.blend(base_color.opacity(0.05 * adjust_ratio))
236 };
237 let thumb_opacity = match (is_on, self.disabled) {
238 (_, true) => 0.2,
239 (true, false) => 1.0,
240 (false, false) => 0.5,
241 };
242
243 let group_id = format!("switch_group_{:?}", self.id);
244
245 h_flex()
246 .id(self.id)
247 .items_center()
248 .w(DynamicSpacing::Base32.rems(cx))
249 .h(DynamicSpacing::Base20.rems(cx))
250 .group(group_id.clone())
251 .child(
252 h_flex()
253 .when(is_on, |on| on.justify_end())
254 .when(!is_on, |off| off.justify_start())
255 .items_center()
256 .size_full()
257 .rounded_full()
258 .px(DynamicSpacing::Base02.px(cx))
259 .bg(bg_color)
260 .border_1()
261 .border_color(border_color)
262 .when(!self.disabled, |this| {
263 this.group_hover(group_id.clone(), |el| el.border_color(border_hover_color))
264 })
265 .child(
266 div()
267 .size(DynamicSpacing::Base12.rems(cx))
268 .rounded_full()
269 .bg(thumb_color)
270 .when(!self.disabled, |this| {
271 this.group_hover(group_id.clone(), |el| el.bg(thumb_hover_color))
272 })
273 .opacity(thumb_opacity),
274 ),
275 )
276 .when_some(
277 self.on_click.filter(|_| !self.disabled),
278 |this, on_click| {
279 this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx))
280 },
281 )
282 }
283}
284
285/// A [`Switch`] that has a [`Label`].
286#[derive(IntoElement)]
287pub struct SwitchWithLabel {
288 id: ElementId,
289 label: Label,
290 checked: ToggleState,
291 on_click: Arc<dyn Fn(&ToggleState, &mut WindowContext) + 'static>,
292}
293
294impl SwitchWithLabel {
295 pub fn new(
296 id: impl Into<ElementId>,
297 label: Label,
298 checked: ToggleState,
299 on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static,
300 ) -> Self {
301 Self {
302 id: id.into(),
303 label,
304 checked,
305 on_click: Arc::new(on_click),
306 }
307 }
308}
309
310impl RenderOnce for SwitchWithLabel {
311 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
312 h_flex()
313 .gap(DynamicSpacing::Base08.rems(cx))
314 .child(Switch::new(self.id.clone(), self.checked).on_click({
315 let on_click = self.on_click.clone();
316 move |checked, cx| {
317 (on_click)(checked, cx);
318 }
319 }))
320 .child(
321 div()
322 .id(SharedString::from(format!("{}-label", self.id)))
323 .on_click(move |_event, cx| {
324 (self.on_click)(&self.checked.inverse(), cx);
325 })
326 .child(self.label),
327 )
328 }
329}
330
331impl ComponentPreview for Checkbox {
332 fn description() -> impl Into<Option<&'static str>> {
333 "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
334 }
335
336 fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
337 vec![
338 example_group_with_title(
339 "Default",
340 vec![
341 single_example(
342 "Unselected",
343 Checkbox::new("checkbox_unselected", ToggleState::Unselected),
344 ),
345 single_example(
346 "Indeterminate",
347 Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate),
348 ),
349 single_example(
350 "Selected",
351 Checkbox::new("checkbox_selected", ToggleState::Selected),
352 ),
353 ],
354 ),
355 example_group_with_title(
356 "Disabled",
357 vec![
358 single_example(
359 "Unselected",
360 Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
361 .disabled(true),
362 ),
363 single_example(
364 "Indeterminate",
365 Checkbox::new(
366 "checkbox_disabled_indeterminate",
367 ToggleState::Indeterminate,
368 )
369 .disabled(true),
370 ),
371 single_example(
372 "Selected",
373 Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
374 .disabled(true),
375 ),
376 ],
377 ),
378 ]
379 }
380}
381
382impl ComponentPreview for Switch {
383 fn description() -> impl Into<Option<&'static str>> {
384 "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting."
385 }
386
387 fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
388 vec![
389 example_group_with_title(
390 "Default",
391 vec![
392 single_example(
393 "Off",
394 Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _cx| {}),
395 ),
396 single_example(
397 "On",
398 Switch::new("switch_on", ToggleState::Selected).on_click(|_, _cx| {}),
399 ),
400 ],
401 ),
402 example_group_with_title(
403 "Disabled",
404 vec![
405 single_example(
406 "Off",
407 Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true),
408 ),
409 single_example(
410 "On",
411 Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true),
412 ),
413 ],
414 ),
415 ]
416 }
417}
418
419impl ComponentPreview for CheckboxWithLabel {
420 fn description() -> impl Into<Option<&'static str>> {
421 "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
422 }
423
424 fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
425 vec![example_group(vec![
426 single_example(
427 "Unselected",
428 CheckboxWithLabel::new(
429 "checkbox_with_label_unselected",
430 Label::new("Always save on quit"),
431 ToggleState::Unselected,
432 |_, _| {},
433 ),
434 ),
435 single_example(
436 "Indeterminate",
437 CheckboxWithLabel::new(
438 "checkbox_with_label_indeterminate",
439 Label::new("Always save on quit"),
440 ToggleState::Indeterminate,
441 |_, _| {},
442 ),
443 ),
444 single_example(
445 "Selected",
446 CheckboxWithLabel::new(
447 "checkbox_with_label_selected",
448 Label::new("Always save on quit"),
449 ToggleState::Selected,
450 |_, _| {},
451 ),
452 ),
453 ])]
454 }
455}
456
457impl ComponentPreview for SwitchWithLabel {
458 fn description() -> impl Into<Option<&'static str>> {
459 "A switch with an associated label, allowing users to select an option while providing a descriptive text."
460 }
461
462 fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
463 vec![example_group(vec![
464 single_example(
465 "Off",
466 SwitchWithLabel::new(
467 "switch_with_label_unselected",
468 Label::new("Always save on quit"),
469 ToggleState::Unselected,
470 |_, _| {},
471 ),
472 ),
473 single_example(
474 "On",
475 SwitchWithLabel::new(
476 "switch_with_label_selected",
477 Label::new("Always save on quit"),
478 ToggleState::Selected,
479 |_, _| {},
480 ),
481 ),
482 ])]
483 }
484}