Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:38:05.228Z 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

dropdown_menu.rs

420 lines · 15.5 KB · rust
1use gpui::{Anchor, AnyView, Entity, Pixels, Point, Role};
2
3use crate::{ButtonLike, ContextMenu, PopoverMenu, prelude::*};
4
5use super::PopoverMenuHandle;
6
7#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum DropdownStyle {
9    #[default]
10    Solid,
11    Outlined,
12    Subtle,
13    Ghost,
14}
15
16enum LabelKind {
17    Text(SharedString),
18    Element(AnyElement),
19}
20
21#[derive(IntoElement, RegisterComponent)]
22pub struct DropdownMenu {
23    id: ElementId,
24    label: LabelKind,
25    trigger_size: ButtonSize,
26    trigger_tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
27    trigger_icon: Option<IconName>,
28    style: DropdownStyle,
29    menu: Entity<ContextMenu>,
30    full_width: bool,
31    disabled: bool,
32    handle: Option<PopoverMenuHandle<ContextMenu>>,
33    attach: Option<Anchor>,
34    offset: Option<Point<Pixels>>,
35    tab_index: Option<isize>,
36    chevron: bool,
37    aria_label: Option<SharedString>,
38    aria_description: Option<SharedString>,
39    aria_value: Option<SharedString>,
40}
41
42impl DropdownMenu {
43    pub fn new(
44        id: impl Into<ElementId>,
45        label: impl Into<SharedString>,
46        menu: Entity<ContextMenu>,
47    ) -> Self {
48        Self {
49            id: id.into(),
50            label: LabelKind::Text(label.into()),
51            trigger_size: ButtonSize::Default,
52            trigger_tooltip: None,
53            trigger_icon: Some(IconName::ChevronUpDown),
54            style: DropdownStyle::default(),
55            menu,
56            full_width: false,
57            disabled: false,
58            handle: None,
59            attach: None,
60            offset: None,
61            tab_index: None,
62            chevron: true,
63            aria_label: None,
64            aria_description: None,
65            aria_value: None,
66        }
67    }
68
69    pub fn new_with_element(
70        id: impl Into<ElementId>,
71        label: AnyElement,
72        menu: Entity<ContextMenu>,
73    ) -> Self {
74        Self {
75            id: id.into(),
76            label: LabelKind::Element(label),
77            trigger_size: ButtonSize::Default,
78            trigger_tooltip: None,
79            trigger_icon: Some(IconName::ChevronUpDown),
80            style: DropdownStyle::default(),
81            menu,
82            full_width: false,
83            disabled: false,
84            handle: None,
85            attach: None,
86            offset: None,
87            tab_index: None,
88            chevron: true,
89            aria_label: None,
90            aria_description: None,
91            aria_value: None,
92        }
93    }
94
95    pub fn style(mut self, style: DropdownStyle) -> Self {
96        self.style = style;
97        self
98    }
99
100    pub fn trigger_size(mut self, size: ButtonSize) -> Self {
101        self.trigger_size = size;
102        self
103    }
104
105    pub fn trigger_tooltip(
106        mut self,
107        tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
108    ) -> Self {
109        self.trigger_tooltip = Some(Box::new(tooltip));
110        self
111    }
112
113    pub fn trigger_icon(mut self, icon: IconName) -> Self {
114        self.trigger_icon = Some(icon);
115        self
116    }
117
118    pub fn full_width(mut self, full_width: bool) -> Self {
119        self.full_width = full_width;
120        self
121    }
122
123    pub fn handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> Self {
124        self.handle = Some(handle);
125        self
126    }
127
128    /// Defines which corner of the handle to attach the menu's anchor to.
129    pub fn attach(mut self, attach: Anchor) -> Self {
130        self.attach = Some(attach);
131        self
132    }
133
134    /// Offsets the position of the menu by that many pixels.
135    pub fn offset(mut self, offset: Point<Pixels>) -> Self {
136        self.offset = Some(offset);
137        self
138    }
139
140    pub fn tab_index(mut self, arg: isize) -> Self {
141        self.tab_index = Some(arg);
142        self
143    }
144
145    pub fn no_chevron(mut self) -> Self {
146        self.chevron = false;
147        self
148    }
149
150    /// Sets the label announced by assistive technology.
151    /// Defaults to the trigger's visible label (typically the current value).
152    pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
153        self.aria_label = Some(label.into());
154        self
155    }
156
157    /// Sets the supplementary description announced by assistive technology
158    /// after the combobox's name, role, and value.
159    pub fn aria_description(mut self, description: impl Into<SharedString>) -> Self {
160        self.aria_description = Some(description.into());
161        self
162    }
163
164    /// Sets the current value announced by assistive technology (the selected
165    /// option). Defaults to the trigger's visible text label.
166    pub fn aria_value(mut self, value: impl Into<SharedString>) -> Self {
167        self.aria_value = Some(value.into());
168        self
169    }
170}
171
172impl Disableable for DropdownMenu {
173    fn disabled(mut self, disabled: bool) -> Self {
174        self.disabled = disabled;
175        self
176    }
177}
178
179impl RenderOnce for DropdownMenu {
180    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
181        let button_style = match self.style {
182            DropdownStyle::Solid => ButtonStyle::Filled,
183            DropdownStyle::Subtle => ButtonStyle::Subtle,
184            DropdownStyle::Outlined => ButtonStyle::Outlined,
185            DropdownStyle::Ghost => ButtonStyle::Transparent,
186        };
187
188        let full_width = self.full_width;
189        let trigger_size = self.trigger_size;
190        // Ensure a handle exists so assistive technology can open/close the menu
191        // via the Expand/Collapse accessibility actions (used by UIA on Windows
192        // and AX on macOS; on Linux/AT-SPI the click action is used instead).
193        let handle = self.handle.unwrap_or_default();
194        let expanded = handle.is_deployed();
195
196        // A combobox should announce its current value (the selected option).
197        // Default to the trigger's visible text label when no explicit value
198        // is provided.
199        let aria_value = self.aria_value.clone().or_else(|| match &self.label {
200            LabelKind::Text(text) => Some(text.clone()),
201            LabelKind::Element(_) => None,
202        });
203        let aria_description = self.aria_description.clone();
204
205        let a11y_actions = |button: Button| {
206            let show_handle = handle.clone();
207            let hide_handle = handle.clone();
208            button
209                .on_a11y_action(gpui::accesskit::Action::Expand, move |_, window, cx| {
210                    show_handle.show(window, cx);
211                })
212                .on_a11y_action(gpui::accesskit::Action::Collapse, move |_, _window, cx| {
213                    hide_handle.hide(cx);
214                })
215        };
216        let a11y_actions_element = |button: ButtonLike| {
217            let show_handle = handle.clone();
218            let hide_handle = handle.clone();
219            button
220                .on_a11y_action(gpui::accesskit::Action::Expand, move |_, window, cx| {
221                    show_handle.show(window, cx);
222                })
223                .on_a11y_action(gpui::accesskit::Action::Collapse, move |_, _window, cx| {
224                    hide_handle.hide(cx);
225                })
226        };
227
228        let (text_button, element_button) = match self.label {
229            LabelKind::Text(text) => (
230                Some(
231                    a11y_actions(Button::new(self.id.clone(), text))
232                        .aria_role(Role::ComboBox)
233                        .when_some(self.aria_label, |this, label| this.aria_label(label))
234                        .when_some(aria_description, |this, description| {
235                            this.aria_description(description)
236                        })
237                        .when_some(aria_value, |this, value| this.aria_value(value))
238                        .aria_expanded(expanded)
239                        .style(button_style)
240                        .when_some(self.trigger_icon.filter(|_| self.chevron), |this, icon| {
241                            this.end_icon(
242                                Icon::new(icon).size(IconSize::XSmall).color(Color::Muted),
243                            )
244                        })
245                        .when(full_width, |this| this.full_width())
246                        .size(trigger_size)
247                        .disabled(self.disabled)
248                        .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
249                ),
250                None,
251            ),
252            LabelKind::Element(element) => (
253                None,
254                Some(
255                    a11y_actions_element(ButtonLike::new(self.id.clone()))
256                        .aria_role(Role::ComboBox)
257                        .when_some(self.aria_label, |this, label| this.aria_label(label))
258                        .when_some(aria_description, |this, description| {
259                            this.aria_description(description)
260                        })
261                        .when_some(aria_value, |this, value| this.aria_value(value))
262                        .aria_expanded(expanded)
263                        .child(element)
264                        .style(button_style)
265                        .when(self.chevron, |this| {
266                            this.child(
267                                Icon::new(IconName::ChevronUpDown)
268                                    .size(IconSize::XSmall)
269                                    .color(Color::Muted),
270                            )
271                        })
272                        .when(full_width, |this| this.full_width())
273                        .size(trigger_size)
274                        .disabled(self.disabled)
275                        .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
276                ),
277            ),
278        };
279
280        // When the menu opens, move the selection to the current value (or the
281        // first item) so assistive technology announces a meaningful item
282        // immediately, instead of focusing the bare menu container and
283        // announcing only "menu". See the ARIA menu button pattern.
284        let menu_for_open = self.menu.clone();
285        let mut popover = PopoverMenu::new((self.id.clone(), "popover"))
286            .full_width(self.full_width)
287            .with_handle(handle)
288            .on_open(std::rc::Rc::new(move |window, cx| {
289                menu_for_open.update(cx, |menu, cx| {
290                    menu.select_toggled_or_first(window, cx);
291                });
292            }))
293            .menu(move |_window, _cx| Some(self.menu.clone()));
294
295        popover = match (text_button, element_button, self.trigger_tooltip) {
296            (Some(text_button), None, Some(tooltip)) => {
297                popover.trigger_with_tooltip(text_button, tooltip)
298            }
299            (Some(text_button), None, None) => popover.trigger(text_button),
300            (None, Some(element_button), Some(tooltip)) => {
301                popover.trigger_with_tooltip(element_button, tooltip)
302            }
303            (None, Some(element_button), None) => popover.trigger(element_button),
304            _ => popover,
305        };
306
307        popover
308            .attach(match self.attach {
309                Some(attach) => attach,
310                None => Anchor::BottomRight,
311            })
312            .when_some(self.offset, |this, offset| this.offset(offset))
313    }
314}
315
316impl Component for DropdownMenu {
317    fn scope() -> ComponentScope {
318        ComponentScope::Input
319    }
320
321    fn name() -> &'static str {
322        "DropdownMenu"
323    }
324
325    fn description() -> &'static str {
326        "A dropdown menu displays a list of actions or options. \
327        A dropdown menu is always activated by clicking a trigger (or via a keybinding)."
328    }
329
330    fn preview(window: &mut Window, cx: &mut App) -> AnyElement {
331        let menu = ContextMenu::build(window, cx, |this, _, _| {
332            this.entry("Option 1", None, |_, _| {})
333                .entry("Option 2", None, |_, _| {})
334                .entry("Option 3", None, |_, _| {})
335                .separator()
336                .entry("Option 4", None, |_, _| {})
337        });
338
339        let menu_with_submenu = ContextMenu::build(window, cx, |this, _, _| {
340            this.entry("Toggle All Docks", None, |_, _| {})
341                .submenu("Editor Layout", |menu, _, _| {
342                    menu.entry("Split Up", None, |_, _| {})
343                        .entry("Split Down", None, |_, _| {})
344                        .separator()
345                        .entry("Split Side", None, |_, _| {})
346                })
347                .separator()
348                .entry("Project Panel", None, |_, _| {})
349                .entry("Outline Panel", None, |_, _| {})
350                .separator()
351                .submenu("Autofill", |menu, _, _| {
352                    menu.entry("Contact…", None, |_, _| {})
353                        .entry("Passwords…", None, |_, _| {})
354                })
355                .submenu_with_icon("Predict", IconName::OmegaPredict, |menu, _, _| {
356                    menu.entry("Everywhere", None, |_, _| {})
357                        .entry("At Cursor", None, |_, _| {})
358                        .entry("Over Here", None, |_, _| {})
359                        .entry("Over There", None, |_, _| {})
360                })
361        });
362
363        v_flex()
364            .gap_6()
365            .children(vec![
366                example_group_with_title(
367                    "Basic Usage",
368                    vec![
369                        single_example(
370                            "Default",
371                            DropdownMenu::new("default", "Select an option", menu.clone())
372                                .into_any_element(),
373                        ),
374                        single_example(
375                            "Full Width",
376                            DropdownMenu::new("full-width", "Full Width Dropdown", menu.clone())
377                                .full_width(true)
378                                .into_any_element(),
379                        ),
380                    ],
381                ),
382                example_group_with_title(
383                    "Submenus",
384                    vec![single_example(
385                        "With Submenus",
386                        DropdownMenu::new("submenu", "Submenu", menu_with_submenu)
387                            .into_any_element(),
388                    )],
389                ),
390                example_group_with_title(
391                    "Styles",
392                    vec![
393                        single_example(
394                            "Outlined",
395                            DropdownMenu::new("outlined", "Outlined Dropdown", menu.clone())
396                                .style(DropdownStyle::Outlined)
397                                .into_any_element(),
398                        ),
399                        single_example(
400                            "Ghost",
401                            DropdownMenu::new("ghost", "Ghost Dropdown", menu.clone())
402                                .style(DropdownStyle::Ghost)
403                                .into_any_element(),
404                        ),
405                    ],
406                ),
407                example_group_with_title(
408                    "States",
409                    vec![single_example(
410                        "Disabled",
411                        DropdownMenu::new("disabled", "Disabled Dropdown", menu)
412                            .disabled(true)
413                            .into_any_element(),
414                    )],
415                ),
416            ])
417            .into_any_element()
418    }
419}
420
Served at tenant.openagents/omega Member data and write actions are omitted.