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