diff --git a/build.zig b/build.zig index d6c170711..b374457ad 100644 --- a/build.zig +++ b/build.zig @@ -5419,6 +5419,7 @@ fn externalCoreFixtureModule( stage_run.addFileInput(b.path("packages/core/src/widget_metrics.ts")); stage_run.addFileInput(b.path("packages/core/src/intrinsic_measure.ts")); 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/stream_policy.ts")); stage_run.addArg("--src"); stage_run.addDirectoryArg(spec.src_dir); diff --git a/build/app.zig b/build/app.zig index d5b7ca76d..ab577da44 100644 --- a/build/app.zig +++ b/build/app.zig @@ -1260,6 +1260,7 @@ fn tsCoreStage( stage_run.addFileInput(dep.path("packages/core/src/widget_metrics.ts")); stage_run.addFileInput(dep.path("packages/core/src/intrinsic_measure.ts")); 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/stream_policy.ts")); if (typescript_view) { stage_run.addArg("--view-markup"); diff --git a/packages/core/src/flow_measurement.ts b/packages/core/src/flow_measurement.ts new file mode 100644 index 000000000..c7e3f1ed7 --- /dev/null +++ b/packages/core/src/flow_measurement.ts @@ -0,0 +1,121 @@ +/** Portable grid admission and first-row measurement. Native retains copied + * requests, executes measurements, and traverses the resulting frames. */ +function nscvGridMeasure(request: Uint8Array): Uint8Array { + if (request.length < 128 || request[0] !== 38 || request[1] !== 1 || request[2]! > 15 || request[3]! > 1) + throw new Error("invalid grid measurement header"); + const w = new DataView(request.buffer, request.byteOffset, request.byteLength), count = w.getUint32(4, true); + if (request.length !== 128 + count * 64 || w.getUint32(8, true) !== 0 || w.getUint32(12, true) !== 0) + throw new Error("invalid grid measurement shape"); + for (let i = 92; i < 128; i++) if (request[i] !== 0) throw new Error("invalid grid measurement reserved bytes"); + const packet = request.slice(16, 92), p = new DataView(packet.buffer), virtual = request[3] === 1; + if (packet[0] !== 4 || packet[1] !== 0 || ((packet[2]! & 1) !== 0) !== virtual || p.getUint32(52, true) !== 0) + throw new Error("invalid grid measurement allocation"); + let flows = 0; + for (let i = 0; i < count; i++) { + const at = 128 + i * 64, flags = w.getUint32(at, true), ready = w.getUint32(at + 4, true); + if (flags > 3 || ready > 1 || (flags & 1) === 0 && ready !== 0) + throw new Error("invalid grid measurement child"); + for (let j = 44; j < 64; j++) if (request[at + j] !== 0) throw new Error("invalid grid measurement child reserved bytes"); + if ((flags & 1) !== 0) flows++; + } + if (p.getUint32(4, true) !== flows || p.getUint32(8, true) !== 0) throw new Error("invalid grid measurement flow count"); + let grid = nscvGridLayout(packet), g = new DataView(grid.buffer); + const result = new Uint8Array(32 + count * 32), out = new DataView(result.buffer); out.setUint32(0, 1, true); + if (flows === 0) return result; + const max = (a: number, b: number): number => nscvRenderExtreme(a, b, request[2]!, true); + const min = (a: number, b: number): number => nscvRenderExtreme(a, b, request[2]!, false); + if (virtual && !(p.getFloat32(48, true) > 0)) { + const columns = g.getUint32(0, true) + g.getUint32(4, true) * 4294967296; + let flow = 0, height = 0; + for (let i = 0; i < count && flow < columns; i++) { + const at = 128 + i * 64; + if ((w.getUint32(at, true) & 1) === 0) continue; + const authored = w.getFloat32(at + 20, true); + if (!(authored > 0) && w.getUint32(at + 4, true) === 0) { + out.setUint32(4, 1, true); out.setUint32(8, i, true); return result.slice(0, 32); + } + const value = authored > 0 ? authored : w.getFloat32(at + 40, true), ceiling = w.getFloat32(at + 36, true); + height = max(height, max(max(0, w.getFloat32(at + 28, true)), ceiling > 0 ? min(value, ceiling) : value)); + flow++; + } + p.setFloat32(52, height, true); grid = nscvGridLayout(packet); g = new DataView(grid.buffer); + } + out.setUint32(12, nscvFlowSemantic(nscvFlowInteger(g, 8)), true); + result.set(grid.subarray(48, 52), 16); out.setUint32(24, 1, true); + const columns = g.getUint32(0, true) + g.getUint32(4, true) * 4294967296; + const start = g.getUint32(16, true) + g.getUint32(20, true) * 4294967296; + const end = g.getUint32(24, true) + g.getUint32(28, true) * 4294967296; + let flow = 0; + for (let i = 0; i < count; i++) { + const at = 128 + i * 64, flags = w.getUint32(at, true); + if ((flags & 1) === 0) continue; + const index = flow++; if (virtual && (Math.floor(index / columns) < start || Math.floor(index / columns) >= end)) continue; + const cell = new Uint8Array(76), c = new DataView(cell.buffer); + cell[0] = 4; cell[1] = 1; cell[2] = (virtual ? 1 : 0) | ((flags & 2) !== 0 ? 2 : 0); + c.setUint32(4, index, true); cell.set(grid.subarray(0, 8), 12); + cell.set(packet.subarray(28, 36), 20); cell.set(grid.subarray(32, 48), 28); + cell.set(request.subarray(at + 8, at + 24), 44); + cell.set(request.subarray(at + 24, at + 32), 60); cell.set(request.subarray(at + 32, at + 40), 68); + const frame = nscvGridLayout(cell), dest = 32 + i * 32; + out.setUint32(dest, 1, true); out.setUint32(dest + 4, index, true); out.setUint32(dest + 8, flows, true); + result.set(frame.subarray(1, 17), dest + 16); + } + return result; +} + +/** Measurement scheduling for uniform/variable virtual rows and scroll + * shelves. Existing allocation operations retain their exact wire formats. */ +function nscvFlowMeasure(request: Uint8Array): Uint8Array { + if (request.length < 144 || request[0] !== 39 || request[1] !== 1 || (request[2] !== 0 && request[2] !== 2) || request[3]! > 15) + throw new Error("invalid flow measurement header"); + const w = new DataView(request.buffer, request.byteOffset, request.byteLength), count = w.getUint32(4, true), ready = w.getUint32(8, true); + if (request.length !== 144 + count * 72 || ready > 1 || w.getUint32(12, true) !== 0) + throw new Error("invalid flow measurement shape"); + const packet = request.slice(16, 144 + count * 64), p = new DataView(packet.buffer), op = request[2]!; + if (packet[0] !== 8 || packet[1] !== op || p.getUint32(4, true) !== count) throw new Error("invalid flow measurement allocation"); + const state = (i: number): number => 144 + count * 64 + i * 8; + let flows = 0; + for (let i = 0; i < count; i++) { + const lane = p.getUint32(128 + i * 64, true), at = state(i); + if (w.getUint32(at, true) > 1 || w.getUint32(at + 4, true) > 1 || lane === 0 && w.getUint32(at, true) !== 0) + throw new Error("invalid flow measurement child state"); + if (lane === 1) flows++; + } + const initial = nscvVirtualFlow(packet), initialWire = new DataView(initial.buffer), mode = initialWire.getUint32(4, true); + const result = new Uint8Array(32), out = new DataView(result.buffer); out.setUint32(0, 1, true); + const command = (action: number, index: number, width: number = 0): Uint8Array => { + out.setUint32(4, action, true); out.setUint32(8, index, true); out.setFloat32(16, width, true); return result; + }; + const max = (a: number, b: number): number => nscvRenderExtreme(a, b, request[3]!, true); + const min = (a: number, b: number): number => nscvRenderExtreme(a, b, request[3]!, false); + if (op === 0 && flows > 0) { + for (let i = 0; i < count; i++) { + const at = 128 + i * 64; + if (p.getUint32(at, true) === 0) continue; + const authored = p.getFloat32(at + 12, true); + if (mode === 1) { + if (!(authored > 0) && w.getUint32(state(i), true) === 0) return command(2, i, p.getFloat32(84, true)); + } else if (!(p.getFloat32(96, true) > 0)) { + if (!(authored > 0) && ready === 0) return command(1, i); + const value = authored > 0 ? authored : p.getFloat32(100, true), ceiling = p.getFloat32(at + 28, true); + p.setFloat32(100, max(max(0, p.getFloat32(at + 24, true)), ceiling > 0 ? min(value, ceiling) : value), true); + break; + } + } + } + if (op === 2) { + for (let i = 0; i < count; i++) { + const at = 128 + i * 64; + if (p.getUint32(at, true) === 0) continue; + const stack = new Uint8Array(84), s = new DataView(stack.buffer); stack[0] = 5; stack[1] = 3; s.setUint32(8, 1, true); + stack.set(initial.subarray(24, 40), 12); + s.setUint32(36, 1 | (w.getUint32(state(i) + 4, true) !== 0 ? 2 : 0), true); + stack.set(packet.subarray(at + 8, at + 16), 44); stack.set(packet.subarray(at + 32, at + 40), 52); + for (const [from, to] of [[16,60],[20,64],[24,68],[28,72]]) stack.set(packet.subarray(at + from!, at + from! + 4), to!); + const frame = nscvContainerLayout(stack); packet.set(frame.subarray(12, 28), at + 48); + if ((initialWire.getUint32(48 + i * 32, true) & 2) !== 0 && w.getUint32(state(i), true) === 0) return command(3, i); + } + } + const final = nscvVirtualFlow(packet), complete = new Uint8Array(32 + final.length); + out.setUint32(24, flows > 0 ? 1 : 0, true); complete.set(result); complete.set(final, 32); return complete; +} diff --git a/packages/core/src/runtime_policy.ts b/packages/core/src/runtime_policy.ts index 07d5db5a3..fadf8fc48 100644 --- a/packages/core/src/runtime_policy.ts +++ b/packages/core/src/runtime_policy.ts @@ -230,6 +230,8 @@ export function native_window_policy(request: Uint8Array): Uint8Array { if (request[0] === 35) return nscvAxisChildMeasure(request); if (request[0] === 36) return nscvSpanSubtree(request); if (request[0] === 37) return nscvAxisMeasure(request); + if (request[0] === 38) return nscvGridMeasure(request); + if (request[0] === 39) return nscvFlowMeasure(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 1dd740ceb..745df3c99 100644 --- a/packages/core/src/view_frontend.ts +++ b/packages/core/src/view_frontend.ts @@ -928,4 +928,5 @@ const viewPrelude = "\n// Portable Native components compiled beside the committ readFileSync(new URL("./widget_metrics.ts", import.meta.url), "utf8") + readFileSync(new URL("./intrinsic_measure.ts", import.meta.url), "utf8") + readFileSync(new URL("./measurement_coordination.ts", import.meta.url), "utf8") + + readFileSync(new URL("./flow_measurement.ts", import.meta.url), "utf8") + readFileSync(new URL("./stream_policy.ts", import.meta.url), "utf8"); diff --git a/packages/core/test/flow_measurement.test.ts b/packages/core/test/flow_measurement.test.ts new file mode 100644 index 000000000..2a1089437 --- /dev/null +++ b/packages/core/test/flow_measurement.test.ts @@ -0,0 +1,81 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { readFileSync } from "node:fs"; +import { stripTypeScriptTypes } from "node:module"; +const source = ["runtime_policy.ts", "flow_measurement.ts"].map(n => readFileSync(new URL(`../src/${n}`, import.meta.url), "utf8")).join("\n"); +const { nscvGridMeasure: grid, nscvFlowMeasure: flow } = await import(`data:text/javascript;base64,${Buffer.from(stripTypeScriptTypes(source) + "\nexport {nscvGridMeasure,nscvFlowMeasure};").toString("base64")}`); +const wire = (b: Uint8Array) => new DataView(b.buffer, b.byteOffset, b.byteLength); +const word = (b: Uint8Array, at: number) => wire(b).getUint32(at, true); +const value = (b: Uint8Array, at: number) => wire(b).getFloat32(at, true); +function gridPacket(virtual = true): Uint8Array { + const b = new Uint8Array(384), w = wire(b); b.set([38, 1, 0, virtual ? 1 : 0]); w.setUint32(4, 4, true); + b.set([4, 0, virtual ? 1 : 0], 16); w.setBigUint64(20, 3n, true); w.setBigUint64(28, 2n, true); + for (const [at, v] of [[44,3.25],[48,5.125],[52,160],[56,100],[60,2],[76,3],[80,2],[84,2],[88,1]]) w.setFloat32(at!, v!, true); + for (const i of [0,2,3]) w.setUint32(128 + i * 64, 1, true); + w.setFloat32(148, 20, true); w.setFloat32(284, 10, true); w.setFloat32(292, 25, true); + return b; +} +function flowPacket(op = 0, count = 3): Uint8Array { + const b = new Uint8Array(144 + count * 72), w = wire(b); b.set([39,1,op,0]); w.setUint32(4, count, true); + b.set([8,op,0,3], 16); w.setUint32(20, count, true); + for (const [at,v] of [[72,count],[76,count],[92,3.25],[96,5.125],[100,160],[104,100],[108,2]]) w.setFloat32(at!, v!, true); + for (let i = 0; i < count; i++) { w.setUint32(144 + i * 64, 1, true); w.setFloat32(148 + i * 64, i, true); } + return b; +} +function reply(b: Uint8Array, index: number, at: number, result: number): void { + wire(b).setUint32(144 + word(b, 4) * 64 + index * 8, 1, true); + wire(b).setFloat32(144 + index * 64 + at, result, true); +} +test("virtual grid measures only missing heights in the first flow row and preserves complete child records", () => { + const b = gridPacket(), w = wire(b); + let r = grid(b); assert.equal(word(r,4),1); assert.equal(word(r,8),2); + w.setUint32(260,1,true); w.setFloat32(296,50,true); r = grid(b); + assert.equal(word(r,4),0); assert.equal(word(r,12),2); assert.equal(value(r,16),25); + assert.equal(word(r,64),0); assert.equal(word(r,96),1); assert.equal(word(r,100),1); assert.equal(word(r,104),3); + assert.equal(value(r,112),84.25); assert.equal(value(r,124),25); assert.equal(word(r,128),1); assert.equal(value(r,148),32.125); +}); +test("authored grid extents suppress measurements; nonvirtual cells preserve authored semantic ownership", () => { + const b = gridPacket(); wire(b).setFloat32(64,40,true); assert.equal(word(grid(b),4),0); assert.equal(value(grid(b),16),40); + const plain = gridPacket(false); const r = grid(plain); assert.equal(word(r,4),0); assert.equal(value(r,16),0); assert.equal(value(r,124),25); assert.equal(value(r,156),49); +}); +test("grid preserves wide column words and copied results without modifying the borrowed request", () => { + const b = gridPacket(false), w = wire(b); w.setBigUint64(28,0xffffffffffffffffn,true); w.setFloat32(80,Math.fround(2 ** 64),true); w.setFloat32(88,Math.fround(2 ** 64),true); + const before = b.slice(), r = grid(b), saved = r.slice(); assert.equal(word(r,12),1); assert.deepEqual(b,before); b.fill(255); assert.deepEqual(r,saved); +}); +test("uniform rows measure only the first flow child and clamp its reply before allocation", () => { + const b = flowPacket(), w = wire(b); w.setUint32(144,0,true); w.setFloat32(212,0,true); w.setFloat32(276,1,true); w.setFloat32(232,12,true); w.setFloat32(236,30,true); + let r = flow(b); assert.equal(word(r,4),1); assert.equal(word(r,8),1); + w.setUint32(8,1,true); w.setFloat32(116,50,true); r = flow(b); + assert.equal(word(r,4),0); assert.equal(word(r,40),2); assert.equal(value(r,44),30); assert.equal(word(r,80),0); + assert.equal(value(r,124),5.125); assert.equal(value(r,156),37.125); +}); +test("variable rows query every missing window height in order and retain the anchor layout", () => { + const b = flowPacket(), w = wire(b); w.setBigUint64(32,100n,true); w.setBigUint64(40,1n,true); w.setFloat32(132,1000,true); w.setFloat32(128,80,true); w.setFloat32(220,40,true); + let r = flow(b); assert.equal(word(r,4),2); assert.equal(word(r,8),0); assert.equal(value(r,16),160); + reply(b,0,40,20); r = flow(b); assert.equal(word(r,4),2); assert.equal(word(r,8),2); + reply(b,2,40,60); r = flow(b); assert.equal(word(r,4),0); assert.equal(word(r,36),1); + assert.equal(value(r,92),63.125); assert.equal(value(r,124),85.125); assert.equal(value(r,156),127.125); +}); +test("scroll shelves measure only horizontal missing widths and preserve stack frames and stale-axis filtering", () => { + const b = flowPacket(2), w = wire(b); w.setUint32(208,0,true); w.setFloat32(280,200,true); w.setFloat32(120,7,true); w.setFloat32(124,9,true); + let r = flow(b); assert.equal(word(r,4),3); assert.equal(word(r,8),0); + reply(b,0,44,300); r = flow(b); assert.equal(word(r,4),0); assert.equal(word(r,36),4); + assert.equal(value(r,88),-3.75); assert.equal(value(r,92),-3.875); assert.equal(value(r,96),300); assert.equal(value(r,160),200); + b[19] = 2; const vertical = flow(b); assert.equal(value(vertical,88),3.25); assert.equal(value(vertical,96),160); +}); +test("empty flow parents retain inactive admission instead of replacing declared metadata", () => { + const b = gridPacket(); const w = wire(b); w.setBigUint64(20,0n,true); + for (let i=0;i<4;i++) w.setUint32(128+i*64,0,true); + assert.equal(word(grid(b),24),0); + const f = flowPacket(0,0); assert.equal(word(flow(f),24),0); assert.equal(word(flow(f),4),0); +}); +test("complete packets reject malformed states, lengths and reserved data before issuing a query", () => { + for (const at of [0,1,2,3,4,8,12,16,17,18,19,20,92,127,128,132,172,191]) { + const b=gridPacket(); b[at]=255; assert.throws(()=>grid(b),/invalid/); + } + for (const at of [0,1,2,3,4,8,12,16,17,18,19,20,136,140,144,336,340]) { + const b=flowPacket(); b[at]=255; assert.throws(()=>flow(b),/invalid/); + } + for (const derive of [grid,flow]) assert.throws(()=>derive(new Uint8Array(143)),/invalid/); + const b=flowPacket(), before=b.slice(), r=flow(b), saved=r.slice(); assert.deepEqual(b,before); b.fill(255); assert.deepEqual(r,saved); +}); diff --git a/packages/core/test/view_frontend.test.ts b/packages/core/test/view_frontend.test.ts index e1d635eb6..ba2163a1e 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", "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", "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/flow_measurement_policy.zig b/src/primitives/canvas/flow_measurement_policy.zig new file mode 100644 index 000000000..170889be7 --- /dev/null +++ b/src/primitives/canvas/flow_measurement_policy.zig @@ -0,0 +1,237 @@ +//! Copied grid and scrolling continuations; native executes measurement queries. +const std = @import("std"); +const builtin = @import("builtin"); +const geometry = @import("geometry"); +const widgets = @import("widgets.zig"); +const surface = @import("surface_layout_policy.zig"); +const virtual = @import("virtual_flow_policy.zig"); +pub const Policy = surface.Policy; + +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); +} +fn put(bytes: []u8, at: usize, value: f32) void { + word(bytes, at, @bitCast(value)); +} +fn float(bytes: []const u8, at: usize) f32 { + return @bitCast(read(bytes, at)); +} +fn length(base: usize, count: usize, stride: usize) !usize { + return std.math.add(usize, base, try std.math.mul(usize, count, stride)); +} +fn absent(bytes: []const u8, first: usize, end: usize) void { + if (!std.mem.allEqual(u8, bytes[first..end], 0)) @panic("invalid absent flow measurement fields"); +} +fn rect(bytes: []const u8, at: usize) geometry.RectF { + return .init(float(bytes, at), float(bytes, at + 4), float(bytes, at + 8), float(bytes, at + 12)); +} +pub const GridResult = struct { + bytes: [32]u8, + pub fn pending(self: GridResult) bool { + return read(&self.bytes, 4) == 1; + } + pub fn index(self: GridResult) usize { + return read(&self.bytes, 8); + } +}; +pub const GridPlan = struct { + allocator: std.mem.Allocator, + policy: Policy, + request: []u8, + result: []u8, + count: usize, + pub fn init(allocator: std.mem.Allocator, policy: Policy, count: usize, flows: usize, content: geometry.RectF, columns: usize, overscan: usize, gap: f32, item_extent: f32, scroll: f32, windowed: bool) !GridPlan { + const request = try allocator.alloc(u8, try length(128, count, 64)); + errdefer allocator.free(request); + const result = try allocator.alloc(u8, try length(32, count, 32)); + @memset(request, 0); + request[0..4].* = .{ 38, 1, @import("render_plan_policy.zig").numericFlags(), @intFromBool(windowed) }; + word(request, 4, std.math.cast(u32, count) orelse @panic("grid measurement count exceeds wire range")); + const packet = request[16..92]; + packet[0..4].* = .{ 4, 0, @as(u8, @intFromBool(windowed)) | (surface.anchorFlags(0, false) >> 2) | @as(u8, if (builtin.mode == .Debug or builtin.mode == .ReleaseSafe) 4 else 0), 0 }; + for ([_]usize{ flows, columns, overscan }, 0..) |value, i| std.mem.writeInt(u64, packet[4 + i * 8 ..][0..8], value, .little); + const values = [_]f32{ content.x, content.y, content.width, content.height, gap, item_extent, 0, scroll, @floatFromInt(flows), @floatFromInt(columns), @floatFromInt(flows -| 1), @floatFromInt(columns -| 1) }; + for (values, 0..) |value, i| put(packet, 28 + i * 4, value); + return .{ .allocator = allocator, .policy = policy, .request = request, .result = result, .count = count }; + } + pub fn deinit(self: GridPlan) void { + self.allocator.free(self.request); + self.allocator.free(self.result); + } + pub fn setChild(self: GridPlan, index: usize, child: widgets.Widget, flow: bool, fills_width: bool) void { + const at = 128 + index * 64; + word(self.request, at, @as(u32, @intFromBool(flow)) | (@as(u32, @intFromBool(fills_width)) << 1)); + const values = [_]f32{ child.frame.x, child.frame.y, child.frame.width, child.frame.height, child.layout.min_size.width, child.layout.min_size.height, child.layout.max_size.width, child.layout.max_size.height }; + for (values, 0..) |value, i| put(self.request, at + 8 + i * 4, value); + } + pub fn run(self: GridPlan) GridResult { + @memset(self.result, 0xa5); + const written = self.policy(self.request, self.result); + if (written < 32 or read(self.result, 0) != 1 or read(self.result, 4) > 1) @panic("invalid grid measurement result"); + const result: GridResult = .{ .bytes = self.result[0..32].* }; + if (result.pending()) { + const index = result.index(); + if (written != 32 or index >= self.count or read(self.request, 128 + index * 64) & 1 == 0 or read(self.request, 132 + index * 64) != 0) @panic("invalid grid measurement continuation"); + absent(&result.bytes, 12, 32); + } else { + if (written != self.result.len or read(self.result, 24) > 1) @panic("invalid grid frame length or admission"); + absent(&result.bytes, 8, 12); + absent(&result.bytes, 20, 24); + absent(&result.bytes, 28, 32); + if (!self.active()) absent(&result.bytes, 12, 20); + for (0..self.count) |i| { + const at = 32 + i * 32; + if (read(self.result, at) > 1) @panic("invalid grid frame flags"); + if (self.enabled(i)) { + if (read(self.request, 128 + i * 64) & 1 == 0 or read(self.result, at + 4) >= read(self.result, at + 8) or read(self.result, at + 8) > self.count) @panic("invalid grid frame identity"); + absent(self.result, at + 12, at + 16); + } else absent(self.result, at, at + 32); + } + } + return result; + } + pub fn reply(self: GridPlan, result: GridResult, height: f32) void { + std.debug.assert(result.pending()); + const at = 128 + result.index() * 64; + word(self.request, at + 4, 1); + put(self.request, at + 40, height); + } + pub fn active(self: GridPlan) bool { + return read(self.result, 24) != 0; + } + pub fn rows(self: GridPlan) u32 { + return read(self.result, 12); + } + pub fn extent(self: GridPlan) f32 { + return float(self.result, 16); + } + pub fn enabled(self: GridPlan, index: usize) bool { + return read(self.result, 32 + index * 32) != 0; + } + pub fn itemIndex(self: GridPlan, index: usize) u32 { + return read(self.result, 36 + index * 32); + } + pub fn itemCount(self: GridPlan, index: usize) u32 { + return read(self.result, 40 + index * 32); + } + pub fn frame(self: GridPlan, index: usize) geometry.RectF { + return rect(self.result, 48 + index * 32); + } +}; + +pub const FlowAction = enum(u32) { done, extent, height, width }; +pub const FlowResult = struct { + bytes: [32]u8, + pub fn action(self: FlowResult) FlowAction { + return @enumFromInt(read(&self.bytes, 4)); + } + pub fn index(self: FlowResult) usize { + return read(&self.bytes, 8); + } + pub fn width(self: FlowResult) f32 { + return float(&self.bytes, 16); + } +}; +pub const FlowPlan = struct { + allocator: std.mem.Allocator, + policy: Policy, + request: []u8, + result: []u8, + count: usize, + pub fn init(allocator: std.mem.Allocator, policy: Policy, count: usize, operation: u8, options: virtual.Options) !FlowPlan { + // Reuse the established host conversion facts without borrowing any + // compiler result or retaining the temporary allocation's storage. + _ = try length(128, count, 64); + _ = try length(48, count, 32); + const base = try virtual.Plan.init(allocator, policy, count, options); + defer base.deinit(); + const request = try allocator.alloc(u8, try length(144, count, 72)); + errdefer allocator.free(request); + const result = try allocator.alloc(u8, try length(80, count, 32)); + @memset(request, 0); + request[0..4].* = .{ 39, 1, operation, @import("render_plan_policy.zig").numericFlags() }; + word(request, 4, std.math.cast(u32, count) orelse @panic("flow measurement count exceeds wire range")); + @memcpy(request[16..][0..base.request.len], base.request); + request[17] = operation; + return .{ .allocator = allocator, .policy = policy, .request = request, .result = result, .count = count }; + } + pub fn deinit(self: FlowPlan) void { + self.allocator.free(self.request); + self.allocator.free(self.result); + } + fn state(self: FlowPlan, index: usize) usize { + return 144 + self.count * 64 + index * 8; + } + pub fn setChild(self: FlowPlan, index: usize, child: widgets.Widget, absolute: usize, flow: bool, fills_width: bool) void { + const at = 144 + index * 64; + word(self.request, at, @intFromBool(flow)); + const values = [_]f32{ @floatFromInt(absolute), child.frame.width, child.frame.height, child.layout.min_size.width, child.layout.max_size.width, child.layout.min_size.height, child.layout.max_size.height, child.frame.x, child.frame.y }; + for (values, 0..) |value, i| put(self.request, at + 4 + i * 4, value); + word(self.request, self.state(index) + 4, @intFromBool(fills_width)); + } + pub fn run(self: FlowPlan) FlowResult { + @memset(self.result, 0xa5); + const written = self.policy(self.request, self.result); + if (written < 32 or read(self.result, 0) != 1 or read(self.result, 4) > 3) @panic("invalid flow measurement result"); + const result: FlowResult = .{ .bytes = self.result[0..32].* }; + absent(&result.bytes, 12, 16); + absent(&result.bytes, 20, 24); + absent(&result.bytes, 28, 32); + if (result.action() == .done) { + if (written != self.result.len or read(self.result, 24) > 1 or read(self.result, 32) != self.count or (if (self.request[2] == 2) read(self.result, 36) != 4 else read(self.result, 36) > 1)) @panic("invalid flow allocation length or mode"); + absent(&result.bytes, 8, 12); + absent(&result.bytes, 16, 20); + for (0..self.count) |i| { + const at = 80 + i * 32; + if (read(self.result, at) != 0 and read(self.result, at) != 1 and read(self.result, at) != 3) @panic("invalid flow frame flags"); + if (self.enabled(i) and read(self.request, 144 + i * 64) == 0) @panic("invalid flow frame admission"); + if (!self.enabled(i)) absent(self.result, at, at + 32); + } + } else { + const index = result.index(); + if (written != 32 or index >= self.count or read(self.request, 144 + index * 64) != 1 or read(self.result, 24) != 0) @panic("invalid flow child continuation"); + if (result.action() == .extent) { + if (read(self.request, 8) != 0 or self.request[2] != 0) @panic("invalid repeated flow extent query"); + } else if (read(self.request, self.state(index)) != 0 or (result.action() == .width) != (self.request[2] == 2)) @panic("invalid repeated flow measurement"); + if (result.action() != .height) absent(&result.bytes, 16, 20); + } + return result; + } + pub fn reply(self: FlowPlan, result: FlowResult, value: f32) void { + switch (result.action()) { + .done => unreachable, + .extent => { + word(self.request, 8, 1); + put(self.request, 116, value); + }, + .height, .width => { + word(self.request, self.state(result.index()), 1); + put(self.request, 144 + result.index() * 64 + @as(usize, if (result.action() == .height) 40 else 44), value); + }, + } + } + pub fn active(self: FlowPlan) bool { + return read(self.result, 24) != 0; + } + pub fn itemCount(self: FlowPlan) u32 { + return read(self.result, 40); + } + pub fn extent(self: FlowPlan) f32 { + return float(self.result, 44); + } + pub fn mode(self: FlowPlan) u32 { + return read(self.result, 36); + } + pub fn enabled(self: FlowPlan, index: usize) bool { + return read(self.result, 80 + index * 32) & 1 != 0; + } + pub fn itemIndex(self: FlowPlan, index: usize) u32 { + return read(self.result, 84 + index * 32); + } + pub fn frame(self: FlowPlan, index: usize) geometry.RectF { + return rect(self.result, 88 + index * 32); + } +}; diff --git a/src/primitives/canvas/root.zig b/src/primitives/canvas/root.zig index 31d092804..7894d63c0 100644 --- a/src/primitives/canvas/root.zig +++ b/src/primitives/canvas/root.zig @@ -910,6 +910,7 @@ pub const surface_recipe_policy = @import("surface_recipe_policy.zig"); pub const widget_metric_policy = @import("widget_metric_policy.zig"); 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 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 9f779a2b6..c743b92e7 100644 --- a/src/primitives/canvas/tokens.zig +++ b/src/primitives/canvas/tokens.zig @@ -1911,6 +1911,8 @@ pub const DesignTokens = struct { container_layout_policy: ?@import("container_layout_policy.zig").Policy = null, /// Portable measurement scheduling, separate from allocation arithmetic. measurement_coordination_policy: ?@import("measurement_coordination_policy.zig").Policy = null, + /// Portable grid and scrolling measurement/admission coordination. + flow_measurement_policy: ?@import("flow_measurement_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_layout.zig b/src/primitives/canvas/widget_layout.zig index c47c3c172..c9b714b17 100644 --- a/src/primitives/canvas/widget_layout.zig +++ b/src/primitives/canvas/widget_layout.zig @@ -1410,6 +1410,7 @@ fn layoutGridChildren( requested_columns: usize, tokens: DesignTokens, ) Error!void { + if (tokens.flow_measurement_policy) |policy| return compiledGridChildren(children, content, parent_index, depth, output, len, .{ .gap = gap, .columns = requested_columns }, 0, false, policy, tokens); const flow_count = widgetFlowChildCount(children); if (flow_count == 0) return; @@ -1464,6 +1465,7 @@ fn layoutVirtualGridChildren( style: WidgetLayoutStyle, tokens: DesignTokens, ) Error!void { + if (tokens.flow_measurement_policy) |policy| return compiledGridChildren(children, content, parent_index, depth, output, len, style, scroll_y, true, policy, tokens); const flow_count = widgetFlowChildCount(children); if (flow_count == 0) return; @@ -1540,6 +1542,32 @@ fn layoutVirtualGridChildren( } } +fn compiledGridChildren(children: []const Widget, content: geometry.RectF, parent_index: usize, depth: usize, output: []WidgetLayoutNode, len: *usize, style: WidgetLayoutStyle, scroll_y: f32, windowed: bool, policy: @import("flow_measurement_policy.zig").Policy, tokens: DesignTokens) Error!void { + var scratch = std.heap.stackFallback(4096, std.heap.page_allocator); + const plan = @import("flow_measurement_policy.zig").GridPlan.init(scratch.get(), policy, children.len, widgetFlowChildCount(children), content, style.columns, style.virtual_overscan, style.gap, style.virtual_item_extent, scroll_y, windowed) catch @panic("grid measurement allocation failed"); + defer plan.deinit(); + for (children, 0..) |child, index| plan.setChild(index, child, widgetTakesFlowSlot(child), primaryUnderlineTabsFillWidth(child, tokens)); + while (true) { + const result = plan.run(); + if (!result.pending()) break; + plan.reply(result, intrinsicWidgetSize(children[result.index()], tokens).height); + } + if (!plan.active()) return; + if (windowed) { + output[parent_index].widget.layout.virtual_item_extent = plan.extent(); + output[parent_index].widget.semantics.list_item_count = plan.rows(); + } + for (children, 0..) |source, index| { + if (!plan.enabled(index)) continue; + var child = source; + if (windowed) { + child.semantics.list_item_index = plan.itemIndex(index); + child.semantics.list_item_count = plan.itemCount(index); + } + _ = try layoutWidgetDepth(child, plan.frame(index), parent_index, depth + 1, output, len, tokens); + } +} + /// A grid cell is the containing width for Geist's primary ruled row. /// Translate the theme-agnostic house width to the remaining cell width, /// while keeping deliberate overflow and explicit max bounds intact. @@ -1586,6 +1614,7 @@ fn layoutScrollChildren( scroll_offset: geometry.OffsetF, tokens: DesignTokens, ) Error!void { + if (tokens.flow_measurement_policy) |policy| return compiledMeasuredFlow(children, parent_index, depth, output, len, 2, .{ .content = content, .horizontal = axes.scrollsHorizontally(), .vertical = axes.scrollsVertically(), .scroll_x = scroll_offset.dx, .scroll_y = scroll_offset.dy }, policy, tokens); if (tokens.intrinsic_layout_policy) |policy| { var scratch = std.heap.stackFallback(4096, std.heap.page_allocator); const plan = @import("virtual_flow_policy.zig").Plan.init(scratch.get(), policy, children.len, .{ .content = content, .horizontal = axes.scrollsHorizontally(), .vertical = axes.scrollsVertically(), .scroll_x = scroll_offset.dx, .scroll_y = scroll_offset.dy }) catch @panic("scroll flow allocation failed"); @@ -1631,6 +1660,7 @@ fn layoutVirtualVerticalChildren( style: WidgetLayoutStyle, tokens: DesignTokens, ) Error!void { + if (tokens.flow_measurement_policy) |policy| return compiledMeasuredFlow(children, parent_index, depth, output, len, 0, .{ .content = content, .first = style.virtual_first_index, .declared = style.virtual_item_count, .anchor = style.virtual_anchor_index, .overscan = style.virtual_overscan, .flow_count = widgetFlowChildCount(children), .gap = style.gap, .item_extent = style.virtual_item_extent, .scroll_y = scroll_y, .anchor_extent = style.virtual_anchor_extent, .total_extent = style.virtual_total_extent }, policy, tokens); if (tokens.intrinsic_layout_policy != null) return compiledVirtualVerticalChildren(children, content, parent_index, depth, output, len, scroll_y, style, tokens); const flow_count = widgetFlowChildCount(children); if (flow_count == 0) return; @@ -1758,6 +1788,42 @@ fn layoutVariableVirtualChildren( } } +fn compiledMeasuredFlow(children: []const Widget, parent_index: usize, depth: usize, output: []WidgetLayoutNode, len: *usize, operation: u8, options: @import("virtual_flow_policy.zig").Options, policy: @import("flow_measurement_policy.zig").Policy, tokens: DesignTokens) Error!void { + var scratch = std.heap.stackFallback(4096, std.heap.page_allocator); + const plan = @import("flow_measurement_policy.zig").FlowPlan.init(scratch.get(), policy, children.len, operation, options) catch @panic("flow measurement allocation failed"); + defer plan.deinit(); + var flow: usize = 0; + for (children, 0..) |child, index| { + const takes_flow = widgetTakesFlowSlot(child); + plan.setChild(index, child, options.first +% flow, takes_flow, primaryUnderlineTabsFillWidth(child, tokens)); + if (takes_flow) flow += 1; + } + while (true) { + const result = plan.run(); + const measured = switch (result.action()) { + .done => break, + .extent => intrinsicWidgetSize(children[result.index()], tokens).height, + .height => variableVirtualRowExtent(children[result.index()], result.width(), tokens, depth + 1), + .width => intrinsicChildSize(children[result.index()], tokens, depth + 1).width, + }; + plan.reply(result, measured); + } + if (!plan.active()) return; + if (operation == 0) { + if (plan.mode() == 0) output[parent_index].widget.layout.virtual_item_extent = plan.extent(); + output[parent_index].widget.semantics.list_item_count = plan.itemCount(); + } + for (children, 0..) |source, index| { + if (!plan.enabled(index)) continue; + var child = source; + if (operation == 0) { + child.semantics.list_item_index = plan.itemIndex(index); + child.semantics.list_item_count = plan.itemCount(); + } + _ = try layoutWidgetDepth(child, plan.frame(index), parent_index, depth + 1, output, len, tokens); + } +} + /// Native measurement and traversal surround one copied portable row plan. fn compiledVirtualVerticalChildren(children: []const Widget, content: geometry.RectF, parent_index: usize, depth: usize, output: []WidgetLayoutNode, len: *usize, scroll_y: f32, style: WidgetLayoutStyle, tokens: DesignTokens) Error!void { const flow_count = widgetFlowChildCount(children); diff --git a/src/runtime/ts_ui_app.zig b/src/runtime/ts_ui_app.zig index 8d27d92cb..75829073b 100644 --- a/src/runtime/ts_ui_app.zig +++ b/src/runtime/ts_ui_app.zig @@ -462,6 +462,8 @@ pub fn TsUiAppWithFeatures(comptime core: type, comptime features: ui_app.UiAppF stamped.intrinsic_layout_policy = core.nativeWindowPolicy; if (options.measurement_coordination_policy != null) @panic("TsUiApp owns measurement_coordination_policy - remove custom measurement coordination wiring"); stamped.measurement_coordination_policy = core.nativeWindowPolicy; + if (options.flow_measurement_policy != null) @panic("TsUiApp owns flow_measurement_policy - remove custom flow measurement wiring"); + stamped.flow_measurement_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 cabd14ca0..2e1781a5c 100644 --- a/src/runtime/ui_app.zig +++ b/src/runtime/ui_app.zig @@ -525,6 +525,7 @@ pub fn UiAppWithFeatures(comptime ModelT: type, comptime MsgT: type, comptime fe grid_layout_policy: ?*const fn ([]const u8, []u8) usize = null, container_layout_policy: ?*const fn ([]const u8, []u8) usize = null, measurement_coordination_policy: ?*const fn ([]const u8, []u8) usize = null, + flow_measurement_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, @@ -2024,6 +2025,7 @@ pub fn UiAppWithFeatures(comptime ModelT: type, comptime MsgT: type, comptime fe tokens.container_layout_policy = self.options.container_layout_policy; tokens.intrinsic_layout_policy = self.options.intrinsic_layout_policy; tokens.measurement_coordination_policy = self.options.measurement_coordination_policy; + tokens.flow_measurement_policy = self.options.flow_measurement_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; @@ -2083,6 +2085,7 @@ pub fn UiAppWithFeatures(comptime ModelT: type, comptime MsgT: type, comptime fe tokens.container_layout_policy = self.options.container_layout_policy; tokens.intrinsic_layout_policy = self.options.intrinsic_layout_policy; tokens.measurement_coordination_policy = self.options.measurement_coordination_policy; + tokens.flow_measurement_policy = self.options.flow_measurement_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/surface_layout_e2e_tests.zig b/tests/ts-core/surface_layout_e2e_tests.zig index b4a72e14b..16ef0efb2 100644 --- a/tests/ts-core/surface_layout_e2e_tests.zig +++ b/tests/ts-core/surface_layout_e2e_tests.zig @@ -233,6 +233,7 @@ test "TypeScript app adapter supplies placement through every theme path" { try std.testing.expect(tokens.container_layout_policy == core.nativeWindowPolicy); try std.testing.expect(tokens.intrinsic_layout_policy == core.nativeWindowPolicy); try std.testing.expect(tokens.measurement_coordination_policy == core.nativeWindowPolicy); + try std.testing.expect(tokens.flow_measurement_policy == core.nativeWindowPolicy); } } diff --git a/tests/ts-core/typography_e2e_tests.zig b/tests/ts-core/typography_e2e_tests.zig index 40df4849c..21476b773 100644 --- a/tests/ts-core/typography_e2e_tests.zig +++ b/tests/ts-core/typography_e2e_tests.zig @@ -32,6 +32,7 @@ test "compiled model themes copy the complete register across frame and commit o reference.container_layout_policy = snapshot.container_layout_policy; reference.intrinsic_layout_policy = snapshot.intrinsic_layout_policy; reference.measurement_coordination_policy = snapshot.measurement_coordination_policy; + reference.flow_measurement_policy = snapshot.flow_measurement_policy; reference.widget_motion_policy = core.nativeWindowPolicy; reference.widget_audit_policy = core.nativeWindowPolicy; reference.widget_routing_policy = core.nativeWindowPolicy; @@ -91,6 +92,7 @@ fn restamp(reference: *canvas.DesignTokens, actual: canvas.DesignTokens) void { reference.container_layout_policy = actual.container_layout_policy; reference.intrinsic_layout_policy = actual.intrinsic_layout_policy; reference.measurement_coordination_policy = actual.measurement_coordination_policy; + reference.flow_measurement_policy = actual.flow_measurement_policy; 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 e215f465f..c92bd728f 100644 --- a/tests/ts-core/widget_metric_e2e_tests.zig +++ b/tests/ts-core/widget_metric_e2e_tests.zig @@ -423,6 +423,190 @@ fn coordinated(tokens: c.DesignTokens) c.DesignTokens { t.measurement_coordination_policy = core.nativeWindowPolicy; return t; } + +fn flowOwned(tokens: c.DesignTokens) c.DesignTokens { + var t = coordinated(tokens); + t.flow_measurement_policy = core.nativeWindowPolicy; + return t; +} +fn flowTree(widget: c.Widget, tokens: c.DesignTokens, trace: *Measures) !void { + var native_nodes: [128]c.WidgetLayoutNode = undefined; + var compiled_nodes: [128]c.WidgetLayoutNode = undefined; + const bounds: sdk.geometry.RectF = .init(0.125, -0.0, 220.25, 137.5); + const reference_tokens = coordinated(tokens); + c.bumpTextMeasureGeneration(); + trace.count = 0; + const expected = try c.layoutWidgetTreeWithTokens(widget, bounds, reference_tokens, &native_nodes); + var expected_semantics: [128]c.WidgetSemanticsNode = undefined; + const semantics = try expected.collectSemantics(&expected_semantics); + var expected_commands: [4096]c.CanvasCommand = undefined; + var expected_builder = c.Builder.init(&expected_commands); + try expected.emitDisplayList(&expected_builder, reference_tokens); + const observations = trace.observations; + const count = trace.count; + c.bumpTextMeasureGeneration(); + trace.count = 0; + const t = flowOwned(tokens); + const actual = try c.layoutWidgetTreeWithTokens(widget, bounds, t, &compiled_nodes); + var actual_semantics: [128]c.WidgetSemanticsNode = undefined; + var actual_commands: [4096]c.CanvasCommand = undefined; + var actual_builder = c.Builder.init(&actual_commands); + try actual.emitDisplayList(&actual_builder, t); + try exact(expected.nodes, actual.nodes); + try exact(semantics, try actual.collectSemantics(&actual_semantics)); + try exact(expected_builder.displayList().commands, actual_builder.displayList().commands); + try std.testing.expectEqual(count, trace.count); + try exact(observations[0..count], trace.observations[0..trace.count]); + core.rt.frameReset(); +} +test "compiled widget metric flow coordination preserves grids complete trees and first-row measurement order" { + _ = core.initialModel(); + var trace: Measures = .{}; + const provider: c.TextMeasureProvider = .{ .context = &trace, .measure_fn = measured }; + const paragraph = [_]c.Widget{.{ .id = 30, .kind = .text, .text = "Grid paragraph\xff\x00", .spans = &.{.{ .text = "Measured\xff\x00 paragraph\nwith wrapping", .scale = 1.125 }} }}; + const children = [_]c.Widget{ + .{ .id = 2, .kind = .card, .text = "First", .children = ¶graph, .layout = .{ .min_size = .init(17.25, 23.125), .max_size = .init(180.5, 71.25) } }, + .{ .id = 3, .kind = .popover, .text = "Out of flow", .children = ¶graph }, + .{ .id = 4, .kind = .separator }, + .{ .id = 5, .kind = .tabs, .text = "Ruled", .frame = .init(3.125, 5.25, 19.5, 31.125), .layout = .{ .min_size = .init(7.25, 11.5) } }, + .{ .id = 6, .kind = .column, .children = ¶graph, .layout = .{ .min_size = .init(0, 13.5), .max_size = .init(0, 51.25) } }, + }; + for ([_]c.ThemePack{ .house, .geist }) |pack| for ([_]usize{ 0, 1, 2, 3, std.math.maxInt(usize) }) |columns| for ([_]bool{ false, true }) |windowed| for ([_]f32{ 0, 37.25 }) |extent| for ([_]f32{ -200, 0, 33.25, 2000 }) |offset| { + var t = c.DesignTokens.theme(.{ .pack = pack }); + t.text_measure = &provider; + const root: c.Widget = .{ .id = 1, .kind = .grid, .value = offset, .children = &children, .layout = .{ .columns = columns, .virtualized = windowed, .virtual_item_extent = extent, .virtual_overscan = 1, .gap = 2.125, .padding = .all(3.25) } }; + flowTree(root, t, &trace) catch |err| { + std.debug.print("grid flow pack {t} columns {d} virtual {} extent {d} offset {d}\n", .{ pack, columns, windowed, extent, offset }); + return err; + }; + }; +} +test "compiled widget metric flow coordination preserves scrolling axes sizing and measurement order" { + _ = core.initialModel(); + var trace: Measures = .{}; + const provider: c.TextMeasureProvider = .{ .context = &trace, .measure_fn = measured }; + const paragraph = [_]c.Widget{.{ .id = 30, .kind = .text, .text = "Long no-wrap shelf and\xff\x00 bytes", .spans = &.{.{ .text = "Width-aware styled shelf\nSecond row" }} }}; + const children = [_]c.Widget{ + .{ .id = 2, .kind = .column, .children = ¶graph, .layout = .{ .min_size = .init(19.25, 23.5), .max_size = .init(180.125, 60.25) } }, + .{ .id = 3, .kind = .popover, .children = ¶graph }, + .{ .id = 4, .kind = .tabs, .text = "Rule", .frame = .init(3.125, 5.5, 19.25, 31.5) }, + .{ .id = 5, .kind = .row, .children = ¶graph, .frame = .init(0, 0, 0, 21.25) }, + }; + for ([_]c.ThemePack{ .house, .geist }) |pack| for ([_]c.ScrollAxes{ .vertical, .horizontal, .both }) |axes| for ([_]f32{ -37.25, 0, 51.125, 2000 }) |offset| { + var t = c.DesignTokens.theme(.{ .pack = pack }); + t.text_measure = &provider; + const root: c.Widget = .{ .id = 1, .kind = .scroll_view, .scroll_axes = axes, .value = offset, .value_x = -offset, .children = &children, .layout = .{ .padding = .all(3.25) } }; + flowTree(root, t, &trace) catch |err| { + std.debug.print("scroll flow pack {t} axes {t} offset {d}\n", .{ pack, axes, offset }); + return err; + }; + }; +} +test "compiled widget metric flow coordination preserves virtual uniform and variable anchored windows" { + _ = core.initialModel(); + var trace: Measures = .{}; + const provider: c.TextMeasureProvider = .{ .context = &trace, .measure_fn = measured }; + const paragraph = [_]c.Widget{.{ .id = 30, .kind = .text, .spans = &.{.{ .text = "Windowed paragraph\xff\x00\nSecond line", .scale = 1.25 }} }}; + const children = [_]c.Widget{ + .{ .id = 2, .kind = .list_item, .children = ¶graph, .layout = .{ .min_size = .init(7.25, 11.125), .max_size = .init(0, 80.5) } }, + .{ .id = 3, .kind = .popover, .children = ¶graph }, + .{ .id = 4, .kind = .list_item, .children = ¶graph, .frame = .init(3.25, 5.5, 0, 31.125) }, + .{ .id = 5, .kind = .column, .children = ¶graph }, + }; + for ([_]bool{ false, true }) |variable| for ([_]usize{ 0, 7, 0xfffffff0 }) |first| for ([_]usize{ 0, 1, 20 }) |anchor| for ([_]f32{ 0, 40.5 }) |extent| for ([_]f32{ -137.5, 0, 93.125, 5000 }) |offset| { + const root: c.Widget = .{ .id = 1, .kind = .column, .value = offset, .children = &children, .layout = .{ .virtualized = true, .virtual_first_index = first, .virtual_item_count = first + 100, .virtual_anchor_index = first + anchor, .virtual_anchor_extent = 71.25, .virtual_total_extent = if (variable) 1024.5 else 0, .virtual_item_extent = extent, .virtual_overscan = 2, .gap = 2.125, .padding = .all(3.25) } }; + flowTree(root, .{ .text_measure = &provider }, &trace) catch |err| { + std.debug.print("virtual flow variable {} first {d} anchor {d} extent {d} offset {d}\n", .{ variable, first, anchor, extent, offset }); + return err; + }; + }; +} +test "compiled widget metric flow admission preserves empty parents and skipped children" { + _ = core.initialModel(); + var trace: Measures = .{}; + const provider: c.TextMeasureProvider = .{ .context = &trace, .measure_fn = measured }; + const excluded: c.Widget = .{ .kind = .popover, .text = "No flow" }; + for ([_]c.WidgetKind{ .grid, .column, .scroll_view }) |kind| for ([_]bool{ false, true }) |empty| for ([_]bool{ false, true }) |windowed| { + const root: c.Widget = .{ .kind = kind, .children = if (empty) &.{} else &.{excluded}, .semantics = .{ .list_item_count = 93 }, .layout = .{ .virtualized = windowed, .virtual_item_count = 100, .virtual_item_extent = 31.25, .virtual_overscan = 2 } }; + try flowTree(root, .{ .text_measure = &provider }, &trace); + }; +} +test "compiled widget metric flow coordination preserves exceptional numeric words and measurement depth" { + _ = core.initialModel(); + var trace: Measures = .{}; + const provider: c.TextMeasureProvider = .{ .context = &trace, .measure_fn = measured }; + const paragraph: c.Widget = .{ .kind = .text, .spans = &.{.{ .text = "Depth-aware paragraph\xff\x00\nSecond line" }} }; + for ([_]c.WidgetKind{ .grid, .column, .scroll_view }) |kind| for ([_]u32{ 0, 0x80000000, 1, 0x7f800000, 0xff800000, 0x7fc12345, 0xffc12345, 0x7f812345, 0xff812345 }) |word_| { + const value_: f32 = @bitCast(word_); + const child: c.Widget = .{ .kind = .column, .children = &.{paragraph}, .layout = .{ .min_size = .init(0, value_), .max_size = .init(0, 71.25) } }; + const root: c.Widget = .{ .kind = kind, .scroll_axes = .both, .children = &.{child}, .layout = .{ .virtualized = kind != .scroll_view, .columns = 1, .virtual_item_count = 100, .virtual_item_extent = value_, .virtual_overscan = 2, .gap = value_ } }; + flowTree(root, .{ .text_measure = &provider }, &trace) catch |err| { + std.debug.print("flow exceptional kind {t} word {x}\n", .{ kind, word_ }); + return err; + }; + }; + var chain: [35]c.Widget = undefined; + for ([_]usize{ 30, 31, 32, 33 }) |depth| { + chain[depth] = paragraph; + var i = depth; + while (i > 0) { + i -= 1; + chain[i] = .{ .kind = if (i == 0) .scroll_view else .column, .scroll_axes = .horizontal, .children = chain[i + 1 .. i + 2] }; + } + if (depth < 32) { + try flowTree(chain[0], .{ .text_measure = &provider }, &trace); + } else { + var expected: [35]c.WidgetLayoutNode = undefined; + var actual: [35]c.WidgetLayoutNode = undefined; + const bounds: sdk.geometry.RectF = .init(0, 0, 220.25, 137.5); + c.bumpTextMeasureGeneration(); + trace.count = 0; + try std.testing.expectError(error.WidgetDepthExceeded, c.layoutWidgetTreeWithTokens(chain[0], bounds, coordinated(.{ .text_measure = &provider }), &expected)); + const count = trace.count; + const observations = trace.observations; + c.bumpTextMeasureGeneration(); + trace.count = 0; + try std.testing.expectError(error.WidgetDepthExceeded, c.layoutWidgetTreeWithTokens(chain[0], bounds, flowOwned(.{ .text_measure = &provider }), &actual)); + try exact(@as([]const c.WidgetLayoutNode, expected[0..32]), @as([]const c.WidgetLayoutNode, actual[0..32])); + try std.testing.expectEqual(count, trace.count); + try exact(observations[0..count], trace.observations[0..trace.count]); + core.rt.frameReset(); + } + } +} +test "compiled widget metric flow packets retain owned continuations results and caller tails" { + _ = core.initialModel(); + const flow_policy = c.flow_measurement_policy; + const child: c.Widget = .{ .kind = .text, .text = "Owned height" }; + const grid = try flow_policy.GridPlan.init(std.testing.allocator, core.nativeWindowPolicy, 1, 1, .init(0, 0, 120, 80), 1, 0, 2, 0, 0, true); + defer grid.deinit(); + grid.setChild(0, child, true, false); + const pending = grid.run(); + try std.testing.expect(pending.pending()); + const saved = pending.bytes; + core.rt.frameReset(); + grid.reply(pending, 23.125); + try std.testing.expect(!grid.run().pending()); + try std.testing.expectEqualSlices(u8, &saved, &pending.bytes); + const plan = try flow_policy.FlowPlan.init(std.testing.allocator, core.nativeWindowPolicy, 1, 0, .{ .content = .init(0, 0, 120, 80), .flow_count = 1 }); + defer plan.deinit(); + plan.setChild(0, child, 0, true, false); + const extent = plan.run(); + try std.testing.expectEqual(flow_policy.FlowAction.extent, extent.action()); + const extent_bytes = extent.bytes; + core.rt.frameReset(); + plan.reply(extent, 31.25); + try std.testing.expectEqual(flow_policy.FlowAction.done, plan.run().action()); + try std.testing.expectEqualSlices(u8, &extent_bytes, &extent.bytes); + for ([_][]const u8{ grid.request, plan.request }) |request| { + var output: [256]u8 = @splat(0xa5); + const written = core.nativeWindowPolicy(request, &output); + const copy = output; + try std.testing.expect(std.mem.allEqual(u8, output[written..], 0xa5)); + core.rt.frameReset(); + try std.testing.expectEqualSlices(u8, ©, &output); + } +} fn coordinatedTree(widget: c.Widget, width: f32, tokens: c.DesignTokens, trace: *Measures) !void { var reference_nodes: [32]c.WidgetLayoutNode = undefined; var compiled_nodes: [32]c.WidgetLayoutNode = undefined;