1use std::cell::Cell;
7use std::rc::Rc;
8
9use gpui::{AnyElement, Bounds, Context, Pixels, Render, anchored, canvas, deferred, point};
10use smallvec::SmallVec;
11
12use crate::prelude::*;
13
14#[derive(Clone)]
16pub struct NavigationMenuSubItem {
17 pub label: SharedString,
18}
19
20#[derive(Clone)]
22pub struct NavigationMenuItemDef {
23 pub label: SharedString,
24 pub items: Vec<NavigationMenuSubItem>,
25}
26
27#[derive(IntoElement)]
29pub struct NavigationMenuList {
30 children: SmallVec<[AnyElement; 4]>,
31}
32
33impl NavigationMenuList {
34 pub fn new() -> Self {
35 Self {
36 children: SmallVec::new(),
37 }
38 }
39}
40
41impl Default for NavigationMenuList {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl ParentElement for NavigationMenuList {
48 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
49 self.children.extend(elements);
50 }
51}
52
53impl RenderOnce for NavigationMenuList {
54 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
55 h_flex().items_center().gap_1().children(self.children)
56 }
57}
58
59#[derive(IntoElement)]
61pub struct NavigationMenuTrigger {
62 label: SharedString,
63 active: bool,
64}
65
66impl NavigationMenuTrigger {
67 pub fn new(label: impl Into<SharedString>) -> Self {
68 Self {
69 label: label.into(),
70 active: false,
71 }
72 }
73
74 pub fn active(mut self, active: bool) -> Self {
75 self.active = active;
76 self
77 }
78}
79
80impl RenderOnce for NavigationMenuTrigger {
81 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
82 if self.active {
83 Button::new("nav-menu-trigger", self.label)
84 .primary()
85 .style(ButtonStyle::Filled)
86 } else {
87 Button::new("nav-menu-trigger", self.label).style(ButtonStyle::Transparent)
88 }
89 }
90}
91
92#[derive(IntoElement)]
94pub struct NavigationMenuViewport {
95 children: SmallVec<[AnyElement; 6]>,
96}
97
98impl NavigationMenuViewport {
99 pub fn new() -> Self {
100 Self {
101 children: SmallVec::new(),
102 }
103 }
104}
105
106impl Default for NavigationMenuViewport {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112impl ParentElement for NavigationMenuViewport {
113 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
114 self.children.extend(elements);
115 }
116}
117
118impl RenderOnce for NavigationMenuViewport {
119 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
120 div()
121 .w_full()
122 .p_4()
123 .rounded_md()
124 .border_1()
125 .border_color(semantic::border(cx))
126 .bg(semantic::elevated_surface(cx))
127 .shadow_level(Shadow::Md)
128 .child(h_flex().gap_6().children(self.children))
129 }
130}
131
132#[derive(IntoElement)]
134pub struct NavigationMenuLink {
135 label: SharedString,
136 on_click: Option<Rc<dyn Fn(&mut Window, &mut App) + 'static>>,
137}
138
139impl NavigationMenuLink {
140 pub fn new(label: impl Into<SharedString>) -> Self {
141 Self {
142 label: label.into(),
143 on_click: None,
144 }
145 }
146
147 pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
148 self.on_click = Some(Rc::new(handler));
149 self
150 }
151}
152
153impl RenderOnce for NavigationMenuLink {
154 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
155 let hover = semantic::hover_bg(cx);
156 let handler = self.on_click;
157 h_flex()
158 .id(ElementId::Name(self.label.clone()))
159 .px_3()
160 .py_2()
161 .rounded_md()
162 .cursor_pointer()
163 .hover(move |s| s.bg(hover))
164 .when_some(handler, |this, handler| {
165 this.on_click(move |_, window, cx| handler(window, cx))
166 })
167 .child(Label::new(self.label))
168 }
169}
170
171#[derive(IntoElement)]
173pub struct NavigationMenuContent {
174 title: SharedString,
175 children: SmallVec<[AnyElement; 4]>,
176}
177
178impl NavigationMenuContent {
179 pub fn new(title: impl Into<SharedString>) -> Self {
180 Self {
181 title: title.into(),
182 children: SmallVec::new(),
183 }
184 }
185}
186
187impl ParentElement for NavigationMenuContent {
188 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
189 self.children.extend(elements);
190 }
191}
192
193impl RenderOnce for NavigationMenuContent {
194 fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
195 v_flex()
196 .gap_2()
197 .min_w(px(160.))
198 .child(
199 Label::new(self.title)
200 .size(LabelSize::Small)
201 .weight(gpui::FontWeight::SEMIBOLD),
202 )
203 .children(self.children)
204 }
205}
206
207pub struct NavigationMenu {
211 items: Vec<NavigationMenuItemDef>,
212 open_index: Option<usize>,
213 trigger_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
220}
221
222impl NavigationMenu {
223 pub fn new(items: Vec<NavigationMenuItemDef>) -> Self {
224 Self {
225 items,
226 open_index: None,
227 trigger_bounds: Rc::new(Cell::new(None)),
228 }
229 }
230
231 pub fn open_index(&self) -> Option<usize> {
232 self.open_index
233 }
234}
235
236impl Render for NavigationMenu {
237 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
238 let open_index = self.open_index;
239
240 let triggers = self.items.iter().enumerate().map(|(index, item)| {
241 let is_open = open_index == Some(index);
242 h_flex()
243 .id(("navigation-menu-item", index))
244 .on_hover(cx.listener(move |this, &hovered, _, cx| {
245 if hovered {
246 this.open_index = Some(index);
247 }
248 cx.notify();
249 }))
250 .child(NavigationMenuTrigger::new(item.label.clone()).active(is_open))
251 });
252
253 let viewport = open_index
254 .and_then(|index| self.items.get(index))
255 .map(|item| {
256 NavigationMenuViewport::new().child(
257 NavigationMenuContent::new(item.label.clone()).children(
258 item.items
259 .iter()
260 .map(|sub| NavigationMenuLink::new(sub.label.clone())),
261 ),
262 )
263 });
264
265 let trigger_bounds = self.trigger_bounds.clone();
273 let trigger_row = div()
274 .child(NavigationMenuList::new().children(triggers))
275 .child({
276 let trigger_bounds = trigger_bounds.clone();
277 canvas(
278 move |bounds, _window, _cx| trigger_bounds.set(Some(bounds)),
279 |_bounds, _state, _window, _cx| {},
280 )
281 .absolute()
282 .top_0()
283 .left_0()
284 .size_full()
285 });
286
287 let floating_viewport = viewport.map(|viewport| {
288 let mut anchor = anchored().snap_to_window_with_margin(px(8.));
289 if let Some(bounds) = self.trigger_bounds.get() {
290 anchor = anchor.position(point(
291 bounds.origin.x,
292 bounds.origin.y + bounds.size.height + px(4.),
293 ));
294 }
295 deferred(anchor.child(div().occlude().child(viewport))).with_priority(1)
296 });
297
298 v_flex()
299 .id("navigation-menu")
300 .gap_2()
301 .on_hover(cx.listener(|this, hovered: &bool, _, cx| {
302 if !hovered {
303 this.open_index = None;
304 cx.notify();
305 }
306 }))
307 .child(trigger_row)
308 .when_some(floating_viewport, |this, floating_viewport| {
309 this.child(floating_viewport)
310 })
311 }
312}
313
314#[derive(IntoElement, RegisterComponent)]
316pub struct NavigationMenuPreview;
317
318impl RenderOnce for NavigationMenuPreview {
319 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
320 cx.new(|_| {
321 NavigationMenu::new(vec![
322 NavigationMenuItemDef {
323 label: "Getting Started".into(),
324 items: vec![
325 NavigationMenuSubItem {
326 label: "Introduction".into(),
327 },
328 NavigationMenuSubItem {
329 label: "Installation".into(),
330 },
331 ],
332 },
333 NavigationMenuItemDef {
334 label: "Components".into(),
335 items: vec![
336 NavigationMenuSubItem {
337 label: "Button".into(),
338 },
339 NavigationMenuSubItem {
340 label: "Dialog".into(),
341 },
342 NavigationMenuSubItem {
343 label: "Table".into(),
344 },
345 ],
346 },
347 ])
348 })
349 }
350}
351
352impl Component for NavigationMenuPreview {
353 fn scope() -> ComponentScope {
354 ComponentScope::Navigation
355 }
356
357 fn description() -> Option<&'static str> {
358 Some("Horizontal navigation menu with hover-intent submenu viewport.")
359 }
360
361 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
362 NavigationMenuPreview
363 .render(window, cx)
364 .into_any_element()
365 .into()
366 }
367}