[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
1import { f32, offsets, struct } from "./typedef.ts";
2
3export interface BoundingBox {
4 x: number;
5 y: number;
6 width: number;
7 height: number;
8}
9
10const BoundingBoxStruct = struct<BoundingBox>({
11 x: f32(),
12 y: f32(),
13 width: f32(),
14 height: f32(),
15});
16
17const BOUNDING_BOX = offsets(BoundingBoxStruct);
18
19const WASM_PAGE_BYTES = 65536;
20const TEXT_TRANSFER_BUFFER_BYTES = 1024 * 1024;
21const CLAY_DEFAULT_MAX_ELEMENT_COUNT = 8192;
22
23// Conservative fixed wire-format budget per element. This covers the largest
24// non-text open/close element encoding we currently support; text, element id,
25// and snapshot payload bytes live in TEXT_TRANSFER_BUFFER_BYTES.
26const MAX_FIXED_ELEMENT_WIRE_BYTES = 116;
27
28export interface Native {
29 memory: WebAssembly.Memory;
30 statePtr: number;
31 opsBuf: number;
32 reduce(
33 ct: number,
34 buf: number,
35 len: number,
36 mode: number,
37 row: number,
38 deltaTime: number,
39 ): void;
40 output(ct: number): number;
41 length(ct: number): number;
42 setPointer(x: number, y: number, down: boolean): void;
43 getPointerOverIds(): string[];
44 getElementBounds(id: string): BoundingBox | undefined;
45 animating(ct: number): number;
46 errorCount(ct: number): number;
47 errorType(ct: number, index: number): number;
48 errorMessage(ct: number, index: number): string;
49}
50
51import { compiled } from "./wasm.ts";
52
53export async function createTermNative(
54 w: number,
55 h: number,
56): Promise<Native> {
57 let memory = new WebAssembly.Memory({ initial: 2 });
58 let exports: Record<string, CallableFunction> = {};
59
60 let instance = await WebAssembly.instantiate(compiled, {
61 env: { memory },
62 clay: {
63 measureTextFunction(
64 ret: number,
65 text: number,
66 _config: number,
67 _userData: number,
68 ) {
69 exports.measure(ret, text);
70 },
71 queryScrollOffsetFunction(
72 ret: number,
73 _elementId: number,
74 _userData: number,
75 ) {
76 let view = new DataView(memory.buffer);
77 view.setFloat32(ret, 0, true);
78 view.setFloat32(ret + 4, 0, true);
79 },
80 },
81 });
82
83 Object.assign(exports, instance.exports);
84
85 let ct = exports as unknown as {
86 __heap_base: WebAssembly.Global;
87 clayterm_size(w: number, h: number): number;
88 init(mem: number, w: number, h: number): number;
89 reduce(
90 ct: number,
91 buf: number,
92 len: number,
93 mode: number,
94 row: number,
95 deltaTime: number,
96 ): void;
97 output(ct: number): number;
98 length(ct: number): number;
99 Clay_SetPointerState(vec: number, down: number): void;
100 pointer_over_count(): number;
101 pointer_over_id_string_length(index: number): number;
102 pointer_over_id_string_ptr(index: number): number;
103 get_element_bounds(name: number, len: number, out: number): number;
104 animating(ct: number): number;
105 error_count(ct: number): number;
106 error_type(ct: number, index: number): number;
107 error_message_length(ct: number, index: number): number;
108 error_message_ptr(ct: number, index: number): number;
109 };
110
111 let heap = ct.__heap_base.value as number;
112 let size = ct.clayterm_size(w, h);
113
114 // Grow memory once to fit heap + renderer state + fixed transfer buffer.
115 // The transfer budget is intentionally fixed: text/id/snapshot payload bytes
116 // get 1MB, and fixed op overhead gets one max-sized element per Clay element.
117 // Do not grow this dynamically per render; improve the wire format instead.
118 let transferBytes = TEXT_TRANSFER_BUFFER_BYTES +
119 CLAY_DEFAULT_MAX_ELEMENT_COUNT * MAX_FIXED_ELEMENT_WIRE_BYTES;
120 let needed = heap + size + transferBytes;
121 let pages = Math.ceil(needed / WASM_PAGE_BYTES);
122 let current = memory.buffer.byteLength / WASM_PAGE_BYTES;
123 if (pages > current) {
124 memory.grow(pages - current);
125 }
126
127 let statePtr = ct.init(heap, w, h);
128 let opsBuf = (heap + size + 3) & ~3;
129
130 return {
131 memory,
132 statePtr,
133 opsBuf,
134 reduce: ct.reduce,
135 output: ct.output,
136 length: ct.length,
137 animating: ct.animating,
138 setPointer(x: number, y: number, down: boolean) {
139 let view = new DataView(memory.buffer);
140 view.setFloat32(opsBuf, x, true);
141 view.setFloat32(opsBuf + 4, y, true);
142 ct.Clay_SetPointerState(opsBuf, down ? 1 : 0);
143 },
144 getPointerOverIds(): string[] {
145 let decoder = new TextDecoder();
146 let count = ct.pointer_over_count();
147 let ids: string[] = [];
148 for (let i = 0; i < count; i++) {
149 let len = ct.pointer_over_id_string_length(i);
150 if (len === 0) continue;
151 let ptr = ct.pointer_over_id_string_ptr(i);
152 ids.push(decoder.decode(new Uint8Array(memory.buffer, ptr, len)));
153 }
154 return ids;
155 },
156 getElementBounds(id: string): BoundingBox | undefined {
157 let enc = new TextEncoder();
158 let bytes = enc.encode(id);
159 new Uint8Array(memory.buffer).set(bytes, opsBuf);
160 let out = opsBuf + 256;
161 let found = ct.get_element_bounds(opsBuf, bytes.length, out);
162 if (!found) {
163 return undefined;
164 }
165 let view = new DataView(memory.buffer);
166 return {
167 x: view.getFloat32(out + BOUNDING_BOX.x, true),
168 y: view.getFloat32(out + BOUNDING_BOX.y, true),
169 width: view.getFloat32(out + BOUNDING_BOX.width, true),
170 height: view.getFloat32(out + BOUNDING_BOX.height, true),
171 };
172 },
173 errorCount(ptr: number): number {
174 return ct.error_count(ptr);
175 },
176 errorType(ptr: number, index: number): number {
177 return ct.error_type(ptr, index);
178 },
179 errorMessage(ptr: number, index: number): string {
180 let len = ct.error_message_length(ptr, index);
181 if (len === 0) return "";
182 let p = ct.error_message_ptr(ptr, index);
183 let decoder = new TextDecoder();
184 return decoder.decode(new Uint8Array(memory.buffer, p, len));
185 },
186 };
187}