diff --git a/build.zig b/build.zig index 2fac2bb09..86a057543 100644 --- a/build.zig +++ b/build.zig @@ -1327,6 +1327,7 @@ pub fn build(b: *std.Build) void { native_api_tests.addFileArg(b.path("packages/core/test/surface_recipes.test.ts")); native_api_tests.addFileArg(b.path("packages/core/test/widget_metrics.test.ts")); native_api_tests.addFileArg(b.path("packages/core/test/layout_coordination.test.ts")); + native_api_tests.addFileArg(b.path("packages/core/test/render_coordination.test.ts")); native_api_tests.addFileArg(b.path("packages/core/test/component_composition.test.ts")); native_api_tests.addFileArg(b.path("packages/core/test/view_frontend.test.ts")); native_api_tests.has_side_effects = true; @@ -5422,6 +5423,7 @@ fn externalCoreFixtureModule( stage_run.addFileInput(b.path("packages/core/src/measurement_coordination.ts")); stage_run.addFileInput(b.path("packages/core/src/flow_measurement.ts")); stage_run.addFileInput(b.path("packages/core/src/layout_coordination.ts")); + stage_run.addFileInput(b.path("packages/core/src/render_coordination.ts")); stage_run.addFileInput(b.path("packages/core/src/stream_policy.ts")); stage_run.addArg("--src"); stage_run.addDirectoryArg(spec.src_dir); diff --git a/build/app.zig b/build/app.zig index 36283fe76..1fbe615e1 100644 --- a/build/app.zig +++ b/build/app.zig @@ -1262,6 +1262,7 @@ fn tsCoreStage( stage_run.addFileInput(dep.path("packages/core/src/measurement_coordination.ts")); stage_run.addFileInput(dep.path("packages/core/src/flow_measurement.ts")); stage_run.addFileInput(dep.path("packages/core/src/layout_coordination.ts")); + stage_run.addFileInput(dep.path("packages/core/src/render_coordination.ts")); stage_run.addFileInput(dep.path("packages/core/src/stream_policy.ts")); if (typescript_view) { stage_run.addArg("--view-markup"); diff --git a/packages/core/src/render_coordination.ts b/packages/core/src/render_coordination.ts new file mode 100644 index 000000000..9efd09cd9 --- /dev/null +++ b/packages/core/src/render_coordination.ts @@ -0,0 +1,143 @@ +/** Ordered paint capabilities. The host owns widget/text/path buffers and + * executes these copied recipes; the compiler arena never owns a display list. */ +function nscvRenderRecipe(request: Uint8Array): Uint8Array { + if (request.length !== 32 || request[0] !== 43 || request[1] !== 1 || request[2]! > 1 || request[3]! > 2) + throw new Error("invalid render recipe header"); + const w = new DataView(request.buffer, request.byteOffset, request.byteLength); + const flags = w.getUint32(4, true), kind = w.getUint32(20, true), retained = request[2] === 1; + if (flags > 2047 || kind > 62 || w.getUint32(24, true) !== 0 || w.getUint32(28, true) !== 0 || + retained && (w.getUint32(8, true) !== 0 || w.getUint32(12, true) !== 0)) + throw new Error("invalid render recipe facts"); + const result = new Uint8Array(64), out = new DataView(result.buffer); + out.setUint32(0, 1, true); + // Direct-tree depth errors precede hiding, and never initialize a command. + if (!retained && (w.getUint32(12, true) !== 0 || w.getUint32(8, true) >= 32)) { + out.setUint32(4, 2, true); return result; + } + if (!retained && (flags & 256) !== 0) { out.setUint32(4, 1, true); return result; } + let count = 0; + const emit = (op: number, arg: number = 0): void => { + if (count >= 6) throw new Error("render recipe capacity exceeded"); + out.setUint32(16 + count * 8, op, true); out.setUint32(20 + count * 8, arg, true); count++; + }; + // op: backdrop, draw, scrim, children, separators, scrollbars, reactions. + // Child arguments: low byte clip (none/content/frame/disclosure), bit 8 + // bubble palette, bit 9 direct-tree segment stamps. Palette restoration is + // implicit on return; reactions always use the caller's palette. + const contentClip = (flags & 1) !== 0 ? 1 : 0; + const virtual = (flags & 2) !== 0, spans = (flags & 4) !== 0; + emit(0); + let draw = -1; + switch (kind) { + case 0: case 1: case 2: draw = 0; break; + case 43: draw = 1; break; + case 12: draw = 2; break; + case 17: draw = 3; break; + case 18: draw = 4; break; + case 19: draw = 5; break; + case 20: draw = 6; break; + case 21: draw = 7; break; + case 14: draw = 8; break; + case 15: draw = 9; break; + case 16: case 22: draw = 10; break; + case 23: draw = 11; break; + case 24: case 25: draw = 12; break; + case 26: draw = (flags & 8) !== 0 ? 14 : spans ? 15 : 13; break; + case 27: draw = 16; break; + case 28: draw = 17; break; + case 61: draw = 18; break; + case 62: draw = 19; break; + case 29: draw = 20; break; + case 30: draw = 21; break; + case 31: case 32: case 50: draw = 22; break; + case 33: draw = 23; break; + case 34: draw = 24; break; + case 35: case 36: draw = 25; break; + case 39: draw = (flags & 16) !== 0 ? 26 : 25; break; + case 37: case 38: draw = 27; break; + case 40: draw = 28; break; + case 41: draw = 29; break; + case 42: draw = 30; break; + case 44: draw = 31; break; + case 45: draw = 32; break; + case 46: draw = 33; break; + case 47: draw = 34; break; + case 48: draw = 35; break; + case 49: draw = 36; break; + case 51: draw = 37; break; + case 52: draw = 38; break; + case 53: draw = 39; break; + case 58: draw = 40; break; + case 54: draw = 41; break; + case 55: draw = 42; break; + case 56: draw = 43; break; + case 60: draw = 44; break; + } + if (kind >= 19 && kind <= 21 && (flags & 1024) !== 0) emit(2); + if (draw >= 0) emit(1, draw); + if (kind === 15) { emit(3, 256 | contentClip); emit(6); } + else if (kind === 14) { + const disclosure = retained ? request[3]! : (flags & 64) !== 0 ? 2 : 0; + if (disclosure !== 0) emit(3, disclosure === 1 ? 3 : contentClip); + } else if (kind === 6) { + emit(3, 2); if ((flags & 32) === 0) emit(5, 1); + } else if (kind === 4 || kind === 5) { + const scroll = retained && virtual; + emit(3, scroll ? contentClip === 1 ? 1 : 2 : contentClip); + if (scroll && (flags & 32) === 0) emit(5, 0); + emit(4, scroll ? 1 : 0); + } else if (retained && (kind === 3 || kind === 7) && virtual) { + emit(3, contentClip === 1 ? 1 : 2); if ((flags & 32) === 0) emit(5, 0); + } else if (retained) emit(3, contentClip); + else if (kind === 9) { + const stamps = (flags & 512) !== 0 || w.getFloat32(16, true) <= 0; + emit(3, contentClip | (stamps ? 512 : 0)); + } else if (kind <= 13 || kind === 16 || kind === 17 || kind === 18 || + kind >= 19 && kind <= 25 || kind === 42 || kind === 43 || kind === 57 || kind === 59 || kind === 60 || + kind === 44 && !spans && (flags & 128) !== 0) emit(3, contentClip); + out.setUint32(8, count, true); return result; +} + +/** Direct-tree siblings include hidden nodes: visiting a hidden child still + * checks its depth. Layer ties retain source order; segment positions count + * visible source siblings, independently of the order in which they paint. */ +function nscvRenderChildren(request: Uint8Array): Uint8Array { + if (request.length < 32 || request[0] !== 44 || request[1] !== 1 || request[2]! > 1 || request[3] !== 0) + throw new Error("invalid render child header"); + const w = new DataView(request.buffer, request.byteOffset, request.byteLength), count = w.getUint32(4, true); + if (request.length !== 32 + count * 12 || w.getUint32(24, true) !== 0 || w.getUint32(28, true) !== 0) + throw new Error("invalid render child shape"); + let visible = 0; + for (let i = 0; i < count; i++) { + const at = 32 + i * 12, flags = w.getUint32(at + 4, true); + if (w.getUint32(at, true) > 62 || flags > 3) throw new Error("invalid render child facts"); + if ((flags & 1) === 0) visible++; + if ((flags & 2) === 0 && w.getUint32(at + 8, true) !== 0) throw new Error("invalid absent render layer"); + } + const layer = (i: number): number => { + const at = 32 + i * 12, kind = w.getUint32(at, true); + if ((w.getUint32(at + 4, true) & 2) !== 0) return w.getInt32(at + 8, true); + return w.getInt32(kind >= 19 && kind <= 21 ? 12 : kind === 23 || kind === 24 || kind === 25 ? 16 : kind === 40 ? 20 : 8, true); + }; + const result = new Uint8Array(8 + count * 8), out = new DataView(result.buffer); out.setUint32(0, 1, true); out.setUint32(4, count, true); + let previous = -1, previousLayer = 0; + for (let dest = 0; dest < count; dest++) { + let best = -1, bestLayer = 0; + for (let i = 0; i < count; i++) { + const value = layer(i); + if (previous >= 0 && (value < previousLayer || value === previousLayer && i <= previous)) continue; + if (best < 0 || value < bestLayer || value === bestLayer && i < best) { best = i; bestLayer = value; } + } + if (best < 0) throw new Error("render child ordering made no progress"); + let segment = 0; + if (request[2] === 1 && visible > 1) { + let ordinal = 0; + if ((w.getUint32(36 + best * 12, true) & 1) === 0) + for (let i = 0; i < best; i++) if ((w.getUint32(36 + i * 12, true) & 1) === 0) ordinal++; + segment = ordinal === 0 ? 1 : ordinal === visible - 1 ? 3 : 2; + } + out.setUint32(8 + dest * 8, best, true); out.setUint32(12 + dest * 8, segment, true); + previous = best; previousLayer = bestLayer; + } + return result; +} diff --git a/packages/core/src/runtime_policy.ts b/packages/core/src/runtime_policy.ts index ccbe77772..ee98e848b 100644 --- a/packages/core/src/runtime_policy.ts +++ b/packages/core/src/runtime_policy.ts @@ -235,6 +235,8 @@ export function native_window_policy(request: Uint8Array): Uint8Array { if (request[0] === 40) return nscvLayoutAdmission(request); if (request[0] === 41) return nscvLayoutChildren(request); if (request[0] === 42) return nscvLayoutRetained(request); + if (request[0] === 43) return nscvRenderRecipe(request); + if (request[0] === 44) return nscvRenderChildren(request); const data = new DataView(request.buffer, request.byteOffset, request.byteLength); let at = 0; const byte = (): number => { diff --git a/packages/core/src/view_frontend.ts b/packages/core/src/view_frontend.ts index 3300db418..fb59c2236 100644 --- a/packages/core/src/view_frontend.ts +++ b/packages/core/src/view_frontend.ts @@ -930,4 +930,5 @@ const viewPrelude = "\n// Portable Native components compiled beside the committ readFileSync(new URL("./measurement_coordination.ts", import.meta.url), "utf8") + readFileSync(new URL("./flow_measurement.ts", import.meta.url), "utf8") + readFileSync(new URL("./layout_coordination.ts", import.meta.url), "utf8") + + readFileSync(new URL("./render_coordination.ts", import.meta.url), "utf8") + readFileSync(new URL("./stream_policy.ts", import.meta.url), "utf8"); diff --git a/packages/core/test/render_coordination.test.ts b/packages/core/test/render_coordination.test.ts new file mode 100644 index 000000000..f1abe5a0e --- /dev/null +++ b/packages/core/test/render_coordination.test.ts @@ -0,0 +1,72 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { readFileSync } from "node:fs"; +import { stripTypeScriptTypes } from "node:module"; +const source = readFileSync(new URL("../src/render_coordination.ts", import.meta.url), "utf8"); +const { nscvRenderRecipe: recipe, nscvRenderChildren: children } = await import(`data:text/javascript;base64,${Buffer.from(stripTypeScriptTypes(source) + "\nexport {nscvRenderRecipe,nscvRenderChildren};").toString("base64")}`); +const wire = (b: Uint8Array) => new DataView(b.buffer, b.byteOffset, b.byteLength); +function packet(kind: number, retained = false, flags = 0, disclosure = 0): Uint8Array { + const b = new Uint8Array(32); b.set([43,1,retained ? 1 : 0,disclosure]); + wire(b).setUint32(4,flags,true);wire(b).setUint32(20,kind,true);return b; +} +function commands(b: Uint8Array): number[][] { + const r = recipe(b), w = wire(r); + assert.equal(r.length,64);assert.equal(w.getUint32(0,true),1);assert.ok(r.subarray(16+w.getUint32(8,true)*8).every(v=>v===0)); + return Array.from({length:w.getUint32(8,true)},(_,i)=>[w.getUint32(16+i*8,true),w.getUint32(20+i*8,true)]); +} +test("bubble recipes preserve the palette cascade and page-token reactions after children",()=>{ + for(const retained of [false,true])assert.deepEqual(commands(packet(15,retained,1)),[[0,0],[1,9],[3,257],[6,0]]); +}); +test("table and virtual scrolling order clips children bars and separators correctly",()=>{ + assert.deepEqual(commands(packet(5,false,2)),[[0,0],[3,0],[4,0]]); + assert.deepEqual(commands(packet(5,true,2)),[[0,0],[3,2],[5,0],[4,1]]); + assert.deepEqual(commands(packet(5,true,3|32)),[[0,0],[3,1],[4,1]]); + assert.deepEqual(commands(packet(6,true,32)),[[0,0],[3,2]]); + assert.deepEqual(commands(packet(6,false)),[[0,0],[3,2],[5,1]]); +}); +test("disclosure recipes preserve discrete tree and three-state retained emission",()=>{ + assert.deepEqual(commands(packet(14,false,0,1)),[[0,0],[1,8]]); + assert.deepEqual(commands(packet(14,false,64|1)),[[0,0],[1,8],[3,1]]); + for(const [pose,clip]of [[0,-1],[1,3],[2,1]])assert.deepEqual(commands(packet(14,true,1,pose!)),[[0,0],[1,8],...(clip!<0?[]:[[3,clip!]])]); +}); +test("retained leaves and direct cells retain their different child contracts",()=>{ + assert.deepEqual(commands(packet(31,false)),[[0,0],[1,22]]); + assert.deepEqual(commands(packet(31,true)),[[0,0],[1,22],[3,0]]); + assert.deepEqual(commands(packet(44,false,128)),[[0,0],[1,31],[3,0]]); + assert.deepEqual(commands(packet(44,false,128|4)),[[0,0],[1,31]]); + assert.deepEqual(commands(packet(26,false,8|4)),[[0,0],[1,14]]); + assert.deepEqual(commands(packet(39,true,16)),[[0,0],[1,26],[3,0]]); +}); +test("modal scrims precede chrome and segment stamps honor style and exceptional gaps",()=>{ + assert.deepEqual(commands(packet(19,true,1024|1)),[[0,0],[2,0],[1,5],[3,1]]); + for(const [gap,detached,stamp]of [[0,false,true],[2,false,false],[-1,false,true],[NaN,false,false],[Infinity,true,true]] as const){ + const b=packet(9,false,detached?512:0);wire(b).setFloat32(16,gap,true); + assert.deepEqual(commands(b),[[0,0],[3,stamp?512:0]]); + } +}); +test("depth errors precede hidden suppression and retain complete empty command tails",()=>{ + const b=packet(31,false,256);assert.equal(wire(recipe(b)).getUint32(4,true),1); + for(const depth of [32n,9007199254740993n,0xffffffffffffffffn]){ + wire(b).setBigUint64(8,depth,true);const r=recipe(b);assert.equal(wire(r).getUint32(4,true),2);assert.ok(r.subarray(8).every(v=>v===0)); + } +}); +function childPacket(rows: [number,boolean,number|null][], stamp=true):Uint8Array{ + const b=new Uint8Array(32+rows.length*12),w=wire(b);b.set([44,1,stamp?1:0,0]);w.setUint32(4,rows.length,true); + for(const [i,value]of [7,20,-5,30].entries())w.setInt32(8+i*4,value,true); + rows.forEach(([kind,hidden,layer],i)=>{const at=32+i*12;w.setUint32(at,kind,true);w.setUint32(at+4,(hidden?1:0)|(layer===null?0:2),true);if(layer!==null)w.setInt32(at+8,layer,true);});return b; +} +function entries(b:Uint8Array):number[][]{const r=children(b),w=wire(r);return Array.from({length:w.getUint32(4,true)},(_,i)=>[w.getUint32(8+i*8,true),w.getUint32(12+i*8,true)]);} +test("direct paint ordering retains hidden visits, exact signed layers and visible source segments",()=>{ + const b=childPacket([[31,false,2147483647],[23,false,null],[31,true,-2147483648],[31,false,null],[40,false,null],[19,false,null],[31,false,null]]); + assert.deepEqual(entries(b),[[2,1],[1,2],[3,2],[6,3],[5,2],[4,2],[0,1]]); + b[2]=0;assert.deepEqual(entries(b).map(e=>e[1]),[0,0,0,0,0,0,0]); + assert.deepEqual(entries(childPacket([])),[]);assert.deepEqual(entries(childPacket([[31,true,null],[31,true,null]])),[[0,0],[1,0]]); +}); +test("all kind and flag recipes are bounded, owned and reject malformed packets",()=>{ + for(let kind=0;kind<63;kind++)for(const retained of [false,true])for(let flags=0;flags<2048;flags++){ + const b=packet(kind,retained,flags,flags%3),saved=b.slice(),r=recipe(b),copy=r.slice();assert.deepEqual(b,saved);b.fill(255);assert.deepEqual(r,copy);assert.ok(wire(r).getUint32(8,true)<=6); + } + for(const at of [0,1,2,3,5,12,20,24,31]){const b=packet(31,true);b[at]=255;assert.throws(()=>recipe(b),/invalid/);} + for(const at of [0,1,2,3,4,24,32,36,40]){const b=childPacket([[31,false,null]]);b[at]=255;assert.throws(()=>children(b),/invalid/);} + const b=childPacket([[31,false,null],[31,false,null]]),saved=b.slice(),r=children(b),copy=r.slice();assert.deepEqual(b,saved);b.fill(255);assert.deepEqual(r,copy); +}); diff --git a/packages/core/test/view_frontend.test.ts b/packages/core/test/view_frontend.test.ts index c45f150d3..5947653a7 100644 --- a/packages/core/test/view_frontend.test.ts +++ b/packages/core/test/view_frontend.test.ts @@ -194,7 +194,7 @@ test("the view frontend and compiled portable component bundle typecheck", () => }; // Production joins these policies in one module before compiling the view. const bundlePath = new URL("../src/view_components_typecheck.ts", import.meta.url).pathname; - const bundle = ["view_components.ts", "runtime_policy.ts", "control_appearance.ts", "component_construction.ts", "widget_motion.ts", "component_composition.ts", "code_content.ts", "chart_content.ts", "markdown_content.ts", "widget_audits.ts", "widget_routing.ts", "widget_changes.ts", "widget_paint.ts", "widget_presentation.ts", "widget_paint_walk.ts", "control_geometry.ts", "control_content.ts", "surface_recipes.ts", "widget_metrics.ts", "intrinsic_measure.ts", "measurement_coordination.ts", "flow_measurement.ts", "layout_coordination.ts", "stream_policy.ts"] + const bundle = ["view_components.ts", "runtime_policy.ts", "control_appearance.ts", "component_construction.ts", "widget_motion.ts", "component_composition.ts", "code_content.ts", "chart_content.ts", "markdown_content.ts", "widget_audits.ts", "widget_routing.ts", "widget_changes.ts", "widget_paint.ts", "widget_presentation.ts", "widget_paint_walk.ts", "control_geometry.ts", "control_content.ts", "surface_recipes.ts", "widget_metrics.ts", "intrinsic_measure.ts", "measurement_coordination.ts", "flow_measurement.ts", "layout_coordination.ts", "render_coordination.ts", "stream_policy.ts"] .map(name => readFileSync(new URL(`../src/${name}`, import.meta.url), "utf8")).join("\n"); const host = ts.createCompilerHost(options); const readSource = host.getSourceFile.bind(host); diff --git a/src/primitives/canvas/render_coordination_policy.zig b/src/primitives/canvas/render_coordination_policy.zig new file mode 100644 index 000000000..58c33f1c5 --- /dev/null +++ b/src/primitives/canvas/render_coordination_policy.zig @@ -0,0 +1,140 @@ +//! Copied paint recipes and sibling programs. Display-list storage stays native. +const std = @import("std"); +const widgets = @import("widgets.zig"); +const tokens = @import("tokens.zig"); +const widget_access = @import("widget_access.zig"); +pub const reference = @import("widget_tree.zig"); +pub const Policy = @import("surface_layout_policy.zig").Policy; +pub const Mode = enum(u8) { tree, retained }; +pub const Status = enum(u32) { ready, hidden, depth }; +pub const Opcode = enum(u32) { backdrop, draw, scrim, children, separators, scrollbars, reactions }; +pub const Draw = enum(u32) { + container, + data_row, + tabs, + alert, + card, + dialog, + drawer, + sheet, + accordion, + bubble, + panel, + popover, + menu, + text, + code_paragraph, + spans, + icon, + image, + media, + terminal, + avatar, + badge, + button, + icon_button, + select, + text_field, + code_editor, + search, + tooltip, + menu_item, + list_item, + data_cell, + status, + segmented, + checkbox, + radio, + toggle, + slider, + progress, + separator, + split_divider, + skeleton, + spinner, + chart, + input_group, +}; +pub const Command = struct { opcode: Opcode, arg: u32 }; +fn word(bytes: []u8, at: usize, value: u32) void { + std.mem.writeInt(u32, bytes[at..][0..4], value, .little); +} +fn read(bytes: []const u8, at: usize) u32 { + return std.mem.readInt(u32, bytes[at..][0..4], .little); +} +pub const Recipe = struct { + status: Status, + count: usize, + commands: [6]Command, + pub fn init(policy: Policy, mode: Mode, widget: widgets.Widget, t: tokens.DesignTokens, depth: usize, disclosure: u8, code_paragraph: bool) Recipe { + var request: [32]u8 = @splat(0); + request[0..4].* = .{ 43, 1, @intFromEnum(mode), disclosure }; + var flags: u32 = 0; + for ([_]bool{ widget.layout.clip_content, widget.layout.virtualized, widget.spans.len > 0, code_paragraph, widget.runtime_flags.code_editor, widget.runtime_flags.native_scroll, widget_access.booleanControlSelected(widget), widget.children.len > 0, widget.semantics.hidden, t.controls.button_group_style == .detached, widget.scrim }, 0..) |value, i| + flags |= @as(u32, @intFromBool(value)) << @intCast(i); + word(&request, 4, flags); + std.mem.writeInt(u64, request[8..16], depth, .little); + word(&request, 16, @bitCast(widget.layout.gap)); + word(&request, 20, @intFromEnum(widget.kind)); + var result: [64]u8 = @splat(0xa5); + if (policy(&request, &result) != 64 or read(&result, 0) != 1 or read(&result, 4) > 2 or read(&result, 8) > 6 or read(&result, 12) != 0) + @panic("invalid render recipe result"); + const count = read(&result, 8); + if (read(&result, 4) != 0 and count != 0) @panic("invalid inactive render recipe"); + if (!std.mem.allEqual(u8, result[16 + count * 8 ..], 0)) @panic("invalid render recipe tail"); + var recipe: Recipe = .{ .status = @enumFromInt(read(&result, 4)), .count = count, .commands = undefined }; + for (recipe.commands[0..count], 0..) |*command, i| { + const op = read(&result, 16 + i * 8); + const arg = read(&result, 20 + i * 8); + if (op > @intFromEnum(Opcode.reactions)) @panic("invalid render capability"); + const valid = switch (@as(Opcode, @enumFromInt(op))) { + .draw => arg <= @intFromEnum(Draw.input_group), + .children => arg & ~@as(u32, 0x303) == 0, + .separators, .scrollbars => arg <= 1, + else => arg == 0, + }; + if (!valid) @panic("invalid render capability arguments"); + command.* = .{ .opcode = @enumFromInt(op), .arg = arg }; + } + return recipe; + } +}; +pub const ChildPlan = struct { + allocator: std.mem.Allocator, + result: []u8, + count: usize, + pub fn init(allocator: std.mem.Allocator, policy: Policy, children: []const widgets.Widget, t: tokens.DesignTokens, stamp: bool) !ChildPlan { + const count = std.math.cast(u32, children.len) orelse @panic("render child count exceeds wire range"); + const request = try allocator.alloc(u8, try std.math.add(usize, 32, try std.math.mul(usize, children.len, 12))); + defer allocator.free(request); + const result = try allocator.alloc(u8, try std.math.add(usize, 8, try std.math.mul(usize, children.len, 8))); + errdefer allocator.free(result); + @memset(request, 0); + request[0..4].* = .{ 44, 1, @intFromBool(stamp), 0 }; + word(request, 4, count); + for ([_]i32{ t.layer.base, t.layer.modal, t.layer.overlay, t.layer.floating }, 0..) |layer, i| word(request, 8 + i * 4, @bitCast(layer)); + for (children, 0..) |child, i| { + const at = 32 + i * 12; + word(request, at, @intFromEnum(child.kind)); + word(request, at + 4, @as(u32, @intFromBool(child.semantics.hidden)) | (@as(u32, @intFromBool(child.layer != null)) << 1)); + if (child.layer) |layer| word(request, at + 8, @bitCast(layer)); + } + @memset(result, 0xa5); + if (policy(request, result) != result.len or read(result, 0) != 1 or read(result, 4) != count) @panic("invalid render child result"); + const plan: ChildPlan = .{ .allocator = allocator, .result = result, .count = count }; + for (0..count) |i| { + if (plan.index(i) >= count or read(result, 12 + i * 8) > 3) @panic("invalid render child entry"); + for (0..i) |j| if (plan.index(i) == plan.index(j)) @panic("duplicate render child entry"); + } + return plan; + } + pub fn deinit(self: ChildPlan) void { + self.allocator.free(self.result); + } + pub fn index(self: ChildPlan, ordinal: usize) usize { + return read(self.result, 8 + ordinal * 8); + } + pub fn segment(self: ChildPlan, ordinal: usize) widgets.WidgetGroupSegment { + return @enumFromInt(read(self.result, 12 + ordinal * 8)); + } +}; diff --git a/src/primitives/canvas/root.zig b/src/primitives/canvas/root.zig index 3a89cb7ea..bfce6afa2 100644 --- a/src/primitives/canvas/root.zig +++ b/src/primitives/canvas/root.zig @@ -915,6 +915,7 @@ pub const intrinsic_measure_policy = @import("intrinsic_measure_policy.zig"); pub const measurement_coordination_policy = @import("measurement_coordination_policy.zig"); pub const flow_measurement_policy = @import("flow_measurement_policy.zig"); pub const layout_coordination_policy = @import("layout_coordination_policy.zig"); +pub const render_coordination_policy = @import("render_coordination_policy.zig"); pub const widget_audit_policy = @import("widget_audit_policy.zig"); pub const a11y = @import("a11y_audit.zig"); pub const expectA11yAuditSweepClean = a11y.expectA11yAuditSweepClean; diff --git a/src/primitives/canvas/tokens.zig b/src/primitives/canvas/tokens.zig index beedca9ba..a33f60958 100644 --- a/src/primitives/canvas/tokens.zig +++ b/src/primitives/canvas/tokens.zig @@ -1915,6 +1915,8 @@ pub const DesignTokens = struct { flow_measurement_policy: ?@import("flow_measurement_policy.zig").Policy = null, /// Portable layout admission, ordered child plans and retained relayout. layout_coordination_policy: ?@import("layout_coordination_policy.zig").Policy = null, + /// Portable paint capability scheduling and direct-tree sibling programs. + render_coordination_policy: ?@import("render_coordination_policy.zig").Policy = null, intrinsic_layout_policy: ?@import("intrinsic_layout_policy.zig").Policy = null, /// Widget motion plans supplied by the compiled app, outside theme overrides. widget_motion_policy: ?*const fn ([]const u8, []u8) usize = null, diff --git a/src/primitives/canvas/widget_render.zig b/src/primitives/canvas/widget_render.zig index 4919b7d31..488feecc2 100644 --- a/src/primitives/canvas/widget_render.zig +++ b/src/primitives/canvas/widget_render.zig @@ -25,6 +25,7 @@ const icon_model = @import("icons.zig"); const chart_model = @import("chart.zig"); const presentation_policy = @import("widget_presentation_policy.zig"); const paint_walk_policy = @import("widget_paint_walk_policy.zig"); +const emission = @import("render_coordination_policy.zig"); const Error = canvas.Error; const ObjectId = canvas.ObjectId; @@ -492,8 +493,20 @@ fn emitWidgetLayoutWindowSurfaces(builder: *Builder, layout: anytype, tokens: De } fn emitWidgetDepth(builder: *Builder, widget: Widget, tokens: DesignTokens, depth: usize) Error!void { - if (depth >= max_widget_depth) return error.WidgetDepthExceeded; - if (widget.semantics.hidden) return; + const recipe = if (tokens.render_coordination_policy) |policy| + emission.Recipe.init(policy, .tree, widget, tokens, depth, 0, isSyntaxCodeParagraph(widget)) + else + null; + if (recipe) |plan| { + switch (plan.status) { + .depth => return error.WidgetDepthExceeded, + .hidden => return, + .ready => {}, + } + } else { + if (depth >= max_widget_depth) return error.WidgetDepthExceeded; + if (widget.semantics.hidden) return; + } const opacity = widgetOpacity(widget); if (opacity <= 0) return; @@ -510,16 +523,17 @@ fn emitWidgetDepth(builder: *Builder, widget: Widget, tokens: DesignTokens, dept else null; try builder.transform(transform); - try emitWidgetDepthContent(builder, widget, tokens, depth); + try emitWidgetDepthContent(builder, widget, tokens, depth, recipe); try builder.transform(inverse_transform); } else { - try emitWidgetDepthContent(builder, widget, tokens, depth); + try emitWidgetDepthContent(builder, widget, tokens, depth, recipe); } if (wrap_opacity) try builder.popOpacity(); } -fn emitWidgetDepthContent(builder: *Builder, widget: Widget, tokens: DesignTokens, depth: usize) Error!void { +fn emitWidgetDepthContent(builder: *Builder, widget: Widget, tokens: DesignTokens, depth: usize, recipe: ?emission.Recipe) Error!void { const paint_widget = widgetWithFrame(widget, pixelSnapGeometryRect(tokens, widget.frame)); + if (recipe) |plan| return emitTreeRecipe(builder, paint_widget, tokens, depth, plan); try emitWidgetBackdropBlur(builder, paint_widget, tokens); switch (paint_widget.kind) { .stack, .row, .column => { @@ -681,6 +695,7 @@ fn emitDataCellContent(builder: *Builder, widget: Widget, tokens: DesignTokens) } fn emitWidgetChildren(builder: *Builder, children: []const Widget, tokens: DesignTokens, depth: usize) Error!void { + if (tokens.render_coordination_policy) |policy| return emitCompiledChildren(builder, children, tokens, depth, policy, false); var emitted: usize = 0; var previous: ?WidgetPaintOrder = null; while (emitted < children.len) : (emitted += 1) { @@ -876,6 +891,12 @@ fn emitWidgetLayoutNodeContent( widget: Widget, ) Error!void { const paint_widget = widgetWithFrame(widget, pixelSnapGeometryRect(tokens, widget.frame)); + if (tokens.render_coordination_policy) |policy| { + const disclosure: u8 = if (widget.kind == .accordion) @intFromEnum(accordionLayoutDisclosure(layout, node_index, paint_widget, state)) else 0; + const recipe = emission.Recipe.init(policy, .retained, paint_widget, tokens, 0, disclosure, isSyntaxCodeParagraph(paint_widget)); + if (recipe.status != .ready) @panic("inactive retained render recipe"); + return emitRetainedRecipe(builder, layout, node_index, tokens, state, paint_widget, recipe); + } try emitWidgetBackdropBlur(builder, paint_widget, tokens); switch (paint_widget.kind) { .stack, .row, .column => try emitLayoutContainerBackground(builder, paint_widget, tokens), @@ -1032,6 +1053,142 @@ fn emitWidgetLayoutNodeContent( try emitWidgetLayoutClippedChildren(builder, layout, node_index, tokens, state, paint_widget); } +// These executors provide explicit drawing capabilities. Kind routing, +// ordered composition, clip selection and scrollbar admission live in the +// copied program; the independent native reference above remains selectable. +fn emitPlannedDraw(builder: *Builder, widget: Widget, tokens: DesignTokens, draw: emission.Draw, visible: ?geometry.RectF, logical_focus: bool, group_focus: bool) Error!void { + switch (draw) { + .container => try emitLayoutContainerBackground(builder, widget, tokens), + .data_row => try emitDataRowWidgetWash(builder, widget, tokens), + .tabs => try widget_render_surfaces.emitTabsListWidgetChrome(builder, widget, tokens), + .alert => try widget_render_surfaces.emitAlertWidgetChrome(builder, widget, tokens), + .card => try widget_render_surfaces.emitCardWidgetChrome(builder, widget, tokens), + .dialog => try widget_render_surfaces.emitDialogSurfaceWidgetChrome(builder, widget, tokens), + .drawer => try widget_render_surfaces.emitDrawerSurfaceWidgetChrome(builder, widget, tokens), + .sheet => try widget_render_surfaces.emitSheetSurfaceWidgetChrome(builder, widget, tokens), + .accordion => try widget_render_surfaces.emitAccordionWidgetChrome(builder, widget, tokens), + .bubble => try widget_render_surfaces.emitBubbleWidgetChrome(builder, widget, tokens), + .panel => try widget_render_surfaces.emitPanelWidgetChrome(builder, widget, tokens), + .popover => try widget_render_surfaces.emitPopoverWidgetChrome(builder, widget, tokens), + .menu => try widget_render_surfaces.emitMenuSurfaceWidgetChrome(builder, widget, tokens), + .text => try emitTextWidget(builder, widget, tokens), + .code_paragraph => if (visible) |bounds| { + try emitVisibleCodeTextSpansWidget(builder, widget, tokens, bounds, .{}); + }, + .spans => if (visible) |bounds| { + try emitVisibleTextSpansWidget(builder, widget, tokens, bounds); + }, + .icon => try emitIconWidget(builder, widget, tokens), + .image => try emitImageWidget(builder, widget), + .media => try emitMediaSurfaceWidget(builder, widget), + .terminal => try emitTerminalWidget(builder, widget, tokens, logical_focus), + .avatar => try emitAvatarWidget(builder, widget, tokens), + .badge => try emitBadgeWidget(builder, widget, tokens), + .button => try widget_render_controls.emitButtonWidget(builder, widget, tokens), + .icon_button => try widget_render_controls.emitIconButtonWidget(builder, widget, tokens), + .select => try widget_render_controls.emitSelectWidget(builder, widget, tokens), + .text_field => try widget_render_controls.emitTextFieldWidget(builder, widget, tokens), + .code_editor => try emitCodeEditorWidget(builder, widget, tokens), + .search => try widget_render_controls.emitSearchFieldWidget(builder, widget, tokens), + .tooltip => try widget_render_controls.emitTooltipWidget(builder, widget, tokens), + .menu_item => try widget_render_controls.emitMenuItemWidget(builder, widget, tokens), + .list_item => try widget_render_controls.emitListItemWidget(builder, widget, tokens), + .data_cell => try emitDataCellContent(builder, widget, tokens), + .status => try emitStatusBarWidget(builder, widget, tokens), + .segmented => try widget_render_controls.emitSegmentedControlWidget(builder, widget, tokens), + .checkbox => try widget_render_controls.emitCheckboxWidget(builder, widget, tokens), + .radio => try widget_render_controls.emitRadioWidget(builder, widget, tokens), + .toggle => try widget_render_controls.emitToggleWidget(builder, widget, tokens), + .slider => try widget_render_controls.emitSliderWidget(builder, widget, tokens), + .progress => try widget_render_controls.emitProgressWidget(builder, widget, tokens), + .separator => try emitSeparatorWidget(builder, widget, tokens), + .split_divider => try emitSplitDividerWidget(builder, widget, tokens), + .skeleton => try emitSkeletonWidget(builder, widget, tokens), + .spinner => try emitSpinnerWidget(builder, widget, tokens), + .chart => try emitChartWidget(builder, widget, tokens), + .input_group => { + var group = widget; + group.state.focused = group_focus; + try widget_render_controls.emitInputGroupWidget(builder, group, tokens); + }, + } +} + +fn plannedClip(widget: Widget, tokens: DesignTokens, arg: u32) ?Clip { + return switch (arg & 3) { + 0 => null, + 1 => widgetContentClip(widget, tokens), + 2 => .{ .id = widgetPartId(widget.id, 1), .rect = widget.frame }, + 3 => .{ .id = widgetPartId(widget.id, 9), .rect = widget.frame }, + else => unreachable, + }; +} +fn emitCompiledChildren(builder: *Builder, children: []const Widget, tokens: DesignTokens, depth: usize, policy: emission.Policy, stamp: bool) Error!void { + const plan = emission.ChildPlan.init(std.heap.page_allocator, policy, children, tokens, stamp) catch @panic("render child allocation failed"); + defer plan.deinit(); + for (0..plan.count) |i| { + var child = children[plan.index(i)]; + if (stamp) child.group_segment = plan.segment(i); + try emitWidgetDepth(builder, child, tokens, depth + 1); + } +} +fn emitTreeRecipe(builder: *Builder, widget: Widget, tokens: DesignTokens, depth: usize, recipe: emission.Recipe) Error!void { + for (recipe.commands[0..recipe.count]) |command| switch (command.opcode) { + .backdrop => try emitWidgetBackdropBlur(builder, widget, tokens), + .scrim => try emitModalSurfaceScrim(builder, widget, tokens), + .draw => { + const draw: emission.Draw = @enumFromInt(command.arg); + const visible = if (draw == .spans or draw == .code_paragraph) visibleBoundsInsideClip(tree_visible_bounds, widget.frame) else null; + const group_focus = if (draw == .input_group) widget.state.focused or widgetSubtreeHasFocusedState(widget) else widget.state.focused; + try emitPlannedDraw(builder, widget, tokens, draw, visible, widget.state.focused, group_focus); + }, + .children => { + const child_tokens = if (command.arg & 256 != 0) widget_render_surfaces.bubbleContentTokens(widget, tokens) else tokens; + const saved_visible = tree_visible_bounds; + defer tree_visible_bounds = saved_visible; + const clip = plannedClip(widget, child_tokens, command.arg); + if (clip) |value| { + tree_visible_bounds = visibleBoundsInsideClip(saved_visible, value.rect); + try builder.pushClip(value); + } + try emitCompiledChildren(builder, widget.children, child_tokens, depth, tokens.render_coordination_policy.?, command.arg & 512 != 0); + if (clip != null) try builder.popClip(); + }, + .separators => try emitTableRowSeparators(builder, widget.children, tokens), + .scrollbars => try widget_render_scroll.emitScrollViewScrollbars(builder, widget.frame, widget_render_scroll.widgetScrollAxisMetricsForWidget(widget, tokens, .vertical), if (command.arg == 1) widget_render_scroll.widgetScrollAxisMetricsForWidget(widget, tokens, .horizontal) else .{}, tokens, widget.id), + .reactions => try widget_render_surfaces.emitBubbleWidgetReactions(builder, widget, tokens), + }; +} +fn emitRetainedRecipe(builder: *Builder, layout: anytype, node_index: usize, tokens: DesignTokens, state: WidgetRenderState, widget: Widget, recipe: emission.Recipe) Error!void { + for (recipe.commands[0..recipe.count]) |command| switch (command.opcode) { + .backdrop => try emitWidgetBackdropBlur(builder, widget, tokens), + .scrim => try emitModalSurfaceScrim(builder, widget, tokens), + .draw => { + const draw: emission.Draw = @enumFromInt(command.arg); + const visible = if (draw == .spans or draw == .code_paragraph) widgetLayoutNodeVisibleBounds(layout, node_index, widget.frame, state) else null; + const logical_focus = if (draw == .terminal) if (activePaintWalk(layout)) |plan| plan.activeLane(node_index, painting_drag_copy).logical_focus else widgetHasLogicalFocus(widget, state) else widget.state.focused; + var group_focus = widget.state.focused; + if (draw == .input_group) { + if (activePaintWalk(layout)) |plan| group_focus = plan.activeLane(node_index, painting_drag_copy).group_focus else if (!group_focus) group_focus = if (state.focused_id != null or state.focus_visible_id != null) + layoutSubtreeHasFocusVisible(layout, node_index, state) + else + layoutSubtreeHasBakedFocus(layout, node_index); + } + try emitPlannedDraw(builder, widget, tokens, draw, visible, logical_focus, group_focus); + }, + .children => { + const child_tokens = if (command.arg & 256 != 0) widget_render_surfaces.bubbleContentTokens(widget, tokens) else tokens; + const clip = plannedClip(widget, child_tokens, command.arg); + if (clip) |value| try builder.pushClip(value); + try emitWidgetLayoutChildren(builder, layout, node_index, child_tokens, state); + if (clip != null) try builder.popClip(); + }, + .separators => try emitTableRowSeparatorsLayout(builder, layout, node_index, tokens, widget, command.arg == 1), + .scrollbars => try widget_render_scroll.emitScrollViewScrollbars(builder, widget.frame, widgetScrollAxisMetrics(layout, node_index, .vertical), if (command.arg == 1) widgetScrollAxisMetrics(layout, node_index, .horizontal) else .{}, tokens, widget.id), + .reactions => try widget_render_surfaces.emitBubbleWidgetReactions(builder, widget, tokens), + }; +} + /// Flow and stacking containers have no implicit surface treatment. An /// actionable container, however, wears the same neutral hover/pressed /// ladder as a list row over its full hit frame; an authored background is diff --git a/src/runtime/ts_ui_app.zig b/src/runtime/ts_ui_app.zig index 9f4c15b91..0f4b21a21 100644 --- a/src/runtime/ts_ui_app.zig +++ b/src/runtime/ts_ui_app.zig @@ -466,6 +466,8 @@ pub fn TsUiAppWithFeatures(comptime core: type, comptime features: ui_app.UiAppF stamped.flow_measurement_policy = core.nativeWindowPolicy; if (options.layout_coordination_policy != null) @panic("TsUiApp owns layout_coordination_policy - remove custom layout coordination wiring"); stamped.layout_coordination_policy = core.nativeWindowPolicy; + if (options.render_coordination_policy != null) @panic("TsUiApp owns render_coordination_policy - remove custom render coordination wiring"); + stamped.render_coordination_policy = core.nativeWindowPolicy; if (options.widget_motion_policy != null) @panic("TsUiApp owns widget_motion_policy - remove custom motion wiring"); stamped.widget_motion_policy = core.nativeWindowPolicy; if (options.widget_audit_policy != null) @panic("TsUiApp owns widget_audit_policy - remove custom audit wiring"); diff --git a/src/runtime/ui_app.zig b/src/runtime/ui_app.zig index d3327f6f5..dfda7da48 100644 --- a/src/runtime/ui_app.zig +++ b/src/runtime/ui_app.zig @@ -527,6 +527,7 @@ pub fn UiAppWithFeatures(comptime ModelT: type, comptime MsgT: type, comptime fe measurement_coordination_policy: ?*const fn ([]const u8, []u8) usize = null, flow_measurement_policy: ?*const fn ([]const u8, []u8) usize = null, layout_coordination_policy: ?*const fn ([]const u8, []u8) usize = null, + render_coordination_policy: ?*const fn ([]const u8, []u8) usize = null, intrinsic_layout_policy: ?*const fn ([]const u8, []u8) usize = null, widget_motion_policy: ?*const fn ([]const u8, []u8) usize = null, widget_audit_policy: ?*const fn ([]const u8, []u8) usize = null, @@ -2028,6 +2029,7 @@ pub fn UiAppWithFeatures(comptime ModelT: type, comptime MsgT: type, comptime fe tokens.measurement_coordination_policy = self.options.measurement_coordination_policy; tokens.flow_measurement_policy = self.options.flow_measurement_policy; tokens.layout_coordination_policy = self.options.layout_coordination_policy; + tokens.render_coordination_policy = self.options.render_coordination_policy; tokens.widget_motion_policy = self.options.widget_motion_policy; tokens.widget_audit_policy = self.options.widget_audit_policy; tokens.widget_routing_policy = self.options.widget_routing_policy; @@ -2089,6 +2091,7 @@ pub fn UiAppWithFeatures(comptime ModelT: type, comptime MsgT: type, comptime fe tokens.measurement_coordination_policy = self.options.measurement_coordination_policy; tokens.flow_measurement_policy = self.options.flow_measurement_policy; tokens.layout_coordination_policy = self.options.layout_coordination_policy; + tokens.render_coordination_policy = self.options.render_coordination_policy; tokens.widget_motion_policy = self.options.widget_motion_policy; tokens.widget_audit_policy = self.options.widget_audit_policy; tokens.widget_routing_policy = self.options.widget_routing_policy; diff --git a/tests/ts-core/render_coordination_e2e_tests.zig b/tests/ts-core/render_coordination_e2e_tests.zig new file mode 100644 index 000000000..6ba128d19 --- /dev/null +++ b/tests/ts-core/render_coordination_e2e_tests.zig @@ -0,0 +1,160 @@ +//! Complete display lists through the production scriptc policy ABI. +const std = @import("std"); +const sdk = @import("native_sdk"); +const core = @import("ts_persist_core"); +const c = sdk.canvas; +const p = c.render_coordination_policy; +const exact = @import("component_construction_e2e_tests.zig").exact; +fn owned(t: c.DesignTokens) c.DesignTokens { + var result = t; + result.render_coordination_policy = core.nativeWindowPolicy; + return result; +} +fn compare(widget: c.Widget, t: c.DesignTokens, state: c.WidgetRenderState, retained: bool, capacity: usize) !void { + var native_commands: [2048]c.CanvasCommand = undefined; + var compiled_commands: [2048]c.CanvasCommand = undefined; + var native = c.Builder.init(native_commands[0..capacity]); + var compiled = c.Builder.init(compiled_commands[0..capacity]); + var nodes: [128]c.WidgetLayoutNode = undefined; + var len: usize = 0; + append(widget, null, 0, &nodes, &len); + const tree: c.WidgetLayoutTree = .{ .nodes = nodes[0..len], .root_bounds = .init(0, 0, 360, 240) }; + const native_error: ?anyerror = if (retained) blk: { + tree.emitDisplayListWithState(&native, t, state) catch |err| break :blk err; + break :blk null; + } else blk: { + c.emitWidgetTree(&native, widget, t) catch |err| break :blk err; + break :blk null; + }; + const compiled_error: ?anyerror = if (retained) blk: { + tree.emitDisplayListWithState(&compiled, owned(t), state) catch |err| break :blk err; + break :blk null; + } else blk: { + c.emitWidgetTree(&compiled, widget, owned(t)) catch |err| break :blk err; + break :blk null; + }; + try std.testing.expectEqual(native_error, compiled_error); + // Compare every initialized command and all retained slices, including + // complete prefixes when a capability exhausts the builder's capacity. + try exact(native.displayList().commands, compiled.displayList().commands); + core.rt.frameReset(); + try exact(native.displayList().commands, compiled.displayList().commands); +} +fn append(widget: c.Widget, parent: ?usize, depth: usize, nodes: []c.WidgetLayoutNode, len: *usize) void { + const index = len.*; + nodes[index] = .{ .widget = widget, .frame = widget.frame, .depth = depth, .parent_index = parent }; + len.* += 1; + for (widget.children) |child| append(child, index, depth + 1, nodes, len); +} +test "compiled widget metric render recipes preserve every kind complete tree and retained commands" { + _ = core.initialModel(); + const children = [_]c.Widget{ + .{ .id = 2, .kind = .button, .text = "Child\xff\x00", .frame = .init(13.25, 41.5, 70.125, 33.75), .state = .{ .focused = true }, .layer = 2 }, + .{ .id = 3, .kind = .text, .text = "Second", .frame = .init(97.25, 45.5, 110.125, 25.75), .layer = -2 }, + }; + const spans = [_]c.TextSpan{ .{ .text = "Rich\xff\x00 ", .scale = 1.125 }, .{ .text = "bytes", .monospace = true } }; + for (std.enums.values(c.WidgetKind)) |kind| for (0..32) |flags| for ([_]c.ThemePack{ .house, .geist }) |pack| { + var widget: c.Widget = .{ .id = 1, .kind = kind, .text = "Root\xff\x00", .icon = "check", .children = &children, .frame = .init(3.125, 5.25, 250.5, 130.75), .value = 0.37, .backdrop_blur = 1.25, .layout = .{ .clip_content = flags & 1 != 0, .virtualized = flags & 2 != 0, .gap = if (flags & 4 != 0) 3.25 else 0 }, .state = .{ .selected = flags & 4 != 0, .hovered = true, .pressed = flags & 8 != 0 }, .runtime_flags = .{ .native_scroll = flags & 8 != 0, .code_editor = flags & 16 != 0 } }; + if ((kind == .text or kind == .data_cell) and flags & 16 != 0) widget.spans = &spans; + var t = c.DesignTokens.theme(.{ .pack = pack }); + t.controls.button_group_style = if (flags & 16 != 0) .detached else .segmented; + for ([_]bool{ false, true }) |retained| compare(widget, t, .{}, retained, 2048) catch |err| { + std.debug.print("paint kind {t} flags {d} pack {t} retained {}\n", .{ kind, flags, pack, retained }); + return err; + }; + }; +} +test "compiled widget metric render recipes preserve nested cascades segment stamps disclosure and focus chrome" { + _ = core.initialModel(); + const buttons = [_]c.Widget{ + .{ .id = 4, .kind = .button, .text = "First", .frame = .init(22, 90, 70, 32), .layer = 8 }, + .{ .id = 5, .kind = .button, .text = "Hidden", .frame = .init(92, 90, 70, 32), .semantics = .{ .hidden = true } }, + .{ .id = 6, .kind = .button, .text = "Last", .frame = .init(92, 90, 70, 32), .state = .{ .focused = true }, .layer = -8 }, + }; + const groups = [_]c.Widget{.{ .id = 3, .kind = .button_group, .frame = .init(15, 85, 200, 45), .children = &buttons, .layout = .{ .clip_content = true } }}; + for ([_]bool{ false, true }) |selected| for ([_]f32{ 50, 150 }) |height| for ([_]bool{ false, true }) |revealing| { + const disclosures = [_]c.Widget{.{ .id = 2, .kind = .accordion, .text = "Details", .frame = .init(10, 35, 250, height), .children = &groups, .state = .{ .selected = selected }, .layout = .{ .clip_content = true } }}; + const widget: c.Widget = .{ .id = 1, .kind = .bubble, .frame = .init(0, 0, 300, 230), .children = &disclosures, .state = .{ .selected = true }, .layout = .{ .clip_content = true }, .text = "Cascade" }; + const state: c.WidgetRenderState = .{ .focus_visible_id = 6, .focused_id = 6, .revealing_disclosure_ids = if (revealing) &.{2} else &.{} }; + for ([_]bool{ false, true }) |retained| try compare(widget, .{}, state, retained, 2048); + }; +} +test "compiled widget metric render recipes preserve numeric accordion disclosure and capacity prefixes" { + _ = core.initialModel(); + const children = [_]c.Widget{.{ .id = 2, .kind = .text, .text = "Numeric disclosure child", .frame = .init(12, 48, 180, 28) }}; + for ([_]bool{ false, true }) |selected| for ([_]f32{ 0, 0.49999997, 0.5, 1, std.math.inf(f32), -std.math.inf(f32), std.math.nan(f32) }) |value| { + const widget: c.Widget = .{ .id = 1, .kind = .accordion, .text = "Details", .value = value, .state = .{ .selected = selected }, .frame = .init(0, 0, 250, 140), .children = &children, .layout = .{ .clip_content = true } }; + for ([_]bool{ false, true }) |retained| for ([_]usize{ 0, 1, 2, 3, 8, 32, 2048 }) |capacity| try compare(widget, .{}, .{}, retained, capacity); + }; +} +test "compiled widget metric render recipes preserve complete capacity failure prefixes" { + _ = core.initialModel(); + const children = [_]c.Widget{.{ .id = 2, .kind = .button, .text = "Child", .frame = .init(10, 45, 100, 30) }}; + for ([_]c.WidgetKind{ .bubble, .dialog, .accordion, .button_group, .table, .scroll_view, .input_group, .spinner, .text }) |kind| for (0..20) |capacity| { + const widget: c.Widget = .{ .id = 1, .kind = kind, .frame = .init(0, 0, 250, 140), .children = &children, .text = "Prefix", .backdrop_blur = 2, .layout = .{ .virtualized = true, .clip_content = true }, .state = .{ .selected = true }, .opacity = 0.5, .transform = .{ .tx = 2.25, .ty = 3.5 } }; + for ([_]bool{ false, true }) |retained| try compare(widget, .{}, .{}, retained, capacity); + }; +} +test "compiled widget metric direct render recipes preserve depth before hidden suppression" { + _ = core.initialModel(); + var widgets: [36]c.Widget = undefined; + for (&widgets, 0..) |*widget, i| widget.* = .{ .id = i + 1, .kind = .column, .frame = .init(0, 0, 100, 50) }; + for (0..widgets.len - 1) |i| widgets[i].children = widgets[i + 1 .. i + 2]; + for ([_]usize{ 30, 31, 32, 33 }) |hidden| { + widgets[hidden].semantics.hidden = true; + try compare(widgets[0], .{}, .{}, false, 2048); + widgets[hidden].semantics.hidden = false; + } + try compare(widgets[0], .{}, .{}, false, 2048); +} +test "compiled widget metric direct sibling programs preserve exact layers source ties segments and owned buffers" { + _ = core.initialModel(); + var children: [63]c.Widget = undefined; + for (std.enums.values(c.WidgetKind), 0..) |kind, i| children[i] = .{ .kind = kind, .id = i + 1, .semantics = .{ .hidden = i % 3 == 0 }, .layer = if (i % 4 == 0) std.math.minInt(i32) else if (i % 4 == 1) std.math.maxInt(i32) else null }; + var t: c.DesignTokens = .{}; + t.layer = .{ .base = 7, .modal = -20, .overlay = 7, .floating = 30 }; + const plan = try p.ChildPlan.init(std.testing.allocator, core.nativeWindowPolicy, &children, t, true); + defer plan.deinit(); + const copy = try std.testing.allocator.dupe(u8, plan.result); + defer std.testing.allocator.free(copy); + var previous: ?c.WidgetPaintOrder = null; + var visible: usize = 0; + for (children) |child| if (!child.semantics.hidden) { + visible += 1; + }; + for (0..children.len) |i| { + const index = p.reference.nextWidgetPaintChild(&children, t, previous).?; + try std.testing.expectEqual(index, plan.index(i)); + var ordinal: usize = 0; + if (!children[index].semantics.hidden) for (children[0..index]) |child| { + if (!child.semantics.hidden) ordinal += 1; + }; + const expected: @TypeOf(children[0].group_segment) = if (visible <= 1) .none else if (ordinal == 0) .first else if (ordinal == visible - 1) .last else .middle; + try std.testing.expectEqual(expected, plan.segment(i)); + previous = .{ .layer = p.reference.widgetPaintLayer(children[index], t), .index = index }; + _ = p.Recipe.init(core.nativeWindowPolicy, .tree, children[index], t, i, 0, false); + core.rt.frameReset(); + try std.testing.expectEqualSlices(u8, copy, plan.result); + } +} +test "compiled widget metric recipe buffers preserve caller tails and survive nested calls and arena resets" { + _ = core.initialModel(); + for ([_]u32{ 0, 0x80000000, 1, 0x7f800000, 0xff800000, 0x7fc12345, 0x7f812345 }) |gap| { + const widget: c.Widget = .{ .kind = .button_group, .layout = .{ .gap = @bitCast(gap) } }; + const recipe = p.Recipe.init(core.nativeWindowPolicy, .tree, widget, .{}, 0, 0, false); + _ = p.Recipe.init(core.nativeWindowPolicy, .retained, .{ .kind = .dialog }, .{}, 0, 0, false); + core.rt.frameReset(); + const stamp = @as(f32, @bitCast(gap)) <= 0; + try std.testing.expectEqual(@as(u32, if (stamp) 512 else 0), recipe.commands[1].arg); + } + var request: [32]u8 = @splat(0); + request[0..4].* = .{ 43, 1, 1, 0 }; + std.mem.writeInt(u32, request[20..24], @intFromEnum(c.WidgetKind.bubble), .little); + var result: [80]u8 = @splat(0xa5); + try std.testing.expectEqual(64, core.nativeWindowPolicy(&request, &result)); + const copy = result; + _ = p.Recipe.init(core.nativeWindowPolicy, .tree, .{ .kind = .text }, .{}, 0, 0, false); + core.rt.frameReset(); + try std.testing.expectEqualSlices(u8, ©, &result); + try std.testing.expect(std.mem.allEqual(u8, result[64..], 0xa5)); +} diff --git a/tests/ts-core/typography_e2e_tests.zig b/tests/ts-core/typography_e2e_tests.zig index 8d3d415e5..948565295 100644 --- a/tests/ts-core/typography_e2e_tests.zig +++ b/tests/ts-core/typography_e2e_tests.zig @@ -34,6 +34,7 @@ test "compiled model themes copy the complete register across frame and commit o reference.measurement_coordination_policy = snapshot.measurement_coordination_policy; reference.flow_measurement_policy = snapshot.flow_measurement_policy; reference.layout_coordination_policy = snapshot.layout_coordination_policy; + reference.render_coordination_policy = core.nativeWindowPolicy; reference.widget_motion_policy = core.nativeWindowPolicy; reference.widget_audit_policy = core.nativeWindowPolicy; reference.widget_routing_policy = core.nativeWindowPolicy; @@ -95,6 +96,7 @@ fn restamp(reference: *canvas.DesignTokens, actual: canvas.DesignTokens) void { reference.measurement_coordination_policy = actual.measurement_coordination_policy; reference.flow_measurement_policy = actual.flow_measurement_policy; reference.layout_coordination_policy = actual.layout_coordination_policy; + reference.render_coordination_policy = core.nativeWindowPolicy; reference.widget_motion_policy = core.nativeWindowPolicy; reference.widget_audit_policy = core.nativeWindowPolicy; reference.widget_routing_policy = core.nativeWindowPolicy; diff --git a/tests/ts-core/widget_metric_e2e_tests.zig b/tests/ts-core/widget_metric_e2e_tests.zig index 70145108a..0e692119d 100644 --- a/tests/ts-core/widget_metric_e2e_tests.zig +++ b/tests/ts-core/widget_metric_e2e_tests.zig @@ -7,6 +7,9 @@ const m = @import("native_sdk").canvas.widget_metric_policy; const reference = m.reference; const layout = c; const exact = @import("component_construction_e2e_tests.zig").exact; +test { + _ = @import("render_coordination_e2e_tests.zig"); +} fn owned(tokens: c.DesignTokens) c.DesignTokens { var t = tokens; t.control_geometry_policy = core.nativeWindowPolicy;