menus
This commit is contained in:
+39
-10
@@ -5,13 +5,14 @@ use kiln_core::Board;
|
||||
use kiln_core::game::GameState;
|
||||
use crate::animation::Animation;
|
||||
use crate::log::LogState;
|
||||
use crate::menu::{MenuItem, MenuCloseAnimation, MenuOpenAnimation, MenuState};
|
||||
use crate::scroll_overlay::{ScrollCloseAnimation, ScrollOpenAnimation, ScrollOverlayState};
|
||||
use crate::transition::TransitionAnimation;
|
||||
|
||||
/// A pair of board `Rc`s (old board, new board) queued for transition pre-rendering.
|
||||
type PendingTransition = Option<(Rc<RefCell<Board>>, Rc<RefCell<Board>>)>;
|
||||
|
||||
/// View-only UI state for the log panel, scroll overlay, and board animations.
|
||||
/// View-only UI state for the log panel, scroll overlay, menu, and board animations.
|
||||
/// Presentation-only state; lives in the front-end, not on [`GameState`].
|
||||
pub(crate) struct Ui {
|
||||
/// State for the log panel (open/closed, height, scroll position).
|
||||
@@ -23,6 +24,11 @@ pub(crate) struct Ui {
|
||||
pub(crate) pending_transition: PendingTransition,
|
||||
/// The currently running animation, if any. All input is suppressed while `Some`.
|
||||
pub(crate) active_animation: Option<Box<dyn Animation>>,
|
||||
/// State for the active menu overlay. `Some` while the menu is open or closing.
|
||||
pub(crate) menu: Option<MenuState>,
|
||||
/// Set by a menu action to signal that the game loop should exit after the
|
||||
/// close animation finishes.
|
||||
pub(crate) should_quit: bool,
|
||||
}
|
||||
|
||||
impl Default for Ui {
|
||||
@@ -32,38 +38,62 @@ impl Default for Ui {
|
||||
overlay: ScrollOverlayState::default(),
|
||||
pending_transition: None,
|
||||
active_animation: None,
|
||||
menu: None,
|
||||
should_quit: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ui {
|
||||
/// Scrolls the overlay by `delta` lines, clamped to the valid range.
|
||||
/// Scrolls the scroll 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;
|
||||
}
|
||||
|
||||
/// Scrolls the menu overlay by `delta` lines, clamped to the valid range.
|
||||
pub(crate) fn scroll_menu(&mut self, delta: i32) {
|
||||
if let Some(m) = self.menu.as_mut() {
|
||||
m.offset = (m.offset as i32 + delta).clamp(0, m.max_scroll as i32) as u16;
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a menu: stores the items as [`MenuState`] and starts the open animation.
|
||||
pub(crate) fn open_menu(&mut self, items: Vec<MenuItem>) {
|
||||
self.active_animation = Some(Box::new(MenuOpenAnimation::new(items.clone())));
|
||||
self.menu = Some(MenuState { items, ..Default::default() });
|
||||
}
|
||||
|
||||
/// Starts the close animation using a snapshot of the current menu items.
|
||||
///
|
||||
/// Called from within a menu item's action closure (e.g. `ui.begin_close_menu()`).
|
||||
pub(crate) fn begin_close_menu(&mut self) {
|
||||
let items = self.menu.as_ref().map(|m| m.items.clone()).unwrap_or_default();
|
||||
self.active_animation = Some(Box::new(MenuCloseAnimation::new(items)));
|
||||
}
|
||||
|
||||
/// Advances all time-driven UI state by `dt` and drives the game tick.
|
||||
///
|
||||
/// While any animation is running, `game.tick` is suppressed. When an
|
||||
/// animation finishes, [`input_mode_after`](Animation::input_mode_after) is
|
||||
/// consulted: returning to [`Board`](crate::input::InputMode::Board) mode
|
||||
/// immediately calls [`handle_scroll`](GameState::handle_scroll) so the
|
||||
/// closed overlay is cleared before the next draw rather than flickering open
|
||||
/// for one extra frame.
|
||||
/// eagerly calls [`handle_scroll`](GameState::handle_scroll) to clear any
|
||||
/// pending scroll, and clears [`menu`](Ui::menu) so the closed overlay is not
|
||||
/// rendered on the next frame.
|
||||
pub(crate) fn tick(&mut self, dt: Duration, game: &mut GameState) {
|
||||
// Advance the active animation; on completion, dispatch any mode-change effect.
|
||||
if let Some(ref mut anim) = self.active_animation {
|
||||
if anim.update(dt.as_secs_f32()) {
|
||||
let mode = anim.input_mode_after();
|
||||
self.active_animation = None;
|
||||
// Returning to Board mode: eagerly close any scroll so it isn't
|
||||
// rendered as open on the next frame before the game tick runs.
|
||||
// Returning to Board mode: eagerly clear scroll and menu state so
|
||||
// neither renders as open for one extra frame before the game tick.
|
||||
if matches!(mode, crate::input::InputMode::Board) {
|
||||
game.handle_scroll();
|
||||
self.menu = None;
|
||||
}
|
||||
}
|
||||
return; // no game tick during any animation (even when animation just ended)
|
||||
return; // no game tick during any animation (even when just ended)
|
||||
}
|
||||
|
||||
// Normal path: tick the game, then check if a scroll appeared.
|
||||
@@ -76,8 +106,7 @@ impl Ui {
|
||||
|
||||
/// Sets the player's scroll choice and starts the close animation.
|
||||
///
|
||||
/// `choice` is `None` when the player dismisses without selecting; it is
|
||||
/// stored on the active scroll and dispatched by the engine on the next tick.
|
||||
/// `choice` is `None` when the player dismisses without selecting.
|
||||
pub(crate) fn begin_close(&mut self, choice: Option<String>, game: &mut GameState) {
|
||||
if let Some(scroll) = game.active_scroll.as_mut() {
|
||||
scroll.choice = choice;
|
||||
|
||||
Reference in New Issue
Block a user