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#[derive(IntoElement)]
28pub struct Sidebar<E: Collapsible + IntoElement + 'static> {
29 style: StyleRefinement,
30 content: Vec<E>,
31 header: Option<AnyElement>,
33 footer: Option<AnyElement>,
35 side: Side,
37 collapsible: bool,
38 collapsed: bool,
39}
40
41impl<E: Collapsible + IntoElement> Sidebar<E> {
42 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 pub fn left() -> Self {
57 Self::new(Side::Left)
58 }
59
60 pub fn right() -> Self {
62 Self::new(Side::Right)
63 }
64
65 pub fn collapsible(mut self, collapsible: bool) -> Self {
67 self.collapsible = collapsible;
68 self
69 }
70
71 pub fn collapsed(mut self, collapsed: bool) -> Self {
73 self.collapsed = collapsed;
74 self
75 }
76
77 pub fn header(mut self, header: impl IntoElement) -> Self {
79 self.header = Some(header.into_any_element());
80 self
81 }
82
83 pub fn footer(mut self, footer: impl IntoElement) -> Self {
85 self.footer = Some(footer.into_any_element());
86 self
87 }
88
89 pub fn child(mut self, child: E) -> Self {
91 self.content.push(child);
92 self
93 }
94
95 pub fn children(mut self, children: impl IntoIterator<Item = E>) -> Self {
97 self.content.extend(children);
98 self
99 }
100}
101
102#[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 pub fn left() -> Self {
123 Self::new(Side::Left)
124 }
125
126 pub fn right() -> Self {
128 Self::new(Side::Right)
129 }
130
131 pub fn side(mut self, side: Side) -> Self {
133 self.side = side;
134 self
135 }
136
137 pub fn collapsed(mut self, collapsed: bool) -> Self {
139 self.collapsed = collapsed;
140 self
141 }
142
143 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}