[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

Select the types of activity you want to include in your feed.

Merge PR #29 floating parameters

Ryan Rauh (May 31, 2026, 10:14 AM EDT) d4176b81 122adb43

+287 -10
+83 -4
ops.ts
··· 185 185 o += 4; 186 186 view.setFloat32(o, f.y ?? 0, true); 187 187 o += 4; 188 + view.setFloat32(o, f.expand?.width ?? 0, true); 189 + o += 4; 190 + view.setFloat32(o, f.expand?.height ?? 0, true); 191 + o += 4; 188 192 view.setUint32(o, f.parent ?? 0, true); 189 193 o += 4; 190 194 view.setUint32( 191 195 o, 192 - (f.attachTo ?? 0) | ((f.attachPoints ?? 0) << 8) | 193 - ((f.zIndex ?? 0) << 16), 196 + encodeAttachTo(f.attachTo) | 197 + (encodeAttachPoint(f.attachPoints?.element) << 8) | 198 + (encodeAttachPoint(f.attachPoints?.parent) << 16) | 199 + (encodePointerCaptureMode(f.pointerCaptureMode) << 24), 200 + true, 201 + ); 202 + o += 4; 203 + view.setUint32( 204 + o, 205 + encodeClipTo(f.clipTo) | (((f.zIndex ?? 0) & 0xffff) << 8), 194 206 true, 195 207 ); 196 208 o += 4; ··· 307 319 floating?: { 308 320 x?: number; 309 321 y?: number; 322 + expand?: { width?: number; height?: number }; 310 323 parent?: number; 311 - attachTo?: number; 312 - attachPoints?: number; 324 + attachTo?: AttachTo; 325 + attachPoints?: { element?: AttachPoint; parent?: AttachPoint }; 326 + pointerCaptureMode?: PointerCaptureMode; 327 + clipTo?: ClipTo; 313 328 zIndex?: number; 314 329 }; 315 330 transition?: Transition; 331 + } 332 + 333 + export type AttachPoint = 334 + | "left-top" 335 + | "left-center" 336 + | "left-bottom" 337 + | "center-top" 338 + | "center-center" 339 + | "center-bottom" 340 + | "right-top" 341 + | "right-center" 342 + | "right-bottom"; 343 + 344 + export type AttachTo = "none" | "parent" | "element" | "root"; 345 + 346 + export type PointerCaptureMode = "capture" | "passthrough"; 347 + 348 + export type ClipTo = "none" | "attached-parent"; 349 + 350 + const ATTACH_POINT: Record<AttachPoint, number> = { 351 + "left-top": 0, 352 + "left-center": 1, 353 + "left-bottom": 2, 354 + "center-top": 3, 355 + "center-center": 4, 356 + "center-bottom": 5, 357 + "right-top": 6, 358 + "right-center": 7, 359 + "right-bottom": 8, 360 + }; 361 + 362 + const ATTACH_TO: Record<AttachTo, number> = { 363 + none: 0, 364 + parent: 1, 365 + element: 2, 366 + root: 3, 367 + }; 368 + 369 + const POINTER_CAPTURE_MODE: Record<PointerCaptureMode, number> = { 370 + capture: 0, 371 + passthrough: 1, 372 + }; 373 + 374 + const CLIP_TO: Record<ClipTo, number> = { 375 + none: 0, 376 + "attached-parent": 1, 377 + }; 378 + 379 + function encodeAttachPoint(value: AttachPoint | undefined): number { 380 + return value === undefined ? 0 : ATTACH_POINT[value]; 381 + } 382 + 383 + function encodeAttachTo(value: AttachTo | undefined): number { 384 + return value === undefined ? 0 : ATTACH_TO[value]; 385 + } 386 + 387 + function encodePointerCaptureMode( 388 + value: PointerCaptureMode | undefined, 389 + ): number { 390 + return value === undefined ? 0 : POINTER_CAPTURE_MODE[value]; 391 + } 392 + 393 + function encodeClipTo(value: ClipTo | undefined): number { 394 + return value === undefined ? 0 : CLIP_TO[value]; 316 395 } 317 396 318 397 export interface Text {
+51 -2
specs/renderer-spec.md
··· 643 643 - **`cornerRadius`** — per-corner radius values, producing rounded box-drawing 644 644 characters 645 645 - **`clip`** — clip region configuration for scroll containers 646 - - **`floating`** — floating-element configuration (offset, parent reference, 647 - attach points, z-index) 646 + - **`floating`** — floating-element configuration (offset, expansion, parent 647 + reference, attach target, structured attach points, pointer capture mode, clip 648 + target, z-index) 648 649 - **`scroll`** — scroll container configuration 650 + 651 + The `floating` object shape is: 652 + 653 + ```ts 654 + floating?: { 655 + x?: number; 656 + y?: number; 657 + expand?: { width?: number; height?: number }; 658 + parent?: number; 659 + attachTo?: "none" | "parent" | "element" | "root"; 660 + attachPoints?: { 661 + element?: 662 + | "left-top" 663 + | "left-center" 664 + | "left-bottom" 665 + | "center-top" 666 + | "center-center" 667 + | "center-bottom" 668 + | "right-top" 669 + | "right-center" 670 + | "right-bottom"; 671 + parent?: 672 + | "left-top" 673 + | "left-center" 674 + | "left-bottom" 675 + | "center-top" 676 + | "center-center" 677 + | "center-bottom" 678 + | "right-top" 679 + | "right-center" 680 + | "right-bottom"; 681 + }; 682 + pointerCaptureMode?: "capture" | "passthrough"; 683 + clipTo?: "none" | "attached-parent"; 684 + /** signed 16-bit integer */ 685 + zIndex?: number; 686 + } 687 + ``` 688 + 689 + The `floating` object configures Clay floating layout behavior. `x` and `y` 690 + provide the floating offset. `expand` expands the floating bounds. `parent` 691 + identifies the target element when `attachTo` is `"element"`. `attachTo` selects 692 + whether the element is attached to no target, its parent, an element, or the 693 + layout root. `attachPoints.element` describes the anchor on the floating 694 + element, and `attachPoints.parent` describes the anchor on the attached target. 695 + `pointerCaptureMode` controls whether the floating element captures pointer 696 + input or lets it pass through, `clipTo` controls inherited clipping, and 697 + `zIndex` controls floating order and is transferred as a signed 16-bit integer. 649 698 650 699 The `text()` constructor currently accepts: `color`, `fontSize`, 651 700 `letterSpacing`, `lineHeight`, and attribute flags (`bold`, `italic`,
+7 -1
src/clayterm.c
··· 560 560 if (mask & PROP_FLOATING) { 561 561 decl.floating.offset.x = rdf(buf, len, &i); 562 562 decl.floating.offset.y = rdf(buf, len, &i); 563 + decl.floating.expand.width = rdf(buf, len, &i); 564 + decl.floating.expand.height = rdf(buf, len, &i); 563 565 decl.floating.parentId = rd(buf, len, &i); 564 566 565 567 uint32_t fc = rd(buf, len, &i); 566 568 decl.floating.attachTo = fc & 0xff; 567 569 decl.floating.attachPoints.element = (fc >> 8) & 0xff; 568 570 decl.floating.attachPoints.parent = (fc >> 16) & 0xff; 569 - decl.floating.zIndex = (int16_t)((fc >> 24) & 0xff); 571 + decl.floating.pointerCaptureMode = (fc >> 24) & 0xff; 572 + 573 + uint32_t fd = rd(buf, len, &i); 574 + decl.floating.clipTo = fd & 0xff; 575 + decl.floating.zIndex = (int16_t)(fd >> 8); 570 576 } 571 577 572 578 if (mask & PROP_TRANSITION) {
+44
test/term.test.ts
··· 200 200 }); 201 201 }); 202 202 203 + it("renders a floating frame with structured attach points", () => { 204 + let out = print( 205 + decode( 206 + term.render([ 207 + open("root", { 208 + layout: { width: fixed(40), height: fixed(10), direction: "ttb" }, 209 + }), 210 + open("frame", { 211 + layout: { 212 + width: fixed(12), 213 + height: fixed(5), 214 + direction: "ttb", 215 + padding: { left: 1, top: 1 }, 216 + }, 217 + border: { 218 + color: rgba(255, 255, 255), 219 + left: 1, 220 + right: 1, 221 + top: 1, 222 + bottom: 1, 223 + }, 224 + floating: { 225 + x: 3, 226 + y: 1, 227 + attachTo: "root", 228 + attachPoints: { 229 + element: "center-center", 230 + parent: "center-center", 231 + }, 232 + }, 233 + }), 234 + text("box"), 235 + close(), 236 + close(), 237 + ]).output, 238 + ), 239 + 40, 240 + 10, 241 + ); 242 + 243 + expect(out).toContain("│box │"); 244 + expect(out).toContain("┌──────────┐"); 245 + }); 246 + 203 247 describe("snapshot", () => { 204 248 it("produces identical output to direct ops", async () => { 205 249 let ops = [
+60
test/validate.test.ts
··· 109 109 it("rejects fractional color", () => { 110 110 expect(validate([text("hi", { color: 1.5 })])).toBe(false); 111 111 }); 112 + 113 + it("accepts structured floating attach points", () => { 114 + expect(validate([ 115 + open("x", { 116 + floating: { 117 + attachPoints: { element: "center-center", parent: "center-center" }, 118 + }, 119 + }), 120 + close(), 121 + ])).toBe(true); 122 + }); 123 + 124 + it("accepts floating expand and clipping fields", () => { 125 + expect(validate([ 126 + open("x", { 127 + floating: { 128 + expand: { width: 2, height: 3 }, 129 + pointerCaptureMode: "passthrough", 130 + clipTo: "attached-parent", 131 + zIndex: 1024, 132 + }, 133 + }), 134 + close(), 135 + ])).toBe(true); 136 + }); 137 + 138 + it("accepts signed floating z-index values", () => { 139 + expect(validate([ 140 + open("x", { floating: { zIndex: -1 } }), 141 + close(), 142 + ])).toBe(true); 143 + expect(validate([ 144 + open("x", { floating: { zIndex: 32767 } }), 145 + close(), 146 + ])).toBe(true); 147 + }); 148 + 149 + it("rejects floating z-index values outside signed 16-bit range", () => { 150 + expect(validate([ 151 + open("x", { floating: { zIndex: -32769 } }), 152 + close(), 153 + ])).toBe(false); 154 + expect(validate([ 155 + open("x", { floating: { zIndex: 32768 } }), 156 + close(), 157 + ])).toBe(false); 158 + }); 159 + 160 + it("rejects numeric floating enum values", () => { 161 + expect(validate([ 162 + // deno-lint-ignore no-explicit-any 163 + open("x", { floating: { attachTo: 3 as any } }), 164 + close(), 165 + ])).toBe(false); 166 + expect(validate([ 167 + // deno-lint-ignore no-explicit-any 168 + open("x", { floating: { attachPoints: 4 as any } }), 169 + close(), 170 + ])).toBe(false); 171 + }); 112 172 }); 113 173 114 174 describe("validated", () => {
+42 -3
validate.ts
··· 7 7 8 8 const u8 = Type.Integer({ minimum: 0, maximum: 255 }); 9 9 const u16 = Type.Integer({ minimum: 0, maximum: 65535 }); 10 + const i16 = Type.Integer({ minimum: -32768, maximum: 32767 }); 10 11 11 12 /* RGBA color packed as (a << 24 | r << 16 | g << 8 | b). When alpha >= 128, 12 13 * bit 31 is set and JavaScript interprets the value as a negative int32. ··· 80 81 vertical: Type.Optional(Type.Boolean()), 81 82 }); 82 83 84 + const AttachPoint = Type.Union([ 85 + Type.Literal("left-top"), 86 + Type.Literal("left-center"), 87 + Type.Literal("left-bottom"), 88 + Type.Literal("center-top"), 89 + Type.Literal("center-center"), 90 + Type.Literal("center-bottom"), 91 + Type.Literal("right-top"), 92 + Type.Literal("right-center"), 93 + Type.Literal("right-bottom"), 94 + ]); 95 + 96 + const AttachTo = Type.Union([ 97 + Type.Literal("none"), 98 + Type.Literal("parent"), 99 + Type.Literal("element"), 100 + Type.Literal("root"), 101 + ]); 102 + 103 + const PointerCaptureMode = Type.Union([ 104 + Type.Literal("capture"), 105 + Type.Literal("passthrough"), 106 + ]); 107 + 108 + const ClipTo = Type.Union([ 109 + Type.Literal("none"), 110 + Type.Literal("attached-parent"), 111 + ]); 112 + 83 113 const Floating = Type.Object({ 84 114 x: Type.Optional(Type.Number()), 85 115 y: Type.Optional(Type.Number()), 116 + expand: Type.Optional(Type.Object({ 117 + width: Type.Optional(Type.Number()), 118 + height: Type.Optional(Type.Number()), 119 + })), 86 120 parent: Type.Optional(Type.Integer({ minimum: 0 })), 87 - attachTo: Type.Optional(u8), 88 - attachPoints: Type.Optional(u8), 89 - zIndex: Type.Optional(u16), 121 + attachTo: Type.Optional(AttachTo), 122 + attachPoints: Type.Optional(Type.Object({ 123 + element: Type.Optional(AttachPoint), 124 + parent: Type.Optional(AttachPoint), 125 + })), 126 + pointerCaptureMode: Type.Optional(PointerCaptureMode), 127 + clipTo: Type.Optional(ClipTo), 128 + zIndex: Type.Optional(i16), 90 129 }); 91 130 92 131 const TransitionProperty = Type.Union([