Adopt the modules/ui UIPipeline framework (as the chess reference app does) and replace the P0 placeholder quad with a BoardView (View protocol, modeled on chess/board_view.sx): - background.png fills the screen; an 8x8 cell.png grid is centered in the safe area; each cell's gem is sampled from gems.png by UV column = gem index (0=red .. 5=purple). - Drive it from board.sx seeded with 1337 (the board_init golden's seed), so the on-screen layout matches that snapshot gem-for-gem. main.sx now hosts the view via UIPipeline (Metal on iOS, GL on desktop) and heap-allocates the board/asset state behind pointers (UFCS method calls on a value-typed global mutate a copy, so mutable state must live behind a pointer as the reference app does). Vendor the C deps the UI module's image/font path needs (stb_image, stb_truetype, kb_text_shape, file_utils); their #include "vendors/..." paths resolve relative to the project root. Evidence: ios-sim build links clean; tools/run_tests.sh 11/11 pass; running app captured at goldens/p4_board.png.
174 lines
5.8 KiB
Plaintext
174 lines
5.8 KiB
Plaintext
// BoardView (P4.3) — render the seeded match-3 board with real gem sprites.
|
||
//
|
||
// Modeled on game/chess/board_view.sx: a `View` that lays out an 8×8 grid and
|
||
// draws tiles/sprites through RenderContext.add_image / add_image_uv, sampling
|
||
// the gem sprite sheet by UV column. The background image fills the whole view;
|
||
// the grid is a centered square inside the safe-area inset.
|
||
#import "modules/std.sx";
|
||
#import "modules/math";
|
||
#import "modules/opengl.sx";
|
||
#import "modules/stb.sx";
|
||
#import "modules/gpu/types.sx";
|
||
#import "modules/gpu/api.sx";
|
||
#import "modules/ui/types.sx";
|
||
#import "modules/ui/render.sx";
|
||
#import "modules/ui/events.sx";
|
||
#import "modules/ui/view.sx";
|
||
#import "board.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;
|
||
|
||
// UV sub-rect of one gem column, spanning the sheet's full height.
|
||
GemUV :: struct {
|
||
uv_min: Point;
|
||
uv_max: Point;
|
||
}
|
||
|
||
// Loads and holds the three board textures (background, cell tile, gem sheet)
|
||
// and maps a gem index to its column UV. Modeled on chess's ChessPieces.
|
||
BoardAssets :: struct {
|
||
bg_tex: u32;
|
||
cell_tex: u32;
|
||
gems_tex: u32;
|
||
cell_u: f32;
|
||
loaded: bool;
|
||
|
||
init :: (self: *BoardAssets) {
|
||
self.bg_tex = 0;
|
||
self.cell_tex = 0;
|
||
self.gems_tex = 0;
|
||
// gems.png is GEM_COUNT columns wide and one row tall, so a gem's UV
|
||
// column IS its gem index (0=red … 5=purple); cell_u is one column wide.
|
||
self.cell_u = 1.0 / cast(f32) GEM_COUNT;
|
||
self.loaded = false;
|
||
}
|
||
|
||
load :: (self: *BoardAssets, gpu: ?GPU) {
|
||
self.bg_tex = load_texture("assets/board/background.png", gpu);
|
||
self.cell_tex = load_texture("assets/board/cell.png", gpu);
|
||
self.gems_tex = load_texture("assets/gems/gems.png", gpu);
|
||
self.loaded = self.bg_tex != 0 and self.cell_tex != 0 and self.gems_tex != 0;
|
||
}
|
||
|
||
gem_uv :: (self: *BoardAssets, index: s64) -> GemUV {
|
||
u0 : f32 = xx index * self.cell_u;
|
||
GemUV.{
|
||
uv_min = Point.{ x = u0, y = 0.0 },
|
||
uv_max = Point.{ x = u0 + self.cell_u, y = 1.0 }
|
||
}
|
||
}
|
||
}
|
||
|
||
// Decode an RGBA image and upload it as a texture, returning the handle (0 on
|
||
// failure). When a GPU backend is bound (iOS Metal) it owns the upload; the
|
||
// desktop GL path falls back to a plain GL_TEXTURE_2D.
|
||
load_texture :: (path: [:0]u8, gpu: ?GPU) -> u32 {
|
||
w : s32 = 0;
|
||
h : s32 = 0;
|
||
ch : s32 = 0;
|
||
pixels := stbi_load(path, @w, @h, @ch, 4);
|
||
if pixels == null {
|
||
out("WARNING: could not load texture: ");
|
||
out(path);
|
||
out("\n");
|
||
return 0;
|
||
}
|
||
|
||
tex : u32 = 0;
|
||
if gpu != null {
|
||
tex = gpu.create_texture(w, h, .rgba8, xx pixels);
|
||
} else {
|
||
glGenTextures(1, @tex);
|
||
glBindTexture(GL_TEXTURE_2D, tex);
|
||
glTexImage2D(GL_TEXTURE_2D, 0, xx GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE);
|
||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);
|
||
}
|
||
|
||
stbi_image_free(pixels);
|
||
return tex;
|
||
}
|
||
|
||
BoardView :: struct {
|
||
board: *Board;
|
||
assets: *BoardAssets;
|
||
safe: EdgeInsets;
|
||
|
||
cell_size: f32;
|
||
origin: Point;
|
||
|
||
// 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
|
||
)
|
||
}
|
||
}
|
||
|
||
impl View for BoardView {
|
||
size_that_fits :: (self: *BoardView, proposal: ProposedSize) -> Size {
|
||
Size.{ width = proposal.width ?? 0.0, height = proposal.height ?? 0.0 }
|
||
}
|
||
|
||
layout :: (self: *BoardView, bounds: Frame) {
|
||
self.compute_layout(bounds);
|
||
}
|
||
|
||
render :: (self: *BoardView, ctx: *RenderContext, frame: Frame) {
|
||
self.compute_layout(frame);
|
||
|
||
// 1. Background image fills the whole view, behind the grid.
|
||
if self.assets.bg_tex != 0 {
|
||
ctx.add_image(frame, self.assets.bg_tex);
|
||
}
|
||
|
||
// 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;
|
||
for 0..BOARD_ROWS: (row) {
|
||
for 0..BOARD_COLS: (col) {
|
||
cf := self.cell_frame(col, row);
|
||
|
||
if self.assets.cell_tex != 0 {
|
||
ctx.add_image(cf, self.assets.cell_tex);
|
||
}
|
||
|
||
g := self.board.at(col, row);
|
||
if g != .empty and self.assets.gems_tex != 0 {
|
||
uv := self.assets.gem_uv(cast(s64) g);
|
||
gf := Frame.make(
|
||
cf.origin.x + gem_inset,
|
||
cf.origin.y + gem_inset,
|
||
gem_dim,
|
||
gem_dim
|
||
);
|
||
ctx.add_image_uv(gf, self.assets.gems_tex, uv.uv_min, uv.uv_max);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
handle_event :: (self: *BoardView, event: *Event, frame: Frame) -> bool {
|
||
false
|
||
}
|
||
}
|