// Pure geometry of the on-screen board: where the centered 8×8 grid sits inside // a frame, and the two-way mapping between cells and screen points. Owns no // rendering and pulls in NO GL/stb imports, so the touch→cell mapping is // unit-testable headless. BoardView composes this for layout + hit-testing, and // P5's swap input reuses `point_to_cell` to resolve a tap to a swap endpoint. #import "modules/std.sx"; #import "modules/math"; #import "modules/ui/types.sx"; #import "board.sx"; BoardLayout :: struct { cell_size: f32; origin: Point; // Center a square 8×8 grid inside the safe-area-inset region of `frame`. compute :: (self: *BoardLayout, frame: Frame, safe: EdgeInsets) { avail := frame.inset(safe); cols : f32 = xx BOARD_COLS; board_dim := min(avail.size.width, avail.size.height); self.cell_size = board_dim / cols; total := self.cell_size * cols; self.origin = Point.{ x = avail.origin.x + (avail.size.width - total) * 0.5, y = avail.origin.y + (avail.size.height - total) * 0.5 }; } cell_frame :: (self: *BoardLayout, col: i64, row: i64) -> Frame { Frame.make( self.origin.x + xx col * self.cell_size, self.origin.y + xx row * self.cell_size, self.cell_size, self.cell_size ) } // Inverse of `cell_frame`: map a view-local point to the grid cell under it, // or null when the point falls outside the 8×8 grid. The `< 0.0` guards run // BEFORE the truncating cast, since casting a small negative float rounds // toward zero into a valid index. Uses the SAME origin / cell_size `compute` // produced, so a tap resolves to exactly the cell drawn under the finger. point_to_cell :: (self: *BoardLayout, p: Point) -> ?Cell { if self.cell_size <= 0.0 { return null; } fx := (p.x - self.origin.x) / self.cell_size; fy := (p.y - self.origin.y) / self.cell_size; if fx < 0.0 or fy < 0.0 { return null; } col : i64 = xx fx; row : i64 = xx fy; if col >= BOARD_COLS or row >= BOARD_ROWS { return null; } Cell.{ col = col, row = row } } // Frame of the whole 8×8 grid (origin + cols/rows × cell_size). The banner // and its dimming overlay are sized off this so they cover exactly the board. grid_frame :: (self: *BoardLayout) -> Frame { Frame.make( self.origin.x, self.origin.y, self.cell_size * cast(f32) BOARD_COLS, self.cell_size * cast(f32) BOARD_ROWS ) } // Win/lose banner geometry (P7.2): an overlay panel centered over the board // grid, with the title band and the restart button inside it. Derived purely // from the SAME grid layout the gems use, so the restart hit-test in // BoardView.handle_event lands on exactly the button BoardView draws. The // headless banner_layout test locks the button-rect ↔ hit-test round-trip. banner :: (self: *BoardLayout) -> BannerLayout { grid := self.grid_frame(); cx := grid.mid_x(); cy := grid.mid_y(); panel_w := grid.size.width * 0.84; panel_h := grid.size.height * 0.44; panel := Frame.make(cx - panel_w * 0.5, cy - panel_h * 0.5, panel_w, panel_h); title := Frame.make(panel.origin.x, panel.origin.y + panel_h * 0.18, panel_w, panel_h * 0.30); btn_w := panel_w * 0.60; btn_h := panel_h * 0.24; btn_y := panel.origin.y + panel_h - btn_h - panel_h * 0.16; button := Frame.make(cx - btn_w * 0.5, btn_y, btn_w, btn_h); BannerLayout.{ panel = panel, title = title, button = button } } } // Resolved rectangles of the win/lose banner: the centered `panel`, the `title` // band where the win/lose headline is centered, and the restart `button` rect // (also the hit-test target). All in the same view-local space as BoardLayout. BannerLayout :: struct { panel: Frame; title: Frame; button: Frame; }