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    /// Create a new Checkbox with the given id.
31    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    /// Set the label for the checkbox.
48    pub fn label(mut self, label: impl Into<Text>) -> Self {
49        self.label = Some(label.into());
50        self
51    }
52
53    /// Set the checked state for the checkbox.
54    pub fn checked(mut self, checked: bool) -> Self {
55        self.checked = checked;
56        self
57    }
58
59    /// Set the click handler for the checkbox.
60    ///
61    /// The `&bool` parameter indicates the new checked state after the click.
62    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    /// Set the tab stop for the checkbox, default is true.
68    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
69        self.tab_stop = tab_stop;
70        self
71    }
72
73    /// Set the tab index for the checkbox, default is 0.
74    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                    // Avoid focus on mouse down.
297                    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}