use eframe::egui; use egui::{Align2, FontId, Pos2, Rect, vec2}; use crate::game::{Board, Glyph, Player}; /// Width of one board cell in pixels. pub(crate) const CELL_W: f32 = 14.0; /// Height of one board cell in pixels. pub(crate) const CELL_H: f32 = 20.0; /// Approximate height of the egui menu bar in pixels, used for window sizing. pub(crate) const MENU_H: f32 = 24.0; /// Default inner margin that egui's CentralPanel adds on all sides, in pixels. pub(crate) const PANEL_MARGIN: f32 = 8.0; /// Default width of the editor side panel in pixels. The panel is resizable. pub(crate) const PALETTE_PANEL_W: f32 = 200.0; /// Default window width in pixels. Independent of map size. pub(crate) const DEFAULT_WINDOW_W: f32 = 840.0; /// Default window height in pixels. Independent of map size. pub(crate) const DEFAULT_WINDOW_H: f32 = 524.0; /// Minimum window width: enough room for 10 cells plus margins. pub(crate) 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. pub(crate) const MIN_WINDOW_H: f32 = CELL_H * 5.0 + MENU_H + 2.0 * PANEL_MARGIN; /// Draws a single cell at grid position `(x, y)` using the given glyph. /// /// Each cell is rendered as two layers: /// 1. A filled rectangle in `glyph.bg` covering the full cell area. /// 2. A monospace character (`glyph.ch`) in `glyph.fg`, centered in the cell. /// /// `origin` is the pixel position of cell `(0, 0)` — the top-left corner of /// the board within the panel. Cell positions are computed from `origin` using /// [`CELL_W`] and [`CELL_H`]. pub(crate) fn draw_glyph(painter: &egui::Painter, origin: Pos2, x: usize, y: usize, glyph: &Glyph) { let tl = origin + vec2(x as f32 * CELL_W, y as f32 * CELL_H); let rect = Rect::from_min_size(tl, vec2(CELL_W, CELL_H)); let center = rect.center(); painter.rect_filled(rect, 0.0, glyph.bg); painter.text(center, Align2::CENTER_CENTER, glyph.ch, FontId::monospace(CELL_H), glyph.fg); } /// Draws all board cells then the player overlay at `origin`. pub(crate) fn draw_board(painter: &egui::Painter, origin: Pos2, board: &Board) { for y in 0..board.height { for x in 0..board.width { let (glyph, _) = board.get(x, y); draw_glyph(painter, origin, x, y, glyph); } } // The player is not a board cell; it is rendered on top as an overlay. // This separate draw will be removed once the player becomes a scripted object. let player_glyph = Glyph::player(); draw_glyph(painter, origin, board.player.x as usize, board.player.y as usize, &player_glyph); } /// Converts a pixel position to cell coordinates relative to `origin`. /// /// Uses floor so positions above or left of `origin` produce negative values — /// callers must guard `>= 0` before treating the result as a valid cell index. pub(crate) fn pos_to_cell(origin: Pos2, pos: Pos2) -> (i32, i32) { let rel = pos - origin; ((rel.x / CELL_W).floor() as i32, (rel.y / CELL_H).floor() as i32) } /// 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. pub(crate) fn board_origin(available: Rect, board_w: usize, board_h: usize, player: 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) }