P4.4: selection highlight + score/moves HUD (sx, iOS sim)

Tap a gem to select it: BoardView hit-tests the touch to a grid cell and
draws a bright rim + translucent fill over it; tapping the same cell clears
the selection, tapping another moves it, tapping off-board clears it.
Selection only — no swap (that's P5). The HUD renders the live score and
remaining moves (out of the move limit) in the Lato font on a translucent
card above the grid.

The touch→cell geometry is factored into a pure BoardLayout (no GL/stb
imports) that BoardView composes and P5 will reuse for swap endpoints.
tests/hit_test.sx locks point_to_cell as the exact inverse of cell_frame
(every cell center round-trips; off-board taps reject) — headless because
BoardLayout pulls no C imports. goldens/p4_hud.png captures the scene after
a real idb tap at (201,437)pt: the HUD plus a yellow selection rim on the
red gem at cell (col 4, row 3).
This commit is contained in:
swipelab
2026-06-05 00:00:48 +03:00
parent 3cd1ef1585
commit 9ed98c73d2
7 changed files with 237 additions and 25 deletions

52
board_layout.sx Normal file
View File

@@ -0,0 +1,52 @@
// 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: s64, row: s64) -> 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 : s64 = xx fx;
row : s64 = xx fy;
if col >= BOARD_COLS or row >= BOARD_ROWS { return null; }
Cell.{ col = col, row = row }
}
}