Skip to main content

ui/components/
sidebar.rs

1use gpui::{AnyElement, ClickEvent, ElementId};
2use smallvec::SmallVec;
3
4use crate::prelude::*;
5
6/// A vertical navigation rail. Holds [`SidebarItem`]s (or any children).
7#[derive(IntoElement, RegisterComponent)]
8pub struct Sidebar {
9    children: SmallVec<[AnyElement; 2]>,
10}
11
12impl Sidebar {
13    pub fn new() -> Self {
14        Self {
15            children: SmallVec::new(),
16        }
17    }
18}
19
20impl Default for Sidebar {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl ParentElement for Sidebar {
27    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
28        self.children.extend(elements);
29    }
30}
31
32impl RenderOnce for Sidebar {
33    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
34        v_flex()
35            .w(px(256.))
36            .h_full()
37            .flex_shrink_0()
38            .gap_1()
39            .p_2()
40            .bg(semantic::surface(cx))
41            .border_r_1()
42            .border_color(semantic::border(cx))
43            .children(self.children)
44    }
45}
46
47impl Component for Sidebar {
48    fn scope() -> ComponentScope {
49        ComponentScope::Navigation
50    }
51
52    fn description() -> Option<&'static str> {
53        Some("A vertical navigation rail holding nav items.")
54    }
55
56    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
57        Some(
58            Sidebar::new()
59                .child(
60                    SidebarItem::new("nav-home", "Home")
61                        .icon(IconName::Box)
62                        .active(true),
63                )
64                .child(SidebarItem::new("nav-projects", "Projects").icon(IconName::Folder))
65                .child(SidebarItem::new("nav-team", "Team").icon(IconName::Person))
66                .child(SidebarItem::new("nav-settings", "Settings").icon(IconName::Settings))
67                .into_any_element(),
68        )
69    }
70}
71
72/// A single clickable navigation row inside a [`Sidebar`].
73#[derive(IntoElement)]
74pub struct SidebarItem {
75    id: ElementId,
76    label: SharedString,
77    icon: Option<IconName>,
78    active: bool,
79    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
80}
81
82impl SidebarItem {
83    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
84        Self {
85            id: id.into(),
86            label: label.into(),
87            icon: None,
88            active: false,
89            on_click: None,
90        }
91    }
92
93    pub fn icon(mut self, icon: IconName) -> Self {
94        self.icon = Some(icon);
95        self
96    }
97
98    pub fn active(mut self, active: bool) -> Self {
99        self.active = active;
100        self
101    }
102
103    pub fn on_click(
104        mut self,
105        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
106    ) -> Self {
107        self.on_click = Some(Box::new(handler));
108        self
109    }
110}
111
112impl RenderOnce for SidebarItem {
113    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
114        let text_color = if self.active {
115            semantic::text(cx)
116        } else {
117            semantic::text_muted(cx)
118        };
119        let hover = semantic::hover_bg(cx);
120
121        h_flex()
122            .id(self.id)
123            .items_center()
124            .gap_2()
125            .px_3()
126            .py_2()
127            .rounded_md()
128            .cursor_pointer()
129            .text_color(text_color)
130            .when(self.active, |this| this.bg(hover))
131            .hover(|this| this.bg(hover))
132            .when_some(self.icon, |this, icon| {
133                this.child(Icon::new(icon).size(IconSize::Small))
134            })
135            .child(Label::new(self.label))
136            .when_some(self.on_click, |this, handler| this.on_click(handler))
137    }
138}