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}