@silvery/term 0.3.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/package.json +54 -0
- package/src/adapters/canvas-adapter.ts +356 -0
- package/src/adapters/dom-adapter.ts +452 -0
- package/src/adapters/flexily-zero-adapter.ts +368 -0
- package/src/adapters/terminal-adapter.ts +305 -0
- package/src/adapters/yoga-adapter.ts +370 -0
- package/src/ansi/ansi.ts +251 -0
- package/src/ansi/constants.ts +76 -0
- package/src/ansi/detection.ts +441 -0
- package/src/ansi/hyperlink.ts +38 -0
- package/src/ansi/index.ts +201 -0
- package/src/ansi/patch-console.ts +159 -0
- package/src/ansi/sgr-codes.ts +34 -0
- package/src/ansi/storybook.ts +209 -0
- package/src/ansi/term.ts +724 -0
- package/src/ansi/types.ts +202 -0
- package/src/ansi/underline.ts +156 -0
- package/src/ansi/utils.ts +65 -0
- package/src/ansi-sanitize.ts +509 -0
- package/src/app.ts +571 -0
- package/src/bound-term.ts +94 -0
- package/src/bracketed-paste.ts +75 -0
- package/src/browser-renderer.ts +174 -0
- package/src/buffer.ts +1984 -0
- package/src/clipboard.ts +74 -0
- package/src/cursor-query.ts +85 -0
- package/src/device-attrs.ts +228 -0
- package/src/devtools.ts +123 -0
- package/src/dom/index.ts +194 -0
- package/src/errors.ts +39 -0
- package/src/focus-reporting.ts +48 -0
- package/src/hit-registry-core.ts +228 -0
- package/src/hit-registry.ts +176 -0
- package/src/index.ts +458 -0
- package/src/input.ts +119 -0
- package/src/inspector.ts +155 -0
- package/src/kitty-detect.ts +95 -0
- package/src/kitty-manager.ts +160 -0
- package/src/layout-engine.ts +296 -0
- package/src/layout.ts +26 -0
- package/src/measurer.ts +74 -0
- package/src/mode-query.ts +106 -0
- package/src/mouse-events.ts +419 -0
- package/src/mouse.ts +83 -0
- package/src/non-tty.ts +223 -0
- package/src/osc-markers.ts +32 -0
- package/src/osc-palette.ts +169 -0
- package/src/output.ts +406 -0
- package/src/pane-manager.ts +248 -0
- package/src/pipeline/CLAUDE.md +587 -0
- package/src/pipeline/content-phase-adapter.ts +976 -0
- package/src/pipeline/content-phase.ts +1765 -0
- package/src/pipeline/helpers.ts +42 -0
- package/src/pipeline/index.ts +416 -0
- package/src/pipeline/layout-phase.ts +686 -0
- package/src/pipeline/measure-phase.ts +198 -0
- package/src/pipeline/measure-stats.ts +21 -0
- package/src/pipeline/output-phase.ts +2593 -0
- package/src/pipeline/render-box.ts +343 -0
- package/src/pipeline/render-helpers.ts +243 -0
- package/src/pipeline/render-text.ts +1255 -0
- package/src/pipeline/types.ts +161 -0
- package/src/pipeline.ts +29 -0
- package/src/pixel-size.ts +119 -0
- package/src/render-adapter.ts +179 -0
- package/src/renderer.ts +1330 -0
- package/src/runtime/create-app.tsx +1845 -0
- package/src/runtime/create-buffer.ts +18 -0
- package/src/runtime/create-runtime.ts +325 -0
- package/src/runtime/diff.ts +56 -0
- package/src/runtime/event-handlers.ts +254 -0
- package/src/runtime/index.ts +119 -0
- package/src/runtime/keys.ts +8 -0
- package/src/runtime/layout.ts +164 -0
- package/src/runtime/run.tsx +318 -0
- package/src/runtime/term-provider.ts +399 -0
- package/src/runtime/terminal-lifecycle.ts +246 -0
- package/src/runtime/tick.ts +219 -0
- package/src/runtime/types.ts +210 -0
- package/src/scheduler.ts +723 -0
- package/src/screenshot.ts +57 -0
- package/src/scroll-region.ts +69 -0
- package/src/scroll-utils.ts +97 -0
- package/src/term-def.ts +267 -0
- package/src/terminal-caps.ts +5 -0
- package/src/terminal-colors.ts +216 -0
- package/src/termtest.ts +224 -0
- package/src/text-sizing.ts +109 -0
- package/src/toolbelt/index.ts +72 -0
- package/src/unicode.ts +1763 -0
- package/src/xterm/index.ts +491 -0
- package/src/xterm/xterm-provider.ts +204 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper functions for silvery pipeline phases.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { BoxProps } from "@silvery/tea/types"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get padding values from props.
|
|
9
|
+
*/
|
|
10
|
+
export function getPadding(props: BoxProps): {
|
|
11
|
+
top: number
|
|
12
|
+
bottom: number
|
|
13
|
+
left: number
|
|
14
|
+
right: number
|
|
15
|
+
} {
|
|
16
|
+
return {
|
|
17
|
+
top: props.paddingTop ?? props.paddingY ?? props.padding ?? 0,
|
|
18
|
+
bottom: props.paddingBottom ?? props.paddingY ?? props.padding ?? 0,
|
|
19
|
+
left: props.paddingLeft ?? props.paddingX ?? props.padding ?? 0,
|
|
20
|
+
right: props.paddingRight ?? props.paddingX ?? props.padding ?? 0,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get border size (1 or 0 for each side).
|
|
26
|
+
*/
|
|
27
|
+
export function getBorderSize(props: BoxProps): {
|
|
28
|
+
top: number
|
|
29
|
+
bottom: number
|
|
30
|
+
left: number
|
|
31
|
+
right: number
|
|
32
|
+
} {
|
|
33
|
+
if (!props.borderStyle) {
|
|
34
|
+
return { top: 0, bottom: 0, left: 0, right: 0 }
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
top: props.borderTop !== false ? 1 : 0,
|
|
38
|
+
bottom: props.borderBottom !== false ? 1 : 0,
|
|
39
|
+
left: props.borderLeft !== false ? 1 : 0,
|
|
40
|
+
right: props.borderRight !== false ? 1 : 0,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Silvery Render Pipeline
|
|
3
|
+
*
|
|
4
|
+
* The 5-phase rendering architecture:
|
|
5
|
+
*
|
|
6
|
+
* Phase 0: RECONCILIATION (React)
|
|
7
|
+
* React reconciliation builds the SilveryNode tree.
|
|
8
|
+
* Components register layout constraints via props.
|
|
9
|
+
*
|
|
10
|
+
* Phase 1: MEASURE (for fit-content nodes)
|
|
11
|
+
* Traverse nodes with width/height="fit-content"
|
|
12
|
+
* Measure intrinsic content size
|
|
13
|
+
* Set Yoga constraints based on measurement
|
|
14
|
+
*
|
|
15
|
+
* Phase 2: LAYOUT
|
|
16
|
+
* Run yoga.calculateLayout()
|
|
17
|
+
* Propagate computed dimensions to all nodes
|
|
18
|
+
* Notify useContentRect() subscribers
|
|
19
|
+
*
|
|
20
|
+
* Phase 3: CONTENT RENDER
|
|
21
|
+
* Render each node to the TerminalBuffer
|
|
22
|
+
* Handle text truncation, styling, borders
|
|
23
|
+
*
|
|
24
|
+
* Phase 4: DIFF & OUTPUT
|
|
25
|
+
* Compare current buffer with previous
|
|
26
|
+
* Emit minimal ANSI sequences for changes
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { createLogger } from "loggily"
|
|
30
|
+
import type { TerminalBuffer } from "../buffer"
|
|
31
|
+
import type { CursorState } from "@silvery/react/hooks/useCursor"
|
|
32
|
+
import type { TeaNode } from "@silvery/tea/types"
|
|
33
|
+
import { runWithMeasurer, type Measurer } from "../unicode"
|
|
34
|
+
import type { OutputPhaseFn } from "./output-phase"
|
|
35
|
+
import type { PipelineContext } from "./types"
|
|
36
|
+
|
|
37
|
+
const log = createLogger("silvery:pipeline")
|
|
38
|
+
const baseLog = createLogger("@silvery/react")
|
|
39
|
+
|
|
40
|
+
// Re-export types
|
|
41
|
+
export type {
|
|
42
|
+
CellChange,
|
|
43
|
+
BorderChars,
|
|
44
|
+
PipelineContext,
|
|
45
|
+
NodeRenderState,
|
|
46
|
+
ClipBounds,
|
|
47
|
+
ContentPhaseStats,
|
|
48
|
+
NodeTraceEntry,
|
|
49
|
+
BgConflictMode,
|
|
50
|
+
} from "./types"
|
|
51
|
+
|
|
52
|
+
// Re-export phase functions
|
|
53
|
+
export { measurePhase } from "./measure-phase"
|
|
54
|
+
export {
|
|
55
|
+
layoutPhase,
|
|
56
|
+
rectEqual,
|
|
57
|
+
scrollPhase,
|
|
58
|
+
stickyPhase,
|
|
59
|
+
screenRectPhase,
|
|
60
|
+
notifyLayoutSubscribers,
|
|
61
|
+
} from "./layout-phase"
|
|
62
|
+
export { contentPhase, clearBgConflictWarnings, setBgConflictMode } from "./content-phase"
|
|
63
|
+
export { contentPhaseAdapter } from "./content-phase-adapter"
|
|
64
|
+
export { outputPhase } from "./output-phase"
|
|
65
|
+
|
|
66
|
+
import { contentPhaseAdapter } from "./content-phase-adapter"
|
|
67
|
+
import { clearBgConflictWarnings, contentPhase } from "./content-phase"
|
|
68
|
+
import { layoutPhase, notifyLayoutSubscribers, screenRectPhase, scrollPhase, stickyPhase } from "./layout-phase"
|
|
69
|
+
// Import for orchestration
|
|
70
|
+
import { measurePhase } from "./measure-phase"
|
|
71
|
+
import { outputPhase } from "./output-phase"
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Execute Render (Orchestration)
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Options for executeRender.
|
|
79
|
+
*/
|
|
80
|
+
export interface ExecuteRenderOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Render mode: fullscreen or inline.
|
|
83
|
+
* Default: 'fullscreen'
|
|
84
|
+
*/
|
|
85
|
+
mode?: "fullscreen" | "inline"
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Skip notifying layout subscribers.
|
|
89
|
+
* Use for static/one-shot renders where layout feedback isn't needed.
|
|
90
|
+
* Default: false
|
|
91
|
+
*/
|
|
92
|
+
skipLayoutNotifications?: boolean
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Skip scroll state updates.
|
|
96
|
+
* Use for fresh render comparisons (SILVERY_STRICT) to avoid mutating state.
|
|
97
|
+
* Default: false
|
|
98
|
+
*/
|
|
99
|
+
skipScrollStateUpdates?: boolean
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Number of lines written to stdout between renders (inline mode only).
|
|
103
|
+
* Used to adjust cursor positioning when external code (e.g., useScrollback)
|
|
104
|
+
* writes directly to stdout between renders.
|
|
105
|
+
* Default: 0
|
|
106
|
+
*/
|
|
107
|
+
scrollbackOffset?: number
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Terminal height in rows (inline mode only).
|
|
111
|
+
* Used to clamp cursor-up offset when content exceeds terminal height.
|
|
112
|
+
* Without this, content taller than the terminal causes rendering corruption
|
|
113
|
+
* because cursor-up can't reach lines that scrolled off screen.
|
|
114
|
+
*/
|
|
115
|
+
termRows?: number
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Cursor position from useCursor() (inline mode only).
|
|
119
|
+
* When provided, the output phase positions the real terminal cursor
|
|
120
|
+
* at this location instead of leaving it at the end of content.
|
|
121
|
+
*/
|
|
122
|
+
cursorPos?: CursorState | null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Pipeline configuration from withRender().
|
|
127
|
+
* Carries term-scoped width measurer and output phase.
|
|
128
|
+
*/
|
|
129
|
+
export interface PipelineConfig {
|
|
130
|
+
/** Width measurer scoped to terminal capabilities */
|
|
131
|
+
readonly measurer: Measurer
|
|
132
|
+
/** Output phase function scoped to terminal capabilities */
|
|
133
|
+
readonly outputPhaseFn: OutputPhaseFn
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Execute the full render pipeline.
|
|
138
|
+
*
|
|
139
|
+
* Pass null for prevBuffer on the first render; pass the returned buffer on
|
|
140
|
+
* subsequent renders to enable incremental content rendering (<1ms vs 20-30ms).
|
|
141
|
+
* SILVERY_DEV=1 warns at runtime if prevBuffer is null after the first frame.
|
|
142
|
+
*/
|
|
143
|
+
export function executeRender(
|
|
144
|
+
root: TeaNode,
|
|
145
|
+
width: number,
|
|
146
|
+
height: number,
|
|
147
|
+
prevBuffer: TerminalBuffer | null,
|
|
148
|
+
options: ExecuteRenderOptions | "fullscreen" | "inline" = "fullscreen",
|
|
149
|
+
config?: PipelineConfig,
|
|
150
|
+
): { output: string; buffer: TerminalBuffer } {
|
|
151
|
+
// Create PipelineContext from config measurer (if provided).
|
|
152
|
+
// The context is threaded explicitly through the pipeline, eliminating
|
|
153
|
+
// the need for pipeline functions to read the _scopedMeasurer global.
|
|
154
|
+
const ctx: PipelineContext | undefined = config?.measurer ? { measurer: config.measurer } : undefined
|
|
155
|
+
|
|
156
|
+
if (config?.measurer) {
|
|
157
|
+
// Keep runWithMeasurer for backward compat: output-phase and other
|
|
158
|
+
// non-pipeline consumers still read the scoped measurer global.
|
|
159
|
+
return runWithMeasurer(config.measurer, () => {
|
|
160
|
+
return executeRenderCore(root, width, height, prevBuffer, options, config, ctx)
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
return executeRenderCore(root, width, height, prevBuffer, options, config, ctx)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Internal: runs the full pipeline. */
|
|
167
|
+
function executeRenderCore(
|
|
168
|
+
root: TeaNode,
|
|
169
|
+
width: number,
|
|
170
|
+
height: number,
|
|
171
|
+
prevBuffer: TerminalBuffer | null,
|
|
172
|
+
options: ExecuteRenderOptions | "fullscreen" | "inline" = "fullscreen",
|
|
173
|
+
config?: PipelineConfig,
|
|
174
|
+
ctx?: PipelineContext,
|
|
175
|
+
): { output: string; buffer: TerminalBuffer } {
|
|
176
|
+
// Normalize options (string shorthand for mode)
|
|
177
|
+
const opts: ExecuteRenderOptions = typeof options === "string" ? { mode: options } : options
|
|
178
|
+
const {
|
|
179
|
+
mode = "fullscreen",
|
|
180
|
+
skipLayoutNotifications = false,
|
|
181
|
+
skipScrollStateUpdates = false,
|
|
182
|
+
scrollbackOffset = 0,
|
|
183
|
+
termRows,
|
|
184
|
+
cursorPos,
|
|
185
|
+
} = opts
|
|
186
|
+
// Dev warning: prevBuffer null after first render means incremental is disabled.
|
|
187
|
+
// Intentional null (SILVERY_STRICT, static/one-shot) passes skipLayoutNotifications.
|
|
188
|
+
// console.warn (not loggily) — must fire regardless of logger config.
|
|
189
|
+
if (process?.env?.SILVERY_DEV && prevBuffer === null && root.prevLayout !== null && !skipLayoutNotifications) {
|
|
190
|
+
console.warn(
|
|
191
|
+
"[silvery] executeRender called with prevBuffer=null on frame 2+ — " +
|
|
192
|
+
"incremental content rendering is disabled (full render every frame). " +
|
|
193
|
+
"Track the returned buffer and pass it as prevBuffer on subsequent renders.",
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const start = performance.now()
|
|
198
|
+
|
|
199
|
+
using render = baseLog.span("pipeline", { width, height, mode })
|
|
200
|
+
|
|
201
|
+
// Clear per-render caches
|
|
202
|
+
clearBgConflictWarnings()
|
|
203
|
+
|
|
204
|
+
// Phase 1: Measure (for fit-content nodes)
|
|
205
|
+
let tMeasure: number
|
|
206
|
+
{
|
|
207
|
+
using _measure = render.span("measure")
|
|
208
|
+
const t1 = performance.now()
|
|
209
|
+
measurePhase(root, ctx)
|
|
210
|
+
tMeasure = performance.now() - t1
|
|
211
|
+
log.debug?.(`measure: ${tMeasure.toFixed(2)}ms`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Phase 2: Layout
|
|
215
|
+
let tLayout: number
|
|
216
|
+
{
|
|
217
|
+
using _layout = render.span("layout")
|
|
218
|
+
const t2 = performance.now()
|
|
219
|
+
layoutPhase(root, width, height)
|
|
220
|
+
tLayout = performance.now() - t2
|
|
221
|
+
log.debug?.(`layout: ${tLayout.toFixed(2)}ms`)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Phase 2.5: Scroll calculation (for overflow='scroll' containers)
|
|
225
|
+
let tScroll: number
|
|
226
|
+
{
|
|
227
|
+
using _scroll = render.span("scroll")
|
|
228
|
+
const t2s = performance.now()
|
|
229
|
+
scrollPhase(root, { skipStateUpdates: skipScrollStateUpdates })
|
|
230
|
+
tScroll = performance.now() - t2s
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Phase 2.55: Sticky phase (non-scroll container sticky children)
|
|
234
|
+
stickyPhase(root)
|
|
235
|
+
|
|
236
|
+
// Phase 2.6: Screen rect calculation (screen-relative positions)
|
|
237
|
+
let tScreenRect: number
|
|
238
|
+
{
|
|
239
|
+
using _screenRect = render.span("screenRect")
|
|
240
|
+
const t2r = performance.now()
|
|
241
|
+
screenRectPhase(root)
|
|
242
|
+
tScreenRect = performance.now() - t2r
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Phase 2.7: Notify layout subscribers
|
|
246
|
+
// This runs AFTER screenRectPhase so useScreenRectCallback reads correct positions
|
|
247
|
+
// Skip for static renders where no one will respond to the feedback
|
|
248
|
+
let tNotify = 0
|
|
249
|
+
if (!skipLayoutNotifications) {
|
|
250
|
+
using _notify = render.span("notify")
|
|
251
|
+
const t2n = performance.now()
|
|
252
|
+
notifyLayoutSubscribers(root)
|
|
253
|
+
tNotify = performance.now() - t2n
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Phase 3: Content render (incremental if we have prevBuffer)
|
|
257
|
+
let buffer: TerminalBuffer
|
|
258
|
+
let tContent: number
|
|
259
|
+
{
|
|
260
|
+
using _content = render.span("content")
|
|
261
|
+
const t3 = performance.now()
|
|
262
|
+
buffer = contentPhase(root, prevBuffer, ctx)
|
|
263
|
+
tContent = performance.now() - t3
|
|
264
|
+
log.debug?.(`content: ${tContent.toFixed(2)}ms`)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Phase 4: Diff and output
|
|
268
|
+
let output: string
|
|
269
|
+
let tOutput: number
|
|
270
|
+
{
|
|
271
|
+
using outputSpan = render.span("output")
|
|
272
|
+
const t4 = performance.now()
|
|
273
|
+
const outputFn = config?.outputPhaseFn ?? outputPhase
|
|
274
|
+
try {
|
|
275
|
+
output = outputFn(prevBuffer, buffer, mode, scrollbackOffset, termRows, cursorPos)
|
|
276
|
+
} catch (e) {
|
|
277
|
+
// Output phase (SILVERY_STRICT_OUTPUT) may throw a diagnostic error.
|
|
278
|
+
// Attach the content-phase buffer so callers can still save it for
|
|
279
|
+
// incremental rendering continuity — the buffer is correct even when
|
|
280
|
+
// the ANSI output verification fails.
|
|
281
|
+
if (e instanceof Error) {
|
|
282
|
+
;(e as any).__silvery_buffer = buffer
|
|
283
|
+
}
|
|
284
|
+
throw e
|
|
285
|
+
}
|
|
286
|
+
tOutput = performance.now() - t4
|
|
287
|
+
outputSpan.spanData.bytes = output.length
|
|
288
|
+
log.debug?.(`output: ${tOutput.toFixed(2)}ms (${output.length} bytes)`)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const total = performance.now() - start
|
|
292
|
+
log.debug?.(`total pipeline: ${total.toFixed(2)}ms`)
|
|
293
|
+
|
|
294
|
+
// Expose phase timing and render count for benchmarking and diagnostics
|
|
295
|
+
;(globalThis as any).__silvery_last_pipeline = {
|
|
296
|
+
measure: tMeasure,
|
|
297
|
+
layout: tLayout,
|
|
298
|
+
scroll: tScroll,
|
|
299
|
+
screenRect: tScreenRect,
|
|
300
|
+
notify: tNotify,
|
|
301
|
+
content: tContent,
|
|
302
|
+
output: tOutput,
|
|
303
|
+
total,
|
|
304
|
+
incremental: prevBuffer !== null,
|
|
305
|
+
}
|
|
306
|
+
;(globalThis as any).__silvery_render_count = ((globalThis as any).__silvery_render_count ?? 0) + 1
|
|
307
|
+
|
|
308
|
+
return { output, buffer }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// Execute Render (Adapter-aware)
|
|
313
|
+
// ============================================================================
|
|
314
|
+
|
|
315
|
+
import { type RenderBuffer, getRenderAdapter, hasRenderAdapter } from "../render-adapter"
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Execute the full render pipeline using the current RenderAdapter.
|
|
319
|
+
*
|
|
320
|
+
* This version works with any adapter (terminal, canvas, etc.) and returns
|
|
321
|
+
* a RenderBuffer instead of a TerminalBuffer.
|
|
322
|
+
*
|
|
323
|
+
* @param root The root SilveryNode
|
|
324
|
+
* @param width Width in adapter units (cells for terminal, pixels for canvas)
|
|
325
|
+
* @param height Height in adapter units
|
|
326
|
+
* @param prevBuffer Previous buffer for diffing (null on first render)
|
|
327
|
+
* @param options Render options
|
|
328
|
+
* @returns Object with output (if any) and current buffer
|
|
329
|
+
*/
|
|
330
|
+
export function executeRenderAdapter(
|
|
331
|
+
root: TeaNode,
|
|
332
|
+
width: number,
|
|
333
|
+
height: number,
|
|
334
|
+
prevBuffer: RenderBuffer | null,
|
|
335
|
+
options: ExecuteRenderOptions | "fullscreen" | "inline" = "fullscreen",
|
|
336
|
+
): { output: string | void; buffer: RenderBuffer } {
|
|
337
|
+
if (!hasRenderAdapter()) {
|
|
338
|
+
throw new Error("executeRenderAdapter called without a render adapter set")
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const opts: ExecuteRenderOptions = typeof options === "string" ? { mode: options } : options
|
|
342
|
+
const { skipLayoutNotifications = false } = opts
|
|
343
|
+
const start = Date.now()
|
|
344
|
+
const adapter = getRenderAdapter()
|
|
345
|
+
|
|
346
|
+
using render = baseLog.span("pipeline-adapter", {
|
|
347
|
+
width,
|
|
348
|
+
height,
|
|
349
|
+
adapter: adapter.name,
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
// Clear per-render caches
|
|
353
|
+
clearBgConflictWarnings()
|
|
354
|
+
|
|
355
|
+
// Phase 1: Measure
|
|
356
|
+
{
|
|
357
|
+
using _measure = render.span("measure")
|
|
358
|
+
const t1 = Date.now()
|
|
359
|
+
measurePhase(root)
|
|
360
|
+
log.debug?.(`measure: ${Date.now() - t1}ms`)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Phase 2: Layout
|
|
364
|
+
{
|
|
365
|
+
using _layout = render.span("layout")
|
|
366
|
+
const t2 = Date.now()
|
|
367
|
+
layoutPhase(root, width, height)
|
|
368
|
+
log.debug?.(`layout: ${Date.now() - t2}ms`)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Phase 2.5: Scroll calculation
|
|
372
|
+
{
|
|
373
|
+
using _scroll = render.span("scroll")
|
|
374
|
+
scrollPhase(root)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Phase 2.55: Sticky phase (non-scroll container sticky children)
|
|
378
|
+
stickyPhase(root)
|
|
379
|
+
|
|
380
|
+
// Phase 2.6: Screen rect calculation
|
|
381
|
+
{
|
|
382
|
+
using _screenRect = render.span("screenRect")
|
|
383
|
+
screenRectPhase(root)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Phase 2.7: Notify layout subscribers
|
|
387
|
+
if (!skipLayoutNotifications) {
|
|
388
|
+
using _notify = render.span("notify")
|
|
389
|
+
notifyLayoutSubscribers(root)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Phase 3: Content render (adapter-aware)
|
|
393
|
+
let buffer: RenderBuffer
|
|
394
|
+
{
|
|
395
|
+
using _content = render.span("content")
|
|
396
|
+
const t3 = Date.now()
|
|
397
|
+
buffer = contentPhaseAdapter(root)
|
|
398
|
+
log.debug?.(`content: ${Date.now() - t3}ms`)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Phase 4: Flush via adapter
|
|
402
|
+
let output: string | void
|
|
403
|
+
{
|
|
404
|
+
using outputSpan = render.span("output")
|
|
405
|
+
const t4 = Date.now()
|
|
406
|
+
output = adapter.flush(buffer, prevBuffer)
|
|
407
|
+
if (typeof output === "string") {
|
|
408
|
+
outputSpan.spanData.bytes = output.length
|
|
409
|
+
}
|
|
410
|
+
log.debug?.(`output: ${Date.now() - t4}ms`)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
log.debug?.(`total pipeline: ${Date.now() - start}ms`)
|
|
414
|
+
|
|
415
|
+
return { output, buffer }
|
|
416
|
+
}
|