From bd2dcc9d10f52b46644c50d44532a2f8f7a0c65e Mon Sep 17 00:00:00 2001 From: Ryan Rauh Date: Sat, 9 May 2026 08:04:28 -0400 Subject: [PATCH 1/2] feat: expand floating parameters --- ops.ts | 50 +++++++++++++++++++++++++++++++++++--- specs/renderer-spec.md | 30 +++++++++++++++++++++-- src/clayterm.c | 8 +++++- test/term.test.ts | 55 +++++++++++++++++++++++++++++++++++++++++- test/validate.test.ts | 33 +++++++++++++++++++++++++ validate.ts | 11 ++++++++- 6 files changed, 179 insertions(+), 8 deletions(-) diff --git a/ops.ts b/ops.ts index 3344eea..9797c5b 100644 --- a/ops.ts +++ b/ops.ts @@ -163,12 +163,24 @@ export function pack( o += 4; view.setFloat32(o, f.y ?? 0, true); o += 4; + view.setFloat32(o, f.expand?.width ?? 0, true); + o += 4; + view.setFloat32(o, f.expand?.height ?? 0, true); + o += 4; view.setUint32(o, f.parent ?? 0, true); o += 4; view.setUint32( o, - (f.attachTo ?? 0) | ((f.attachPoints ?? 0) << 8) | - ((f.zIndex ?? 0) << 16), + (f.attachTo ?? 0) | + ((f.attachPoints?.element ?? 0) << 8) | + ((f.attachPoints?.parent ?? 0) << 16) | + ((f.pointerCaptureMode ?? 0) << 24), + true, + ); + o += 4; + view.setUint32( + o, + (f.clipTo ?? 0) | (((f.zIndex ?? 0) & 0xffff) << 8), true, ); o += 4; @@ -264,13 +276,45 @@ export interface OpenElement { floating?: { x?: number; y?: number; + expand?: { width?: number; height?: number }; parent?: number; attachTo?: number; - attachPoints?: number; + attachPoints?: { element?: number; parent?: number }; + pointerCaptureMode?: number; + clipTo?: number; zIndex?: number; }; } +export const ATTACH_POINT = { + LEFT_TOP: 0, + LEFT_CENTER: 1, + LEFT_BOTTOM: 2, + CENTER_TOP: 3, + CENTER_CENTER: 4, + CENTER_BOTTOM: 5, + RIGHT_TOP: 6, + RIGHT_CENTER: 7, + RIGHT_BOTTOM: 8, +} as const; + +export const ATTACH_TO = { + NONE: 0, + PARENT: 1, + ELEMENT_WITH_ID: 2, + ROOT: 3, +} as const; + +export const POINTER_CAPTURE_MODE = { + CAPTURE: 0, + PASSTHROUGH: 1, +} as const; + +export const CLIP_TO = { + NONE: 0, + ATTACHED_PARENT: 1, +} as const; + export interface Text { directive: typeof OP_TEXT; content: string; diff --git a/specs/renderer-spec.md b/specs/renderer-spec.md index fa4276a..079363c 100644 --- a/specs/renderer-spec.md +++ b/specs/renderer-spec.md @@ -609,10 +609,36 @@ The `open()` constructor currently accepts the following property groups in its - **`cornerRadius`** — per-corner radius values, producing rounded box-drawing characters - **`clip`** — clip region configuration for scroll containers -- **`floating`** — floating-element configuration (offset, parent reference, - attach points, z-index) +- **`floating`** — floating-element configuration (offset, expansion, parent + reference, attach target, structured attach points, pointer capture mode, clip + target, z-index) - **`scroll`** — scroll container configuration +The current floating surface is: + +```ts +floating?: { + x?: number; + y?: number; + expand?: { width?: number; height?: number }; + parent?: number; + attachTo?: number; + attachPoints?: { + element?: number; + parent?: number; + }; + pointerCaptureMode?: number; + clipTo?: number; + zIndex?: number; +} +``` + +This shape extends the earlier floating surface in two ways. First, +`attachPoints` is structured as separate element and parent anchor values +instead of a single packed enum. Second, the surface exposes additional Clay +floating controls that were previously unavailable at the TypeScript layer: +`expand`, `pointerCaptureMode`, and `clipTo`. + The `text()` constructor currently accepts: `color`, `fontSize`, `letterSpacing`, `lineHeight`, and attribute flags (`bold`, `italic`, `underline`, `strikethrough`). diff --git a/src/clayterm.c b/src/clayterm.c index 2af9afd..36c2670 100644 --- a/src/clayterm.c +++ b/src/clayterm.c @@ -546,13 +546,19 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) { if (mask & PROP_FLOATING) { decl.floating.offset.x = rdf(buf, len, &i); decl.floating.offset.y = rdf(buf, len, &i); + decl.floating.expand.width = rdf(buf, len, &i); + decl.floating.expand.height = rdf(buf, len, &i); decl.floating.parentId = rd(buf, len, &i); uint32_t fc = rd(buf, len, &i); decl.floating.attachTo = fc & 0xff; decl.floating.attachPoints.element = (fc >> 8) & 0xff; decl.floating.attachPoints.parent = (fc >> 16) & 0xff; - decl.floating.zIndex = (int16_t)((fc >> 24) & 0xff); + decl.floating.pointerCaptureMode = (fc >> 24) & 0xff; + + uint32_t fd = rd(buf, len, &i); + decl.floating.clipTo = fd & 0xff; + decl.floating.zIndex = (int16_t)(fd >> 8); } Clay__ConfigureOpenElement(decl); diff --git a/test/term.test.ts b/test/term.test.ts index 47d8c94..16ace04 100644 --- a/test/term.test.ts +++ b/test/term.test.ts @@ -1,6 +1,15 @@ import { beforeEach, describe, expect, it } from "./suite.ts"; import { createTerm, type Term } from "../term.ts"; -import { close, fixed, grow, open, rgba, text } from "../ops.ts"; +import { + ATTACH_POINT, + ATTACH_TO, + close, + fixed, + grow, + open, + rgba, + text, +} from "../ops.ts"; import { print } from "./print.ts"; const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes); @@ -191,6 +200,50 @@ describe("term", () => { }); }); + it("renders a floating frame with structured attach points", () => { + let out = print( + decode( + term.render([ + open("root", { + layout: { width: fixed(40), height: fixed(10), direction: "ttb" }, + }), + open("frame", { + layout: { + width: fixed(12), + height: fixed(5), + direction: "ttb", + padding: { left: 1, top: 1 }, + }, + border: { + color: rgba(255, 255, 255), + left: 1, + right: 1, + top: 1, + bottom: 1, + }, + floating: { + x: 3, + y: 1, + attachTo: ATTACH_TO.ROOT, + attachPoints: { + element: ATTACH_POINT.CENTER_CENTER, + parent: ATTACH_POINT.CENTER_CENTER, + }, + }, + }), + text("box"), + close(), + close(), + ]).output, + ), + 40, + 10, + ); + + expect(out).toContain("│box │"); + expect(out).toContain("┌──────────┐"); + }); + describe("row offset", () => { it("renders two frames at the offset position", async () => { let term = await createTerm({ width: 20, height: 5 }); diff --git a/test/validate.test.ts b/test/validate.test.ts index 8db4af9..3991288 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -78,6 +78,39 @@ describe("validate", () => { it("rejects fractional color", () => { expect(validate([text("hi", { color: 1.5 })])).toBe(false); }); + + it("accepts structured floating attach points", () => { + expect(validate([ + open("x", { + floating: { + attachPoints: { element: 4, parent: 4 }, + }, + }), + close(), + ])).toBe(true); + }); + + it("accepts floating expand and clipping fields", () => { + expect(validate([ + open("x", { + floating: { + expand: { width: 2, height: 3 }, + pointerCaptureMode: 1, + clipTo: 1, + zIndex: 1024, + }, + }), + close(), + ])).toBe(true); + }); + + it("rejects numeric floating attachPoints legacy shape", () => { + expect(validate([ + // deno-lint-ignore no-explicit-any + open("x", { floating: { attachPoints: 4 as any } }), + close(), + ])).toBe(false); + }); }); describe("validated", () => { diff --git a/validate.ts b/validate.ts index 248ea48..1e697a2 100644 --- a/validate.ts +++ b/validate.ts @@ -83,9 +83,18 @@ const Clip = Type.Object({ const Floating = Type.Object({ x: Type.Optional(Type.Number()), y: Type.Optional(Type.Number()), + expand: Type.Optional(Type.Object({ + width: Type.Optional(Type.Number()), + height: Type.Optional(Type.Number()), + })), parent: Type.Optional(Type.Integer({ minimum: 0 })), attachTo: Type.Optional(u8), - attachPoints: Type.Optional(u8), + attachPoints: Type.Optional(Type.Object({ + element: Type.Optional(u8), + parent: Type.Optional(u8), + })), + pointerCaptureMode: Type.Optional(u8), + clipTo: Type.Optional(u8), zIndex: Type.Optional(u16), }); From ea34f5bdf292bebe0ebd4883a862998967ccf080 Mon Sep 17 00:00:00 2001 From: Ryan Rauh Date: Sun, 10 May 2026 07:07:51 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20use=20string=20literal?= =?UTF-8?q?s=20for=20floating=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ops.ts | 109 +++++++++++++++++++++++++++-------------- specs/renderer-spec.md | 42 ++++++++++++---- test/term.test.ts | 17 ++----- test/validate.test.ts | 13 +++-- validate.ts | 39 +++++++++++++-- 5 files changed, 151 insertions(+), 69 deletions(-) diff --git a/ops.ts b/ops.ts index 9797c5b..7f327f8 100644 --- a/ops.ts +++ b/ops.ts @@ -171,16 +171,16 @@ export function pack( o += 4; view.setUint32( o, - (f.attachTo ?? 0) | - ((f.attachPoints?.element ?? 0) << 8) | - ((f.attachPoints?.parent ?? 0) << 16) | - ((f.pointerCaptureMode ?? 0) << 24), + encodeAttachTo(f.attachTo) | + (encodeAttachPoint(f.attachPoints?.element) << 8) | + (encodeAttachPoint(f.attachPoints?.parent) << 16) | + (encodePointerCaptureMode(f.pointerCaptureMode) << 24), true, ); o += 4; view.setUint32( o, - (f.clipTo ?? 0) | (((f.zIndex ?? 0) & 0xffff) << 8), + encodeClipTo(f.clipTo) | (((f.zIndex ?? 0) & 0xffff) << 8), true, ); o += 4; @@ -278,42 +278,77 @@ export interface OpenElement { y?: number; expand?: { width?: number; height?: number }; parent?: number; - attachTo?: number; - attachPoints?: { element?: number; parent?: number }; - pointerCaptureMode?: number; - clipTo?: number; + attachTo?: AttachTo; + attachPoints?: { element?: AttachPoint; parent?: AttachPoint }; + pointerCaptureMode?: PointerCaptureMode; + clipTo?: ClipTo; zIndex?: number; }; } -export const ATTACH_POINT = { - LEFT_TOP: 0, - LEFT_CENTER: 1, - LEFT_BOTTOM: 2, - CENTER_TOP: 3, - CENTER_CENTER: 4, - CENTER_BOTTOM: 5, - RIGHT_TOP: 6, - RIGHT_CENTER: 7, - RIGHT_BOTTOM: 8, -} as const; - -export const ATTACH_TO = { - NONE: 0, - PARENT: 1, - ELEMENT_WITH_ID: 2, - ROOT: 3, -} as const; - -export const POINTER_CAPTURE_MODE = { - CAPTURE: 0, - PASSTHROUGH: 1, -} as const; - -export const CLIP_TO = { - NONE: 0, - ATTACHED_PARENT: 1, -} as const; +export type AttachPoint = + | "left-top" + | "left-center" + | "left-bottom" + | "center-top" + | "center-center" + | "center-bottom" + | "right-top" + | "right-center" + | "right-bottom"; + +export type AttachTo = "none" | "parent" | "element" | "root"; + +export type PointerCaptureMode = "capture" | "passthrough"; + +export type ClipTo = "none" | "attached-parent"; + +const ATTACH_POINT: Record = { + "left-top": 0, + "left-center": 1, + "left-bottom": 2, + "center-top": 3, + "center-center": 4, + "center-bottom": 5, + "right-top": 6, + "right-center": 7, + "right-bottom": 8, +}; + +const ATTACH_TO: Record = { + none: 0, + parent: 1, + element: 2, + root: 3, +}; + +const POINTER_CAPTURE_MODE: Record = { + capture: 0, + passthrough: 1, +}; + +const CLIP_TO: Record = { + none: 0, + "attached-parent": 1, +}; + +function encodeAttachPoint(value: AttachPoint | undefined): number { + return value === undefined ? 0 : ATTACH_POINT[value]; +} + +function encodeAttachTo(value: AttachTo | undefined): number { + return value === undefined ? 0 : ATTACH_TO[value]; +} + +function encodePointerCaptureMode( + value: PointerCaptureMode | undefined, +): number { + return value === undefined ? 0 : POINTER_CAPTURE_MODE[value]; +} + +function encodeClipTo(value: ClipTo | undefined): number { + return value === undefined ? 0 : CLIP_TO[value]; +} export interface Text { directive: typeof OP_TEXT; diff --git a/specs/renderer-spec.md b/specs/renderer-spec.md index 079363c..0fe559e 100644 --- a/specs/renderer-spec.md +++ b/specs/renderer-spec.md @@ -622,22 +622,44 @@ floating?: { y?: number; expand?: { width?: number; height?: number }; parent?: number; - attachTo?: number; + attachTo?: "none" | "parent" | "element" | "root"; attachPoints?: { - element?: number; - parent?: number; + element?: + | "left-top" + | "left-center" + | "left-bottom" + | "center-top" + | "center-center" + | "center-bottom" + | "right-top" + | "right-center" + | "right-bottom"; + parent?: + | "left-top" + | "left-center" + | "left-bottom" + | "center-top" + | "center-center" + | "center-bottom" + | "right-top" + | "right-center" + | "right-bottom"; }; - pointerCaptureMode?: number; - clipTo?: number; + pointerCaptureMode?: "capture" | "passthrough"; + clipTo?: "none" | "attached-parent"; zIndex?: number; } ``` -This shape extends the earlier floating surface in two ways. First, -`attachPoints` is structured as separate element and parent anchor values -instead of a single packed enum. Second, the surface exposes additional Clay -floating controls that were previously unavailable at the TypeScript layer: -`expand`, `pointerCaptureMode`, and `clipTo`. +The `floating` object configures Clay floating layout behavior. `x` and `y` +provide the floating offset. `expand` expands the floating bounds. `parent` +identifies the target element when `attachTo` is `"element"`. `attachTo` selects +whether the element is attached to no target, its parent, an element, or the +layout root. `attachPoints.element` describes the anchor on the floating +element, and `attachPoints.parent` describes the anchor on the attached target. +`pointerCaptureMode` controls whether the floating element captures pointer +input or lets it pass through, `clipTo` controls inherited clipping, and +`zIndex` controls floating order. The `text()` constructor currently accepts: `color`, `fontSize`, `letterSpacing`, `lineHeight`, and attribute flags (`bold`, `italic`, diff --git a/test/term.test.ts b/test/term.test.ts index 16ace04..06b970d 100644 --- a/test/term.test.ts +++ b/test/term.test.ts @@ -1,15 +1,6 @@ import { beforeEach, describe, expect, it } from "./suite.ts"; import { createTerm, type Term } from "../term.ts"; -import { - ATTACH_POINT, - ATTACH_TO, - close, - fixed, - grow, - open, - rgba, - text, -} from "../ops.ts"; +import { close, fixed, grow, open, rgba, text } from "../ops.ts"; import { print } from "./print.ts"; const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes); @@ -224,10 +215,10 @@ describe("term", () => { floating: { x: 3, y: 1, - attachTo: ATTACH_TO.ROOT, + attachTo: "root", attachPoints: { - element: ATTACH_POINT.CENTER_CENTER, - parent: ATTACH_POINT.CENTER_CENTER, + element: "center-center", + parent: "center-center", }, }, }), diff --git a/test/validate.test.ts b/test/validate.test.ts index 3991288..35bcf08 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -83,7 +83,7 @@ describe("validate", () => { expect(validate([ open("x", { floating: { - attachPoints: { element: 4, parent: 4 }, + attachPoints: { element: "center-center", parent: "center-center" }, }, }), close(), @@ -95,8 +95,8 @@ describe("validate", () => { open("x", { floating: { expand: { width: 2, height: 3 }, - pointerCaptureMode: 1, - clipTo: 1, + pointerCaptureMode: "passthrough", + clipTo: "attached-parent", zIndex: 1024, }, }), @@ -104,7 +104,12 @@ describe("validate", () => { ])).toBe(true); }); - it("rejects numeric floating attachPoints legacy shape", () => { + it("rejects numeric floating enum values", () => { + expect(validate([ + // deno-lint-ignore no-explicit-any + open("x", { floating: { attachTo: 3 as any } }), + close(), + ])).toBe(false); expect(validate([ // deno-lint-ignore no-explicit-any open("x", { floating: { attachPoints: 4 as any } }), diff --git a/validate.ts b/validate.ts index 1e697a2..8d2ad63 100644 --- a/validate.ts +++ b/validate.ts @@ -80,6 +80,35 @@ const Clip = Type.Object({ vertical: Type.Optional(Type.Boolean()), }); +const AttachPoint = Type.Union([ + Type.Literal("left-top"), + Type.Literal("left-center"), + Type.Literal("left-bottom"), + Type.Literal("center-top"), + Type.Literal("center-center"), + Type.Literal("center-bottom"), + Type.Literal("right-top"), + Type.Literal("right-center"), + Type.Literal("right-bottom"), +]); + +const AttachTo = Type.Union([ + Type.Literal("none"), + Type.Literal("parent"), + Type.Literal("element"), + Type.Literal("root"), +]); + +const PointerCaptureMode = Type.Union([ + Type.Literal("capture"), + Type.Literal("passthrough"), +]); + +const ClipTo = Type.Union([ + Type.Literal("none"), + Type.Literal("attached-parent"), +]); + const Floating = Type.Object({ x: Type.Optional(Type.Number()), y: Type.Optional(Type.Number()), @@ -88,13 +117,13 @@ const Floating = Type.Object({ height: Type.Optional(Type.Number()), })), parent: Type.Optional(Type.Integer({ minimum: 0 })), - attachTo: Type.Optional(u8), + attachTo: Type.Optional(AttachTo), attachPoints: Type.Optional(Type.Object({ - element: Type.Optional(u8), - parent: Type.Optional(u8), + element: Type.Optional(AttachPoint), + parent: Type.Optional(AttachPoint), })), - pointerCaptureMode: Type.Optional(u8), - clipTo: Type.Optional(u8), + pointerCaptureMode: Type.Optional(PointerCaptureMode), + clipTo: Type.Optional(ClipTo), zIndex: Type.Optional(u16), });