Files
kiln/kiln-tui/src/editor_menu.rs
T
2026-06-20 18:36:28 -05:00

149 lines
6.3 KiB
Rust

use kiln_core::{Archetype, Direction};
use crate::editor::EditorState;
use crate::menu::{MenuItem, MenuKey};
use crate::mode::PendingMode;
/// One level in the editor's sidebar menu tree.
///
/// The menu is a stack of levels (the current one is the stack's last element); a
/// breadcrumb is built from their [`title`](MenuLevel::title)s. A choice either opens
/// a [dialog](kiln_ui::dialog) or pushes a child level — e.g. [`Main`](MenuLevel::Main)
/// descends into [`World`](MenuLevel::World) or [`Floor`](MenuLevel::Floor).
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum MenuLevel {
/// The top-level main menu: switch boards, view scripts, more later
Main,
/// Menu of world-related concerns: set name / entry board
World,
/// Menu of floor options
Floor,
/// Normal things that go on a board: terrain
Terrain,
/// Things that do defined actions: machines
Machines,
/// Various directions of pushers
Pushers,
}
impl MenuLevel {
/// The breadcrumb segment shown for this level.
pub(crate) fn title(self) -> &'static str {
match self {
MenuLevel::Main => "Main",
MenuLevel::World => "World",
MenuLevel::Floor => "Floor",
MenuLevel::Terrain => "Terrain",
MenuLevel::Machines => "Machines",
MenuLevel::Pushers => "Pushers",
}
}
/// The list of entries for each menu
pub(crate) fn entries(self) -> Vec<MenuEntry> {
match self {
MenuLevel::Main => vec![
MenuEntry::item('w', "World...", |ed| ed.menu.push(MenuLevel::World)),
MenuEntry::item('s', "Scripts...", |ed| ed.open_scripts_dialog()),
MenuEntry::item('b', "Boards...", |ed| ed.open_boards_dialog()),
MenuEntry::Separator,
MenuEntry::item('f', "Floor", |ed| ed.menu.push(MenuLevel::Floor)),
MenuEntry::item('t', "Terrain", |ed| ed.menu.push(MenuLevel::Terrain)),
MenuEntry::item('m', "Machines", |ed| ed.menu.push(MenuLevel::Machines)),
],
MenuLevel::World => vec![
MenuEntry::item('n', "Name", |ed| ed.open_name_dialog()),
MenuEntry::value(|ed| ed.world.name.clone()),
MenuEntry::item('e', "Entry board...", |ed| ed.open_entry_dialog()),
MenuEntry::value(|ed| ed.world.start.clone()),
],
MenuLevel::Floor => vec![
MenuEntry::item('g', "Grass", |_| {}),
MenuEntry::item('d', "Dirt", |_| {}),
MenuEntry::item('s', "Stone", |_| {}),
MenuEntry::item('c', "Custom glyph [TODO]", |_| {}),
],
MenuLevel::Terrain => vec![
MenuEntry::item('w', "Wall", |ed| ed.set_current(Archetype::Wall)),
MenuEntry::item('c', "■ Crate", |ed| ed.set_current(Archetype::Crate)),
MenuEntry::item('v', "↕ Crate", |ed| ed.set_current(Archetype::VCrate)),
MenuEntry::item('h', "↔ Crate", |ed| ed.set_current(Archetype::HCrate)),
],
MenuLevel::Machines => vec![
MenuEntry::item('p', "Pushers...", |ed| ed.menu.push(MenuLevel::Pushers)),
],
MenuLevel::Pushers => vec![
MenuEntry::item('n', "▲ Pusher", |ed| ed.set_current(Archetype::Pusher(Direction::North))),
MenuEntry::item('s', "▼ Pusher", |ed| ed.set_current(Archetype::Pusher(Direction::South))),
MenuEntry::item('e', "► Pusher", |ed| ed.set_current(Archetype::Pusher(Direction::East))),
MenuEntry::item('w', "◄ Pusher", |ed| ed.set_current(Archetype::Pusher(Direction::West))),
],
}
}
/// Dispatches a letter key against this menu level: finds the selectable
/// item bound to `ch` (spacers are skipped) and runs its action with full `&mut`
/// access to the editor session. Unmatched keys are ignored.
pub(crate) fn perform_key(self, ch: char, editor: &mut EditorState) {
let action = self.entries().into_iter().find_map(|entry| match entry {
MenuEntry::Item{ key, action, .. } if key == ch => Some(action),
_ => None,
});
if let Some(action) = action {
action(editor);
}
}
}
/// One line in a sidebar menu level: a selectable [`Item`](MenuEntry::Item), a
/// [`CurrentValue`](MenuEntry::CurrentValue) readout, or a visual
/// [`Blank`](MenuEntry::Blank)/[`Separator`](MenuEntry::Separator) spacer.
pub(crate) enum MenuEntry {
/// A selectable, key-activated item.
Item{
/// The letter the user presses to activate this item.
key: char,
/// Display label shown after the `[key]` in the sidebar.
label: String,
/// Run when the user presses `key`; mutates the editor session.
action: Box<dyn FnOnce(&mut EditorState)>,
},
/// A blank spacer line. Part of the menu API; no current level uses one yet.
#[allow(dead_code)]
Blank,
/// A horizontal rule spanning the sidebar width.
Separator,
/// The current value of some field
CurrentValue(Box<dyn FnOnce(& EditorState) -> String>)
}
impl MenuEntry {
pub(crate) fn item<F: FnOnce(&mut EditorState) + 'static>(key: char, label: impl Into<String>, action: F) -> Self {
MenuEntry::Item { key, label: label.into(), action: Box::new(action) }
}
pub(crate) fn value<F: FnOnce(& EditorState) -> String + 'static>(action: F) -> Self {
MenuEntry::CurrentValue(Box::new(action))
}
}
/// Returns the items for the editor's pause menu (opened with `Esc` while editing).
///
/// `[p]` reloads the world and returns to play; `[q]` quits; `Esc` resumes editing.
/// Like all menu actions these take only `&mut Ui` — the mode swap is deferred to
/// the run loop via [`Ui::pending_mode`](crate::ui::Ui::pending_mode).
pub(crate) fn editor_menu_items() -> Vec<MenuItem> {
vec![
MenuItem::new(MenuKey::Char('p'), "Play world", |ui| {
ui.pending_mode = Some(PendingMode::PlayWorld);
ui.begin_close_menu();
}),
MenuItem::new(MenuKey::Char('q'), "Quit game", |ui| {
ui.should_quit = true;
ui.begin_close_menu();
}),
MenuItem::new(MenuKey::Esc, "Resume editing", |ui| {
ui.begin_close_menu();
}),
]
}