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
+62 -33
View File
@@ -10,19 +10,31 @@
mod animation;
mod cp437;
mod editor;
mod input;
mod log;
mod menu;
mod mode;
mod overlay;
mod render;
mod scroll_overlay;
mod speech;
mod term;
mod transition;
mod utils;
mod speech;
mod ui;
mod input;
mod utils;
use crate::animation::{AnimWidget, AnimationLayer};
use crate::editor::{EditorState, draw_editor, handle_dialog_input};
use crate::input::{
InputMode, current_input_mode, handle_board_input, handle_editor_input, handle_menu_input,
handle_scroll_input,
};
use crate::log::{LogWidget, log_preview_line};
use crate::menu::MenuWidget;
use crate::mode::{Mode, PendingMode};
use crate::scroll_overlay::ScrollOverlayWidget;
use crate::speech::SpeechBubblesWidget;
use crate::ui::Ui;
use kiln_core::game::GameState;
use kiln_core::log::LogLine;
use kiln_core::world;
@@ -39,18 +51,6 @@ use std::io;
use std::process::ExitCode;
use std::time::{Duration, Instant};
use term::TerminalCaps;
use crate::animation::{AnimationLayer, AnimWidget};
use crate::editor::{draw_editor, EditorState};
use crate::input::{
current_input_mode, handle_board_input, handle_editor_input, handle_menu_input,
handle_scroll_input, InputMode,
};
use crate::log::{log_preview_line, LogWidget};
use crate::menu::MenuWidget;
use crate::mode::{Mode, PendingMode};
use crate::scroll_overlay::ScrollOverlayWidget;
use crate::speech::SpeechBubblesWidget;
use crate::ui::Ui;
/// Entry point: parse the map path, load the board, then run the play loop with
/// the terminal in raw/alternate-screen mode (restored on every exit path).
@@ -96,7 +96,10 @@ fn main() -> ExitCode {
let mut mode = Mode::Play(game);
// Remember the world path so the editor (and play-reload) can reload from it.
let mut ui = Ui { world_path: path.clone(), ..Ui::default() };
let mut ui = Ui {
world_path: path.clone(),
..Ui::default()
};
let result = run(&mut terminal, &mut mode, &mut ui);
if caps.keyboard_enhancement {
@@ -121,11 +124,7 @@ const FRAME: Duration = Duration::from_nanos(1_000_000_000 / 30);
/// only until the next frame deadline (`event::poll`), then advances the game by
/// the real time elapsed since the last tick. `poll` returns the instant a key
/// arrives, so input stays responsive while the game keeps ticking on its own.
fn run(
terminal: &mut ratatui::DefaultTerminal,
mode: &mut Mode,
ui: &mut Ui,
) -> io::Result<()> {
fn run(terminal: &mut ratatui::DefaultTerminal, mode: &mut Mode, ui: &mut Ui) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
// Redraw every iteration; ratatui diffs against the previous frame so
@@ -158,14 +157,18 @@ fn run(
handle_board_input(event, ui.log.open, size.height, game, ui);
}
InputMode::Scroll => handle_scroll_input(event, game, ui),
_ => unreachable!("Menu and Ignore handled above; Editor input mode only occurs in Edit mode"),
_ => unreachable!(
"Menu and Ignore handled above; Editor input mode only occurs in Edit mode"
),
},
Mode::Edit(ed) => match input_mode {
InputMode::Editor => handle_editor_input(event, size.width, ed, ui),
_ => unreachable!("Menu and Ignore handled above; Board/Scroll/Frozen input modes only occur in Play mode")
InputMode::Dialog => handle_dialog_input(event, ed),
_ => unreachable!(
"Menu and Ignore handled above; Board/Scroll/Frozen input modes only occur in Play mode"
),
},
}
}
}
@@ -194,7 +197,9 @@ fn run(
/// [`GameState`] (so it reflects any edits) and re-runs object `init()` hooks. A
/// load failure is logged to the play game (if any) and leaves the mode unchanged.
fn apply_pending_mode(mode: &mut Mode, ui: &mut Ui) {
let Some(pending) = ui.pending_mode.take() else { return };
let Some(pending) = ui.pending_mode.take() else {
return;
};
match world::load(&ui.world_path) {
Ok(world) => match pending {
PendingMode::EnterEditor => *mode = Mode::Edit(EditorState::new(world)),
@@ -225,19 +230,38 @@ fn draw(frame: &mut Frame, mode: &mut Mode, ui: &mut Ui) {
}
// Overlay layer: animation > menu > scroll (mutually exclusive at runtime).
let is_overlay_anim = ui.active_animation.as_ref()
let is_overlay_anim = ui
.active_animation
.as_ref()
.map(|a| matches!(a.layer(), AnimationLayer::Overlay))
.unwrap_or(false);
let area = frame.area();
if is_overlay_anim {
frame.render_widget(AnimWidget(ui.active_animation.as_ref().unwrap().as_ref()), area);
frame.render_widget(
AnimWidget(ui.active_animation.as_ref().unwrap().as_ref()),
area,
);
} else if let Some(menu) = ui.menu.as_mut() {
frame.render_stateful_widget(MenuWidget, area, menu);
} else if let Mode::Play(game) = mode {
// The scroll window only exists while playing.
if let Some(scroll) = &game.active_scroll {
frame.render_stateful_widget(ScrollOverlayWidget::new(&scroll.lines), area, &mut ui.overlay);
} else {
match mode {
// The scroll window only exists while playing.
Mode::Play(game) => {
if let Some(scroll) = &game.active_scroll {
frame.render_stateful_widget(
ScrollOverlayWidget::new(&scroll.lines),
area,
&mut ui.overlay,
);
}
}
// The dialog overlay only exists while editing.
Mode::Edit(ed) => {
if let Some(d) = &ed.dialog {
d.draw(frame);
}
}
}
}
}
@@ -284,13 +308,18 @@ fn draw_board_area(frame: &mut Frame, inner: Rect, game: &GameState, ui: &mut Ui
ui.start_transition(&old_rc.borrow(), &new_rc.borrow(), inner);
}
let is_board_anim = ui.active_animation.as_ref()
let is_board_anim = ui
.active_animation
.as_ref()
.map(|a| matches!(a.layer(), AnimationLayer::Board))
.unwrap_or(false);
if is_board_anim {
// Board-layer animation (wipe): replaces the board entirely.
frame.render_widget(AnimWidget(ui.active_animation.as_ref().unwrap().as_ref()), inner);
frame.render_widget(
AnimWidget(ui.active_animation.as_ref().unwrap().as_ref()),
inner,
);
} else {
let board = &game.board();
frame.render_widget(BoardWidget::new(board), inner);