bezel

Components / Menus & actions

Menu

Markdown crates/ui/src/popover.rs docs.rs

use motion::{Fade, Painter};
use ui::popover;

let view = Painter::of(cx);

popover::popover_card(&theme).w(px(240.0)).children([
    popover::menu_heading(&theme, "Section").into_any_element(),
    popover::menu_row(&theme, false, Some(Fade::new(view, "m-one")))
        .child("First item")
        .into_any_element(),
    popover::menu_row(&theme, true, None)
        .child("Active item")
        .into_any_element(),
    popover::divider().into_any_element(),
])

active is the row the cursor is on, and a menu has exactly one cursor. Pass Some(fade) to let the mouse light rows by itself, None when the menu owns an active index and moves it from on_mouse_move.

Floating it

trigger.child(popover::anchored_menu_below("theme-menu", card, self.menu.closing_since()))

Closing

if self.menu.begin_close() {
    popover::reap_popup(self, cx, |view: &mut Self| &mut view.menu);
}

gpui unmounts an element the frame its state drops, so Popup is what holds it alive while menu-out plays.

Described rows

menu::Item::action("Open…")
    .with_long_description("Choose a markdown file from this workspace to edit")

One clipped line under the title, which widens the panel to 280px. with_tooltip sets hover text alone, including why a disabled row is disabled.

Submenus

menu::card(&theme, "file-menu", &items, &self.cursor, cx, |view, hit, _, cx| match hit {
    menu::Hit::Point(path) => {
        if view.cursor.point_at(&items, &path) { cx.notify() }
    }
    menu::Hit::Choose(path) => view.run(path, cx),
    menu::Hit::Dismiss => view.close(cx),
})

One cursor, both devices: the pointer moves the cursor rather than lighting a row of its own, so an open submenu can only hang off the row that is live. Hover, click and right all arrive as Hit::Point.

API

// ui::popover

pub fn popover_card(theme: &Theme) -> gpui::Div;
pub fn menu_heading(theme: &Theme, label: impl Into<SharedString>) -> gpui::Div;
pub fn menu_row(theme: &Theme, active: bool, fade: Option<Fade>) -> gpui::Div;
pub fn divider() -> gpui::Div;

/// Pins to the trigger's top-left. `_below` for dropdowns, `_above` and
/// `_above_end` near the window's bottom and right edges — gpui does not flip
/// sides for you.
pub fn anchored_menu(
    id: impl Into<SharedString>,
    content: AnyElement,
    closing: Option<web_time::Instant>,
) -> AnyElement;

/// Begin and schedule the close together.
pub fn close_popup<V: 'static, T: 'static>(
    view: &mut V,
    cx: &mut gpui::Context<V>,
    popup: impl Fn(&mut V) -> &mut Popup<T> + Copy + 'static,
);

// ...
// ui::menu

/// Paints every panel; the caller holds one `Cursor`.
pub fn card<V: 'static>(
    theme: &Theme,
    id: impl Into<SharedString>,
    items: &[Item],
    cursor: &Cursor,
    cx: &mut Context<V>,
    on: impl Fn(&mut V, Hit, &mut Window, &mut Context<V>) + 'static,
) -> gpui::Div;

/// A hit carries a path — one row index per level, outermost first.
/// `Dismiss` comes back from the card rather than from `.on_mouse_down_out`:
/// with a submenu open, a click on its rows lands outside the parent.
pub enum Hit { Point(Vec<usize>), Choose(Vec<usize>), Dismiss }

pub fn at<'a>(items: &'a [Item], path: &[usize]) -> Option<&'a Item>;

impl Cursor {
    /// Keyboard travel, pure and tested on its own.
    pub fn step(&mut self, root: &[Item], delta: isize);
    pub fn descend(&mut self, root: &[Item]) -> bool;
    pub fn ascend(&mut self) -> bool;

    /// Which row each panel draws lit.
    pub fn lit(&self, depth: usize) -> Option<usize>;

    /// Hover, click and `right` all arrive here.
    pub fn point_at(&mut self, root: &[Item], path: &[usize]) -> bool;

    // ...
}

Every layer occludes — hitboxes are paint-order only in gpui, so without it a click on a row would also fire whatever sits underneath.

Menu