T08b menus: menu bar over the native menus, searchable menu, pie menu; megamenu points to the navigation menu; menus follow their hosts

This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 13:43:12 -04:00
1 parent f990586abd
commit d6b982016d
18 files changed
+1516 -261

No files matched your search

+8 -3
View File
@@ -16,8 +16,8 @@ use gpui::{
actions!(gallery, [Quit, About, LightMode, DarkMode, NewWindow]);
/// The gallery's menu bar and Dock menu: NativeMenu and JumpList at work.
fn menus(window: WindowHandle<shell::Gallery>, cx: &mut App) {
/// The gallery's menu bar and Dock menu: NativeMenu and JumpList at work. Set before the window opens, whose first frame may draw them.
fn menus(cx: &mut App) {
cx.set_menus(vec![
Menu {
name: "Ely".into(),
@@ -36,6 +36,10 @@ fn menus(window: WindowHandle<shell::Gallery>, cx: &mut App) {
},
]);
cx.set_dock_menu(vec![MenuItem::action("New Window", NewWindow)]);
}
/// What the menus' actions do; the theme ones need the window.
fn menu_actions(window: WindowHandle<shell::Gallery>, cx: &mut App) {
cx.on_action(|_: &About, cx| pages::open_about(cx));
cx.on_action(|_: &NewWindow, cx| pages::open_managed(cx));
cx.on_action(move |_: &LightMode, cx| choose(window, shell::Choice::Light, cx));
@@ -84,6 +88,7 @@ fn main() -> Result<()> {
ely_gpui_component::init(cx);
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
cx.on_action(|_: &Quit, cx| cx.quit());
menus(cx);
let bounds = Bounds::centered(None, size(px(1280.0), px(820.0)), cx);
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
@@ -100,7 +105,7 @@ fn main() -> Result<()> {
cx.new(|cx| shell::Gallery::new(args.page.unwrap_or(0), window, cx))
})
.expect("gallery window failed to open");
menus(window, cx);
menu_actions(window, cx);
cx.activate(true);
if let Some(dir) = args.capture {
capture::run(window, dir, args.page, cx);
+81
View File
@@ -0,0 +1,81 @@
mod more;
mod rows;
use gpui::{AnyElement, App, IntoElement, ParentElement, Window, div};
use super::Page;
use crate::script::Step;
pub const PAGE: Page = Page {
number: 8,
slug: "menus",
title: "Menus",
summary: "Rows of actions that open from a button, a right click or an arrow, and nest.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::Rest,
Step::Click("file-menu"),
Step::Wait(300),
Step::Key("down"),
Step::Key("down"),
Step::Key("down"),
Step::Key("right"),
Step::Wait(400),
Step::Shot("submenu"),
Step::Key("escape"),
Step::Key("escape"),
Step::Click("view-menu"),
Step::Wait(400),
Step::Shot("checks"),
Step::Key("escape"),
Step::ClickEnd("merge"),
Step::Wait(400),
Step::Shot("split"),
Step::Key("escape"),
Step::Click("more"),
Step::Wait(400),
Step::Shot("overflow"),
Step::Key("escape"),
Step::RightAt("canvas", 180.0, 70.0),
Step::Wait(400),
Step::Shot("context"),
Step::Key("escape"),
Step::DownAt("menubar", 16.0, 12.0),
Step::UpAt("menubar", 16.0, 12.0),
Step::Wait(400),
Step::Shot("menubar"),
Step::Key("right"),
Step::Wait(300),
Step::Shot("menubar-view"),
Step::Key("escape"),
Step::Click("move"),
Step::Wait(400),
Step::Type("re"),
Step::Wait(300),
Step::Shot("searchable"),
Step::Key("escape"),
Step::RightAt("pie-area", 210.0, 130.0),
Step::Wait(600),
Step::Key("right"),
Step::Key("right"),
Step::Wait(300),
Step::Shot("pie"),
Step::Key("escape"),
Step::Rest,
];
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(rows::file_menu(window, cx))
.child(rows::view_menu(window, cx))
.child(rows::hosts(window, cx))
.child(rows::context(window, cx))
.child(more::menu_bar(cx))
.child(more::mega_menu(cx))
.child(more::searchable(window, cx))
.child(more::pie(window, cx))
.into_any_element()
}
+111
View File
@@ -0,0 +1,111 @@
use ely_gpui_component::{
menus::{Menu, MenuBar, MenuItem, PieItem, PieMenu, SearchableMenu},
primitives::IconName,
theme::{ActiveTheme, Radius},
typography::Caption,
};
use gpui::{App, IntoElement, ParentElement, Styled, Window, div, px};
use super::rows::{ran, run};
use crate::{
probe::probe,
ui::{section, specimen},
};
pub fn menu_bar(cx: &mut App) -> impl IntoElement + use<> {
let menus = cx.get_menus().expect("the gallery sets its menus in main");
section(
"MenuBar Menu",
"The app's own menus, the ones cx.set_menus gave the system, drawn in the window. Picking a row dispatches its action.",
cx,
)
.child(specimen(
"hover moves between menus while one is open",
probe("menubar", MenuBar::new("app-bar", menus)),
cx,
))
}
pub fn mega_menu(cx: &App) -> impl IntoElement + use<> {
section(
"MegaMenu",
"A menu of links wide enough for a line of detail each.",
cx,
)
.child(Caption::new(
"It is navigation::NavigationMenu: each entry opens a panel of links. See Navigation, chapter 7.",
))
}
const PLACES: [(&str, IconName); 8] = [
("Inbox", IconName::Inbox),
("Roadmap", IconName::Map),
("Design", IconName::PenTool),
("Engineering", IconName::Code),
("Research", IconName::BookOpen),
("Marketing", IconName::Globe),
("Archive", IconName::Archive),
("Trash", IconName::Trash2),
];
pub fn searchable(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (last, caption) = ran("move-ran", window, cx);
let menu = PLACES.into_iter().fold(Menu::new(), |menu, (name, icon)| {
menu.item(MenuItem::new(name).icon(icon).on_click(run(&last, name)))
});
section(
"SearchableMenu",
"A filter field heads the menu. Typing narrows the rows, best first, and marks the letters that matched.",
cx,
)
.child(specimen(
caption,
probe(
"move",
SearchableMenu::new("move", "Move to", menu)
.icon(IconName::FolderOpen)
.placeholder("Find a place"),
),
cx,
))
}
pub fn pie(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (last, caption) = ran("pie-ran", window, cx);
let items = [
(IconName::Copy, "Copy"),
(IconName::Scissors, "Cut"),
(IconName::Clipboard, "Paste"),
(IconName::Share2, "Share"),
(IconName::Pin, "Pin"),
(IconName::Trash2, "Delete"),
]
.map(|(icon, name)| PieItem::new(icon, name).on_click(run(&last, name)));
let theme = cx.theme();
section(
"PieMenu / RadialMenu",
"A right click opens a ring at the pointer. The pointer's direction marks a slice and a click runs it; arrows turn the ring, the hub cancels.",
cx,
)
.child(specimen(
caption,
probe(
"pie-area",
PieMenu::new("pie", items).child(
div()
.flex()
.items_center()
.justify_center()
.w(px(420.0))
.h(px(260.0))
.border_1()
.border_dashed()
.border_color(theme.colors.border_strong)
.rounded(theme.radius(Radius::Lg))
.text_color(theme.colors.fg_subtle)
.child("Right-click for the ring"),
),
),
cx,
))
}
@@ -4,59 +4,15 @@ use ely_gpui_component::{
primitives::IconName,
theme::{ActiveTheme, Radius},
};
use gpui::{
AnyElement, App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px,
};
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
use super::Page;
use crate::{
probe::probe,
script::Step,
ui::{keep, row, section, set, specimen},
};
pub const PAGE: Page = Page {
number: 8,
slug: "menus",
title: "Menus",
summary: "Rows of actions that open from a button, a right click or an arrow, and nest.",
render,
script: SCRIPT,
};
const SCRIPT: &[Step] = &[
Step::Rest,
Step::Click("file-menu"),
Step::Wait(300),
Step::Key("down"),
Step::Key("down"),
Step::Key("down"),
Step::Key("right"),
Step::Wait(400),
Step::Shot("submenu"),
Step::Key("escape"),
Step::Key("escape"),
Step::Click("view-menu"),
Step::Wait(400),
Step::Shot("checks"),
Step::Key("escape"),
Step::ClickEnd("merge"),
Step::Wait(400),
Step::Shot("split"),
Step::Key("escape"),
Step::Click("more"),
Step::Wait(400),
Step::Shot("overflow"),
Step::Key("escape"),
Step::RightAt("canvas", 180.0, 70.0),
Step::Wait(400),
Step::Shot("context"),
Step::Key("escape"),
Step::Rest,
];
/// Records the last row run, for the captions.
fn ran(
pub(super) fn ran(
key: &'static str,
window: &mut Window,
cx: &mut App,
@@ -69,7 +25,7 @@ fn ran(
(last, caption)
}
fn run(
pub(super) fn run(
last: &Entity<Option<SharedString>>,
name: &'static str,
) -> impl Fn(&mut Window, &mut App) + 'static {
@@ -77,7 +33,7 @@ fn run(
move |_, cx| set(&last, Some(name.into()), cx)
}
fn file_menu(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
pub fn file_menu(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (last, caption) = ran("file-ran", window, cx);
let recent = Menu::new()
.item(
@@ -142,7 +98,7 @@ fn file_menu(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
))
}
fn view_menu(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
pub fn view_menu(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let shown = keep("view-shown", || [true, false, true], window, cx);
let density = keep("view-density", || 1usize, window, cx);
let (now, dense) = (*shown.read(cx), *density.read(cx));
@@ -192,7 +148,7 @@ fn view_menu(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
))
}
fn hosts(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
pub fn hosts(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (last, caption) = ran("hosts-ran", window, cx);
let merge = Menu::new()
.item(MenuItem::new("Squash and merge").on_click(run(&last, "squash and merge")))
@@ -252,7 +208,7 @@ fn hosts(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
))
}
fn context(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
pub fn context(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let (last, caption) = ran("context-ran", window, cx);
let menu = Menu::new()
.item(
@@ -311,12 +267,3 @@ fn context(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
cx,
))
}
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
div()
.child(file_menu(window, cx))
.child(view_menu(window, cx))
.child(hosts(window, cx))
.child(context(window, cx))
.into_any_element()
}