Skip to main content

ui/components/
breadcrumb.rs

1use gpui::{AnyElement, ClickEvent};
2use smallvec::SmallVec;
3
4use crate::prelude::*;
5
6/// A single crumb within a [`Breadcrumb`] trail.
7///
8/// The last item added to a [`Breadcrumb`] is always rendered as the
9/// non-interactive "current" item, regardless of whether [`Self::on_click`]
10/// was set.
11pub struct BreadcrumbItem {
12    id: ElementId,
13    label: SharedString,
14    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
15}
16
17impl BreadcrumbItem {
18    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
19        Self {
20            id: id.into(),
21            label: label.into(),
22            on_click: None,
23        }
24    }
25
26    pub fn on_click(
27        mut self,
28        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
29    ) -> Self {
30        self.on_click = Some(Box::new(handler));
31        self
32    }
33}
34
35/// A horizontal trail of links showing the user's location within a hierarchy.
36#[derive(IntoElement, RegisterComponent)]
37pub struct Breadcrumb {
38    items: SmallVec<[BreadcrumbItem; 4]>,
39}
40
41impl Breadcrumb {
42    pub fn new() -> Self {
43        Self {
44            items: SmallVec::new(),
45        }
46    }
47
48    pub fn item(mut self, item: BreadcrumbItem) -> Self {
49        self.items.push(item);
50        self
51    }
52
53    pub fn items(mut self, items: impl IntoIterator<Item = BreadcrumbItem>) -> Self {
54        self.items.extend(items);
55        self
56    }
57}
58
59impl Default for Breadcrumb {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl RenderOnce for Breadcrumb {
66    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
67        let last_index = self.items.len().saturating_sub(1);
68        let hover_color = semantic::text(cx);
69
70        h_flex()
71            .items_center()
72            .gap_1()
73            .children(self.items.into_iter().enumerate().map(|(index, item)| {
74                let is_current = index == last_index;
75
76                let crumb = if is_current {
77                    div()
78                        .text_color(semantic::text(cx))
79                        .child(Label::new(item.label))
80                        .into_any_element()
81                } else {
82                    h_flex()
83                        .id(item.id)
84                        .cursor_pointer()
85                        .text_color(semantic::text_muted(cx))
86                        .hover(move |this| this.text_color(hover_color))
87                        .child(Label::new(item.label))
88                        .when_some(item.on_click, |this, handler| this.on_click(handler))
89                        .into_any_element()
90                };
91
92                h_flex()
93                    .items_center()
94                    .gap_1()
95                    .when(index > 0, |this| {
96                        this.child(
97                            Icon::new(IconName::ChevronRight)
98                                .size(IconSize::XSmall)
99                                .color(Color::Muted),
100                        )
101                    })
102                    .child(crumb)
103                    .into_any_element()
104            }))
105    }
106}
107
108impl Component for Breadcrumb {
109    fn scope() -> ComponentScope {
110        ComponentScope::Navigation
111    }
112
113    fn description() -> Option<&'static str> {
114        Some("A horizontal trail of links showing the user's location within a hierarchy.")
115    }
116
117    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
118        Some(
119            v_flex()
120                .gap_6()
121                .child(example_group_with_title(
122                    "Basic Usage",
123                    vec![
124                        single_example(
125                            "Three Levels",
126                            Breadcrumb::new()
127                                .item(BreadcrumbItem::new("crumb-home", "Home"))
128                                .item(BreadcrumbItem::new("crumb-projects", "Projects"))
129                                .item(BreadcrumbItem::new("crumb-current", "Current Page"))
130                                .into_any_element(),
131                        ),
132                        single_example(
133                            "Deep Hierarchy",
134                            Breadcrumb::new()
135                                .item(BreadcrumbItem::new("crumb-org", "Acme Corp"))
136                                .item(BreadcrumbItem::new("crumb-team", "Engineering"))
137                                .item(BreadcrumbItem::new("crumb-repo", "rust-dex"))
138                                .item(BreadcrumbItem::new("crumb-file", "gallery_app.rs"))
139                                .into_any_element(),
140                        ),
141                        single_example(
142                            "Two Levels",
143                            Breadcrumb::new()
144                                .item(BreadcrumbItem::new("crumb-root", "Dashboard"))
145                                .item(BreadcrumbItem::new("crumb-leaf", "Settings"))
146                                .into_any_element(),
147                        ),
148                    ],
149                ))
150                .into_any_element(),
151        )
152    }
153}