use gpui::prelude::*;
use gpui::*;
use crate::theme::{get_theme_or, Theme};
use super::focus_navigation::{with_focus_actions, EnabledCursorExt};
use super::selection::SelectionItem;
actions!(ccf_sidebar_nav, [SelectPrevious, SelectNext]);
pub fn register_keybindings(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("up", SelectPrevious, Some("CcfSidebarNav")),
KeyBinding::new("down", SelectNext, Some("CcfSidebarNav")),
]);
}
#[derive(Debug, Clone)]
pub enum SidebarNavEvent<T> {
Change(T),
}
pub struct SidebarNav<T: SelectionItem> {
items: Vec<T>,
selected: T,
focus_handle: FocusHandle,
custom_theme: Option<Theme>,
enabled: bool,
width: Option<Pixels>,
}
impl<T: SelectionItem> SidebarNav<T> {
pub fn new(items: Vec<T>, selected: T, cx: &mut Context<Self>) -> Self {
Self {
items,
selected,
focus_handle: cx.focus_handle().tab_stop(true),
custom_theme: None,
enabled: true,
width: None,
}
}
#[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
}
#[must_use]
pub fn with_width(mut self, width: Pixels) -> Self {
self.width = Some(width);
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>) {
self.selected = item;
cx.notify();
}
pub fn set_selected_index(&mut self, index: usize, cx: &mut Context<Self>) {
if let Some(item) = self.items.get(index).cloned() {
self.selected = item;
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_previous(&mut self, cx: &mut Context<Self>) {
if self.items.is_empty() {
return;
}
let current_index = self.items.iter().position(|t| *t == self.selected).unwrap_or(0);
let new_index = if current_index == 0 {
self.items.len() - 1
} else {
current_index - 1
};
if let Some(item) = self.items.get(new_index) {
self.selected = item.clone();
cx.emit(SidebarNavEvent::Change(self.selected.clone()));
cx.notify();
}
}
fn select_next(&mut self, cx: &mut Context<Self>) {
if self.items.is_empty() {
return;
}
let current_index = self.items.iter().position(|t| *t == self.selected).unwrap_or(0);
let new_index = if current_index >= self.items.len() - 1 {
0
} else {
current_index + 1
};
if let Some(item) = self.items.get(new_index) {
self.selected = item.clone();
cx.emit(SidebarNavEvent::Change(self.selected.clone()));
cx.notify();
}
}
}
impl<T: SelectionItem> EventEmitter<SidebarNavEvent<T>> for SidebarNav<T> {}
impl<T: SelectionItem> Focusable for SidebarNav<T> {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl<T: SelectionItem> Render for SidebarNav<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 selected_item = self.selected.clone();
let is_focused = self.focus_handle.is_focused(window);
let enabled = self.enabled;
with_focus_actions(
div()
.id("ccf_sidebar_nav")
.key_context("CcfSidebarNav")
.track_focus(&self.focus_handle)
.tab_stop(enabled),
cx,
)
.flex()
.flex_col()
.when_some(self.width, |d, w| d.w(w))
.when(enabled, |d| d.bg(rgb(theme.bg_input)))
.when(!enabled, |d| d.bg(rgb(theme.disabled_bg)))
.border_r_1()
.border_color(rgb(theme.border_default))
.p_2()
.on_action(cx.listener(|this, _: &SelectPrevious, _window, cx| {
if this.enabled {
this.select_previous(cx);
}
}))
.on_action(cx.listener(|this, _: &SelectNext, _window, cx| {
if this.enabled {
this.select_next(cx);
}
}))
.children(self.items.iter().map(|item| {
let item = item.clone();
let is_selected = item == selected_item;
let show_focus = is_selected && is_focused && enabled;
div()
.id(item.id())
.cursor_for_enabled(enabled)
.px_2()
.py_1()
.mb_1()
.rounded(px(4.0))
.when(enabled, |d| {
let item_clone = item.clone();
d.on_click({
cx.listener(move |this, _event: &ClickEvent, _window, cx| {
this.selected = item_clone.clone();
cx.emit(SidebarNavEvent::Change(item_clone.clone()));
cx.notify();
})
})
})
.when(is_selected && enabled, |d| {
d.bg(rgb(theme.bg_hover))
.text_color(rgb(theme.accent))
})
.when(!is_selected && enabled, |d| {
d.bg(rgb(theme.bg_input))
.text_color(rgb(theme.text_primary))
.hover(|d| {
d.bg(rgb(theme.bg_secondary))
})
})
.when(is_selected && !enabled, |d| {
d.bg(rgb(theme.disabled_bg))
.text_color(rgb(theme.disabled_text))
})
.when(!is_selected && !enabled, |d| {
d.bg(rgb(theme.disabled_bg))
.text_color(rgb(theme.disabled_text))
})
.child(
div()
.px_1()
.border_1()
.rounded_sm()
.when(show_focus, |d| d.border_color(rgb(theme.border_focus)))
.when(!show_focus, |d| d.border_color(rgba(0x00000000)))
.child(item.label())
)
}))
}
}