gpui_component/sidebar/
mod.rs

1use crate::{
2    ActiveTheme, Collapsible, Icon, IconName, Side, Sizable, StyledExt,
3    button::{Button, ButtonVariants},
4    h_flex,
5    scroll::ScrollableElement,
6    v_flex,
7};
8use gpui::{
9    AnyElement, App, ClickEvent, EdgesRefinement, InteractiveElement as _, IntoElement,
10    ParentElement, Pixels, RenderOnce, StyleRefinement, Styled, Window, div,
11    prelude::FluentBuilder, px,
12};
13use std::rc::Rc;
14
15mod footer;
16mod group;
17mod header;
18mod menu;
19pub use footer::*;
20pub use group::*;
21pub use header::*;
22pub use menu::*;
23
24const DEFAULT_WIDTH: Pixels = px(255.);
25const COLLAPSED_WIDTH: Pixels = px(48.);
26
27/// A Sidebar element that can contain collapsible child elements.
28#[derive(IntoElement)]
29pub struct Sidebar<E: Collapsible + IntoElement + 'static> {
30    style: StyleRefinement,
31    content: Vec<E>,
32    /// header view
33    header: Option<AnyElement>,
34    /// footer view
35    footer: Option<AnyElement>,
36    /// The side of the sidebar
37    side: Side,
38    collapsible: bool,
39    collapsed: bool,
40}
41
42impl<E: Collapsible + IntoElement> Sidebar<E> {
43    /// Create a new Sidebar on the given [`Side`].
44    pub fn new(side: Side) -> Self {
45        Self {
46            style: StyleRefinement::default(),
47            content: vec![],
48            header: None,
49            footer: None,
50            side,
51            collapsible: true,
52            collapsed: false,
53        }
54    }
55
56    /// Create a new Sidebar on the left side.
57    pub fn left() -> Self {
58        Self::new(Side::Left)
59    }
60
61    /// Create a new Sidebar on the right side.
62    pub fn right() -> Self {
63        Self::new(Side::Right)
64    }
65
66    /// Set the sidebar to be collapsible, default is true
67    pub fn collapsible(mut self, collapsible: bool) -> Self {
68        self.collapsible = collapsible;
69        self
70    }
71
72    /// Set the sidebar to be collapsed
73    pub fn collapsed(mut self, collapsed: bool) -> Self {
74        self.collapsed = collapsed;
75        self
76    }
77
78    /// Set the header of the sidebar.
79    pub fn header(mut self, header: impl IntoElement) -> Self {
80        self.header = Some(header.into_any_element());
81        self
82    }
83
84    /// Set the footer of the sidebar.
85    pub fn footer(mut self, footer: impl IntoElement) -> Self {
86        self.footer = Some(footer.into_any_element());
87        self
88    }
89
90    /// Add a child element to the sidebar, the child must implement `Collapsible`
91    pub fn child(mut self, child: E) -> Self {
92        self.content.push(child);
93        self
94    }
95
96    /// Add multiple children to the sidebar, the children must implement `Collapsible`
97    pub fn children(mut self, children: impl IntoIterator<Item = E>) -> Self {
98        self.content.extend(children);
99        self
100    }
101}
102
103/// Toggle button to collapse/expand the [`Sidebar`].
104#[derive(IntoElement)]
105pub struct SidebarToggleButton {
106    btn: Button,
107    collapsed: bool,
108    side: Side,
109    on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
110}
111
112impl SidebarToggleButton {
113    fn new(side: Side) -> Self {
114        Self {
115            btn: Button::new("collapse").ghost().small(),
116            collapsed: false,
117            side,
118            on_click: None,
119        }
120    }
121
122    /// Create a new SidebarToggleButton on the left side.
123    pub fn left() -> Self {
124        Self::new(Side::Left)
125    }
126
127    /// Create a new SidebarToggleButton on the right side.
128    pub fn right() -> Self {
129        Self::new(Side::Right)
130    }
131
132    /// Set the side of the toggle button.
133    pub fn side(mut self, side: Side) -> Self {
134        self.side = side;
135        self
136    }
137
138    /// Set the collapsed state of the toggle button.
139    pub fn collapsed(mut self, collapsed: bool) -> Self {
140        self.collapsed = collapsed;
141        self
142    }
143
144    /// Add a click handler to the toggle button.
145    pub fn on_click(
146        mut self,
147        on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
148    ) -> Self {
149        self.on_click = Some(Rc::new(on_click));
150        self
151    }
152}
153
154impl RenderOnce for SidebarToggleButton {
155    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
156        let collapsed = self.collapsed;
157        let on_click = self.on_click.clone();
158
159        let icon = if collapsed {
160            if self.side.is_left() {
161                IconName::PanelLeftOpen
162            } else {
163                IconName::PanelRightOpen
164            }
165        } else {
166            if self.side.is_left() {
167                IconName::PanelLeftClose
168            } else {
169                IconName::PanelRightClose
170            }
171        };
172
173        self.btn
174            .when_some(on_click, |this, on_click| {
175                this.on_click(move |ev, window, cx| {
176                    on_click(ev, window, cx);
177                })
178            })
179            .icon(Icon::new(icon).size_4())
180    }
181}
182
183impl<E: Collapsible + IntoElement> Styled for Sidebar<E> {
184    fn style(&mut self) -> &mut StyleRefinement {
185        &mut self.style
186    }
187}
188
189impl<E: Collapsible + IntoElement> RenderOnce for Sidebar<E> {
190    fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
191        self.style.padding = EdgesRefinement::default();
192
193        v_flex()
194            .id("sidebar")
195            .w(DEFAULT_WIDTH)
196            .flex_shrink_0()
197            .h_full()
198            .overflow_hidden()
199            .relative()
200            .bg(cx.theme().sidebar)
201            .text_color(cx.theme().sidebar_foreground)
202            .border_color(cx.theme().sidebar_border)
203            .map(|this| match self.side {
204                Side::Left => this.border_r_1(),
205                Side::Right => this.border_l_1(),
206            })
207            .refine_style(&self.style)
208            .when(self.collapsed, |this| this.w(COLLAPSED_WIDTH).gap_2())
209            .when_some(self.header.take(), |this, header| {
210                this.child(
211                    h_flex()
212                        .id("header")
213                        .pt_3()
214                        .px_3()
215                        .gap_2()
216                        .when(self.collapsed, |this| this.pt_2().px_2())
217                        .child(header),
218                )
219            })
220            .child(
221                v_flex().id("content").flex_1().min_h_0().child(
222                    v_flex()
223                        .id("inner")
224                        .p_3()
225                        .when(self.collapsed, |this| this.p_2())
226                        .children(
227                            self.content.into_iter().enumerate().map(|(ix, c)| {
228                                div().id(ix).mt_3().child(c.collapsed(self.collapsed))
229                            }),
230                        )
231                        .overflow_y_scrollbar(),
232                ),
233            )
234            .when_some(self.footer.take(), |this, footer| {
235                this.child(
236                    h_flex()
237                        .id("footer")
238                        .pb_3()
239                        .px_3()
240                        .gap_2()
241                        .when(self.collapsed, |this| this.pt_2().px_2())
242                        .child(footer),
243                )
244            })
245    }
246}