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