@svelterm/core 0.1.0 → 0.21.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/CHANGELOG.md +425 -0
- package/README.md +42 -29
- package/dist/src/cli/build.d.ts +13 -0
- package/dist/src/cli/build.js +119 -0
- package/dist/src/cli/bundle.d.ts +25 -0
- package/dist/src/cli/bundle.js +61 -0
- package/dist/src/cli/dev.d.ts +10 -0
- package/dist/src/cli/dev.js +152 -0
- package/dist/src/cli/devtools.d.ts +9 -0
- package/dist/src/cli/devtools.js +47 -0
- package/dist/src/cli/init.d.ts +8 -0
- package/dist/src/cli/init.js +153 -0
- package/dist/src/cli/main.d.ts +9 -0
- package/dist/src/cli/main.js +52 -0
- package/dist/src/cli/svt-bin.d.ts +2 -0
- package/dist/src/cli/svt-bin.js +6 -0
- package/dist/src/cli/svt.d.ts +14 -0
- package/dist/src/cli/svt.js +76 -0
- package/dist/src/components/text-buffer.js +8 -5
- package/dist/src/css/animation-runner.d.ts +15 -6
- package/dist/src/css/animation-runner.js +80 -29
- package/dist/src/css/animation.d.ts +12 -0
- package/dist/src/css/animation.js +21 -0
- package/dist/src/css/calc.js +4 -3
- package/dist/src/css/color.d.ts +19 -0
- package/dist/src/css/color.js +371 -62
- package/dist/src/css/compute.d.ts +30 -3
- package/dist/src/css/compute.js +272 -33
- package/dist/src/css/defaults.d.ts +1 -1
- package/dist/src/css/defaults.js +9 -0
- package/dist/src/css/easing.d.ts +9 -0
- package/dist/src/css/easing.js +95 -0
- package/dist/src/css/incremental.d.ts +1 -1
- package/dist/src/css/incremental.js +2 -2
- package/dist/src/css/interpolate.d.ts +13 -0
- package/dist/src/css/interpolate.js +41 -0
- package/dist/src/css/parser.js +59 -3
- package/dist/src/css/pseudo-elements.d.ts +9 -0
- package/dist/src/css/pseudo-elements.js +97 -0
- package/dist/src/css/selector.d.ts +17 -2
- package/dist/src/css/selector.js +128 -13
- package/dist/src/css/specificity.js +17 -6
- package/dist/src/css/values.d.ts +6 -1
- package/dist/src/css/values.js +13 -6
- package/dist/src/debug/context.d.ts +13 -0
- package/dist/src/debug/context.js +11 -0
- package/dist/src/debug/css.d.ts +12 -0
- package/dist/src/debug/css.js +28 -0
- package/dist/src/debug/dom.d.ts +17 -0
- package/dist/src/debug/dom.js +92 -0
- package/dist/src/devtools/DevTools.compiled.js +327 -0
- package/dist/src/devtools/DevTools.css.js +1 -0
- package/dist/src/devtools/client.d.ts +36 -0
- package/dist/src/devtools/client.js +76 -0
- package/dist/src/framelog.d.ts +54 -0
- package/dist/src/framelog.js +99 -0
- package/dist/src/headless.js +12 -4
- package/dist/src/index.d.ts +65 -3
- package/dist/src/index.js +609 -81
- package/dist/src/input/checkable.d.ts +8 -0
- package/dist/src/input/checkable.js +66 -0
- package/dist/src/input/details.d.ts +6 -0
- package/dist/src/input/details.js +34 -0
- package/dist/src/input/focus.d.ts +6 -0
- package/dist/src/input/focus.js +27 -9
- package/dist/src/input/keyboard.d.ts +2 -2
- package/dist/src/input/keyboard.js +32 -5
- package/dist/src/input/label.d.ts +8 -0
- package/dist/src/input/label.js +53 -0
- package/dist/src/input/modal.d.ts +9 -0
- package/dist/src/input/modal.js +28 -0
- package/dist/src/input/mouse.d.ts +2 -2
- package/dist/src/input/mouse.js +15 -2
- package/dist/src/input/select.d.ts +12 -0
- package/dist/src/input/select.js +63 -0
- package/dist/src/input/selection.d.ts +48 -0
- package/dist/src/input/selection.js +150 -0
- package/dist/src/layout/engine.d.ts +2 -0
- package/dist/src/layout/engine.js +1084 -142
- package/dist/src/layout/flex.js +4 -4
- package/dist/src/layout/size.js +3 -2
- package/dist/src/layout/text.d.ts +3 -2
- package/dist/src/layout/text.js +96 -17
- package/dist/src/layout/unicode.d.ts +20 -0
- package/dist/src/layout/unicode.js +121 -0
- package/dist/src/render/animation-clock.d.ts +51 -0
- package/dist/src/render/animation-clock.js +213 -0
- package/dist/src/render/ansi-text.d.ts +26 -0
- package/dist/src/render/ansi-text.js +131 -0
- package/dist/src/render/ansi.d.ts +18 -0
- package/dist/src/render/ansi.js +64 -19
- package/dist/src/render/border.js +166 -17
- package/dist/src/render/buffer.d.ts +1 -0
- package/dist/src/render/buffer.js +5 -2
- package/dist/src/render/color-depth.d.ts +8 -0
- package/dist/src/render/color-depth.js +59 -0
- package/dist/src/render/context.d.ts +1 -0
- package/dist/src/render/context.js +17 -21
- package/dist/src/render/cursor-emit.d.ts +18 -0
- package/dist/src/render/cursor-emit.js +50 -0
- package/dist/src/render/diff.d.ts +12 -0
- package/dist/src/render/diff.js +120 -0
- package/dist/src/render/generation.d.ts +9 -0
- package/dist/src/render/generation.js +14 -0
- package/dist/src/render/graphics-layer.d.ts +27 -0
- package/dist/src/render/graphics-layer.js +86 -0
- package/dist/src/render/image.d.ts +27 -0
- package/dist/src/render/image.js +113 -0
- package/dist/src/render/incremental-paint.d.ts +7 -3
- package/dist/src/render/incremental-paint.js +52 -79
- package/dist/src/render/inline.d.ts +59 -0
- package/dist/src/render/inline.js +219 -0
- package/dist/src/render/kitty-graphics.d.ts +24 -0
- package/dist/src/render/kitty-graphics.js +58 -0
- package/dist/src/render/paint-text.js +68 -22
- package/dist/src/render/paint.d.ts +8 -1
- package/dist/src/render/paint.js +328 -30
- package/dist/src/render/png.d.ts +13 -0
- package/dist/src/render/png.js +145 -0
- package/dist/src/render/scrollbar.d.ts +8 -2
- package/dist/src/render/scrollbar.js +71 -14
- package/dist/src/render/snapshot.js +3 -1
- package/dist/src/renderer/default.d.ts +7 -0
- package/dist/src/renderer/default.js +11 -0
- package/dist/src/renderer/index.d.ts +8 -2
- package/dist/src/renderer/index.js +4 -2
- package/dist/src/renderer/node.d.ts +109 -0
- package/dist/src/renderer/node.js +165 -1
- package/dist/src/terminal/capabilities.d.ts +33 -0
- package/dist/src/terminal/capabilities.js +66 -0
- package/dist/src/terminal/clipboard.d.ts +9 -0
- package/dist/src/terminal/clipboard.js +39 -0
- package/dist/src/terminal/io.d.ts +82 -0
- package/dist/src/terminal/io.js +155 -0
- package/dist/src/terminal/screen.d.ts +3 -10
- package/dist/src/terminal/screen.js +5 -28
- package/dist/src/terminal/stdin-router.d.ts +8 -5
- package/dist/src/terminal/stdin-router.js +22 -11
- package/dist/src/utils/node-map.d.ts +24 -0
- package/dist/src/utils/node-map.js +75 -0
- package/dist/src/vite/config.d.ts +62 -0
- package/dist/src/vite/config.js +191 -0
- package/docs/compatibility.md +67 -0
- package/docs/debug/devtools.md +40 -0
- package/docs/debug/svt.md +50 -0
- package/docs/distribution.md +106 -0
- package/docs/elements.md +120 -0
- package/docs/getting-started.md +177 -0
- package/docs/guide/css.md +187 -0
- package/docs/guide/input.md +143 -0
- package/docs/guide/layout.md +171 -0
- package/docs/guide/theming.md +94 -0
- package/docs/how-it-works.md +115 -0
- package/docs/inline-mode.md +77 -0
- package/docs/layout.md +106 -0
- package/docs/motion.md +91 -0
- package/docs/reference/README.md +65 -0
- package/docs/reference/css/properties/border-corner.md +82 -0
- package/docs/reference/css/properties/border-style.md +168 -0
- package/docs/reference.md +226 -0
- package/docs/selectors.md +80 -0
- package/docs/terminal-css.md +149 -0
- package/docs/terminals.md +83 -0
- package/package.json +28 -7
package/dist/src/render/paint.js
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
|
+
import { SvtRegionNode, childrenWithPseudos, hasBooleanAttribute } from '../renderer/node.js';
|
|
1
2
|
import { renderBorder } from './border.js';
|
|
2
3
|
import { paintTextContent } from './paint-text.js';
|
|
4
|
+
import { blendColor } from '../css/color.js';
|
|
5
|
+
import { stringWidth } from '../layout/unicode.js';
|
|
6
|
+
import { bumpPaintGeneration, paintGeneration } from './generation.js';
|
|
7
|
+
import { ensureImageLoading, paintImage } from './image.js';
|
|
8
|
+
import { renderScrollbar, renderHScrollbar } from './scrollbar.js';
|
|
9
|
+
import { dispatchEvent } from '../input/dispatch.js';
|
|
10
|
+
import { selectOptions, selectedIndex } from '../input/select.js';
|
|
3
11
|
const DEFAULT_VISUALS = {
|
|
4
12
|
fg: 'default', bg: 'default',
|
|
5
13
|
bold: false, italic: false, underline: false, strikethrough: false, dim: false,
|
|
6
14
|
};
|
|
7
15
|
const NO_SCROLL = { x: 0, y: 0 };
|
|
8
|
-
export function paint(root, buffer, styles, layout) {
|
|
9
|
-
|
|
16
|
+
export function paint(root, buffer, styles, layout, damageClip) {
|
|
17
|
+
// Damage-clipped paints repaint a region, not the whole tree — nodes
|
|
18
|
+
// outside the damage keep their cached cursor positions.
|
|
19
|
+
if (!damageClip)
|
|
20
|
+
bumpPaintGeneration();
|
|
21
|
+
paintNode(root, buffer, styles, layout, DEFAULT_VISUALS, null, NO_SCROLL, damageClip);
|
|
10
22
|
}
|
|
11
|
-
function paintNode(node, buffer, styles, layout, inherited, clip, scroll) {
|
|
23
|
+
function paintNode(node, buffer, styles, layout, inherited, clip, scroll, damageClip) {
|
|
12
24
|
if (node.nodeType === 'comment')
|
|
13
25
|
return;
|
|
14
26
|
const visuals = resolveVisuals(node, styles, inherited);
|
|
15
27
|
const rawBox = layout?.get(node.id);
|
|
16
28
|
const box = rawBox ? applyScroll(rawBox, scroll) : undefined;
|
|
29
|
+
// Skip nodes entirely outside the damage region
|
|
30
|
+
if (damageClip && box && !boxesOverlap(box, damageClip))
|
|
31
|
+
return;
|
|
32
|
+
// Cull subtrees fully outside the active clip: every cell write is
|
|
33
|
+
// clipped anyway, so this can't change output — it skips the walk.
|
|
34
|
+
// (In-flow descendants sit inside their ancestor's box; positioned
|
|
35
|
+
// ones offset from their parent, so they leave with it.)
|
|
36
|
+
if (clip && box && box.width > 0 && box.height > 0 && !boxesOverlap(box, clip))
|
|
37
|
+
return;
|
|
17
38
|
// Check display:none — element and all descendants are invisible and take no space
|
|
18
39
|
const ownStyle = node.nodeType === 'element' ? styles?.get(node.id) : undefined;
|
|
19
40
|
const parentStyle = node.parent ? styles?.get(node.parent.id) : undefined;
|
|
@@ -29,19 +50,49 @@ function paintNode(node, buffer, styles, layout, inherited, clip, scroll) {
|
|
|
29
50
|
return;
|
|
30
51
|
}
|
|
31
52
|
if (node.nodeType === 'element' && box && !isHidden) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
const hideEmptyCell = ownStyle?.emptyCells === 'hide'
|
|
54
|
+
&& ownStyle.display === 'table-cell' && isEmptyCell(node);
|
|
55
|
+
if (!hideEmptyCell) {
|
|
56
|
+
fillBackground(buffer, box, visuals, clip, ownStyle);
|
|
57
|
+
if (ownStyle && ownStyle.borderStyle !== 'none') {
|
|
58
|
+
renderBorder(buffer, box, ownStyle);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (node.tag === 'img') {
|
|
62
|
+
ensureImageLoading(node);
|
|
63
|
+
paintImage(node, buffer, box, clip);
|
|
64
|
+
return; // replaced element — no children render
|
|
35
65
|
}
|
|
36
66
|
if (node.tag === 'hr') {
|
|
37
67
|
paintHorizontalRule(buffer, box, visuals, clip);
|
|
38
68
|
return;
|
|
39
69
|
}
|
|
70
|
+
if (node.tag === 'progress' || node.tag === 'meter') {
|
|
71
|
+
paintValueBar(node, buffer, box, visuals, clip);
|
|
72
|
+
return; // fallback content is not rendered, as in browsers
|
|
73
|
+
}
|
|
74
|
+
if (node.tag === 'select') {
|
|
75
|
+
paintSelect(node, buffer, box, visuals, clip);
|
|
76
|
+
return; // options render through the select's own label
|
|
77
|
+
}
|
|
40
78
|
if (node.tag === 'li') {
|
|
41
79
|
paintListMarker(node, buffer, box, visuals, clip);
|
|
42
80
|
}
|
|
81
|
+
if (node.tag === 'summary') {
|
|
82
|
+
paintSummaryMarker(node, buffer, box, visuals, clip);
|
|
83
|
+
}
|
|
43
84
|
if (node.tag === 'input' && box) {
|
|
44
|
-
|
|
85
|
+
const inputType = node.attributes.get('type');
|
|
86
|
+
if (inputType === 'checkbox' || inputType === 'radio') {
|
|
87
|
+
paintCheckable(node, buffer, box, visuals, clip);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
paintInput(node, buffer, box, visuals, clip);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (node instanceof SvtRegionNode) {
|
|
94
|
+
paintRegion(node, buffer, box, clip);
|
|
95
|
+
return;
|
|
45
96
|
}
|
|
46
97
|
}
|
|
47
98
|
// Determine clip and scroll for children
|
|
@@ -49,16 +100,101 @@ function paintNode(node, buffer, styles, layout, inherited, clip, scroll) {
|
|
|
49
100
|
let childScroll = scroll;
|
|
50
101
|
if (node.nodeType === 'element' && box) {
|
|
51
102
|
const ownStyle = styles?.get(node.id);
|
|
52
|
-
if (
|
|
53
|
-
|
|
103
|
+
if (node.tag === 'root') {
|
|
104
|
+
// Root clips to the terminal viewport (buffer bounds)
|
|
105
|
+
childClip = intersectClip(clip, { x: 0, y: 0, width: buffer.width, height: buffer.height });
|
|
106
|
+
}
|
|
107
|
+
else if (ownStyle && ownStyle.overflow !== 'visible') {
|
|
108
|
+
// Clip to the content box, inside any border — otherwise
|
|
109
|
+
// scrolled content paints over the border cells.
|
|
110
|
+
const inset = (ownStyle.borderStyle && ownStyle.borderStyle !== 'none') ? 1 : 0;
|
|
111
|
+
childClip = intersectClip(clip, {
|
|
112
|
+
x: box.x + inset, y: box.y + inset,
|
|
113
|
+
width: Math.max(0, box.width - inset * 2),
|
|
114
|
+
height: Math.max(0, box.height - inset * 2),
|
|
115
|
+
});
|
|
54
116
|
}
|
|
55
117
|
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
|
|
56
118
|
childScroll = { x: scroll.x + node.scrollLeft, y: scroll.y + node.scrollTop };
|
|
57
119
|
}
|
|
58
120
|
}
|
|
59
|
-
for (const child of node
|
|
60
|
-
paintNode(child, buffer, styles, layout, visuals, childClip, childScroll);
|
|
121
|
+
for (const child of childrenWithPseudos(node)) {
|
|
122
|
+
paintNode(child, buffer, styles, layout, visuals, childClip, childScroll, damageClip);
|
|
123
|
+
}
|
|
124
|
+
// Render scrollbar overlays for scrollable containers
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
const showVScroll = node.nodeType === 'element' && node.scrollbarVisibleUntil > now;
|
|
127
|
+
const showHScroll = node.nodeType === 'element' && node.hScrollbarVisibleUntil > now;
|
|
128
|
+
if (showVScroll || showHScroll) {
|
|
129
|
+
const nodeBox = layout?.get(node.id);
|
|
130
|
+
let contentHeight = 0;
|
|
131
|
+
let contentWidth = 0;
|
|
132
|
+
if (nodeBox && layout) {
|
|
133
|
+
// Scroll frames reuse the same layout map — walking a huge
|
|
134
|
+
// list's children every scrollbar frame would dominate paint.
|
|
135
|
+
const extent = cachedContentExtent(node, layout, nodeBox);
|
|
136
|
+
contentHeight = extent.height;
|
|
137
|
+
contentWidth = extent.width;
|
|
138
|
+
}
|
|
139
|
+
const visibleMs = 600;
|
|
140
|
+
const fadeMs = 400;
|
|
141
|
+
if (showVScroll) {
|
|
142
|
+
const remaining = node.scrollbarVisibleUntil - now;
|
|
143
|
+
const opacity = remaining > fadeMs ? 1 : remaining / fadeMs;
|
|
144
|
+
if (node.tag === 'root') {
|
|
145
|
+
renderScrollbar(buffer, 0, 0, buffer.width, buffer.height, contentHeight, node.scrollTop, opacity);
|
|
146
|
+
}
|
|
147
|
+
else if (box) {
|
|
148
|
+
renderScrollbar(buffer, box.x, box.y, box.width, box.height, contentHeight, node.scrollTop, opacity);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (showHScroll && !showVScroll) {
|
|
152
|
+
const remaining = node.hScrollbarVisibleUntil - now;
|
|
153
|
+
const opacity = remaining > fadeMs ? 1 : remaining / fadeMs;
|
|
154
|
+
if (node.tag === 'root') {
|
|
155
|
+
renderHScrollbar(buffer, 0, 0, buffer.width, buffer.height, contentWidth, node.scrollLeft, opacity);
|
|
156
|
+
}
|
|
157
|
+
else if (box) {
|
|
158
|
+
renderHScrollbar(buffer, box.x, box.y, box.width, box.height, contentWidth, node.scrollLeft, opacity);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Content extents per (layout map, node) — valid as long as the layout is. */
|
|
164
|
+
const extentCache = new WeakMap();
|
|
165
|
+
function cachedContentExtent(node, layout, nodeBox) {
|
|
166
|
+
let perNode = extentCache.get(layout);
|
|
167
|
+
if (!perNode) {
|
|
168
|
+
perNode = new Map();
|
|
169
|
+
extentCache.set(layout, perNode);
|
|
61
170
|
}
|
|
171
|
+
const cached = perNode.get(node.id);
|
|
172
|
+
if (cached)
|
|
173
|
+
return cached;
|
|
174
|
+
let height = 0;
|
|
175
|
+
let width = 0;
|
|
176
|
+
const walk = (n) => {
|
|
177
|
+
const cBox = layout.get(n.id);
|
|
178
|
+
if (cBox) {
|
|
179
|
+
height = Math.max(height, cBox.y - nodeBox.y + cBox.height);
|
|
180
|
+
width = Math.max(width, cBox.x - nodeBox.x + cBox.width);
|
|
181
|
+
}
|
|
182
|
+
for (const child of n.children)
|
|
183
|
+
walk(child);
|
|
184
|
+
};
|
|
185
|
+
for (const child of node.children)
|
|
186
|
+
walk(child);
|
|
187
|
+
const extent = { width, height };
|
|
188
|
+
perNode.set(node.id, extent);
|
|
189
|
+
return extent;
|
|
190
|
+
}
|
|
191
|
+
/** A table cell is empty (for empty-cells: hide) if it has no element children and no visible text. */
|
|
192
|
+
function isEmptyCell(node) {
|
|
193
|
+
return node.children.every(child => child.nodeType === 'text' && (child.textContent ?? '').trim() === '');
|
|
194
|
+
}
|
|
195
|
+
function boxesOverlap(a, b) {
|
|
196
|
+
return a.x < b.x + b.width && a.x + a.width > b.x
|
|
197
|
+
&& a.y < b.y + b.height && a.y + a.height > b.y;
|
|
62
198
|
}
|
|
63
199
|
function applyScroll(box, scroll) {
|
|
64
200
|
if (scroll.x === 0 && scroll.y === 0)
|
|
@@ -70,17 +206,58 @@ function paintText(node, buffer, box, visuals, clip, parentStyle, parentBox, sty
|
|
|
70
206
|
return;
|
|
71
207
|
paintTextContent(node, buffer, box, visuals, styles, layout, clip);
|
|
72
208
|
}
|
|
73
|
-
function fillBackground(buffer, box, visuals, clip) {
|
|
209
|
+
function fillBackground(buffer, box, visuals, clip, style) {
|
|
74
210
|
if (visuals.bg === 'default')
|
|
75
211
|
return;
|
|
76
|
-
|
|
77
|
-
|
|
212
|
+
// For inner-facing block-character borders, the border cells' bg should
|
|
213
|
+
// stop AT the stroke, not extend past it. Skip the entire border-cell ring
|
|
214
|
+
// so the stroke is the visible outer edge of the colored area. The stroke
|
|
215
|
+
// glyph still paints its 1/8 or 1/2 cell mark on a transparent cell.
|
|
216
|
+
const skipBorderRing = style ? isInnerFacingBlockBorder(style) : false;
|
|
217
|
+
// For borders with blank corners (no corner glyph), the corner cells stay
|
|
218
|
+
// transparent so bg doesn't leak through the gap.
|
|
219
|
+
const skipCorners = style ? hasBlankCorners(style) : false;
|
|
220
|
+
const left = box.x;
|
|
221
|
+
const right = box.x + box.width - 1;
|
|
222
|
+
const top = box.y;
|
|
223
|
+
const bottom = box.y + box.height - 1;
|
|
224
|
+
const bgHasAlpha = visuals.bg.startsWith('#') && visuals.bg.length === 9;
|
|
225
|
+
for (let row = top; row <= bottom; row++) {
|
|
226
|
+
for (let col = left; col <= right; col++) {
|
|
78
227
|
if (clip && !inClip(col, row, clip))
|
|
79
228
|
continue;
|
|
80
|
-
|
|
229
|
+
if (skipBorderRing && isBorderCell(col, row, left, right, top, bottom))
|
|
230
|
+
continue;
|
|
231
|
+
if (skipCorners && isCorner(col, row, left, right, top, bottom))
|
|
232
|
+
continue;
|
|
233
|
+
const bg = bgHasAlpha
|
|
234
|
+
? blendColor(buffer.getCell(col, row)?.bg ?? 'default', visuals.bg)
|
|
235
|
+
: visuals.bg;
|
|
236
|
+
buffer.setCell(col, row, { bg });
|
|
81
237
|
}
|
|
82
238
|
}
|
|
83
239
|
}
|
|
240
|
+
function isInnerFacingBlockBorder(style) {
|
|
241
|
+
const bs = style.borderStyle;
|
|
242
|
+
return bs === 'eighth-cell-inner' || bs === 'half-cell-inner';
|
|
243
|
+
}
|
|
244
|
+
function hasBlankCorners(style) {
|
|
245
|
+
if (style.borderCorner !== 'none')
|
|
246
|
+
return false;
|
|
247
|
+
// Only eighth-cell-inner has blank corners in the default configuration:
|
|
248
|
+
// the stroke sits at the inner edge, so the corner cell has nothing to
|
|
249
|
+
// draw and the bg should not leak through.
|
|
250
|
+
// eighth-cell-outer extends top/bottom strokes through corners by default
|
|
251
|
+
// (see renderBlockBorder), so the bg should fill the corner cells too.
|
|
252
|
+
// half-cell-* use quadrant/L corner glyphs.
|
|
253
|
+
return style.borderStyle === 'eighth-cell-inner';
|
|
254
|
+
}
|
|
255
|
+
function isBorderCell(col, row, left, right, top, bottom) {
|
|
256
|
+
return col === left || col === right || row === top || row === bottom;
|
|
257
|
+
}
|
|
258
|
+
function isCorner(col, row, left, right, top, bottom) {
|
|
259
|
+
return (col === left || col === right) && (row === top || row === bottom);
|
|
260
|
+
}
|
|
84
261
|
function paintListMarker(node, buffer, box, visuals, clip) {
|
|
85
262
|
const parent = node.parent;
|
|
86
263
|
if (!parent)
|
|
@@ -94,13 +271,55 @@ function paintListMarker(node, buffer, box, visuals, clip) {
|
|
|
94
271
|
else {
|
|
95
272
|
marker = '• ';
|
|
96
273
|
}
|
|
274
|
+
// Paint marker before the content (offset left by marker width)
|
|
275
|
+
const markerX = box.x - marker.length;
|
|
97
276
|
for (let i = 0; i < marker.length; i++) {
|
|
98
|
-
const cx =
|
|
277
|
+
const cx = markerX + i;
|
|
278
|
+
if (cx < 0)
|
|
279
|
+
continue;
|
|
99
280
|
if (clip && !inClip(cx, box.y, clip))
|
|
100
281
|
continue;
|
|
101
282
|
buffer.setCell(cx, box.y, { char: marker[i], fg: visuals.fg, dim: visuals.dim });
|
|
102
283
|
}
|
|
103
284
|
}
|
|
285
|
+
/** The selected option's label with a cycle indicator: "label ▾". */
|
|
286
|
+
function paintSelect(node, buffer, box, visuals, clip) {
|
|
287
|
+
const option = selectOptions(node)[selectedIndex(node)];
|
|
288
|
+
const text = `${option ? option.textContent.trim() : ''} ▾`;
|
|
289
|
+
for (let i = 0; i < Math.min(text.length, box.width); i++) {
|
|
290
|
+
const col = box.x + i;
|
|
291
|
+
if (clip && !inClip(col, box.y, clip))
|
|
292
|
+
continue;
|
|
293
|
+
buffer.setCell(col, box.y, {
|
|
294
|
+
char: text[i], fg: visuals.fg, bg: visuals.bg,
|
|
295
|
+
bold: visuals.bold, dim: visuals.dim,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/** Disclosure triangle in the summary's marker padding: ▶ closed, ▼ open. */
|
|
300
|
+
function paintSummaryMarker(node, buffer, box, visuals, clip) {
|
|
301
|
+
const details = node.parent;
|
|
302
|
+
const open = details?.tag === 'details' && hasBooleanAttribute(details, 'open');
|
|
303
|
+
if (clip && !inClip(box.x, box.y, clip))
|
|
304
|
+
return;
|
|
305
|
+
buffer.setCell(box.x, box.y, { char: open ? '▼' : '▶', fg: visuals.fg });
|
|
306
|
+
}
|
|
307
|
+
/** Checkbox → [x]/[ ], radio → (•)/( ), coloured by the element's visuals. */
|
|
308
|
+
function paintCheckable(node, buffer, box, visuals, clip) {
|
|
309
|
+
const checked = hasBooleanAttribute(node, 'checked');
|
|
310
|
+
const glyphs = node.attributes.get('type') === 'radio'
|
|
311
|
+
? (checked ? '(•)' : '( )')
|
|
312
|
+
: (checked ? '[x]' : '[ ]');
|
|
313
|
+
for (let i = 0; i < glyphs.length; i++) {
|
|
314
|
+
const col = box.x + i;
|
|
315
|
+
if (clip && !inClip(col, box.y, clip))
|
|
316
|
+
continue;
|
|
317
|
+
buffer.setCell(col, box.y, {
|
|
318
|
+
char: glyphs[i], fg: visuals.fg, bg: visuals.bg,
|
|
319
|
+
bold: visuals.bold, dim: visuals.dim,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
104
323
|
function paintInput(node, buffer, box, visuals, clip) {
|
|
105
324
|
const value = node.attributes.get('value') ?? '';
|
|
106
325
|
const isFocused = node.attributes.has('data-focused');
|
|
@@ -160,19 +379,18 @@ function paintInput(node, buffer, box, visuals, clip) {
|
|
|
160
379
|
buffer.setCell(cx, contentY, { char: '…', fg: visuals.fg, dim: true });
|
|
161
380
|
}
|
|
162
381
|
}
|
|
163
|
-
//
|
|
382
|
+
// Publish the text-cursor screen position so the post-paint emitter
|
|
383
|
+
// can drive the real terminal cursor. No cell is painted — the real
|
|
384
|
+
// cursor brings its own blink and shape.
|
|
164
385
|
if (isFocused) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
386
|
+
// Cursor cell offset = cell width of the text before it (wide
|
|
387
|
+
// glyphs take two columns), not its code-unit index.
|
|
388
|
+
const cursorCells = stringWidth(value.substring(0, cursor));
|
|
389
|
+
const cursorScreenX = contentX + (cursorCells - scrollOffset);
|
|
390
|
+
const inViewport = cursorScreenX >= contentX
|
|
391
|
+
&& cursorScreenX <= contentX + contentW
|
|
392
|
+
&& (!clip || inClip(cursorScreenX, contentY, clip));
|
|
393
|
+
node.cache.cursorScreen = { x: cursorScreenX, y: contentY, inViewport, generation: paintGeneration() };
|
|
176
394
|
}
|
|
177
395
|
}
|
|
178
396
|
function resolvePadVal(v) {
|
|
@@ -180,6 +398,42 @@ function resolvePadVal(v) {
|
|
|
180
398
|
return v;
|
|
181
399
|
return 0;
|
|
182
400
|
}
|
|
401
|
+
/** Left-partial block glyphs by eighths (index 1–7). */
|
|
402
|
+
const PARTIAL_BLOCKS = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉'];
|
|
403
|
+
/** <progress>/<meter>: filled blocks for value, light shade for the track. */
|
|
404
|
+
function paintValueBar(node, buffer, box, visuals, clip) {
|
|
405
|
+
const ratio = valueBarRatio(node);
|
|
406
|
+
const fillCells = ratio * box.width;
|
|
407
|
+
let fullCells = Math.floor(fillCells);
|
|
408
|
+
let eighths = Math.round((fillCells - fullCells) * 8);
|
|
409
|
+
if (eighths === 8) {
|
|
410
|
+
fullCells++;
|
|
411
|
+
eighths = 0;
|
|
412
|
+
}
|
|
413
|
+
for (let r = 0; r < box.height; r++) {
|
|
414
|
+
for (let c = 0; c < box.width; c++) {
|
|
415
|
+
const col = box.x + c;
|
|
416
|
+
const row = box.y + r;
|
|
417
|
+
if (clip && !inClip(col, row, clip))
|
|
418
|
+
continue;
|
|
419
|
+
const char = c < fullCells ? '█'
|
|
420
|
+
: (c === fullCells && eighths > 0) ? PARTIAL_BLOCKS[eighths]
|
|
421
|
+
: '░';
|
|
422
|
+
buffer.setCell(col, row, { char, fg: visuals.fg, bg: visuals.bg });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/** Fraction filled, from value/max (progress) or value within min/max (meter). */
|
|
427
|
+
function valueBarRatio(node) {
|
|
428
|
+
const value = parseFloat(node.attributes.get('value') ?? '');
|
|
429
|
+
if (isNaN(value))
|
|
430
|
+
return 0; // indeterminate
|
|
431
|
+
const min = node.tag === 'meter' ? (parseFloat(node.attributes.get('min') ?? '') || 0) : 0;
|
|
432
|
+
const max = parseFloat(node.attributes.get('max') ?? '') || 1;
|
|
433
|
+
if (max <= min)
|
|
434
|
+
return 0;
|
|
435
|
+
return Math.max(0, Math.min(1, (value - min) / (max - min)));
|
|
436
|
+
}
|
|
183
437
|
function paintHorizontalRule(buffer, box, visuals, clip) {
|
|
184
438
|
for (let col = box.x; col < box.x + box.width; col++) {
|
|
185
439
|
if (clip && !inClip(col, box.y, clip))
|
|
@@ -187,6 +441,31 @@ function paintHorizontalRule(buffer, box, visuals, clip) {
|
|
|
187
441
|
buffer.setCell(col, box.y, { char: '─', fg: visuals.fg, dim: true });
|
|
188
442
|
}
|
|
189
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Paint an svt-region. Fires `resize` if the allocated cell dimensions
|
|
446
|
+
* have changed since the last paint, then calls the consumer-registered
|
|
447
|
+
* cell source for each cell of the region's box (local coordinates) and
|
|
448
|
+
* writes the returned cell into the buffer at the absolute position.
|
|
449
|
+
*/
|
|
450
|
+
function paintRegion(node, buffer, box, clip) {
|
|
451
|
+
node.notifyAllocatedSize(box.width, box.height, (cols, rows) => {
|
|
452
|
+
dispatchEvent(node, 'resize', { cols, rows });
|
|
453
|
+
});
|
|
454
|
+
node.lastBoxX = box.x;
|
|
455
|
+
node.lastBoxY = box.y;
|
|
456
|
+
const cellSource = node.getCellSource();
|
|
457
|
+
if (!cellSource)
|
|
458
|
+
return;
|
|
459
|
+
for (let r = 0; r < box.height; r++) {
|
|
460
|
+
for (let c = 0; c < box.width; c++) {
|
|
461
|
+
const x = box.x + c;
|
|
462
|
+
const y = box.y + r;
|
|
463
|
+
if (clip && !inClip(x, y, clip))
|
|
464
|
+
continue;
|
|
465
|
+
buffer.setCell(x, y, cellSource(c, r));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
190
469
|
function inClip(col, row, clip) {
|
|
191
470
|
return col >= clip.x && col < clip.x + clip.width
|
|
192
471
|
&& row >= clip.y && row < clip.y + clip.height;
|
|
@@ -207,9 +486,10 @@ function resolveVisuals(node, styles, inherited) {
|
|
|
207
486
|
if (!own)
|
|
208
487
|
return inherited;
|
|
209
488
|
const hyperlink = node.tag === 'a' ? node.attributes.get('href') : inherited.hyperlink;
|
|
489
|
+
const opacity = own.opacity;
|
|
210
490
|
return {
|
|
211
|
-
fg: own.fg !== 'default' ? own.fg : inherited.fg,
|
|
212
|
-
bg: own.bg !== 'default' ? own.bg : inherited.bg,
|
|
491
|
+
fg: applyOpacity(own.fg !== 'default' ? own.fg : inherited.fg, opacity),
|
|
492
|
+
bg: applyOpacity(own.bg !== 'default' ? own.bg : inherited.bg, opacity),
|
|
213
493
|
bold: own.bold || inherited.bold,
|
|
214
494
|
italic: own.italic || inherited.italic,
|
|
215
495
|
underline: own.underline || inherited.underline,
|
|
@@ -218,3 +498,21 @@ function resolveVisuals(node, styles, inherited) {
|
|
|
218
498
|
hyperlink,
|
|
219
499
|
};
|
|
220
500
|
}
|
|
501
|
+
/** Fold a numeric opacity into the colour's alpha channel. */
|
|
502
|
+
function applyOpacity(color, opacity) {
|
|
503
|
+
if (opacity >= 1 || color === 'default')
|
|
504
|
+
return color;
|
|
505
|
+
const hex = color.startsWith('#') ? color : nominalHex(color);
|
|
506
|
+
if (!hex)
|
|
507
|
+
return color;
|
|
508
|
+
const existing = hex.length === 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 1;
|
|
509
|
+
const alpha = Math.round(existing * opacity * 255);
|
|
510
|
+
return hex.slice(0, 7) + alpha.toString(16).padStart(2, '0');
|
|
511
|
+
}
|
|
512
|
+
const NOMINAL_HEX = {
|
|
513
|
+
black: '#000000', red: '#cd0000', green: '#00cd00', yellow: '#cdcd00',
|
|
514
|
+
blue: '#0000ee', magenta: '#cd00cd', cyan: '#00cdcd', white: '#e5e5e5',
|
|
515
|
+
};
|
|
516
|
+
function nominalHex(name) {
|
|
517
|
+
return NOMINAL_HEX[name] ?? null;
|
|
518
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal PNG decoder for <img>: 8-bit RGB/RGBA/greyscale/palette, no
|
|
3
|
+
* interlace, inflate via node:zlib — no dependencies. Half-block
|
|
4
|
+
* rendering needs pixels, not fidelity; anything fancier should be
|
|
5
|
+
* converted before shipping to a terminal anyway.
|
|
6
|
+
*/
|
|
7
|
+
export interface DecodedImage {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
/** Row-major RGBA, 4 bytes per pixel. */
|
|
11
|
+
rgba: Uint8Array;
|
|
12
|
+
}
|
|
13
|
+
export declare function decodePng(data: Uint8Array): DecodedImage;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal PNG decoder for <img>: 8-bit RGB/RGBA/greyscale/palette, no
|
|
3
|
+
* interlace, inflate via node:zlib — no dependencies. Half-block
|
|
4
|
+
* rendering needs pixels, not fidelity; anything fancier should be
|
|
5
|
+
* converted before shipping to a terminal anyway.
|
|
6
|
+
*/
|
|
7
|
+
import { inflateSync } from 'node:zlib';
|
|
8
|
+
const SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
9
|
+
/** Bytes per pixel for the colour types we support. */
|
|
10
|
+
const CHANNELS = { 0: 1, 2: 3, 3: 1, 6: 4 };
|
|
11
|
+
export function decodePng(data) {
|
|
12
|
+
for (let i = 0; i < SIGNATURE.length; i++) {
|
|
13
|
+
if (data[i] !== SIGNATURE[i])
|
|
14
|
+
throw new Error('Not a PNG file');
|
|
15
|
+
}
|
|
16
|
+
let width = 0;
|
|
17
|
+
let height = 0;
|
|
18
|
+
let colourType = -1;
|
|
19
|
+
let palette = null;
|
|
20
|
+
const idat = [];
|
|
21
|
+
let offset = 8;
|
|
22
|
+
while (offset + 8 <= data.length) {
|
|
23
|
+
const length = readU32(data, offset);
|
|
24
|
+
const type = String.fromCharCode(...data.slice(offset + 4, offset + 8));
|
|
25
|
+
const body = data.slice(offset + 8, offset + 8 + length);
|
|
26
|
+
if (type === 'IHDR') {
|
|
27
|
+
width = readU32(body, 0);
|
|
28
|
+
height = readU32(body, 4);
|
|
29
|
+
const bitDepth = body[8];
|
|
30
|
+
colourType = body[9];
|
|
31
|
+
if (bitDepth !== 8)
|
|
32
|
+
throw new Error(`PNG bit depth ${bitDepth} not supported (8 only)`);
|
|
33
|
+
if (!(colourType in CHANNELS))
|
|
34
|
+
throw new Error(`PNG colour type ${colourType} not supported`);
|
|
35
|
+
if (body[12] !== 0)
|
|
36
|
+
throw new Error('Interlaced PNG not supported');
|
|
37
|
+
}
|
|
38
|
+
else if (type === 'PLTE') {
|
|
39
|
+
palette = body;
|
|
40
|
+
}
|
|
41
|
+
else if (type === 'IDAT') {
|
|
42
|
+
idat.push(body);
|
|
43
|
+
}
|
|
44
|
+
else if (type === 'IEND') {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
offset += 12 + length;
|
|
48
|
+
}
|
|
49
|
+
if (width === 0 || height === 0 || idat.length === 0)
|
|
50
|
+
throw new Error('Truncated PNG');
|
|
51
|
+
const channels = CHANNELS[colourType];
|
|
52
|
+
const raw = inflateSync(concat(idat));
|
|
53
|
+
const stride = width * channels;
|
|
54
|
+
const unfiltered = unfilter(raw, width, height, channels);
|
|
55
|
+
const rgba = new Uint8Array(width * height * 4);
|
|
56
|
+
for (let y = 0; y < height; y++) {
|
|
57
|
+
for (let x = 0; x < width; x++) {
|
|
58
|
+
const src = y * stride + x * channels;
|
|
59
|
+
const dst = (y * width + x) * 4;
|
|
60
|
+
switch (colourType) {
|
|
61
|
+
case 6:
|
|
62
|
+
rgba.set(unfiltered.slice(src, src + 4), dst);
|
|
63
|
+
break;
|
|
64
|
+
case 2:
|
|
65
|
+
rgba.set(unfiltered.slice(src, src + 3), dst);
|
|
66
|
+
rgba[dst + 3] = 255;
|
|
67
|
+
break;
|
|
68
|
+
case 0: {
|
|
69
|
+
const grey = unfiltered[src];
|
|
70
|
+
rgba[dst] = grey;
|
|
71
|
+
rgba[dst + 1] = grey;
|
|
72
|
+
rgba[dst + 2] = grey;
|
|
73
|
+
rgba[dst + 3] = 255;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 3: {
|
|
77
|
+
const index = unfiltered[src] * 3;
|
|
78
|
+
rgba[dst] = palette?.[index] ?? 0;
|
|
79
|
+
rgba[dst + 1] = palette?.[index + 1] ?? 0;
|
|
80
|
+
rgba[dst + 2] = palette?.[index + 2] ?? 0;
|
|
81
|
+
rgba[dst + 3] = 255;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { width, height, rgba };
|
|
88
|
+
}
|
|
89
|
+
/** Undo per-row PNG filters (types 0–4). */
|
|
90
|
+
function unfilter(raw, width, height, channels) {
|
|
91
|
+
const stride = width * channels;
|
|
92
|
+
const out = new Uint8Array(stride * height);
|
|
93
|
+
for (let y = 0; y < height; y++) {
|
|
94
|
+
const filter = raw[y * (stride + 1)];
|
|
95
|
+
const rowIn = raw.slice(y * (stride + 1) + 1, (y + 1) * (stride + 1));
|
|
96
|
+
const rowOut = out.subarray(y * stride, (y + 1) * stride);
|
|
97
|
+
const prev = y > 0 ? out.subarray((y - 1) * stride, y * stride) : null;
|
|
98
|
+
for (let i = 0; i < stride; i++) {
|
|
99
|
+
const left = i >= channels ? rowOut[i - channels] : 0;
|
|
100
|
+
const up = prev ? prev[i] : 0;
|
|
101
|
+
const upLeft = prev && i >= channels ? prev[i - channels] : 0;
|
|
102
|
+
let value = rowIn[i];
|
|
103
|
+
switch (filter) {
|
|
104
|
+
case 1:
|
|
105
|
+
value += left;
|
|
106
|
+
break;
|
|
107
|
+
case 2:
|
|
108
|
+
value += up;
|
|
109
|
+
break;
|
|
110
|
+
case 3:
|
|
111
|
+
value += Math.floor((left + up) / 2);
|
|
112
|
+
break;
|
|
113
|
+
case 4:
|
|
114
|
+
value += paeth(left, up, upLeft);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
rowOut[i] = value & 0xff;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
function paeth(a, b, c) {
|
|
123
|
+
const p = a + b - c;
|
|
124
|
+
const pa = Math.abs(p - a);
|
|
125
|
+
const pb = Math.abs(p - b);
|
|
126
|
+
const pc = Math.abs(p - c);
|
|
127
|
+
if (pa <= pb && pa <= pc)
|
|
128
|
+
return a;
|
|
129
|
+
if (pb <= pc)
|
|
130
|
+
return b;
|
|
131
|
+
return c;
|
|
132
|
+
}
|
|
133
|
+
function readU32(data, offset) {
|
|
134
|
+
return (data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]) >>> 0;
|
|
135
|
+
}
|
|
136
|
+
function concat(parts) {
|
|
137
|
+
const total = parts.reduce((sum, p) => sum + p.length, 0);
|
|
138
|
+
const out = new Uint8Array(total);
|
|
139
|
+
let offset = 0;
|
|
140
|
+
for (const part of parts) {
|
|
141
|
+
out.set(part, offset);
|
|
142
|
+
offset += part.length;
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import { CellBuffer } from './buffer.js';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Render a vertical scrollbar overlay on the rightmost column.
|
|
4
|
+
*/
|
|
5
|
+
export declare function renderScrollbar(buffer: CellBuffer, viewportX: number, viewportY: number, viewportWidth: number, viewportHeight: number, contentHeight: number, scrollTop: number, opacity: number): void;
|
|
6
|
+
/**
|
|
7
|
+
* Render a horizontal scrollbar overlay on the bottom row.
|
|
8
|
+
*/
|
|
9
|
+
export declare function renderHScrollbar(buffer: CellBuffer, viewportX: number, viewportY: number, viewportWidth: number, viewportHeight: number, contentWidth: number, scrollLeft: number, opacity: number): void;
|