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