1use crate::{prelude::*, ButtonLike};
2use smallvec::SmallVec;
3
4use gpui::*;
5
6#[derive(Default, Clone, Copy, Debug, PartialEq)]
7pub enum ContainerStyle {
8 #[default]
9 None,
10 Card,
11}
12
13struct ContainerStyles {
14 pub background_color: Hsla,
15 pub border_color: Hsla,
16 pub text_color: Hsla,
17}
18
19#[derive(IntoElement)]
20pub struct CollapsibleContainer {
21 id: ElementId,
22 base: ButtonLike,
23 toggle: bool,
24 /// A slot for content that appears before the label, like an icon or avatar.
25 start_slot: Option<AnyElement>,
26 /// A slot for content that appears after the label, usually on the other side of the header.
27 /// This might be a button, a disclosure arrow, a face pile, etc.
28 end_slot: Option<AnyElement>,
29 style: ContainerStyle,
30 children: SmallVec<[AnyElement; 1]>,
31}
32
33impl CollapsibleContainer {
34 pub fn new(id: impl Into<ElementId>, toggle: bool) -> Self {
35 Self {
36 id: id.into(),
37 base: ButtonLike::new("button_base"),
38 toggle,
39 start_slot: None,
40 end_slot: None,
41 style: ContainerStyle::Card,
42 children: SmallVec::new(),
43 }
44 }
45
46 pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
47 self.start_slot = start_slot.into().map(IntoElement::into_any_element);
48 self
49 }
50
51 pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
52 self.end_slot = end_slot.into().map(IntoElement::into_any_element);
53 self
54 }
55
56 pub fn child<E: IntoElement>(mut self, child: E) -> Self {
57 self.children.push(child.into_any_element());
58 self
59 }
60}
61
62impl Clickable for CollapsibleContainer {
63 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
64 self.base = self.base.on_click(handler);
65 self
66 }
67}
68
69impl RenderOnce for CollapsibleContainer {
70 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
71 let color = cx.theme().colors();
72
73 let styles = match self.style {
74 ContainerStyle::None => ContainerStyles {
75 background_color: color.ghost_element_background,
76 border_color: color.border_transparent,
77 text_color: color.text,
78 },
79 ContainerStyle::Card => ContainerStyles {
80 background_color: color.elevated_surface_background,
81 border_color: color.border,
82 text_color: color.text,
83 },
84 };
85
86 v_flex()
87 .id(self.id)
88 .relative()
89 .rounded_md()
90 .bg(styles.background_color)
91 .border_1()
92 .border_color(styles.border_color)
93 .text_color(styles.text_color)
94 .overflow_hidden()
95 .child(
96 h_flex()
97 .overflow_hidden()
98 .w_full()
99 .group("toggleable_container_header")
100 .border_b_1()
101 .border_color(if self.toggle {
102 styles.border_color
103 } else {
104 color.border_transparent
105 })
106 .child(
107 self.base.full_width().style(ButtonStyle::Subtle).child(
108 div()
109 .h_7()
110 .p_1()
111 .flex()
112 .flex_1()
113 .items_center()
114 .justify_between()
115 .w_full()
116 .gap_1()
117 .cursor_pointer()
118 .group_hover("toggleable_container_header", |this| {
119 this.bg(color.element_hover)
120 })
121 .child(
122 h_flex()
123 .gap_1()
124 .child(
125 IconButton::new(
126 "toggle_icon",
127 match self.toggle {
128 true => IconName::ChevronDown,
129 false => IconName::ChevronRight,
130 },
131 )
132 .icon_color(Color::Muted)
133 .icon_size(IconSize::XSmall),
134 )
135 .child(
136 div()
137 .id("label_container")
138 .flex()
139 .gap_1()
140 .items_center()
141 .children(self.start_slot),
142 ),
143 )
144 .child(h_flex().children(self.end_slot)),
145 ),
146 ),
147 )
148 .when(self.toggle, |this| {
149 this.child(h_flex().flex_1().w_full().p_1().children(self.children))
150 })
151 }
152}