Files
oak-gpui/crates/gpui_widgets/examples/menus_dialogs.rs
T
Mike-Solar d72d2e8210 feat(gpui_widgets): add menu bar, context menu and modal dialog framework
W2 of the oak task list. A pure menu model (nesting, disabled items,
separators, checked state, wrap-around keyboard navigation arithmetic) feeds
two views: a MenuBar with drop-down popups and a right-click ContextMenu,
both built on anchored+deferred with keyboard navigation (up/down/enter/
escape), hover-opened submenus, check marks, shortcut labels and request
events. The Modal framework provides the mask, title bar, content slot and
button row with escape/enter defaults; message_box (info/warning/error),
progress_dialog (driven by ProgressContent) and file_dialog (native gpui
fallback - the fork has no prompt_for_paths platform API) are built on it.
A menus_dialogs example demonstrates everything. 79 tests pass.
2026-08-09 05:33:29 +08:00

269 lines
10 KiB
Rust

//! A demo of the menu and dialog framework: a menu bar, a right-click
//! context menu, and the message/progress/file dialogs. Every action is
//! printed as a request event.
use gpui::{
App, Bounds, Context, Entity, Focusable, MouseButton, MouseDownEvent, Render, Window,
WindowBounds, WindowOptions, colors::DefaultColors, div, prelude::*, px, size,
};
use gpui_widgets::dialog::file_dialog::{FileDialogContent, file_dialog};
use gpui_widgets::dialog::message_box::{MessageBoxLevel, message_box};
use gpui_widgets::dialog::progress::{ProgressContent, progress_dialog};
use gpui_widgets::dialog::{Modal, ModalEvent};
use gpui_widgets::menu::{ContextMenu, ContextMenuEvent, Menu, MenuBar, MenuBarEntry, MenuBarEvent, MenuItem};
struct Example {
menu_bar: Entity<MenuBar>,
context_menu: Entity<ContextMenu>,
message: Entity<Modal>,
progress: Entity<Modal>,
progress_content: Entity<ProgressContent>,
file: Entity<Modal>,
file_content: Entity<FileDialogContent>,
show_message: bool,
show_progress: bool,
show_file: bool,
}
impl Example {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let menu_bar = cx.new(|cx| {
MenuBar::new(
1,
vec![
MenuBarEntry::new(
"File",
Menu::new(vec![
MenuItem::new(10, "New…").with_shortcut("⌘N"),
MenuItem::new(11, "Open…").with_shortcut("⌘O"),
MenuItem::new(12, "Save").with_shortcut("⌘S").separated(),
MenuItem::new(13, "Export").disabled(),
]),
),
MenuBarEntry::new(
"Edit",
Menu::new(vec![
MenuItem::new(20, "Undo").with_shortcut("⌘Z"),
MenuItem::new(21, "Redo").with_shortcut("⇧⌘Z").separated(),
MenuItem::new(22, "Show Diagnostics").with_checked(false),
]),
),
MenuBarEntry::new(
"View",
Menu::new(vec![
MenuItem::new(30, "Toolbars"),
MenuItem::new(31, "Scopes"),
MenuItem::new(32, "Theme").with_submenu(Menu::new(vec![
MenuItem::new(33, "Olive Dark").with_checked(true),
MenuItem::new(34, "Olive Light"),
])),
]),
),
],
window,
cx,
)
});
cx.subscribe(
&menu_bar,
|_this: &mut Self, _m: Entity<MenuBar>, event: &MenuBarEvent, _cx| {
println!("menu bar: {event:?}");
},
)
.detach();
let context_menu = cx.new(|cx| ContextMenu::new(2, window, cx));
cx.subscribe(
&context_menu,
|_this: &mut Self, _m: Entity<ContextMenu>, event: &ContextMenuEvent, _cx| {
println!("context menu: {event:?}");
},
)
.detach();
let message = message_box(
3,
MessageBoxLevel::Warning,
"Unsaved changes",
"Your project has unsaved changes. Export anyway?",
window,
cx,
);
cx.subscribe(
&message,
|this: &mut Self, _m: Entity<Modal>, event: &ModalEvent, _cx| {
println!("message box: {event:?}");
if matches!(event, ModalEvent::ButtonClicked { .. }) {
this.show_message = false;
}
},
)
.detach();
let (progress, progress_content) = progress_dialog(4, "Exporting…", "Encoding video", window, cx);
cx.subscribe(
&progress,
|this: &mut Self, _m: Entity<Modal>, event: &ModalEvent, _cx| {
println!("progress: {event:?}");
if matches!(event, ModalEvent::ButtonClicked { .. }) {
this.show_progress = false;
}
},
)
.detach();
let (file, file_content) = file_dialog(5, "Open media…", window, cx);
cx.subscribe(
&file,
|this: &mut Self, _m: Entity<Modal>, event: &ModalEvent, _cx| {
if let ModalEvent::ButtonClicked { button: 0, .. } = event {
let path = this.file_content.read(_cx).path(_cx);
println!("open file: {path}");
}
println!("file dialog: {event:?}");
if matches!(event, ModalEvent::ButtonClicked { .. }) {
this.show_file = false;
}
},
)
.detach();
Self {
menu_bar,
context_menu,
message,
progress,
progress_content,
file,
file_content,
show_message: false,
show_progress: false,
show_file: false,
}
}
}
impl Render for Example {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.default_colors().clone();
let mut root = div()
.size_full()
.bg(colors.background)
.flex()
.flex_col()
.child(self.menu_bar.clone())
.child(
div()
.flex_1()
.flex()
.items_center()
.justify_center()
.gap_2()
.text_color(colors.text)
.id("example-main")
.on_mouse_down(
MouseButton::Right,
cx.listener(|this, event: &MouseDownEvent, _window, cx| {
this.context_menu.update(cx, |menu, cx| {
menu.show(
event.position,
Menu::new(vec![
MenuItem::new(40, "Cut").with_shortcut("⌘X"),
MenuItem::new(41, "Copy").with_shortcut("⌘C"),
MenuItem::new(42, "Paste").with_shortcut("⌘V"),
MenuItem::new(43, "Delete").disabled().separated(),
MenuItem::new(44, "Properties…"),
]),
cx,
);
});
}),
)
.child(div().child("Right-click for a context menu"))
.child(
div()
.id("example-btn-message")
.px_3()
.py_1()
.rounded_md()
.bg(colors.selected)
.text_color(colors.selected_text)
.cursor_pointer()
.on_click(cx.listener(|this, _event: &gpui::ClickEvent, window, cx| {
this.show_message = true;
window.focus(&this.message.read(cx).focus_handle(cx), cx);
}))
.child("Message box"),
)
.child(
div()
.id("example-btn-progress")
.px_3()
.py_1()
.rounded_md()
.bg(colors.selected)
.text_color(colors.selected_text)
.cursor_pointer()
.on_click(cx.listener(|this, _event: &gpui::ClickEvent, window, cx| {
this.show_progress = true;
this.progress_content
.update(cx, |content, cx| content.set_progress(0.4, cx));
window.focus(&this.progress.read(cx).focus_handle(cx), cx);
}))
.child("Progress"),
)
.child(
div()
.id("example-btn-file")
.px_3()
.py_1()
.rounded_md()
.bg(colors.selected)
.text_color(colors.selected_text)
.cursor_pointer()
.on_click(cx.listener(|this, _event: &gpui::ClickEvent, window, cx| {
this.show_file = true;
window.focus(&this.file.read(cx).focus_handle(cx), cx);
}))
.child("Open file…"),
),
)
.child(self.context_menu.clone());
if self.show_message {
root = root.child(self.message.clone());
}
if self.show_progress {
root = root.child(self.progress.clone());
}
if self.show_file {
root = root.child(self.file.clone());
}
root
}
}
fn main() {
gpui_platform::application().run(|cx: &mut App| {
cx.init_colors();
let bounds = Bounds::centered(None, size(px(640.0), px(480.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| cx.new(|cx| Example::new(window, cx)),
)
.expect("Failed to open window");
cx.activate(true);
cx.on_window_closed(|cx, _| {
if cx.windows().is_empty() {
cx.quit();
}
})
.detach();
});
}