1use std::{rc::Rc, time::Duration};
2
3use crate::{
4 text::Text, v_flex, ActiveTheme, Disableable, FocusableExt, IconName, Selectable, Sizable,
5 Size, StyledExt as _,
6};
7use gpui::{
8 div, prelude::FluentBuilder as _, px, relative, rems, svg, Animation, AnimationExt, AnyElement,
9 App, Div, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
10 StatefulInteractiveElement, StyleRefinement, Styled, Window,
11};
12
13#[derive(IntoElement)]
15pub struct Checkbox {
16 id: ElementId,
17 base: Div,
18 style: StyleRefinement,
19 label: Option<Text>,
20 children: Vec<AnyElement>,
21 checked: bool,
22 disabled: bool,
23 size: Size,
24 tab_stop: bool,
25 tab_index: isize,
26 on_click: Option<Rc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
27}
28
29impl Checkbox {
30 pub fn new(id: impl Into<ElementId>) -> Self {
31 Self {
32 id: id.into(),
33 base: div(),
34 style: StyleRefinement::default(),
35 label: None,
36 children: Vec::new(),
37 checked: false,
38 disabled: false,
39 size: Size::default(),
40 on_click: None,
41 tab_stop: true,
42 tab_index: 0,
43 }
44 }
45
46 pub fn label(mut self, label: impl Into<Text>) -> Self {
47 self.label = Some(label.into());
48 self
49 }
50
51 pub fn checked(mut self, checked: bool) -> Self {
52 self.checked = checked;
53 self
54 }
55
56 pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
57 self.on_click = Some(Rc::new(handler));
58 self
59 }
60
61 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
63 self.tab_stop = tab_stop;
64 self
65 }
66
67 pub fn tab_index(mut self, tab_index: isize) -> Self {
69 self.tab_index = tab_index;
70 self
71 }
72
73 fn handle_click(
74 on_click: &Option<Rc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
75 checked: bool,
76 window: &mut Window,
77 cx: &mut App,
78 ) {
79 let new_checked = !checked;
80 if let Some(f) = on_click {
81 (f)(&new_checked, window, cx);
82 }
83 }
84}
85
86impl InteractiveElement for Checkbox {
87 fn interactivity(&mut self) -> &mut gpui::Interactivity {
88 self.base.interactivity()
89 }
90}
91impl StatefulInteractiveElement for Checkbox {}
92
93impl Styled for Checkbox {
94 fn style(&mut self) -> &mut gpui::StyleRefinement {
95 &mut self.style
96 }
97}
98
99impl Disableable for Checkbox {
100 fn disabled(mut self, disabled: bool) -> Self {
101 self.disabled = disabled;
102 self
103 }
104}
105
106impl Selectable for Checkbox {
107 fn selected(self, selected: bool) -> Self {
108 self.checked(selected)
109 }
110
111 fn is_selected(&self) -> bool {
112 self.checked
113 }
114}
115
116impl ParentElement for Checkbox {
117 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
118 self.children.extend(elements);
119 }
120}
121
122impl Sizable for Checkbox {
123 fn with_size(mut self, size: impl Into<Size>) -> Self {
124 self.size = size.into();
125 self
126 }
127}
128
129pub(crate) fn checkbox_check_icon(
130 id: ElementId,
131 size: Size,
132 checked: bool,
133 disabled: bool,
134 window: &mut Window,
135 cx: &mut App,
136) -> impl IntoElement {
137 let toggle_state = window.use_keyed_state(id, cx, |_, _| checked);
138 let color = if disabled {
139 cx.theme().primary_foreground.opacity(0.5)
140 } else {
141 cx.theme().primary_foreground
142 };
143
144 svg()
145 .absolute()
146 .top_px()
147 .left_px()
148 .map(|this| match size {
149 Size::XSmall => this.size_2(),
150 Size::Small => this.size_2p5(),
151 Size::Medium => this.size_3(),
152 Size::Large => this.size_3p5(),
153 _ => this.size_3(),
154 })
155 .text_color(color)
156 .map(|this| match checked {
157 true => this.path(IconName::Check.path()),
158 _ => this,
159 })
160 .map(|this| {
161 if !disabled && checked != *toggle_state.read(cx) {
162 let duration = Duration::from_secs_f64(0.25);
163 cx.spawn({
164 let toggle_state = toggle_state.clone();
165 async move |cx| {
166 cx.background_executor().timer(duration).await;
167 _ = toggle_state.update(cx, |this, _| *this = checked);
168 }
169 })
170 .detach();
171
172 this.with_animation(
173 ElementId::NamedInteger("toggle".into(), checked as u64),
174 Animation::new(Duration::from_secs_f64(0.25)),
175 move |this, delta| {
176 this.opacity(if checked { 1.0 * delta } else { 1.0 - delta })
177 },
178 )
179 .into_any_element()
180 } else {
181 this.into_any_element()
182 }
183 })
184}
185
186impl RenderOnce for Checkbox {
187 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
188 let checked = self.checked;
189
190 let focus_handle = window
191 .use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
192 .read(cx)
193 .clone();
194 let is_focused = focus_handle.is_focused(window);
195
196 let border_color = if checked {
197 cx.theme().primary
198 } else {
199 cx.theme().input
200 };
201 let color = if self.disabled {
202 border_color.opacity(0.5)
203 } else {
204 border_color
205 };
206 let radius = cx.theme().radius.min(px(4.));
207
208 div().child(
209 self.base
210 .id(self.id.clone())
211 .when(!self.disabled, |this| {
212 this.track_focus(
213 &focus_handle
214 .tab_stop(self.tab_stop)
215 .tab_index(self.tab_index),
216 )
217 })
218 .h_flex()
219 .gap_2()
220 .items_start()
221 .line_height(relative(1.))
222 .text_color(cx.theme().foreground)
223 .map(|this| match self.size {
224 Size::XSmall => this.text_xs(),
225 Size::Small => this.text_sm(),
226 Size::Medium => this.text_base(),
227 Size::Large => this.text_lg(),
228 _ => this,
229 })
230 .when(self.disabled, |this| {
231 this.text_color(cx.theme().muted_foreground)
232 })
233 .rounded(cx.theme().radius * 0.5)
234 .focus_ring(is_focused, px(2.), window, cx)
235 .refine_style(&self.style)
236 .child(
237 div()
238 .relative()
239 .map(|this| match self.size {
240 Size::XSmall => this.size_3(),
241 Size::Small => this.size_3p5(),
242 Size::Medium => this.size_4(),
243 Size::Large => this.size(rems(1.125)),
244 _ => this.size_4(),
245 })
246 .flex_shrink_0()
247 .border_1()
248 .border_color(color)
249 .rounded(radius)
250 .when(cx.theme().shadow && !self.disabled, |this| this.shadow_xs())
251 .map(|this| match checked {
252 false => this.bg(cx.theme().background),
253 _ => this.bg(color),
254 })
255 .child(checkbox_check_icon(
256 self.id,
257 self.size,
258 checked,
259 self.disabled,
260 window,
261 cx,
262 )),
263 )
264 .when(self.label.is_some() || !self.children.is_empty(), |this| {
265 this.child(
266 v_flex()
267 .w_full()
268 .line_height(relative(1.2))
269 .gap_1()
270 .map(|this| {
271 if let Some(label) = self.label {
272 this.child(
273 div()
274 .size_full()
275 .text_color(cx.theme().foreground)
276 .when(self.disabled, |this| {
277 this.text_color(cx.theme().muted_foreground)
278 })
279 .line_height(relative(1.))
280 .child(label),
281 )
282 } else {
283 this
284 }
285 })
286 .children(self.children),
287 )
288 })
289 .on_mouse_down(gpui::MouseButton::Left, |_, window, _| {
290 window.prevent_default();
292 })
293 .when(!self.disabled, |this| {
294 this.on_click({
295 let on_click = self.on_click.clone();
296 move |_, window, cx| {
297 window.prevent_default();
298 cx.stop_propagation();
299 Self::handle_click(&on_click, checked, window, cx);
300 }
301 })
302 }),
303 )
304 }
305}