use std::rc::Rc; use gpui::prelude::FluentBuilder as _; use gpui::{ AnyElement, App, ElementId, FocusHandle, InteractiveElement as _, IntoElement, KeyDownEvent, ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, div, }; use gpui_native_icons::{IconRole, icon}; use gpui_native_theme::ActiveTheme as _; use gpui_native_utils::{nav_key, paint_selectable_row, selectable_label_color}; /// The change reporter, fired with the next selection by pointer and keys. type ChangeHandler = Rc, &mut Window, &mut App)>; type ItemsFn = Box Vec>; /// One row of a [`List`]. pub struct ListItem { label: SharedString, icon: Option, detail: Option, disabled: bool, } impl ListItem { /// Creates a row with this label. pub fn new(label: L) -> Self where L: Into, { Self { label: label.into(), icon: None, detail: None, disabled: false, } } /// Shows an icon leading the label. pub fn icon(mut self, role: IconRole) -> Self { self.icon = Some(role); self } /// Shows muted trailing text. pub fn detail(mut self, detail: D) -> Self where D: Into, { self.detail = Some(detail.into()); self } /// Renders the row dimmed; clicks and keyboard skip it. pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } } /// A single-selection list. #[derive(IntoElement)] pub struct List { id: ElementId, selected: Option, a11y_label: Option, on_change: Option, items: Option, } impl List { /// Creates a list with nothing selected. pub fn new(id: I) -> Self where I: Into, { Self { id: id.into(), selected: None, a11y_label: None, on_change: None, items: None, } } /// Sets the current selection. pub fn selected(mut self, selected: Option) -> Self { self.selected = selected; self } /// Reports the next selection, from clicks and keys alike. pub fn on_change(mut self, handler: F) -> Self where F: Fn(Option, &mut Window, &mut App) + 'static, { self.on_change = Some(Rc::new(handler)); self } /// Sets the rows, built fresh each render. pub fn items(mut self, items: F) -> Self where F: Fn(&mut Window, &mut App) -> Vec + 'static, { self.items = Some(Box::new(items)); self } /// Overrides the accessibility label, which defaults to the id. pub fn accessibility_label(mut self, label: L) -> Self where L: Into, { self.a11y_label = Some(label.into()); self } } impl RenderOnce for List { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let items = match self.items { Some(build) => build(window, cx), None => Vec::new(), }; let selectable: Vec = items .iter() .enumerate() .filter(|(_, item)| !item.disabled) .map(|(index, _)| index) .collect(); let focus = gpui_native_utils::focus_handle(&self.id, window, cx); let theme = cx.theme(); let label = self .a11y_label .clone() .unwrap_or_else(|| SharedString::from(self.id.to_string())); let rows: Vec = items .iter() .enumerate() .map(|(index, item)| { render_row( cx, index, items.len(), item, self.selected == Some(index), &self.on_change, &focus, ) }) .collect(); div() .id(self.id) .flex() .flex_col() .gap_0p5() .text_size(theme.sizes.medium.text_size) .track_focus(&focus) .focusable() .tab_stop(true) .role(Role::List) .aria_label(label) .on_key_down({ let selected = self.selected; let on_change = self.on_change.clone(); move |event: &KeyDownEvent, window, cx| { if nav_key(&selectable, selected, &event.keystroke.key, |next| { if let Some(on_change) = &on_change { on_change(next, window, cx); } }) { cx.stop_propagation(); } } }) .children(rows) } } fn render_row( cx: &App, index: usize, count: usize, item: &ListItem, selected: bool, on_change: &Option, focus: &FocusHandle, ) -> AnyElement { let theme = cx.theme(); let label_color = selectable_label_color(cx, selected, item.disabled); let disabled = item.disabled; div() .id(("list-item", index)) .flex() .items_center() .gap_2() .px_2() .py_1() .rounded(theme.radius.sm) .role(Role::ListItem) .aria_selected(selected) .aria_position_in_set(index + 1) .aria_size_of_set(count) .map(|row| paint_selectable_row(cx, row, selected, disabled)) .when(!disabled, |row| { row.cursor_pointer().on_click({ let on_change = on_change.clone(); let focus = focus.clone(); move |_, window, cx| { window.focus(&focus, cx); if let Some(on_change) = &on_change { on_change(Some(index), window, cx); } } }) }) .when_some(item.icon, |row, role| { row.child( icon(role) .size(theme.sizes.medium.icon_size) .color(label_color), ) }) .child( div() .flex_1() .text_color(label_color) .child(item.label.clone()), ) .when_some(item.detail.clone(), |row, detail| { row.child( div() .flex_none() .text_sm() .text_color(if selected { theme.colors.primary_foreground } else { theme.colors.muted_foreground }) .child(detail), ) }) .into_any_element() }