1use gpui::{Anchor, Context, Entity, Render};
2
3use crate::{ContextMenu, PopoverMenu, PopoverMenuHandle, prelude::*};
4
5struct MenubarItem {
6 label: SharedString,
7 menu: Entity<ContextMenu>,
8 handle: PopoverMenuHandle<ContextMenu>,
9}
10
11#[derive(RegisterComponent)]
18pub struct Menubar {
19 items: Vec<MenubarItem>,
20 open_index: Option<usize>,
21 focus_index: usize,
22}
23
24impl Menubar {
25 pub fn new(_cx: &mut Context<Self>) -> Self {
26 Self {
27 items: Vec::new(),
28 open_index: None,
29 focus_index: 0,
30 }
31 }
32
33 pub fn item(mut self, label: impl Into<SharedString>, menu: Entity<ContextMenu>) -> Self {
34 self.items.push(MenubarItem {
35 label: label.into(),
36 menu,
37 handle: PopoverMenuHandle::default(),
38 });
39 self
40 }
41
42 fn close_all(&mut self, cx: &mut App) {
43 for item in &self.items {
44 item.handle.hide(cx);
45 }
46 self.open_index = None;
47 }
48
49 fn open_at(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
50 if index >= self.items.len() {
51 return;
52 }
53 self.close_all(cx);
54 self.focus_index = index;
55 self.open_index = Some(index);
56 self.items[index].handle.show(window, cx);
57 cx.notify();
58 }
59
60 fn move_focus(&mut self, delta: isize, window: &mut Window, cx: &mut Context<Self>) {
61 if self.items.is_empty() {
62 return;
63 }
64 let len = self.items.len() as isize;
65 let next = (self.focus_index as isize + delta).rem_euclid(len) as usize;
66 if self.open_index.is_some() {
67 self.open_at(next, window, cx);
68 } else {
69 self.focus_index = next;
70 cx.notify();
71 }
72 }
73}
74
75impl Render for Menubar {
76 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
77 let mut bar = h_flex()
78 .id("menubar")
79 .w_full()
80 .items_center()
81 .gap_1()
82 .px_3()
83 .py_1()
84 .bg(semantic::surface(cx))
85 .border_b_1()
86 .border_color(semantic::border(cx))
87 .on_action(cx.listener(|this, _: &menu::SelectPrevious, window, cx| {
88 this.move_focus(-1, window, cx);
89 }))
90 .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| {
91 this.move_focus(1, window, cx);
92 }));
93
94 for (i, item) in self.items.iter().enumerate() {
95 let label = item.label.clone();
96 let menu = item.menu.clone();
97 let handle = item.handle.clone();
98 let is_open = self.open_index == Some(i);
99 let is_focused = self.focus_index == i;
100
101 bar = bar.child(
102 PopoverMenu::new(("menubar", i))
103 .attach(Anchor::BottomLeft)
104 .with_handle(handle)
105 .menu(move |_, _| Some(menu.clone()))
106 .trigger(
107 Button::new(("menubar-trigger", i), label)
108 .style(ButtonStyle::Transparent)
109 .toggle_state(is_open)
110 .when(is_focused, |this| {
111 this.color(Color::Custom(palette::primary(600)))
112 }),
113 ),
114 );
115 }
116
117 bar
118 }
119}
120
121impl Component for Menubar {
122 fn scope() -> ComponentScope {
123 ComponentScope::Navigation
124 }
125
126 fn description() -> Option<&'static str> {
127 Some("A horizontal menubar with dropdown menus and cross-menu keyboard navigation.")
128 }
129
130 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
131 let file_menu = ContextMenu::build(window, cx, |this, _, _| {
132 this.entry("New Tab", None, |_, _| {})
133 .entry("New Window", None, |_, _| {})
134 .separator()
135 .entry("Close Tab", None, |_, _| {})
136 });
137 let edit_menu = ContextMenu::build(window, cx, |this, _, _| {
138 this.entry("Undo", None, |_, _| {})
139 .entry("Redo", None, |_, _| {})
140 .separator()
141 .entry("Cut", None, |_, _| {})
142 .entry("Copy", None, |_, _| {})
143 .entry("Paste", None, |_, _| {})
144 });
145 let view_menu = ContextMenu::build(window, cx, |this, _, _| {
146 this.entry("Sidebar", None, |_, _| {})
147 .entry("Panel", None, |_, _| {})
148 });
149
150 Some(
151 cx.new(|cx| {
152 Menubar::new(cx)
153 .item("File", file_menu)
154 .item("Edit", edit_menu)
155 .item("View", view_menu)
156 })
157 .into_any_element(),
158 )
159 }
160}