Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ pub fn build(b: *std.Build) void {
// index-aligned, and a bad declaration is a hard error.
"test/hook_ordering_tests.zig",
"test/animation_assets_tests.zig",
"test/cursor_binding_tests.zig",
// The HOOK ROUTE INSPECTOR (labelle-assembler#724,
// docs/design/hook-route-inspection.md). The load-bearing test
// generates a `main.zig` through the real emitter and compares the
Expand All @@ -477,6 +478,7 @@ pub fn build(b: *std.Build) void {
"test/i18n_system_locale_tests.zig",
};

const cursor_test_step = b.step("test-cursor", "Test generated cursor binding and startup ordering");
for (test_files) |test_file| {
const t = b.addTest(.{
.root_module = b.createModule(.{
Expand All @@ -492,6 +494,7 @@ pub fn build(b: *std.Build) void {
}),
});
const run_test = b.addRunArtifact(t);
if (std.mem.eql(u8, test_file, "test/cursor_binding_tests.zig") or std.mem.eql(u8, test_file, "test/main_zig_tests.zig")) cursor_test_step.dependOn(&run_test.step);
test_step.dependOn(&run_test.step);
if (std.mem.eql(u8, test_file, "test/build_zig_tests.zig") or std.mem.eql(u8, test_file, "test/build_zig_zon_tests.zig")) material_test_step.dependOn(&run_test.step);
if (std.mem.eql(u8, test_file, "test/apk_assets_tests.zig")) {
Expand Down
2 changes: 2 additions & 0 deletions src/codegen/lifecycle/callback.zig
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ pub fn Mixin(comptime Self: type) type {

try @import("../blocks/animation_assets.zig").emit(w, self.animation_jsonc_names, .catch_panic_style);

try @import("loop.zig").emitCursorBinding(w);

// Device language (RFC-I18N section 8) — same hand-off and
// placement as the loop-path setup (`loop.emitSystemLocale`).
if (self.i18n) try @import("loop.zig").emitSystemLocale(w);
Expand Down
8 changes: 8 additions & 0 deletions src/codegen/lifecycle/loop.zig
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ pub fn emitSystemLocale(w: anytype) !void {
try w.writeAll(" }\n\n");
}

/// Bind the optional shared cursor seam before any script setup runs. Older
/// engines fold this away; newer engines adapt older window backends safely.
pub fn emitCursorBinding(w: anytype) !void {
try w.writeAll(" if (comptime @hasDecl(AssembledGame, \"bindCursorWindow\")) g.bindCursorWindow(window);\n");
}

pub fn Mixin(comptime Self: type) type {
return struct {
pub fn buildSetupCode(self: *Self) ![]const u8 {
Expand All @@ -87,6 +93,8 @@ pub fn Mixin(comptime Self: type) type {

try @import("../blocks/animation_assets.zig").emit(w, self.animation_jsonc_names, .try_style);

try emitCursorBinding(w);

// Window icon hand-off (labelle-cli#359). Desktop only: the
// loop templates call `window.initWindow` before `{{setup_code}}`,
// so the window exists here. Gated on the backend DECL, so raylib/
Expand Down
37 changes: 37 additions & 0 deletions test/cursor_binding_tests.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! Compile and run the emitted binding with both old and new engine surfaces.
const std = @import("std");
const h = @import("helpers.zig");

test "generated cursor binding runs on a new engine and compiles away on an old one" {
const allocator = std.testing.allocator;
var ctx = h.emptyCodegen(allocator);
const setup = try ctx.buildCallbackInitCode();
defer allocator.free(setup);
const start = std.mem.indexOf(u8, setup, " if (comptime @hasDecl(AssembledGame, \"bindCursorWindow\"))").?;
const end = start + std.mem.indexOfScalar(u8, setup[start..], '\n').?;
for ([_]bool{ false, true }) |modern| {
const header = if (modern)
"const AssembledGame = struct { calls: u32 = 0, pub fn bindCursorWindow(self: *@This(), comptime W: type) void { self.calls += 1; _ = W; } };\n"
else
"const AssembledGame = struct { calls: u32 = 0 };\n";
const source = try std.fmt.allocPrint(allocator, "{s}const window = struct {{}};\nvar g = AssembledGame{{}};\ntest \"binding\" {{\n{s}\ntry @import(\"std\").testing.expectEqual(@as(u32, {d}), g.calls);\n}}\n", .{ header, setup[start..end], @intFromBool(modern) });
defer allocator.free(source);
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var file = try tmp.dir.createFile(std.testing.io, "binding.zig", .{});
try file.writeStreamingAll(std.testing.io, source);
file.close(std.testing.io);
const root = try tmp.dir.realPathFileAlloc(std.testing.io, ".", allocator);
defer allocator.free(root);
const result = try std.process.run(allocator, std.testing.io, .{
.argv = &.{ @import("test_options").zig_exe, "test", "binding.zig" },
.cwd = .{ .path = root },
});
defer allocator.free(result.stdout);
defer allocator.free(result.stderr);
if (result.term != .exited or result.term.exited != 0) {
std.debug.print("cursor binding failed:\n{s}\n", .{result.stderr});
return error.CursorBindingFailed;
}
}
}
1 change: 1 addition & 0 deletions test/goldens/acme_callback_main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export fn init() callconv(.c) void {
g = AssembledGame.init(allocator);
g.setHooks(&hooks);
g.setScreenHeight(@as(f32, @floatFromInt(screen_h)));
if (comptime @hasDecl(AssembledGame, "bindCursorWindow")) g.bindCursorWindow(window);
// ── Image asset backend wiring (Asset Streaming RFC, #53) ──
// `engine.ImageLoader.setBackend` installs function-pointer adapters
// that marshal between `BackendGfx`'s `DecodedImage`/`Texture` and
Expand Down
1 change: 1 addition & 0 deletions test/goldens/bgfx_wasm_editor_preview_main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ pub fn main() !void {

g.setScreenHeight(@as(f32, @floatFromInt(screen_h)));

if (comptime @hasDecl(AssembledGame, "bindCursorWindow")) g.bindCursorWindow(window);
// ── Image asset backend wiring (Asset Streaming RFC, #53) ──
// `engine.ImageLoader.setBackend` installs function-pointer adapters
// that marshal between `BackendGfx`'s `DecodedImage`/`Texture` and
Expand Down
1 change: 1 addition & 0 deletions test/goldens/sokol_ios_main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ export fn init() callconv(.c) void {
const allocator = gpa.allocator();
g = AssembledGame.init(allocator);
g.setHooks(&hooks);
if (comptime @hasDecl(AssembledGame, "bindCursorWindow")) g.bindCursorWindow(window);
// ── Image asset backend wiring (Asset Streaming RFC, #53) ──
// `engine.ImageLoader.setBackend` installs function-pointer adapters
// that marshal between `BackendGfx`'s `DecodedImage`/`Texture` and
Expand Down
22 changes: 22 additions & 0 deletions test/main_zig_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1801,3 +1801,25 @@ pub const WINDOW_ICON = struct {
}
}
};


pub const CURSOR_BINDING = struct {
fn verify(source: []const u8) !void {
const bind = std.mem.indexOf(u8, source, "if (comptime @hasDecl(AssembledGame, \"bindCursorWindow\")) g.bindCursorWindow(window);") orelse return error.MissingCursorBinding;
const init = std.mem.indexOf(u8, source, "g = AssembledGame.init(") orelse return error.MissingInit;
const setup = std.mem.indexOf(u8, source, "runner.setup(&g);") orelse return error.MissingSetup;
try std.testing.expect(init < bind and bind < setup);
}
test "loop startup binds cursor after game init and before scripts" {
const source = try SYSTEM_LOCALE.generateMain(.desktop, raylib_lifecycle);
defer std.testing.allocator.free(source);
try verify(source);
}
test "callback startup binds cursor after game init and before scripts" {
h.setBgfxAndroidLifecycle();
defer h.clearLifecycleOverrides();
const source = try SYSTEM_LOCALE.generateMain(.android, bgfx_android_lifecycle);
defer std.testing.allocator.free(source);
try verify(source);
}
};
Loading