gpui_component/
checkbox.rs

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/// A Checkbox element.
14#[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    /// Set the tab stop for the checkbox, default is true.
62    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
63        self.tab_stop = tab_stop;
64        self
65    }
66
67    /// Set the tab index for the checkbox, default is 0.
68    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                    // Avoid focus on mouse down.
291                    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}