gpui_component/sidebar/
mod.rs

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