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#[derive(IntoElement)]
27pub struct Sidebar<E: Collapsible + IntoElement + 'static> {
28 content: Vec<E>,
29 header: Option<AnyElement>,
31 footer: Option<AnyElement>,
33 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 {
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 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 width(mut self, width: impl Into<DefiniteLength>) -> Self {
68 self.width = width.into();
69 self
70 }
71
72 pub fn border_width(mut self, border_width: impl Into<Pixels>) -> Self {
74 self.border_width = border_width.into();
75 self
76 }
77
78 pub fn collapsible(mut self, collapsible: bool) -> Self {
80 self.collapsible = collapsible;
81 self
82 }
83
84 pub fn collapsed(mut self, collapsed: bool) -> Self {
86 self.collapsed = collapsed;
87 self
88 }
89
90 pub fn header(mut self, header: impl IntoElement) -> Self {
92 self.header = Some(header.into_any_element());
93 self
94 }
95
96 pub fn footer(mut self, footer: impl IntoElement) -> Self {
98 self.footer = Some(footer.into_any_element());
99 self
100 }
101
102 pub fn child(mut self, child: E) -> Self {
104 self.content.push(child);
105 self
106 }
107
108 pub fn children(mut self, children: impl IntoIterator<Item = E>) -> Self {
110 self.content.extend(children);
111 self
112 }
113}
114
115#[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 pub fn left() -> Self {
136 Self::new(Side::Left)
137 }
138
139 pub fn right() -> Self {
141 Self::new(Side::Right)
142 }
143
144 pub fn side(mut self, side: Side) -> Self {
146 self.side = side;
147 self
148 }
149
150 pub fn collapsed(mut self, collapsed: bool) -> Self {
152 self.collapsed = collapsed;
153 self
154 }
155
156 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}