facett-menu — mounting menus & a command palette (host guide)
Class:
guide· What it's for: the how-to for a host that wants a menubar, right-click menus, or a Ctrl-K command palette over one action set. The what is indesign.md. The crate is a pure leaf — you own what a command does; it owns the surfaces + selection state.
1. Add the dependency
[dependencies]
facett-menu = { path = "../facett-menu", version = "0.1" }
No network, no nornir, no secrets pulled in.
2. Describe your actions as data
use facett_menu::Command;
let cmds = vec![
Command::new("copy", "Copy", "Edit").shortcut("Ctrl+C"),
Command::new("paste", "Paste", "Edit"),
Command::new("case.new", "New case", "Case").keywords(["create", "investigation"]),
Command::new("case.open","Open case","Case"),
];
id is stable and headless-addressable; group becomes a menubar dropdown / a palette section; keywords are matched by the palette but never shown; disabled commands are dimmed and never matched.
3a. The Elm Facet — MenuView (menubar + palette in one)
Mount it like any facett component; act on the Effect it returns.
use facett_menu::{MenuView, Msg, Effect};
let mut menu = MenuView::new("Commands", cmds);
// each frame, via the Facet::ui bridge (update applied for you) …
// menu.ui(ui); // (from `impl Facet`)
// … or drive it yourself and handle the effect:
for eff in menu.update(Msg::Select("case.new".into())) {
match eff { Effect::Selected(id) => dispatch(&id) }
}
Ctrl-K / Ctrl-Shift-P opens the palette; type to fuzzy-filter, arrows to move (wrapping), Enter to pick, Esc to close. Selecting closes the palette and emits Effect::Selected(id).
3b. The free surfaces (splice anywhere)
If you don't want the whole Facet, the immediate-mode primitives take the same Command slice and return the chosen id:
use facett_menu::{menu_bar, attach_context_menu, CommandPalette};
if let Some(id) = menu_bar(ui, &cmds) { dispatch(&id); } // top panel
let resp = ui.label("right-click me");
attach_context_menu(&resp, &cmds, |id| dispatch(id)); // context menu
let mut palette = CommandPalette::default(); // Ctrl-K overlay
if let Some(id) = palette.ui(ctx, &cmds) { dispatch(&id); }
4. Drive it headless (robot / remote)
Everything is reachable without pixels through the Facet JSON seam or the typed Elm harness:
use facett_core::Facet;
menu.update_json("\"open_palette\"");
menu.update_json("{\"set_query\":\"case\"}");
let hits = menu.state_json()["hits"].clone(); // ["case.new","case.open"] — filtered
menu.update_json("{\"select\":\"case.new\"}"); // records + closes; host acts on it
state_json carries the live hits list so a test asserts the fuzzy filter narrowed. See tests/robot.rs for the full open → filter → select → effect loop, and run the warehouse rows with cargo test -p facett-menu --features testmatrix.
The one rule
Nothing is pointer-only. Give every command a stable id and it is reachable by right-click, by typing, and by Select(id) from code — the same action, three ways.