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
38 changes: 14 additions & 24 deletions ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ const PROP_BORDER = 0x08;
const PROP_CLIP = 0x10;
const PROP_FLOATING = 0x20;

/* ── Packing ──────────────────────────────────────────────────────── */

const encoder = new TextEncoder();

function packAxis(view: DataView, offset: number, axis: SizingAxis): number {
Expand Down Expand Up @@ -73,18 +71,18 @@ export function pack(
let o = offset;

for (let op of ops) {
switch (op.id) {
switch (op.directive) {
case OP_CLOSE_ELEMENT:
view.setUint32(o, op.id, true);
view.setUint32(o, op.directive, true);
o += 4;
break;

case OP_OPEN_ELEMENT: {
view.setUint32(o, OP_OPEN_ELEMENT, true);
o += 4;

let id = encoder.encode(op.name);
o = packString(view, id, o);
let bytes = encoder.encode(op.id);
o = packString(view, bytes, o);

let mask = 0;
if (op.layout) mask |= PROP_LAYOUT;
Expand Down Expand Up @@ -210,15 +208,11 @@ export function pack(
return (o - offset) / 4;
}

/* ── Color ────────────────────────────────────────────────────────── */

export function rgba(r: number, g: number, b: number, a = 255): number {
return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) |
(b & 0xFF);
}

/* ── Sizing axis types ────────────────────────────────────────────── */

export type SizingAxis =
| { type: "fit"; min?: number; max?: number }
| { type: "grow"; min?: number; max?: number }
Expand All @@ -241,15 +235,13 @@ export const percent = (value: number): SizingAxis => ({
});
export const fixed = (value: number): SizingAxis => ({ type: "fixed", value });

/* ── Op descriptors ───────────────────────────────────────────────── */

export interface CloseElement {
id: typeof OP_CLOSE_ELEMENT;
directive: typeof OP_CLOSE_ELEMENT;
}

export interface OpenElement {
id: typeof OP_OPEN_ELEMENT;
name: string;
directive: typeof OP_OPEN_ELEMENT;
id: string;
layout?: {
width?: SizingAxis;
height?: SizingAxis;
Expand Down Expand Up @@ -280,7 +272,7 @@ export interface OpenElement {
}

export interface Text {
id: typeof OP_TEXT;
directive: typeof OP_TEXT;
content: string;
color?: number;
fontSize?: number;
Expand All @@ -291,22 +283,20 @@ export interface Text {

export type Op = OpenElement | Text | CloseElement;

/* ── Descriptor constructors ──────────────────────────────────────── */

export function open(
name: string,
props: Omit<OpenElement, "id" | "name"> = {},
id: string,
props: Omit<OpenElement, "directive" | "id"> = {},
): OpenElement {
return { id: OP_OPEN_ELEMENT, name, ...props };
return { directive: OP_OPEN_ELEMENT, id, ...props };
}

export function text(
content: string,
props: Omit<Text, "id" | "content"> = {},
props: Omit<Text, "directive" | "content"> = {},
): Text {
return { id: OP_TEXT, content, ...props };
return { directive: OP_TEXT, content, ...props };
}

export function close(): CloseElement {
return { id: OP_CLOSE_ELEMENT };
return { directive: OP_CLOSE_ELEMENT };
}
28 changes: 10 additions & 18 deletions specs/renderer-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,7 @@ stages. The caller MUST NOT need to perform any of these operations.
symmetric: both calls occur within the same render transaction, in the same
function scope.

**INV-7. Element identity disambiguation.** When multiple elements within a
frame share the same id, the renderer MUST disambiguate their identities so that
the layout engine does not conflate them. The disambiguation mechanism is an
implementation detail, but the guarantee is normative: identical ids MUST NOT
cause layout corruption or element conflation.

**INV-8. Separation of concerns.** The rendering concern and the input-parsing
**INV-7. Separation of concerns.** The rendering concern and the input-parsing
concern MUST remain independent. Neither MUST depend on the other's state,
types, or API surface. They MAY share a compiled WASM binary for loading
efficiency, but this is an implementation convenience, not an architectural
Expand Down Expand Up @@ -374,7 +368,8 @@ open(id: string, props?): OpenElement

Creates an element-open directive. The `id` parameter provides an identity for
the element within the frame, used by the underlying layout engine for element
tracking and hit-testing. The optional `props` parameter carries configuration
tracking and hit-testing. IDs MUST be unique within a frame; passing duplicate
IDs is undefined behavior. The optional `props` parameter carries configuration
for layout, styling, and behavior.

Elements opened with `open()` MUST be closed with a corresponding `close()`
Expand Down Expand Up @@ -473,12 +468,10 @@ transfer mechanism is an implementation detail described in Section 12.1.

### 9.3 Directive identity

Each element directive is assigned an identity within the frame for use by the
underlying layout engine. When multiple elements share the same id (the `id`
parameter to `open()`), the renderer MUST disambiguate their identities
automatically. The disambiguation mechanism is an implementation detail. The
normative requirement is that the caller MUST NOT need to provide globally
unique ids; the renderer handles uniqueness internally.
Each element directive carries an `id` provided by the caller via `open()`.
Element IDs MUST be unique within a frame. The renderer uses the ID directly as
the element's identity for the layout engine. Passing duplicate IDs within a
single frame is undefined behavior.

---

Expand All @@ -501,10 +494,9 @@ renderer processes directives in the order they appear in the array.

### 10.3 Element identity within a frame

Within a single frame, each element MUST have an unambiguous identity for the
layout engine. As specified in Section 9.3, the renderer handles disambiguation.
Two elements with the same id in the same frame MUST NOT cause layout
corruption, hash collision, or identity conflation.
Within a single frame, each element MUST have a unique identity for the layout
engine. As specified in Section 9.3, element IDs MUST be unique within a frame.
Passing duplicate IDs is undefined behavior.

### 10.4 No cross-frame identity

Expand Down
3 changes: 1 addition & 2 deletions src/clayterm.c
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,6 @@ struct Clayterm *init(void *mem, int w, int h) {

void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) {
int i = 0;
uint32_t idx = 0;

Clay_BeginLayout();

Expand All @@ -454,7 +453,7 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) {

if (id_len > 0) {
Clay_String str = {.length = (int32_t)id_len, .chars = id_chars};
Clay_ElementId eid = Clay__HashString(str, idx++);
Clay_ElementId eid = Clay__HashString(str, 0);
Clay__OpenElementWithId(eid);
} else {
Clay__OpenElement();
Expand Down
14 changes: 7 additions & 7 deletions test/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ describe("validate", () => {
expect(validate([])).toBe(true);
});

it("rejects ops with wrong id", () => {
expect(validate([{ id: 0xff }])).toBe(false);
it("rejects ops with wrong directive", () => {
expect(validate([{ directive: 0xff }])).toBe(false);
});

it("rejects open element missing name", () => {
expect(validate([{ id: 0x02 }])).toBe(false);
it("rejects open element missing id", () => {
expect(validate([{ directive: 0x02 }])).toBe(false);
});

it("rejects text missing content", () => {
expect(validate([{ id: 0x03 }])).toBe(false);
expect(validate([{ directive: 0x03 }])).toBe(false);
});

it("rejects non-array", () => {
Expand All @@ -40,7 +40,7 @@ describe("validate", () => {
});

it("assert throws TypeError on bad input", () => {
expect(() => assert([{ id: 0x02 }])).toThrow(TypeError);
expect(() => assert([{ directive: 0x02 }])).toThrow(TypeError);
});

it("rejects padding > 255 (u8 overflow)", () => {
Expand Down Expand Up @@ -106,6 +106,6 @@ describe("validated", () => {

it("throws on invalid ops", () => {
// deno-lint-ignore no-explicit-any
expect(() => term.render([{ id: 0xff }] as any)).toThrow(TypeError);
expect(() => term.render([{ directive: 0xff }] as any)).toThrow(TypeError);
});
});
10 changes: 5 additions & 5 deletions validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ const Floating = Type.Object({
zIndex: Type.Optional(u16),
});

/* ── Op types (discriminated on `id`) ─────────────────────────────── */
/* ── Op types (discriminated on `directive`) ──────────────────────── */

const CloseElement = Type.Object({ id: Type.Literal(0x04) });
const CloseElement = Type.Object({ directive: Type.Literal(0x04) });

const OpenElement = Type.Object({
id: Type.Literal(0x02),
name: Type.String(),
directive: Type.Literal(0x02),
id: Type.String(),
layout: Type.Optional(Layout),
bg: Type.Optional(rgba),
cornerRadius: Type.Optional(CornerRadius),
Expand All @@ -105,7 +105,7 @@ const OpenElement = Type.Object({
});

const TextOp = Type.Object({
id: Type.Literal(0x03),
directive: Type.Literal(0x03),
content: Type.String(),
color: Type.Optional(rgba),
fontSize: Type.Optional(u8),
Expand Down
Loading