ccf_gpui_widgets/widgets/
checkbox_group.rs1use std::collections::HashSet;
26
27use gpui::prelude::*;
28use gpui::*;
29
30use crate::theme::{get_theme_or, Theme};
31use super::focus_navigation::{handle_tab_navigation, with_focus_actions, EnabledCursorExt};
32
33#[derive(Clone, Debug)]
35pub enum CheckboxGroupEvent {
36 Change(Vec<String>),
38}
39
40pub struct CheckboxGroup {
42 choices: Vec<String>,
43 selected: HashSet<String>,
44 focus_handle: FocusHandle,
45 highlight_index: usize,
46 custom_theme: Option<Theme>,
47 enabled: bool,
49}
50
51impl EventEmitter<CheckboxGroupEvent> for CheckboxGroup {}
52
53impl Focusable for CheckboxGroup {
54 fn focus_handle(&self, _cx: &App) -> FocusHandle {
55 self.focus_handle.clone()
56 }
57}
58
59impl CheckboxGroup {
60 pub fn new(cx: &mut Context<Self>) -> Self {
62 Self {
63 choices: Vec::new(),
64 selected: HashSet::new(),
65 focus_handle: cx.focus_handle().tab_stop(true),
66 highlight_index: 0,
67 custom_theme: None,
68 enabled: true,
69 }
70 }
71
72 #[must_use]
74 pub fn choices(mut self, choices: Vec<String>) -> Self {
75 self.choices = choices;
76 self
77 }
78
79 #[must_use]
81 pub fn with_selected(mut self, selected: Vec<String>) -> Self {
82 self.selected = selected.into_iter().collect();
83 self
84 }
85
86 #[must_use]
88 pub fn theme(mut self, theme: Theme) -> Self {
89 self.custom_theme = Some(theme);
90 self
91 }
92
93 #[must_use]
95 pub fn with_enabled(mut self, enabled: bool) -> Self {
96 self.enabled = enabled;
97 self
98 }
99
100 pub fn get_selected(&self) -> Vec<String> {
102 let mut result: Vec<String> = self.selected.iter().cloned().collect();
103 result.sort();
104 result
105 }
106
107 pub fn is_selected(&self, value: &str) -> bool {
109 self.selected.contains(value)
110 }
111
112 pub fn set_selected(&mut self, selected: Vec<String>, cx: &mut Context<Self>) {
114 self.selected = selected.into_iter().collect();
115 cx.emit(CheckboxGroupEvent::Change(self.get_selected()));
116 cx.notify();
117 }
118
119 pub fn focus_handle(&self) -> &FocusHandle {
121 &self.focus_handle
122 }
123
124 pub fn is_enabled(&self) -> bool {
126 self.enabled
127 }
128
129 pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
131 if self.enabled != enabled {
132 self.enabled = enabled;
133 cx.notify();
134 }
135 }
136
137 fn toggle_choice(&mut self, choice: String, cx: &mut Context<Self>) {
138 if self.selected.contains(&choice) {
139 self.selected.remove(&choice);
140 } else {
141 self.selected.insert(choice);
142 }
143 cx.emit(CheckboxGroupEvent::Change(self.get_selected()));
144 }
145}
146
147impl Render for CheckboxGroup {
148 fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
149 let theme = get_theme_or(cx, self.custom_theme.as_ref());
150 let focus_handle = self.focus_handle.clone();
151 let is_focused = self.focus_handle.is_focused(window);
152 let highlight_index = self.highlight_index;
153 let num_choices = self.choices.len();
154 let enabled = self.enabled;
155
156 with_focus_actions(
157 div()
158 .id("ccf_checkbox_group")
159 .track_focus(&focus_handle)
160 .tab_stop(enabled),
161 cx,
162 )
163 .on_key_down(cx.listener(move |group, event: &KeyDownEvent, window, cx| {
164 if !group.enabled {
165 return;
166 }
167 if handle_tab_navigation(event, window) {
168 return;
169 }
170 match event.keystroke.key.as_str() {
171 "up" => {
172 if group.highlight_index > 0 {
173 group.highlight_index -= 1;
174 } else if num_choices > 0 {
175 group.highlight_index = num_choices - 1;
176 }
177 cx.notify();
178 }
179 "down" => {
180 if group.highlight_index < num_choices.saturating_sub(1) {
181 group.highlight_index += 1;
182 } else {
183 group.highlight_index = 0;
184 }
185 cx.notify();
186 }
187 "space" => {
188 if let Some(choice) = group.choices.get(group.highlight_index).cloned() {
189 group.toggle_choice(choice, cx);
190 }
191 cx.notify();
192 }
193 _ => {}
194 }
195 }))
196 .flex()
197 .flex_col()
198 .gap_1()
199 .p_2()
200 .when(enabled, |d| d.bg(rgb(theme.bg_input)))
201 .when(!enabled, |d| d.bg(rgb(theme.disabled_bg)))
202 .border_1()
203 .when(enabled, |d| {
204 d.border_color(if is_focused { rgb(theme.border_focus) } else { rgb(theme.border_input) })
205 })
206 .when(!enabled, |d| d.border_color(rgb(theme.disabled_bg)))
207 .rounded_md()
208 .children(self.choices.iter().enumerate().map(|(idx, choice)| {
209 let choice_clone = choice.clone();
210 let is_selected = self.selected.contains(choice);
211 let is_highlighted = is_focused && idx == highlight_index && enabled;
212
213 div()
214 .id(("ccf_checkbox_group_choice", idx))
215 .flex()
216 .flex_row()
217 .gap_2()
218 .items_center()
219 .py_1()
220 .px_1()
221 .cursor_for_enabled(enabled)
222 .rounded_sm()
223 .when(is_highlighted, |d| d.bg(rgb(theme.bg_input_hover)))
224 .when(!is_highlighted && enabled, |d| d.hover(|d| d.bg(rgb(theme.bg_input_hover))))
225 .when(enabled, |d| {
226 d.on_click(cx.listener(move |group, _event, window, cx| {
227 group.focus_handle.focus(window);
228 group.highlight_index = idx;
229 group.toggle_choice(choice_clone.clone(), cx);
230 cx.notify();
231 }))
232 })
233 .child({
234 let (bg_color, border_color, check_color) = if enabled {
236 if is_selected {
237 (theme.accent, theme.border_checkbox, theme.bg_white)
238 } else {
239 (theme.bg_input, theme.border_checkbox, 0)
240 }
241 } else if is_selected {
242 (theme.disabled_text, theme.disabled_text, theme.disabled_bg)
243 } else {
244 (theme.disabled_bg, theme.disabled_text, 0)
245 };
246
247 div()
248 .w(px(16.))
249 .h(px(16.))
250 .border_1()
251 .border_color(rgb(border_color))
252 .bg(rgb(bg_color))
253 .rounded(px(3.))
254 .when(is_selected, |d| {
255 d.child(
256 div()
257 .flex()
258 .items_center()
259 .justify_center()
260 .size_full()
261 .text_color(rgb(check_color))
262 .text_xs()
263 .child("✓")
264 )
265 })
266 })
267 .child(
268 div()
269 .text_sm()
270 .when(enabled, |d| d.text_color(rgb(theme.text_value)))
271 .when(!enabled, |d| d.text_color(rgb(theme.disabled_text)))
272 .child(choice.clone())
273 )
274 }))
275 }
276}