Skip to content

Guide Recipes ContextMenu

MeowLynxSea edited this page Jun 13, 2026 · 4 revisions

Recipe: Context menu on a list row (popover + right-click)

This recipe shows how to attach a right-click context menu to a row using the v0.3 headless API.

Goal:

  • Right-click opens a menu.
  • Clicking outside closes it.
  • Menu items dispatch actions.

Building blocks

  • popover — headless composite that anchors content next to its trigger. Lives in crates/yororen-ui-core/src/headless/popover.rs. Owns an Entity<PopoverState> with open() / close() / toggle(). The renderer floats the content beside the trigger when state.is_open().
  • menu — headless body-only shell that renders one row per DropdownItem. Lives in crates/yororen-ui-core/src/headless/menu.rs. The renderer's click handler fires state.on_select.
  • Right-click on a div — wire .on_mouse_down(MouseButton::Right, …) on the row. There is no context_menu_trigger primitive in v0.3 (it was removed because it added no behaviour over a plain div().on_mouse_down(...)); do it directly on the row.

Pattern: popover-driven menu triggered by right click

The clean shape is a popover whose trigger is a row that captures right-click, and whose content is a menu driven by an Entity<MenuState>. The right-click handler opens the popover; the menu's set_on_select callback dispatches the action and closes the popover.

use gpui::{Context, IntoElement, MouseButton, ParentElement, Render, SharedString, Window, div};
use yororen_ui::headless::button::button;
use yororen_ui::headless::dropdown_menu::{
    DropdownItem, DropdownMenuItem,
};
use yororen_ui::headless::list_item::list_item;
use yororen_ui::headless::menu::{menu, MenuState};
use yororen_ui::headless::popover::{PopoverState, popover};

pub struct RowWithMenu {
    pub row_label:     String,
    pub popover_state: gpui::Entity<PopoverState>,
    pub menu_state:    gpui::Entity<MenuState>,
}

impl RowWithMenu {
    pub fn new(cx: &mut gpui::App) -> Self {
        Self {
            row_label: "Row".into(),
            popover_state: PopoverState::new(cx),
            menu_state:    MenuState::new(cx),
        }
    }
}

impl Render for RowWithMenu {
    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let popover_state     = self.popover_state.clone();
        let menu_state        = self.menu_state.clone();
        let popover_for_open  = popover_state.clone();
        let popover_for_close = popover_state.clone();

        // Wire the menu items + on_select once per render
        // (cheap; `MenuState` stores the callbacks for the
        // renderer's click handlers to fire).
        menu_state.update(cx, |s, _cx| {
            s.set_items(vec![
                DropdownItem::Item(DropdownMenuItem::new("open",   "Open")),
                DropdownItem::Item(DropdownMenuItem::new("rename", "Rename")),
                DropdownItem::Separator,
                DropdownItem::Item(DropdownMenuItem::new("delete", "Delete")),
            ]);
            s.set_on_select(move |id: SharedString, _w, cx| {
                match id.as_str() {
                    "open"   => { /* … */ }
                    "rename" => { /* … */ }
                    "delete" => { /* … */ }
                    _ => {}
                }
                // Close the popover after pick.
                popover_for_close.update(cx, |s, _cx| s.close());
            });
        });

        // The trigger: a row that captures right-click. There is
        // no `context_menu_trigger` factory — wire the
        // mouse-down handler directly on the row.
        let popover_state_for_open = popover_state.clone();
        let row = list_item("row-content", self.row_label.clone(), cx)
            .selected(false)
            .render(cx)
            .on_mouse_down(MouseButton::Right, move |_ev, _window, cx| {
                popover_state_for_open.update(cx, |s, _cx| s.toggle());
            });

        // The body: a menu driven by menu_state.
        let content = menu("row-menu", menu_state.clone()).render(cx);

        // The popover composes trigger + content. The renderer
        // floats `content` next to `row` when popover_state
        // is open.
        popover("row-menu", popover_for_open)
            .trigger(row.into_any_element())
            .content(content)
            .render(cx)
    }
}

Notes

  • Right-click is captured with .on_mouse_down(MouseButton::Right, …) on the row. The closure receives &MouseDownEvent, which carries the screen position: Point<Pixels> if you want to anchor the popover at the cursor rather than at the trigger bounds; call PopoverState::set_placement to your preferred value.
  • The popover's dismiss_on_outside_click and dismiss_on_escape are both true by default — clicking outside or pressing Escape closes the menu without extra wiring.
  • set_on_select is Fn(SharedString, &mut Window, &mut App) — the id comes in as a SharedString, not &str. Use id.as_str() to compare against string literals.
  • DropdownItem::Item(DropdownMenuItem::new(id, label)) is the item variant. DropdownItem::Separator draws a divider. There is no separate menu_item factory — items are constructed directly and passed via set_items(Vec<DropdownItem>).

Alternative: click-triggered dropdown_menu

If you don't need right-click and want a click-triggered menu (think: "⋯" button on a row), reach for dropdown_menu directly with a button trigger:

use gpui::{Context, IntoElement, ParentElement, Render, SharedString, Window, div};
use yororen_ui::headless::button::button;
use yororen_ui::headless::dropdown_menu::{
    DropdownItem, DropdownMenuItem, DropdownMenuState, dropdown_menu,
};

pub struct RowWithDropdown {
    pub row_label:  String,
    pub menu_state: gpui::Entity<DropdownMenuState>,
}

impl RowWithDropdown {
    pub fn new(cx: &mut gpui::App) -> Self {
        Self {
            row_label: "Row".into(),
            menu_state: DropdownMenuState::new(cx),
        }
    }
}

impl Render for RowWithDropdown {
    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let menu_state = self.menu_state.clone();

        menu_state.update(cx, |s, _cx| {
            s.set_items(vec![
                DropdownItem::Item(DropdownMenuItem::new("open",   "Open")),
                DropdownItem::Item(DropdownMenuItem::new("rename", "Rename")),
                DropdownItem::Item(DropdownMenuItem::new("delete", "Delete")),
            ]);
            s.set_on_select(move |id: SharedString, _w, _cx| {
                // … handle the pick …
            });
        });

        let trigger = button("row-trigger", cx)
            .render(cx)
            .child("⋯");

        dropdown_menu("row-dropdown", menu_state.clone())
            .trigger(trigger.into_any_element())
            .render(cx)
    }
}

Use popover + right-click when the trigger is a list row; use dropdown_menu when the trigger is a button.

See also

Clone this wiki locally