@trendr/core 0.1.0 → 0.2.1
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/README.md +420 -56
- package/index.js +9 -1
- package/package.json +23 -3
- package/src/animation.js +204 -0
- package/src/ansi.js +3 -0
- package/src/buffer.js +49 -15
- package/src/button.js +10 -1
- package/src/checkbox.js +12 -3
- package/src/diff.js +115 -12
- package/src/hooks.js +142 -2
- package/src/input.js +59 -7
- package/src/layout.js +90 -18
- package/src/list.js +142 -17
- package/src/modal.js +2 -2
- package/src/pick-list.js +204 -0
- package/src/progress.js +57 -9
- package/src/radio.js +16 -2
- package/src/renderer.js +364 -68
- package/src/scroll-box.js +105 -0
- package/src/scrollable-text.js +28 -6
- package/src/select.js +188 -68
- package/src/shimmer.js +95 -0
- package/src/signal.js +12 -0
- package/src/spinner.js +13 -4
- package/src/split-pane.js +66 -0
- package/src/table.js +68 -23
- package/src/task.js +43 -0
- package/src/text-area.js +8 -5
- package/src/text-input.js +9 -6
- package/src/toast.js +4 -6
- package/src/wrap.js +110 -11
package/src/renderer.js
CHANGED
|
@@ -1,29 +1,45 @@
|
|
|
1
|
-
import { createBuffer, clearBuffer, fillRect, writeText, dimBuffer } from './buffer.js'
|
|
1
|
+
import { createBuffer, clearBuffer, fillRect, writeText, dimBuffer, blitRect } from './buffer.js'
|
|
2
2
|
import { diff } from './diff.js'
|
|
3
|
-
import { computeLayout } from './layout.js'
|
|
3
|
+
import { computeLayout, resolveBorderEdges } from './layout.js'
|
|
4
4
|
import { Fragment } from './element.js'
|
|
5
5
|
import { createScheduler } from './scheduler.js'
|
|
6
6
|
import { createInputHandler } from './input.js'
|
|
7
|
-
import { setSchedulerHook, setHookRegistrar, createScope, disposeScope, onCleanup } from './signal.js'
|
|
7
|
+
import { setSchedulerHook, setHookRegistrar, createScope, disposeScope, onCleanup, startRenderTracking, stopRenderTracking } from './signal.js'
|
|
8
8
|
import { wordWrap, measureText, sliceVisible } from './wrap.js'
|
|
9
9
|
import * as ansi from './ansi.js'
|
|
10
10
|
|
|
11
11
|
let activeContext = null
|
|
12
12
|
let overlays = []
|
|
13
|
+
let lastFrameStats = { changed: 0, total: 0, bytes: 0, fps: 0 }
|
|
14
|
+
let frameTimeWindow = []
|
|
15
|
+
let lastFrameTimestamp = 0
|
|
13
16
|
|
|
14
17
|
export function getContext() {
|
|
15
18
|
return activeContext
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
const DEFAULT_CURSOR = { blink: false, rate: 530, style: 'block' }
|
|
18
22
|
const DEFAULT_THEME = { accent: 'cyan' }
|
|
19
23
|
|
|
20
24
|
export function getTheme() {
|
|
21
25
|
return activeContext?.theme ?? DEFAULT_THEME
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
export function getCursor(propCursor) {
|
|
29
|
+
const themeCursor = activeContext?.theme?.cursor
|
|
30
|
+
if (!propCursor && !themeCursor) return DEFAULT_CURSOR
|
|
31
|
+
if (propCursor === true) return { ...DEFAULT_CURSOR, blink: true, ...themeCursor }
|
|
32
|
+
return { ...DEFAULT_CURSOR, ...themeCursor, ...propCursor }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getFrameStats() {
|
|
36
|
+
return lastFrameStats
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
export function getInstanceLayout() {
|
|
25
40
|
if (!currentHookOwner) return { x: 0, y: 0, width: 0, height: 0 }
|
|
26
|
-
|
|
41
|
+
if (!currentHookOwner.layout) currentHookOwner.layout = { x: 0, y: 0, width: 0, height: 0 }
|
|
42
|
+
return currentHookOwner.layout
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
export function registerOverlay(element, { backdrop, fullscreen } = {}) {
|
|
@@ -32,10 +48,25 @@ export function registerOverlay(element, { backdrop, fullscreen } = {}) {
|
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
const BORDER_CHARS = {
|
|
35
|
-
single: { tl: '\u250c', tr: '\u2510', bl: '\u2514', br: '\u2518', h: '\u2500', v: '\u2502' },
|
|
36
|
-
double: { tl: '\u2554', tr: '\u2557', bl: '\u255a', br: '\u255d', h: '\u2550', v: '\u2551' },
|
|
37
|
-
round: { tl: '\u256d', tr: '\u256e', bl: '\u2570', br: '\u256f', h: '\u2500', v: '\u2502' },
|
|
38
|
-
bold: { tl: '\u250f', tr: '\u2513', bl: '\u2517', br: '\u251b', h: '\u2501', v: '\u2503' },
|
|
51
|
+
single: { tl: '\u250c', tr: '\u2510', bl: '\u2514', br: '\u2518', h: '\u2500', v: '\u2502', tDown: '\u252c', tUp: '\u2534', tRight: '\u251c', tLeft: '\u2524' },
|
|
52
|
+
double: { tl: '\u2554', tr: '\u2557', bl: '\u255a', br: '\u255d', h: '\u2550', v: '\u2551', tDown: '\u2566', tUp: '\u2569', tRight: '\u2560', tLeft: '\u2563' },
|
|
53
|
+
round: { tl: '\u256d', tr: '\u256e', bl: '\u2570', br: '\u256f', h: '\u2500', v: '\u2502', tDown: '\u252c', tUp: '\u2534', tRight: '\u251c', tLeft: '\u2524' },
|
|
54
|
+
bold: { tl: '\u250f', tr: '\u2513', bl: '\u2517', br: '\u251b', h: '\u2501', v: '\u2503', tDown: '\u2533', tUp: '\u253b', tRight: '\u2523', tLeft: '\u252b' },
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const TEXTURE_PRESETS = {
|
|
58
|
+
'shade-light': '░',
|
|
59
|
+
'shade-medium': '▒',
|
|
60
|
+
'shade-heavy': '▓',
|
|
61
|
+
'dots': '·',
|
|
62
|
+
'cross': '╳',
|
|
63
|
+
'grid': '┼',
|
|
64
|
+
'dash': '╌',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveTexture(texture) {
|
|
68
|
+
if (!texture) return null
|
|
69
|
+
return TEXTURE_PRESETS[texture] ?? texture
|
|
39
70
|
}
|
|
40
71
|
|
|
41
72
|
function resolveAttrs(style) {
|
|
@@ -49,7 +80,7 @@ function resolveAttrs(style) {
|
|
|
49
80
|
return attrs
|
|
50
81
|
}
|
|
51
82
|
|
|
52
|
-
function paintBorder(buf, rect, borderStyle, fg) {
|
|
83
|
+
function paintBorder(buf, rect, borderStyle, fg, edges) {
|
|
53
84
|
const chars = typeof borderStyle === 'string'
|
|
54
85
|
? (BORDER_CHARS[borderStyle] ?? BORDER_CHARS.single)
|
|
55
86
|
: BORDER_CHARS.single
|
|
@@ -58,20 +89,62 @@ function paintBorder(buf, rect, borderStyle, fg) {
|
|
|
58
89
|
if (width < 2 || height < 2) return
|
|
59
90
|
|
|
60
91
|
const cell = (ch) => ({ ch, fg: fg ?? null, bg: null, attrs: 0 })
|
|
92
|
+
const { top, right, bottom, left } = edges
|
|
93
|
+
|
|
94
|
+
if (top && left) buf.cells[y * buf.width + x] = cell(chars.tl)
|
|
95
|
+
else if (top) buf.cells[y * buf.width + x] = cell(chars.h)
|
|
96
|
+
else if (left) buf.cells[y * buf.width + x] = cell(chars.v)
|
|
61
97
|
|
|
62
|
-
buf.cells[y * buf.width + x] = cell(chars.
|
|
63
|
-
buf.cells[y * buf.width + x + width - 1] = cell(chars.
|
|
64
|
-
buf.cells[
|
|
65
|
-
buf.cells[(y + height - 1) * buf.width + x + width - 1] = cell(chars.br)
|
|
98
|
+
if (top && right) buf.cells[y * buf.width + x + width - 1] = cell(chars.tr)
|
|
99
|
+
else if (top) buf.cells[y * buf.width + x + width - 1] = cell(chars.h)
|
|
100
|
+
else if (right) buf.cells[y * buf.width + x + width - 1] = cell(chars.v)
|
|
66
101
|
|
|
67
|
-
|
|
102
|
+
if (bottom && left) buf.cells[(y + height - 1) * buf.width + x] = cell(chars.bl)
|
|
103
|
+
else if (bottom) buf.cells[(y + height - 1) * buf.width + x] = cell(chars.h)
|
|
104
|
+
else if (left) buf.cells[(y + height - 1) * buf.width + x] = cell(chars.v)
|
|
105
|
+
|
|
106
|
+
if (bottom && right) buf.cells[(y + height - 1) * buf.width + x + width - 1] = cell(chars.br)
|
|
107
|
+
else if (bottom) buf.cells[(y + height - 1) * buf.width + x + width - 1] = cell(chars.h)
|
|
108
|
+
else if (right) buf.cells[(y + height - 1) * buf.width + x + width - 1] = cell(chars.v)
|
|
109
|
+
|
|
110
|
+
if (top) for (let col = x + 1; col < x + width - 1; col++)
|
|
68
111
|
buf.cells[y * buf.width + col] = cell(chars.h)
|
|
112
|
+
|
|
113
|
+
if (bottom) for (let col = x + 1; col < x + width - 1; col++)
|
|
69
114
|
buf.cells[(y + height - 1) * buf.width + col] = cell(chars.h)
|
|
70
|
-
}
|
|
71
115
|
|
|
72
|
-
for (let row = y + 1; row < y + height - 1; row++)
|
|
116
|
+
if (left) for (let row = y + 1; row < y + height - 1; row++)
|
|
73
117
|
buf.cells[row * buf.width + x] = cell(chars.v)
|
|
118
|
+
|
|
119
|
+
if (right) for (let row = y + 1; row < y + height - 1; row++)
|
|
74
120
|
buf.cells[row * buf.width + x + width - 1] = cell(chars.v)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function paintJunctions(buf, rect, borderStyle, fg, children, edges) {
|
|
124
|
+
if (!children) return
|
|
125
|
+
const chars = typeof borderStyle === 'string'
|
|
126
|
+
? (BORDER_CHARS[borderStyle] ?? BORDER_CHARS.single)
|
|
127
|
+
: BORDER_CHARS.single
|
|
128
|
+
const cell = (ch) => ({ ch, fg: fg ?? null, bg: null, attrs: 0 })
|
|
129
|
+
|
|
130
|
+
for (const child of children) {
|
|
131
|
+
const leaf = child._resolved ? child._resolved : child
|
|
132
|
+
const divider = leaf?.props?.style?._divider
|
|
133
|
+
if (!divider) continue
|
|
134
|
+
const cl = leaf._layout
|
|
135
|
+
if (!cl) continue
|
|
136
|
+
|
|
137
|
+
if (divider === 'vertical') {
|
|
138
|
+
if (cl.x >= rect.x && cl.x < rect.x + rect.width) {
|
|
139
|
+
if (edges.top) buf.cells[rect.y * buf.width + cl.x] = cell(chars.tDown)
|
|
140
|
+
if (edges.bottom) buf.cells[(rect.y + rect.height - 1) * buf.width + cl.x] = cell(chars.tUp)
|
|
141
|
+
}
|
|
142
|
+
} else if (divider === 'horizontal') {
|
|
143
|
+
if (cl.y >= rect.y && cl.y < rect.y + rect.height) {
|
|
144
|
+
if (edges.left) buf.cells[cl.y * buf.width + rect.x] = cell(chars.tRight)
|
|
145
|
+
if (edges.right) buf.cells[cl.y * buf.width + rect.x + rect.width - 1] = cell(chars.tLeft)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
75
148
|
}
|
|
76
149
|
}
|
|
77
150
|
|
|
@@ -79,14 +152,25 @@ function findContentRect(node) {
|
|
|
79
152
|
if (!node?._layout) return null
|
|
80
153
|
const style = node.props?.style ?? {}
|
|
81
154
|
if (style.border) return node._layout
|
|
155
|
+
if (node.type === 'text' || style.bg || style.inverse) return node._layout
|
|
156
|
+
|
|
157
|
+
let bounds = null
|
|
158
|
+
const merge = (rect) => {
|
|
159
|
+
if (!rect) return
|
|
160
|
+
if (!bounds) { bounds = { ...rect }; return }
|
|
161
|
+
const r = Math.max(bounds.x + bounds.width, rect.x + rect.width)
|
|
162
|
+
const b = Math.max(bounds.y + bounds.height, rect.y + rect.height)
|
|
163
|
+
bounds.x = Math.min(bounds.x, rect.x)
|
|
164
|
+
bounds.y = Math.min(bounds.y, rect.y)
|
|
165
|
+
bounds.width = r - bounds.x
|
|
166
|
+
bounds.height = b - bounds.y
|
|
167
|
+
}
|
|
168
|
+
|
|
82
169
|
if (node._resolvedChildren) {
|
|
83
|
-
for (const child of node._resolvedChildren)
|
|
84
|
-
const found = findContentRect(child)
|
|
85
|
-
if (found) return found
|
|
86
|
-
}
|
|
170
|
+
for (const child of node._resolvedChildren) merge(findContentRect(child))
|
|
87
171
|
}
|
|
88
|
-
if (node._resolved)
|
|
89
|
-
return
|
|
172
|
+
if (node._resolved) merge(findContentRect(node._resolved))
|
|
173
|
+
return bounds
|
|
90
174
|
}
|
|
91
175
|
|
|
92
176
|
function clearOverlayRect(overlayTree, buf) {
|
|
@@ -95,6 +179,40 @@ function clearOverlayRect(overlayTree, buf) {
|
|
|
95
179
|
fillRect(buf, rect.x, rect.y, rect.width, rect.height, ' ', null, null, 0)
|
|
96
180
|
}
|
|
97
181
|
|
|
182
|
+
function findScrollContentHeight(node) {
|
|
183
|
+
if (!node) return null
|
|
184
|
+
if (node._contentHeight != null) return node._contentHeight
|
|
185
|
+
if (node._resolved) return findScrollContentHeight(node._resolved)
|
|
186
|
+
if (node._resolvedChildren) {
|
|
187
|
+
for (const child of node._resolvedChildren) {
|
|
188
|
+
const h = findScrollContentHeight(child)
|
|
189
|
+
if (h != null) return h
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function updateOverlayLayouts(node) {
|
|
196
|
+
if (!node) return
|
|
197
|
+
if (node._instance) {
|
|
198
|
+
const rect = node._availableRect ?? node._layout
|
|
199
|
+
if (rect) {
|
|
200
|
+
const ch = findScrollContentHeight(node)
|
|
201
|
+
if (!node._instance.layout) node._instance.layout = { x: 0, y: 0, width: 0, height: 0 }
|
|
202
|
+
const target = node._instance.layout
|
|
203
|
+
target.x = rect.x
|
|
204
|
+
target.y = rect.y
|
|
205
|
+
target.width = rect.width
|
|
206
|
+
target.height = rect.height
|
|
207
|
+
target.contentHeight = ch
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (node._resolved) updateOverlayLayouts(node._resolved)
|
|
211
|
+
if (node._resolvedChildren) {
|
|
212
|
+
for (const child of node._resolvedChildren) updateOverlayLayouts(child)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
98
216
|
function clipRect(a, b) {
|
|
99
217
|
const x = Math.max(a.x, b.x)
|
|
100
218
|
const y = Math.max(a.y, b.y)
|
|
@@ -103,23 +221,61 @@ function clipRect(a, b) {
|
|
|
103
221
|
return { x, y, width: Math.max(0, r - x), height: Math.max(0, bot - y) }
|
|
104
222
|
}
|
|
105
223
|
|
|
106
|
-
function
|
|
224
|
+
function propagateDirty(node) {
|
|
225
|
+
if (!node) return false
|
|
226
|
+
if (node._resolved) {
|
|
227
|
+
const childDirty = propagateDirty(node._resolved)
|
|
228
|
+
const inst = node._instance
|
|
229
|
+
if (inst) {
|
|
230
|
+
inst._subtreeDirty = inst._dirty || childDirty
|
|
231
|
+
return inst._subtreeDirty
|
|
232
|
+
}
|
|
233
|
+
return childDirty
|
|
234
|
+
}
|
|
235
|
+
if (node._resolvedChildren) {
|
|
236
|
+
let anyDirty = false
|
|
237
|
+
for (const child of node._resolvedChildren) {
|
|
238
|
+
if (propagateDirty(child)) anyDirty = true
|
|
239
|
+
}
|
|
240
|
+
return anyDirty
|
|
241
|
+
}
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function layoutEqual(a, b) {
|
|
246
|
+
return a && b && a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function paintTree(node, buf, clip, offset, prevBuf) {
|
|
107
250
|
if (!node) return
|
|
108
251
|
|
|
109
252
|
if (node._resolved) {
|
|
110
|
-
|
|
253
|
+
const inst = node._instance
|
|
254
|
+
if (prevBuf && inst && !inst._subtreeDirty) {
|
|
255
|
+
const layout = node._resolved?._layout ?? node._layout
|
|
256
|
+
if (layout && layoutEqual(layout, inst._lastLayout)) {
|
|
257
|
+
blitRect(prevBuf, buf, layout.x, layout.y, layout.width, layout.height)
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (inst) inst._lastLayout = node._resolved?._layout ?? node._layout
|
|
262
|
+
paintTree(node._resolved, buf, clip, offset, prevBuf)
|
|
111
263
|
return
|
|
112
264
|
}
|
|
113
265
|
|
|
114
266
|
if (node.type === Fragment) {
|
|
115
267
|
if (node._resolvedChildren) {
|
|
116
|
-
for (const child of node._resolvedChildren) paintTree(child, buf, clip)
|
|
268
|
+
for (const child of node._resolvedChildren) paintTree(child, buf, clip, offset, prevBuf)
|
|
117
269
|
}
|
|
118
270
|
return
|
|
119
271
|
}
|
|
120
272
|
|
|
121
|
-
const
|
|
122
|
-
if (!
|
|
273
|
+
const rawLayout = node._layout
|
|
274
|
+
if (!rawLayout || rawLayout.width <= 0 || rawLayout.height <= 0) return
|
|
275
|
+
|
|
276
|
+
const layout = offset
|
|
277
|
+
? { x: rawLayout.x + offset.x, y: rawLayout.y + offset.y, width: rawLayout.width, height: rawLayout.height }
|
|
278
|
+
: rawLayout
|
|
123
279
|
|
|
124
280
|
const clipped = clip ? clipRect(layout, clip) : layout
|
|
125
281
|
if (clipped.width <= 0 || clipped.height <= 0) return
|
|
@@ -153,33 +309,66 @@ function paintTree(node, buf, clip) {
|
|
|
153
309
|
return
|
|
154
310
|
}
|
|
155
311
|
|
|
156
|
-
if (style.bg) {
|
|
157
|
-
|
|
312
|
+
if (style.bg || style.texture) {
|
|
313
|
+
const ch = resolveTexture(style.texture) ?? ' '
|
|
314
|
+
const fg = style.textureColor ?? null
|
|
315
|
+
fillRect(buf, clipped.x, clipped.y, clipped.width, clipped.height, ch, fg, style.bg, 0)
|
|
158
316
|
}
|
|
159
317
|
|
|
160
318
|
if (style.border) {
|
|
161
|
-
|
|
319
|
+
const edges = resolveBorderEdges(style)
|
|
320
|
+
paintBorder(buf, layout, style.border, style.borderColor, edges)
|
|
321
|
+
paintJunctions(buf, layout, style.border, style.borderColor, node._resolvedChildren, edges)
|
|
162
322
|
}
|
|
163
323
|
|
|
164
324
|
const childClip = clip ? clipRect(layout, clip) : layout
|
|
165
325
|
|
|
326
|
+
let childOffset = offset
|
|
327
|
+
if (style.overflow === 'scroll') {
|
|
328
|
+
const scrollY = style.scrollOffset ?? 0
|
|
329
|
+
childOffset = { x: offset?.x ?? 0, y: (offset?.y ?? 0) - scrollY }
|
|
330
|
+
}
|
|
331
|
+
|
|
166
332
|
if (node._resolvedChildren) {
|
|
167
333
|
for (const child of node._resolvedChildren) {
|
|
168
|
-
paintTree(child, buf, childClip)
|
|
334
|
+
paintTree(child, buf, childClip, childOffset, prevBuf)
|
|
169
335
|
}
|
|
170
336
|
}
|
|
171
337
|
}
|
|
172
338
|
|
|
173
|
-
function extractText(node) {
|
|
339
|
+
function extractText(node, parentCtx) {
|
|
174
340
|
if (node == null || node === true || node === false) return ''
|
|
175
341
|
if (typeof node === 'string') return node
|
|
176
342
|
if (typeof node === 'number') return String(node)
|
|
343
|
+
|
|
177
344
|
const children = node.props?.children
|
|
178
345
|
if (children == null || children === true || children === false) return ''
|
|
179
|
-
if (typeof children === 'string') return children
|
|
180
|
-
if (typeof children === 'number') return String(children)
|
|
181
|
-
|
|
182
|
-
|
|
346
|
+
if (typeof children === 'string' && !node.props?.style) return children
|
|
347
|
+
if (typeof children === 'number' && !node.props?.style) return String(children)
|
|
348
|
+
|
|
349
|
+
const style = node.props?.style
|
|
350
|
+
const ownAttrs = style ? resolveAttrs(style) : 0
|
|
351
|
+
const hasOwnStyle = style && (style.color != null || style.bg != null || ownAttrs)
|
|
352
|
+
|
|
353
|
+
const myCtx = hasOwnStyle ? {
|
|
354
|
+
fg: style.color ?? parentCtx?.fg ?? null,
|
|
355
|
+
bg: style.bg ?? parentCtx?.bg ?? null,
|
|
356
|
+
attrs: ownAttrs || parentCtx?.attrs || 0,
|
|
357
|
+
} : (parentCtx || null)
|
|
358
|
+
|
|
359
|
+
let inner
|
|
360
|
+
if (typeof children === 'string') inner = children
|
|
361
|
+
else if (typeof children === 'number') inner = String(children)
|
|
362
|
+
else if (Array.isArray(children)) inner = children.map(c => extractText(c, myCtx)).join('')
|
|
363
|
+
else inner = ''
|
|
364
|
+
|
|
365
|
+
if (parentCtx !== undefined && hasOwnStyle) {
|
|
366
|
+
const prefix = ansi.sgr(myCtx.fg, myCtx.bg, myCtx.attrs)
|
|
367
|
+
const suffix = parentCtx ? ansi.sgr(parentCtx.fg, parentCtx.bg, parentCtx.attrs) : ansi.sgrReset
|
|
368
|
+
return prefix + inner + suffix
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return inner
|
|
183
372
|
}
|
|
184
373
|
|
|
185
374
|
function flattenChildren(children) {
|
|
@@ -269,7 +458,36 @@ export function registerHook(setupFn) {
|
|
|
269
458
|
// instances are keyed by component function + occurrence index,
|
|
270
459
|
// so multiple instances of the same component each get their own state.
|
|
271
460
|
|
|
272
|
-
function
|
|
461
|
+
function shallowPropsEqual(a, b) {
|
|
462
|
+
if (a === b) return true
|
|
463
|
+
if (!a || !b) return false
|
|
464
|
+
const keysA = Object.keys(a)
|
|
465
|
+
const keysB = Object.keys(b)
|
|
466
|
+
if (keysA.length !== keysB.length) return false
|
|
467
|
+
for (const k of keysA) {
|
|
468
|
+
if (k === 'children') continue
|
|
469
|
+
if (a[k] !== b[k]) return false
|
|
470
|
+
}
|
|
471
|
+
return true
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function isInstanceClean(instance, newProps) {
|
|
475
|
+
if (!instance._trackedSignals) return false
|
|
476
|
+
if (!shallowPropsEqual(instance._lastProps, newProps)) return false
|
|
477
|
+
const sigs = instance._trackedSignals
|
|
478
|
+
const vals = instance._signalValues
|
|
479
|
+
for (let i = 0; i < sigs.length; i++) {
|
|
480
|
+
if (sigs[i]() !== vals[i]) return false
|
|
481
|
+
}
|
|
482
|
+
return true
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function snapshotSignals(instance, signals) {
|
|
486
|
+
instance._trackedSignals = signals
|
|
487
|
+
instance._signalValues = signals.map(g => g())
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function resolveForFrame(element, parent, instances, counters, visited, scope) {
|
|
273
491
|
if (element == null || typeof element === 'boolean') return null
|
|
274
492
|
|
|
275
493
|
if (typeof element === 'string' || typeof element === 'number') {
|
|
@@ -296,49 +514,63 @@ function resolveForFrame(element, parent, instances, counters, visited) {
|
|
|
296
514
|
|
|
297
515
|
if (typeof element.type === 'function') {
|
|
298
516
|
const fn = element.type
|
|
299
|
-
const
|
|
300
|
-
counters.
|
|
517
|
+
const counterKey = `${scope}/${fn.name}`
|
|
518
|
+
const count = counters.get(counterKey) ?? 0
|
|
519
|
+
counters.set(counterKey, count + 1)
|
|
301
520
|
|
|
302
|
-
const instanceKey = element.key != null ? `${fn.name}:key:${element.key}` : `${fn.name}:${count}`
|
|
521
|
+
const instanceKey = element.key != null ? `${scope}/${fn.name}:key:${element.key}` : `${scope}/${fn.name}:${count}`
|
|
303
522
|
if (visited) visited.add(instanceKey)
|
|
304
523
|
let instance = instances.get(instanceKey)
|
|
305
524
|
|
|
306
525
|
if (!instance) {
|
|
307
526
|
let result
|
|
308
|
-
instance = { scope: null, fn, hooks: [], node: null, layout: null }
|
|
527
|
+
instance = { scope: null, fn, hooks: [], node: null, layout: null, _dirty: true }
|
|
309
528
|
instances.set(instanceKey, instance)
|
|
310
529
|
instance.scope = createScope(() => {
|
|
311
530
|
startHookTracking(instance)
|
|
531
|
+
startRenderTracking()
|
|
312
532
|
result = fn(element.props ?? {})
|
|
533
|
+
const signals = stopRenderTracking()
|
|
313
534
|
endHookTracking()
|
|
535
|
+
snapshotSignals(instance, signals)
|
|
536
|
+
instance._lastProps = element.props
|
|
314
537
|
})
|
|
315
|
-
node._resolved = resolveForFrame(result, node, instances, counters, visited)
|
|
538
|
+
node._resolved = resolveForFrame(result, node, instances, counters, visited, instanceKey)
|
|
316
539
|
} else {
|
|
540
|
+
const clean = isInstanceClean(instance, element.props)
|
|
541
|
+
instance._dirty = !clean
|
|
542
|
+
|
|
317
543
|
startHookTracking(instance)
|
|
544
|
+
startRenderTracking()
|
|
318
545
|
const result = fn(element.props ?? {})
|
|
546
|
+
const signals = stopRenderTracking()
|
|
319
547
|
endHookTracking()
|
|
320
|
-
|
|
548
|
+
snapshotSignals(instance, signals)
|
|
549
|
+
instance._lastProps = element.props
|
|
550
|
+
|
|
551
|
+
node._resolved = resolveForFrame(result, node, instances, counters, visited, instanceKey)
|
|
321
552
|
}
|
|
322
553
|
|
|
554
|
+
node._instance = instance
|
|
323
555
|
instance.node = node
|
|
324
556
|
return node
|
|
325
557
|
}
|
|
326
558
|
|
|
327
559
|
if (element.type === Fragment) {
|
|
328
560
|
const children = flattenChildren(element.props?.children)
|
|
329
|
-
node._resolvedChildren = children.map(c => resolveForFrame(c, node, instances, counters, visited)).filter(Boolean)
|
|
561
|
+
node._resolvedChildren = children.map(c => resolveForFrame(c, node, instances, counters, visited, scope)).filter(Boolean)
|
|
330
562
|
return node
|
|
331
563
|
}
|
|
332
564
|
|
|
333
565
|
const children = flattenChildren(element.props?.children)
|
|
334
566
|
if (children.length > 0) {
|
|
335
|
-
node._resolvedChildren = children.map(c => resolveForFrame(c, node, instances, counters, visited)).filter(Boolean)
|
|
567
|
+
node._resolvedChildren = children.map(c => resolveForFrame(c, node, instances, counters, visited, scope)).filter(Boolean)
|
|
336
568
|
}
|
|
337
569
|
|
|
338
570
|
return node
|
|
339
571
|
}
|
|
340
572
|
|
|
341
|
-
export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
573
|
+
export function mount(rootComponent, { stream, stdin, title, theme, onExit: onExitCb } = {}) {
|
|
342
574
|
const out = stream ?? process.stdout
|
|
343
575
|
const inp = stdin ?? process.stdin
|
|
344
576
|
|
|
@@ -355,6 +587,8 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
355
587
|
// component instance cache persists across frames
|
|
356
588
|
// maps instanceKey -> { scope, fn, hooks }
|
|
357
589
|
const instances = new Map()
|
|
590
|
+
let forceFullPaint = false
|
|
591
|
+
let prevHadOverlays = false
|
|
358
592
|
|
|
359
593
|
function frame() {
|
|
360
594
|
const prevCtx = activeContext
|
|
@@ -367,38 +601,77 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
367
601
|
const counters = new Map()
|
|
368
602
|
const visited = new Set()
|
|
369
603
|
const element = { type: rootComponent, props: {}, key: null }
|
|
370
|
-
const tree = resolveForFrame(element, null, instances, counters, visited)
|
|
604
|
+
const tree = resolveForFrame(element, null, instances, counters, visited, '')
|
|
371
605
|
|
|
372
606
|
computeLayout(tree, { x: 0, y: 0, width, height })
|
|
373
607
|
|
|
608
|
+
let layoutChanged = false
|
|
374
609
|
for (const inst of instances.values()) {
|
|
375
|
-
|
|
376
|
-
|
|
610
|
+
const rect = inst.node?._availableRect ?? inst.node?._layout
|
|
611
|
+
if (!rect) continue
|
|
612
|
+
const ch = findScrollContentHeight(inst.node)
|
|
613
|
+
if (!inst.layout) inst.layout = { x: 0, y: 0, width: 0, height: 0 }
|
|
614
|
+
const prev = inst.layout
|
|
615
|
+
if (prev.width !== rect.width || prev.height !== rect.height || prev.contentHeight !== ch) {
|
|
616
|
+
layoutChanged = true
|
|
617
|
+
}
|
|
618
|
+
prev.x = rect.x
|
|
619
|
+
prev.y = rect.y
|
|
620
|
+
prev.width = rect.width
|
|
621
|
+
prev.height = rect.height
|
|
622
|
+
prev.contentHeight = ch
|
|
377
623
|
}
|
|
378
624
|
|
|
379
|
-
|
|
625
|
+
// layout values changed - re-resolve so components see updated useLayout()
|
|
626
|
+
if (layoutChanged) {
|
|
627
|
+
overlays = []
|
|
628
|
+
counters.clear()
|
|
629
|
+
visited.clear()
|
|
630
|
+
const tree2 = resolveForFrame(element, null, instances, counters, visited, '')
|
|
631
|
+
computeLayout(tree2, { x: 0, y: 0, width, height })
|
|
632
|
+
for (const inst of instances.values()) {
|
|
633
|
+
const rect = inst.node?._availableRect ?? inst.node?._layout
|
|
634
|
+
if (!rect) continue
|
|
635
|
+
const ch = findScrollContentHeight(inst.node)
|
|
636
|
+
if (!inst.layout) inst.layout = { x: 0, y: 0, width: 0, height: 0 }
|
|
637
|
+
inst.layout.x = rect.x
|
|
638
|
+
inst.layout.y = rect.y
|
|
639
|
+
inst.layout.width = rect.width
|
|
640
|
+
inst.layout.height = rect.height
|
|
641
|
+
inst.layout.contentHeight = ch
|
|
642
|
+
inst._dirty = true
|
|
643
|
+
}
|
|
644
|
+
propagateDirty(tree2)
|
|
645
|
+
paintTree(tree2, curr, null, null, null)
|
|
646
|
+
} else {
|
|
647
|
+
propagateDirty(tree)
|
|
648
|
+
paintTree(tree, curr, null, null, (forceFullPaint || prevHadOverlays) ? null : prev)
|
|
649
|
+
forceFullPaint = false
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const hasOverlays = overlays.length > 0
|
|
380
653
|
|
|
381
654
|
for (const { element: overlayEl, owner, backdrop, fullscreen } of overlays) {
|
|
382
655
|
if (backdrop) dimBuffer(curr)
|
|
383
656
|
|
|
384
|
-
const
|
|
657
|
+
const overlayRect = (backdrop || fullscreen)
|
|
658
|
+
? { x: 0, y: 0, width, height }
|
|
659
|
+
: (() => {
|
|
660
|
+
const anchor = owner.node?._layout ?? owner.layout ?? { x: 0, y: 0, width: 0, height: 0 }
|
|
661
|
+
return { x: anchor.x, y: anchor.y + 1, width: width - anchor.x, height: height - anchor.y - 1 }
|
|
662
|
+
})()
|
|
663
|
+
|
|
664
|
+
const overlayTree = resolveForFrame(overlayEl, null, instances, counters, visited, '')
|
|
385
665
|
if (overlayTree) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
} else {
|
|
389
|
-
const anchor = owner.node?._layout ?? owner.layout ?? { x: 0, y: 0, width: 0, height: 0 }
|
|
390
|
-
computeLayout(overlayTree, {
|
|
391
|
-
x: anchor.x,
|
|
392
|
-
y: anchor.y + 1,
|
|
393
|
-
width: width - anchor.x,
|
|
394
|
-
height: height - anchor.y - 1,
|
|
395
|
-
})
|
|
396
|
-
}
|
|
666
|
+
computeLayout(overlayTree, overlayRect)
|
|
667
|
+
updateOverlayLayouts(overlayTree)
|
|
397
668
|
clearOverlayRect(overlayTree, curr)
|
|
398
669
|
paintTree(overlayTree, curr)
|
|
399
670
|
}
|
|
400
671
|
}
|
|
401
672
|
|
|
673
|
+
prevHadOverlays = hasOverlays
|
|
674
|
+
|
|
402
675
|
for (const [key, inst] of instances) {
|
|
403
676
|
if (!visited.has(key)) {
|
|
404
677
|
disposeScope(inst.scope)
|
|
@@ -408,8 +681,20 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
408
681
|
|
|
409
682
|
activeContext = prevCtx
|
|
410
683
|
|
|
411
|
-
const output = diff(prev, curr)
|
|
412
|
-
if (
|
|
684
|
+
const { output, changed } = diff(prev, curr)
|
|
685
|
+
if (changed > 0) {
|
|
686
|
+
out.write(ansi.hideCursor)
|
|
687
|
+
out.write(output)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const now = performance.now()
|
|
691
|
+
if (lastFrameTimestamp > 0) {
|
|
692
|
+
frameTimeWindow.push(now - lastFrameTimestamp)
|
|
693
|
+
if (frameTimeWindow.length > 30) frameTimeWindow.shift()
|
|
694
|
+
}
|
|
695
|
+
lastFrameTimestamp = now
|
|
696
|
+
const avgMs = frameTimeWindow.length > 0 ? frameTimeWindow.reduce((a, b) => a + b, 0) / frameTimeWindow.length : 16.67
|
|
697
|
+
lastFrameStats = { changed, total: width * height, bytes: output ? Buffer.byteLength(output) : 0, fps: Math.round(1000 / avgMs) }
|
|
413
698
|
|
|
414
699
|
const tmp = prev
|
|
415
700
|
prev = curr
|
|
@@ -423,7 +708,7 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
423
708
|
|
|
424
709
|
setSchedulerHook(scheduler.requestFrame)
|
|
425
710
|
|
|
426
|
-
out.write(ansi.altScreen + ansi.hideCursor + ansi.clearScreen + (title ? ansi.setTitle(title) : ''))
|
|
711
|
+
out.write(ansi.altScreen + ansi.hideCursor + ansi.clearScreen + ansi.enableMouse + (title ? ansi.setTitle(title) : ''))
|
|
427
712
|
if (inp.isTTY && inp.setRawMode) inp.setRawMode(true)
|
|
428
713
|
|
|
429
714
|
frame()
|
|
@@ -442,7 +727,8 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
442
727
|
input.onKey((event) => {
|
|
443
728
|
if (event.key === 'c' && event.ctrl) {
|
|
444
729
|
unmount()
|
|
445
|
-
|
|
730
|
+
if (onExitCb) onExitCb()
|
|
731
|
+
else process.exit(0)
|
|
446
732
|
}
|
|
447
733
|
})
|
|
448
734
|
|
|
@@ -462,7 +748,7 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
462
748
|
}
|
|
463
749
|
instances.clear()
|
|
464
750
|
|
|
465
|
-
out.write(ansi.sgrReset + ansi.showCursor + ansi.exitAltScreen)
|
|
751
|
+
out.write(ansi.sgrReset + ansi.disableMouse + ansi.showCursor + ansi.exitAltScreen)
|
|
466
752
|
if (inp.isTTY && inp.setRawMode) inp.setRawMode(false)
|
|
467
753
|
activeContext = null
|
|
468
754
|
setSchedulerHook(null)
|
|
@@ -474,5 +760,15 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
474
760
|
|
|
475
761
|
process.on('exit', onExit)
|
|
476
762
|
|
|
477
|
-
|
|
763
|
+
function repaint() {
|
|
764
|
+
prev = createBuffer(width, height)
|
|
765
|
+
curr = createBuffer(width, height)
|
|
766
|
+
forceFullPaint = true
|
|
767
|
+
out.write(ansi.clearScreen)
|
|
768
|
+
scheduler.forceFrame()
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
ctx.repaint = repaint
|
|
772
|
+
|
|
773
|
+
return { unmount, repaint, getBuffer: () => prev }
|
|
478
774
|
}
|