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
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    pub fn new(side: Side) -> Self {
43        Self {
44            content: vec![],
45            header: None,
46            footer: None,
47            side,
48            collapsible: true,
49            width: DEFAULT_WIDTH.into(),
50            border_width: px(1.),
51            collapsed: false,
52        }
53    }
54
55    pub fn left() -> Self {
56        Self::new(Side::Left)
57    }
58
59    pub fn right() -> Self {
60        Self::new(Side::Right)
61    }
62
63    /// Set the width of the sidebar
64    pub fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
65        self.width = width.into();
66        self
67    }
68
69    /// Set border width of the sidebar
70    pub fn border_width(mut self, border_width: impl Into<Pixels>) -> Self {
71        self.border_width = border_width.into();
72        self
73    }
74
75    /// Set the sidebar to be collapsible, default is true
76    pub fn collapsible(mut self, collapsible: bool) -> Self {
77        self.collapsible = collapsible;
78        self
79    }
80
81    /// Set the sidebar to be collapsed
82    pub fn collapsed(mut self, collapsed: bool) -> Self {
83        self.collapsed = collapsed;
84        self
85    }
86
87    /// Set the header of the sidebar.
88    pub fn header(mut self, header: impl IntoElement) -> Self {
89        self.header = Some(header.into_any_element());
90        self
91    }
92
93    /// Set the footer of the sidebar.
94    pub fn footer(mut self, footer: impl IntoElement) -> Self {
95        self.footer = Some(footer.into_any_element());
96        self
97    }
98
99    /// Add a child element to the sidebar, the child must implement `Collapsible`
100    pub fn child(mut self, child: E) -> Self {
101        self.content.push(child);
102        self
103    }
104
105    /// Add multiple children to the sidebar, the children must implement `Collapsible`
106    pub fn children(mut self, children: impl IntoIterator<Item = E>) -> Self {
107        self.content.extend(children);
108        self
109    }
110}
111
112/// Sidebar collapse button with Icon.
113#[derive(IntoElement)]
114pub struct SidebarToggleButton {
115    btn: Button,
116    collapsed: bool,
117    side: Side,
118    on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
119}
120
121impl SidebarToggleButton {
122    fn new(side: Side) -> Self {
123        Self {
124            btn: Button::new("collapse").ghost().small(),
125            collapsed: false,
126            side,
127            on_click: None,
128        }
129    }
130
131    pub fn left() -> Self {
132        Self::new(Side::Left)
133    }
134
135    pub fn right() -> Self {
136        Self::new(Side::Right)
137    }
138
139    pub fn side(mut self, side: Side) -> Self {
140        self.side = side;
141        self
142    }
143
144    pub fn collapsed(mut self, collapsed: bool) -> Self {
145        self.collapsed = collapsed;
146        self
147    }
148
149    pub fn on_click(
150        mut self,
151        on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
152    ) -> Self {
153        self.on_click = Some(Rc::new(on_click));
154        self
155    }
156}
157
158impl RenderOnce for SidebarToggleButton {
159    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
160        let collapsed = self.collapsed;
161        let on_click = self.on_click.clone();
162
163        let icon = if collapsed {
164            if self.side.is_left() {
165                IconName::PanelLeftOpen
166            } else {
167                IconName::PanelRightOpen
168            }
169        } else {
170            if self.side.is_left() {
171                IconName::PanelLeftClose
172            } else {
173                IconName::PanelRightClose
174            }
175        };
176
177        self.btn
178            .when_some(on_click, |this, on_click| {
179                this.on_click(move |ev, window, cx| {
180                    on_click(ev, window, cx);
181                })
182            })
183            .icon(Icon::new(icon).size_4())
184    }
185}
186
187impl<E: Collapsible + IntoElement> RenderOnce for Sidebar<E> {
188    fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
189        v_flex()
190            .id("sidebar")
191            .w(self.width)
192            .when(self.collapsed, |this| this.w(COLLAPSED_WIDTH))
193            .flex_shrink_0()
194            .h_full()
195            .overflow_hidden()
196            .relative()
197            .bg(cx.theme().sidebar)
198            .text_color(cx.theme().sidebar_foreground)
199            .border_color(cx.theme().sidebar_border)
200            .map(|this| match self.side {
201                Side::Left => this.border_r(self.border_width),
202                Side::Right => this.border_l(self.border_width),
203            })
204            .when_some(self.header.take(), |this, header| {
205                this.child(h_flex().id("header").p_2().gap_2().child(header))
206            })
207            .child(
208                v_flex().id("content").flex_1().min_h_0().child(
209                    div()
210                        .children(
211                            self.content
212                                .into_iter()
213                                .enumerate()
214                                .map(|(ix, c)| div().id(ix).child(c.collapsed(self.collapsed))),
215                        )
216                        .gap_2()
217                        .scrollable(ScrollbarAxis::Vertical),
218                ),
219            )
220            .when_some(self.footer.take(), |this, footer| {
221                this.child(h_flex().id("footer").gap_2().p_2().child(footer))
222            })
223    }
224}