Skip to repository content346 lines · 12.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:59:49.495Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
application_menu.rs
1use gpui::{Action, Entity, OwnedMenu, OwnedMenuItem, Subscription, actions};
2use settings::{Settings, SettingsStore};
3use workspace::AccessibleMode;
4
5use schemars::JsonSchema;
6use serde::Deserialize;
7
8use smallvec::SmallVec;
9use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
10
11use crate::title_bar_settings::TitleBarSettings;
12
13actions!(
14 app_menu,
15 [
16 /// Activates the menu on the right in the client-side application menu.
17 ///
18 /// Does not apply to platform menu bars (e.g. on macOS).
19 ActivateMenuRight,
20 /// Activates the menu on the left in the client-side application menu.
21 ///
22 /// Does not apply to platform menu bars (e.g. on macOS).
23 ActivateMenuLeft
24 ]
25);
26
27/// Opens the named menu in the client-side application menu.
28///
29/// Does not apply to platform menu bars (e.g. on macOS).
30#[derive(Clone, Deserialize, JsonSchema, PartialEq, Default, Action)]
31#[action(namespace = app_menu)]
32pub struct OpenApplicationMenu(String);
33
34#[cfg(not(target_os = "macos"))]
35pub enum ActivateDirection {
36 Left,
37 Right,
38}
39
40#[derive(Clone)]
41struct MenuEntry {
42 menu: OwnedMenu,
43 handle: PopoverMenuHandle<ContextMenu>,
44}
45
46pub struct ApplicationMenu {
47 entries: SmallVec<[MenuEntry; 8]>,
48 pending_menu_open: Option<String>,
49 _settings_subscription: Subscription,
50}
51
52impl ApplicationMenu {
53 pub fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
54 let menus = cx.get_menus().unwrap_or_default();
55 // Re-render when settings change so toggling "accessible mode" expands or
56 // collapses the menu bar (see `all_menus_shown`).
57 let settings_subscription = cx.observe_global::<SettingsStore>(|_, cx| cx.notify());
58 Self {
59 entries: menus
60 .into_iter()
61 .map(|menu| MenuEntry {
62 menu,
63 handle: PopoverMenuHandle::default(),
64 })
65 .collect(),
66 pending_menu_open: None,
67 _settings_subscription: settings_subscription,
68 }
69 }
70
71 fn sanitize_menu_items(items: Vec<OwnedMenuItem>) -> Vec<OwnedMenuItem> {
72 let mut cleaned = Vec::new();
73 let mut last_was_separator = false;
74
75 for item in items {
76 match item {
77 OwnedMenuItem::Separator => {
78 if !last_was_separator {
79 cleaned.push(item);
80 last_was_separator = true;
81 }
82 }
83 OwnedMenuItem::Submenu(submenu) => {
84 // Skip empty submenus
85 if !submenu.items.is_empty() {
86 cleaned.push(OwnedMenuItem::Submenu(submenu));
87 last_was_separator = false;
88 }
89 }
90 item => {
91 cleaned.push(item);
92 last_was_separator = false;
93 }
94 }
95 }
96
97 // Remove trailing separator
98 if let Some(OwnedMenuItem::Separator) = cleaned.last() {
99 cleaned.pop();
100 }
101
102 cleaned
103 }
104
105 fn build_menu_from_items(
106 entry: MenuEntry,
107 window: &mut Window,
108 cx: &mut App,
109 ) -> Entity<ContextMenu> {
110 ContextMenu::build(window, cx, |menu, window, cx| {
111 // Grab current focus handle so menu can shown items in context with the focused element
112 let menu = menu.when_some(window.focused(cx), |menu, focused| menu.context(focused));
113 let sanitized_items = Self::sanitize_menu_items(entry.menu.items);
114
115 sanitized_items
116 .into_iter()
117 .fold(menu, |menu, item| match item {
118 OwnedMenuItem::Separator => menu.separator(),
119 OwnedMenuItem::Action {
120 name,
121 action,
122 checked,
123 disabled,
124 ..
125 } => menu.action_checked_with_disabled(name, action, checked, disabled),
126 OwnedMenuItem::Submenu(submenu) => {
127 submenu
128 .items
129 .into_iter()
130 .fold(menu, |menu, item| match item {
131 OwnedMenuItem::Separator => menu.separator(),
132 OwnedMenuItem::Action {
133 name,
134 action,
135 checked,
136 disabled,
137 ..
138 } => menu
139 .action_checked_with_disabled(name, action, checked, disabled),
140 OwnedMenuItem::Submenu(_) => menu,
141 OwnedMenuItem::SystemMenu(_) => {
142 // A system menu doesn't make sense in this context, so ignore it
143 menu
144 }
145 })
146 }
147 OwnedMenuItem::SystemMenu(_) => {
148 // A system menu doesn't make sense in this context, so ignore it
149 menu
150 }
151 })
152 })
153 }
154
155 fn render_application_menu(&self, entry: &MenuEntry) -> impl IntoElement {
156 let handle = entry.handle.clone();
157
158 let menu_name = entry.menu.name.clone();
159 let entry = entry.clone();
160
161 // Application menu must have same ids as first menu item in standard menu
162 div()
163 .id(format!("{}-menu-item", menu_name))
164 .occlude()
165 .child(
166 PopoverMenu::new(format!("{}-menu-popover", menu_name))
167 .menu(move |window, cx| {
168 Self::build_menu_from_items(entry.clone(), window, cx).into()
169 })
170 .trigger_with_tooltip(
171 IconButton::new(
172 SharedString::from(format!("{}-menu-trigger", menu_name)),
173 ui::IconName::Menu,
174 )
175 .style(ButtonStyle::Subtle)
176 .icon_size(IconSize::Small)
177 .tab_index(0isize)
178 .aria_label("Application menu"),
179 Tooltip::text("Open Application Menu"),
180 )
181 .with_handle(handle),
182 )
183 }
184
185 fn render_standard_menu(&self, entry: &MenuEntry) -> impl IntoElement {
186 let current_handle = entry.handle.clone();
187
188 let menu_name = entry.menu.name.clone();
189 let entry = entry.clone();
190
191 let all_handles: Vec<_> = self
192 .entries
193 .iter()
194 .map(|entry| entry.handle.clone())
195 .collect();
196
197 div()
198 .id(format!("{}-menu-item", menu_name))
199 .occlude()
200 .child(
201 PopoverMenu::new(format!("{}-menu-popover", menu_name))
202 .menu(move |window, cx| {
203 Self::build_menu_from_items(entry.clone(), window, cx).into()
204 })
205 .trigger(
206 Button::new(
207 SharedString::from(format!("{}-menu-trigger", menu_name)),
208 menu_name,
209 )
210 .style(ButtonStyle::Subtle)
211 .label_size(LabelSize::Small)
212 .tab_index(0isize),
213 )
214 .with_handle(current_handle.clone()),
215 )
216 .on_hover(move |hover_enter, window, cx| {
217 if *hover_enter && !current_handle.is_deployed() {
218 all_handles.iter().for_each(|h| h.hide(cx));
219
220 // We need to defer this so that this menu handle can take focus from the previous menu
221 let handle = current_handle.clone();
222 window.defer(cx, move |window, cx| handle.show(window, cx));
223 }
224 })
225 }
226
227 #[cfg(not(target_os = "macos"))]
228 pub fn open_menu(
229 &mut self,
230 action: &OpenApplicationMenu,
231 _window: &mut Window,
232 _cx: &mut Context<Self>,
233 ) {
234 self.pending_menu_open = Some(action.0.clone());
235 }
236
237 #[cfg(not(target_os = "macos"))]
238 pub fn navigate_menus_in_direction(
239 &mut self,
240 direction: ActivateDirection,
241 window: &mut Window,
242 cx: &mut Context<Self>,
243 ) {
244 let current_index = self
245 .entries
246 .iter()
247 .position(|entry| entry.handle.is_deployed());
248 let Some(current_index) = current_index else {
249 // No menu is open, so there is nothing to switch between. Let the
250 // arrow key continue to the title bar's toolbar navigation so it
251 // can move focus between title bar controls. Without this, the
252 // `left`/`right` bindings in the `ApplicationMenu` context would
253 // silently swallow the key as a no-op.
254 cx.propagate();
255 return;
256 };
257
258 let next_index = match direction {
259 ActivateDirection::Left => {
260 if current_index == 0 {
261 self.entries.len() - 1
262 } else {
263 current_index - 1
264 }
265 }
266 ActivateDirection::Right => {
267 if current_index == self.entries.len() - 1 {
268 0
269 } else {
270 current_index + 1
271 }
272 }
273 };
274
275 self.entries[current_index].handle.hide(cx);
276
277 // We need to defer this so that this menu handle can take focus from the previous menu
278 let next_handle = self.entries[next_index].handle.clone();
279 cx.defer_in(window, move |_, window, cx| next_handle.show(window, cx));
280 }
281
282 pub fn all_menus_shown(&self, cx: &mut Context<Self>) -> bool {
283 show_menus(cx)
284 || self.entries.iter().any(|entry| entry.handle.is_deployed())
285 || self.pending_menu_open.is_some()
286 // In accessible mode, keep the full menu bar expanded so every menu
287 // is individually reachable and labeled for assistive technology.
288 || cx.accessible_mode()
289 }
290}
291
292pub(crate) fn show_menus(cx: &mut App) -> bool {
293 TitleBarSettings::get_global(cx).show_menus
294 && (cfg!(not(target_os = "macos")) || option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some())
295}
296
297impl Render for ApplicationMenu {
298 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
299 let all_menus_shown = self.all_menus_shown(cx);
300
301 if let Some(pending_menu_open) = self.pending_menu_open.take()
302 && let Some(entry) = self
303 .entries
304 .iter()
305 .find(|entry| entry.menu.name == pending_menu_open && !entry.handle.is_deployed())
306 {
307 let handle_to_show = entry.handle.clone();
308 let handles_to_hide: Vec<_> = self
309 .entries
310 .iter()
311 .filter(|e| e.menu.name != pending_menu_open && e.handle.is_deployed())
312 .map(|e| e.handle.clone())
313 .collect();
314
315 if handles_to_hide.is_empty() {
316 // We need to wait for the next frame to show all menus first,
317 // before we can handle show/hide operations
318 window.on_next_frame(move |window, cx| {
319 handles_to_hide.iter().for_each(|handle| handle.hide(cx));
320 window.defer(cx, move |window, cx| handle_to_show.show(window, cx));
321 });
322 } else {
323 // Since menus are already shown, we can directly handle show/hide operations
324 handles_to_hide.iter().for_each(|handle| handle.hide(cx));
325 cx.defer_in(window, move |_, window, cx| handle_to_show.show(window, cx));
326 }
327 }
328
329 div()
330 .key_context("ApplicationMenu")
331 .flex()
332 .flex_row()
333 .gap_x_1()
334 .when(!all_menus_shown && !self.entries.is_empty(), |this| {
335 this.child(self.render_application_menu(&self.entries[0]))
336 })
337 .when(all_menus_shown, |this| {
338 this.children(
339 self.entries
340 .iter()
341 .map(|entry| self.render_standard_menu(entry)),
342 )
343 })
344 }
345}
346