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 {
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 pub fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
65 self.width = width.into();
66 self
67 }
68
69 pub fn border_width(mut self, border_width: impl Into<Pixels>) -> Self {
71 self.border_width = border_width.into();
72 self
73 }
74
75 pub fn collapsible(mut self, collapsible: bool) -> Self {
77 self.collapsible = collapsible;
78 self
79 }
80
81 pub fn collapsed(mut self, collapsed: bool) -> Self {
83 self.collapsed = collapsed;
84 self
85 }
86
87 pub fn header(mut self, header: impl IntoElement) -> Self {
89 self.header = Some(header.into_any_element());
90 self
91 }
92
93 pub fn footer(mut self, footer: impl IntoElement) -> Self {
95 self.footer = Some(footer.into_any_element());
96 self
97 }
98
99 pub fn child(mut self, child: E) -> Self {
101 self.content.push(child);
102 self
103 }
104
105 pub fn children(mut self, children: impl IntoIterator<Item = E>) -> Self {
107 self.content.extend(children);
108 self
109 }
110}
111
112#[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}