map editor scroll
This commit is contained in:
@@ -300,7 +300,7 @@ impl EditorState {
|
|||||||
/// board (right-click handling). No-op when the click is off the board. When draw
|
/// 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.
|
/// 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) {
|
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 {
|
if let Some((bx, by)) = cell {
|
||||||
let target = (bx as i32, by as i32);
|
let target = (bx as i32, by as i32);
|
||||||
let moved = target != self.cursor;
|
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);
|
let inner = block.inner(board_area);
|
||||||
frame.render_widget(block, 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).
|
// Overlay the cursor during its visible phase (a light-gray solid block).
|
||||||
if ed.blink_on {
|
if ed.blink_on {
|
||||||
let (cx, cy) = (ed.cursor.0 as usize, ed.cursor.1 as usize);
|
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))
|
&& let Some(cell) = frame.buffer_mut().cell_mut((sx, sy))
|
||||||
{
|
{
|
||||||
cell.set_char(CURSOR_CHAR)
|
cell.set_char(CURSOR_CHAR)
|
||||||
|
|||||||
+44
-18
@@ -13,16 +13,27 @@ use ratatui::layout::Rect;
|
|||||||
use ratatui::widgets::Widget;
|
use ratatui::widgets::Widget;
|
||||||
|
|
||||||
/// A ratatui widget that draws a [`Board`] (with objects and the player) into a
|
/// 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> {
|
pub struct BoardWidget<'a> {
|
||||||
/// The board to render. Borrowed for the duration of the draw.
|
/// The board to render. Borrowed for the duration of the draw.
|
||||||
pub board: &'a Board,
|
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> {
|
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 {
|
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.
|
/// 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
|
/// Maps a board cell `(bx, by)` to its screen position within `area`, using the
|
||||||
/// same centering/scrolling layout as [`BoardWidget`]. Returns `None` when the
|
/// same centering/scrolling layout as [`BoardWidget`] with the given `focus`.
|
||||||
/// cell is scrolled out of view.
|
/// 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
|
/// The inverse of [`screen_to_board`]; both reuse [`BoardWidget::axis`] so they
|
||||||
/// stay consistent with how the board is actually drawn. (Unlike
|
/// stay consistent with how the board is actually drawn. (Unlike
|
||||||
/// `speech::board_screen_pos`, which clamps off-screen cells to the edge, this
|
/// `speech::board_screen_pos`, which clamps off-screen cells to the edge, this
|
||||||
/// reports off-screen cells as `None` — needed for exact cursor placement.)
|
/// 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)> {
|
pub fn board_to_screen(
|
||||||
let (off_x, pad_x, cols) = BoardWidget::axis(board.width, area.width as usize, board.player.x);
|
area: Rect,
|
||||||
let (off_y, pad_y, rows) =
|
board: &Board,
|
||||||
BoardWidget::axis(board.height, area.height as usize, board.player.y);
|
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.
|
// 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 {
|
if bx < off_x || bx >= off_x + cols || by < off_y || by >= off_y + rows {
|
||||||
return None;
|
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`,
|
/// 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 inverse of [`board_to_screen`]. `focus` must match the focus used when
|
||||||
/// the drawn board (in the centering pad or past the visible cells).
|
/// the board was rendered so the coordinate mapping is consistent.
|
||||||
pub fn screen_to_board(area: Rect, board: &Board, sx: u16, sy: u16) -> Option<(usize, usize)> {
|
/// Returns `None` when the point lies outside the drawn board (in the centering
|
||||||
let (off_x, pad_x, cols) = BoardWidget::axis(board.width, area.width as usize, board.player.x);
|
/// pad or past the visible cells).
|
||||||
let (off_y, pad_y, rows) =
|
pub fn screen_to_board(
|
||||||
BoardWidget::axis(board.height, area.height as usize, board.player.y);
|
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.
|
// 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 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);
|
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) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let board = self.board;
|
let board = self.board;
|
||||||
// Resolve each axis independently: a small board is centered, a large
|
// Resolve each axis independently: a small board is centered, a large
|
||||||
// one scrolls to keep the player on screen.
|
// one scrolls to keep the focus cell on screen.
|
||||||
let (off_x, pad_x, cols) = Self::axis(board.width, area.width as usize, board.player.x);
|
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, board.player.y);
|
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.
|
// Walk the visible board cells, placing each after the centering pad.
|
||||||
for row in 0..rows {
|
for row in 0..rows {
|
||||||
|
|||||||
Reference in New Issue
Block a user