Overlays
UI that paints above the page. Modal, ConfirmModal, Drawer,
LoadingOverlay, Tooltip are stateless builders; Menu, MenuBar,
ContextMenu, Popover, HoverCard, Spotlight are stateful entities. Most
are built on the same mechanism: a deferred() layer (optionally occlude()d)
so it overlays sibling content.
Popover is the reusable anchored-floating primitive; Menu/Select predate
it and still hand-roll their own dropdown, but new flyouts should build on it.
Modal
A centered dialog over a dimming backdrop. Controlled: the parent owns an
opened flag and renders the Modal only while it is true, passing an
on_close handler. Implements ParentElement.
// In render, after the page content:
let mut root = div().relative().size_full().child(page);
if self.modal_open {
root = root.child(
Modal::new()
.title("Delete project?")
.on_close(cx.listener(|this, _ev, _w, cx| { this.modal_open = false; cx.notify(); }))
.child(Text::new("This cannot be undone.").dimmed())
.child(
Group::new().justify(Justify::End)
.child(Button::new("cancel", "Cancel").variant(Variant::Default)
.on_click(cx.listener(|this, _, _, cx| { this.modal_open = false; cx.notify(); })))
.child(Button::new("confirm", "Delete").color(ColorName::Red)
.on_click(cx.listener(|this, _, _, cx| { this.modal_open = false; cx.notify(); }))),
),
);
}
root
| Method | Default |
|---|---|
new() |
— |
title(impl Into<SharedString>) |
none (adds a header with a close ×) |
width(f32) |
440.0 |
padding(Size) |
Lg |
radius(Size) |
Md |
on_close(handler) |
none — clicking the backdrop or × calls it |
Notes:
- Render the modal as a child of a full-size root (
div().relative().size_full()) so the backdrop covers the window. The backdrop sizes itself to the viewport and paints viadeferred, so it overlays everything. - The dialog stops click propagation, so clicking inside it won't close it.
on_closetakesFn(&ClickEvent, &mut Window, &mut App), socx.listenerworks directly.
ConfirmModal
A confirm/cancel dialog composed from Modal. Controlled the
same way: the parent owns an opened flag, renders the ConfirmModal only
while it is true, and flips the flag from on_confirm / on_cancel — neither
button closes the dialog by itself. The backdrop and the header × also run
on_cancel. Implements ParentElement for body content beyond the plain
message.
if self.confirm_open {
root = root.child(
ConfirmModal::new()
.title("Delete file?")
.message("del.rs will be moved to the Trash.")
.confirm_label("Delete")
.danger()
.on_confirm(cx.listener(|this, _ev, _w, cx| { this.confirm_open = false; cx.notify(); }))
.on_cancel(cx.listener(|this, _ev, _w, cx| { this.confirm_open = false; cx.notify(); })),
);
}
| Method | Default | Notes |
|---|---|---|
new() |
— | |
title(impl Into<SharedString>) |
none | |
message(impl Into<SharedString>) |
none | dimmed body text; children also work |
confirm_label(...) |
"Confirm" |
|
cancel_label(...) |
"Cancel" |
|
danger() |
off | red confirm button for destructive actions |
width(f32) |
Modal's 440 |
|
on_confirm(handler) |
none | Fn(&ClickEvent, &mut Window, &mut App) |
on_cancel(handler) |
none | also fired by the backdrop and × |
Render it as a child of a full-size root, exactly like Modal.
Menu (entity)
A dropdown of actions: a trigger plus a deferred list of items, section labels, and dividers. Each item carries its own handler and the menu auto-closes.
let menu = cx.new(|cx| {
Menu::new(cx, "Actions")
.section("Edit")
.item("Copy", |_w, _app| { /* ... */ })
.item("Rename", |_w, _app| { /* ... */ })
.divider()
.danger_item("Delete", |_w, _app| { /* ... */ })
});
Methods: new(cx, trigger), item(label, |window, app| ...),
danger_item(label, handler) (red), section(label), divider(), size.
Keyboard: the trigger takes focus when opened, so ↑/↓ move the highlight across items, Enter runs the highlighted item, and Esc closes.
Item handlers get
(&mut Window, &mut App). To mutate a parent view from one, capture aWeakEntityof it andupdateinside.
MenuBar (entity)
A horizontal application menu — a row of top-level labels (File / Edit / View / …), each opening a dropdown. Once any menu is open, moving the pointer onto a sibling label switches to it, the way a desktop menu bar does. This is the themed, in-window counterpart to the native OS menu (see Window menu): use it when you draw your own titlebar, or on platforms with no native menu bar.
let bar = cx.new(|cx| {
MenuBar::new(cx)
.menu("File", |m| {
m.item_shortcut("New Tab", "⌘T", |_w, _app| { /* ... */ })
.item_shortcut("New Window", "⌘N", |_w, _app| { /* ... */ })
.divider()
.danger_item("Quit", |_w, _app| { /* ... */ })
})
.menu("Edit", |m| {
m.item_shortcut("Undo", "⌘Z", |_w, _app| {})
.disabled_item("Redo")
.divider()
.section("Clipboard")
.item_shortcut("Copy", "⌘C", |_w, _app| {})
.item_shortcut("Paste", "⌘V", |_w, _app| {})
})
});
Drop it into a titlebar strip or a StatusBar slot.
Each menu is built with a MenuColumn (the closure argument), which offers
item, item_shortcut(label, shortcut, handler), danger_item (red),
disabled_item(label) (greyed, inert), section(label), and divider().
MenuColumn is also exported, so menus can be assembled programmatically and
added with MenuBar::push(column).
Keyboard (while open): ←/→ switch menus, ↑/↓ move the highlight, Enter runs the
highlighted item, Esc closes. Set the top-level label size with size(Size).
ContextMenu (entity)
A right-click action menu at the pointer. Unlike Menu there
is no trigger element — call show(position, window, cx) with the window
coordinates of a MouseButton::Right mouse-down, and render the entity
anywhere in the tree (it paints nothing while closed). While open, a deferred
full-viewport backdrop occludes the page: clicking away, pressing Esc, or
running an item closes the menu.
let menu = cx.new(|cx| {
ContextMenu::new(cx)
.section("File")
.item_icon(IconName::Copy, "Copy path", |_w, _app| { /* ... */ })
.item("Rename", |_w, _app| { /* ... */ })
.divider()
.danger_item("Delete", |_w, _app| { /* ... */ })
});
// In the parent's render (MouseButton / MouseDownEvent are gpui types):
div()
.id("target")
.child("Right-click me")
.on_mouse_down(MouseButton::Right, cx.listener(|this, ev: &MouseDownEvent, window, cx| {
let position = ev.position;
this.menu.update(cx, |menu, cx| menu.show(position, window, cx));
}))
.child(self.menu.clone())
| Method | Default | Notes |
|---|---|---|
new(cx) |
— | items via the builders below |
item(label, handler) |
— | handler gets (&mut Window, &mut App) |
item_icon(IconName, label, handler) |
— | leading icon |
danger_item(label, handler) |
— | rendered in red |
section(label) / divider() |
— | non-interactive |
size(Size) |
Sm |
item font scale |
width(f32) |
220.0 |
fixed; also drives the edge clamp |
show(position, window, cx) |
— | opens at window coordinates, grabs focus so Esc closes |
close(window, cx) / is_open() |
— | programmatic state; closing restores the previous focus |
Notes:
- The menu clamps to the window edges (8px margin). gpui hands elements no
bounds of their own before paint, so the clamp works from an estimated menu
size — a
widththat matches your longest label keeps the right-edge clamp exact. - Item handlers run after the menu closes, like
Menu.
Popover (entity)
The reusable anchored-floating primitive: a trigger plus a deferred panel
positioned relative to it. Both the trigger and content are builder closures,
re-invoked each render so they show live data. Closes on Esc or a second trigger
click; call close(cx) from a content action to dismiss.
let pop = cx.new(|cx| {
Popover::new(
cx,
|_w, _app| Button::new("trigger", "Options").into_any_element(),
|_w, _app| Text::new("Panel content").into_any_element(),
)
.placement(Placement::Bottom)
.width(220.0)
});
Methods: new(cx, trigger_fn, content_fn), placement(Placement),
width(f32). State: is_open(), open(cx), close(cx), toggle(cx).
Placement is Bottom | BottomEnd | Top | TopEnd.
HoverCard (entity)
An anchored panel that opens on hover — Popover's
pointer-driven sibling. Trigger and content are builder closures re-invoked
each render. The card opens after open_delay of hovering the trigger and
closes close_delay after the pointer leaves both the trigger and the card,
so the card's content is reachable and interactive.
let card = cx.new(|cx| {
HoverCard::new(
cx,
|_w, _app| Badge::new("@ada").into_any_element(),
|_w, _app| Text::new("Ada Lovelace — wrote the first program.").into_any_element(),
)
.placement(Placement::Bottom)
.width(260.0)
});
| Method | Default | Notes |
|---|---|---|
new(cx, trigger_fn, content_fn) |
— | closures return AnyElement |
placement(Placement) |
Bottom |
same enum as Popover |
width(f32) |
content-sized, min 180px | |
open_delay(Duration) |
300ms | hover time before opening |
close_delay(Duration) |
150ms | grace period to reach the card |
is_open() / close(cx) |
— | close dismisses immediately |
At most one delayed transition is ever pending — re-entering the trigger or the card cancels a scheduled close.
Drawer
A panel that slides in from a window edge over a scrim. Controlled like
Modal: render it only while opened, place it in a full-size root, pass an
on_close. Implements ParentElement.
if self.drawer_open {
root = root.child(
Drawer::new()
.title("Filters")
.side(Side::Right)
.size(360.0)
.on_close(cx.listener(|this, _ev, _w, cx| { this.drawer_open = false; cx.notify(); }))
.child(filters),
);
}
Methods: new(), title, side(Side) (default Right), size(f32) (width for
left/right, height for top/bottom), padding(Size), on_close. Side is
Left | Right | Top | Bottom.
Spotlight (entity)
A command palette: a centered overlay with a search field and a keyboard-navigable command list. Type to filter, ↑/↓ to move, Enter to run, Esc to dismiss. Render it in a full-size root; open it from an action.
let palette = cx.new(|cx| {
Spotlight::new(cx)
.item("New file", |_w, _app| { /* ... */ })
.item_hint("Toggle theme", "⌘T", |_w, _app| { /* ... */ })
});
// open from a handler that has a window:
palette.update(cx, |s, cx| s.open(window, cx));
Methods: new(cx), item(label, handler), item_hint(label, hint, handler).
State: is_open(), open(window, cx), close(cx).
LoadingOverlay
A dimming busy layer over one container. Stateless: render it as the
last child of the container and flip visible. While visible it fills the
parent with the body color at 60% opacity, centers a
Loader, and occludes the mouse so the content
underneath is inert. Hidden, it renders nothing at all.
div()
.relative() // required — the overlay is absolutely positioned
.child(form)
.child(LoadingOverlay::new().visible(self.saving))
| Method | Default | Notes |
|---|---|---|
new() |
— | |
visible(bool) |
false |
|
loader(Loader) |
Loader::new() |
swap variant/color/size |
The parent must be .relative(). The overlay is an
absolutely-positioned size_full child, and gpui resolves absolute
positioning against the nearest non-statically-positioned ancestor — without
.relative() it anchors to the wrong box and dims the wrong content.
Tooltip
A small themed bubble, plugged into gpui's built-in .tooltip(...). Use the
tooltip(...) helper, which returns the builder closure gpui expects, and
attach it to any interactive element:
div()
.id("hoverme")
.child("Hover me")
.tooltip(guise::tooltip("Helpful hint"))
tooltip(label) returns impl Fn(&mut Window, &mut App) -> AnyView. The
Tooltip view itself is public if you need a custom builder.