Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:59:49.495Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

application_menu.rs

346 lines · 12.8 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.