Skip to content

Guide Recipes ContextMenu

MeowLynxSea edited this page Jun 13, 2026 · 4 revisions

Recipe: Context menu on a list row (context_menu_trigger + popover)

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

  • context_menu_trigger — headless factory that wires a right-click handler. Lives in crates/yororen-ui-core/src/headless/context_menu_trigger.rs. Exposes .on_show(point, window, cx) — your callback opens the popover. context_menu_trigger(id, cx) takes 2 args.
  • 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().
  • dropdown_menu / menu — body variants. menu (headless body-only shell) plus DropdownItem::Item(...) / DropdownItem::Separator are the menu primitives; the dropdown_menu factory wraps a trigger + content. See Component-DropdownMenu.

Pattern: popover-with-menu triggered by right click

The clean shape is a popover whose trigger is a context_menu_trigger-wrapped row, and whose content is a dropdown_menu driven by an Entity<DropdownMenuState>. The right-click handler opens the popover; the dropdown's set_on_select callback dispatches the action.

use gpui::{Context, IntoElement, ParentElement, Render, SharedString, Window, div};
use yororen_ui::headless::context_menu_trigger::context_menu_trigger;
use yororen_ui::headless::dropdown_menu::{
    DropdownItem, DropdownMenuItem, DropdownMenuState, dropdown_menu,
};
use yororen_ui::headless::list_item::list_item;
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<DropdownMenuState>,
}

impl RowWithMenu {
    pub fn new(cx: &mut gpui::App) -> Self {
        Self {
            row_label: "Row".into(),
            popover_state: PopoverState::new(cx),
            menu_state:    DropdownMenuState::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;
        // `DropdownMenuState` 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 list_item wrapped in context_menu_trigger.
        // Right-click fires on_show → we open the popover.
        let row = list_item("row-content", self.row_label.clone(), cx)
            .selected(false)
            .render(cx);

        let trigger = context_menu_trigger("row-trigger", &mut *cx)
            .on_show({
                let popover_state = popover_state.clone();
                move |_point, _w, cx| {
                    popover_state.update(cx, |s, _cx| s.toggle());
                }
            })
            .apply(row);

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

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

Notes

  • context_menu_trigger(id, cx) returns a ContextMenuTriggerProps that supports .on_show(point, window, cx) and .apply(div) — the apply path wires the right-click mouse handler. There's no .render(cx) because the visual is whatever div you pass to apply (here, a list_item).
  • Right-click position is delivered to on_show via &gpui::Point<gpui::Pixels> — useful if you want to anchor the popover at the cursor rather than at the trigger bounds. The default popover placement is BottomStart of the trigger; for cursor-anchored menus, set 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>).
  • popover.trigger(t: AnyElement) requires an AnyElement. context_menu_trigger.apply(div) returns Stateful<Div>, so call .into_any_element() to convert.

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 + context_menu_trigger when the trigger is right-click; use dropdown_menu when the trigger is a button.

See also

Clone this wiki locally