Skip to main content

ui/components/
vertical_nav.rs

1use gpui::{AnyElement, ClickEvent};
2use smallvec::SmallVec;
3
4use crate::prelude::*;
5
6/// A flat list of navigation links for in-page sub-navigation (e.g. a
7/// settings page's left-hand link list).
8///
9/// **Distinct from [`Sidebar`](crate::Sidebar):** `Sidebar` is the app-level,
10/// collapsible navigation container that also hosts nested sections.
11/// `VerticalNav` is a minimal, flat link list only — it does NOT support
12/// collapse/nesting. Use `Sidebar` if you need that.
13#[derive(IntoElement, RegisterComponent)]
14pub struct VerticalNav {
15    children: SmallVec<[AnyElement; 4]>,
16}
17
18impl VerticalNav {
19    pub fn new() -> Self {
20        Self {
21            children: SmallVec::new(),
22        }
23    }
24}
25
26impl Default for VerticalNav {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl ParentElement for VerticalNav {
33    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
34        self.children.extend(elements);
35    }
36}
37
38impl RenderOnce for VerticalNav {
39    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
40        v_flex().w(px(220.)).gap_1().children(self.children)
41    }
42}
43
44impl Component for VerticalNav {
45    fn scope() -> ComponentScope {
46        ComponentScope::Navigation
47    }
48
49    fn description() -> Option<&'static str> {
50        Some(
51            "A flat list of navigation links for in-page sub-navigation; distinct from the collapsible Sidebar.",
52        )
53    }
54
55    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
56        Some(
57            v_flex()
58                .gap_6()
59                .child(example_group_with_title(
60                    "Basic Usage",
61                    vec![single_example(
62                        "Default",
63                        VerticalNav::new()
64                            .child(
65                                VerticalNavItem::new("vnav-general", "General")
66                                    .icon(IconName::Check)
67                                    .active(true),
68                            )
69                            .child(VerticalNavItem::new("vnav-security", "Security"))
70                            .child(VerticalNavItem::new("vnav-billing", "Billing"))
71                            .into_any_element(),
72                    )],
73                ))
74                .into_any_element(),
75        )
76    }
77}
78
79/// A single clickable link within a [`VerticalNav`].
80#[derive(IntoElement)]
81pub struct VerticalNavItem {
82    id: ElementId,
83    label: SharedString,
84    icon: Option<IconName>,
85    active: bool,
86    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
87}
88
89impl VerticalNavItem {
90    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
91        Self {
92            id: id.into(),
93            label: label.into(),
94            icon: None,
95            active: false,
96            on_click: None,
97        }
98    }
99
100    pub fn icon(mut self, icon: IconName) -> Self {
101        self.icon = Some(icon);
102        self
103    }
104
105    pub fn active(mut self, active: bool) -> Self {
106        self.active = active;
107        self
108    }
109
110    pub fn on_click(
111        mut self,
112        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
113    ) -> Self {
114        self.on_click = Some(Box::new(handler));
115        self
116    }
117}
118
119impl RenderOnce for VerticalNavItem {
120    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
121        let text_color = if self.active {
122            semantic::text(cx)
123        } else {
124            semantic::text_muted(cx)
125        };
126        let hover = semantic::hover_bg(cx);
127
128        h_flex()
129            .id(self.id)
130            .items_center()
131            .gap_2()
132            .px_4()
133            .py_2()
134            .rounded_md()
135            .cursor_pointer()
136            .text_color(text_color)
137            .when(self.active, |this| this.bg(semantic::elevated_surface(cx)))
138            .hover(move |this| this.bg(hover))
139            .when_some(self.icon, |this, icon| {
140                this.child(Icon::new(icon).size(IconSize::Small))
141            })
142            .child(Label::new(self.label))
143            .when_some(self.on_click, |this, handler| this.on_click(handler))
144    }
145}