use gpui::prelude::*;
use gpui::*;
use crate::theme::{get_theme_or, Theme};
use super::focus_navigation::{handle_tab_navigation, with_focus_actions, EnabledCursorExt};
use super::selection::{SelectionItem, StringItem};
#[derive(Clone, Debug)]
pub enum RadioGroupEvent<T: SelectionItem> {
Change(T),
}
pub struct RadioGroup<T: SelectionItem = StringItem> {
items: Vec<T>,
selected: T,
focus_handle: FocusHandle,
highlight_index: usize,
custom_theme: Option<Theme>,
enabled: bool,
}
impl<T: SelectionItem> EventEmitter<RadioGroupEvent<T>> for RadioGroup<T> {}
impl<T: SelectionItem> Focusable for RadioGroup<T> {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl RadioGroup<StringItem> {
pub fn new(cx: &mut Context<Self>) -> Self {
Self {
items: Vec::new(),
selected: StringItem::new(""),
focus_handle: cx.focus_handle().tab_stop(true),
highlight_index: 0,
custom_theme: None,
enabled: true,
}
}
#[must_use]
pub fn choices(mut self, choices: Vec<String>) -> Self {
self.items = choices.into_iter().map(StringItem::new).collect();
if !self.items.is_empty() && self.selected.value().is_empty() {
self.selected = self.items[0].clone();
}
self
}
#[must_use]
pub fn with_selected_value(mut self, value: &str) -> Self {
if let Some(index) = self.items.iter().position(|c| c.value() == value) {
self.selected = self.items[index].clone();
self.highlight_index = index;
}
self
}
pub fn set_selected_value(&mut self, value: &str, cx: &mut Context<Self>) {
if let Some(index) = self.items.iter().position(|c| c.value() == value) {
if self.selected.value() != value {
self.selected = self.items[index].clone();
self.highlight_index = index;
cx.emit(RadioGroupEvent::Change(self.selected.clone()));
cx.notify();
}
}
}
pub fn selected_value(&self) -> &str {
self.selected.value()
}
}
impl<T: SelectionItem> RadioGroup<T> {
pub fn new_with_items(items: Vec<T>, selected: T, cx: &mut Context<Self>) -> Self {
let highlight_index = items.iter().position(|i| *i == selected).unwrap_or(0);
Self {
items,
selected,
focus_handle: cx.focus_handle().tab_stop(true),
highlight_index,
custom_theme: None,
enabled: true,
}
}
#[must_use]
pub fn with_items(mut self, items: Vec<T>) -> Self {
self.items = items;
if !self.items.is_empty() {
self.selected = self.items[0].clone();
self.highlight_index = 0;
}
self
}
#[must_use]
pub fn with_selected(mut self, item: T) -> Self {
if let Some(index) = self.items.iter().position(|i| *i == item) {
self.selected = item;
self.highlight_index = index;
}
self
}
#[must_use]
pub fn with_selected_index(mut self, index: usize) -> Self {
if let Some(item) = self.items.get(index) {
self.selected = item.clone();
self.highlight_index = index;
}
self
}
#[must_use]
pub fn theme(mut self, theme: Theme) -> Self {
self.custom_theme = Some(theme);
self
}
#[must_use]
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn selected(&self) -> &T {
&self.selected
}
pub fn selected_index(&self) -> usize {
self.items.iter().position(|i| *i == self.selected).unwrap_or(0)
}
pub fn set_selected(&mut self, item: T, cx: &mut Context<Self>) {
if let Some(index) = self.items.iter().position(|i| *i == item) {
if self.selected != item {
self.selected = item;
self.highlight_index = index;
cx.emit(RadioGroupEvent::Change(self.selected.clone()));
cx.notify();
}
}
}
pub fn set_selected_index(&mut self, index: usize, cx: &mut Context<Self>) {
if let Some(item) = self.items.get(index).cloned() {
if self.selected != item {
self.selected = item;
self.highlight_index = index;
cx.emit(RadioGroupEvent::Change(self.selected.clone()));
cx.notify();
}
}
}
pub fn focus_handle(&self) -> &FocusHandle {
&self.focus_handle
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
if self.enabled != enabled {
self.enabled = enabled;
cx.notify();
}
}
fn select_by_index(&mut self, cx: &mut Context<Self>) {
if let Some(item) = self.items.get(self.highlight_index) {
if self.selected != *item {
self.selected = item.clone();
cx.emit(RadioGroupEvent::Change(self.selected.clone()));
}
}
}
}
impl<T: SelectionItem> Render for RadioGroup<T> {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let theme = get_theme_or(cx, self.custom_theme.as_ref());
let focus_handle = self.focus_handle.clone();
let is_focused = self.focus_handle.is_focused(window);
let highlight_index = self.highlight_index;
let num_items = self.items.len();
let enabled = self.enabled;
with_focus_actions(
div()
.id("ccf_radio_group")
.track_focus(&focus_handle)
.tab_stop(enabled),
cx,
)
.on_key_down(cx.listener(move |radio_group, event: &KeyDownEvent, window, cx| {
if !radio_group.enabled {
return;
}
if handle_tab_navigation(event, window) {
return;
}
match event.keystroke.key.as_str() {
"up" => {
if radio_group.highlight_index > 0 {
radio_group.highlight_index -= 1;
} else if num_items > 0 {
radio_group.highlight_index = num_items - 1;
}
radio_group.select_by_index(cx);
cx.notify();
}
"down" => {
if radio_group.highlight_index < num_items.saturating_sub(1) {
radio_group.highlight_index += 1;
} else {
radio_group.highlight_index = 0;
}
radio_group.select_by_index(cx);
cx.notify();
}
"space" => {
radio_group.select_by_index(cx);
cx.notify();
}
_ => {}
}
}))
.flex()
.flex_col()
.gap_1()
.p_2()
.when(enabled, |d| d.bg(rgb(theme.bg_input)))
.when(!enabled, |d| d.bg(rgb(theme.disabled_bg)))
.border_1()
.when(enabled, |d| {
d.border_color(if is_focused { rgb(theme.border_focus) } else { rgb(theme.border_input) })
})
.when(!enabled, |d| d.border_color(rgb(theme.disabled_bg)))
.rounded_md()
.children(self.items.iter().enumerate().map(|(idx, item)| {
let item_clone = item.clone();
let is_selected = self.selected == *item;
let is_highlighted = is_focused && idx == highlight_index && enabled;
div()
.id(item.id())
.flex()
.flex_row()
.gap_2()
.items_center()
.py_1()
.px_1()
.cursor_for_enabled(enabled)
.rounded_sm()
.when(is_highlighted, |d| d.bg(rgb(theme.bg_input_hover)))
.when(!is_highlighted && enabled, |d| d.hover(|d| d.bg(rgb(theme.bg_input_hover))))
.when(enabled, |d| {
d.on_click(cx.listener(move |radio_group, _event, window, cx| {
radio_group.focus_handle.focus(window);
radio_group.selected = item_clone.clone();
radio_group.highlight_index = idx;
cx.emit(RadioGroupEvent::Change(item_clone.clone()));
cx.notify();
}))
})
.child({
let border_color = if enabled { theme.border_checkbox } else { theme.disabled_text };
let inner_color = if enabled { theme.accent } else { theme.disabled_text };
div()
.w(px(16.))
.h(px(16.))
.border_1()
.border_color(rgb(border_color))
.rounded(px(8.))
.when(is_selected, |d| {
d.child(
div()
.flex()
.items_center()
.justify_center()
.size_full()
.child(
div()
.w(px(8.))
.h(px(8.))
.bg(rgb(inner_color))
.rounded(px(4.))
)
)
})
})
.child(
div()
.text_sm()
.when(enabled, |d| d.text_color(rgb(theme.text_value)))
.when(!enabled, |d| d.text_color(rgb(theme.disabled_text)))
.child(item.label())
)
}))
}
}