ccf_gpui_widgets/widgets/
segmented_control.rs1use gpui::prelude::*;
69use gpui::*;
70
71use crate::theme::{get_theme_or, Theme};
72use super::focus_navigation::{handle_tab_navigation, with_focus_actions, EnabledCursorExt};
73use super::selection::SelectionItem;
74
75#[derive(Clone, Debug)]
77pub enum SegmentedControlEvent<T: SelectionItem> {
78 Change(T),
80}
81
82#[derive(Clone, PartialEq, Debug)]
87pub struct SegmentOption {
88 pub value: String,
90 pub label: String,
92}
93
94impl SegmentOption {
95 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
97 Self {
98 value: value.into(),
99 label: label.into(),
100 }
101 }
102}
103
104impl SelectionItem for SegmentOption {
105 fn label(&self) -> SharedString {
106 self.label.clone().into()
107 }
108
109 fn id(&self) -> ElementId {
110 let id_str = format!("segment_{}", self.value.to_lowercase().replace(' ', "_"));
111 ElementId::Name(id_str.into())
112 }
113}
114
115pub struct SegmentedControl<T: SelectionItem = SegmentOption> {
120 items: Vec<T>,
121 selected: T,
122 focus_handle: FocusHandle,
123 highlight_index: usize,
124 custom_theme: Option<Theme>,
125 enabled: bool,
126 button_gap: Pixels,
128}
129
130impl<T: SelectionItem> EventEmitter<SegmentedControlEvent<T>> for SegmentedControl<T> {}
131
132impl<T: SelectionItem> Focusable for SegmentedControl<T> {
133 fn focus_handle(&self, _cx: &App) -> FocusHandle {
134 self.focus_handle.clone()
135 }
136}
137
138impl SegmentedControl<SegmentOption> {
139 pub fn new(cx: &mut Context<Self>) -> Self {
143 Self {
144 items: Vec::new(),
145 selected: SegmentOption::new("", ""),
146 focus_handle: cx.focus_handle().tab_stop(true),
147 highlight_index: 0,
148 custom_theme: None,
149 enabled: true,
150 button_gap: px(8.0),
151 }
152 }
153
154 #[must_use]
156 pub fn options(mut self, options: Vec<(&str, &str)>) -> Self {
157 self.items = options
158 .into_iter()
159 .map(|(v, l)| SegmentOption::new(v, l))
160 .collect();
161 if !self.items.is_empty() && self.selected.value.is_empty() {
162 self.selected = self.items[0].clone();
163 }
164 self
165 }
166
167 #[must_use]
169 pub fn with_options(mut self, options: Vec<SegmentOption>) -> Self {
170 self.items = options;
171 if !self.items.is_empty() && self.selected.value.is_empty() {
172 self.selected = self.items[0].clone();
173 }
174 self
175 }
176
177 #[must_use]
181 pub fn with_selected_value(mut self, value: &str) -> Self {
182 if let Some(index) = self.items.iter().position(|o| o.value == value) {
183 self.selected = self.items[index].clone();
184 self.highlight_index = index;
185 }
186 self
187 }
188
189 pub fn set_selected_value(&mut self, value: &str, cx: &mut Context<Self>) {
193 if let Some(index) = self.items.iter().position(|o| o.value == value) {
194 if self.selected.value != value {
195 self.selected = self.items[index].clone();
196 self.highlight_index = index;
197 cx.emit(SegmentedControlEvent::Change(self.selected.clone()));
198 cx.notify();
199 }
200 }
201 }
202
203 pub fn selected_value(&self) -> &str {
207 &self.selected.value
208 }
209}
210
211impl<T: SelectionItem> SegmentedControl<T> {
212 pub fn new_with_items(items: Vec<T>, selected: T, cx: &mut Context<Self>) -> Self {
214 let highlight_index = items.iter().position(|i| *i == selected).unwrap_or(0);
215 Self {
216 items,
217 selected,
218 focus_handle: cx.focus_handle().tab_stop(true),
219 highlight_index,
220 custom_theme: None,
221 enabled: true,
222 button_gap: px(8.0),
223 }
224 }
225
226 #[must_use]
228 pub fn with_items(mut self, items: Vec<T>) -> Self {
229 self.items = items;
230 if !self.items.is_empty() {
231 self.selected = self.items[0].clone();
232 self.highlight_index = 0;
233 }
234 self
235 }
236
237 #[must_use]
239 pub fn with_selected(mut self, item: T) -> Self {
240 if let Some(index) = self.items.iter().position(|i| *i == item) {
241 self.selected = item;
242 self.highlight_index = index;
243 }
244 self
245 }
246
247 #[must_use]
249 pub fn with_selected_index(mut self, index: usize) -> Self {
250 if let Some(item) = self.items.get(index) {
251 self.selected = item.clone();
252 self.highlight_index = index;
253 }
254 self
255 }
256
257 #[must_use]
259 pub fn theme(mut self, theme: Theme) -> Self {
260 self.custom_theme = Some(theme);
261 self
262 }
263
264 #[must_use]
266 pub fn with_enabled(mut self, enabled: bool) -> Self {
267 self.enabled = enabled;
268 self
269 }
270
271 #[must_use]
275 pub fn with_button_gap(mut self, gap: impl Into<Pixels>) -> Self {
276 self.button_gap = gap.into();
277 self
278 }
279
280 pub fn selected(&self) -> &T {
282 &self.selected
283 }
284
285 pub fn selected_index(&self) -> usize {
287 self.items.iter().position(|i| *i == self.selected).unwrap_or(0)
288 }
289
290 pub fn set_selected(&mut self, item: T, cx: &mut Context<Self>) {
294 if let Some(index) = self.items.iter().position(|i| *i == item) {
295 if self.selected != item {
296 self.selected = item;
297 self.highlight_index = index;
298 cx.emit(SegmentedControlEvent::Change(self.selected.clone()));
299 cx.notify();
300 }
301 }
302 }
303
304 pub fn set_selected_index(&mut self, index: usize, cx: &mut Context<Self>) {
308 if let Some(item) = self.items.get(index).cloned() {
309 if self.selected != item {
310 self.selected = item;
311 self.highlight_index = index;
312 cx.emit(SegmentedControlEvent::Change(self.selected.clone()));
313 cx.notify();
314 }
315 }
316 }
317
318 pub fn focus_handle(&self) -> &FocusHandle {
320 &self.focus_handle
321 }
322
323 pub fn is_enabled(&self) -> bool {
325 self.enabled
326 }
327
328 pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
330 if self.enabled != enabled {
331 self.enabled = enabled;
332 cx.notify();
333 }
334 }
335
336 fn select_by_index(&mut self, cx: &mut Context<Self>) {
337 if let Some(item) = self.items.get(self.highlight_index) {
338 if self.selected != *item {
339 self.selected = item.clone();
340 cx.emit(SegmentedControlEvent::Change(self.selected.clone()));
341 }
342 }
343 }
344}
345
346impl<T: SelectionItem> Render for SegmentedControl<T> {
347 fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
348 let theme = get_theme_or(cx, self.custom_theme.as_ref());
349 let focus_handle = self.focus_handle.clone();
350 let is_focused = self.focus_handle.is_focused(window);
351 let highlight_index = self.highlight_index;
352 let num_items = self.items.len();
353 let enabled = self.enabled;
354
355 with_focus_actions(
356 div()
357 .id("ccf_segmented_control")
358 .track_focus(&focus_handle)
359 .tab_stop(enabled),
360 cx,
361 )
362 .on_key_down(cx.listener(move |control, event: &KeyDownEvent, window, cx| {
363 if !control.enabled {
364 return;
365 }
366 if handle_tab_navigation(event, window) {
367 return;
368 }
369 match event.keystroke.key.as_str() {
370 "left" => {
371 if control.highlight_index > 0 {
372 control.highlight_index -= 1;
373 } else if num_items > 0 {
374 control.highlight_index = num_items - 1;
375 }
376 control.select_by_index(cx);
377 cx.notify();
378 cx.stop_propagation();
379 }
380 "right" => {
381 if control.highlight_index < num_items.saturating_sub(1) {
382 control.highlight_index += 1;
383 } else {
384 control.highlight_index = 0;
385 }
386 control.select_by_index(cx);
387 cx.notify();
388 cx.stop_propagation();
389 }
390 "space" | "enter" => {
391 control.select_by_index(cx);
392 cx.notify();
393 cx.stop_propagation();
394 }
395 _ => {}
396 }
397 }))
398 .flex()
399 .flex_row()
400 .gap(self.button_gap)
401 .children(self.items.iter().enumerate().map(|(idx, item)| {
402 let item_clone = item.clone();
403 let is_selected = self.selected == *item;
404 let is_highlighted = is_focused && idx == highlight_index && enabled;
405
406 let mut segment = div()
407 .id(item.id())
408 .px_3()
409 .py_1()
410 .rounded(px(4.0))
411 .border_1()
412 .text_sm()
413 .cursor_for_enabled(enabled);
414
415 let selected_bg = (theme.border_focus << 8) | 0x22;
418
419 segment = if !enabled {
420 segment
421 .border_color(rgb(theme.disabled_bg))
422 .text_color(rgb(theme.disabled_text))
423 .bg(rgb(theme.disabled_bg))
424 } else if is_selected {
425 segment
426 .border_color(rgb(theme.border_focus))
427 .bg(rgba(selected_bg))
428 .text_color(rgb(theme.text_primary))
429 } else if is_highlighted {
430 segment
431 .border_color(rgb(theme.border_input))
432 .bg(rgb(theme.bg_hover))
433 .text_color(rgb(theme.text_primary))
434 } else {
435 segment
436 .border_color(rgb(theme.border_default))
437 .text_color(rgb(theme.text_value))
438 };
439
440 if enabled {
441 segment = segment
442 .hover(|s| s.bg(rgb(theme.bg_hover)))
443 .on_mouse_down(MouseButton::Left, cx.listener(move |control, _event: &MouseDownEvent, window, cx| {
444 control.focus_handle.focus(window);
445 if let Some(index) = control.items.iter().position(|i| *i == item_clone) {
446 control.highlight_index = index;
447 control.select_by_index(cx);
448 }
449 cx.notify();
450 }));
451 }
452
453 segment.child(
455 div()
456 .px_1()
457 .border_1()
458 .rounded_sm()
459 .when(is_highlighted, |d| d.border_color(rgb(theme.border_focus)))
460 .when(!is_highlighted, |d| d.border_color(rgba(0x00000000)))
461 .child(item.label())
462 )
463 }))
464 }
465}