1use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
2
3use crate::prelude::*;
4use crate::{Color, Icon, IconName, Selection};
5
6pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
7
8/// # Checkbox
9///
10/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
11/// Each checkbox works independently from other checkboxes in the list,
12/// therefore checking an additional box does not affect any other selections.
13#[derive(IntoElement)]
14pub struct Checkbox {
15 id: ElementId,
16 checked: Selection,
17 disabled: bool,
18 on_click: Option<CheckHandler>,
19}
20
21impl Checkbox {
22 pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
23 Self {
24 id: id.into(),
25 checked,
26 disabled: false,
27 on_click: None,
28 }
29 }
30
31 pub fn disabled(mut self, disabled: bool) -> Self {
32 self.disabled = disabled;
33 self
34 }
35
36 pub fn on_click(
37 mut self,
38 handler: impl 'static + Fn(&Selection, &mut WindowContext) + Send + Sync,
39 ) -> Self {
40 self.on_click = Some(Box::new(handler));
41 self
42 }
43}
44
45impl RenderOnce for Checkbox {
46 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
47 let group_id = format!("checkbox_group_{:?}", self.id);
48
49 let icon = match self.checked {
50 Selection::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
51 if self.disabled {
52 Color::Disabled
53 } else {
54 Color::Selected
55 },
56 )),
57 Selection::Indeterminate => Some(
58 Icon::new(IconName::Dash)
59 .size(IconSize::Small)
60 .color(if self.disabled {
61 Color::Disabled
62 } else {
63 Color::Selected
64 }),
65 ),
66 Selection::Unselected => None,
67 };
68
69 // A checkbox could be in an indeterminate state,
70 // for example the indeterminate state could represent:
71 // - a group of options of which only some are selected
72 // - an enabled option that is no longer available
73 // - a previously agreed to license that has been updated
74 //
75 // For the sake of styles we treat the indeterminate state as selected,
76 // but it's icon will be different.
77 let selected =
78 self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
79
80 // We could use something like this to make the checkbox background when selected:
81 //
82 // ```rs
83 // ...
84 // .when(selected, |this| {
85 // this.bg(cx.theme().colors().element_selected)
86 // })
87 // ```
88 //
89 // But we use a match instead here because the checkbox might be disabled,
90 // and it could be disabled _while_ it is selected, as well as while it is not selected.
91 let (bg_color, border_color) = match (self.disabled, selected) {
92 (true, _) => (
93 cx.theme().colors().ghost_element_disabled,
94 cx.theme().colors().border_disabled,
95 ),
96 (false, true) => (
97 cx.theme().colors().element_selected,
98 cx.theme().colors().border,
99 ),
100 (false, false) => (
101 cx.theme().colors().element_background,
102 cx.theme().colors().border,
103 ),
104 };
105
106 h_stack()
107 .id(self.id)
108 // Rather than adding `px_1()` to add some space around the checkbox,
109 // we use a larger parent element to create a slightly larger
110 // click area for the checkbox.
111 .size_5()
112 // Because we've enlarged the click area, we need to create a
113 // `group` to pass down interactivity events to the checkbox.
114 .group(group_id.clone())
115 .child(
116 div()
117 .flex()
118 // This prevent the flex element from growing
119 // or shrinking in response to any size changes
120 .flex_none()
121 // The combo of `justify_center()` and `items_center()`
122 // is used frequently to center elements in a flex container.
123 //
124 // We use this to center the icon in the checkbox.
125 .justify_center()
126 .items_center()
127 .m_1()
128 .size_4()
129 .rounded_sm()
130 .bg(bg_color)
131 .border()
132 .border_color(border_color)
133 // We only want the interactivity states to fire when we
134 // are in a checkbox that isn't disabled.
135 .when(!self.disabled, |this| {
136 // Here instead of `hover()` we use `group_hover()`
137 // to pass it the group id.
138 this.group_hover(group_id.clone(), |el| {
139 el.bg(cx.theme().colors().element_hover)
140 })
141 })
142 .children(icon),
143 )
144 .when_some(
145 self.on_click.filter(|_| !self.disabled),
146 |this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
147 )
148 }
149}