@tooee/renderers 0.1.4 → 0.1.6
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/dist/CodeView.d.ts +5 -1
- package/dist/CodeView.d.ts.map +1 -1
- package/dist/CodeView.js +28 -61
- package/dist/CodeView.js.map +1 -1
- package/dist/MarkdownView.d.ts +6 -2
- package/dist/MarkdownView.d.ts.map +1 -1
- package/dist/MarkdownView.js +29 -24
- package/dist/MarkdownView.js.map +1 -1
- package/dist/RowDocumentRenderable.d.ts +87 -0
- package/dist/RowDocumentRenderable.d.ts.map +1 -0
- package/dist/RowDocumentRenderable.js +428 -0
- package/dist/RowDocumentRenderable.js.map +1 -0
- package/dist/Table.d.ts +5 -1
- package/dist/Table.d.ts.map +1 -1
- package/dist/Table.js +24 -31
- package/dist/Table.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/row-document.d.ts +7 -0
- package/dist/row-document.d.ts.map +1 -0
- package/dist/row-document.js +4 -0
- package/dist/row-document.js.map +1 -0
- package/package.json +2 -2
- package/src/CodeView.tsx +39 -81
- package/src/MarkdownView.tsx +46 -51
- package/src/RowDocumentRenderable.ts +607 -0
- package/src/Table.tsx +37 -35
- package/src/index.ts +6 -0
- package/src/row-document.ts +10 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type RenderContext,
|
|
3
|
+
type LineInfo,
|
|
4
|
+
type LineInfoProvider,
|
|
5
|
+
type Renderable,
|
|
6
|
+
ScrollBoxRenderable,
|
|
7
|
+
type ScrollBoxOptions,
|
|
8
|
+
RGBA,
|
|
9
|
+
} from "@opentui/core"
|
|
10
|
+
import type { OptimizedBuffer } from "@opentui/core"
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Types
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface RowDocumentPalette {
|
|
17
|
+
gutterFg?: string
|
|
18
|
+
gutterBg?: string
|
|
19
|
+
cursorBg?: string
|
|
20
|
+
selectionBg?: string
|
|
21
|
+
matchBg?: string
|
|
22
|
+
currentMatchBg?: string
|
|
23
|
+
toggledBg?: string
|
|
24
|
+
cursorSign?: string
|
|
25
|
+
cursorSignFg?: string
|
|
26
|
+
matchSign?: string
|
|
27
|
+
matchSignFg?: string
|
|
28
|
+
currentMatchSignFg?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RowDocumentDecorations {
|
|
32
|
+
cursorRow?: number
|
|
33
|
+
selection?: { start: number; end: number } | null
|
|
34
|
+
matchingRows?: Set<number>
|
|
35
|
+
currentMatchRow?: number
|
|
36
|
+
toggledRows?: Set<number>
|
|
37
|
+
signs?: Map<number, { text: string; fg?: string }>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RowDocumentOptions extends ScrollBoxOptions {
|
|
41
|
+
mode?: "auto" | "multi" | "provider"
|
|
42
|
+
|
|
43
|
+
// Gutter
|
|
44
|
+
showGutter?: boolean
|
|
45
|
+
showLineNumbers?: boolean
|
|
46
|
+
lineNumberStart?: number
|
|
47
|
+
signColumnWidth?: number
|
|
48
|
+
gutterPaddingRight?: number
|
|
49
|
+
|
|
50
|
+
// Multi-child: skip N leading children that aren't logical rows
|
|
51
|
+
rowChildOffset?: number
|
|
52
|
+
|
|
53
|
+
// Colors
|
|
54
|
+
palette?: RowDocumentPalette
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Provider detection
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function isRowContentProvider(x: unknown): x is LineInfoProvider {
|
|
62
|
+
return (
|
|
63
|
+
!!x &&
|
|
64
|
+
typeof x === "object" &&
|
|
65
|
+
"lineInfo" in x &&
|
|
66
|
+
"lineCount" in x &&
|
|
67
|
+
"virtualLineCount" in x
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Color cache — avoids re-parsing hex strings every frame
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
const colorCache = new Map<string, RGBA>()
|
|
76
|
+
|
|
77
|
+
function cachedColor(hex: string): RGBA {
|
|
78
|
+
let c = colorCache.get(hex)
|
|
79
|
+
if (!c) {
|
|
80
|
+
c = RGBA.fromHex(hex)
|
|
81
|
+
colorCache.set(hex, c)
|
|
82
|
+
}
|
|
83
|
+
return c
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// RowDocumentRenderable
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
export class RowDocumentRenderable extends ScrollBoxRenderable {
|
|
91
|
+
// -- Options --
|
|
92
|
+
private _mode: "auto" | "multi" | "provider"
|
|
93
|
+
private _showGutter: boolean
|
|
94
|
+
private _showLineNumbers: boolean
|
|
95
|
+
private _lineNumberStart: number
|
|
96
|
+
private _signColumnWidth: number
|
|
97
|
+
private _gutterPaddingRight: number
|
|
98
|
+
private _rowChildOffset: number
|
|
99
|
+
private _palette: Required<RowDocumentPalette>
|
|
100
|
+
|
|
101
|
+
// -- Decorations --
|
|
102
|
+
private _deco: RowDocumentDecorations = {}
|
|
103
|
+
|
|
104
|
+
// -- Geometry arrays --
|
|
105
|
+
private _rowVirtualStarts: number[] = []
|
|
106
|
+
private _rowVirtualHeights: number[] = []
|
|
107
|
+
private _virtualRowToRow: number[] = []
|
|
108
|
+
private _virtualRowWraps: number[] = []
|
|
109
|
+
private _rowCount = 0
|
|
110
|
+
private _lastGeometryHash = ""
|
|
111
|
+
|
|
112
|
+
constructor(ctx: RenderContext, options: RowDocumentOptions) {
|
|
113
|
+
super(ctx, options)
|
|
114
|
+
|
|
115
|
+
this._mode = options.mode ?? "auto"
|
|
116
|
+
this._showGutter = options.showGutter ?? true
|
|
117
|
+
this._showLineNumbers = options.showLineNumbers ?? true
|
|
118
|
+
this._lineNumberStart = options.lineNumberStart ?? 1
|
|
119
|
+
this._signColumnWidth = options.signColumnWidth ?? 0
|
|
120
|
+
this._gutterPaddingRight = options.gutterPaddingRight ?? 1
|
|
121
|
+
this._rowChildOffset = options.rowChildOffset ?? 0
|
|
122
|
+
|
|
123
|
+
const p = options.palette ?? {}
|
|
124
|
+
this._palette = {
|
|
125
|
+
gutterFg: p.gutterFg ?? "#6e7681",
|
|
126
|
+
gutterBg: p.gutterBg ?? "#0d1117",
|
|
127
|
+
cursorBg: p.cursorBg ?? "#1c2128",
|
|
128
|
+
selectionBg: p.selectionBg ?? "#1f3a5f",
|
|
129
|
+
matchBg: p.matchBg ?? "#3b2e00",
|
|
130
|
+
currentMatchBg: p.currentMatchBg ?? "#5a4a00",
|
|
131
|
+
toggledBg: p.toggledBg ?? "#1c2128",
|
|
132
|
+
cursorSign: p.cursorSign ?? "▸",
|
|
133
|
+
cursorSignFg: p.cursorSignFg ?? "#58a6ff",
|
|
134
|
+
matchSign: p.matchSign ?? "●",
|
|
135
|
+
matchSignFg: p.matchSignFg ?? "#d29922",
|
|
136
|
+
currentMatchSignFg: p.currentMatchSignFg ?? "#f0c000",
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// -----------------------------------------------------------------------
|
|
141
|
+
// Decorations
|
|
142
|
+
// -----------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
setDecorations(decorations: RowDocumentDecorations): void {
|
|
145
|
+
this._deco = decorations
|
|
146
|
+
this.requestRender()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// -----------------------------------------------------------------------
|
|
150
|
+
// Gutter config setters
|
|
151
|
+
// -----------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
set showGutter(value: boolean) {
|
|
154
|
+
if (this._showGutter !== value) {
|
|
155
|
+
this._showGutter = value
|
|
156
|
+
this._applyGutterPadding()
|
|
157
|
+
this.requestRender()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
set showLineNumbers(value: boolean) {
|
|
162
|
+
if (this._showLineNumbers !== value) {
|
|
163
|
+
this._showLineNumbers = value
|
|
164
|
+
this._applyGutterPadding()
|
|
165
|
+
this.requestRender()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// -----------------------------------------------------------------------
|
|
170
|
+
// Geometry — public API
|
|
171
|
+
// -----------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
get rowCount(): number {
|
|
174
|
+
return this._rowCount
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get virtualRowCount(): number {
|
|
178
|
+
return this._virtualRowToRow.length
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getRowMetrics(
|
|
182
|
+
row: number,
|
|
183
|
+
): { row: number; virtualTop: number; virtualHeight: number } | null {
|
|
184
|
+
if (row < 0 || row >= this._rowCount) return null
|
|
185
|
+
return {
|
|
186
|
+
row,
|
|
187
|
+
virtualTop: this._rowVirtualStarts[row],
|
|
188
|
+
virtualHeight: this._rowVirtualHeights[row],
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
getRowAtVirtualY(y: number): number {
|
|
193
|
+
const clamped = Math.max(0, Math.min(y, this._virtualRowToRow.length - 1))
|
|
194
|
+
const row = this._virtualRowToRow[clamped]
|
|
195
|
+
if (row != null && row >= 0) return row
|
|
196
|
+
// Gap row — search backward for nearest valid row
|
|
197
|
+
for (let i = clamped - 1; i >= 0; i--) {
|
|
198
|
+
const r = this._virtualRowToRow[i]
|
|
199
|
+
if (r != null && r >= 0) return r
|
|
200
|
+
}
|
|
201
|
+
return 0
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getVisibleRange(): {
|
|
205
|
+
virtualTop: number
|
|
206
|
+
virtualBottom: number
|
|
207
|
+
firstRow: number
|
|
208
|
+
lastRow: number
|
|
209
|
+
} {
|
|
210
|
+
const top = Math.floor(this.scrollTop)
|
|
211
|
+
const bottom = top + this.viewport.height
|
|
212
|
+
return {
|
|
213
|
+
virtualTop: top,
|
|
214
|
+
virtualBottom: bottom,
|
|
215
|
+
firstRow: this.getRowAtVirtualY(top),
|
|
216
|
+
lastRow: this.getRowAtVirtualY(bottom - 1),
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// -----------------------------------------------------------------------
|
|
221
|
+
// scrollToRow
|
|
222
|
+
// -----------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
scrollToRow(
|
|
225
|
+
row: number,
|
|
226
|
+
align: "nearest" | "start" | "center" | "end" = "nearest",
|
|
227
|
+
): void {
|
|
228
|
+
const metrics = this.getRowMetrics(row)
|
|
229
|
+
if (!metrics) return
|
|
230
|
+
|
|
231
|
+
const vpHeight = this.viewport.height
|
|
232
|
+
const { virtualTop, virtualHeight } = metrics
|
|
233
|
+
|
|
234
|
+
let target = this.scrollTop
|
|
235
|
+
|
|
236
|
+
switch (align) {
|
|
237
|
+
case "start":
|
|
238
|
+
target = virtualTop
|
|
239
|
+
break
|
|
240
|
+
case "center":
|
|
241
|
+
target = virtualTop - (vpHeight - virtualHeight) / 2
|
|
242
|
+
break
|
|
243
|
+
case "end":
|
|
244
|
+
target = virtualTop + virtualHeight - vpHeight
|
|
245
|
+
break
|
|
246
|
+
case "nearest": {
|
|
247
|
+
if (virtualTop < this.scrollTop) {
|
|
248
|
+
target = virtualTop
|
|
249
|
+
} else if (virtualTop + virtualHeight > this.scrollTop + vpHeight) {
|
|
250
|
+
target = virtualTop + virtualHeight - vpHeight
|
|
251
|
+
}
|
|
252
|
+
break
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// When scrolling to the first row, include header content (pre-offset children)
|
|
257
|
+
if (row === 0 && this._rowChildOffset > 0) {
|
|
258
|
+
target = Math.min(target, 0)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// When the next row is the last logical row (e.g. bottom border),
|
|
262
|
+
// extend the scroll to include it
|
|
263
|
+
if (row + 1 === this._rowCount - 1) {
|
|
264
|
+
const next = this.getRowMetrics(row + 1)
|
|
265
|
+
if (next) {
|
|
266
|
+
const bottomEdge = next.virtualTop + next.virtualHeight
|
|
267
|
+
if (bottomEdge > target + vpHeight) {
|
|
268
|
+
target = bottomEdge - vpHeight
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this.scrollTop = Math.max(0, Math.round(target))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// -----------------------------------------------------------------------
|
|
277
|
+
// Rendering
|
|
278
|
+
// -----------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
protected renderSelf(buffer: OptimizedBuffer): void {
|
|
281
|
+
// Let ScrollBox do its own rendering first
|
|
282
|
+
super.renderSelf(buffer)
|
|
283
|
+
|
|
284
|
+
// Recompute geometry every frame (cheap — just iterating children)
|
|
285
|
+
this._computeGeometry()
|
|
286
|
+
|
|
287
|
+
// Apply gutter padding to content area
|
|
288
|
+
this._applyGutterPadding()
|
|
289
|
+
|
|
290
|
+
// Paint decorations and gutter on top
|
|
291
|
+
this._paintDecorations(buffer)
|
|
292
|
+
if (this._showGutter) {
|
|
293
|
+
this._paintGutter(buffer)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// -----------------------------------------------------------------------
|
|
298
|
+
// Geometry computation
|
|
299
|
+
// -----------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
private _detectMode(): "multi" | "provider" {
|
|
302
|
+
if (this._mode === "multi") return "multi"
|
|
303
|
+
if (this._mode === "provider") return "provider"
|
|
304
|
+
|
|
305
|
+
// Auto-detect: single child with LineInfoProvider → provider mode
|
|
306
|
+
const children = this.getChildren()
|
|
307
|
+
if (children.length === 1 && isRowContentProvider(children[0])) {
|
|
308
|
+
return "provider"
|
|
309
|
+
}
|
|
310
|
+
return "multi"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private _computeGeometry(): void {
|
|
314
|
+
const mode = this._detectMode()
|
|
315
|
+
if (mode === "provider") {
|
|
316
|
+
this._computeFromProvider()
|
|
317
|
+
} else {
|
|
318
|
+
this._computeFromChildren()
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private _computeFromChildren(): void {
|
|
323
|
+
const children = this.getChildren()
|
|
324
|
+
const offset = this._rowChildOffset
|
|
325
|
+
const contentY = this.content.y
|
|
326
|
+
const totalHeight = this.scrollHeight
|
|
327
|
+
|
|
328
|
+
if (totalHeight === 0 || children.length <= offset) {
|
|
329
|
+
this._finishGeometry([], [], [], [], 0)
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const rowCount = children.length - offset
|
|
334
|
+
const rowVirtualStarts: number[] = []
|
|
335
|
+
const rowVirtualHeights: number[] = []
|
|
336
|
+
const virtualRowToRow: number[] = new Array(totalHeight).fill(-1)
|
|
337
|
+
const virtualRowWraps: number[] = new Array(totalHeight).fill(0)
|
|
338
|
+
|
|
339
|
+
for (let i = offset; i < children.length; i++) {
|
|
340
|
+
const row = i - offset
|
|
341
|
+
const child = children[i]
|
|
342
|
+
const childStart = child.y - contentY
|
|
343
|
+
const h = Math.max(1, child.height)
|
|
344
|
+
|
|
345
|
+
rowVirtualStarts[row] = childStart
|
|
346
|
+
rowVirtualHeights[row] = h
|
|
347
|
+
|
|
348
|
+
for (let r = 0; r < h; r++) {
|
|
349
|
+
const vRow = childStart + r
|
|
350
|
+
if (vRow >= 0 && vRow < totalHeight) {
|
|
351
|
+
virtualRowToRow[vRow] = row
|
|
352
|
+
virtualRowWraps[vRow] = r === 0 ? 0 : 1
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this._finishGeometry(
|
|
358
|
+
rowVirtualStarts,
|
|
359
|
+
rowVirtualHeights,
|
|
360
|
+
virtualRowToRow,
|
|
361
|
+
virtualRowWraps,
|
|
362
|
+
rowCount,
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private _computeFromProvider(): void {
|
|
367
|
+
const children = this.getChildren()
|
|
368
|
+
if (children.length === 0) return
|
|
369
|
+
|
|
370
|
+
const provider = children[0] as unknown as LineInfoProvider
|
|
371
|
+
if (!isRowContentProvider(provider)) return
|
|
372
|
+
|
|
373
|
+
const info = provider.lineInfo
|
|
374
|
+
const { lineSources, lineWraps } = info
|
|
375
|
+
|
|
376
|
+
const rowVirtualStarts: number[] = []
|
|
377
|
+
const rowVirtualHeights: number[] = []
|
|
378
|
+
const virtualRowToRow: number[] = []
|
|
379
|
+
const virtualRowWraps: number[] = []
|
|
380
|
+
|
|
381
|
+
let currentRow = -1
|
|
382
|
+
let rowCount = 0
|
|
383
|
+
|
|
384
|
+
for (let v = 0; v < lineSources.length; v++) {
|
|
385
|
+
const row = lineSources[v]
|
|
386
|
+
virtualRowToRow[v] = row
|
|
387
|
+
virtualRowWraps[v] = lineWraps[v]
|
|
388
|
+
|
|
389
|
+
if (row !== currentRow) {
|
|
390
|
+
// New logical row
|
|
391
|
+
if (currentRow >= 0) {
|
|
392
|
+
rowVirtualHeights[currentRow] = v - rowVirtualStarts[currentRow]
|
|
393
|
+
}
|
|
394
|
+
rowVirtualStarts[row] = v
|
|
395
|
+
currentRow = row
|
|
396
|
+
rowCount = Math.max(rowCount, row + 1)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Finalize last row
|
|
401
|
+
if (currentRow >= 0) {
|
|
402
|
+
rowVirtualHeights[currentRow] =
|
|
403
|
+
lineSources.length - rowVirtualStarts[currentRow]
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this._finishGeometry(
|
|
407
|
+
rowVirtualStarts,
|
|
408
|
+
rowVirtualHeights,
|
|
409
|
+
virtualRowToRow,
|
|
410
|
+
virtualRowWraps,
|
|
411
|
+
rowCount,
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private _finishGeometry(
|
|
416
|
+
rowVirtualStarts: number[],
|
|
417
|
+
rowVirtualHeights: number[],
|
|
418
|
+
virtualRowToRow: number[],
|
|
419
|
+
virtualRowWraps: number[],
|
|
420
|
+
rowCount: number,
|
|
421
|
+
): void {
|
|
422
|
+
// Change detection via hash
|
|
423
|
+
const hash = rowVirtualStarts.join(",")
|
|
424
|
+
if (hash !== this._lastGeometryHash) {
|
|
425
|
+
this._rowVirtualStarts = rowVirtualStarts
|
|
426
|
+
this._rowVirtualHeights = rowVirtualHeights
|
|
427
|
+
this._virtualRowToRow = virtualRowToRow
|
|
428
|
+
this._virtualRowWraps = virtualRowWraps
|
|
429
|
+
this._rowCount = rowCount
|
|
430
|
+
this._lastGeometryHash = hash
|
|
431
|
+
this.emit("row-geometry-change")
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// -----------------------------------------------------------------------
|
|
436
|
+
// Gutter width calculation and padding
|
|
437
|
+
// -----------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
private _computeGutterWidth(): number {
|
|
440
|
+
if (!this._showGutter) return 0
|
|
441
|
+
|
|
442
|
+
let width = 0
|
|
443
|
+
|
|
444
|
+
if (this._showLineNumbers) {
|
|
445
|
+
const maxLineNum = this._lineNumberStart + this._rowCount - 1
|
|
446
|
+
width += Math.max(String(maxLineNum).length, 1)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
width += this._signColumnWidth
|
|
450
|
+
width += this._gutterPaddingRight
|
|
451
|
+
|
|
452
|
+
return width
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private _applyGutterPadding(): void {
|
|
456
|
+
const gutterWidth = this._computeGutterWidth()
|
|
457
|
+
this.content.paddingLeft = gutterWidth
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// -----------------------------------------------------------------------
|
|
461
|
+
// Decoration painting (row backgrounds)
|
|
462
|
+
// -----------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
private _paintDecorations(buffer: OptimizedBuffer): void {
|
|
465
|
+
const {
|
|
466
|
+
cursorRow,
|
|
467
|
+
selection,
|
|
468
|
+
matchingRows,
|
|
469
|
+
currentMatchRow,
|
|
470
|
+
toggledRows,
|
|
471
|
+
} = this._deco
|
|
472
|
+
|
|
473
|
+
const hasDecorations =
|
|
474
|
+
cursorRow != null ||
|
|
475
|
+
selection != null ||
|
|
476
|
+
(matchingRows && matchingRows.size > 0) ||
|
|
477
|
+
currentMatchRow != null ||
|
|
478
|
+
(toggledRows && toggledRows.size > 0)
|
|
479
|
+
|
|
480
|
+
if (!hasDecorations) return
|
|
481
|
+
|
|
482
|
+
const vpX = this.viewport.x
|
|
483
|
+
const vpY = this.viewport.y
|
|
484
|
+
const vpHeight = this.viewport.height
|
|
485
|
+
const vpWidth = this.viewport.width
|
|
486
|
+
const top = Math.floor(this.scrollTop)
|
|
487
|
+
|
|
488
|
+
for (let screenY = 0; screenY < vpHeight; screenY++) {
|
|
489
|
+
const vRow = top + screenY
|
|
490
|
+
if (vRow >= this._virtualRowToRow.length) break
|
|
491
|
+
|
|
492
|
+
const row = this._virtualRowToRow[vRow]
|
|
493
|
+
if (row < 0) continue // Skip gap rows (margins)
|
|
494
|
+
let bg: RGBA | null = null
|
|
495
|
+
|
|
496
|
+
// Priority order: currentMatch > match > selection > toggled > cursor
|
|
497
|
+
if (cursorRow != null && row === cursorRow) {
|
|
498
|
+
bg = cachedColor(this._palette.cursorBg)
|
|
499
|
+
}
|
|
500
|
+
if (toggledRows && toggledRows.has(row)) {
|
|
501
|
+
bg = cachedColor(this._palette.toggledBg)
|
|
502
|
+
}
|
|
503
|
+
if (selection && row >= selection.start && row <= selection.end) {
|
|
504
|
+
bg = cachedColor(this._palette.selectionBg)
|
|
505
|
+
}
|
|
506
|
+
if (matchingRows && matchingRows.has(row)) {
|
|
507
|
+
bg = cachedColor(this._palette.matchBg)
|
|
508
|
+
}
|
|
509
|
+
if (currentMatchRow != null && row === currentMatchRow) {
|
|
510
|
+
bg = cachedColor(this._palette.currentMatchBg)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (bg) {
|
|
514
|
+
buffer.fillRect(vpX, vpY + screenY, vpWidth, 1, bg)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// -----------------------------------------------------------------------
|
|
520
|
+
// Gutter painting
|
|
521
|
+
// -----------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
private _paintGutter(buffer: OptimizedBuffer): void {
|
|
524
|
+
const gutterWidth = this._computeGutterWidth()
|
|
525
|
+
if (gutterWidth === 0) return
|
|
526
|
+
|
|
527
|
+
const vpX = this.viewport.x
|
|
528
|
+
const vpY = this.viewport.y
|
|
529
|
+
const vpHeight = this.viewport.height
|
|
530
|
+
const top = Math.floor(this.scrollTop)
|
|
531
|
+
|
|
532
|
+
const gutterBg = cachedColor(this._palette.gutterBg)
|
|
533
|
+
const gutterFg = cachedColor(this._palette.gutterFg)
|
|
534
|
+
|
|
535
|
+
// Fill gutter background
|
|
536
|
+
buffer.fillRect(vpX, vpY, gutterWidth, vpHeight, gutterBg)
|
|
537
|
+
|
|
538
|
+
const lineNumWidth = this._showLineNumbers
|
|
539
|
+
? Math.max(String(this._lineNumberStart + this._rowCount - 1).length, 1)
|
|
540
|
+
: 0
|
|
541
|
+
|
|
542
|
+
for (let screenY = 0; screenY < vpHeight; screenY++) {
|
|
543
|
+
const vRow = top + screenY
|
|
544
|
+
if (vRow >= this._virtualRowToRow.length) break
|
|
545
|
+
|
|
546
|
+
const row = this._virtualRowToRow[vRow]
|
|
547
|
+
if (row < 0) continue // Skip gap rows (margins)
|
|
548
|
+
const isFirstLine = this._virtualRowWraps[vRow] === 0
|
|
549
|
+
|
|
550
|
+
if (!isFirstLine) continue
|
|
551
|
+
|
|
552
|
+
const drawX = vpX
|
|
553
|
+
let col = 0
|
|
554
|
+
|
|
555
|
+
// Line number
|
|
556
|
+
if (this._showLineNumbers) {
|
|
557
|
+
const lineNum = String(this._lineNumberStart + row)
|
|
558
|
+
const padded = lineNum.padStart(lineNumWidth, " ")
|
|
559
|
+
buffer.drawText(padded, drawX + col, vpY + screenY, gutterFg, gutterBg)
|
|
560
|
+
col += lineNumWidth
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Sign column
|
|
564
|
+
if (this._signColumnWidth > 0) {
|
|
565
|
+
const { cursorRow, matchingRows, currentMatchRow } = this._deco
|
|
566
|
+
const customSign = this._deco.signs?.get(row)
|
|
567
|
+
|
|
568
|
+
if (customSign) {
|
|
569
|
+
const signFg = customSign.fg
|
|
570
|
+
? cachedColor(customSign.fg)
|
|
571
|
+
: gutterFg
|
|
572
|
+
buffer.drawText(
|
|
573
|
+
customSign.text.slice(0, this._signColumnWidth),
|
|
574
|
+
drawX + col,
|
|
575
|
+
vpY + screenY,
|
|
576
|
+
signFg,
|
|
577
|
+
gutterBg,
|
|
578
|
+
)
|
|
579
|
+
} else if (cursorRow != null && row === cursorRow) {
|
|
580
|
+
buffer.drawText(
|
|
581
|
+
this._palette.cursorSign,
|
|
582
|
+
drawX + col,
|
|
583
|
+
vpY + screenY,
|
|
584
|
+
cachedColor(this._palette.cursorSignFg),
|
|
585
|
+
gutterBg,
|
|
586
|
+
)
|
|
587
|
+
} else if (currentMatchRow != null && row === currentMatchRow) {
|
|
588
|
+
buffer.drawText(
|
|
589
|
+
this._palette.matchSign,
|
|
590
|
+
drawX + col,
|
|
591
|
+
vpY + screenY,
|
|
592
|
+
cachedColor(this._palette.currentMatchSignFg),
|
|
593
|
+
gutterBg,
|
|
594
|
+
)
|
|
595
|
+
} else if (matchingRows && matchingRows.has(row)) {
|
|
596
|
+
buffer.drawText(
|
|
597
|
+
this._palette.matchSign,
|
|
598
|
+
drawX + col,
|
|
599
|
+
vpY + screenY,
|
|
600
|
+
cachedColor(this._palette.matchSignFg),
|
|
601
|
+
gutterBg,
|
|
602
|
+
)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
package/src/Table.tsx
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { useTerminalDimensions } from "@opentui/react"
|
|
2
2
|
import { useTheme } from "@tooee/themes"
|
|
3
|
+
import { useEffect, useRef, type RefObject } from "react"
|
|
3
4
|
import type { ColumnDef, TableRow } from "./table-types.js"
|
|
5
|
+
import type { RowDocumentRenderable, RowDocumentPalette, RowDocumentDecorations } from "./RowDocumentRenderable.js"
|
|
6
|
+
import "./row-document.js"
|
|
4
7
|
|
|
5
8
|
export interface TableProps {
|
|
6
9
|
columns: ColumnDef[]
|
|
@@ -19,6 +22,7 @@ export interface TableProps {
|
|
|
19
22
|
matchingRows?: Set<number>
|
|
20
23
|
currentMatchRow?: number
|
|
21
24
|
toggledRows?: Set<number>
|
|
25
|
+
docRef?: RefObject<RowDocumentRenderable | null>
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
const PADDING = 1
|
|
@@ -171,6 +175,7 @@ export function Table({
|
|
|
171
175
|
matchingRows,
|
|
172
176
|
currentMatchRow,
|
|
173
177
|
toggledRows,
|
|
178
|
+
docRef,
|
|
174
179
|
}: TableProps) {
|
|
175
180
|
const { theme } = useTheme()
|
|
176
181
|
const { width: terminalWidth } = useTerminalDimensions()
|
|
@@ -203,50 +208,47 @@ export function Table({
|
|
|
203
208
|
const headerLine = buildDataLine(headers, colWidths, alignments)
|
|
204
209
|
const dataLines = normalizedRows.map((row) => buildDataLine(row, colWidths, alignments))
|
|
205
210
|
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
const isSelected =
|
|
209
|
-
selectionStart != null &&
|
|
210
|
-
selectionEnd != null &&
|
|
211
|
-
rowIndex >= selectionStart &&
|
|
212
|
-
rowIndex <= selectionEnd
|
|
213
|
-
const isMatch = matchingRows?.has(rowIndex)
|
|
214
|
-
const isCurrentMatch = currentMatchRow === rowIndex
|
|
215
|
-
const isToggled = toggledRows?.has(rowIndex)
|
|
211
|
+
const internalRef = useRef<RowDocumentRenderable>(null)
|
|
212
|
+
const effectiveRef = docRef ?? internalRef
|
|
216
213
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (isToggled && !isCursor && !isSelected) {
|
|
225
|
-
bg = theme.backgroundPanel
|
|
226
|
-
}
|
|
227
|
-
if (isCursor) {
|
|
228
|
-
bg = theme.cursorLine
|
|
229
|
-
}
|
|
214
|
+
const palette: RowDocumentPalette = {
|
|
215
|
+
cursorBg: theme.cursorLine,
|
|
216
|
+
selectionBg: theme.selection,
|
|
217
|
+
matchBg: theme.warning,
|
|
218
|
+
currentMatchBg: theme.primary,
|
|
219
|
+
toggledBg: theme.backgroundPanel,
|
|
220
|
+
}
|
|
230
221
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
const decorations: RowDocumentDecorations = {
|
|
224
|
+
cursorRow: cursor,
|
|
225
|
+
selection: selectionStart != null && selectionEnd != null
|
|
226
|
+
? { start: selectionStart, end: selectionEnd }
|
|
227
|
+
: null,
|
|
228
|
+
matchingRows: matchingRows,
|
|
229
|
+
currentMatchRow: currentMatchRow,
|
|
230
|
+
toggledRows: toggledRows,
|
|
234
231
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
232
|
+
effectiveRef.current?.setDecorations(decorations)
|
|
233
|
+
}, [cursor, selectionStart, selectionEnd, matchingRows, currentMatchRow, toggledRows])
|
|
238
234
|
|
|
239
235
|
return (
|
|
240
|
-
<
|
|
236
|
+
<row-document
|
|
237
|
+
ref={effectiveRef}
|
|
238
|
+
mode="multi"
|
|
239
|
+
rowChildOffset={3}
|
|
240
|
+
showGutter={false}
|
|
241
|
+
palette={palette}
|
|
242
|
+
style={{ flexGrow: 1, marginLeft: 1, marginRight: 1, marginBottom: 1 }}
|
|
243
|
+
>
|
|
241
244
|
<text content={topBorder} fg={theme.border} />
|
|
242
245
|
<text content={headerLine} fg={theme.primary} />
|
|
243
246
|
<text content={headerSep} fg={theme.border} />
|
|
244
|
-
{dataLines.map((line, i) =>
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
})}
|
|
247
|
+
{dataLines.map((line, i) => (
|
|
248
|
+
<text key={i} content={line} fg={theme.text} />
|
|
249
|
+
))}
|
|
248
250
|
<text content={bottomBorder} fg={theme.border} />
|
|
249
|
-
</
|
|
251
|
+
</row-document>
|
|
250
252
|
)
|
|
251
253
|
}
|
|
252
254
|
|