diff --git a/build.zig b/build.zig index 19fde3dc9..06ac7f0cd 100644 --- a/build.zig +++ b/build.zig @@ -1582,6 +1582,7 @@ pub fn build(b: *std.Build) void { b.step("test-ts-static-markup-values", "Compare static markup dynamic values, exact payloads, keys and complete failure effects").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-static-markup-value-tests", &.{ "static markup values", "compiled markup values" })).step); b.step("test-ts-static-markup-collections", "Compare static virtual windows, row consumption, exact keys, stepper indices and complete failure effects").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-static-markup-collection-tests", &.{ "static markup collections", "compiled markup numeric", "compiled markup specialized" })).step); b.step("test-ts-static-markup-expansion", "Compare static branches, item sequencing, lexical captures, exact inherited keys and complete failure effects").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-static-markup-expansion-tests", &.{ "static markup expansion", "static markup collections" })).step); + b.step("test-ts-static-markup-specialized", "Compare static specialized metadata, forwarded handlers, tab triggers and complete failure effects").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-static-markup-specialized-tests", &.{ "static markup", "compiled markup values", "compiled markup metadata", "compiled markup options", "compiled markup postprocessing", "compiled markup numeric", "compiled markup admission", "compiled markup literals" })).step); b.step("test-ts-markup-postprocessing", "Compare forwarded handlers, provenance, tabs and borrowed binding paths").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-markup-postprocessing-tests", &.{ "compiled markup postprocessing", "compiled markup literals", "compiled markup admission", "compiled markup construction" })).step); b.step("test-ts-markup-literals", "Compare exact literal inference, template defaults and lexical capture ownership").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-markup-literal-tests", &.{ "compiled markup literals", "compiled markup admission", "compiled markup construction" })).step); b.step("test-ts-markup-admission", "Compare specialized attribute forms, raw flags and complete eager failure effects").dependOn(&b.addRunArtifact(filteredTestArtifact(b, ts_core_artifacts.surface_layout.root_module, "ts-markup-admission-tests", &.{ "compiled markup admission", "compiled markup specialized", "compiled markup options", "compiled markup metadata" })).step); diff --git a/packages/core/src/markup_values.ts b/packages/core/src/markup_values.ts index 41835a9a7..317a7aaa0 100644 --- a/packages/core/src/markup_values.ts +++ b/packages/core/src/markup_values.ts @@ -316,10 +316,13 @@ function nscmValues(r: NscElementReader, out: number[], mode: number): void { const path = r.blob(), dot = nscmDot(path); // Return borrowed byte offsets; a trailing dot has a present empty tail. nsceEmit(out, 93, 0, dot < 0 ? path.length : dot, dot < 0 ? 0 : dot + 1); + } else if (mode === 36) { + // A use-site handler replaces the local timeline handler before construction. + nsceEmit(out, r.flag() ? 1 : 0); } else throw new Error("invalid markup value mode"); } function nscvMarkupValues(input: Uint8Array): Uint8Array { - if (input.length < 4 || input[0] !== 113 || input[1] !== 1 || input[2]! > 35 || input[3]! > 1) throw new Error("invalid markup value header"); + if (input.length < 4 || input[0] !== 113 || input[1] !== 1 || input[2]! > 36 || input[3]! > 1) throw new Error("invalid markup value header"); const r = new NscElementReader(input), out: number[] = []; nscmValues(r, out, input[2]!); if (r.at !== input.length) throw new Error("trailing markup value facts"); diff --git a/packages/core/test/markup_values.test.ts b/packages/core/test/markup_values.test.ts index 924a908c0..35190eeec 100644 --- a/packages/core/test/markup_values.test.ts +++ b/packages/core/test/markup_values.test.ts @@ -302,3 +302,12 @@ test("binding paths retain byte offsets including empty, trailing and malformed assert.deepEqual(row!.fields, [dot < 0 ? path.length : dot, dot < 0 ? 0 : dot + 1, 0, 0, 0]); } }); + +test("timeline forwarding selects use-site handlers and rejects malformed facts", () => { + assert.deepEqual(actions(new Packet(36).word(0)), [0]); + assert.deepEqual(actions(new Packet(36).word(1)), [1]); + const valid = new Packet(36).word(1).bytes(); + for (let end = 0; end < valid.length; end++) assert.throws(() => run(valid.slice(0, end))); + assert.throws(() => run(new Packet(36).word(2).bytes())); + assert.throws(() => run(new Packet(36).word(0, 1).bytes())); +}); diff --git a/src/primitives/canvas/ui_markup_compiled.zig b/src/primitives/canvas/ui_markup_compiled.zig index dff244cdf..8de2b3474 100644 --- a/src/primitives/canvas/ui_markup_compiled.zig +++ b/src/primitives/canvas/ui_markup_compiled.zig @@ -35,6 +35,7 @@ const reflect = @import("ui_markup_reflect.zig"); const runtime_values = @import("ui_markup_compiled_values.zig"); const specialized = @import("ui_markup_specialized.zig"); const construction = @import("ui_markup_construction.zig"); +const value_programs = @import("ui_markup_values.zig"); const Value = interpreter.Value; @@ -273,6 +274,34 @@ fn CompiledMarkupEngine(comptime ModelT: type, comptime MsgT: type, comptime res } fn buildElementWithForwardedPress(comptime node: markup.MarkupNode, comptime entries: []const ScopeEntry, ui: *Ui, model: *const ModelT, scope: anytype, forwarded_press: ?MsgT) Ui.Node { + if (ui.expression_policy) |owner| return buildElementWithForwardedPressOwned(node, entries, ui, model, scope, forwarded_press, owner); + return buildElementWithForwardedPressReference(node, entries, ui, model, scope, forwarded_press); + } + + fn buildElementWithForwardedPressOwned(comptime node: markup.MarkupNode, comptime entries: []const ScopeEntry, ui: *Ui, model: *const ModelT, scope: anytype, forwarded_press: ?MsgT, owner: markup.expr.Policy) Ui.Node { + var request = value_programs.Request.init(33); + defer request.deinit(); + // Static views do not attach interpreter provenance. + request.words(&.{ @intFromBool(forwarded_press != null), 0 }); + var program = request.program(owner) catch return runtimeFail(Ui.Node, ui); + defer program.deinit(); + var built: ?Ui.Node = null; + while (program.next()) |step| switch (step.action) { + 88 => built = buildElementInner(node, entries, ui, model, scope, forwarded_press), + 89 => { + built.?.on_press = forwarded_press.?; + built.?.widget.semantics.focusable = true; + }, + 91 => { + if (program.next() != null) @panic("trailing static element publication capability"); + return built orelse @panic("static element publication did not build a node"); + }, + else => @panic("invalid static element publication capability"), + }; + @panic("incomplete static element publication program"); + } + + fn buildElementWithForwardedPressReference(comptime node: markup.MarkupNode, comptime entries: []const ScopeEntry, ui: *Ui, model: *const ModelT, scope: anytype, forwarded_press: ?MsgT) Ui.Node { var built = buildElementInner(node, entries, ui, model, scope, forwarded_press); if (forwarded_press) |msg| { // A press declared on belongs to the expanded root. @@ -284,6 +313,23 @@ fn CompiledMarkupEngine(comptime ModelT: type, comptime MsgT: type, comptime res return built; } + fn lowerTabsTriggers(ui: *Ui, children: []Ui.Node) void { + const owner = ui.expression_policy orelse return interpreter.lowerTabsTriggers(children); + var request = value_programs.Request.init(34); + defer request.deinit(); + request.word(children.len); + for (children) |child| request.blob(@tagName(child.widget.kind)); + var program = request.program(owner) catch { + ui.failed = true; + return; + }; + defer program.deinit(); + while (program.next()) |step| switch (step.action) { + 92 => children[step.fields[0]].widget.kind = .segmented_control, + else => @panic("invalid static tab trigger capability"), + }; + } + fn buildElementInner(comptime node: markup.MarkupNode, comptime entries: []const ScopeEntry, ui: *Ui, model: *const ModelT, scope: anytype, forwarded_press: ?MsgT) Ui.Node { if (comptime std.mem.eql(u8, node.name, "virtual-window")) return buildVirtualWindow(node, entries, ui, model, scope); if (comptime std.mem.eql(u8, node.name, "virtual-list")) return buildVirtualList(node, entries, ui, model, scope); @@ -559,7 +605,7 @@ fn CompiledMarkupEngine(comptime ModelT: type, comptime MsgT: type, comptime res // `ToggleCaption"; + try check(source, .{}, true); + try std.testing.expect(modes[34] > 0); +} diff --git a/tests/ts-core/static_markup_values_e2e_tests.zig b/tests/ts-core/static_markup_values_e2e_tests.zig index b02d7a89b..c3f5a9373 100644 --- a/tests/ts-core/static_markup_values_e2e_tests.zig +++ b/tests/ts-core/static_markup_values_e2e_tests.zig @@ -25,7 +25,7 @@ fn exact(a: anytype, b: @TypeOf(a)) anyerror!void { } } -var operations: [36]usize = @splat(0); +var operations: [256]usize = @splat(0); var keys: usize = 0; fn observed(input: []const u8, output: []u8) usize { if (input[0] == 113) operations[input[2]] += 1; diff --git a/tests/ts-core/surface_layout_e2e_tests.zig b/tests/ts-core/surface_layout_e2e_tests.zig index ebeabe1f8..548b695eb 100644 --- a/tests/ts-core/surface_layout_e2e_tests.zig +++ b/tests/ts-core/surface_layout_e2e_tests.zig @@ -1012,4 +1012,5 @@ test { test { _ = @import("static_markup_collections_e2e_tests.zig"); _ = @import("static_markup_expansion_e2e_tests.zig"); + _ = @import("static_markup_specialized_e2e_tests.zig"); }