@trendr/core 0.1.0 → 0.2.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/README.md +368 -54
- package/index.js +7 -1
- package/package.json +21 -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 +139 -16
- package/src/progress.js +57 -9
- package/src/radio.js +16 -2
- package/src/renderer.js +336 -63
- 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 +46 -19
- 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/radio.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs } from '../jsx-runtime.js'
|
|
2
2
|
import { createSignal } from './signal.js'
|
|
3
|
-
import { useInput, useTheme } from './hooks.js'
|
|
3
|
+
import { useInput, useMouse, useLayout, useTheme } from './hooks.js'
|
|
4
4
|
|
|
5
5
|
export function Radio({ options, selected, onSelect, focused = false }) {
|
|
6
6
|
const { accent = 'cyan' } = useTheme()
|
|
@@ -29,12 +29,26 @@ export function Radio({ options, selected, onSelect, focused = false }) {
|
|
|
29
29
|
}
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
+
const layout = useLayout()
|
|
33
|
+
|
|
34
|
+
useMouse((event) => {
|
|
35
|
+
if (event.action !== 'press' || event.button !== 'left') return
|
|
36
|
+
const { x, y } = event
|
|
37
|
+
if (x < layout.x || x >= layout.x + layout.width || y < layout.y || y >= layout.y + layout.height) return
|
|
38
|
+
const idx = y - layout.y
|
|
39
|
+
if (idx >= 0 && idx < options.length) {
|
|
40
|
+
setCursor(idx)
|
|
41
|
+
onSelect?.(options[idx])
|
|
42
|
+
event.stopPropagation()
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
32
46
|
const c = cursor()
|
|
33
47
|
|
|
34
48
|
const children = options.map((option, i) => {
|
|
35
49
|
const isSelected = option === selected
|
|
36
50
|
const isCursor = focused && i === c
|
|
37
|
-
const icon = isSelected ? '\
|
|
51
|
+
const icon = isSelected ? '\u25cf' : '\u25cb'
|
|
38
52
|
const bg = isCursor ? accent : null
|
|
39
53
|
const color = isCursor ? 'black' : null
|
|
40
54
|
|
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)
|
|
97
|
+
|
|
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)
|
|
101
|
+
|
|
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)
|
|
61
105
|
|
|
62
|
-
buf.cells[y * buf.width + x] = cell(chars.
|
|
63
|
-
buf.cells[y * buf.width + x + width - 1] = cell(chars.
|
|
64
|
-
buf.cells[(y + height - 1) * buf.width + x] = cell(chars.
|
|
65
|
-
buf.cells[(y + height - 1) * buf.width + x + width - 1] = cell(chars.br)
|
|
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)
|
|
66
109
|
|
|
67
|
-
for (let col = x + 1; col < x + width - 1; col++)
|
|
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,19 +309,29 @@ 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
|
}
|
|
@@ -269,7 +435,36 @@ export function registerHook(setupFn) {
|
|
|
269
435
|
// instances are keyed by component function + occurrence index,
|
|
270
436
|
// so multiple instances of the same component each get their own state.
|
|
271
437
|
|
|
272
|
-
function
|
|
438
|
+
function shallowPropsEqual(a, b) {
|
|
439
|
+
if (a === b) return true
|
|
440
|
+
if (!a || !b) return false
|
|
441
|
+
const keysA = Object.keys(a)
|
|
442
|
+
const keysB = Object.keys(b)
|
|
443
|
+
if (keysA.length !== keysB.length) return false
|
|
444
|
+
for (const k of keysA) {
|
|
445
|
+
if (k === 'children') continue
|
|
446
|
+
if (a[k] !== b[k]) return false
|
|
447
|
+
}
|
|
448
|
+
return true
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function isInstanceClean(instance, newProps) {
|
|
452
|
+
if (!instance._trackedSignals) return false
|
|
453
|
+
if (!shallowPropsEqual(instance._lastProps, newProps)) return false
|
|
454
|
+
const sigs = instance._trackedSignals
|
|
455
|
+
const vals = instance._signalValues
|
|
456
|
+
for (let i = 0; i < sigs.length; i++) {
|
|
457
|
+
if (sigs[i]() !== vals[i]) return false
|
|
458
|
+
}
|
|
459
|
+
return true
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function snapshotSignals(instance, signals) {
|
|
463
|
+
instance._trackedSignals = signals
|
|
464
|
+
instance._signalValues = signals.map(g => g())
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function resolveForFrame(element, parent, instances, counters, visited, scope) {
|
|
273
468
|
if (element == null || typeof element === 'boolean') return null
|
|
274
469
|
|
|
275
470
|
if (typeof element === 'string' || typeof element === 'number') {
|
|
@@ -296,49 +491,63 @@ function resolveForFrame(element, parent, instances, counters, visited) {
|
|
|
296
491
|
|
|
297
492
|
if (typeof element.type === 'function') {
|
|
298
493
|
const fn = element.type
|
|
299
|
-
const
|
|
300
|
-
counters.
|
|
494
|
+
const counterKey = `${scope}/${fn.name}`
|
|
495
|
+
const count = counters.get(counterKey) ?? 0
|
|
496
|
+
counters.set(counterKey, count + 1)
|
|
301
497
|
|
|
302
|
-
const instanceKey = element.key != null ? `${fn.name}:key:${element.key}` : `${fn.name}:${count}`
|
|
498
|
+
const instanceKey = element.key != null ? `${scope}/${fn.name}:key:${element.key}` : `${scope}/${fn.name}:${count}`
|
|
303
499
|
if (visited) visited.add(instanceKey)
|
|
304
500
|
let instance = instances.get(instanceKey)
|
|
305
501
|
|
|
306
502
|
if (!instance) {
|
|
307
503
|
let result
|
|
308
|
-
instance = { scope: null, fn, hooks: [], node: null, layout: null }
|
|
504
|
+
instance = { scope: null, fn, hooks: [], node: null, layout: null, _dirty: true }
|
|
309
505
|
instances.set(instanceKey, instance)
|
|
310
506
|
instance.scope = createScope(() => {
|
|
311
507
|
startHookTracking(instance)
|
|
508
|
+
startRenderTracking()
|
|
312
509
|
result = fn(element.props ?? {})
|
|
510
|
+
const signals = stopRenderTracking()
|
|
313
511
|
endHookTracking()
|
|
512
|
+
snapshotSignals(instance, signals)
|
|
513
|
+
instance._lastProps = element.props
|
|
314
514
|
})
|
|
315
|
-
node._resolved = resolveForFrame(result, node, instances, counters, visited)
|
|
515
|
+
node._resolved = resolveForFrame(result, node, instances, counters, visited, instanceKey)
|
|
316
516
|
} else {
|
|
517
|
+
const clean = isInstanceClean(instance, element.props)
|
|
518
|
+
instance._dirty = !clean
|
|
519
|
+
|
|
317
520
|
startHookTracking(instance)
|
|
521
|
+
startRenderTracking()
|
|
318
522
|
const result = fn(element.props ?? {})
|
|
523
|
+
const signals = stopRenderTracking()
|
|
319
524
|
endHookTracking()
|
|
320
|
-
|
|
525
|
+
snapshotSignals(instance, signals)
|
|
526
|
+
instance._lastProps = element.props
|
|
527
|
+
|
|
528
|
+
node._resolved = resolveForFrame(result, node, instances, counters, visited, instanceKey)
|
|
321
529
|
}
|
|
322
530
|
|
|
531
|
+
node._instance = instance
|
|
323
532
|
instance.node = node
|
|
324
533
|
return node
|
|
325
534
|
}
|
|
326
535
|
|
|
327
536
|
if (element.type === Fragment) {
|
|
328
537
|
const children = flattenChildren(element.props?.children)
|
|
329
|
-
node._resolvedChildren = children.map(c => resolveForFrame(c, node, instances, counters, visited)).filter(Boolean)
|
|
538
|
+
node._resolvedChildren = children.map(c => resolveForFrame(c, node, instances, counters, visited, scope)).filter(Boolean)
|
|
330
539
|
return node
|
|
331
540
|
}
|
|
332
541
|
|
|
333
542
|
const children = flattenChildren(element.props?.children)
|
|
334
543
|
if (children.length > 0) {
|
|
335
|
-
node._resolvedChildren = children.map(c => resolveForFrame(c, node, instances, counters, visited)).filter(Boolean)
|
|
544
|
+
node._resolvedChildren = children.map(c => resolveForFrame(c, node, instances, counters, visited, scope)).filter(Boolean)
|
|
336
545
|
}
|
|
337
546
|
|
|
338
547
|
return node
|
|
339
548
|
}
|
|
340
549
|
|
|
341
|
-
export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
550
|
+
export function mount(rootComponent, { stream, stdin, title, theme, onExit: onExitCb } = {}) {
|
|
342
551
|
const out = stream ?? process.stdout
|
|
343
552
|
const inp = stdin ?? process.stdin
|
|
344
553
|
|
|
@@ -355,6 +564,8 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
355
564
|
// component instance cache persists across frames
|
|
356
565
|
// maps instanceKey -> { scope, fn, hooks }
|
|
357
566
|
const instances = new Map()
|
|
567
|
+
let forceFullPaint = false
|
|
568
|
+
let prevHadOverlays = false
|
|
358
569
|
|
|
359
570
|
function frame() {
|
|
360
571
|
const prevCtx = activeContext
|
|
@@ -367,38 +578,77 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
367
578
|
const counters = new Map()
|
|
368
579
|
const visited = new Set()
|
|
369
580
|
const element = { type: rootComponent, props: {}, key: null }
|
|
370
|
-
const tree = resolveForFrame(element, null, instances, counters, visited)
|
|
581
|
+
const tree = resolveForFrame(element, null, instances, counters, visited, '')
|
|
371
582
|
|
|
372
583
|
computeLayout(tree, { x: 0, y: 0, width, height })
|
|
373
584
|
|
|
585
|
+
let layoutChanged = false
|
|
374
586
|
for (const inst of instances.values()) {
|
|
375
|
-
|
|
376
|
-
|
|
587
|
+
const rect = inst.node?._availableRect ?? inst.node?._layout
|
|
588
|
+
if (!rect) continue
|
|
589
|
+
const ch = findScrollContentHeight(inst.node)
|
|
590
|
+
if (!inst.layout) inst.layout = { x: 0, y: 0, width: 0, height: 0 }
|
|
591
|
+
const prev = inst.layout
|
|
592
|
+
if (prev.width !== rect.width || prev.height !== rect.height || prev.contentHeight !== ch) {
|
|
593
|
+
layoutChanged = true
|
|
594
|
+
}
|
|
595
|
+
prev.x = rect.x
|
|
596
|
+
prev.y = rect.y
|
|
597
|
+
prev.width = rect.width
|
|
598
|
+
prev.height = rect.height
|
|
599
|
+
prev.contentHeight = ch
|
|
377
600
|
}
|
|
378
601
|
|
|
379
|
-
|
|
602
|
+
// layout values changed - re-resolve so components see updated useLayout()
|
|
603
|
+
if (layoutChanged) {
|
|
604
|
+
overlays = []
|
|
605
|
+
counters.clear()
|
|
606
|
+
visited.clear()
|
|
607
|
+
const tree2 = resolveForFrame(element, null, instances, counters, visited, '')
|
|
608
|
+
computeLayout(tree2, { x: 0, y: 0, width, height })
|
|
609
|
+
for (const inst of instances.values()) {
|
|
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
|
+
inst.layout.x = rect.x
|
|
615
|
+
inst.layout.y = rect.y
|
|
616
|
+
inst.layout.width = rect.width
|
|
617
|
+
inst.layout.height = rect.height
|
|
618
|
+
inst.layout.contentHeight = ch
|
|
619
|
+
inst._dirty = true
|
|
620
|
+
}
|
|
621
|
+
propagateDirty(tree2)
|
|
622
|
+
paintTree(tree2, curr, null, null, null)
|
|
623
|
+
} else {
|
|
624
|
+
propagateDirty(tree)
|
|
625
|
+
paintTree(tree, curr, null, null, (forceFullPaint || prevHadOverlays) ? null : prev)
|
|
626
|
+
forceFullPaint = false
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const hasOverlays = overlays.length > 0
|
|
380
630
|
|
|
381
631
|
for (const { element: overlayEl, owner, backdrop, fullscreen } of overlays) {
|
|
382
632
|
if (backdrop) dimBuffer(curr)
|
|
383
633
|
|
|
384
|
-
const
|
|
634
|
+
const overlayRect = (backdrop || fullscreen)
|
|
635
|
+
? { x: 0, y: 0, width, height }
|
|
636
|
+
: (() => {
|
|
637
|
+
const anchor = owner.node?._layout ?? owner.layout ?? { x: 0, y: 0, width: 0, height: 0 }
|
|
638
|
+
return { x: anchor.x, y: anchor.y + 1, width: width - anchor.x, height: height - anchor.y - 1 }
|
|
639
|
+
})()
|
|
640
|
+
|
|
641
|
+
const overlayTree = resolveForFrame(overlayEl, null, instances, counters, visited, '')
|
|
385
642
|
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
|
-
}
|
|
643
|
+
computeLayout(overlayTree, overlayRect)
|
|
644
|
+
updateOverlayLayouts(overlayTree)
|
|
397
645
|
clearOverlayRect(overlayTree, curr)
|
|
398
646
|
paintTree(overlayTree, curr)
|
|
399
647
|
}
|
|
400
648
|
}
|
|
401
649
|
|
|
650
|
+
prevHadOverlays = hasOverlays
|
|
651
|
+
|
|
402
652
|
for (const [key, inst] of instances) {
|
|
403
653
|
if (!visited.has(key)) {
|
|
404
654
|
disposeScope(inst.scope)
|
|
@@ -408,8 +658,20 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
408
658
|
|
|
409
659
|
activeContext = prevCtx
|
|
410
660
|
|
|
411
|
-
const output = diff(prev, curr)
|
|
412
|
-
if (
|
|
661
|
+
const { output, changed } = diff(prev, curr)
|
|
662
|
+
if (changed > 0) {
|
|
663
|
+
out.write(ansi.hideCursor)
|
|
664
|
+
out.write(output)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const now = performance.now()
|
|
668
|
+
if (lastFrameTimestamp > 0) {
|
|
669
|
+
frameTimeWindow.push(now - lastFrameTimestamp)
|
|
670
|
+
if (frameTimeWindow.length > 30) frameTimeWindow.shift()
|
|
671
|
+
}
|
|
672
|
+
lastFrameTimestamp = now
|
|
673
|
+
const avgMs = frameTimeWindow.length > 0 ? frameTimeWindow.reduce((a, b) => a + b, 0) / frameTimeWindow.length : 16.67
|
|
674
|
+
lastFrameStats = { changed, total: width * height, bytes: output ? Buffer.byteLength(output) : 0, fps: Math.round(1000 / avgMs) }
|
|
413
675
|
|
|
414
676
|
const tmp = prev
|
|
415
677
|
prev = curr
|
|
@@ -423,7 +685,7 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
423
685
|
|
|
424
686
|
setSchedulerHook(scheduler.requestFrame)
|
|
425
687
|
|
|
426
|
-
out.write(ansi.altScreen + ansi.hideCursor + ansi.clearScreen + (title ? ansi.setTitle(title) : ''))
|
|
688
|
+
out.write(ansi.altScreen + ansi.hideCursor + ansi.clearScreen + ansi.enableMouse + (title ? ansi.setTitle(title) : ''))
|
|
427
689
|
if (inp.isTTY && inp.setRawMode) inp.setRawMode(true)
|
|
428
690
|
|
|
429
691
|
frame()
|
|
@@ -442,7 +704,8 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
442
704
|
input.onKey((event) => {
|
|
443
705
|
if (event.key === 'c' && event.ctrl) {
|
|
444
706
|
unmount()
|
|
445
|
-
|
|
707
|
+
if (onExitCb) onExitCb()
|
|
708
|
+
else process.exit(0)
|
|
446
709
|
}
|
|
447
710
|
})
|
|
448
711
|
|
|
@@ -462,7 +725,7 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
462
725
|
}
|
|
463
726
|
instances.clear()
|
|
464
727
|
|
|
465
|
-
out.write(ansi.sgrReset + ansi.showCursor + ansi.exitAltScreen)
|
|
728
|
+
out.write(ansi.sgrReset + ansi.disableMouse + ansi.showCursor + ansi.exitAltScreen)
|
|
466
729
|
if (inp.isTTY && inp.setRawMode) inp.setRawMode(false)
|
|
467
730
|
activeContext = null
|
|
468
731
|
setSchedulerHook(null)
|
|
@@ -474,5 +737,15 @@ export function mount(rootComponent, { stream, stdin, title, theme } = {}) {
|
|
|
474
737
|
|
|
475
738
|
process.on('exit', onExit)
|
|
476
739
|
|
|
477
|
-
|
|
740
|
+
function repaint() {
|
|
741
|
+
prev = createBuffer(width, height)
|
|
742
|
+
curr = createBuffer(width, height)
|
|
743
|
+
forceFullPaint = true
|
|
744
|
+
out.write(ansi.clearScreen)
|
|
745
|
+
scheduler.forceFrame()
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
ctx.repaint = repaint
|
|
749
|
+
|
|
750
|
+
return { unmount, repaint }
|
|
478
751
|
}
|