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#[derive(IntoElement)]
29pub struct Sidebar<E: Collapsible + IntoElement + 'static> {
30 style: StyleRefinement,
31 content: Vec<E>,
32 header: Option<AnyElement>,
34 footer: Option<AnyElement>,
36 side: Side,
38 collapsible: bool,
39 collapsed: bool,
40}
41
42impl<E: Collapsible + IntoElement> Sidebar<E> {
43 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 pub fn left() -> Self {
58 Self::new(Side::Left)
59 }
60
61 pub fn right() -> Self {
63 Self::new(Side::Right)
64 }
65
66 pub fn collapsible(mut self, collapsible: bool) -> Self {
68 self.collapsible = collapsible;
69 self
70 }
71
72 pub fn collapsed(mut self, collapsed: bool) -> Self {
74 self.collapsed = collapsed;
75 self
76 }
77
78 pub fn header(mut self, header: impl IntoElement) -> Self {
80 self.header = Some(header.into_any_element());
81 self
82 }
83
84 pub fn footer(mut self, footer: impl IntoElement) -> Self {
86 self.footer = Some(footer.into_any_element());
87 self
88 }
89
90 pub fn child(mut self, child: E) -> Self {
92 self.content.push(child);
93 self
94 }
95
96 pub fn children(mut self, children: impl IntoIterator<Item = E>) -> Self {
98 self.content.extend(children);
99 self
100 }
101}
102
103#[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 pub fn left() -> Self {
124 Self::new(Side::Left)
125 }
126
127 pub fn right() -> Self {
129 Self::new(Side::Right)
130 }
131
132 pub fn side(mut self, side: Side) -> Self {
134 self.side = side;
135 self
136 }
137
138 pub fn collapsed(mut self, collapsed: bool) -> Self {
140 self.collapsed = collapsed;
141 self
142 }
143
144 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}