diff --git a/ops.ts b/ops.ts index 3344eea..7f327f8 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), + encodeAttachTo(f.attachTo) | + (encodeAttachPoint(f.attachPoints?.element) << 8) | + (encodeAttachPoint(f.attachPoints?.parent) << 16) | + (encodePointerCaptureMode(f.pointerCaptureMode) << 24), + true, + ); + o += 4; + view.setUint32( + o, + encodeClipTo(f.clipTo) | (((f.zIndex ?? 0) & 0xffff) << 8), true, ); o += 4; @@ -264,13 +276,80 @@ export interface OpenElement { floating?: { x?: number; y?: number; + expand?: { width?: number; height?: number }; parent?: number; - attachTo?: number; - attachPoints?: number; + attachTo?: AttachTo; + attachPoints?: { element?: AttachPoint; parent?: AttachPoint }; + pointerCaptureMode?: PointerCaptureMode; + clipTo?: ClipTo; zIndex?: number; }; } +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; content: string; diff --git a/specs/renderer-spec.md b/specs/renderer-spec.md index fa4276a..0fe559e 100644 --- a/specs/renderer-spec.md +++ b/specs/renderer-spec.md @@ -609,10 +609,58 @@ 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?: "none" | "parent" | "element" | "root"; + attachPoints?: { + 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?: "capture" | "passthrough"; + clipTo?: "none" | "attached-parent"; + zIndex?: number; +} +``` + +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`, `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..06b970d 100644 --- a/test/term.test.ts +++ b/test/term.test.ts @@ -191,6 +191,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: "root", + attachPoints: { + element: "center-center", + parent: "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..35bcf08 100644 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -78,6 +78,44 @@ 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: "center-center", parent: "center-center" }, + }, + }), + close(), + ])).toBe(true); + }); + + it("accepts floating expand and clipping fields", () => { + expect(validate([ + open("x", { + floating: { + expand: { width: 2, height: 3 }, + pointerCaptureMode: "passthrough", + clipTo: "attached-parent", + zIndex: 1024, + }, + }), + close(), + ])).toBe(true); + }); + + 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 } }), + close(), + ])).toBe(false); + }); }); describe("validated", () => { diff --git a/validate.ts b/validate.ts index 248ea48..8d2ad63 100644 --- a/validate.ts +++ b/validate.ts @@ -80,12 +80,50 @@ 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()), + 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), + attachTo: Type.Optional(AttachTo), + attachPoints: Type.Optional(Type.Object({ + element: Type.Optional(AttachPoint), + parent: Type.Optional(AttachPoint), + })), + pointerCaptureMode: Type.Optional(PointerCaptureMode), + clipTo: Type.Optional(ClipTo), zIndex: Type.Optional(u16), });