editor ui

This commit is contained in:
2026-06-17 18:25:09 -05:00
parent 6464e47747
commit fb8f6df501
11 changed files with 982 additions and 135 deletions
+68 -35
View File
@@ -1,10 +1,10 @@
use ratatui::crossterm::event::{Event, KeyCode, MouseButton, MouseEventKind};
use kiln_core::Direction;
use kiln_core::game::{GameState, ScrollLine};
use crate::editor::{editor_menu_items, EditorState};
use crate::menu::{MenuItem, MenuKey, pause_menu_items, ActionRc as MenuActionRc};
use crate::editor::EditorState;
use crate::menu::{ActionRc as MenuActionRc, MenuItem, MenuKey, pause_menu_items};
use crate::mode::Mode;
use crate::ui::Ui;
use kiln_core::Direction;
use kiln_core::game::{GameState, ScrollLine};
use ratatui::crossterm::event::{Event, KeyCode, MouseButton, MouseEventKind};
/// 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.
@@ -29,6 +29,8 @@ pub enum InputMode {
Menu,
/// Editing a world: arrow keys move the cursor, the sidebar is shown.
Editor,
/// A dialog overlay is open over the editor; all input drives the dialog.
Dialog,
/// An animation is running and has not claimed any input mode; all input is discarded.
Ignore,
/// Reserved for a post-transition freeze; nothing currently produces this mode.
@@ -42,32 +44,52 @@ pub(crate) fn current_input_mode(mode: &Mode, ui: &Ui) -> InputMode {
if let Some(anim) = &ui.active_animation {
return anim.input_mode_during();
}
if ui.pending_transition.is_some() { return InputMode::Ignore; }
if ui.menu.is_some() { return InputMode::Menu; }
if ui.pending_transition.is_some() {
return InputMode::Ignore;
}
if ui.menu.is_some() {
return InputMode::Menu;
}
match mode {
Mode::Edit(_) => InputMode::Editor,
Mode::Edit(ed) => {
if ed.dialog.is_some() {
InputMode::Dialog
} else {
InputMode::Editor
}
}
Mode::Play(game) => {
if game.active_scroll.is_some() { InputMode::Scroll } else { InputMode::Board }
if game.active_scroll.is_some() {
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 {
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::Esc => ui.open_menu(pause_menu_items()),
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::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::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::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.
@@ -87,20 +109,19 @@ pub(crate) fn handle_board_input(event: Event, log_open: bool, terminal_h: u16,
/// 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).
pub(crate) fn handle_editor_input(
event: Event,
term_w: u16,
ed: &mut EditorState,
ui: &mut Ui,
) {
pub(crate) fn handle_editor_input(event: Event, term_w: u16, ed: &mut EditorState, ui: &mut Ui) {
match event {
Event::Key(key) => match key.code {
KeyCode::Esc => ui.open_menu(editor_menu_items()),
KeyCode::Up => ed.move_cursor(0, -1),
KeyCode::Down => ed.move_cursor(0, 1),
KeyCode::Left => ed.move_cursor(-1, 0),
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),
KeyCode::Right => ed.move_cursor(1, 0),
KeyCode::Char('l') => ui.log.toggle(),
// A menu letter opens its dialog; unmatched letters are ignored.
KeyCode::Char(c) => {
ed.menu_key(c);
}
_ => {}
},
Event::Mouse(m) => match m.kind {
@@ -119,7 +140,9 @@ 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()); }
if c == letter as char {
return Some(choice.clone());
}
letter += 1;
}
}
@@ -128,7 +151,9 @@ fn resolve_choice(lines: &[ScrollLine], c: char) -> Option<String> {
/// Handles an input event in [`InputMode::Scroll`].
pub(crate) fn handle_scroll_input(event: Event, game: &mut GameState, ui: &mut Ui) {
let lines: Vec<ScrollLine> = game.active_scroll.as_ref()
let lines: Vec<ScrollLine> = game
.active_scroll
.as_ref()
.map(|s| s.lines.clone())
.unwrap_or_default();
let has_choices = lines.iter().any(|l| matches!(l, ScrollLine::Choice { .. }));
@@ -137,7 +162,7 @@ pub(crate) fn handle_scroll_input(event: Event, game: &mut GameState, ui: &mut U
Event::Key(key) => match key.code {
// Esc dismisses only when there are no choices.
KeyCode::Esc if !has_choices => ui.begin_close(None, game),
KeyCode::Up => ui.scroll_lines(-1),
KeyCode::Up => ui.scroll_lines(-1),
KeyCode::Down => ui.scroll_lines(1),
KeyCode::Char(c) => {
if let Some(ch) = resolve_choice(&lines, c) {
@@ -147,7 +172,7 @@ pub(crate) fn handle_scroll_input(event: Event, game: &mut GameState, ui: &mut U
_ => {}
},
Event::Mouse(m) => match m.kind {
MouseEventKind::ScrollUp => ui.scroll_lines(-1),
MouseEventKind::ScrollUp => ui.scroll_lines(-1),
MouseEventKind::ScrollDown => ui.scroll_lines(1),
_ => {}
},
@@ -163,15 +188,23 @@ pub(crate) fn handle_menu_input(event: Event, ui: &mut Ui) {
match event {
Event::Key(key) => {
let pressed = match key.code {
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 }
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
}
_ => None,
};
if let Some(mk) = pressed {
// Clone the Rc to release the borrow on ui.menu before calling the action.
let action: Option<MenuActionRc> = ui.menu.as_ref()
let action: Option<MenuActionRc> = ui
.menu
.as_ref()
.and_then(|m| m.items.iter().find(|i| i.key == mk))
.map(|i| i.action_rc());
if let Some(action) = action {
@@ -180,7 +213,7 @@ pub(crate) fn handle_menu_input(event: Event, ui: &mut Ui) {
}
}
Event::Mouse(m) => match m.kind {
MouseEventKind::ScrollUp => ui.scroll_menu(-1),
MouseEventKind::ScrollUp => ui.scroll_menu(-1),
MouseEventKind::ScrollDown => ui.scroll_menu(1),
_ => {}
},