1use gpui::{
2 AnyElement, AnyView, ElementId, Hsla, IntoElement, Styled, Window, div, hsla, prelude::*,
3};
4use std::sync::Arc;
5
6use crate::utils::is_light;
7use crate::{Color, Icon, IconName, ToggleState};
8use crate::{ElevationIndex, KeyBinding, prelude::*};
9
10// TODO: Checkbox, CheckboxWithLabel, and Switch could all be
11// restructured to use a ToggleLike, similar to Button/Buttonlike, Label/Labellike
12
13/// Creates a new checkbox.
14pub fn checkbox(id: impl Into<ElementId>, toggle_state: ToggleState) -> Checkbox {
15 Checkbox::new(id, toggle_state)
16}
17
18/// Creates a new switch.
19pub fn switch(id: impl Into<ElementId>, toggle_state: ToggleState) -> Switch {
20 Switch::new(id, toggle_state)
21}
22
23/// The visual style of a toggle.
24#[derive(Debug, Default, Clone, PartialEq, Eq)]
25pub enum ToggleStyle {
26 /// Toggle has a transparent background
27 #[default]
28 Ghost,
29 /// Toggle has a filled background based on the
30 /// elevation index of the parent container
31 ElevationBased(ElevationIndex),
32 /// A custom style using a color to tint the toggle
33 Custom(Hsla),
34}
35
36/// # Checkbox
37///
38/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
39/// Each checkbox works independently from other checkboxes in the list,
40/// therefore checking an additional box does not affect any other selections.
41#[derive(IntoElement, IntoComponent)]
42#[component(scope = "Input")]
43pub struct Checkbox {
44 id: ElementId,
45 toggle_state: ToggleState,
46 disabled: bool,
47 placeholder: bool,
48 on_click: Option<Box<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>>,
49 filled: bool,
50 style: ToggleStyle,
51 tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
52 label: Option<SharedString>,
53}
54
55impl Checkbox {
56 /// Creates a new [`Checkbox`].
57 pub fn new(id: impl Into<ElementId>, checked: ToggleState) -> Self {
58 Self {
59 id: id.into(),
60 toggle_state: checked,
61 disabled: false,
62 on_click: None,
63 filled: false,
64 style: ToggleStyle::default(),
65 tooltip: None,
66 label: None,
67 placeholder: false,
68 }
69 }
70
71 /// Sets the disabled state of the [`Checkbox`].
72 pub fn disabled(mut self, disabled: bool) -> Self {
73 self.disabled = disabled;
74 self
75 }
76
77 /// Sets the disabled state of the [`Checkbox`].
78 pub fn placeholder(mut self, placeholder: bool) -> Self {
79 self.placeholder = placeholder;
80 self
81 }
82
83 /// Binds a handler to the [`Checkbox`] that will be called when clicked.
84 pub fn on_click(
85 mut self,
86 handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
87 ) -> Self {
88 self.on_click = Some(Box::new(handler));
89 self
90 }
91
92 /// Sets the `fill` setting of the checkbox, indicating whether it should be filled.
93 pub fn fill(mut self) -> Self {
94 self.filled = true;
95 self
96 }
97
98 /// Sets the style of the checkbox using the specified [`ToggleStyle`].
99 pub fn style(mut self, style: ToggleStyle) -> Self {
100 self.style = style;
101 self
102 }
103
104 /// Match the style of the checkbox to the current elevation using [`ToggleStyle::ElevationBased`].
105 pub fn elevation(mut self, elevation: ElevationIndex) -> Self {
106 self.style = ToggleStyle::ElevationBased(elevation);
107 self
108 }
109
110 /// Sets the tooltip for the checkbox.
111 pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
112 self.tooltip = Some(Box::new(tooltip));
113 self
114 }
115
116 /// Set the label for the checkbox.
117 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
118 self.label = Some(label.into());
119 self
120 }
121}
122
123impl Checkbox {
124 fn bg_color(&self, cx: &App) -> Hsla {
125 let style = self.style.clone();
126 match (style, self.filled) {
127 (ToggleStyle::Ghost, false) => cx.theme().colors().ghost_element_background,
128 (ToggleStyle::Ghost, true) => cx.theme().colors().element_background,
129 (ToggleStyle::ElevationBased(_), false) => gpui::transparent_black(),
130 (ToggleStyle::ElevationBased(elevation), true) => elevation.darker_bg(cx),
131 (ToggleStyle::Custom(_), false) => gpui::transparent_black(),
132 (ToggleStyle::Custom(color), true) => color.opacity(0.2),
133 }
134 }
135
136 fn border_color(&self, cx: &App) -> Hsla {
137 if self.disabled {
138 return cx.theme().colors().border_variant;
139 }
140
141 match self.style.clone() {
142 ToggleStyle::Ghost => cx.theme().colors().border,
143 ToggleStyle::ElevationBased(_) => cx.theme().colors().border,
144 ToggleStyle::Custom(color) => color.opacity(0.3),
145 }
146 }
147
148 /// container size
149 pub fn container_size() -> Pixels {
150 px(20.0)
151 }
152}
153
154impl RenderOnce for Checkbox {
155 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
156 let group_id = format!("checkbox_group_{:?}", self.id);
157 let color = if self.disabled {
158 Color::Disabled
159 } else {
160 Color::Selected
161 };
162 let icon = match self.toggle_state {
163 ToggleState::Selected => {
164 if self.placeholder {
165 None
166 } else {
167 Some(
168 Icon::new(IconName::Check)
169 .size(IconSize::Small)
170 .color(color),
171 )
172 }
173 }
174 ToggleState::Indeterminate => {
175 Some(Icon::new(IconName::Dash).size(IconSize::Small).color(color))
176 }
177 ToggleState::Unselected => None,
178 };
179
180 let bg_color = self.bg_color(cx);
181 let border_color = self.border_color(cx);
182 let hover_border_color = border_color.alpha(0.7);
183
184 let size = Self::container_size();
185
186 let checkbox = h_flex()
187 .id(self.id.clone())
188 .justify_center()
189 .items_center()
190 .size(size)
191 .group(group_id.clone())
192 .child(
193 div()
194 .flex()
195 .flex_none()
196 .justify_center()
197 .items_center()
198 .m_1()
199 .size_4()
200 .rounded_xs()
201 .bg(bg_color)
202 .border_1()
203 .border_color(border_color)
204 .when(self.disabled, |this| this.cursor_not_allowed())
205 .when(self.disabled, |this| {
206 this.bg(cx.theme().colors().element_disabled.opacity(0.6))
207 })
208 .when(!self.disabled, |this| {
209 this.group_hover(group_id.clone(), |el| el.border_color(hover_border_color))
210 })
211 .when(self.placeholder, |this| {
212 this.child(
213 div()
214 .flex_none()
215 .rounded_full()
216 .bg(color.color(cx).alpha(0.5))
217 .size(px(4.)),
218 )
219 })
220 .children(icon),
221 );
222
223 h_flex()
224 .id(self.id)
225 .gap(DynamicSpacing::Base06.rems(cx))
226 .child(checkbox)
227 .when_some(
228 self.on_click.filter(|_| !self.disabled),
229 |this, on_click| {
230 this.on_click(move |_, window, cx| {
231 on_click(&self.toggle_state.inverse(), window, cx)
232 })
233 },
234 )
235 // TODO: Allow label size to be different from default.
236 // TODO: Allow label color to be different from muted.
237 .when_some(self.label, |this, label| {
238 this.child(Label::new(label).color(Color::Muted))
239 })
240 .when_some(self.tooltip, |this, tooltip| {
241 this.tooltip(move |window, cx| tooltip(window, cx))
242 })
243 }
244}
245
246/// A [`Checkbox`] that has a [`Label`].
247#[derive(IntoElement, IntoComponent)]
248#[component(scope = "Input")]
249pub struct CheckboxWithLabel {
250 id: ElementId,
251 label: Label,
252 checked: ToggleState,
253 on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
254 filled: bool,
255 style: ToggleStyle,
256 checkbox_position: IconPosition,
257}
258
259// TODO: Remove `CheckboxWithLabel` now that `label` is a method of `Checkbox`.
260impl CheckboxWithLabel {
261 /// Creates a checkbox with an attached label.
262 pub fn new(
263 id: impl Into<ElementId>,
264 label: Label,
265 checked: ToggleState,
266 on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
267 ) -> Self {
268 Self {
269 id: id.into(),
270 label,
271 checked,
272 on_click: Arc::new(on_click),
273 filled: false,
274 style: ToggleStyle::default(),
275 checkbox_position: IconPosition::Start,
276 }
277 }
278
279 /// Sets the style of the checkbox using the specified [`ToggleStyle`].
280 pub fn style(mut self, style: ToggleStyle) -> Self {
281 self.style = style;
282 self
283 }
284
285 /// Match the style of the checkbox to the current elevation using [`ToggleStyle::ElevationBased`].
286 pub fn elevation(mut self, elevation: ElevationIndex) -> Self {
287 self.style = ToggleStyle::ElevationBased(elevation);
288 self
289 }
290
291 /// Sets the `fill` setting of the checkbox, indicating whether it should be filled.
292 pub fn fill(mut self) -> Self {
293 self.filled = true;
294 self
295 }
296
297 pub fn checkbox_position(mut self, position: IconPosition) -> Self {
298 self.checkbox_position = position;
299 self
300 }
301}
302
303impl RenderOnce for CheckboxWithLabel {
304 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
305 h_flex()
306 .gap(DynamicSpacing::Base08.rems(cx))
307 .when(self.checkbox_position == IconPosition::Start, |this| {
308 this.child(
309 Checkbox::new(self.id.clone(), self.checked)
310 .style(self.style.clone())
311 .when(self.filled, Checkbox::fill)
312 .on_click({
313 let on_click = self.on_click.clone();
314 move |checked, window, cx| {
315 (on_click)(checked, window, cx);
316 }
317 }),
318 )
319 })
320 .child(
321 div()
322 .id(SharedString::from(format!("{}-label", self.id)))
323 .on_click({
324 let on_click = self.on_click.clone();
325 move |_event, window, cx| {
326 (on_click)(&self.checked.inverse(), window, cx);
327 }
328 })
329 .child(self.label),
330 )
331 .when(self.checkbox_position == IconPosition::End, |this| {
332 this.child(
333 Checkbox::new(self.id.clone(), self.checked)
334 .style(self.style)
335 .when(self.filled, Checkbox::fill)
336 .on_click(move |checked, window, cx| {
337 (self.on_click)(checked, window, cx);
338 }),
339 )
340 })
341 }
342}
343
344/// # Switch
345///
346/// Switches are used to represent opposite states, such as enabled or disabled.
347#[derive(IntoElement, IntoComponent)]
348#[component(scope = "Input")]
349pub struct Switch {
350 id: ElementId,
351 toggle_state: ToggleState,
352 disabled: bool,
353 on_click: Option<Box<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>>,
354 label: Option<SharedString>,
355 key_binding: Option<KeyBinding>,
356}
357
358impl Switch {
359 /// Creates a new [`Switch`].
360 pub fn new(id: impl Into<ElementId>, state: ToggleState) -> Self {
361 Self {
362 id: id.into(),
363 toggle_state: state,
364 disabled: false,
365 on_click: None,
366 label: None,
367 key_binding: None,
368 }
369 }
370
371 /// Sets the disabled state of the [`Switch`].
372 pub fn disabled(mut self, disabled: bool) -> Self {
373 self.disabled = disabled;
374 self
375 }
376
377 /// Binds a handler to the [`Switch`] that will be called when clicked.
378 pub fn on_click(
379 mut self,
380 handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
381 ) -> Self {
382 self.on_click = Some(Box::new(handler));
383 self
384 }
385
386 /// Sets the label of the [`Switch`].
387 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
388 self.label = Some(label.into());
389 self
390 }
391
392 /// Display the keybinding that triggers the switch action.
393 pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
394 self.key_binding = key_binding.into();
395 self
396 }
397}
398
399impl RenderOnce for Switch {
400 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
401 let is_on = self.toggle_state == ToggleState::Selected;
402 let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 };
403 let base_color = cx.theme().colors().text;
404
405 let bg_color = if is_on {
406 cx.theme()
407 .colors()
408 .element_background
409 .blend(base_color.opacity(0.08))
410 } else {
411 cx.theme().colors().element_background
412 };
413 let thumb_color = base_color.opacity(0.8);
414 let thumb_hover_color = base_color;
415 let border_color = cx.theme().colors().border_variant;
416 // Lighter themes need higher contrast borders
417 let border_hover_color = if is_on {
418 border_color.blend(base_color.opacity(0.16 * adjust_ratio))
419 } else {
420 border_color.blend(base_color.opacity(0.05 * adjust_ratio))
421 };
422 let thumb_opacity = match (is_on, self.disabled) {
423 (_, true) => 0.2,
424 (true, false) => 1.0,
425 (false, false) => 0.5,
426 };
427
428 let group_id = format!("switch_group_{:?}", self.id);
429
430 let switch = h_flex()
431 .w(DynamicSpacing::Base32.rems(cx))
432 .h(DynamicSpacing::Base20.rems(cx))
433 .group(group_id.clone())
434 .child(
435 h_flex()
436 .when(is_on, |on| on.justify_end())
437 .when(!is_on, |off| off.justify_start())
438 .items_center()
439 .size_full()
440 .rounded_full()
441 .px(DynamicSpacing::Base02.px(cx))
442 .bg(bg_color)
443 .border_1()
444 .border_color(border_color)
445 .when(!self.disabled, |this| {
446 this.group_hover(group_id.clone(), |el| el.border_color(border_hover_color))
447 })
448 .child(
449 div()
450 .size(DynamicSpacing::Base12.rems(cx))
451 .rounded_full()
452 .bg(thumb_color)
453 .when(!self.disabled, |this| {
454 this.group_hover(group_id.clone(), |el| el.bg(thumb_hover_color))
455 })
456 .opacity(thumb_opacity),
457 ),
458 );
459
460 h_flex()
461 .id(self.id)
462 .gap(DynamicSpacing::Base06.rems(cx))
463 .cursor_pointer()
464 .child(switch)
465 .when_some(
466 self.on_click.filter(|_| !self.disabled),
467 |this, on_click| {
468 this.on_click(move |_, window, cx| {
469 on_click(&self.toggle_state.inverse(), window, cx)
470 })
471 },
472 )
473 .when_some(self.label, |this, label| {
474 this.child(Label::new(label).size(LabelSize::Small))
475 })
476 .children(self.key_binding)
477 }
478}
479
480/// A [`Switch`] that has a [`Label`].
481#[derive(IntoElement)]
482// #[component(scope = "input")]
483pub struct SwitchWithLabel {
484 id: ElementId,
485 label: Label,
486 toggle_state: ToggleState,
487 on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
488 disabled: bool,
489}
490
491impl SwitchWithLabel {
492 /// Creates a switch with an attached label.
493 pub fn new(
494 id: impl Into<ElementId>,
495 label: Label,
496 toggle_state: impl Into<ToggleState>,
497 on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
498 ) -> Self {
499 Self {
500 id: id.into(),
501 label,
502 toggle_state: toggle_state.into(),
503 on_click: Arc::new(on_click),
504 disabled: false,
505 }
506 }
507
508 /// Sets the disabled state of the [`SwitchWithLabel`].
509 pub fn disabled(mut self, disabled: bool) -> Self {
510 self.disabled = disabled;
511 self
512 }
513}
514
515impl RenderOnce for SwitchWithLabel {
516 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
517 h_flex()
518 .id(SharedString::from(format!("{}-container", self.id)))
519 .gap(DynamicSpacing::Base08.rems(cx))
520 .child(
521 Switch::new(self.id.clone(), self.toggle_state)
522 .disabled(self.disabled)
523 .on_click({
524 let on_click = self.on_click.clone();
525 move |checked, window, cx| {
526 (on_click)(checked, window, cx);
527 }
528 }),
529 )
530 .child(
531 div()
532 .id(SharedString::from(format!("{}-label", self.id)))
533 .child(self.label),
534 )
535 }
536}
537
538// View this component preview using `workspace: open component-preview`
539impl ComponentPreview for Checkbox {
540 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
541 v_flex()
542 .gap_6()
543 .children(vec![
544 example_group_with_title(
545 "States",
546 vec![
547 single_example(
548 "Unselected",
549 Checkbox::new("checkbox_unselected", ToggleState::Unselected)
550 .into_any_element(),
551 ),
552 single_example(
553 "Placeholder",
554 Checkbox::new("checkbox_indeterminate", ToggleState::Selected)
555 .placeholder(true)
556 .into_any_element(),
557 ),
558 single_example(
559 "Indeterminate",
560 Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate)
561 .into_any_element(),
562 ),
563 single_example(
564 "Selected",
565 Checkbox::new("checkbox_selected", ToggleState::Selected)
566 .into_any_element(),
567 ),
568 ],
569 ),
570 example_group_with_title(
571 "Styles",
572 vec![
573 single_example(
574 "Default",
575 Checkbox::new("checkbox_default", ToggleState::Selected)
576 .into_any_element(),
577 ),
578 single_example(
579 "Filled",
580 Checkbox::new("checkbox_filled", ToggleState::Selected)
581 .fill()
582 .into_any_element(),
583 ),
584 single_example(
585 "ElevationBased",
586 Checkbox::new("checkbox_elevation", ToggleState::Selected)
587 .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface))
588 .into_any_element(),
589 ),
590 single_example(
591 "Custom Color",
592 Checkbox::new("checkbox_custom", ToggleState::Selected)
593 .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7)))
594 .into_any_element(),
595 ),
596 ],
597 ),
598 example_group_with_title(
599 "Disabled",
600 vec![
601 single_example(
602 "Unselected",
603 Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
604 .disabled(true)
605 .into_any_element(),
606 ),
607 single_example(
608 "Selected",
609 Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
610 .disabled(true)
611 .into_any_element(),
612 ),
613 ],
614 ),
615 example_group_with_title(
616 "With Label",
617 vec![single_example(
618 "Default",
619 Checkbox::new("checkbox_with_label", ToggleState::Selected)
620 .label("Always save on quit")
621 .into_any_element(),
622 )],
623 ),
624 ])
625 .into_any_element()
626 }
627}
628
629// View this component preview using `workspace: open component-preview`
630impl ComponentPreview for Switch {
631 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
632 v_flex()
633 .gap_6()
634 .children(vec![
635 example_group_with_title(
636 "States",
637 vec![
638 single_example(
639 "Off",
640 Switch::new("switch_off", ToggleState::Unselected)
641 .on_click(|_, _, _cx| {})
642 .into_any_element(),
643 ),
644 single_example(
645 "On",
646 Switch::new("switch_on", ToggleState::Selected)
647 .on_click(|_, _, _cx| {})
648 .into_any_element(),
649 ),
650 ],
651 ),
652 example_group_with_title(
653 "Disabled",
654 vec![
655 single_example(
656 "Off",
657 Switch::new("switch_disabled_off", ToggleState::Unselected)
658 .disabled(true)
659 .into_any_element(),
660 ),
661 single_example(
662 "On",
663 Switch::new("switch_disabled_on", ToggleState::Selected)
664 .disabled(true)
665 .into_any_element(),
666 ),
667 ],
668 ),
669 example_group_with_title(
670 "With Label",
671 vec![
672 single_example(
673 "Label",
674 Switch::new("switch_with_label", ToggleState::Selected)
675 .label("Always save on quit")
676 .into_any_element(),
677 ),
678 // TODO: Where did theme_preview_keybinding go?
679 // single_example(
680 // "Keybinding",
681 // Switch::new("switch_with_keybinding", ToggleState::Selected)
682 // .key_binding(theme_preview_keybinding("cmd-shift-e"))
683 // .into_any_element(),
684 // ),
685 ],
686 ),
687 ])
688 .into_any_element()
689 }
690}
691
692// View this component preview using `workspace: open component-preview`
693impl ComponentPreview for CheckboxWithLabel {
694 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
695 v_flex()
696 .gap_6()
697 .children(vec![example_group_with_title(
698 "States",
699 vec![
700 single_example(
701 "Unselected",
702 CheckboxWithLabel::new(
703 "checkbox_with_label_unselected",
704 Label::new("Always save on quit"),
705 ToggleState::Unselected,
706 |_, _, _| {},
707 )
708 .into_any_element(),
709 ),
710 single_example(
711 "Indeterminate",
712 CheckboxWithLabel::new(
713 "checkbox_with_label_indeterminate",
714 Label::new("Always save on quit"),
715 ToggleState::Indeterminate,
716 |_, _, _| {},
717 )
718 .into_any_element(),
719 ),
720 single_example(
721 "Selected",
722 CheckboxWithLabel::new(
723 "checkbox_with_label_selected",
724 Label::new("Always save on quit"),
725 ToggleState::Selected,
726 |_, _, _| {},
727 )
728 .into_any_element(),
729 ),
730 ],
731 )])
732 .into_any_element()
733 }
734}