@svelterm/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/src/components/spinner.d.ts +11 -0
- package/dist/src/components/spinner.js +19 -0
- package/dist/src/components/text-buffer.d.ts +21 -0
- package/dist/src/components/text-buffer.js +87 -0
- package/dist/src/css/animation-runner.d.ts +17 -0
- package/dist/src/css/animation-runner.js +72 -0
- package/dist/src/css/animation.d.ts +5 -0
- package/dist/src/css/animation.js +6 -0
- package/dist/src/css/calc.d.ts +5 -0
- package/dist/src/css/calc.js +130 -0
- package/dist/src/css/color.d.ts +1 -0
- package/dist/src/css/color.js +157 -0
- package/dist/src/css/compute.d.ts +63 -0
- package/dist/src/css/compute.js +606 -0
- package/dist/src/css/defaults.d.ts +8 -0
- package/dist/src/css/defaults.js +44 -0
- package/dist/src/css/incremental.d.ts +9 -0
- package/dist/src/css/incremental.js +46 -0
- package/dist/src/css/index.d.ts +5 -0
- package/dist/src/css/index.js +3 -0
- package/dist/src/css/media.d.ts +11 -0
- package/dist/src/css/media.js +59 -0
- package/dist/src/css/parser.d.ts +20 -0
- package/dist/src/css/parser.js +241 -0
- package/dist/src/css/selector.d.ts +17 -0
- package/dist/src/css/selector.js +272 -0
- package/dist/src/css/specificity.d.ts +7 -0
- package/dist/src/css/specificity.js +89 -0
- package/dist/src/css/values.d.ts +17 -0
- package/dist/src/css/values.js +58 -0
- package/dist/src/css/variables.d.ts +6 -0
- package/dist/src/css/variables.js +42 -0
- package/dist/src/debug/console.d.ts +16 -0
- package/dist/src/debug/console.js +65 -0
- package/dist/src/debug/server.d.ts +22 -0
- package/dist/src/debug/server.js +90 -0
- package/dist/src/headless.d.ts +21 -0
- package/dist/src/headless.js +26 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +485 -0
- package/dist/src/input/dispatch.d.ts +18 -0
- package/dist/src/input/dispatch.js +70 -0
- package/dist/src/input/focus.d.ts +18 -0
- package/dist/src/input/focus.js +81 -0
- package/dist/src/input/hit.d.ts +3 -0
- package/dist/src/input/hit.js +29 -0
- package/dist/src/input/keyboard.d.ts +9 -0
- package/dist/src/input/keyboard.js +100 -0
- package/dist/src/input/mouse.d.ts +7 -0
- package/dist/src/input/mouse.js +35 -0
- package/dist/src/input/scroll.d.ts +2 -0
- package/dist/src/input/scroll.js +24 -0
- package/dist/src/layout/cache.d.ts +4 -0
- package/dist/src/layout/cache.js +8 -0
- package/dist/src/layout/engine.d.ts +9 -0
- package/dist/src/layout/engine.js +455 -0
- package/dist/src/layout/flex.d.ts +4 -0
- package/dist/src/layout/flex.js +30 -0
- package/dist/src/layout/incremental.d.ts +8 -0
- package/dist/src/layout/incremental.js +58 -0
- package/dist/src/layout/size.d.ts +2 -0
- package/dist/src/layout/size.js +25 -0
- package/dist/src/layout/text.d.ts +7 -0
- package/dist/src/layout/text.js +52 -0
- package/dist/src/render/ansi.d.ts +23 -0
- package/dist/src/render/ansi.js +108 -0
- package/dist/src/render/border.d.ts +4 -0
- package/dist/src/render/border.js +60 -0
- package/dist/src/render/buffer.d.ts +23 -0
- package/dist/src/render/buffer.js +70 -0
- package/dist/src/render/context.d.ts +19 -0
- package/dist/src/render/context.js +98 -0
- package/dist/src/render/diff.d.ts +2 -0
- package/dist/src/render/diff.js +53 -0
- package/dist/src/render/incremental-paint.d.ts +10 -0
- package/dist/src/render/incremental-paint.js +94 -0
- package/dist/src/render/paint-text.d.ts +29 -0
- package/dist/src/render/paint-text.js +120 -0
- package/dist/src/render/paint.d.ts +5 -0
- package/dist/src/render/paint.js +220 -0
- package/dist/src/render/queue.d.ts +24 -0
- package/dist/src/render/queue.js +54 -0
- package/dist/src/render/scrollbar.d.ts +3 -0
- package/dist/src/render/scrollbar.js +19 -0
- package/dist/src/render/snapshot.d.ts +18 -0
- package/dist/src/render/snapshot.js +126 -0
- package/dist/src/renderer/default.d.ts +3 -0
- package/dist/src/renderer/default.js +3 -0
- package/dist/src/renderer/index.d.ts +11 -0
- package/dist/src/renderer/index.js +116 -0
- package/dist/src/renderer/node.d.ts +44 -0
- package/dist/src/renderer/node.js +153 -0
- package/dist/src/terminal/screen.d.ts +10 -0
- package/dist/src/terminal/screen.js +31 -0
- package/dist/src/terminal/stdin-router.d.ts +31 -0
- package/dist/src/terminal/stdin-router.js +133 -0
- package/package.json +64 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { computeMainStart, computeItemGap, computeCrossOffset } from './flex.js';
|
|
2
|
+
import { measureText } from './text.js';
|
|
3
|
+
import { resolveSize, constrain } from './size.js';
|
|
4
|
+
/** Flatten display:contents elements, promoting their children. */
|
|
5
|
+
function flattenContents(children, styles) {
|
|
6
|
+
const result = [];
|
|
7
|
+
for (const child of children) {
|
|
8
|
+
if (child.nodeType === 'element' && styles.get(child.id)?.display === 'contents') {
|
|
9
|
+
result.push(...flattenContents(child.children, styles));
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
result.push(child);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
export function computeLayout(root, styles, availWidth, availHeight) {
|
|
18
|
+
const boxes = new Map();
|
|
19
|
+
layoutNode(root, styles, boxes, 0, 0, availWidth, availHeight);
|
|
20
|
+
return boxes;
|
|
21
|
+
}
|
|
22
|
+
function layoutNode(node, styles, boxes, x, y, availWidth, availHeight) {
|
|
23
|
+
if (node.nodeType === 'text')
|
|
24
|
+
return layoutText(node, boxes, x, y, availWidth, styles);
|
|
25
|
+
if (node.nodeType === 'comment')
|
|
26
|
+
return { width: 0, height: 0 };
|
|
27
|
+
if (node.nodeType === 'fragment')
|
|
28
|
+
return layoutFragment(node, styles, boxes, x, y, availWidth, availHeight);
|
|
29
|
+
return layoutElement(node, styles, boxes, x, y, availWidth, availHeight);
|
|
30
|
+
}
|
|
31
|
+
function layoutText(node, boxes, x, y, availWidth = Infinity, styles) {
|
|
32
|
+
const text = node.text ?? '';
|
|
33
|
+
const parentStyle = node.parent ? styles?.get(node.parent.id) : undefined;
|
|
34
|
+
const preserveWhitespace = parentStyle?.whiteSpace === 'pre';
|
|
35
|
+
// Skip empty text and inter-element whitespace.
|
|
36
|
+
// Whitespace-only text between element siblings is formatting, not content.
|
|
37
|
+
// Preserve whitespace-only text that is the sole child (e.g. grid rows of spaces).
|
|
38
|
+
const isInterElementWhitespace = !preserveWhitespace
|
|
39
|
+
&& text.trim() === ''
|
|
40
|
+
&& node.parent?.children.some(c => c.nodeType === 'element');
|
|
41
|
+
if (text === '' || isInterElementWhitespace) {
|
|
42
|
+
boxes.set(node.id, { x, y, width: 0, height: 0 });
|
|
43
|
+
return { width: 0, height: 0 };
|
|
44
|
+
}
|
|
45
|
+
// Check parent's whiteSpace
|
|
46
|
+
const noWrap = parentStyle?.whiteSpace === 'nowrap';
|
|
47
|
+
const wrapWidth = noWrap ? Infinity : (availWidth > 0 ? availWidth : Infinity);
|
|
48
|
+
const measured = measureText(text, wrapWidth);
|
|
49
|
+
boxes.set(node.id, { x, y, width: measured.width, height: measured.height });
|
|
50
|
+
return measured;
|
|
51
|
+
}
|
|
52
|
+
function layoutFragment(node, styles, boxes, x, y, availWidth, availHeight) {
|
|
53
|
+
return layoutBlockFlow(node.children, styles, boxes, x, y, availWidth, availHeight);
|
|
54
|
+
}
|
|
55
|
+
function layoutElement(node, styles, boxes, x, y, availWidth, availHeight) {
|
|
56
|
+
const style = styles.get(node.id);
|
|
57
|
+
if (style?.display === 'none')
|
|
58
|
+
return { width: 0, height: 0 };
|
|
59
|
+
// display: contents — element is invisible to layout, children promoted
|
|
60
|
+
if (style?.display === 'contents') {
|
|
61
|
+
return layoutBlockFlow(node.children, styles, boxes, x, y, availWidth, availHeight);
|
|
62
|
+
}
|
|
63
|
+
// Absolute positioning: use top/left offsets relative to parent, don't consume space in flow
|
|
64
|
+
if (style?.position === 'absolute' || style?.position === 'fixed') {
|
|
65
|
+
const absX = x + (style.left ?? 0);
|
|
66
|
+
const absY = y + (style.top ?? 0);
|
|
67
|
+
return layoutAbsolute(node, styles, boxes, absX, absY, availWidth, availHeight, style);
|
|
68
|
+
}
|
|
69
|
+
let margin = {
|
|
70
|
+
top: resolvePadding(style?.marginTop, availWidth),
|
|
71
|
+
right: resolvePadding(style?.marginRight, availWidth),
|
|
72
|
+
bottom: resolvePadding(style?.marginBottom, availWidth),
|
|
73
|
+
left: resolvePadding(style?.marginLeft, availWidth),
|
|
74
|
+
};
|
|
75
|
+
const borderWidth = (style?.borderStyle && style.borderStyle !== 'none') ? 1 : 0;
|
|
76
|
+
const inset = {
|
|
77
|
+
top: resolvePadding(style?.paddingTop, availWidth) + borderWidth,
|
|
78
|
+
right: resolvePadding(style?.paddingRight, availWidth) + borderWidth,
|
|
79
|
+
bottom: resolvePadding(style?.paddingBottom, availWidth) + borderWidth,
|
|
80
|
+
left: resolvePadding(style?.paddingLeft, availWidth) + borderWidth,
|
|
81
|
+
};
|
|
82
|
+
// Resolve auto margins for centering
|
|
83
|
+
const nodeWidthForAutoMargin = resolveSize(style?.width, availWidth);
|
|
84
|
+
if (margin.left === -1 && margin.right === -1 && nodeWidthForAutoMargin !== null) {
|
|
85
|
+
const remaining = availWidth - nodeWidthForAutoMargin;
|
|
86
|
+
margin = { ...margin, left: Math.floor(remaining / 2), right: Math.ceil(remaining / 2) };
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (margin.left === -1)
|
|
90
|
+
margin = { ...margin, left: 0 };
|
|
91
|
+
if (margin.right === -1)
|
|
92
|
+
margin = { ...margin, right: 0 };
|
|
93
|
+
}
|
|
94
|
+
const boxX = x + margin.left;
|
|
95
|
+
const boxY = y + margin.top;
|
|
96
|
+
const explicitWidth = resolveSize(style?.width, availWidth - margin.left - margin.right);
|
|
97
|
+
const nodeWidth = explicitWidth !== null ? Math.min(explicitWidth, availWidth - margin.left - margin.right) : null;
|
|
98
|
+
const nodeHeight = resolveSize(style?.height, availHeight - margin.top - margin.bottom);
|
|
99
|
+
const innerW = (nodeWidth ?? (availWidth - margin.left - margin.right)) - inset.left - inset.right;
|
|
100
|
+
const innerH = (nodeHeight ?? (availHeight - margin.top - margin.bottom)) - inset.top - inset.bottom;
|
|
101
|
+
const display = style?.display ?? 'block';
|
|
102
|
+
let content;
|
|
103
|
+
if (display === 'flex') {
|
|
104
|
+
content = positionChildren(node.children, styles, boxes, boxX + inset.left, boxY + inset.top, innerW, innerH, style?.flexDirection ?? 'column', style?.gap ?? 0, style?.justifyContent ?? 'start', style?.alignItems ?? 'start', style?.flexWrap ?? 'nowrap');
|
|
105
|
+
}
|
|
106
|
+
else if (display === 'table') {
|
|
107
|
+
content = layoutTable(node, styles, boxes, boxX + inset.left, boxY + inset.top, innerW, innerH);
|
|
108
|
+
}
|
|
109
|
+
else if (display === 'grid' && style) {
|
|
110
|
+
content = layoutGrid(node, styles, boxes, boxX + inset.left, boxY + inset.top, innerW, innerH, style);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// block or inline — use block flow (inline children flow horizontally within)
|
|
114
|
+
content = layoutBlockFlow(node.children, styles, boxes, boxX + inset.left, boxY + inset.top, innerW, innerH);
|
|
115
|
+
}
|
|
116
|
+
// Block elements fill parent width; inline/inline-block shrink-wrap to content.
|
|
117
|
+
// Flex/grid children are sized by the flex/grid algorithm, so they shrink-wrap.
|
|
118
|
+
const parentDisplay = node.parent ? styles.get(node.parent.id)?.display : undefined;
|
|
119
|
+
const isFlexOrGridChild = parentDisplay === 'flex' || parentDisplay === 'grid';
|
|
120
|
+
const isBlock = (display === 'block' || display === 'flex' || display === 'grid' || display === 'table')
|
|
121
|
+
&& !isFlexOrGridChild;
|
|
122
|
+
const autoWidth = isBlock
|
|
123
|
+
? (availWidth - margin.left - margin.right)
|
|
124
|
+
: content.width + inset.left + inset.right;
|
|
125
|
+
// Input/textarea have intrinsic minimum height of 1 row for the value text
|
|
126
|
+
const intrinsicHeight = (node.tag === 'input' || node.tag === 'textarea')
|
|
127
|
+
? Math.max(content.height, 1)
|
|
128
|
+
: content.height;
|
|
129
|
+
const autoHeight = intrinsicHeight + inset.top + inset.bottom;
|
|
130
|
+
const finalWidth = constrain(nodeWidth ?? autoWidth, style?.minWidth, style?.maxWidth);
|
|
131
|
+
const finalHeight = constrain(nodeHeight ?? autoHeight, style?.minHeight, style?.maxHeight);
|
|
132
|
+
boxes.set(node.id, { x: boxX, y: boxY, width: finalWidth, height: finalHeight });
|
|
133
|
+
// Return outer size including margin
|
|
134
|
+
return { width: finalWidth + margin.left + margin.right, height: finalHeight + margin.top + margin.bottom };
|
|
135
|
+
}
|
|
136
|
+
function layoutAbsolute(node, styles, boxes, x, y, availWidth, availHeight, style) {
|
|
137
|
+
const borderWidth = (style.borderStyle && style.borderStyle !== 'none') ? 1 : 0;
|
|
138
|
+
const inset = {
|
|
139
|
+
top: resolvePadding(style.paddingTop, availWidth) + borderWidth,
|
|
140
|
+
right: resolvePadding(style.paddingRight, availWidth) + borderWidth,
|
|
141
|
+
bottom: resolvePadding(style.paddingBottom, availWidth) + borderWidth,
|
|
142
|
+
left: resolvePadding(style.paddingLeft, availWidth) + borderWidth,
|
|
143
|
+
};
|
|
144
|
+
const nodeWidth = resolveSize(style.width, availWidth);
|
|
145
|
+
const nodeHeight = resolveSize(style.height, availHeight);
|
|
146
|
+
const innerW = (nodeWidth ?? availWidth) - inset.left - inset.right;
|
|
147
|
+
const innerH = (nodeHeight ?? availHeight) - inset.top - inset.bottom;
|
|
148
|
+
const content = positionChildren(node.children, styles, boxes, x + inset.left, y + inset.top, innerW, innerH, style.flexDirection ?? 'column', style.gap ?? 0, style.justifyContent ?? 'start', style.alignItems ?? 'start');
|
|
149
|
+
const finalWidth = constrain(nodeWidth ?? (content.width + inset.left + inset.right), style.minWidth, style.maxWidth);
|
|
150
|
+
const finalHeight = constrain(nodeHeight ?? (content.height + inset.top + inset.bottom), style.minHeight, style.maxHeight);
|
|
151
|
+
boxes.set(node.id, { x, y, width: finalWidth, height: finalHeight });
|
|
152
|
+
// Return zero size — absolute elements don't consume space in flow
|
|
153
|
+
return { width: 0, height: 0 };
|
|
154
|
+
}
|
|
155
|
+
/** Resolve a padding/margin value that may be a number or a % string */
|
|
156
|
+
function resolvePadding(value, availWidth) {
|
|
157
|
+
if (value === undefined)
|
|
158
|
+
return 0;
|
|
159
|
+
if (typeof value === 'number')
|
|
160
|
+
return value;
|
|
161
|
+
return resolveSize(value, availWidth) ?? 0;
|
|
162
|
+
}
|
|
163
|
+
function layoutBlockFlow(children, styles, boxes, x, y, availW, availH) {
|
|
164
|
+
// Layout absolute children first
|
|
165
|
+
for (const child of children) {
|
|
166
|
+
const s = styles.get(child.id);
|
|
167
|
+
if (s?.position === 'absolute' || s?.position === 'fixed') {
|
|
168
|
+
layoutNode(child, styles, boxes, x, y, availW, availH);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
let cursorX = x;
|
|
172
|
+
let cursorY = y;
|
|
173
|
+
let lineHeight = 0;
|
|
174
|
+
let maxWidth = 0;
|
|
175
|
+
let prevBlockMarginBottom = 0;
|
|
176
|
+
const flatChildren = flattenContents(children, styles);
|
|
177
|
+
for (const child of flatChildren) {
|
|
178
|
+
if (child.nodeType === 'comment')
|
|
179
|
+
continue;
|
|
180
|
+
const s = styles.get(child.id);
|
|
181
|
+
if (s?.display === 'none')
|
|
182
|
+
continue;
|
|
183
|
+
if (s?.position === 'absolute' || s?.position === 'fixed')
|
|
184
|
+
continue;
|
|
185
|
+
const isInline = child.nodeType === 'text' || s?.display === 'inline' || s?.display === 'inline-block';
|
|
186
|
+
if (isInline) {
|
|
187
|
+
// Flow horizontally
|
|
188
|
+
const size = layoutNode(child, styles, boxes, cursorX, cursorY, availW - (cursorX - x), availH);
|
|
189
|
+
cursorX += size.width;
|
|
190
|
+
lineHeight = Math.max(lineHeight, size.height);
|
|
191
|
+
maxWidth = Math.max(maxWidth, cursorX - x);
|
|
192
|
+
prevBlockMarginBottom = 0;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// Block element — new line first if we have inline content
|
|
196
|
+
if (cursorX > x) {
|
|
197
|
+
cursorY += lineHeight;
|
|
198
|
+
cursorX = x;
|
|
199
|
+
lineHeight = 0;
|
|
200
|
+
}
|
|
201
|
+
// Margin collapsing: adjacent vertical margins collapse to the larger
|
|
202
|
+
const childMarginTop = resolvePadding(s?.marginTop, availW);
|
|
203
|
+
if (prevBlockMarginBottom > 0 && childMarginTop > 0) {
|
|
204
|
+
const collapsed = Math.max(prevBlockMarginBottom, childMarginTop);
|
|
205
|
+
const overlap = prevBlockMarginBottom + childMarginTop - collapsed;
|
|
206
|
+
cursorY -= overlap;
|
|
207
|
+
}
|
|
208
|
+
const size = layoutNode(child, styles, boxes, x, cursorY, availW, availH - (cursorY - y));
|
|
209
|
+
cursorY += size.height;
|
|
210
|
+
maxWidth = Math.max(maxWidth, size.width);
|
|
211
|
+
prevBlockMarginBottom = resolvePadding(s?.marginBottom, availW);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Account for trailing inline content
|
|
215
|
+
if (cursorX > x) {
|
|
216
|
+
cursorY += lineHeight;
|
|
217
|
+
}
|
|
218
|
+
return { width: maxWidth, height: cursorY - y };
|
|
219
|
+
}
|
|
220
|
+
function layoutTable(node, styles, boxes, x, y, availW, availH) {
|
|
221
|
+
// Collect rows and cells
|
|
222
|
+
const rows = [];
|
|
223
|
+
for (const child of node.children) {
|
|
224
|
+
if (child.tag === 'tr') {
|
|
225
|
+
const cells = child.children.filter(c => c.tag === 'td' || c.tag === 'th');
|
|
226
|
+
rows.push(cells);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (rows.length === 0)
|
|
230
|
+
return { width: 0, height: 0 };
|
|
231
|
+
const numCols = Math.max(...rows.map(r => r.length));
|
|
232
|
+
const colWidths = new Array(numCols).fill(0);
|
|
233
|
+
// First pass: measure all cells to find max column widths
|
|
234
|
+
for (const row of rows) {
|
|
235
|
+
for (let col = 0; col < row.length; col++) {
|
|
236
|
+
const cell = row[col];
|
|
237
|
+
const size = layoutNode(cell, styles, boxes, 0, 0, availW, availH);
|
|
238
|
+
colWidths[col] = Math.max(colWidths[col], size.width);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Add 2 cells padding between columns
|
|
242
|
+
const colGap = 2;
|
|
243
|
+
// Second pass: position cells with aligned columns
|
|
244
|
+
let rowY = y;
|
|
245
|
+
for (const row of rows) {
|
|
246
|
+
let colX = x;
|
|
247
|
+
let rowHeight = 0;
|
|
248
|
+
// Layout the tr element
|
|
249
|
+
const trNode = node.children.find(c => c.tag === 'tr' && c.children.includes(row[0]));
|
|
250
|
+
for (let col = 0; col < row.length; col++) {
|
|
251
|
+
const cell = row[col];
|
|
252
|
+
const size = layoutNode(cell, styles, boxes, colX, rowY, colWidths[col], availH);
|
|
253
|
+
// Table cells fill their column width
|
|
254
|
+
const cellBox = boxes.get(cell.id);
|
|
255
|
+
if (cellBox && cellBox.width < colWidths[col])
|
|
256
|
+
cellBox.width = colWidths[col];
|
|
257
|
+
rowHeight = Math.max(rowHeight, size.height);
|
|
258
|
+
colX += colWidths[col] + colGap;
|
|
259
|
+
}
|
|
260
|
+
if (trNode) {
|
|
261
|
+
const trWidth = colWidths.reduce((sum, w) => sum + w, 0) + colGap * (numCols - 1);
|
|
262
|
+
boxes.set(trNode.id, { x, y: rowY, width: trWidth, height: rowHeight });
|
|
263
|
+
}
|
|
264
|
+
rowY += rowHeight;
|
|
265
|
+
}
|
|
266
|
+
const totalWidth = colWidths.reduce((sum, w) => sum + w, 0) + colGap * Math.max(0, numCols - 1);
|
|
267
|
+
return { width: totalWidth, height: rowY - y };
|
|
268
|
+
}
|
|
269
|
+
function layoutGrid(node, styles, boxes, x, y, availW, availH, style) {
|
|
270
|
+
const children = node.children.filter(c => c.nodeType === 'element' && styles.get(c.id)?.display !== 'none');
|
|
271
|
+
if (children.length === 0)
|
|
272
|
+
return { width: 0, height: 0 };
|
|
273
|
+
const colWidths = parseGridTemplate(style.gridTemplateColumns ?? '', availW);
|
|
274
|
+
const rowHeights = parseGridTemplate(style.gridTemplateRows ?? '', availH);
|
|
275
|
+
const numCols = colWidths.length || 1;
|
|
276
|
+
const gap = style.gap ?? 0;
|
|
277
|
+
let rowY = y;
|
|
278
|
+
let maxWidth = 0;
|
|
279
|
+
let col = 0;
|
|
280
|
+
let rowIdx = 0;
|
|
281
|
+
let currentRowHeight = 0; // auto-computed from content
|
|
282
|
+
for (const child of children) {
|
|
283
|
+
if (col >= numCols) {
|
|
284
|
+
const explicitH = rowHeights[rowIdx];
|
|
285
|
+
rowY += (explicitH ?? currentRowHeight) + gap;
|
|
286
|
+
col = 0;
|
|
287
|
+
rowIdx++;
|
|
288
|
+
currentRowHeight = 0;
|
|
289
|
+
}
|
|
290
|
+
const colX = x + colWidths.slice(0, col).reduce((sum, w) => sum + w + gap, 0);
|
|
291
|
+
const colW = colWidths[col] ?? availW;
|
|
292
|
+
const explicitRowH = rowHeights[rowIdx];
|
|
293
|
+
const size = layoutNode(child, styles, boxes, colX, rowY, colW, explicitRowH ?? (availH - (rowY - y)));
|
|
294
|
+
// Grid children fill their column width
|
|
295
|
+
const childBox = boxes.get(child.id);
|
|
296
|
+
if (childBox && childBox.width < colW)
|
|
297
|
+
childBox.width = colW;
|
|
298
|
+
currentRowHeight = Math.max(currentRowHeight, size.height);
|
|
299
|
+
maxWidth = Math.max(maxWidth, colX - x + colW);
|
|
300
|
+
col++;
|
|
301
|
+
}
|
|
302
|
+
const finalRowH = rowHeights[rowIdx] ?? currentRowHeight;
|
|
303
|
+
return { width: maxWidth, height: (rowY - y) + finalRowH };
|
|
304
|
+
}
|
|
305
|
+
function parseGridTemplate(template, availW) {
|
|
306
|
+
if (!template)
|
|
307
|
+
return [];
|
|
308
|
+
const parts = template.trim().split(/\s+/);
|
|
309
|
+
const widths = [];
|
|
310
|
+
const frParts = [];
|
|
311
|
+
let fixedTotal = 0;
|
|
312
|
+
for (let i = 0; i < parts.length; i++) {
|
|
313
|
+
const part = parts[i];
|
|
314
|
+
if (part.endsWith('cell')) {
|
|
315
|
+
const w = Math.round(parseFloat(part));
|
|
316
|
+
widths.push(w);
|
|
317
|
+
fixedTotal += w;
|
|
318
|
+
}
|
|
319
|
+
else if (part.endsWith('%')) {
|
|
320
|
+
const w = Math.floor(availW * parseFloat(part) / 100);
|
|
321
|
+
widths.push(w);
|
|
322
|
+
fixedTotal += w;
|
|
323
|
+
}
|
|
324
|
+
else if (part.endsWith('fr')) {
|
|
325
|
+
const fr = parseFloat(part);
|
|
326
|
+
widths.push(0); // placeholder
|
|
327
|
+
frParts.push({ index: i, fr });
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
widths.push(0);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Distribute remaining space to fr units
|
|
334
|
+
if (frParts.length > 0) {
|
|
335
|
+
const totalFr = frParts.reduce((sum, p) => sum + p.fr, 0);
|
|
336
|
+
const remaining = Math.max(0, availW - fixedTotal);
|
|
337
|
+
for (const { index, fr } of frParts) {
|
|
338
|
+
widths[index] = Math.floor(remaining * fr / totalFr);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return widths;
|
|
342
|
+
}
|
|
343
|
+
function positionChildren(children, styles, boxes, innerX, innerY, innerW, innerH, dir, gap, justify, align, wrap = 'nowrap') {
|
|
344
|
+
// Layout absolute children first (they don't affect flow)
|
|
345
|
+
for (const child of children) {
|
|
346
|
+
const s = styles.get(child.id);
|
|
347
|
+
if (s?.position === 'absolute' || s?.position === 'fixed') {
|
|
348
|
+
layoutNode(child, styles, boxes, innerX, innerY, innerW, innerH);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Flatten display:contents children into the list
|
|
352
|
+
const flatChildren = flattenContents(children, styles);
|
|
353
|
+
const visible = flatChildren.filter(c => {
|
|
354
|
+
if (c.nodeType === 'comment')
|
|
355
|
+
return false;
|
|
356
|
+
const s = styles.get(c.id);
|
|
357
|
+
if (s?.display === 'none')
|
|
358
|
+
return false;
|
|
359
|
+
if (s?.position === 'absolute' || s?.position === 'fixed')
|
|
360
|
+
return false;
|
|
361
|
+
return true;
|
|
362
|
+
});
|
|
363
|
+
if (visible.length === 0)
|
|
364
|
+
return { width: 0, height: 0 };
|
|
365
|
+
// Pre-measure to filter out zero-size items (e.g. whitespace text nodes)
|
|
366
|
+
const measured = visible.map(child => ({
|
|
367
|
+
child,
|
|
368
|
+
size: layoutNode(child, styles, boxes, 0, 0, innerW, innerH),
|
|
369
|
+
}));
|
|
370
|
+
const nonEmpty = measured.filter(({ size }) => size.width > 0 || size.height > 0);
|
|
371
|
+
if (nonEmpty.length === 0)
|
|
372
|
+
return { width: 0, height: 0 };
|
|
373
|
+
// Sort by order property, then handle reverse
|
|
374
|
+
const sorted = [...nonEmpty].sort((a, b) => {
|
|
375
|
+
const orderA = styles.get(a.child.id)?.order ?? 0;
|
|
376
|
+
const orderB = styles.get(b.child.id)?.order ?? 0;
|
|
377
|
+
return orderA - orderB;
|
|
378
|
+
});
|
|
379
|
+
const isReverse = dir === 'row-reverse' || dir === 'column-reverse';
|
|
380
|
+
const baseDir = (dir === 'row' || dir === 'row-reverse') ? 'row' : 'column';
|
|
381
|
+
const orderedItems = isReverse ? sorted.reverse() : sorted;
|
|
382
|
+
const ordered = orderedItems.map(item => item.child);
|
|
383
|
+
// Use pre-measured sizes, overridden by flex-basis when set
|
|
384
|
+
const sizes = orderedItems.map(item => {
|
|
385
|
+
const s = styles.get(item.child.id);
|
|
386
|
+
const basis = s?.flexBasis;
|
|
387
|
+
if (basis !== undefined && basis !== 'auto') {
|
|
388
|
+
const basisValue = typeof basis === 'number' ? basis : 0;
|
|
389
|
+
return baseDir === 'row'
|
|
390
|
+
? { width: basisValue, height: item.size.height }
|
|
391
|
+
: { width: item.size.width, height: basisValue };
|
|
392
|
+
}
|
|
393
|
+
return item.size;
|
|
394
|
+
});
|
|
395
|
+
const growValues = ordered.map(child => styles.get(child.id)?.flexGrow ?? 0);
|
|
396
|
+
const shrinkValues = ordered.map(child => styles.get(child.id)?.flexShrink ?? 1);
|
|
397
|
+
const totalGrow = growValues.reduce((a, b) => a + b, 0);
|
|
398
|
+
const totalMain = sizes.reduce((sum, s, i) => {
|
|
399
|
+
return sum + (baseDir === 'row' ? s.width : s.height) + (i > 0 ? gap : 0);
|
|
400
|
+
}, 0);
|
|
401
|
+
const availMain = baseDir === 'row' ? innerW : innerH;
|
|
402
|
+
const freeSpace = Math.max(0, availMain - totalMain);
|
|
403
|
+
const overflow = Math.max(0, totalMain - availMain);
|
|
404
|
+
const hasGrow = totalGrow > 0;
|
|
405
|
+
const totalShrink = overflow > 0 ? shrinkValues.reduce((a, b) => a + b, 0) : 0;
|
|
406
|
+
// Position
|
|
407
|
+
let mainPos = computeMainStart(justify, freeSpace, ordered.length, hasGrow);
|
|
408
|
+
const itemGap = computeItemGap(justify, gap, freeSpace, ordered.length, hasGrow);
|
|
409
|
+
let contentWidth = 0;
|
|
410
|
+
let contentHeight = 0;
|
|
411
|
+
let crossPos = 0;
|
|
412
|
+
let lineHeight = 0;
|
|
413
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
414
|
+
let mainSize = baseDir === 'row' ? sizes[i].width : sizes[i].height;
|
|
415
|
+
if (hasGrow && growValues[i] > 0) {
|
|
416
|
+
mainSize += Math.floor(freeSpace * growValues[i] / totalGrow);
|
|
417
|
+
}
|
|
418
|
+
if (overflow > 0 && totalShrink > 0 && shrinkValues[i] > 0) {
|
|
419
|
+
mainSize -= Math.floor(overflow * shrinkValues[i] / totalShrink);
|
|
420
|
+
mainSize = Math.max(0, mainSize);
|
|
421
|
+
}
|
|
422
|
+
// Wrap check
|
|
423
|
+
if (wrap === 'wrap' && mainPos + mainSize > availMain && i > 0) {
|
|
424
|
+
crossPos += lineHeight + gap;
|
|
425
|
+
mainPos = 0;
|
|
426
|
+
lineHeight = 0;
|
|
427
|
+
}
|
|
428
|
+
const crossSize = baseDir === 'row' ? sizes[i].height : sizes[i].width;
|
|
429
|
+
const crossAvail = baseDir === 'row' ? innerH : innerW;
|
|
430
|
+
// Check align-self
|
|
431
|
+
const childStyle = styles.get(ordered[i].id);
|
|
432
|
+
const selfAlign = childStyle?.alignSelf !== 'auto'
|
|
433
|
+
? childStyle?.alignSelf ?? align
|
|
434
|
+
: align;
|
|
435
|
+
const crossOffset = computeCrossOffset(selfAlign, crossAvail, crossSize);
|
|
436
|
+
const finalCx = baseDir === 'row' ? innerX + mainPos : innerX + crossOffset;
|
|
437
|
+
const finalCy = baseDir === 'row' ? innerY + crossPos + crossOffset : innerY + mainPos;
|
|
438
|
+
const childAvailW = baseDir === 'row' ? mainSize : innerW;
|
|
439
|
+
const childAvailH = baseDir === 'row' ? innerH : mainSize;
|
|
440
|
+
layoutNode(ordered[i], styles, boxes, finalCx, finalCy, childAvailW, childAvailH);
|
|
441
|
+
// Override main-axis size for flex-grown/shrunk items
|
|
442
|
+
const box = boxes.get(ordered[i].id);
|
|
443
|
+
if (box) {
|
|
444
|
+
if (baseDir === 'row' && box.width !== mainSize)
|
|
445
|
+
box.width = mainSize;
|
|
446
|
+
if (baseDir === 'column' && box.height !== mainSize)
|
|
447
|
+
box.height = mainSize;
|
|
448
|
+
}
|
|
449
|
+
lineHeight = Math.max(lineHeight, baseDir === 'row' ? sizes[i].height : sizes[i].width);
|
|
450
|
+
mainPos += mainSize + (i < ordered.length - 1 ? itemGap : 0);
|
|
451
|
+
contentWidth = baseDir === 'row' ? Math.max(contentWidth, mainPos) : Math.max(contentWidth, sizes[i].width);
|
|
452
|
+
contentHeight = baseDir === 'row' ? crossPos + lineHeight : mainPos;
|
|
453
|
+
}
|
|
454
|
+
return { width: contentWidth, height: contentHeight };
|
|
455
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ResolvedStyle } from '../css/compute.js';
|
|
2
|
+
export declare function computeMainStart(justify: ResolvedStyle['justifyContent'], freeSpace: number, count: number, hasGrow: boolean): number;
|
|
3
|
+
export declare function computeItemGap(justify: ResolvedStyle['justifyContent'], gap: number, freeSpace: number, count: number, hasGrow: boolean): number;
|
|
4
|
+
export declare function computeCrossOffset(align: ResolvedStyle['alignItems'], crossAvail: number, crossSize: number): number;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function computeMainStart(justify, freeSpace, count, hasGrow) {
|
|
2
|
+
if (hasGrow || count === 0)
|
|
3
|
+
return 0;
|
|
4
|
+
switch (justify) {
|
|
5
|
+
case 'end': return freeSpace;
|
|
6
|
+
case 'center': return Math.floor(freeSpace / 2);
|
|
7
|
+
case 'space-around': return Math.floor(freeSpace / (count * 2));
|
|
8
|
+
case 'space-evenly': return Math.floor(freeSpace / (count + 1));
|
|
9
|
+
default: return 0;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function computeItemGap(justify, gap, freeSpace, count, hasGrow) {
|
|
13
|
+
if (hasGrow)
|
|
14
|
+
return gap;
|
|
15
|
+
if (count <= 1)
|
|
16
|
+
return 0;
|
|
17
|
+
switch (justify) {
|
|
18
|
+
case 'space-between': return Math.floor(freeSpace / (count - 1));
|
|
19
|
+
case 'space-around': return Math.floor(freeSpace / count);
|
|
20
|
+
case 'space-evenly': return Math.floor(freeSpace / (count + 1));
|
|
21
|
+
default: return gap;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function computeCrossOffset(align, crossAvail, crossSize) {
|
|
25
|
+
switch (align) {
|
|
26
|
+
case 'end': return crossAvail - crossSize;
|
|
27
|
+
case 'center': return Math.floor((crossAvail - crossSize) / 2);
|
|
28
|
+
default: return 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TermNode } from '../renderer/node.js';
|
|
2
|
+
import { ResolvedStyle } from '../css/compute.js';
|
|
3
|
+
import { LayoutBox } from './engine.js';
|
|
4
|
+
/**
|
|
5
|
+
* Incrementally re-layout only dirty subtrees.
|
|
6
|
+
* Falls back to full layout when dirty nodes affect auto-sized ancestors.
|
|
7
|
+
*/
|
|
8
|
+
export declare function computeLayoutIncremental(root: TermNode, styles: Map<number, ResolvedStyle>, existingLayout: Map<number, LayoutBox>, dirtyNodes: Set<TermNode>, availWidth: number, availHeight: number, onLayout?: () => void): Map<number, LayoutBox>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { computeLayout } from './engine.js';
|
|
2
|
+
/**
|
|
3
|
+
* Incrementally re-layout only dirty subtrees.
|
|
4
|
+
* Falls back to full layout when dirty nodes affect auto-sized ancestors.
|
|
5
|
+
*/
|
|
6
|
+
export function computeLayoutIncremental(root, styles, existingLayout, dirtyNodes, availWidth, availHeight, onLayout) {
|
|
7
|
+
if (dirtyNodes.size === 0)
|
|
8
|
+
return existingLayout;
|
|
9
|
+
// Check if any dirty node has an auto-sized ancestor — if so, full re-layout
|
|
10
|
+
// because the size change may propagate up
|
|
11
|
+
const needsFullLayout = [...dirtyNodes].some(node => hasAutoSizedAncestor(node, styles));
|
|
12
|
+
if (needsFullLayout) {
|
|
13
|
+
onLayout?.();
|
|
14
|
+
return computeLayout(root, styles, availWidth, availHeight);
|
|
15
|
+
}
|
|
16
|
+
// All dirty nodes are within fixed-size containers — only re-layout those subtrees
|
|
17
|
+
const result = new Map(existingLayout);
|
|
18
|
+
for (const dirtyNode of dirtyNodes) {
|
|
19
|
+
const boundary = findLayoutBoundary(dirtyNode, styles);
|
|
20
|
+
const boundaryBox = existingLayout.get(boundary.id);
|
|
21
|
+
if (!boundaryBox)
|
|
22
|
+
continue;
|
|
23
|
+
onLayout?.();
|
|
24
|
+
const subtreeLayout = computeLayout(boundary, styles, boundaryBox.width, boundaryBox.height);
|
|
25
|
+
// Merge subtree layout into result, adjusting positions
|
|
26
|
+
for (const [id, box] of subtreeLayout) {
|
|
27
|
+
result.set(id, {
|
|
28
|
+
x: box.x + boundaryBox.x,
|
|
29
|
+
y: box.y + boundaryBox.y,
|
|
30
|
+
width: box.width,
|
|
31
|
+
height: box.height,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// The boundary itself keeps its original position
|
|
35
|
+
result.set(boundary.id, boundaryBox);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
function hasAutoSizedAncestor(node, styles) {
|
|
40
|
+
let current = node.parent;
|
|
41
|
+
while (current) {
|
|
42
|
+
const style = styles.get(current.id);
|
|
43
|
+
if (!style || style.width === null || style.height === null)
|
|
44
|
+
return true;
|
|
45
|
+
current = current.parent;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
function findLayoutBoundary(node, styles) {
|
|
50
|
+
let current = node.parent;
|
|
51
|
+
while (current?.parent) {
|
|
52
|
+
const style = styles.get(current.id);
|
|
53
|
+
if (style && style.width !== null && style.height !== null)
|
|
54
|
+
return current;
|
|
55
|
+
current = current.parent;
|
|
56
|
+
}
|
|
57
|
+
return current ?? node;
|
|
58
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { evaluateCalc } from '../css/calc.js';
|
|
2
|
+
export function resolveSize(value, available) {
|
|
3
|
+
if (value === null || value === undefined)
|
|
4
|
+
return null;
|
|
5
|
+
if (typeof value === 'number')
|
|
6
|
+
return value;
|
|
7
|
+
if (typeof value === 'string') {
|
|
8
|
+
if (value.endsWith('%')) {
|
|
9
|
+
return Math.floor(available * parseFloat(value) / 100);
|
|
10
|
+
}
|
|
11
|
+
// calc(), min(), max(), clamp()
|
|
12
|
+
const calcResult = evaluateCalc(value, available);
|
|
13
|
+
if (calcResult !== null)
|
|
14
|
+
return calcResult;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
export function constrain(value, min, max) {
|
|
19
|
+
let result = value;
|
|
20
|
+
if (min != null)
|
|
21
|
+
result = Math.max(result, min);
|
|
22
|
+
if (max != null)
|
|
23
|
+
result = Math.min(result, max);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function wrapText(text: string, width: number): string[];
|
|
2
|
+
export declare function truncateText(text: string, width: number): string;
|
|
3
|
+
export declare function truncateMiddle(text: string, width: number): string;
|
|
4
|
+
export declare function measureText(text: string, availWidth: number): {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function wrapText(text, width) {
|
|
2
|
+
if (text === '')
|
|
3
|
+
return [''];
|
|
4
|
+
if (text.length <= width)
|
|
5
|
+
return [text];
|
|
6
|
+
const lines = [];
|
|
7
|
+
let remaining = text;
|
|
8
|
+
while (remaining.length > 0) {
|
|
9
|
+
if (remaining.length <= width) {
|
|
10
|
+
lines.push(remaining);
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
// Find last space within width
|
|
14
|
+
let breakAt = remaining.lastIndexOf(' ', width);
|
|
15
|
+
if (breakAt <= 0) {
|
|
16
|
+
// No space found — hard break at width
|
|
17
|
+
breakAt = width;
|
|
18
|
+
lines.push(remaining.substring(0, breakAt));
|
|
19
|
+
remaining = remaining.substring(breakAt);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
lines.push(remaining.substring(0, breakAt));
|
|
23
|
+
remaining = remaining.substring(breakAt + 1); // skip the space
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return lines;
|
|
27
|
+
}
|
|
28
|
+
export function truncateText(text, width) {
|
|
29
|
+
if (width <= 0)
|
|
30
|
+
return '';
|
|
31
|
+
if (text.length <= width)
|
|
32
|
+
return text;
|
|
33
|
+
if (width === 1)
|
|
34
|
+
return '…';
|
|
35
|
+
return text.substring(0, width - 1) + '…';
|
|
36
|
+
}
|
|
37
|
+
export function truncateMiddle(text, width) {
|
|
38
|
+
if (width <= 0)
|
|
39
|
+
return '';
|
|
40
|
+
if (text.length <= width)
|
|
41
|
+
return text;
|
|
42
|
+
if (width <= 3)
|
|
43
|
+
return text.substring(0, width - 1) + '…';
|
|
44
|
+
const half = Math.floor((width - 1) / 2);
|
|
45
|
+
const endLen = width - 1 - half;
|
|
46
|
+
return text.substring(0, half) + '…' + text.substring(text.length - endLen);
|
|
47
|
+
}
|
|
48
|
+
export function measureText(text, availWidth) {
|
|
49
|
+
const lines = wrapText(text, availWidth);
|
|
50
|
+
const maxLineWidth = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
51
|
+
return { width: maxLineWidth, height: lines.length };
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare function moveTo(col: number, row: number): string;
|
|
2
|
+
export declare function clearScreen(): string;
|
|
3
|
+
export declare function hideCursor(): string;
|
|
4
|
+
export declare function showCursor(): string;
|
|
5
|
+
export declare function enterAltScreen(): string;
|
|
6
|
+
export declare function exitAltScreen(): string;
|
|
7
|
+
export declare function resetStyle(): string;
|
|
8
|
+
export declare function bold(): string;
|
|
9
|
+
export declare function dim(): string;
|
|
10
|
+
export declare function italic(): string;
|
|
11
|
+
export declare function underline(): string;
|
|
12
|
+
export declare function strikethrough(): string;
|
|
13
|
+
export declare function fgColor(color: string): string;
|
|
14
|
+
export declare function bgColor(color: string): string;
|
|
15
|
+
export declare function hyperlinkOpen(url: string): string;
|
|
16
|
+
export declare function hyperlinkClose(): string;
|
|
17
|
+
export declare function enableMouse(): string;
|
|
18
|
+
export declare function disableMouse(): string;
|
|
19
|
+
export declare function setCursorShape(shape: 'block' | 'underline' | 'bar'): string;
|
|
20
|
+
export declare function enableBracketedPaste(): string;
|
|
21
|
+
export declare function disableBracketedPaste(): string;
|
|
22
|
+
export declare function beginSyncUpdate(): string;
|
|
23
|
+
export declare function endSyncUpdate(): string;
|