use gpui::prelude::*;
use gpui::*;
use crate::theme::{get_theme_or, Theme};
use super::focus_navigation::{handle_tab_navigation, with_focus_actions, EnabledCursorExt};
#[derive(Clone, Debug)]
pub enum CollapsibleEvent {
Change(bool),
}
pub struct Collapsible {
title: SharedString,
collapsed: bool,
focus_handle: FocusHandle,
custom_theme: Option<Theme>,
enabled: bool,
collapsible: bool,
}
impl EventEmitter<CollapsibleEvent> for Collapsible {}
impl Focusable for Collapsible {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Collapsible {
pub fn new(title: impl Into<SharedString>, cx: &mut Context<Self>) -> Self {
Self {
title: title.into(),
collapsed: false,
focus_handle: cx.focus_handle().tab_stop(true),
custom_theme: None,
enabled: true,
collapsible: true,
}
}
#[must_use]
pub fn with_collapsed(mut self, collapsed: bool) -> Self {
self.collapsed = collapsed;
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
}
#[must_use]
pub fn collapsible(mut self, collapsible: bool) -> Self {
self.collapsible = collapsible;
self
}
pub fn focus_handle(&self) -> &FocusHandle {
&self.focus_handle
}
pub fn is_collapsed(&self) -> bool {
self.collapsed
}
pub fn set_collapsed(&mut self, collapsed: bool, cx: &mut Context<Self>) {
if self.collapsed != collapsed {
self.collapsed = collapsed;
cx.emit(CollapsibleEvent::Change(collapsed));
cx.notify();
}
}
pub fn toggle(&mut self, cx: &mut Context<Self>) {
self.collapsed = !self.collapsed;
cx.emit(CollapsibleEvent::Change(self.collapsed));
cx.notify();
}
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();
}
}
pub fn is_collapsible(&self) -> bool {
self.collapsible
}
pub fn set_collapsible(&mut self, collapsible: bool, cx: &mut Context<Self>) {
if self.collapsible != collapsible {
self.collapsible = collapsible;
cx.notify();
}
}
}
impl Render for Collapsible {
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 collapsed = self.collapsed;
let title = self.title.clone();
let collapsible = self.collapsible;
let enabled = self.enabled;
let interactive = collapsible && enabled;
if !collapsible {
return div()
.id("ccf_collapsible_header")
.flex()
.flex_row()
.items_center()
.gap_2()
.py(px(6.))
.px_2()
.bg(rgb(theme.bg_section_header))
.rounded_t_md()
.border_2()
.border_color(rgba(0x00000000))
.child(
div()
.text_sm()
.font_weight(FontWeight::SEMIBOLD)
.text_color(rgb(theme.text_section_header))
.child(title)
);
}
let chevron = if collapsed { "▶" } else { "▼" };
let focus_handle = self.focus_handle.clone();
let is_focused = self.focus_handle.is_focused(window);
with_focus_actions(
div()
.id("ccf_collapsible_header")
.track_focus(&focus_handle)
.tab_stop(enabled),
cx,
)
.on_key_down(cx.listener(move |this, event: &KeyDownEvent, window, cx| {
if !this.enabled {
return;
}
if handle_tab_navigation(event, window) {
return;
}
match event.keystroke.key.as_str() {
"down" => this.set_collapsed(false, cx),
"up" => this.set_collapsed(true, cx),
"space" | "enter" => this.toggle(cx),
_ => {}
}
}))
.flex()
.flex_row()
.items_center()
.gap_2()
.py(px(6.))
.px_2()
.when(enabled, |d| d.bg(rgb(theme.bg_section_header)))
.when(!enabled, |d| d.bg(rgb(theme.disabled_bg)))
.when(collapsed, |d| d.rounded_md())
.when(!collapsed, |d| d.rounded_t_md())
.cursor_for_enabled(interactive)
.border_2()
.border_color(if is_focused && enabled { rgb(theme.border_focus) } else { rgba(0x00000000) })
.when(interactive, |d| {
d.hover(|d| d.bg(rgb(theme.bg_section_header_hover)))
.on_mouse_down(MouseButton::Left, cx.listener(|this, _event, window, cx| {
this.focus_handle.focus(window);
this.toggle(cx);
}))
})
.child(
div()
.text_sm()
.when(enabled, |d| d.text_color(rgb(theme.text_dimmed)))
.when(!enabled, |d| d.text_color(rgb(theme.disabled_text)))
.w(px(16.))
.child(chevron)
)
.child(
div()
.text_sm()
.font_weight(FontWeight::SEMIBOLD)
.when(enabled, |d| d.text_color(rgb(theme.text_section_header)))
.when(!enabled, |d| d.text_color(rgb(theme.disabled_text)))
.child(title)
)
}
}