[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
9.1 kB
369 lines
1/* Command buffer opcodes — mirrors ops.h */
2const OP_OPEN_ELEMENT = 0x02;
3const OP_TEXT = 0x03;
4const OP_CLOSE_ELEMENT = 0x04;
5const OP_SNAPSHOT = 0x05;
6
7/* Property group masks for OPEN_ELEMENT */
8const PROP_LAYOUT = 0x01;
9const PROP_BG_COLOR = 0x02;
10const PROP_CORNER_RADIUS = 0x04;
11const PROP_BORDER = 0x08;
12const PROP_CLIP = 0x10;
13const PROP_FLOATING = 0x20;
14
15const encoder = new TextEncoder();
16
17function packAxis(view: DataView, offset: number, axis: SizingAxis): number {
18 let o = offset;
19 switch (axis.type) {
20 case "fit":
21 view.setUint32(o, 0, true);
22 o += 4;
23 view.setFloat32(o, axis.min ?? 0, true);
24 o += 4;
25 view.setFloat32(o, axis.max ?? 0, true);
26 o += 4;
27 break;
28 case "grow":
29 view.setUint32(o, 1, true);
30 o += 4;
31 view.setFloat32(o, axis.min ?? 0, true);
32 o += 4;
33 view.setFloat32(o, axis.max ?? 0, true);
34 o += 4;
35 break;
36 case "percent":
37 view.setUint32(o, 2, true);
38 o += 4;
39 view.setFloat32(o, axis.value, true);
40 o += 4;
41 view.setFloat32(o, 0, true);
42 o += 4;
43 break;
44 case "fixed":
45 view.setUint32(o, 3, true);
46 o += 4;
47 view.setFloat32(o, axis.value, true);
48 o += 4;
49 view.setFloat32(o, 0, true);
50 o += 4;
51 break;
52 }
53 return o;
54}
55
56function packString(
57 view: DataView,
58 bytes: Uint8Array,
59 o: number,
60 end: number,
61 context: string,
62): number {
63 let paddedLength = Math.ceil(bytes.length / 4) * 4;
64 let next = o + 4 + paddedLength;
65 if (next > end) {
66 throw new RangeError(
67 `clayterm transfer buffer capacity exceeded while packing ${context} ` +
68 `(${next} byte offset, ${end} byte limit). ` +
69 `Render a smaller visible slice or reduce frame content.`,
70 );
71 }
72
73 view.setUint32(o, bytes.length, true);
74 o += 4;
75 new Uint8Array(view.buffer).set(bytes, o);
76 o += paddedLength;
77 return o;
78}
79
80export function pack(
81 ops: Op[],
82 mem: ArrayBufferLike,
83 offset: number,
84 limit?: number,
85): number {
86 let view = new DataView(mem);
87 let end = limit ?? mem.byteLength;
88 let o = offset;
89
90 for (let op of ops) {
91 switch (op.directive) {
92 case OP_CLOSE_ELEMENT:
93 view.setUint32(o, op.directive, true);
94 o += 4;
95 break;
96
97 case OP_OPEN_ELEMENT: {
98 view.setUint32(o, OP_OPEN_ELEMENT, true);
99 o += 4;
100
101 let bytes = encoder.encode(op.id);
102 o = packString(view, bytes, o, end, "element id");
103
104 let mask = 0;
105 if (op.layout) mask |= PROP_LAYOUT;
106 if (op.bg !== undefined) mask |= PROP_BG_COLOR;
107 if (op.cornerRadius) mask |= PROP_CORNER_RADIUS;
108 if (op.border) mask |= PROP_BORDER;
109 if (op.clip) mask |= PROP_CLIP;
110 if (op.floating) mask |= PROP_FLOATING;
111 view.setUint32(o, mask, true);
112 o += 4;
113
114 if (op.layout) {
115 let l = op.layout;
116 o = packAxis(view, o, l.width ?? { type: "fit" });
117 o = packAxis(view, o, l.height ?? { type: "fit" });
118
119 let p = l.padding ?? {};
120 view.setUint32(
121 o,
122 (p.left ?? 0) | ((p.right ?? 0) << 8) | ((p.top ?? 0) << 16) |
123 ((p.bottom ?? 0) << 24),
124 true,
125 );
126 o += 4;
127
128 view.setUint32(
129 o,
130 (l.gap ?? 0) | ((l.direction === "ttb" ? 1 : 0) << 16),
131 true,
132 );
133 o += 4;
134
135 view.setUint32(o, (l.alignX ?? 0) | ((l.alignY ?? 0) << 8), true);
136 o += 4;
137 }
138
139 if (op.bg !== undefined) {
140 view.setUint32(o, op.bg, true);
141 o += 4;
142 }
143
144 if (op.cornerRadius) {
145 let cr = op.cornerRadius;
146 view.setUint32(
147 o,
148 (cr.tl ?? 0) | ((cr.tr ?? 0) << 8) | ((cr.bl ?? 0) << 16) |
149 ((cr.br ?? 0) << 24),
150 true,
151 );
152 o += 4;
153 }
154
155 if (op.border) {
156 let b = op.border;
157 view.setUint32(o, b.color, true);
158 o += 4;
159 view.setUint32(
160 o,
161 (b.left ?? 0) | ((b.right ?? 0) << 8) | ((b.top ?? 0) << 16) |
162 ((b.bottom ?? 0) << 24),
163 true,
164 );
165 o += 4;
166 }
167
168 if (op.clip) {
169 view.setUint32(
170 o,
171 (op.clip.horizontal ? 1 : 0) | ((op.clip.vertical ? 1 : 0) << 8),
172 true,
173 );
174 o += 4;
175 }
176
177 if (op.floating) {
178 let f = op.floating;
179 view.setFloat32(o, f.x ?? 0, true);
180 o += 4;
181 view.setFloat32(o, f.y ?? 0, true);
182 o += 4;
183 view.setUint32(o, f.parent ?? 0, true);
184 o += 4;
185 view.setUint32(
186 o,
187 (f.attachTo ?? 0) | ((f.attachPoints ?? 0) << 8) |
188 ((f.zIndex ?? 0) << 16),
189 true,
190 );
191 o += 4;
192 }
193 break;
194 }
195
196 case OP_SNAPSHOT: {
197 new Uint8Array(mem).set(op.data, o);
198 o += op.data.length;
199 break;
200 }
201
202 case OP_TEXT: {
203 view.setUint32(o, OP_TEXT, true);
204 o += 4;
205 view.setUint32(o, op.color ?? 0xFFFFFFFF, true);
206 o += 4;
207 view.setUint32(
208 o,
209 (op.fontSize ?? 1) |
210 ((op.fontId ?? 0) << 8) |
211 ((op.wrap ?? 0) << 16) |
212 ((op.attrs ?? 0) << 24),
213 true,
214 );
215 o += 4;
216
217 let str = encoder.encode(op.content);
218 o = packString(view, str, o, end, "text content");
219 break;
220 }
221 }
222 if (o > end) {
223 throw new RangeError(
224 `ops exceed buffer capacity (${o - offset} bytes packed, ${
225 end - offset
226 } available)`,
227 );
228 }
229 }
230
231 return (o - offset) / 4;
232}
233
234export function rgba(r: number, g: number, b: number, a = 255): number {
235 return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) |
236 (b & 0xFF);
237}
238
239export type SizingAxis =
240 | { type: "fit"; min?: number; max?: number }
241 | { type: "grow"; min?: number; max?: number }
242 | { type: "percent"; value: number }
243 | { type: "fixed"; value: number };
244
245export const fit = (min = 0, max = 0): SizingAxis => ({
246 type: "fit",
247 min,
248 max,
249});
250export const grow = (min = 0, max = 0): SizingAxis => ({
251 type: "grow",
252 min,
253 max,
254});
255export const percent = (value: number): SizingAxis => ({
256 type: "percent",
257 value,
258});
259export const fixed = (value: number): SizingAxis => ({ type: "fixed", value });
260
261export interface CloseElement {
262 directive: typeof OP_CLOSE_ELEMENT;
263}
264
265export interface OpenElement {
266 directive: typeof OP_OPEN_ELEMENT;
267 id: string;
268 layout?: {
269 width?: SizingAxis;
270 height?: SizingAxis;
271 padding?: { left?: number; right?: number; top?: number; bottom?: number };
272 gap?: number;
273 direction?: "ltr" | "ttb";
274 alignX?: number;
275 alignY?: number;
276 };
277 bg?: number;
278 cornerRadius?: { tl?: number; tr?: number; bl?: number; br?: number };
279 border?: {
280 color: number;
281 left?: number;
282 right?: number;
283 top?: number;
284 bottom?: number;
285 };
286 clip?: { horizontal?: boolean; vertical?: boolean };
287 floating?: {
288 x?: number;
289 y?: number;
290 parent?: number;
291 attachTo?: number;
292 attachPoints?: number;
293 zIndex?: number;
294 };
295}
296
297export interface Text {
298 directive: typeof OP_TEXT;
299 content: string;
300 color?: number;
301 fontSize?: number;
302 fontId?: number;
303 wrap?: number;
304 attrs?: number;
305}
306
307interface Snapshot {
308 directive: typeof OP_SNAPSHOT;
309 data: Uint8Array;
310}
311
312export type Op = OpenElement | Text | CloseElement | Snapshot;
313
314export function open(
315 id: string,
316 props: Omit<OpenElement, "directive" | "id"> = {},
317): OpenElement {
318 return { directive: OP_OPEN_ELEMENT, id, ...props };
319}
320
321export function text(
322 content: string,
323 props: Omit<Text, "directive" | "content"> = {},
324): Text {
325 return { directive: OP_TEXT, content, ...props };
326}
327
328export function close(): CloseElement {
329 return { directive: OP_CLOSE_ELEMENT };
330}
331
332function packSize(ops: Op[]): number {
333 let n = 0;
334 for (let op of ops) {
335 switch (op.directive) {
336 case OP_CLOSE_ELEMENT:
337 n += 4;
338 break;
339 case OP_SNAPSHOT:
340 n += op.data.length;
341 break;
342 case OP_OPEN_ELEMENT: {
343 n += 4; // opcode
344 n += 4 + Math.ceil(encoder.encode(op.id).length / 4) * 4; // id string
345 n += 4; // mask
346 if (op.layout) n += 6 * 4 + 4 + 4 + 4; // 2 axes (3 words each) + pad + gap + align
347 if (op.bg !== undefined) n += 4;
348 if (op.cornerRadius) n += 4;
349 if (op.border) n += 8;
350 if (op.clip) n += 4;
351 if (op.floating) n += 16;
352 break;
353 }
354 case OP_TEXT: {
355 n += 4 + 4 + 4; // opcode + color + cfg
356 n += 4 + Math.ceil(encoder.encode(op.content).length / 4) * 4; // string
357 break;
358 }
359 }
360 }
361 return n;
362}
363
364export function snapshot(ops: Op[]): Op {
365 let size = packSize(ops);
366 let buf = new ArrayBuffer(size);
367 let words = pack(ops, buf, 0, size);
368 return { directive: OP_SNAPSHOT, data: new Uint8Array(buf, 0, words * 4) };
369}