use crate::{ button::{Button, ButtonVariants}, h_flex, scroll::ScrollbarAxis, v_flex, ActiveTheme, Collapsible, Icon, IconName, Side, Sizable, StyledExt, }; use gpui::{ div, prelude::FluentBuilder, px, AnyElement, App, ClickEvent, EdgesRefinement, InteractiveElement as _, IntoElement, ParentElement, Pixels, RenderOnce, StyleRefinement, Styled, Window, }; use std::rc::Rc; mod footer; mod group; mod header; mod menu; pub use footer::*; pub use group::*; pub use header::*; pub use menu::*; const DEFAULT_WIDTH: Pixels = px(255.); const COLLAPSED_WIDTH: Pixels = px(48.); /// A Sidebar element that can contain collapsible child elements. #[derive(IntoElement)] pub struct Sidebar { style: StyleRefinement, content: Vec, /// header view header: Option, /// footer view footer: Option, /// The side of the sidebar side: Side, collapsible: bool, collapsed: bool, } impl Sidebar { /// Create a new Sidebar on the given [`Side`]. pub fn new(side: Side) -> Self { Self { style: StyleRefinement::default(), content: vec![], header: None, footer: None, side, collapsible: true, collapsed: false, } } /// Create a new Sidebar on the left side. pub fn left() -> Self { Self::new(Side::Left) } /// Create a new Sidebar on the right side. pub fn right() -> Self { Self::new(Side::Right) } /// Set the sidebar to be collapsible, default is true pub fn collapsible(mut self, collapsible: bool) -> Self { self.collapsible = collapsible; self } /// Set the sidebar to be collapsed pub fn collapsed(mut self, collapsed: bool) -> Self { self.collapsed = collapsed; self } /// Set the header of the sidebar. pub fn header(mut self, header: impl IntoElement) -> Self { self.header = Some(header.into_any_element()); self } /// Set the footer of the sidebar. pub fn footer(mut self, footer: impl IntoElement) -> Self { self.footer = Some(footer.into_any_element()); self } /// Add a child element to the sidebar, the child must implement `Collapsible` pub fn child(mut self, child: E) -> Self { self.content.push(child); self } /// Add multiple children to the sidebar, the children must implement `Collapsible` pub fn children(mut self, children: impl IntoIterator) -> Self { self.content.extend(children); self } } /// Toggle button to collapse/expand the [`Sidebar`]. #[derive(IntoElement)] pub struct SidebarToggleButton { btn: Button, collapsed: bool, side: Side, on_click: Option>, } impl SidebarToggleButton { fn new(side: Side) -> Self { Self { btn: Button::new("collapse").ghost().small(), collapsed: false, side, on_click: None, } } /// Create a new SidebarToggleButton on the left side. pub fn left() -> Self { Self::new(Side::Left) } /// Create a new SidebarToggleButton on the right side. pub fn right() -> Self { Self::new(Side::Right) } /// Set the side of the toggle button. pub fn side(mut self, side: Side) -> Self { self.side = side; self } /// Set the collapsed state of the toggle button. pub fn collapsed(mut self, collapsed: bool) -> Self { self.collapsed = collapsed; self } /// Add a click handler to the toggle button. pub fn on_click( mut self, on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> Self { self.on_click = Some(Rc::new(on_click)); self } } impl RenderOnce for SidebarToggleButton { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { let collapsed = self.collapsed; let on_click = self.on_click.clone(); let icon = if collapsed { if self.side.is_left() { IconName::PanelLeftOpen } else { IconName::PanelRightOpen } } else { if self.side.is_left() { IconName::PanelLeftClose } else { IconName::PanelRightClose } }; self.btn .when_some(on_click, |this, on_click| { this.on_click(move |ev, window, cx| { on_click(ev, window, cx); }) }) .icon(Icon::new(icon).size_4()) } } impl Styled for Sidebar { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } } impl RenderOnce for Sidebar { fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement { self.style.padding = EdgesRefinement::default(); v_flex() .id("sidebar") .w(DEFAULT_WIDTH) .flex_shrink_0() .h_full() .overflow_hidden() .relative() .bg(cx.theme().sidebar) .text_color(cx.theme().sidebar_foreground) .border_color(cx.theme().sidebar_border) .map(|this| match self.side { Side::Left => this.border_r_1(), Side::Right => this.border_l_1(), }) .refine_style(&self.style) .when(self.collapsed, |this| this.w(COLLAPSED_WIDTH).gap_2()) .when_some(self.header.take(), |this, header| { this.child( h_flex() .id("header") .pt_3() .px_3() .gap_2() .when(self.collapsed, |this| this.pt_2().px_2()) .child(header), ) }) .child( v_flex().id("content").flex_1().min_h_0().child( v_flex() .gap_3() .p_3() .when(self.collapsed, |this| this.p_2()) .children( self.content .into_iter() .enumerate() .map(|(ix, c)| div().id(ix).child(c.collapsed(self.collapsed))), ) .scrollable(ScrollbarAxis::Vertical), ), ) .when_some(self.footer.take(), |this, footer| { this.child( h_flex() .id("footer") .pb_3() .px_3() .gap_2() .when(self.collapsed, |this| this.pt_2().px_2()) .child(footer), ) }) } }