Add player-centered viewport scrolling for variable map sizes
Window size is now fixed (840×524) and independent of map dimensions. Small maps center in the viewport; maps larger than the viewport keep the player centered and clamp at board edges so no empty space shows. Also derives Copy+Clone on Player to enable the per-frame origin calculation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -190,6 +190,7 @@ pub struct PortalDef {
|
||||
/// the player will eventually become a scripted object that responds to
|
||||
/// input events, at which point this struct may be removed or made optional.
|
||||
/// See `ARCHITECTURE.md` for details.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Player {
|
||||
/// Column position (0-indexed, increasing rightward).
|
||||
pub x: i32,
|
||||
|
||||
+48
-18
@@ -13,10 +13,17 @@ const CELL_H: f32 = 20.0;
|
||||
/// Approximate height of the egui menu bar in pixels, used for window sizing.
|
||||
const MENU_H: f32 = 24.0;
|
||||
/// Default inner margin that egui's CentralPanel adds on all sides, in pixels.
|
||||
/// The minimum window size accounts for this so the board fits without clipping.
|
||||
const PANEL_MARGIN: f32 = 8.0;
|
||||
/// Width of the editor palette side panel in pixels.
|
||||
const PALETTE_PANEL_W: f32 = 120.0;
|
||||
/// Default window width in pixels. Independent of map size.
|
||||
const DEFAULT_WINDOW_W: f32 = 840.0;
|
||||
/// Default window height in pixels. Independent of map size.
|
||||
const DEFAULT_WINDOW_H: f32 = 524.0;
|
||||
/// Minimum window width: enough room for 10 cells plus margins.
|
||||
const MIN_WINDOW_W: f32 = CELL_W * 10.0 + 2.0 * PANEL_MARGIN;
|
||||
/// Minimum window height: enough room for 5 cells plus menu and margins.
|
||||
const MIN_WINDOW_H: f32 = CELL_H * 5.0 + MENU_H + 2.0 * PANEL_MARGIN;
|
||||
|
||||
/// Whether the application is in play mode or edit mode.
|
||||
///
|
||||
@@ -44,18 +51,12 @@ impl EditorState {
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result<()> {
|
||||
// Load the board before creating the window so its dimensions can drive
|
||||
// the initial and minimum window size.
|
||||
let board = map_file::load("maps/start.toml").expect("failed to load maps/start.toml");
|
||||
let board_px_w = board.width as f32 * CELL_W + 2.0 * PANEL_MARGIN;
|
||||
let board_px_h = board.height as f32 * CELL_H + MENU_H + 2.0 * PANEL_MARGIN;
|
||||
|
||||
// Window min-size is sized for Play mode. In Edit mode the palette panel
|
||||
// steals PALETTE_PANEL_W pixels; the board may clip at minimum window size.
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([board_px_w, board_px_h])
|
||||
.with_min_inner_size([board_px_w, board_px_h]),
|
||||
.with_inner_size([DEFAULT_WINDOW_W, DEFAULT_WINDOW_H])
|
||||
.with_min_inner_size([MIN_WINDOW_W, MIN_WINDOW_H]),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
@@ -138,15 +139,12 @@ impl eframe::App for App {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
let available = ui.available_rect_before_wrap();
|
||||
|
||||
// Center the board within the panel. If the window is at minimum
|
||||
// size the offset is zero; if the window is larger the board floats
|
||||
// in the middle.
|
||||
let board_px = vec2(
|
||||
self.state.board.width as f32 * CELL_W,
|
||||
self.state.board.height as f32 * CELL_H,
|
||||
);
|
||||
let offset = ((available.size() - board_px) / 2.0).max(egui::Vec2::ZERO);
|
||||
let origin = available.min + offset;
|
||||
// Extract board dimensions and player position before the immutable
|
||||
// borrow scope so `board_origin` can use them without conflicting
|
||||
// with the later mutable borrow for click-to-paint.
|
||||
let (board_w, board_h) = (self.state.board.width, self.state.board.height);
|
||||
let player = self.state.board.player;
|
||||
let origin = board_origin(available, board_w, board_h, player);
|
||||
|
||||
// Draw board and player. Immutable borrow of `board` ends after
|
||||
// this block, freeing `self.state.board` for mutation below.
|
||||
@@ -222,3 +220,35 @@ fn draw_glyph(painter: &egui::Painter, origin: Pos2, x: usize, y: usize, glyph:
|
||||
painter.rect_filled(rect, 0.0, glyph.bg);
|
||||
painter.text(center, Align2::CENTER_CENTER, glyph.ch, FontId::monospace(CELL_H), glyph.fg);
|
||||
}
|
||||
|
||||
/// Computes the pixel position of board cell (0, 0) within `available`.
|
||||
///
|
||||
/// If the board fits along an axis, it is centered. If it overflows, the
|
||||
/// viewport is centered on the player and clamped so no empty space appears
|
||||
/// at the board edges.
|
||||
fn board_origin(available: Rect, board_w: usize, board_h: usize, player: game::Player) -> Pos2 {
|
||||
let board_px_w = board_w as f32 * CELL_W;
|
||||
let board_px_h = board_h as f32 * CELL_H;
|
||||
|
||||
let origin_x = if board_px_w <= available.width() {
|
||||
// Board fits: center it horizontally.
|
||||
available.min.x + (available.width() - board_px_w) / 2.0
|
||||
} else {
|
||||
// Board overflows: keep player at viewport center, clamp to board edges.
|
||||
let player_px_x = player.x as f32 * CELL_W + CELL_W / 2.0;
|
||||
(available.center().x - player_px_x)
|
||||
.max(available.max.x - board_px_w) // right edge must reach viewport right
|
||||
.min(available.min.x) // left edge must reach viewport left
|
||||
};
|
||||
|
||||
let origin_y = if board_px_h <= available.height() {
|
||||
available.min.y + (available.height() - board_px_h) / 2.0
|
||||
} else {
|
||||
let player_px_y = player.y as f32 * CELL_H + CELL_H / 2.0;
|
||||
(available.center().y - player_px_y)
|
||||
.max(available.max.y - board_px_h)
|
||||
.min(available.min.y)
|
||||
};
|
||||
|
||||
Pos2::new(origin_x, origin_y)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user