input
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
use ratatui::crossterm::event::{Event, KeyCode, MouseEventKind};
|
||||||
|
use kiln_core::Direction;
|
||||||
|
use kiln_core::game::{GameState, ScrollLine};
|
||||||
|
use crate::resolve_choice;
|
||||||
|
use crate::scroll_overlay::ScrollAnimState;
|
||||||
|
use crate::ui::Ui;
|
||||||
|
|
||||||
|
/// 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(bool),
|
||||||
|
/// A scroll overlay is active; all input is captured for scroll navigation.
|
||||||
|
Scroll,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the current input mode from game and UI state.
|
||||||
|
pub(crate) fn current_input_mode(game: &GameState, ui: &Ui) -> InputMode {
|
||||||
|
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(ui.log.open) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 => game.try_move(Direction::North),
|
||||||
|
KeyCode::Down => game.try_move(Direction::South),
|
||||||
|
KeyCode::Left => game.try_move(Direction::West),
|
||||||
|
KeyCode::Right => game.try_move(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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-69
@@ -15,6 +15,7 @@ mod term;
|
|||||||
mod utils;
|
mod utils;
|
||||||
mod speech;
|
mod speech;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
mod input;
|
||||||
|
|
||||||
use kiln_core::game::{GameState, ScrollLine};
|
use kiln_core::game::{GameState, ScrollLine};
|
||||||
use kiln_core::log::LogLine;
|
use kiln_core::log::LogLine;
|
||||||
@@ -33,6 +34,7 @@ use std::io;
|
|||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use term::TerminalCaps;
|
use term::TerminalCaps;
|
||||||
|
use crate::input::{current_input_mode, handle_board_input, handle_scroll_input, InputMode};
|
||||||
use crate::log::{log_preview_line, LogWidget};
|
use crate::log::{log_preview_line, LogWidget};
|
||||||
use crate::scroll_overlay::{ScrollAnimState, ScrollOverlayWidget};
|
use crate::scroll_overlay::{ScrollAnimState, ScrollOverlayWidget};
|
||||||
use crate::speech::SpeechBubblesWidget;
|
use crate::speech::SpeechBubblesWidget;
|
||||||
@@ -138,77 +140,20 @@ fn run(
|
|||||||
// tick even with no keypresses.
|
// tick even with no keypresses.
|
||||||
let timeout = FRAME.saturating_sub(last_tick.elapsed());
|
let timeout = FRAME.saturating_sub(last_tick.elapsed());
|
||||||
if event::poll(timeout)? {
|
if event::poll(timeout)? {
|
||||||
match event::read()? {
|
let event = event::read()?;
|
||||||
// Act on Press and Repeat so holding a key moves the player
|
// Ignore key-release events (the Kitty protocol emits these; acting on
|
||||||
// continuously (the Kitty protocol emits Repeat events while a key
|
// them would fire a second move per keypress).
|
||||||
// is held). Ignore Release, which that protocol also emits and
|
if matches!(&event, Event::Key(k) if k.kind == KeyEventKind::Release) {
|
||||||
// which would otherwise fire a second, spurious move per keypress.
|
continue;
|
||||||
Event::Key(key) if key.kind != KeyEventKind::Release => {
|
}
|
||||||
// When a scroll overlay is active (and not already animating
|
let terminal_h = terminal.size()?.height;
|
||||||
// closed), intercept all keys for scroll navigation/choices.
|
match current_input_mode(game, ui) {
|
||||||
let scroll_active = game.active_scroll.is_some()
|
InputMode::Board(log_open) => {
|
||||||
&& !matches!(ui.overlay.anim, Some(ScrollAnimState::Closing(_) | ScrollAnimState::Done));
|
if handle_board_input(event, log_open, terminal_h, game, ui) {
|
||||||
if scroll_active {
|
return Ok(());
|
||||||
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 the scroll only when there are no choices;
|
|
||||||
// with choices present the player must select one.
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
|
||||||
KeyCode::Up => game.try_move(Direction::North),
|
|
||||||
KeyCode::Down => game.try_move(Direction::South),
|
|
||||||
KeyCode::Left => game.try_move(Direction::West),
|
|
||||||
// Right arrow moves right; 'l' is the log panel.
|
|
||||||
KeyCode::Right => game.try_move(Direction::East),
|
|
||||||
// 'l' toggles the log panel; reset scroll to newest.
|
|
||||||
KeyCode::Char('l') => ui.log.toggle(),
|
|
||||||
// PageUp/PageDown scroll the log when it's open.
|
|
||||||
KeyCode::PageUp if ui.log.open => ui.log.scroll_by(-5, game.log.len()),
|
|
||||||
KeyCode::PageDown if ui.log.open => ui.log.scroll_by(5, game.log.len()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Mouse: scroll overlay captures wheel events when active; otherwise
|
InputMode::Scroll => handle_scroll_input(event, game, ui),
|
||||||
// wheel scrolls the log and drag resizes it.
|
|
||||||
Event::Mouse(m) => {
|
|
||||||
let scroll_active = game.active_scroll.is_some()
|
|
||||||
&& !matches!(ui.overlay.anim, Some(ScrollAnimState::Closing(_) | ScrollAnimState::Done));
|
|
||||||
if scroll_active {
|
|
||||||
match m.kind {
|
|
||||||
MouseEventKind::ScrollUp => ui.scroll_lines(-1),
|
|
||||||
MouseEventKind::ScrollDown => ui.scroll_lines(1),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
} else if ui.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
|
|
||||||
// it up (smaller row) grows the panel. Keep both usable.
|
|
||||||
let total = terminal.size()?.height;
|
|
||||||
let target = total.saturating_sub(m.row);
|
|
||||||
ui.log.resize(target, total);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user