use crate::editor::EditorState; use crate::menu::{MenuItem, MenuKey}; use crate::mode::PendingMode; use kiln_core::{Archetype, Builtin}; /// 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 opens a /// [dialog](kiln_ui::dialog), pushes a child level (e.g. [`Main`](MenuLevel::Main) /// descends into [`World`](MenuLevel::World) or [`Terrain`](MenuLevel::Terrain)), or /// sets the drawing tools' current archetype (e.g. the [`Terrain`](MenuLevel::Terrain) /// and [`Pushers`](MenuLevel::Pushers) choices, via /// [`set_current`](EditorState::set_current)). #[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, /// Collectible items the player picks up (e.g. gems) Items, } 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", MenuLevel::Items => "Items", } } /// The list of entries for each menu pub(crate) fn entries(self) -> Vec { match self { MenuLevel::Main => vec![ MenuEntry::item('p', "Play board", |ed| ed.playtest()), MenuEntry::Separator, 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)), MenuEntry::item('i', "Items", |ed| ed.menu.push(MenuLevel::Items)), ], 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)), MenuEntry::item('s', "/ CW spinner", |ed| { ed.set_current(Archetype::Builtin(Builtin::Spinner, "spinner_cw")) }), MenuEntry::item('c', "\\ CCW spinner", |ed| { ed.set_current(Archetype::Builtin(Builtin::Spinner, "spinner_ccw")) }), ], MenuLevel::Pushers => vec![ MenuEntry::item('n', "▲ Pusher", |ed| { ed.set_current(Archetype::Builtin(Builtin::Pusher, "pusher_north")) }), MenuEntry::item('s', "▼ Pusher", |ed| { ed.set_current(Archetype::Builtin(Builtin::Pusher, "pusher_south")) }), MenuEntry::item('e', "► Pusher", |ed| { ed.set_current(Archetype::Builtin(Builtin::Pusher, "pusher_east")) }), MenuEntry::item('w', "◄ Pusher", |ed| { ed.set_current(Archetype::Builtin(Builtin::Pusher, "pusher_west")) }), ], MenuLevel::Items => vec![MenuEntry::item('t', "♦ Gem", |ed| { // 't' for 'treasure' — 'g' conflicts with glyph picker ed.set_current(Archetype::Builtin(Builtin::Gem, "gem")) })], } } /// 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, }, /// 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 String>), } impl MenuEntry { pub(crate) fn item( key: char, label: impl Into, action: F, ) -> Self { MenuEntry::Item { key, label: label.into(), action: Box::new(action), } } pub(crate) fn value 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 { 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(); }), ] }