Skip to main content

liora_components/
checkbox.rs

1use crate::gpui_compat::element_id;
2use crate::motion::pop_in;
3use gpui::{
4    App, Context, EventEmitter, FocusHandle, Focusable, Hsla, KeyBinding, MouseButton, Render,
5    Rgba, SharedString, Window, prelude::*, px,
6};
7use liora_icons::Icon;
8use liora_icons_lucide::IconName;
9
10fn rgba(r: u8, g: u8, b: u8, a: f32) -> Hsla {
11    Rgba {
12        r: r as f32 / 255.0,
13        g: g as f32 / 255.0,
14        b: b as f32 / 255.0,
15        a,
16    }
17    .into()
18}
19
20gpui::actions!(checkbox, [CheckboxToggle]);
21
22pub struct Checkbox {
23    checked: bool,
24    disabled: bool,
25    label: Option<SharedString>,
26    focus_handle: FocusHandle,
27    on_change: Option<Box<dyn Fn(bool, &mut Window, &mut App) + 'static>>,
28}
29
30#[derive(Clone, Copy)]
31pub struct CheckboxChanged(pub bool);
32
33impl EventEmitter<CheckboxChanged> for Checkbox {}
34
35impl Checkbox {
36    pub fn new(checked: bool, cx: &mut Context<Self>) -> Self {
37        Self {
38            checked,
39            disabled: false,
40            label: None,
41            focus_handle: cx.focus_handle(),
42            on_change: None,
43        }
44    }
45
46    pub fn disabled(mut self, d: bool) -> Self {
47        self.disabled = d;
48        self
49    }
50    pub fn label(mut self, text: impl Into<SharedString>) -> Self {
51        self.label = Some(text.into());
52        self
53    }
54    pub fn on_change(mut self, cb: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
55        self.on_change = Some(Box::new(cb));
56        self
57    }
58
59    pub fn set_disabled(&mut self, d: bool, cx: &mut Context<Self>) {
60        self.disabled = d;
61        cx.notify();
62    }
63
64    pub fn register_key_bindings(cx: &mut App) {
65        cx.bind_keys([
66            KeyBinding::new("space", CheckboxToggle, None),
67            KeyBinding::new("enter", CheckboxToggle, None),
68        ]);
69    }
70
71    fn toggle(&mut self, _: &CheckboxToggle, window: &mut Window, cx: &mut Context<Self>) {
72        if !self.disabled {
73            self.checked = !self.checked;
74            cx.emit(CheckboxChanged(self.checked));
75            cx.notify();
76            if let Some(ref cb) = self.on_change {
77                cb(self.checked, window, cx);
78            }
79        }
80    }
81}
82
83impl Focusable for Checkbox {
84    fn focus_handle(&self, _cx: &App) -> FocusHandle {
85        self.focus_handle.clone()
86    }
87}
88
89impl Render for Checkbox {
90    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
91        let theme = &cx.global::<liora_core::Config>().theme;
92        let focused = self.focus_handle.is_focused(_window);
93        let sz = 16.0;
94
95        let (bg, border, check_color) = if self.disabled {
96            (
97                theme.neutral.hover,
98                theme.neutral.border,
99                theme.neutral.text_disabled,
100            )
101        } else if self.checked {
102            (
103                theme.primary.base,
104                theme.primary.base,
105                rgba(255, 255, 255, 1.0),
106            )
107        } else {
108            (
109                rgba(0, 0, 0, 0.0),
110                if focused {
111                    theme.primary.base
112                } else {
113                    theme.neutral.border
114                },
115                rgba(0, 0, 0, 0.0),
116            )
117        };
118
119        let mut row = gpui::div()
120            .flex()
121            .flex_row()
122            .items_center()
123            .gap_2()
124            .on_action(cx.listener(Self::toggle));
125
126        if !self.disabled {
127            row = row.cursor_pointer().track_focus(&self.focus_handle);
128            row = row.on_mouse_down(
129                MouseButton::Left,
130                cx.listener(|this, _, window, cx| {
131                    window.focus(&this.focus_handle);
132                }),
133            );
134            row = row.on_mouse_up(
135                MouseButton::Left,
136                cx.listener(|this, _, window, cx| {
137                    this.toggle(&CheckboxToggle, window, cx);
138                }),
139            );
140        } else {
141            row = row.cursor_not_allowed();
142        }
143
144        let mut box_el = gpui::div()
145            .flex_none()
146            .w(px(sz))
147            .h(px(sz))
148            .rounded(px(2.0))
149            .bg(bg)
150            .border_1()
151            .border_color(border)
152            .flex()
153            .items_center()
154            .justify_center();
155
156        if self.checked {
157            box_el = box_el.child(pop_in(
158                element_id(format!(
159                    "liora-checkbox-check-motion-{}",
160                    cx.entity().entity_id()
161                )),
162                gpui::div()
163                    .flex()
164                    .items_center()
165                    .justify_center()
166                    .child(Icon::new(IconName::Check).size(px(12.0)).color(check_color)),
167            ));
168        }
169
170        row = row.child(box_el);
171
172        if let Some(ref label) = self.label {
173            row = row.child(
174                gpui::div()
175                    .text_size(px(theme.font_size.md))
176                    .text_color(theme.neutral.text_1)
177                    .child(label.clone()),
178            );
179        }
180
181        row
182    }
183}