2026-06-17 18:25:09 -05:00
|
|
|
use crate::editor::EditorState;
|
|
|
|
|
use crate::menu::{ActionRc as MenuActionRc, MenuItem, MenuKey, pause_menu_items};
|
2026-06-15 22:02:15 -05:00
|
|
|
use crate::mode::Mode;
|
2026-06-11 22:09:32 -05:00
|
|
|
use crate::ui::Ui;
|
2026-06-17 18:25:09 -05:00
|
|
|
use kiln_core::Direction;
|
|
|
|
|
use kiln_core::game::{GameState, ScrollLine};
|
|
|
|
|
use ratatui::crossterm::event::{Event, KeyCode, MouseButton, MouseEventKind};
|
2026-06-11 22:09:32 -05:00
|
|
|
|
2026-06-13 18:43:29 -05:00
|
|
|
/// Moves the player and, if the move crosses a portal, captures both board Rcs
|
|
|
|
|
/// and stores them in [`Ui::pending_transition`] for the next draw call.
|
|
|
|
|
fn try_move_with_transition(game: &mut GameState, ui: &mut Ui, dir: Direction) {
|
|
|
|
|
let old_rc = game.board_rc();
|
|
|
|
|
let old_name = game.current_board_name().to_string();
|
|
|
|
|
game.try_move(dir);
|
|
|
|
|
if game.current_board_name() != old_name {
|
|
|
|
|
// Portal crossed: queue transition; consume the engine-side signal.
|
|
|
|
|
ui.pending_transition = Some((old_rc, game.board_rc()));
|
|
|
|
|
game.board_transition = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 22:09:32 -05:00
|
|
|
/// Which input mode the front-end is currently in.
|
|
|
|
|
pub enum InputMode {
|
|
|
|
|
/// Normal gameplay: arrow keys move the player, `l` toggles the log.
|
2026-06-13 18:43:29 -05:00
|
|
|
Board,
|
2026-06-11 22:09:32 -05:00
|
|
|
/// A scroll overlay is active; all input is captured for scroll navigation.
|
|
|
|
|
Scroll,
|
2026-06-15 10:42:21 -05:00
|
|
|
/// A menu overlay is active; all input is captured for menu navigation.
|
|
|
|
|
Menu,
|
2026-06-15 22:02:15 -05:00
|
|
|
/// Editing a world: arrow keys move the cursor, the sidebar is shown.
|
|
|
|
|
Editor,
|
2026-06-17 18:25:09 -05:00
|
|
|
/// A dialog overlay is open over the editor; all input drives the dialog.
|
|
|
|
|
Dialog,
|
2026-06-18 11:40:06 -05:00
|
|
|
/// The script code editor is open; all input drives the editor.
|
|
|
|
|
CodeEditor,
|
2026-06-20 15:27:09 -05:00
|
|
|
/// The glyph picker is open over the editor; all input drives the picker.
|
|
|
|
|
GlyphDialog,
|
2026-06-15 11:44:18 -05:00
|
|
|
/// An animation is running and has not claimed any input mode; all input is discarded.
|
|
|
|
|
Ignore,
|
2026-06-15 09:15:30 -05:00
|
|
|
/// Reserved for a post-transition freeze; nothing currently produces this mode.
|
|
|
|
|
#[allow(dead_code)]
|
2026-06-13 18:43:29 -05:00
|
|
|
Frozen,
|
2026-06-11 22:09:32 -05:00
|
|
|
}
|
|
|
|
|
|
2026-06-15 22:02:15 -05:00
|
|
|
/// Determines the current input mode from the active [`Mode`] and UI state.
|
|
|
|
|
pub(crate) fn current_input_mode(mode: &Mode, ui: &Ui) -> InputMode {
|
2026-06-15 11:44:18 -05:00
|
|
|
// An active animation declares its own input mode; pending transitions ignore input.
|
|
|
|
|
if let Some(anim) = &ui.active_animation {
|
|
|
|
|
return anim.input_mode_during();
|
2026-06-13 18:43:29 -05:00
|
|
|
}
|
2026-06-17 18:25:09 -05:00
|
|
|
if ui.pending_transition.is_some() {
|
|
|
|
|
return InputMode::Ignore;
|
|
|
|
|
}
|
|
|
|
|
if ui.menu.is_some() {
|
|
|
|
|
return InputMode::Menu;
|
|
|
|
|
}
|
2026-06-15 22:02:15 -05:00
|
|
|
match mode {
|
2026-06-17 18:25:09 -05:00
|
|
|
Mode::Edit(ed) => {
|
2026-06-20 15:27:09 -05:00
|
|
|
if ed.glyph_dialog.is_some() {
|
|
|
|
|
InputMode::GlyphDialog
|
|
|
|
|
} else if ed.dialog.is_some() {
|
2026-06-17 18:25:09 -05:00
|
|
|
InputMode::Dialog
|
2026-06-18 11:40:06 -05:00
|
|
|
} else if ed.code_editor.is_some() {
|
|
|
|
|
InputMode::CodeEditor
|
2026-06-17 18:25:09 -05:00
|
|
|
} else {
|
|
|
|
|
InputMode::Editor
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-15 22:02:15 -05:00
|
|
|
Mode::Play(game) => {
|
2026-06-17 18:25:09 -05:00
|
|
|
if game.active_scroll.is_some() {
|
|
|
|
|
InputMode::Scroll
|
|
|
|
|
} else {
|
|
|
|
|
InputMode::Board
|
|
|
|
|
}
|
2026-06-15 22:02:15 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-11 22:09:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Handles an input event in [`InputMode::Board`]. Returns `true` if the player quit.
|
2026-06-17 18:25:09 -05:00
|
|
|
pub(crate) fn handle_board_input(
|
|
|
|
|
event: Event,
|
|
|
|
|
log_open: bool,
|
|
|
|
|
terminal_h: u16,
|
|
|
|
|
game: &mut GameState,
|
|
|
|
|
ui: &mut Ui,
|
|
|
|
|
) -> bool {
|
2026-06-11 22:09:32 -05:00
|
|
|
match event {
|
|
|
|
|
Event::Key(key) => match key.code {
|
2026-06-20 19:05:46 -05:00
|
|
|
// During a playtest, Esc returns to the editor instead of opening the menu.
|
|
|
|
|
KeyCode::Esc if ui.suspended_editor.is_some() => ui.exit_playtest = true,
|
2026-06-15 10:42:21 -05:00
|
|
|
KeyCode::Esc => ui.open_menu(pause_menu_items()),
|
2026-06-17 18:25:09 -05:00
|
|
|
KeyCode::Up => try_move_with_transition(game, ui, Direction::North),
|
|
|
|
|
KeyCode::Down => try_move_with_transition(game, ui, Direction::South),
|
|
|
|
|
KeyCode::Left => try_move_with_transition(game, ui, Direction::West),
|
2026-06-13 18:43:29 -05:00
|
|
|
KeyCode::Right => try_move_with_transition(game, ui, Direction::East),
|
2026-06-11 22:09:32 -05:00
|
|
|
KeyCode::Char('l') => ui.log.toggle(),
|
2026-06-17 18:25:09 -05:00
|
|
|
KeyCode::PageUp if log_open => ui.log.scroll_by(-5, game.log.len()),
|
2026-06-11 22:09:32 -05:00
|
|
|
KeyCode::PageDown if log_open => ui.log.scroll_by(5, game.log.len()),
|
|
|
|
|
_ => {}
|
|
|
|
|
},
|
|
|
|
|
Event::Mouse(m) if log_open => match m.kind {
|
2026-06-17 18:25:09 -05:00
|
|
|
MouseEventKind::ScrollUp => ui.log.scroll_by(-1, game.log.len()),
|
2026-06-11 22:09:32 -05:00
|
|
|
MouseEventKind::ScrollDown => ui.log.scroll_by(1, game.log.len()),
|
|
|
|
|
MouseEventKind::Drag(_) => {
|
|
|
|
|
// The divider sits at row `total - height`; dragging up grows the panel.
|
|
|
|
|
let target = terminal_h.saturating_sub(m.row);
|
|
|
|
|
ui.log.resize(target, terminal_h);
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
},
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 22:02:15 -05:00
|
|
|
/// Handles an input event in [`InputMode::Editor`].
|
|
|
|
|
///
|
|
|
|
|
/// Arrow keys move the cursor; `l` toggles the log; `Esc` opens the editor menu.
|
|
|
|
|
/// A right-click jumps the cursor to the clicked board cell, and dragging the
|
|
|
|
|
/// board/sidebar divider resizes the sidebar (the divider sits at `term_w -
|
|
|
|
|
/// sidebar_width`, mirroring the log's bottom divider).
|
2026-06-17 18:25:09 -05:00
|
|
|
pub(crate) fn handle_editor_input(event: Event, term_w: u16, ed: &mut EditorState, ui: &mut Ui) {
|
2026-06-15 22:02:15 -05:00
|
|
|
match event {
|
|
|
|
|
Event::Key(key) => match key.code {
|
2026-06-17 18:25:09 -05:00
|
|
|
KeyCode::Esc => ed.menu_escape(ui),
|
|
|
|
|
KeyCode::Up => ed.move_cursor(0, -1),
|
|
|
|
|
KeyCode::Down => ed.move_cursor(0, 1),
|
|
|
|
|
KeyCode::Left => ed.move_cursor(-1, 0),
|
2026-06-15 22:02:15 -05:00
|
|
|
KeyCode::Right => ed.move_cursor(1, 0),
|
|
|
|
|
KeyCode::Char('l') => ui.log.toggle(),
|
2026-06-20 17:53:47 -05:00
|
|
|
// `g` opens the glyph picker to edit the current drawing glyph.
|
2026-06-20 15:27:09 -05:00
|
|
|
KeyCode::Char('g') => ed.open_glyph_picker(),
|
2026-06-20 17:53:47 -05:00
|
|
|
// Space stamps the current thing; Tab toggles draw mode.
|
|
|
|
|
KeyCode::Char(' ') => ed.place_current(),
|
|
|
|
|
KeyCode::Tab => ed.toggle_draw_mode(),
|
2026-06-17 18:25:09 -05:00
|
|
|
// A menu letter opens its dialog; unmatched letters are ignored.
|
|
|
|
|
KeyCode::Char(c) => {
|
|
|
|
|
ed.menu_key(c);
|
|
|
|
|
}
|
2026-06-15 22:02:15 -05:00
|
|
|
_ => {}
|
|
|
|
|
},
|
|
|
|
|
Event::Mouse(m) => match m.kind {
|
|
|
|
|
// Right-click moves the cursor to the clicked cell (if on the board).
|
|
|
|
|
MouseEventKind::Down(MouseButton::Right) => ed.set_cursor_at_screen(m.column, m.row),
|
|
|
|
|
// Dragging the divider resizes the sidebar.
|
|
|
|
|
MouseEventKind::Drag(_) => ed.resize_sidebar(term_w.saturating_sub(m.column), term_w),
|
|
|
|
|
_ => {}
|
|
|
|
|
},
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 22:17:46 -05:00
|
|
|
/// Returns the choice string for the given key character, scanning the scroll lines.
|
|
|
|
|
fn resolve_choice(lines: &[ScrollLine], c: char) -> Option<String> {
|
|
|
|
|
let mut letter = b'a';
|
|
|
|
|
for line in lines {
|
|
|
|
|
if let ScrollLine::Choice { choice, .. } = line {
|
2026-06-17 18:25:09 -05:00
|
|
|
if c == letter as char {
|
|
|
|
|
return Some(choice.clone());
|
|
|
|
|
}
|
2026-06-11 22:17:46 -05:00
|
|
|
letter += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 22:09:32 -05:00
|
|
|
/// Handles an input event in [`InputMode::Scroll`].
|
|
|
|
|
pub(crate) fn handle_scroll_input(event: Event, game: &mut GameState, ui: &mut Ui) {
|
2026-06-17 18:25:09 -05:00
|
|
|
let lines: Vec<ScrollLine> = game
|
|
|
|
|
.active_scroll
|
|
|
|
|
.as_ref()
|
2026-06-15 09:15:30 -05:00
|
|
|
.map(|s| s.lines.clone())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
let has_choices = lines.iter().any(|l| matches!(l, ScrollLine::Choice { .. }));
|
|
|
|
|
|
2026-06-11 22:09:32 -05:00
|
|
|
match event {
|
2026-06-15 09:15:30 -05:00
|
|
|
Event::Key(key) => match key.code {
|
|
|
|
|
// Esc dismisses only when there are no choices.
|
|
|
|
|
KeyCode::Esc if !has_choices => ui.begin_close(None, game),
|
2026-06-17 18:25:09 -05:00
|
|
|
KeyCode::Up => ui.scroll_lines(-1),
|
2026-06-15 09:15:30 -05:00
|
|
|
KeyCode::Down => ui.scroll_lines(1),
|
|
|
|
|
KeyCode::Char(c) => {
|
|
|
|
|
if let Some(ch) = resolve_choice(&lines, c) {
|
|
|
|
|
ui.begin_close(Some(ch), game);
|
2026-06-11 22:09:32 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-15 09:15:30 -05:00
|
|
|
_ => {}
|
|
|
|
|
},
|
2026-06-11 22:09:32 -05:00
|
|
|
Event::Mouse(m) => match m.kind {
|
2026-06-17 18:25:09 -05:00
|
|
|
MouseEventKind::ScrollUp => ui.scroll_lines(-1),
|
2026-06-11 22:09:32 -05:00
|
|
|
MouseEventKind::ScrollDown => ui.scroll_lines(1),
|
|
|
|
|
_ => {}
|
|
|
|
|
},
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-15 10:42:21 -05:00
|
|
|
|
|
|
|
|
/// Handles an input event in [`InputMode::Menu`].
|
|
|
|
|
///
|
|
|
|
|
/// Uses the clone-then-dispatch pattern: the action `Rc` is cloned to release
|
|
|
|
|
/// the immutable borrow on `ui.menu`, then the action is called with full `&mut Ui`.
|
2026-06-15 22:02:15 -05:00
|
|
|
pub(crate) fn handle_menu_input(event: Event, ui: &mut Ui) {
|
2026-06-15 10:42:21 -05:00
|
|
|
match event {
|
|
|
|
|
Event::Key(key) => {
|
|
|
|
|
let pressed = match key.code {
|
2026-06-17 18:25:09 -05:00
|
|
|
KeyCode::Esc => Some(MenuKey::Esc),
|
|
|
|
|
KeyCode::Char(c) => Some(MenuKey::Char(c)),
|
|
|
|
|
KeyCode::Up => {
|
|
|
|
|
ui.scroll_menu(-1);
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Down => {
|
|
|
|
|
ui.scroll_menu(1);
|
|
|
|
|
None
|
|
|
|
|
}
|
2026-06-15 10:42:21 -05:00
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
if let Some(mk) = pressed {
|
|
|
|
|
// Clone the Rc to release the borrow on ui.menu before calling the action.
|
2026-06-17 18:25:09 -05:00
|
|
|
let action: Option<MenuActionRc> = ui
|
|
|
|
|
.menu
|
|
|
|
|
.as_ref()
|
2026-06-15 10:42:21 -05:00
|
|
|
.and_then(|m| m.items.iter().find(|i| i.key == mk))
|
|
|
|
|
.map(|i| i.action_rc());
|
|
|
|
|
if let Some(action) = action {
|
2026-06-15 22:02:15 -05:00
|
|
|
MenuItem::call_action_rc(action, ui);
|
2026-06-15 10:42:21 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Event::Mouse(m) => match m.kind {
|
2026-06-17 18:25:09 -05:00
|
|
|
MouseEventKind::ScrollUp => ui.scroll_menu(-1),
|
2026-06-15 10:42:21 -05:00
|
|
|
MouseEventKind::ScrollDown => ui.scroll_menu(1),
|
|
|
|
|
_ => {}
|
|
|
|
|
},
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|