collapsible_container.rs

  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}