From 1debfaf5f530cb8da0f9eb561db533e7b035a9e8 Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Thu, 11 Jun 2026 21:45:50 -0500 Subject: [PATCH] more refactor --- kiln-tui/src/main.rs | 70 ++++---------------------------------------- kiln-tui/src/ui.rs | 66 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 64 deletions(-) create mode 100644 kiln-tui/src/ui.rs diff --git a/kiln-tui/src/main.rs b/kiln-tui/src/main.rs index 6baf74d..c4c84a4 100644 --- a/kiln-tui/src/main.rs +++ b/kiln-tui/src/main.rs @@ -13,6 +13,7 @@ mod scroll_overlay; mod term; mod utils; mod speech; +mod ui; use kiln_core::game::{GameState, ScrollLine}; use kiln_core::log::LogLine; @@ -33,53 +34,11 @@ use std::io; use std::process::ExitCode; use std::time::{Duration, Instant}; use term::TerminalCaps; -use crate::scroll_overlay::{ScrollAnimState, ScrollOverlayState, ScrollOverlayWidget}; +use crate::scroll_overlay::{ScrollAnimState, ScrollOverlayWidget}; use crate::speech::SpeechBubblesWidget; +use crate::ui::Ui; use crate::utils::logline_to_line; -/// View-only UI state for the log panel and scroll overlay. This is presentation -/// state, so it lives in the front-end rather than on the engine's [`GameState`]. -struct Ui { - /// Whether the bottom log panel is open. - log_open: bool, - /// Height (in terminal rows, borders included) of the log panel when open. - log_height: u16, - /// Vertical scroll offset of the log panel, in lines from the top (newest). - scroll: u16, - /// Scroll position, bounds, and animation state for an active scroll overlay. - overlay: ScrollOverlayState, -} - -impl Default for Ui { - fn default() -> Self { - Self { - log_open: false, - log_height: 10, - scroll: 0, - overlay: ScrollOverlayState::default(), - } - } -} - -impl Ui { - /// Scrolls the overlay by `delta` lines, clamped to the valid range. - fn scroll_lines(&mut self, delta: i32) { - self.overlay.offset = (self.overlay.offset as i32 + delta) - .clamp(0, self.overlay.max_scroll as i32) as u16; - } -} - -/// Starts the scroll-close animation with the optional player choice. -/// Uses the current opening progress if the overlay was still animating in. -fn begin_close(ui: &mut Ui, choice: Option) { - let progress = match &ui.overlay.anim { - Some(ScrollAnimState::Opening(p)) => p.min(1.0), - _ => 1.0, - }; - ui.overlay.pending_choice = choice; - ui.overlay.anim = Some(ScrollAnimState::Closing(progress)); -} - /// Returns the choice string for the given key character, or `None` if it doesn't /// match any choice in the scroll. Choices are assigned letters a, b, c… in order. fn resolve_choice(lines: &[ScrollLine], c: char) -> Option { @@ -197,12 +156,12 @@ fn run( 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 => begin_close(ui, None), + 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) { - begin_close(ui, Some(ch)); + ui.begin_close(Some(ch)); } } _ => {} @@ -263,24 +222,7 @@ fn run( if last_tick.elapsed() >= FRAME { let dt = last_tick.elapsed(); last_tick = Instant::now(); - - // Advance scroll animation regardless of game pause state. - let dt_s = dt.as_secs_f32(); - ui.overlay.advance_anim(dt_s); - if ui.overlay.close_finished() { - // Animation done — dispatch choice and clear the scroll. - game.close_scroll(ui.overlay.take_choice().as_deref()); - ui.overlay = ScrollOverlayState::default(); - } - - // Tick the game only when no scroll overlay is blocking. - if game.active_scroll.is_none() { - game.tick(dt); - // Detect a scroll just opened by the tick; start its open animation. - if game.active_scroll.is_some() && ui.overlay.anim.is_none() { - ui.overlay.anim = Some(ScrollAnimState::Opening(0.0)); - } - } + ui.tick(dt, game); } } } diff --git a/kiln-tui/src/ui.rs b/kiln-tui/src/ui.rs new file mode 100644 index 0000000..467a644 --- /dev/null +++ b/kiln-tui/src/ui.rs @@ -0,0 +1,66 @@ +use std::time::Duration; +use kiln_core::game::GameState; +use crate::scroll_overlay::{ScrollAnimState, ScrollOverlayState}; + +/// View-only UI state for the log panel and scroll overlay. This is presentation +/// state, so it lives in the front-end rather than on the engine's [`GameState`]. +pub(crate) struct Ui { + /// Whether the bottom log panel is open. + pub(crate) log_open: bool, + /// Height (in terminal rows, borders included) of the log panel when open. + pub(crate) log_height: u16, + /// Vertical scroll offset of the log panel, in lines from the top (newest). + pub(crate) scroll: u16, + /// Scroll position, bounds, and animation state for an active scroll overlay. + pub(crate) overlay: ScrollOverlayState, +} + +impl Default for Ui { + fn default() -> Self { + Self { + log_open: false, + log_height: 10, + scroll: 0, + overlay: ScrollOverlayState::default(), + } + } +} + +impl Ui { + /// Scrolls the overlay by `delta` lines, clamped to the valid range. + pub(crate) fn scroll_lines(&mut self, delta: i32) { + self.overlay.offset = (self.overlay.offset as i32 + delta) + .clamp(0, self.overlay.max_scroll as i32) as u16; + } + + /// Advances all time-driven UI state by `dt` and drives the game tick. + /// + /// Advances the scroll overlay animation, dispatches any finished close + /// animation, ticks the game when no overlay is blocking, and starts the + /// open animation when a new scroll appears. + pub(crate) fn tick(&mut self, dt: Duration, game: &mut GameState) { + self.overlay.advance_anim(dt.as_secs_f32()); + if self.overlay.close_finished() { + game.close_scroll(self.overlay.take_choice().as_deref()); + self.overlay = ScrollOverlayState::default(); + } + + if game.active_scroll.is_none() { + game.tick(dt); + if game.active_scroll.is_some() && self.overlay.anim.is_none() { + self.overlay.anim = Some(ScrollAnimState::Opening(0.0)); + } + } + } + + /// Starts the scroll-close animation with the optional player choice. + /// Uses the current opening progress if the overlay was still animating in. + pub(crate) fn begin_close(&mut self, choice: Option) { + let progress = match self.overlay.anim { + Some(ScrollAnimState::Opening(p)) => p.min(1.0), + _ => 1.0, + }; + self.overlay.pending_choice = choice; + self.overlay.anim = Some(ScrollAnimState::Closing(progress)); + } +} \ No newline at end of file