Wire touch input into the model in BoardView.handle_event. A press records the drag start (new DragInput, heap-allocated so it survives the per-frame BoardView rebuild between mouse_down and mouse_up); the release resolves the gesture against the same layout it was drawn with. A swipe — start→end mapped by swipe_intent to an adjacent-swap intent — is fed straight into commit_swap: a legal swap applies, cascades (clear→collapse→refill), accrues score and spends a move; an illegal one reverts, no move. A sub-threshold / off-board drag carries no intent and falls back to the tap behaviour (toggle/clear selection). The next frame re-renders board + HUD from the model. Reuses swipe.sx + board_layout.sx + commit_swap unchanged — this is wiring, not new legality/cascade logic. tests/swipe_commit.sx (new golden) drives the full path on the seeded board (SEED 1337): a rightward swipe (0,0)->(1,0) is illegal (two reds) and reverts byte-for-byte with no score/move; (5,4)->(6,4) is legal, completes R,R,R on row 4, awards 30, spends one move. Sim evidence (iPhone 17, iOS 26.0): goldens/p5_swap_before.png (SCORE 0, MOVES 30/30) and goldens/p5_swap_after.png (SCORE 30, MOVES 29/30) bracket a real idb-injected swipe at (276,475)->(327,475) pt = cell (5,4)->(6,4); the three reds clear and the board matches the model's resolved state.
160 lines
5.8 KiB
Plaintext
160 lines
5.8 KiB
Plaintext
#import "modules/std.sx";
|
|
#import "build.sx";
|
|
#import "modules/compiler.sx";
|
|
#import "modules/opengl.sx";
|
|
#import "modules/sdl3.sx";
|
|
#import "modules/math";
|
|
#import "modules/stb.sx";
|
|
#import "modules/stb_truetype.sx";
|
|
#import "modules/gpu/api.sx";
|
|
#import "modules/gpu/types.sx";
|
|
#import "modules/gpu/metal.sx";
|
|
#import "modules/ui";
|
|
#import "modules/platform/api.sx";
|
|
#import "modules/platform/sdl3.sx";
|
|
#import "modules/platform/uikit.sx";
|
|
#import "board.sx";
|
|
#import "board_view.sx";
|
|
|
|
#run configure_build();
|
|
|
|
// Fixed seed for the rendered board — the same seed tests/board_init.sx locks
|
|
// as a snapshot, so the on-screen layout matches that golden gem-for-gem.
|
|
BOARD_SEED :: 1337;
|
|
|
|
g_plat : Platform = ---;
|
|
g_pipeline : *UIPipeline = ---;
|
|
g_viewport_w : f32 = 800.0;
|
|
g_viewport_h : f32 = 600.0;
|
|
g_safe_insets : EdgeInsets = .{};
|
|
|
|
// iOS-only concrete handles kept alongside the boxed `g_plat` so the frame loop
|
|
// can reach the CAMetalLayer pointer / pixel dims without going through the
|
|
// protocol box.
|
|
g_uikit_plat : *UIKitPlatform = null;
|
|
g_metal_gpu : *MetalGPU = null;
|
|
|
|
// The pure-sx model (board.sx) and its sprites, seeded once in main() and
|
|
// rendered every frame. Heap-allocated so the view holds stable pointers to
|
|
// the mutable state across frames.
|
|
g_board : *Board = null;
|
|
g_assets : *BoardAssets = null;
|
|
|
|
// Current cell selection (P4.4). Heap-allocated so it survives BoardView's
|
|
// per-frame rebuild; a tap hit-tests a cell and toggles this.
|
|
g_sel : *BoardSelection = null;
|
|
|
|
// In-progress touch drag (P5.2). Heap-allocated for the same reason: the press
|
|
// and release that bracket a swipe land on different per-frame BoardView values,
|
|
// so the drag start must persist between them.
|
|
g_drag : *DragInput = null;
|
|
|
|
// Rebuilt each frame inside the pipeline's arena; carries the current safe-area
|
|
// insets so the grid stays inside the notch / home-indicator region.
|
|
build_ui :: () -> View {
|
|
BoardView.{ board = g_board, assets = g_assets, sel = g_sel, drag = g_drag, safe = g_safe_insets }
|
|
}
|
|
|
|
frame :: () {
|
|
fc := g_plat.begin_frame();
|
|
g_viewport_w = fc.viewport_w;
|
|
g_viewport_h = fc.viewport_h;
|
|
g_safe_insets = g_plat.safe_insets();
|
|
|
|
if fc.viewport_w != g_pipeline.screen_width or fc.viewport_h != g_pipeline.screen_height {
|
|
g_pipeline.resize(fc.viewport_w, fc.viewport_h);
|
|
}
|
|
|
|
for g_plat.poll_events(): (*ev) {
|
|
inline if OS != .ios {
|
|
if ev == {
|
|
case .key_up: (e) {
|
|
if e.key == .escape { g_plat.stop(); }
|
|
}
|
|
}
|
|
}
|
|
g_pipeline.dispatch_event(ev);
|
|
}
|
|
|
|
inline if OS == .ios {
|
|
// Lazy-attach Metal once -[SxAppDelegate didFinishLaunching:] has
|
|
// installed the SxMetalView and its bounds have been measured; both can
|
|
// lag the first CADisplayLink tick, and a zero-sized drawable aborts
|
|
// via XPC.
|
|
if g_uikit_plat.gl_layer == null { return; }
|
|
if g_uikit_plat.pixel_w <= 0 or g_uikit_plat.pixel_h <= 0 { return; }
|
|
if g_metal_gpu.layer == null {
|
|
g_metal_gpu.init(g_uikit_plat.gl_layer, g_uikit_plat.pixel_w, g_uikit_plat.pixel_h);
|
|
} else if g_metal_gpu.pixel_w != g_uikit_plat.pixel_w or g_metal_gpu.pixel_h != g_uikit_plat.pixel_h {
|
|
g_metal_gpu.resize(g_uikit_plat.pixel_w, g_uikit_plat.pixel_h);
|
|
}
|
|
clear : ClearColor = .{ r = 0.05, g = 0.06, b = 0.10, a = 1.0 };
|
|
if !g_metal_gpu.begin_frame(clear) { return; }
|
|
g_pipeline.tick();
|
|
g_metal_gpu.end_frame(fc.target_present_time);
|
|
} else {
|
|
glViewport(0, 0, fc.pixel_w, fc.pixel_h);
|
|
glClearColor(0.05, 0.06, 0.10, 1.0);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
g_pipeline.tick();
|
|
}
|
|
g_plat.end_frame();
|
|
}
|
|
|
|
main :: () -> void {
|
|
inline if OS == .ios {
|
|
u : *UIKitPlatform = xx context.allocator.alloc(size_of(UIKitPlatform));
|
|
u.gpu_mode = .metal;
|
|
if !u.init("m3te", 800, 600) { return; }
|
|
g_plat = xx u;
|
|
g_uikit_plat = u;
|
|
|
|
// The CAMetalLayer doesn't exist until didFinishLaunching: runs after we
|
|
// return into UIApplicationMain, so attach lazily on the first frame.
|
|
// init(null, 0, 0) only needs the MTLDevice, which is enough for the
|
|
// texture uploads below.
|
|
g_metal_gpu = xx context.allocator.alloc(size_of(MetalGPU));
|
|
// alloc returns uninitialized memory; struct field defaults are NOT
|
|
// applied, so List caps/lens would be garbage without this memset.
|
|
memset(xx g_metal_gpu, 0, size_of(MetalGPU));
|
|
if !g_metal_gpu.init(null, 0, 0) { return; }
|
|
} else {
|
|
s : *SdlPlatform = xx context.allocator.alloc(size_of(SdlPlatform));
|
|
if !s.init("m3te", 800, 600) { return; }
|
|
g_plat = xx s;
|
|
}
|
|
|
|
fc := g_plat.begin_frame();
|
|
g_viewport_w = fc.viewport_w;
|
|
g_viewport_h = fc.viewport_h;
|
|
g_safe_insets = g_plat.safe_insets();
|
|
|
|
g_pipeline = xx context.allocator.alloc(size_of(UIPipeline));
|
|
// Same alloc caveat as above: zero so the optional `gpu` reads as null on
|
|
// the desktop path (where set_gpu is not called) and the Lists start empty.
|
|
memset(xx g_pipeline, 0, size_of(UIPipeline));
|
|
inline if OS == .ios {
|
|
g_pipeline.set_gpu(xx g_metal_gpu);
|
|
}
|
|
g_pipeline.init(fc.viewport_w, fc.viewport_h);
|
|
g_pipeline.init_font("assets/fonts/default.ttf", 32.0, fc.dpi_scale);
|
|
|
|
g_board = xx context.allocator.alloc(size_of(Board));
|
|
g_board.init(BOARD_SEED);
|
|
|
|
g_assets = xx context.allocator.alloc(size_of(BoardAssets));
|
|
g_assets.init();
|
|
g_assets.load(g_pipeline.gpu);
|
|
|
|
g_sel = xx context.allocator.alloc(size_of(BoardSelection));
|
|
g_sel.init();
|
|
|
|
g_drag = xx context.allocator.alloc(size_of(DragInput));
|
|
g_drag.init();
|
|
|
|
g_pipeline.set_body(closure(build_ui));
|
|
|
|
g_plat.run_frame_loop(closure(frame));
|
|
g_plat.shutdown();
|
|
}
|