facett-menu/examples/menu_demo.rs
//! **menu_demo** — the facett overlay menu system, live. One [`Command`] set
//! drives all three surfaces:
//!
//! - a **menu bar** (File / Edit / View / Case dropdowns) across the top,
//! - a **right-click context menu** on the central content (a small graph facet),
//! - a **command palette** opened with **Ctrl-K** (or Ctrl-Shift-P): fuzzy-filter,
//! arrow keys to move (wrapping), Enter to run, Esc to close — the selected row
//! gets the theme accent + glow.
//!
//! A theme switcher proves every surface is palette-driven, and the last fired
//! command id is shown so you can see right-click / palette / menu-bar all land
//! the same action. The central content is a real [`facett_core::Facet`], so the
//! `$FACETT_TRAIL` harness path stays exercised.
//!
//! Run: `cargo run -p facett-menu --example menu_demo`
use eframe::egui;
use facett_core::{Facet, FacetCaps, Scene, Theme, draw, hash_color, set_theme};
use facett_menu::{Command, CommandPalette, attach_context_menu, menu_bar};
/// The uniform command registry — the same list feeds the bar, the context menu,
/// and the palette. `cut` is disabled to show dimming / unmatchability.
fn commands() -> Vec<Command> {
vec![
Command::new("file.new", "New file", "File").shortcut("Ctrl+N"),
Command::new("file.open", "Open…", "File").shortcut("Ctrl+O"),
Command::new("file.save", "Save", "File").shortcut("Ctrl+S"),
Command::new("copy", "Copy", "Edit").shortcut("Ctrl+C").keywords(["clipboard"]),
Command::new("cut", "Cut", "Edit").shortcut("Ctrl+X").enabled(false),
Command::new("paste", "Paste", "Edit").shortcut("Ctrl+V").keywords(["clipboard"]),
Command::new("view.zoom_in", "Zoom in", "View").shortcut("Ctrl+="),
Command::new("view.zoom_out", "Zoom out", "View").shortcut("Ctrl+-"),
Command::new("view.reset", "Reset view", "View"),
Command::new("case.new", "New case", "Case").keywords(["create", "investigation"]),
Command::new("case.open", "Open case", "Case"),
Command::new("case.close", "Close case", "Case"),
]
}
/// A tiny graph facet so the central content is a real `Facet` (harness-testable).
struct GraphFacet {
scene: Scene,
}
impl Default for GraphFacet {
fn default() -> Self {
let mut scene = Scene::new();
let a = scene.node("Nordkapp", hash_color("Nordkapp"));
let b = scene.node("Port", hash_color("Port"));
let c = scene.node("AIS gap", hash_color("AIS gap"));
scene.edge(a, b);
scene.edge(a, c);
Self { scene }
}
}
impl Facet for GraphFacet {
fn title(&self) -> &str {
"menu-demo"
}
fn ui(&mut self, ui: &mut egui::Ui) {
draw(ui, &self.scene, facett_core::Layout::Force, "empty");
}
fn state_json(&self) -> serde_json::Value {
serde_json::json!({ "nodes": self.scene.nodes.len(), "edges": self.scene.edges.len() })
}
fn caps(&self) -> FacetCaps {
FacetCaps::NONE.themeable().selectable()
}
}
struct Demo {
theme_idx: usize,
palette: CommandPalette,
content: GraphFacet,
last_fired: Option<String>,
}
impl Default for Demo {
fn default() -> Self {
Self { theme_idx: 1, palette: CommandPalette::default(), content: GraphFacet::default(), last_fired: None }
}
}
impl eframe::App for Demo {
fn ui(&mut self, root: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = root.ctx().clone();
set_theme(&ctx, Theme::ALL[self.theme_idx]());
let cmds = commands();
egui::Panel::top("menubar").show(root, |ui| {
// Theme switcher.
ui.horizontal_wrapped(|ui| {
ui.label("Theme:");
for (i, ctor) in Theme::ALL.iter().enumerate() {
ui.selectable_value(&mut self.theme_idx, i, ctor().name);
}
});
// The dropdown menu bar (File / Edit / View / Case).
if let Some(id) = menu_bar(ui, &cmds) {
self.last_fired = Some(id);
}
ui.label("Ctrl-K (or Ctrl-Shift-P) opens the command palette · right-click the graph for a context menu");
ui.label(format!("Last fired: {}", self.last_fired.as_deref().unwrap_or("—")));
});
egui::CentralPanel::default().show(root, |ui| {
let resp = ui.interact(ui.max_rect(), ui.id().with("content"), egui::Sense::click());
// Right-click context menu, built from the same Command set.
attach_context_menu(&resp, &cmds, |id| {
self.last_fired = Some(id.to_string());
});
self.content.ui(ui);
});
// Draw the palette overlay LAST so it lands on top of everything.
if let Some(id) = self.palette.ui(&ctx, &cmds) {
self.last_fired = Some(id);
}
}
}
fn main() -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size(egui::vec2(720.0, 560.0)),
..Default::default()
};
eframe::run_native("facett · menu_demo", options, Box::new(|_cc| Ok(Box::<Demo>::default())))
}