// 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 } }