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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1328,6 +1328,7 @@ pub fn build(b: *std.Build) void {
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/control_commands.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;
Expand Down Expand Up @@ -5424,6 +5425,7 @@ fn externalCoreFixtureModule(
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/control_commands.ts"));
stage_run.addFileInput(b.path("packages/core/src/stream_policy.ts"));
stage_run.addArg("--src");
stage_run.addDirectoryArg(spec.src_dir);
Expand Down
1 change: 1 addition & 0 deletions build/app.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,7 @@ fn tsCoreStage(
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/control_commands.ts"));
stage_run.addFileInput(dep.path("packages/core/src/stream_policy.ts"));
if (typescript_view) {
stage_run.addArg("--view-markup");
Expand Down
115 changes: 115 additions & 0 deletions packages/core/src/control_commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/** Control paint programs over copied observations. Native executes explicit
* drawing/font/icon capabilities and owns the resulting text and path storage. */
function nscvControlCommands(request: Uint8Array): Uint8Array {
if (request.length !== 32 || request[0] !== 45 || request[1] !== 1 || request[2]! > 16 || request[3] !== 0)
throw new Error("invalid control command header");
const w = new DataView(request.buffer, request.byteOffset, request.byteLength);
const flags = w.getUint32(4, true), family = request[2]!;
if (flags > 1048575) throw new Error("invalid control command facts");
const focused = (flags & 1) !== 0, text = (flags & 2) !== 0, placeholder = (flags & 4) !== 0;
const icon = (flags & 8) !== 0, committed = (flags & 16) !== 0;
const selected = committed || w.getFloat32(8, true) >= 0.5;
const background = (flags & 32) !== 0, border = (flags & 64) !== 0;
const clip = (flags & 128) !== 0, selection = (flags & 256) !== 0;
const selectionRange = (flags & 512) !== 0, composition = (flags & 1024) !== 0;
const compositionRange = (flags & 2048) !== 0, underline = (flags & 4096) !== 0;
const bar = (flags & 8192) !== 0, clear = (flags & 16384) !== 0;
const chevron = (flags & 32768) !== 0, check = (flags & 65536) !== 0;
const seam = (flags & 131072) !== 0, combobox = (flags & 262144) !== 0;
if (selectionRange && !selection || compositionRange && !composition)
throw new Error("invalid control editing observations");
const result = new Uint8Array(200), out = new DataView(result.buffer); out.setUint32(0, 1, true);
let count = 0;
// fill, stroke, focus, icon, text, selection, selected glyphs, composition,
// caret, clip, unclip, mark, seam clip, seam unclip, shadow.
const emit = (op: number, slot: number, variant: number = 0): void => {
if (count >= 16) throw new Error("control command capacity exceeded");
const at = 8 + count * 12;
out.setUint32(at, op, true); out.setUint32(at + 4, slot, true); out.setUint32(at + 8, variant, true); count++;
};
switch (family) {
case 0: case 1: // command buttons, including flush seam clips
emit(0, 1);
if (w.getFloat32(12, true) > 0) {
if (seam) emit(12, 0);
emit(1, 2);
if (seam) emit(13, 0);
}
if (focused) emit(2, family === 0 ? 3 : 15);
if (icon) emit(3, family === 0 ? 5 : 3, family === 0 ? 0 : 1);
if (family === 0 ? !icon || text : !icon && text) emit(4, family === 0 ? 4 : 3, icon ? 1 : 0);
break;
case 2: // selects reserve the chevron before resolving placeholder ink
emit(0, 1); emit(1, 2); if (focused) emit(2, 6);
if (text || placeholder) emit(4, 3, text ? 0 : 2);
if (chevron) emit(3, 4, 3);
break;
case 3: case 5: { // editing fields and search/combobox chrome
const search = family === 5, limit = search ? 1 : 4;
emit(0, 1); emit(1, 2); if (focused) emit(2, search ? 14 : 7);
if (search && (flags & 524288) !== 0) emit(3, 3, 2);
if (clip) emit(9, search ? 7 : 16);
if (selectionRange) emit(5, search ? 8 : 3, (search ? 0 : 13) | (limit << 8));
if (text || placeholder) emit(4, search ? 9 : selection || composition ? 4 : 3, text ? 0 : 2);
if (selectionRange) emit(6, 0, limit);
if (compositionRange) emit(7, search ? 10 : 5, (search ? 0 : 10) | (limit << 8));
if (focused && selection && !selectionRange) emit(8, search ? 11 : 6);
if (clip) emit(10, 0);
if (search && combobox && chevron) emit(3, 12, 4);
if (search && clear) emit(3, 15, 6);
break;
}
case 4: // input group focus means focus within
emit(0, 1); emit(1, 2); if (focused) emit(2, 3);
break;
case 6:
if (w.getFloat32(20, true) !== 0 || w.getFloat32(24, true) !== 0 || w.getFloat32(28, true) !== 0) emit(14, 1);
emit(0, 2); if (text) emit(4, 3);
break;
case 7: case 8: // menu attention and commitment remain independent
if (w.getFloat32(16, true) > 0) emit(0, 1);
if (family === 8 && focused) emit(2, 2);
if (icon) emit(3, 4);
emit(4, 3, icon ? 1 : 0);
if (family === 7 && committed && check) emit(3, 12, 5);
break;
case 9: case 10: // cell chrome is shared with span-carrying cells
if (w.getFloat32(16, true) > 0) emit(0, 1);
if (border) emit(1, 2);
if (focused) emit(2, 3);
if (family === 10 && text) emit(4, 4);
break;
case 11:
if (!underline && selected) { emit(0, 1, 1); emit(1, 2); }
else if (background) emit(0, 1);
if (underline && selected && bar) emit(0, 2, 3);
if (focused) emit(2, 4);
if (underline && icon) emit(3, 5);
if (!(underline && icon) || text) emit(4, 3, underline && icon ? 1 : 0);
break;
case 12: case 13:
emit(0, 1, family === 12 && selected ? 1 : 0);
emit(1, 2, family === 12 && selected ? 1 : 0);
if (focused) emit(2, 3);
if (selected) emit(11, 4, family === 12 ? 0 : 1);
if (text) emit(4, family === 12 ? 6 : 5);
break;
case 14:
emit(0, 1, selected ? 1 : 0);
if (w.getFloat32(12, true) > 0) emit(1, 2);
emit(0, 3, selected ? 5 : 4);
if (focused) emit(2, 4);
if (text) emit(4, 5);
break;
case 15:
emit(0, 1);
if (bar) emit(0, 2, 3);
emit(0, 3, 4); emit(1, 4, 1); if (focused) emit(2, 5, 2);
break;
case 16:
if (background) emit(0, 1);
if (bar) emit(0, 2, 3);
break;
}
out.setUint32(4, count, true); return result;
}
1 change: 1 addition & 0 deletions packages/core/src/runtime_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export function native_window_policy(request: Uint8Array): Uint8Array {
if (request[0] === 42) return nscvLayoutRetained(request);
if (request[0] === 43) return nscvRenderRecipe(request);
if (request[0] === 44) return nscvRenderChildren(request);
if (request[0] === 45) return nscvControlCommands(request);
const data = new DataView(request.buffer, request.byteOffset, request.byteLength);
let at = 0;
const byte = (): number => {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/view_frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -931,4 +931,5 @@ const viewPrelude = "\n// Portable Native components compiled beside the committ
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("./control_commands.ts", import.meta.url), "utf8") +
readFileSync(new URL("./stream_policy.ts", import.meta.url), "utf8");
60 changes: 60 additions & 0 deletions packages/core/test/control_commands.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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/control_commands.ts", import.meta.url), "utf8");
const { nscvControlCommands: program } = await import(`data:text/javascript;base64,${Buffer.from(stripTypeScriptTypes(source) + "\nexport {nscvControlCommands};").toString("base64")}`);
function packet(family: number, flags = 0, value = 0, stroke = 0): Uint8Array {
const b = new Uint8Array(32), w = new DataView(b.buffer); b.set([45, 1, family, 0]);
w.setUint32(4, flags, true); w.setFloat32(8, value, true); w.setFloat32(12, stroke, true); return b;
}
function commands(b: Uint8Array): number[][] {
const r = program(b), w = new DataView(r.buffer, r.byteOffset, r.byteLength);
assert.equal(r.length, 200); assert.equal(w.getUint32(0, true), 1);
const count = w.getUint32(4, true); assert.ok(count <= 16); assert.ok(r.subarray(8 + count * 12).every(v => v === 0));
return Array.from({ length: count }, (_, i) => [0, 4, 8].map(j => w.getUint32(8 + i * 12 + j, true)));
}
test("button seams preserve fill clip stroke unclip focus icon and label order", () => {
assert.deepEqual(commands(packet(0, 1 | 2 | 8 | 131072, 0, 1)), [[0,1,0],[12,0,0],[1,2,0],[13,0,0],[2,3,0],[3,5,0],[4,4,1]]);
assert.deepEqual(commands(packet(0, 8, 0, 0)), [[0,1,0],[3,5,0]]);
assert.deepEqual(commands(packet(0)), [[0,1,0],[4,4,0]]);
assert.deepEqual(commands(packet(1, 1 | 2)), [[0,1,0],[2,15,0],[4,3,0]]);
});
test("editing overlays stay inside content clips while search chrome stays outside", () => {
const flags = 1 | 2 | 128 | 256 | 512 | 1024 | 2048;
assert.deepEqual(commands(packet(3, flags)), [[0,1,0],[1,2,0],[2,7,0],[9,16,0],[5,3,1037],[4,4,0],[6,0,4],[7,5,1034],[10,0,0]]);
assert.deepEqual(commands(packet(5, flags | 16384 | 32768 | 262144 | 524288)), [[0,1,0],[1,2,0],[2,14,0],[3,3,2],[9,7,0],[5,8,256],[4,9,0],[6,0,1],[7,10,256],[10,0,0],[3,12,4],[3,15,6]]);
assert.deepEqual(commands(packet(3, 1 | 4 | 256)), [[0,1,0],[1,2,0],[2,7,0],[4,4,2],[8,6,0]]);
});
test("menu attention and committed marks remain independent and never add a focus ring", () => {
const b = packet(7, 1 | 2 | 8 | 16 | 65536); new DataView(b.buffer).setFloat32(16, 0.5, true);
assert.deepEqual(commands(b), [[0,1,0],[3,4,0],[4,3,1],[3,12,5]]);
new DataView(b.buffer).setFloat32(16, 0, true);
assert.deepEqual(commands(b), [[3,4,0],[4,3,1],[3,12,5]]);
});
test("selection numeric thresholds preserve overrides and exceptional float behavior", () => {
for (const family of [12,13,14]) for (const value of [0,0.49999997,0.5,1,NaN,Infinity,-Infinity]) for (const override of [false,true]) {
const p = commands(packet(family, 1 | 2 | (override ? 16 : 0), value));
const selected = override || Math.fround(value) >= 0.5;
if (family < 14) assert.equal(p.some(c => c[0] === 11), selected);
else assert.equal(p.find(c => c[0] === 0 && c[1] === 3)?.[2], selected ? 5 : 4);
}
assert.deepEqual(commands(packet(15, 0)), [[0,1,0],[0,3,4],[1,4,1]]);
assert.deepEqual(commands(packet(16, 32 | 8192)), [[0,1,0],[0,2,3]]);
});
test("tab pill and underline registers preserve icon and empty-label contracts", () => {
assert.deepEqual(commands(packet(11, 16)), [[0,1,1],[1,2,0],[4,3,0]]);
assert.deepEqual(commands(packet(11, 16 | 4096 | 8192 | 8)), [[0,2,3],[3,5,0]]);
assert.deepEqual(commands(packet(11, 32 | 4096 | 8 | 2)), [[0,1,0],[3,5,0],[4,3,1]]);
});
test("command packets are bounded and copied across all families and editing observations", () => {
for (let family = 0; family <= 16; family++) for (let flags = 0; flags < 2048; flags++) {
if (flags & 512 && !(flags & 256)) continue;
const b = packet(family, flags, 0.5, 1), saved = b.slice(), r = program(b), copy = r.slice();
commands(b); assert.deepEqual(b, saved); b.fill(255); assert.deepEqual(r, copy);
}
for (const at of [0,1,2,3,6,7]) { const b=packet(3);b[at]=255;assert.throws(()=>program(b),/invalid/); }
assert.throws(()=>program(packet(3,512)),/editing/);
assert.throws(()=>program(packet(3,2048)),/editing/);
assert.throws(()=>program(new Uint8Array(31)),/header/);
});
2 changes: 1 addition & 1 deletion packages/core/test/view_frontend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", "render_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", "control_commands.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);
Expand Down
75 changes: 75 additions & 0 deletions src/primitives/canvas/control_command_policy.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! Copied control command programs. The drawing host owns all emitted storage.
const std = @import("std");
pub const Policy = @import("surface_layout_policy.zig").Policy;
pub const Family = enum(u8) { button, icon_button, select, text_field, input_group, search, tooltip, menu_item, list_item, cell_chrome, cell, segmented, checkbox, radio, toggle, slider, progress };
pub const Opcode = enum(u32) { fill, stroke, focus, icon, text, selection, selected_glyphs, composition, caret, clip, unclip, mark, seam_clip, seam_unclip, shadow };
pub const Facts = struct {
focused: bool = false,
text: bool = false,
placeholder: bool = false,
icon: bool = false,
selected: bool = false,
background: bool = false,
border: bool = false,
clip: bool = false,
selection: bool = false,
selection_range: bool = false,
composition: bool = false,
composition_range: bool = false,
underline: bool = false,
bar: bool = false,
clear: bool = false,
chevron: bool = false,
check: bool = false,
seam: bool = false,
combobox: bool = false,
search_icon: bool = false,
value: f32 = 0,
stroke_width: f32 = 0,
wash_alpha: f32 = 0,
shadow_y: f32 = 0,
shadow_blur: f32 = 0,
shadow_spread: f32 = 0,
};
pub const Command = struct { opcode: Opcode, slot: u32, variant: u32 };
pub const Program = struct {
count: usize,
commands: [16]Command,
pub fn init(policy: Policy, family: Family, facts: Facts) Program {
var request: [32]u8 = @splat(0);
request[0..4].* = .{ 45, 1, @intFromEnum(family), 0 };
var flags: u32 = 0;
inline for (std.meta.fields(Facts), 0..) |field, i| {
if (field.type == bool) flags |= @as(u32, @intFromBool(@field(facts, field.name))) << @intCast(i);
}
std.mem.writeInt(u32, request[4..8], flags, .little);
for ([_]f32{ facts.value, facts.stroke_width, facts.wash_alpha, facts.shadow_y, facts.shadow_blur, facts.shadow_spread }, 0..) |value, i|
std.mem.writeInt(u32, request[8 + i * 4 ..][0..4], @bitCast(value), .little);
var result: [200]u8 = @splat(0xa5);
if (policy(&request, &result) != result.len or read(&result, 0) != 1 or read(&result, 4) > 16) @panic("invalid control command result");
const count = read(&result, 4);
if (!std.mem.allEqual(u8, result[8 + count * 12 ..], 0)) @panic("invalid control command tail");
var program: Program = .{ .count = count, .commands = undefined };
for (program.commands[0..count], 0..) |*command, i| {
const at = 8 + i * 12;
const op = read(&result, at);
const slot = read(&result, at + 4);
const variant = read(&result, at + 8);
if (op > @intFromEnum(Opcode.shadow) or slot > 16) @panic("invalid control command capability");
const valid = switch (@as(Opcode, @enumFromInt(op))) {
.selection, .composition => variant == 256 or variant == 1034 or variant == 1037,
.selected_glyphs => variant == 1 or variant == 4,
.icon => variant <= 6,
.fill => variant <= 5,
.stroke, .text, .focus, .mark => variant <= 2,
else => variant == 0,
};
if (!valid) @panic("invalid control command operands");
command.* = .{ .opcode = @enumFromInt(op), .slot = slot, .variant = variant };
}
return program;
}
};
fn read(bytes: []const u8, at: usize) u32 {
return std.mem.readInt(u32, bytes[at..][0..4], .little);
}
1 change: 1 addition & 0 deletions src/primitives/canvas/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,7 @@ pub const measurement_coordination_policy = @import("measurement_coordination_po
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 control_command_policy = @import("control_command_policy.zig");
pub const widget_audit_policy = @import("widget_audit_policy.zig");
pub const a11y = @import("a11y_audit.zig");
pub const expectA11yAuditSweepClean = a11y.expectA11yAuditSweepClean;
Expand Down
2 changes: 2 additions & 0 deletions src/primitives/canvas/tokens.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1917,6 +1917,8 @@ pub const DesignTokens = struct {
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,
/// Portable control primitive scheduling, part slots and editing overlays.
control_command_policy: ?@import("control_command_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,
Expand Down
Loading
Loading