From 8637c0a52ad6be246fe0d4b3a3dd3b21ce515208 Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Tue, 23 Jun 2026 21:59:40 -0500 Subject: [PATCH] map editor scroll --- kiln-tui/src/editor.rs | 6 ++-- kiln-tui/src/render.rs | 62 ++++++++++++++++++++++++++++++------------ 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/kiln-tui/src/editor.rs b/kiln-tui/src/editor.rs index 42edb39..67ab485 100644 --- a/kiln-tui/src/editor.rs +++ b/kiln-tui/src/editor.rs @@ -300,7 +300,7 @@ impl EditorState { /// board (right-click handling). No-op when the click is off the board. When draw /// mode is on and the cursor lands on a new cell, stamps the current thing there. pub(crate) fn set_cursor_at_screen(&mut self, sx: u16, sy: u16) { - let cell = screen_to_board(self.last_board_area, &self.board(), sx, sy); + let cell = screen_to_board(self.last_board_area, &self.board(), sx, sy, self.cursor); if let Some((bx, by)) = cell { let target = (bx as i32, by as i32); let moved = target != self.cursor; @@ -417,12 +417,12 @@ pub(crate) fn draw_editor(frame: &mut Frame, ed: &mut EditorState, ui: &mut Ui) } let inner = block.inner(board_area); frame.render_widget(block, board_area); - frame.render_widget(BoardWidget::new(&board), inner); + frame.render_widget(BoardWidget::new(&board).with_focus(ed.cursor.0, ed.cursor.1), inner); // Overlay the cursor during its visible phase (a light-gray solid block). if ed.blink_on { let (cx, cy) = (ed.cursor.0 as usize, ed.cursor.1 as usize); - if let Some((sx, sy)) = board_to_screen(inner, &board, cx, cy) + if let Some((sx, sy)) = board_to_screen(inner, &board, cx, cy, (ed.cursor.0, ed.cursor.1)) && let Some(cell) = frame.buffer_mut().cell_mut((sx, sy)) { cell.set_char(CURSOR_CHAR) diff --git a/kiln-tui/src/render.rs b/kiln-tui/src/render.rs index 59b7225..9fd6ac6 100644 --- a/kiln-tui/src/render.rs +++ b/kiln-tui/src/render.rs @@ -13,16 +13,27 @@ use ratatui::layout::Rect; use ratatui::widgets::Widget; /// A ratatui widget that draws a [`Board`] (with objects and the player) into a -/// rectangular area, scrolled so the player stays visible on larger boards. +/// rectangular area, scrolled so a chosen focus cell stays visible on larger boards. pub struct BoardWidget<'a> { /// The board to render. Borrowed for the duration of the draw. pub board: &'a Board, + /// The board cell (x, y) the viewport scrolls to keep visible. Defaults to + /// the player position so play mode requires no extra setup. + focus: (i32, i32), } impl<'a> BoardWidget<'a> { - /// Creates a widget that renders `board`. + /// Creates a widget that renders `board`, scrolling to follow the player. pub fn new(board: &'a Board) -> Self { - Self { board } + let focus = (board.player.x, board.player.y); + Self { board, focus } + } + + /// Overrides the scroll focus to `(x, y)` — use in the editor to follow the + /// cursor instead of the player. + pub fn with_focus(mut self, x: i32, y: i32) -> Self { + self.focus = (x, y); + self } /// Lays out one axis of the board within a viewport `view` cells long. @@ -51,17 +62,25 @@ impl<'a> BoardWidget<'a> { } /// Maps a board cell `(bx, by)` to its screen position within `area`, using the -/// same centering/scrolling layout as [`BoardWidget`]. Returns `None` when the -/// cell is scrolled out of view. +/// same centering/scrolling layout as [`BoardWidget`] with the given `focus`. +/// Returns `None` when the cell is scrolled out of view. +/// +/// Pass `(board.player.x, board.player.y)` as `focus` in play mode, or the +/// editor cursor coordinates in editor mode, to match the active viewport. /// /// The inverse of [`screen_to_board`]; both reuse [`BoardWidget::axis`] so they /// stay consistent with how the board is actually drawn. (Unlike /// `speech::board_screen_pos`, which clamps off-screen cells to the edge, this /// reports off-screen cells as `None` — needed for exact cursor placement.) -pub fn board_to_screen(area: Rect, board: &Board, bx: usize, by: usize) -> Option<(u16, u16)> { - let (off_x, pad_x, cols) = BoardWidget::axis(board.width, area.width as usize, board.player.x); - let (off_y, pad_y, rows) = - BoardWidget::axis(board.height, area.height as usize, board.player.y); +pub fn board_to_screen( + area: Rect, + board: &Board, + bx: usize, + by: usize, + focus: (i32, i32), +) -> Option<(u16, u16)> { + let (off_x, pad_x, cols) = BoardWidget::axis(board.width, area.width as usize, focus.0); + let (off_y, pad_y, rows) = BoardWidget::axis(board.height, area.height as usize, focus.1); // Cell must fall within the visible window on both axes. if bx < off_x || bx >= off_x + cols || by < off_y || by >= off_y + rows { return None; @@ -72,12 +91,19 @@ pub fn board_to_screen(area: Rect, board: &Board, bx: usize, by: usize) -> Optio } /// Maps a screen position `(sx, sy)` to the board cell drawn there within `area`, -/// the inverse of [`board_to_screen`]. Returns `None` when the point lies outside -/// the drawn board (in the centering pad or past the visible cells). -pub fn screen_to_board(area: Rect, board: &Board, sx: u16, sy: u16) -> Option<(usize, usize)> { - let (off_x, pad_x, cols) = BoardWidget::axis(board.width, area.width as usize, board.player.x); - let (off_y, pad_y, rows) = - BoardWidget::axis(board.height, area.height as usize, board.player.y); +/// the inverse of [`board_to_screen`]. `focus` must match the focus used when +/// the board was rendered so the coordinate mapping is consistent. +/// Returns `None` when the point lies outside the drawn board (in the centering +/// pad or past the visible cells). +pub fn screen_to_board( + area: Rect, + board: &Board, + sx: u16, + sy: u16, + focus: (i32, i32), +) -> Option<(usize, usize)> { + let (off_x, pad_x, cols) = BoardWidget::axis(board.width, area.width as usize, focus.0); + let (off_y, pad_y, rows) = BoardWidget::axis(board.height, area.height as usize, focus.1); // Screen-space offset from the first drawn cell; reject anything before it. let col = (sx as i32) - (area.x as i32 + pad_x as i32); let row = (sy as i32) - (area.y as i32 + pad_y as i32); @@ -118,9 +144,9 @@ impl Widget for BoardWidget<'_> { fn render(self, area: Rect, buf: &mut Buffer) { let board = self.board; // Resolve each axis independently: a small board is centered, a large - // one scrolls to keep the player on screen. - let (off_x, pad_x, cols) = Self::axis(board.width, area.width as usize, board.player.x); - let (off_y, pad_y, rows) = Self::axis(board.height, area.height as usize, board.player.y); + // one scrolls to keep the focus cell on screen. + let (off_x, pad_x, cols) = Self::axis(board.width, area.width as usize, self.focus.0); + let (off_y, pad_y, rows) = Self::axis(board.height, area.height as usize, self.focus.1); // Walk the visible board cells, placing each after the centering pad. for row in 0..rows {