Files
kiln/kiln-tui/src/input.rs
T
2026-06-13 18:43:29 -05:00

116 lines
4.9 KiB
Rust

use ratatui::crossterm::event::{Event, KeyCode, MouseEventKind};
use kiln_core::Direction;
use kiln_core::game::{GameState, ScrollLine};
use crate::scroll_overlay::ScrollAnimState;
use crate::ui::Ui;
/// 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;
}
}
/// Which input mode the front-end is currently in.
pub enum InputMode {
/// Normal gameplay: arrow keys move the player, `l` toggles the log.
/// The `bool` is whether the log panel is open (affects PageUp/Down and mouse).
Board,
/// A scroll overlay is active; all input is captured for scroll navigation.
Scroll,
/// A board-wipe animation is running; all events are silently discarded.
Transition,
/// The wipe just finished; game is frozen until the player presses any key.
Frozen,
}
/// Determines the current input mode from game and UI state.
pub(crate) fn current_input_mode(game: &GameState, ui: &Ui) -> InputMode {
// Transition takes highest priority (pending or animating).
if ui.transition.is_some() || ui.pending_transition.is_some() {
return InputMode::Transition;
}
// Post-wipe freeze: show the new board but don't tick or move until a key is pressed.
if ui.post_transition_wait {
return InputMode::Frozen;
}
let scroll_active = game.active_scroll.is_some()
&& !matches!(ui.overlay.anim, Some(ScrollAnimState::Closing(_) | ScrollAnimState::Done));
if scroll_active { InputMode::Scroll } else { InputMode::Board }
}
/// Handles an input event in [`InputMode::Board`]. Returns `true` if the player quit.
pub(crate) fn handle_board_input(event: Event, log_open: bool, terminal_h: u16, game: &mut GameState, ui: &mut Ui) -> bool {
match event {
Event::Key(key) => match key.code {
KeyCode::Char('q') | KeyCode::Esc => return true,
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),
KeyCode::Right => try_move_with_transition(game, ui, Direction::East),
KeyCode::Char('l') => ui.log.toggle(),
KeyCode::PageUp if log_open => ui.log.scroll_by(-5, game.log.len()),
KeyCode::PageDown if log_open => ui.log.scroll_by(5, game.log.len()),
_ => {}
},
Event::Mouse(m) if log_open => match m.kind {
MouseEventKind::ScrollUp => ui.log.scroll_by(-1, game.log.len()),
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
}
/// 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 {
if c == letter as char { return Some(choice.clone()); }
letter += 1;
}
}
None
}
/// Handles an input event in [`InputMode::Scroll`].
pub(crate) fn handle_scroll_input(event: Event, game: &mut GameState, ui: &mut Ui) {
match event {
Event::Key(key) => {
let scroll = game.active_scroll.as_ref().unwrap();
let has_choices = scroll.lines.iter().any(|l| matches!(l, ScrollLine::Choice { .. }));
match key.code {
// Esc dismisses only when there are no choices; with choices the player must select.
KeyCode::Esc if !has_choices => ui.begin_close(None),
KeyCode::Up => ui.scroll_lines(-1),
KeyCode::Down => ui.scroll_lines(1),
KeyCode::Char(c) => {
if let Some(ch) = resolve_choice(&scroll.lines, c) {
ui.begin_close(Some(ch));
}
}
_ => {}
}
}
Event::Mouse(m) => match m.kind {
MouseEventKind::ScrollUp => ui.scroll_lines(-1),
MouseEventKind::ScrollDown => ui.scroll_lines(1),
_ => {}
},
_ => {}
}
}