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:
127
board_view.sx
127
board_view.sx
@@ -14,12 +14,28 @@
|
||||
#import "modules/ui/render.sx";
|
||||
#import "modules/ui/events.sx";
|
||||
#import "modules/ui/view.sx";
|
||||
#import "modules/ui/font.sx";
|
||||
#import "board.sx";
|
||||
#import "board_layout.sx";
|
||||
|
||||
// Fraction of a cell each gem occupies; the remainder is margin so a gem sits
|
||||
// inside its cell tile rather than touching the tile's edges.
|
||||
GEM_FILL_FRAC :f32: 0.84;
|
||||
|
||||
// Selection overlay: a translucent warm fill plus a bright opaque rim around the
|
||||
// chosen cell. `add_stroked_rect` draws the rim in its FILL color (the renderer
|
||||
// ignores the separate stroke color), so SELECT_RIM is passed as the fill.
|
||||
SELECT_FILL :: Color.{ r = 255, g = 240, b = 120, a = 70 };
|
||||
SELECT_RIM :: Color.{ r = 255, g = 228, b = 60, a = 255 };
|
||||
|
||||
// HUD: a translucent card with the score and remaining moves, in the loaded Lato
|
||||
// font. Placed in the empty band above the centered grid (inside the safe area).
|
||||
HUD_FONT :f32: 34.0;
|
||||
HUD_PAD :f32: 14.0;
|
||||
HUD_LINE_GAP :f32: 6.0;
|
||||
HUD_TEXT :: Color.{ r = 255, g = 255, b = 255, a = 255 };
|
||||
HUD_PANEL :: Color.{ r = 12, g = 14, b = 22, a = 185 };
|
||||
|
||||
// UV sub-rect of one gem column, spanning the sheet's full height.
|
||||
GemUV :: struct {
|
||||
uv_min: Point;
|
||||
@@ -93,34 +109,47 @@ load_texture :: (path: [:0]u8, gpu: ?GPU) -> u32 {
|
||||
return tex;
|
||||
}
|
||||
|
||||
// Which board cell the player has currently selected, if any. Lives behind a
|
||||
// pointer (heap-allocated in main) because BoardView is a value rebuilt every
|
||||
// frame from `build_ui`, so the view itself cannot carry state across frames.
|
||||
// Selection only — P5 turns a selected→adjacent tap into a swap.
|
||||
BoardSelection :: struct {
|
||||
active: bool;
|
||||
cell: Cell;
|
||||
|
||||
init :: (self: *BoardSelection) {
|
||||
self.active = false;
|
||||
self.cell = Cell.{ col = 0, row = 0 };
|
||||
}
|
||||
|
||||
clear :: (self: *BoardSelection) {
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
// Tapping a cell selects it; tapping the cell already selected clears the
|
||||
// selection, so a tap toggles its own cell and moves it to any other.
|
||||
toggle :: (self: *BoardSelection, c: Cell) {
|
||||
if self.active and self.cell.col == c.col and self.cell.row == c.row {
|
||||
self.active = false;
|
||||
} else {
|
||||
self.active = true;
|
||||
self.cell = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BoardView :: struct {
|
||||
board: *Board;
|
||||
assets: *BoardAssets;
|
||||
sel: *BoardSelection;
|
||||
safe: EdgeInsets;
|
||||
|
||||
cell_size: f32;
|
||||
origin: Point;
|
||||
// Where the grid sits + the touch↔cell mapping. Recomputed each render /
|
||||
// event from the current frame so the hit-test matches what was drawn.
|
||||
layout: BoardLayout;
|
||||
|
||||
// Center a square 8×8 grid inside the safe-area-inset region of `frame`.
|
||||
compute_layout :: (self: *BoardView, frame: Frame) {
|
||||
avail := frame.inset(self.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: *BoardView, 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
|
||||
)
|
||||
self.layout.compute(frame, self.safe);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,11 +171,11 @@ impl View for BoardView {
|
||||
}
|
||||
|
||||
// 2. One cell tile per board cell, then its gem sampled by index column.
|
||||
gem_inset := self.cell_size * (1.0 - GEM_FILL_FRAC) * 0.5;
|
||||
gem_dim := self.cell_size * GEM_FILL_FRAC;
|
||||
gem_inset := self.layout.cell_size * (1.0 - GEM_FILL_FRAC) * 0.5;
|
||||
gem_dim := self.layout.cell_size * GEM_FILL_FRAC;
|
||||
for 0..BOARD_ROWS: (row) {
|
||||
for 0..BOARD_COLS: (col) {
|
||||
cf := self.cell_frame(col, row);
|
||||
cf := self.layout.cell_frame(col, row);
|
||||
|
||||
if self.assets.cell_tex != 0 {
|
||||
ctx.add_image(cf, self.assets.cell_tex);
|
||||
@@ -165,9 +194,59 @@ impl View for BoardView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Selection overlay on the chosen cell: a translucent fill under a
|
||||
// bright rim, drawn over the whole grid so it reads as a highlight.
|
||||
if self.sel != null and self.sel.active {
|
||||
cf := self.layout.cell_frame(self.sel.cell.col, self.sel.cell.row);
|
||||
ctx.add_rect(cf, SELECT_FILL);
|
||||
rim_w := max(2.0, self.layout.cell_size * 0.06);
|
||||
ctx.add_stroked_rect(cf, SELECT_RIM, SELECT_RIM, rim_w, self.layout.cell_size * 0.14);
|
||||
}
|
||||
|
||||
// 4. HUD card with score + remaining moves, in the band above the grid.
|
||||
avail := frame.inset(self.safe);
|
||||
render_hud(ctx, self.board, avail);
|
||||
}
|
||||
|
||||
handle_event :: (self: *BoardView, event: *Event, frame: Frame) -> bool {
|
||||
self.compute_layout(frame);
|
||||
if event.* == {
|
||||
case .mouse_down: (d) {
|
||||
if hit := self.layout.point_to_cell(d.position) {
|
||||
self.sel.toggle(hit);
|
||||
} else {
|
||||
self.sel.clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the HUD card — current score and remaining moves (out of the move limit)
|
||||
// — centered horizontally in the top of `avail`, the safe-area-inset region the
|
||||
// grid is centered in. Reads live model state, so it tracks score/moves as the
|
||||
// game progresses. A translucent panel sits behind the text for legibility over
|
||||
// the board art.
|
||||
render_hud :: (ctx: *RenderContext, board: *Board, avail: Frame) {
|
||||
score_str := format("SCORE {}", board.score);
|
||||
moves_str := format("MOVES {}/{}", board.moves_remaining(), board.move_limit);
|
||||
|
||||
score_sz := measure_text(score_str, HUD_FONT);
|
||||
moves_sz := measure_text(moves_str, HUD_FONT);
|
||||
text_w := max(score_sz.width, moves_sz.width);
|
||||
|
||||
panel_w := text_w + HUD_PAD * 2.0;
|
||||
panel_h := score_sz.height + HUD_LINE_GAP + moves_sz.height + HUD_PAD * 2.0;
|
||||
panel_x := avail.origin.x + (avail.size.width - panel_w) * 0.5;
|
||||
panel_y := avail.origin.y + HUD_PAD;
|
||||
ctx.add_rounded_rect(Frame.make(panel_x, panel_y, panel_w, panel_h), HUD_PANEL, 12.0);
|
||||
|
||||
tx := panel_x + HUD_PAD;
|
||||
ty := panel_y + HUD_PAD;
|
||||
ctx.add_text(Frame.make(tx, ty, score_sz.width, score_sz.height), score_str, HUD_FONT, HUD_TEXT);
|
||||
ty += score_sz.height + HUD_LINE_GAP;
|
||||
ctx.add_text(Frame.make(tx, ty, moves_sz.width, moves_sz.height), moves_str, HUD_FONT, HUD_TEXT);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user