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.