@opengeni/react 0.48.0 → 0.52.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.
Files changed (284) hide show
  1. package/README.md +90 -2
  2. package/dist/artifacts-document.d.ts +3 -0
  3. package/dist/artifacts-document.js +17 -0
  4. package/dist/artifacts-document.js.map +1 -0
  5. package/dist/artifacts-presentation.d.ts +3 -0
  6. package/dist/artifacts-presentation.js +17 -0
  7. package/dist/artifacts-presentation.js.map +1 -0
  8. package/dist/artifacts-spreadsheet.d.ts +3 -0
  9. package/dist/artifacts-spreadsheet.js +17 -0
  10. package/dist/artifacts-spreadsheet.js.map +1 -0
  11. package/dist/artifacts.d.ts +2 -0
  12. package/dist/artifacts.js +170 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/chunk-6BBWWOOU.js +82 -0
  15. package/dist/chunk-6BBWWOOU.js.map +1 -0
  16. package/dist/{chunk-FWXGSNXT.js → chunk-724BW4TW.js} +585 -34
  17. package/dist/chunk-724BW4TW.js.map +1 -0
  18. package/dist/chunk-7I2KB3OK.js +3895 -0
  19. package/dist/chunk-7I2KB3OK.js.map +1 -0
  20. package/dist/chunk-7WCLAEFV.js +2828 -0
  21. package/dist/chunk-7WCLAEFV.js.map +1 -0
  22. package/dist/{chunk-I4MPFDRP.js → chunk-FWS3KC3Z.js} +3 -3
  23. package/dist/{chunk-EKZH6IDG.js → chunk-HDBG7XGO.js} +62 -1
  24. package/dist/chunk-HDBG7XGO.js.map +1 -0
  25. package/dist/chunk-HUIKIDM5.js +2148 -0
  26. package/dist/chunk-HUIKIDM5.js.map +1 -0
  27. package/dist/{chunk-XCNTXWBB.js → chunk-JAGDUCZQ.js} +854 -340
  28. package/dist/chunk-JAGDUCZQ.js.map +1 -0
  29. package/dist/{chunk-LHLYWG5R.js → chunk-JR3QFY6B.js} +413 -37
  30. package/dist/chunk-JR3QFY6B.js.map +1 -0
  31. package/dist/chunk-JVYQRRC4.js +53 -0
  32. package/dist/chunk-JVYQRRC4.js.map +1 -0
  33. package/dist/chunk-KCGXPP5V.js +157 -0
  34. package/dist/chunk-KCGXPP5V.js.map +1 -0
  35. package/dist/chunk-PEI6RANI.js +3234 -0
  36. package/dist/chunk-PEI6RANI.js.map +1 -0
  37. package/dist/{chunk-NJ7M2B6Z.js → chunk-ROIRTM37.js} +18 -10
  38. package/dist/{chunk-NJ7M2B6Z.js.map → chunk-ROIRTM37.js.map} +1 -1
  39. package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
  40. package/dist/chunk-WSK7G5LE.js.map +1 -0
  41. package/dist/{chunk-OL6QVXGF.js → chunk-X4S56Y2H.js} +27 -7
  42. package/dist/{chunk-OL6QVXGF.js.map → chunk-X4S56Y2H.js.map} +1 -1
  43. package/dist/client.d.ts +7 -1
  44. package/dist/clipboard.d.ts +1 -0
  45. package/dist/clipboard.js +7 -0
  46. package/dist/clipboard.js.map +1 -0
  47. package/dist/commands/index.d.ts +3 -3
  48. package/dist/commands/registry.d.ts +1 -1
  49. package/dist/commands/types.d.ts +1 -1
  50. package/dist/components/approval-surface.d.ts +2 -2
  51. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  52. package/dist/components/artifacts/document-editor.d.ts +172 -0
  53. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  54. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  55. package/dist/components/artifacts/editable-document.d.ts +24 -0
  56. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  57. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  58. package/dist/components/artifacts/index.d.ts +8 -0
  59. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  60. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  61. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  62. package/dist/components/browser-viewer.d.ts +29 -0
  63. package/dist/components/chat-composer.d.ts +8 -8
  64. package/dist/components/code-editor.d.ts +25 -3
  65. package/dist/components/command-palette.d.ts +2 -2
  66. package/dist/components/composer-transcription-control.d.ts +2 -2
  67. package/dist/components/composer.d.ts +17 -17
  68. package/dist/components/computer-viewer.d.ts +23 -0
  69. package/dist/components/copy-button.d.ts +2 -2
  70. package/dist/components/desktop-viewer.d.ts +25 -21
  71. package/dist/components/diff-view.d.ts +1 -1
  72. package/dist/components/enrollment-consent.d.ts +1 -1
  73. package/dist/components/enrollment-device-flow.d.ts +1 -1
  74. package/dist/components/file-browser.d.ts +2 -2
  75. package/dist/components/fleet-tile.d.ts +1 -1
  76. package/dist/components/generated-video-player.d.ts +10 -0
  77. package/dist/components/human-input-form.d.ts +1 -1
  78. package/dist/components/human-input-surface.d.ts +2 -2
  79. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  80. package/dist/components/machine-card.d.ts +2 -2
  81. package/dist/components/machine-dock-bar.d.ts +3 -3
  82. package/dist/components/machine-health-pill.d.ts +2 -2
  83. package/dist/components/machine-input-display.d.ts +1 -1
  84. package/dist/components/machine-metrics.d.ts +2 -2
  85. package/dist/components/machine-status-pill.d.ts +4 -4
  86. package/dist/components/machines/health.d.ts +1 -1
  87. package/dist/components/machines/machine-detail.d.ts +3 -3
  88. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  89. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  90. package/dist/components/machines/series.d.ts +2 -2
  91. package/dist/components/machines-dashboard.d.ts +2 -2
  92. package/dist/components/markdown.d.ts +1 -1
  93. package/dist/components/message-timeline.d.ts +12 -5
  94. package/dist/components/model-picker.d.ts +2 -2
  95. package/dist/components/model-policy-picker.d.ts +7 -7
  96. package/dist/components/pierre-diff.d.ts +1 -1
  97. package/dist/components/pierre-file.d.ts +1 -1
  98. package/dist/components/queue-draft-policy.d.ts +1 -1
  99. package/dist/components/queue-surface-implementation.d.ts +3 -3
  100. package/dist/components/queue-surface-state.d.ts +4 -4
  101. package/dist/components/queue-surface.d.ts +5 -5
  102. package/dist/components/sandbox-files.d.ts +3 -3
  103. package/dist/components/sandbox-terminal.d.ts +21 -4
  104. package/dist/components/sandbox-workspace.d.ts +25 -9
  105. package/dist/components/session-chrome.d.ts +4 -4
  106. package/dist/components/session-status.d.ts +2 -2
  107. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  108. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  109. package/dist/components/timeline-annotations.d.ts +13 -0
  110. package/dist/components/tooltip.d.ts +4 -4
  111. package/dist/components/user-message-body.d.ts +2 -2
  112. package/dist/components/workbench-changes.d.ts +2 -2
  113. package/dist/components/workspace-dock.d.ts +1 -1
  114. package/dist/composer.d.ts +11 -11
  115. package/dist/composer.js +5 -5
  116. package/dist/hooks/internal.d.ts +1 -1
  117. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  118. package/dist/hooks/use-available-models.d.ts +2 -2
  119. package/dist/hooks/use-billing-usage.d.ts +1 -1
  120. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  121. package/dist/hooks/use-browser-identities.d.ts +26 -0
  122. package/dist/hooks/use-browser-session.d.ts +28 -0
  123. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  124. package/dist/hooks/use-codex-accounts.d.ts +2 -2
  125. package/dist/hooks/use-composer.d.ts +36 -9
  126. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  127. package/dist/hooks/use-computer-session.d.ts +28 -0
  128. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  129. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  130. package/dist/hooks/use-environments.d.ts +1 -1
  131. package/dist/hooks/use-file-attachments.d.ts +1 -1
  132. package/dist/hooks/use-goal.d.ts +2 -2
  133. package/dist/hooks/use-human-input.d.ts +2 -2
  134. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  135. package/dist/hooks/use-machine-chip.d.ts +2 -2
  136. package/dist/hooks/use-machines.d.ts +4 -4
  137. package/dist/hooks/use-packs.d.ts +7 -2
  138. package/dist/hooks/use-rigs.d.ts +1 -1
  139. package/dist/hooks/use-sandbox-files.d.ts +6 -4
  140. package/dist/hooks/use-sandbox-git.d.ts +6 -5
  141. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  142. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  143. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  144. package/dist/hooks/use-session-control.d.ts +1 -1
  145. package/dist/hooks/use-session-events.d.ts +2 -2
  146. package/dist/hooks/use-session-lineage.d.ts +1 -1
  147. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  148. package/dist/hooks/use-session.d.ts +2 -2
  149. package/dist/hooks/use-slash-commands.d.ts +1 -1
  150. package/dist/hooks/use-terminal-stream.d.ts +8 -2
  151. package/dist/hooks/use-turn-queue.d.ts +2 -2
  152. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  153. package/dist/hooks/use-voice-input.d.ts +1 -1
  154. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  155. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  156. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  157. package/dist/hooks/use-workspaces.d.ts +1 -1
  158. package/dist/index.d.ts +158 -155
  159. package/dist/index.js +906 -462
  160. package/dist/index.js.map +1 -1
  161. package/dist/interaction.d.ts +23 -0
  162. package/dist/interaction.js +36 -0
  163. package/dist/interaction.js.map +1 -0
  164. package/dist/interactive-workbench-viewers-KHNOHWQE.js +13 -0
  165. package/dist/interactive-workbench-viewers-KHNOHWQE.js.map +1 -0
  166. package/dist/lib/relay-wire.d.ts +4 -0
  167. package/dist/lib/sandbox-liveness.d.ts +9 -0
  168. package/dist/lib/terminal-capability.d.ts +5 -0
  169. package/dist/lib/xterm-theme.d.ts +1 -1
  170. package/dist/machines.d.ts +30 -30
  171. package/dist/machines.js +3 -3
  172. package/dist/provider.d.ts +4 -4
  173. package/dist/{queue-surface-implementation-U5JFG7HT.js → queue-surface-implementation-2KVJTYXH.js} +100 -48
  174. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  175. package/dist/realtime/dropdown-menu.d.ts +8 -8
  176. package/dist/realtime/realtime-control.d.ts +6 -6
  177. package/dist/realtime.d.ts +4 -4
  178. package/dist/realtime.js +3 -3
  179. package/dist/session-context.d.ts +19 -1
  180. package/dist/session-ui.d.ts +14 -14
  181. package/dist/session-ui.js +5 -4
  182. package/dist/session.d.ts +28 -28
  183. package/dist/session.js +5 -5
  184. package/dist/timeline/activity-rail.d.ts +5 -3
  185. package/dist/timeline/disclosure-context.d.ts +1 -1
  186. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  187. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  188. package/dist/timeline/index.d.ts +19 -19
  189. package/dist/timeline/parsers.d.ts +4 -1
  190. package/dist/timeline/projection.d.ts +3 -3
  191. package/dist/timeline/registry.d.ts +8 -2
  192. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  193. package/dist/timeline/shared.d.ts +21 -10
  194. package/dist/timeline/tool-diff.d.ts +2 -2
  195. package/dist/timeline/tool-renderers.d.ts +1 -1
  196. package/dist/timeline/turn-summary.d.ts +3 -3
  197. package/dist/timeline/types.d.ts +39 -3
  198. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  199. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  200. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  201. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  202. package/package.json +36 -7
  203. package/src/artifacts-document.ts +31 -0
  204. package/src/artifacts-presentation.ts +36 -0
  205. package/src/artifacts-spreadsheet.ts +21 -0
  206. package/src/artifacts.ts +86 -0
  207. package/src/client.ts +87 -1
  208. package/src/clipboard.ts +2 -0
  209. package/src/components/artifacts/artifact-surface.tsx +93 -0
  210. package/src/components/artifacts/document-editor.tsx +2255 -0
  211. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  212. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  213. package/src/components/artifacts/editable-document.tsx +657 -0
  214. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  215. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  216. package/src/components/artifacts/index.ts +99 -0
  217. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  218. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  219. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  220. package/src/components/browser-viewer.tsx +1592 -0
  221. package/src/components/chat-composer.tsx +13 -0
  222. package/src/components/code-editor.tsx +89 -30
  223. package/src/components/composer-transcription-control.tsx +10 -6
  224. package/src/components/computer-viewer.tsx +1139 -0
  225. package/src/components/desktop-viewer.tsx +101 -73
  226. package/src/components/generated-video-player.tsx +89 -0
  227. package/src/components/interactive-workbench-viewers.ts +2 -0
  228. package/src/components/machine-input-display.ts +3 -0
  229. package/src/components/message-timeline.tsx +289 -32
  230. package/src/components/queue-surface-implementation.tsx +116 -60
  231. package/src/components/sandbox-files.tsx +2 -2
  232. package/src/components/sandbox-terminal.tsx +106 -41
  233. package/src/components/sandbox-workspace.tsx +215 -87
  234. package/src/components/session-chrome.tsx +53 -13
  235. package/src/components/timeline-annotation-selection.tsx +221 -0
  236. package/src/components/timeline-annotations-dialog.tsx +213 -0
  237. package/src/components/timeline-annotations.tsx +84 -0
  238. package/src/components/workbench-changes.tsx +188 -31
  239. package/src/hooks/use-attached-browsers.ts +152 -0
  240. package/src/hooks/use-browser-frame-stream.ts +316 -0
  241. package/src/hooks/use-browser-identities.ts +225 -0
  242. package/src/hooks/use-browser-session.ts +476 -0
  243. package/src/hooks/use-browser-sessions.ts +281 -0
  244. package/src/hooks/use-composer.ts +769 -24
  245. package/src/hooks/use-computer-frame-stream.ts +311 -0
  246. package/src/hooks/use-computer-session.ts +442 -0
  247. package/src/hooks/use-computer-sessions.ts +245 -0
  248. package/src/hooks/use-machine-chip.ts +5 -3
  249. package/src/hooks/use-machines.ts +33 -3
  250. package/src/hooks/use-packs.ts +51 -0
  251. package/src/hooks/use-sandbox-files.ts +98 -55
  252. package/src/hooks/use-sandbox-git.ts +93 -38
  253. package/src/hooks/use-sandbox-terminal.ts +3 -2
  254. package/src/hooks/use-session-capabilities.ts +59 -46
  255. package/src/hooks/use-terminal-stream.ts +102 -23
  256. package/src/hooks/use-video-artifact-playback.ts +28 -0
  257. package/src/hooks/use-workspace-edit.ts +2 -1
  258. package/src/index.ts +10 -0
  259. package/src/interaction.ts +71 -0
  260. package/src/lib/relay-wire.ts +4 -0
  261. package/src/lib/sandbox-liveness.ts +11 -0
  262. package/src/lib/terminal-capability.ts +10 -0
  263. package/src/session-context.ts +90 -0
  264. package/src/timeline/activity-rail.tsx +19 -3
  265. package/src/timeline/index.ts +3 -0
  266. package/src/timeline/parsers.ts +14 -1
  267. package/src/timeline/projection.ts +211 -14
  268. package/src/timeline/registry.ts +13 -1
  269. package/src/timeline/screenshot-lightbox.tsx +19 -9
  270. package/src/timeline/shared.tsx +56 -8
  271. package/src/timeline/tool-renderers.tsx +303 -50
  272. package/src/timeline/types.ts +55 -3
  273. package/styles/compiled.css +248 -15
  274. package/styles/index.css +15 -0
  275. package/styles/responsive.css +13 -1
  276. package/dist/chunk-EKZH6IDG.js.map +0 -1
  277. package/dist/chunk-FWXGSNXT.js.map +0 -1
  278. package/dist/chunk-LHLYWG5R.js.map +0 -1
  279. package/dist/chunk-O4VXOBGF.js.map +0 -1
  280. package/dist/chunk-XCNTXWBB.js.map +0 -1
  281. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  282. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  283. package/dist/queue-surface-implementation-U5JFG7HT.js.map +0 -1
  284. /package/dist/{chunk-I4MPFDRP.js.map → chunk-FWS3KC3Z.js.map} +0 -0
@@ -0,0 +1,799 @@
1
+ const TILE_SIZE = 256;
2
+ const MAX_CACHE_PHYSICAL_PIXELS = 32_000_000;
3
+ const MAX_CACHE_TILES = 96;
4
+ const MAX_DEVICE_PIXEL_RATIO = 4;
5
+ const MAX_AXIS_MARKS_PER_PAINT = 8_192;
6
+ const MAX_CANVAS_EDGE = 8_192;
7
+ const MAX_CANVAS_PIXELS = 16_000_000;
8
+ const MAX_TILES_PER_PAINT = 512;
9
+
10
+ export type SpreadsheetCanvasCellFormat = {
11
+ fill?: string | undefined;
12
+ font?:
13
+ | {
14
+ name?: string | undefined;
15
+ size?: number | undefined;
16
+ bold?: boolean | undefined;
17
+ italic?: boolean | undefined;
18
+ underline?: boolean | undefined;
19
+ color?: string | undefined;
20
+ }
21
+ | undefined;
22
+ horizontalAlignment?: "left" | "center" | "right" | "justify" | undefined;
23
+ verticalAlignment?: "top" | "center" | "bottom" | undefined;
24
+ wrapText?: boolean | undefined;
25
+ };
26
+
27
+ /** One sparse, already-authorized cell projected into the renderer. */
28
+ export type SpreadsheetCanvasCell = {
29
+ row: number;
30
+ col: number;
31
+ value: unknown;
32
+ formula?: string | null | undefined;
33
+ format: SpreadsheetCanvasCellFormat;
34
+ };
35
+
36
+ export type SpreadsheetCanvasCoverage = {
37
+ rowStart: number;
38
+ rowEnd: number;
39
+ columnStart: number;
40
+ columnEnd: number;
41
+ };
42
+
43
+ /**
44
+ * A bounded value projection. `coverage` is end-exclusive; omitting it means
45
+ * the source can answer for the entire sheet. A session-backed adapter can
46
+ * replace this object as viewport subscriptions advance without making the
47
+ * React layer or renderer own the canonical workbook model.
48
+ */
49
+ export type SpreadsheetCanvasProjection = {
50
+ sheetId: string;
51
+ generationId: string | null;
52
+ revision: string | number;
53
+ coverage?: SpreadsheetCanvasCoverage | undefined;
54
+ cells: SpreadsheetCanvasCellSource;
55
+ valueAt: (cell: SpreadsheetCanvasCell) => unknown;
56
+ };
57
+
58
+ export type SpreadsheetCanvasCellSource = {
59
+ forEachInRange: (
60
+ rowStart: number,
61
+ rowEnd: number,
62
+ columnStart: number,
63
+ columnEnd: number,
64
+ visit: (cell: SpreadsheetCanvasCell) => void,
65
+ ) => void;
66
+ };
67
+
68
+ export type SpreadsheetCanvasAxis = {
69
+ count: number;
70
+ total: number;
71
+ offsetAt: (index: number) => number;
72
+ sizeAt: (index: number) => number;
73
+ indexAtOffset: (offset: number) => number;
74
+ };
75
+
76
+ export type SpreadsheetCanvasSelection = {
77
+ top: number;
78
+ bottom: number;
79
+ left: number;
80
+ right: number;
81
+ focusRow: number;
82
+ focusColumn: number;
83
+ };
84
+
85
+ export type SpreadsheetCanvasTheme = {
86
+ background: string;
87
+ headerBackground: string;
88
+ border: string;
89
+ foreground: string;
90
+ mutedForeground: string;
91
+ accent: string;
92
+ error: string;
93
+ fontFamily: string;
94
+ fontSize: number;
95
+ headerFontSize: number;
96
+ signature: string;
97
+ };
98
+
99
+ export type SpreadsheetCanvasPaint = {
100
+ canvas: HTMLCanvasElement;
101
+ projection: SpreadsheetCanvasProjection;
102
+ rows: SpreadsheetCanvasAxis;
103
+ columns: SpreadsheetCanvasAxis;
104
+ selection: SpreadsheetCanvasSelection;
105
+ logicalScrollLeft: number;
106
+ logicalScrollTop: number;
107
+ viewportWidth: number;
108
+ viewportHeight: number;
109
+ rowHeaderWidth: number;
110
+ columnHeaderHeight: number;
111
+ dimensionRevision: string | number;
112
+ devicePixelRatio: number;
113
+ theme: SpreadsheetCanvasTheme;
114
+ };
115
+
116
+ export type SpreadsheetCanvasPaintStats = {
117
+ cacheSize: number;
118
+ paintedTiles: number;
119
+ reusedTiles: number;
120
+ uncachedTiles: number;
121
+ devicePixelRatio: number;
122
+ };
123
+
124
+ type IndexedRow = {
125
+ row: number;
126
+ cells: SpreadsheetCanvasCell[];
127
+ };
128
+
129
+ type Tile = {
130
+ canvas: HTMLCanvasElement;
131
+ lastUsed: number;
132
+ };
133
+
134
+ /**
135
+ * A row-major sparse index over stored worksheet cells. Empty cells never enter
136
+ * this structure, so a tile query costs O(log k + v), where v is the number of
137
+ * stored cells intersecting that tile.
138
+ */
139
+ export class SparseSpreadsheetCellIndex {
140
+ readonly size: number;
141
+ private readonly rows: IndexedRow[];
142
+
143
+ constructor(entries: Iterable<SpreadsheetCanvasCell>) {
144
+ const sorted = [...entries].sort((left, right) => left.row - right.row || left.col - right.col);
145
+ this.size = sorted.length;
146
+ this.rows = [];
147
+ for (const entry of sorted) {
148
+ const previous = this.rows.at(-1);
149
+ if (previous?.row === entry.row) previous.cells.push(entry);
150
+ else this.rows.push({ row: entry.row, cells: [entry] });
151
+ }
152
+ }
153
+
154
+ forEachInRange(
155
+ rowStart: number,
156
+ rowEnd: number,
157
+ columnStart: number,
158
+ columnEnd: number,
159
+ visit: (cell: SpreadsheetCanvasCell) => void,
160
+ ): void {
161
+ let rowIndex = lowerBound(this.rows, rowStart, (row) => row.row);
162
+ while (rowIndex < this.rows.length) {
163
+ const row = this.rows[rowIndex]!;
164
+ if (row.row >= rowEnd) break;
165
+ let cellIndex = lowerBound(row.cells, columnStart, (cell) => cell.col);
166
+ while (cellIndex < row.cells.length) {
167
+ const cell = row.cells[cellIndex]!;
168
+ if (cell.col >= columnEnd) break;
169
+ visit(cell);
170
+ cellIndex += 1;
171
+ }
172
+ rowIndex += 1;
173
+ }
174
+ }
175
+ }
176
+
177
+ function lowerBound<T>(
178
+ values: readonly T[],
179
+ target: number,
180
+ valueAt: (value: T) => number,
181
+ ): number {
182
+ let low = 0;
183
+ let high = values.length;
184
+ while (low < high) {
185
+ const middle = (low + high) >>> 1;
186
+ if (valueAt(values[middle]!) < target) low = middle + 1;
187
+ else high = middle;
188
+ }
189
+ return low;
190
+ }
191
+
192
+ function coverageContains(
193
+ coverage: SpreadsheetCanvasCoverage | undefined,
194
+ query: SpreadsheetCanvasCoverage,
195
+ ): boolean {
196
+ return (
197
+ coverage === undefined ||
198
+ (coverage.rowStart <= query.rowStart &&
199
+ coverage.rowEnd >= query.rowEnd &&
200
+ coverage.columnStart <= query.columnStart &&
201
+ coverage.columnEnd >= query.columnEnd)
202
+ );
203
+ }
204
+
205
+ function intersectCoverage(
206
+ query: SpreadsheetCanvasCoverage,
207
+ coverage: SpreadsheetCanvasCoverage | undefined,
208
+ ): SpreadsheetCanvasCoverage | null {
209
+ if (coverage === undefined) return query;
210
+ const intersection = {
211
+ rowStart: Math.max(query.rowStart, coverage.rowStart),
212
+ rowEnd: Math.min(query.rowEnd, coverage.rowEnd),
213
+ columnStart: Math.max(query.columnStart, coverage.columnStart),
214
+ columnEnd: Math.min(query.columnEnd, coverage.columnEnd),
215
+ };
216
+ return intersection.rowStart < intersection.rowEnd &&
217
+ intersection.columnStart < intersection.columnEnd
218
+ ? intersection
219
+ : null;
220
+ }
221
+
222
+ export function spreadsheetCanvasDevicePixelRatio(value: number): number {
223
+ if (!Number.isFinite(value) || value <= 0) return 1;
224
+ return Math.max(1, Math.min(MAX_DEVICE_PIXEL_RATIO, value));
225
+ }
226
+
227
+ /** Caps backing-store allocation while retaining the full CSS viewport. */
228
+ export function spreadsheetCanvasBackingScale(
229
+ width: number,
230
+ height: number,
231
+ devicePixelRatio: number,
232
+ ): number {
233
+ const safeWidth = finiteExtent(width);
234
+ const safeHeight = finiteExtent(height);
235
+ const requested = spreadsheetCanvasDevicePixelRatio(devicePixelRatio);
236
+ return Math.max(
237
+ Number.EPSILON,
238
+ Math.min(
239
+ requested,
240
+ MAX_CANVAS_EDGE / safeWidth,
241
+ MAX_CANVAS_EDGE / safeHeight,
242
+ Math.sqrt(MAX_CANVAS_PIXELS / (safeWidth * safeHeight)),
243
+ ),
244
+ );
245
+ }
246
+
247
+ /** Retained 2D tile renderer. Dynamic selection and headers are painted above cached body tiles. */
248
+ export class SpreadsheetCanvasRenderer {
249
+ private readonly tiles = new Map<string, Tile>();
250
+ private generation = "";
251
+ private clock = 0;
252
+
253
+ dispose(): void {
254
+ this.tiles.clear();
255
+ this.generation = "";
256
+ }
257
+
258
+ paint(input: SpreadsheetCanvasPaint): SpreadsheetCanvasPaintStats | null {
259
+ const context = safeCanvasContext(input.canvas);
260
+ if (!context) return null;
261
+
262
+ const width = finiteExtent(input.viewportWidth);
263
+ const height = finiteExtent(input.viewportHeight);
264
+ const dpr = spreadsheetCanvasBackingScale(width, height, input.devicePixelRatio);
265
+ const pixelWidth = Math.max(1, Math.min(MAX_CANVAS_EDGE, Math.round(width * dpr)));
266
+ const pixelHeight = Math.max(1, Math.min(MAX_CANVAS_EDGE, Math.round(height * dpr)));
267
+ if (input.canvas.width !== pixelWidth) input.canvas.width = pixelWidth;
268
+ if (input.canvas.height !== pixelHeight) input.canvas.height = pixelHeight;
269
+ input.canvas.style.width = `${width}px`;
270
+ input.canvas.style.height = `${height}px`;
271
+ context.setTransform(dpr, 0, 0, dpr, 0, 0);
272
+ context.clearRect(0, 0, width, height);
273
+
274
+ const generation = [
275
+ input.projection.sheetId,
276
+ input.projection.generationId ?? "<none>",
277
+ input.projection.revision,
278
+ input.dimensionRevision,
279
+ dpr,
280
+ input.theme.signature,
281
+ ].join("/");
282
+ if (generation !== this.generation) {
283
+ this.tiles.clear();
284
+ this.generation = generation;
285
+ }
286
+
287
+ setFill(context, input.theme.background, "#ffffff");
288
+ context.fillRect(0, 0, width, height);
289
+
290
+ const bodyLeft = input.rowHeaderWidth;
291
+ const bodyTop = input.columnHeaderHeight;
292
+ const visibleLeft = clamp(input.logicalScrollLeft, 0, input.columns.total);
293
+ const visibleTop = clamp(input.logicalScrollTop, 0, input.rows.total);
294
+ const visibleRight = clamp(
295
+ input.logicalScrollLeft + width - bodyLeft,
296
+ visibleLeft,
297
+ input.columns.total,
298
+ );
299
+ const visibleBottom = clamp(
300
+ input.logicalScrollTop + height - bodyTop,
301
+ visibleTop,
302
+ input.rows.total,
303
+ );
304
+ let paintedTiles = 0;
305
+ let reusedTiles = 0;
306
+ let uncachedTiles = 0;
307
+
308
+ context.save();
309
+ context.beginPath();
310
+ context.rect(bodyLeft, bodyTop, Math.max(0, width - bodyLeft), Math.max(0, height - bodyTop));
311
+ context.clip();
312
+ const firstTileColumn = Math.floor(visibleLeft / TILE_SIZE);
313
+ const lastTileColumn = Math.floor(
314
+ Math.max(visibleLeft, visibleRight - Number.EPSILON) / TILE_SIZE,
315
+ );
316
+ const firstTileRow = Math.floor(visibleTop / TILE_SIZE);
317
+ const lastTileRow = Math.floor(
318
+ Math.max(visibleTop, visibleBottom - Number.EPSILON) / TILE_SIZE,
319
+ );
320
+ let visitedTiles = 0;
321
+ tileLoop: for (let tileRow = firstTileRow; tileRow <= lastTileRow; tileRow += 1) {
322
+ for (let tileColumn = firstTileColumn; tileColumn <= lastTileColumn; tileColumn += 1) {
323
+ if (visitedTiles >= MAX_TILES_PER_PAINT) break tileLoop;
324
+ visitedTiles += 1;
325
+ const key = `${tileColumn}:${tileRow}`;
326
+ let tile = this.tiles.get(key);
327
+ if (!tile) {
328
+ const painted = this.paintTile(input, tileColumn, tileRow, dpr);
329
+ tile = {
330
+ canvas: painted.canvas,
331
+ lastUsed: ++this.clock,
332
+ };
333
+ if (painted.cacheable) this.tiles.set(key, tile);
334
+ else uncachedTiles += 1;
335
+ paintedTiles += 1;
336
+ } else {
337
+ tile.lastUsed = ++this.clock;
338
+ reusedTiles += 1;
339
+ }
340
+ const logicalX = tileColumn * TILE_SIZE;
341
+ const logicalY = tileRow * TILE_SIZE;
342
+ context.drawImage(
343
+ tile.canvas,
344
+ bodyLeft + logicalX - input.logicalScrollLeft,
345
+ bodyTop + logicalY - input.logicalScrollTop,
346
+ TILE_SIZE,
347
+ TILE_SIZE,
348
+ );
349
+ }
350
+ }
351
+
352
+ paintSelection(context, input);
353
+ paintVisibleGrid(context, input, visibleLeft, visibleTop, visibleRight, visibleBottom);
354
+ context.restore();
355
+ paintHeaders(
356
+ context,
357
+ input,
358
+ width,
359
+ height,
360
+ visibleLeft,
361
+ visibleTop,
362
+ visibleRight,
363
+ visibleBottom,
364
+ );
365
+
366
+ this.evictTiles(dpr);
367
+ return {
368
+ cacheSize: this.tiles.size,
369
+ paintedTiles,
370
+ reusedTiles,
371
+ uncachedTiles,
372
+ devicePixelRatio: dpr,
373
+ };
374
+ }
375
+
376
+ private paintTile(
377
+ input: SpreadsheetCanvasPaint,
378
+ tileColumn: number,
379
+ tileRow: number,
380
+ dpr: number,
381
+ ): { canvas: HTMLCanvasElement; cacheable: boolean } {
382
+ const tile = input.canvas.ownerDocument.createElement("canvas");
383
+ tile.width = Math.max(1, Math.round(TILE_SIZE * dpr));
384
+ tile.height = Math.max(1, Math.round(TILE_SIZE * dpr));
385
+ const context = safeCanvasContext(tile);
386
+ if (!context) return { canvas: tile, cacheable: false };
387
+ context.setTransform(dpr, 0, 0, dpr, 0, 0);
388
+ setFill(context, input.theme.background, "#ffffff");
389
+ context.fillRect(0, 0, TILE_SIZE, TILE_SIZE);
390
+
391
+ const tileLeft = tileColumn * TILE_SIZE;
392
+ const tileTop = tileRow * TILE_SIZE;
393
+ const tileRight = Math.min(input.columns.total, tileLeft + TILE_SIZE);
394
+ const tileBottom = Math.min(input.rows.total, tileTop + TILE_SIZE);
395
+ const firstColumn = input.columns.indexAtOffset(tileLeft);
396
+ const lastColumn = input.columns.indexAtOffset(Math.max(tileLeft, tileRight - Number.EPSILON));
397
+ const firstRow = input.rows.indexAtOffset(tileTop);
398
+ const lastRow = input.rows.indexAtOffset(Math.max(tileTop, tileBottom - Number.EPSILON));
399
+ const query = {
400
+ rowStart: firstRow,
401
+ rowEnd: Math.min(input.rows.count, lastRow + 1),
402
+ columnStart: firstColumn,
403
+ columnEnd: Math.min(input.columns.count, lastColumn + 1),
404
+ };
405
+ const coveredQuery = intersectCoverage(query, input.projection.coverage);
406
+ const cacheable = coverageContains(input.projection.coverage, query);
407
+ const cells: SpreadsheetCanvasCell[] = [];
408
+ if (coveredQuery) {
409
+ input.projection.cells.forEachInRange(
410
+ coveredQuery.rowStart,
411
+ coveredQuery.rowEnd,
412
+ coveredQuery.columnStart,
413
+ coveredQuery.columnEnd,
414
+ (cell) => cells.push(cell),
415
+ );
416
+ }
417
+
418
+ for (const cell of cells) {
419
+ const fill = cell.format.fill;
420
+ if (!fill) continue;
421
+ const x = input.columns.offsetAt(cell.col) - tileLeft;
422
+ const y = input.rows.offsetAt(cell.row) - tileTop;
423
+ const width = input.columns.sizeAt(cell.col);
424
+ const height = input.rows.sizeAt(cell.row);
425
+ setFill(context, fill, input.theme.background);
426
+ context.fillRect(x, y, width, height);
427
+ }
428
+
429
+ paintTileGrid(context, input, tileLeft, tileTop, tileRight, tileBottom);
430
+
431
+ for (const cell of cells) {
432
+ const value = input.projection.valueAt(cell);
433
+ const text = displayValue(value);
434
+ if (!text) continue;
435
+ paintCellText(context, input, cell, text, tileLeft, tileTop);
436
+ }
437
+
438
+ return { canvas: tile, cacheable };
439
+ }
440
+
441
+ private evictTiles(dpr: number): void {
442
+ const pixelsPerTile = Math.max(1, Math.round(TILE_SIZE * dpr)) ** 2;
443
+ const maximum = Math.max(
444
+ 4,
445
+ Math.min(MAX_CACHE_TILES, Math.floor(MAX_CACHE_PHYSICAL_PIXELS / pixelsPerTile)),
446
+ );
447
+ if (this.tiles.size <= maximum) return;
448
+ const oldest = [...this.tiles.entries()].sort(
449
+ (left, right) => left[1].lastUsed - right[1].lastUsed,
450
+ );
451
+ for (let index = 0; index < oldest.length - maximum; index += 1) {
452
+ this.tiles.delete(oldest[index]![0]);
453
+ }
454
+ }
455
+ }
456
+
457
+ function finiteExtent(value: number): number {
458
+ if (!Number.isFinite(value) || value <= 0) return 1;
459
+ return Math.max(1, Math.floor(value));
460
+ }
461
+
462
+ function safeCanvasContext(canvas: HTMLCanvasElement): CanvasRenderingContext2D | null {
463
+ try {
464
+ return canvas.getContext("2d", { alpha: false });
465
+ } catch {
466
+ return null;
467
+ }
468
+ }
469
+
470
+ function setFill(context: CanvasRenderingContext2D, value: string, fallback: string): void {
471
+ context.fillStyle = fallback;
472
+ try {
473
+ context.fillStyle = value;
474
+ } catch {
475
+ // Invalid workbook colors behave like invalid CSS declarations: ignore them.
476
+ }
477
+ }
478
+
479
+ function setStroke(context: CanvasRenderingContext2D, value: string, fallback: string): void {
480
+ context.strokeStyle = fallback;
481
+ try {
482
+ context.strokeStyle = value;
483
+ } catch {
484
+ // Invalid workbook colors behave like invalid CSS declarations: ignore them.
485
+ }
486
+ }
487
+
488
+ function paintTileGrid(
489
+ context: CanvasRenderingContext2D,
490
+ input: SpreadsheetCanvasPaint,
491
+ tileLeft: number,
492
+ tileTop: number,
493
+ tileRight: number,
494
+ tileBottom: number,
495
+ ): void {
496
+ setFill(context, input.theme.border, "#d1d5db");
497
+ visitAxisBoundaries(input.columns, tileLeft, tileRight, (offset) => {
498
+ context.fillRect(offset - tileLeft - 1, 0, 1, TILE_SIZE);
499
+ });
500
+ visitAxisBoundaries(input.rows, tileTop, tileBottom, (offset) => {
501
+ context.fillRect(0, offset - tileTop - 1, TILE_SIZE, 1);
502
+ });
503
+ }
504
+
505
+ function paintVisibleGrid(
506
+ context: CanvasRenderingContext2D,
507
+ input: SpreadsheetCanvasPaint,
508
+ visibleLeft: number,
509
+ visibleTop: number,
510
+ visibleRight: number,
511
+ visibleBottom: number,
512
+ ): void {
513
+ setFill(context, input.theme.border, "#d1d5db");
514
+ visitAxisBoundaries(input.columns, visibleLeft, visibleRight, (offset) => {
515
+ const x = input.rowHeaderWidth + offset - input.logicalScrollLeft;
516
+ context.fillRect(x - 1, input.columnHeaderHeight, 1, input.viewportHeight);
517
+ });
518
+ visitAxisBoundaries(input.rows, visibleTop, visibleBottom, (offset) => {
519
+ const y = input.columnHeaderHeight + offset - input.logicalScrollTop;
520
+ context.fillRect(input.rowHeaderWidth, y - 1, input.viewportWidth, 1);
521
+ });
522
+ }
523
+
524
+ function visitAxisBoundaries(
525
+ axis: SpreadsheetCanvasAxis,
526
+ start: number,
527
+ end: number,
528
+ visit: (offset: number) => void,
529
+ ): void {
530
+ if (axis.count <= 0 || end < start) return;
531
+ let index = axis.indexAtOffset(start);
532
+ let visits = 0;
533
+ while (index < axis.count && visits < MAX_AXIS_MARKS_PER_PAINT) {
534
+ const boundary = axis.offsetAt(index + 1);
535
+ if (boundary > end + 1) break;
536
+ if (boundary >= start) visit(boundary);
537
+ index += 1;
538
+ visits += 1;
539
+ }
540
+ }
541
+
542
+ function paintSelection(context: CanvasRenderingContext2D, input: SpreadsheetCanvasPaint): void {
543
+ const { selection } = input;
544
+ const left =
545
+ input.rowHeaderWidth + input.columns.offsetAt(selection.left) - input.logicalScrollLeft;
546
+ const right =
547
+ input.rowHeaderWidth + input.columns.offsetAt(selection.right + 1) - input.logicalScrollLeft;
548
+ const top =
549
+ input.columnHeaderHeight + input.rows.offsetAt(selection.top) - input.logicalScrollTop;
550
+ const bottom =
551
+ input.columnHeaderHeight + input.rows.offsetAt(selection.bottom + 1) - input.logicalScrollTop;
552
+ context.save();
553
+ setFill(context, input.theme.accent, "#3b82f6");
554
+ context.globalAlpha = 0.1;
555
+ context.fillRect(left, top, right - left, bottom - top);
556
+ context.restore();
557
+
558
+ const activeLeft =
559
+ input.rowHeaderWidth + input.columns.offsetAt(selection.focusColumn) - input.logicalScrollLeft;
560
+ const activeTop =
561
+ input.columnHeaderHeight + input.rows.offsetAt(selection.focusRow) - input.logicalScrollTop;
562
+ const activeWidth = input.columns.sizeAt(selection.focusColumn);
563
+ const activeHeight = input.rows.sizeAt(selection.focusRow);
564
+ context.save();
565
+ setStroke(context, input.theme.accent, "#3b82f6");
566
+ context.lineWidth = 2;
567
+ context.strokeRect(
568
+ activeLeft + 1,
569
+ activeTop + 1,
570
+ Math.max(0, activeWidth - 2),
571
+ Math.max(0, activeHeight - 2),
572
+ );
573
+ context.restore();
574
+ }
575
+
576
+ function paintHeaders(
577
+ context: CanvasRenderingContext2D,
578
+ input: SpreadsheetCanvasPaint,
579
+ width: number,
580
+ height: number,
581
+ visibleLeft: number,
582
+ visibleTop: number,
583
+ visibleRight: number,
584
+ visibleBottom: number,
585
+ ): void {
586
+ setFill(context, input.theme.headerBackground, "#f3f4f6");
587
+ context.fillRect(0, 0, width, input.columnHeaderHeight);
588
+ context.fillRect(0, input.columnHeaderHeight, input.rowHeaderWidth, height);
589
+ setFill(context, input.theme.border, "#d1d5db");
590
+ context.fillRect(0, input.columnHeaderHeight - 1, width, 1);
591
+ context.fillRect(input.rowHeaderWidth - 1, 0, 1, height);
592
+
593
+ context.save();
594
+ context.beginPath();
595
+ context.rect(
596
+ input.rowHeaderWidth,
597
+ 0,
598
+ Math.max(0, width - input.rowHeaderWidth),
599
+ input.columnHeaderHeight,
600
+ );
601
+ context.clip();
602
+ setFill(context, input.theme.mutedForeground, "#6b7280");
603
+ context.font = `600 ${input.theme.headerFontSize}px ${input.theme.fontFamily}`;
604
+ context.textAlign = "center";
605
+ context.textBaseline = "middle";
606
+ visitAxisItems(input.columns, visibleLeft, visibleRight, (column, offset, size) => {
607
+ const x = input.rowHeaderWidth + offset - input.logicalScrollLeft;
608
+ setFill(context, input.theme.border, "#d1d5db");
609
+ context.fillRect(x + size - 1, 0, 1, input.columnHeaderHeight);
610
+ if (size >= 8) {
611
+ setFill(context, input.theme.mutedForeground, "#6b7280");
612
+ context.fillText(columnName(column), x + size / 2, input.columnHeaderHeight / 2);
613
+ }
614
+ });
615
+ context.restore();
616
+
617
+ context.save();
618
+ context.beginPath();
619
+ context.rect(
620
+ 0,
621
+ input.columnHeaderHeight,
622
+ input.rowHeaderWidth,
623
+ Math.max(0, height - input.columnHeaderHeight),
624
+ );
625
+ context.clip();
626
+ setFill(context, input.theme.mutedForeground, "#6b7280");
627
+ context.font = `400 ${input.theme.headerFontSize}px ${input.theme.fontFamily}`;
628
+ context.textAlign = "center";
629
+ context.textBaseline = "middle";
630
+ visitAxisItems(input.rows, visibleTop, visibleBottom, (row, offset, size) => {
631
+ const y = input.columnHeaderHeight + offset - input.logicalScrollTop;
632
+ setFill(context, input.theme.border, "#d1d5db");
633
+ context.fillRect(0, y + size - 1, input.rowHeaderWidth, 1);
634
+ if (size >= 8) {
635
+ setFill(context, input.theme.mutedForeground, "#6b7280");
636
+ context.fillText(String(row + 1), input.rowHeaderWidth / 2, y + size / 2);
637
+ }
638
+ });
639
+ context.restore();
640
+ }
641
+
642
+ function visitAxisItems(
643
+ axis: SpreadsheetCanvasAxis,
644
+ start: number,
645
+ end: number,
646
+ visit: (index: number, offset: number, size: number) => void,
647
+ ): void {
648
+ if (axis.count <= 0 || end < start) return;
649
+ let index = axis.indexAtOffset(start);
650
+ let visits = 0;
651
+ while (index < axis.count && visits < MAX_AXIS_MARKS_PER_PAINT) {
652
+ const offset = axis.offsetAt(index);
653
+ if (offset > end) break;
654
+ visit(index, offset, axis.sizeAt(index));
655
+ index += 1;
656
+ visits += 1;
657
+ }
658
+ }
659
+
660
+ function paintCellText(
661
+ context: CanvasRenderingContext2D,
662
+ input: SpreadsheetCanvasPaint,
663
+ cell: SpreadsheetCanvasCell,
664
+ text: string,
665
+ tileLeft: number,
666
+ tileTop: number,
667
+ ): void {
668
+ const format = cell.format;
669
+ const x = input.columns.offsetAt(cell.col) - tileLeft;
670
+ const y = input.rows.offsetAt(cell.row) - tileTop;
671
+ const width = input.columns.sizeAt(cell.col);
672
+ const height = input.rows.sizeAt(cell.row);
673
+ if (width <= 2 || height <= 2) return;
674
+ const fontSize = finitePositive(format.font?.size, input.theme.fontSize);
675
+ const fontFamily = format.font?.name || input.theme.fontFamily;
676
+ const weight = format.font?.bold ? 600 : 400;
677
+ const italic = format.font?.italic ? "italic " : "";
678
+ context.save();
679
+ context.beginPath();
680
+ context.rect(x + 1, y + 1, Math.max(0, width - 2), Math.max(0, height - 2));
681
+ context.clip();
682
+ context.font = `${italic}${weight} ${fontSize}px ${fontFamily}`;
683
+ setFill(
684
+ context,
685
+ format.font?.color ?? (text.startsWith("#") ? input.theme.error : input.theme.foreground),
686
+ input.theme.foreground,
687
+ );
688
+ const alignment = format.horizontalAlignment;
689
+ context.textAlign = alignment === "center" ? "center" : alignment === "right" ? "right" : "left";
690
+ context.textBaseline = "alphabetic";
691
+ const padding = 6;
692
+ const textX =
693
+ alignment === "center"
694
+ ? x + width / 2
695
+ : alignment === "right"
696
+ ? x + width - padding
697
+ : x + padding;
698
+ const lineHeight = fontSize * 1.5;
699
+ const availableWidth = Math.max(0, width - padding * 2);
700
+ const lines = format.wrapText
701
+ ? wrapText(context, text, availableWidth, Math.max(1, Math.floor(height / lineHeight)))
702
+ : [ellipsize(context, text.replaceAll(/\r?\n/g, " "), availableWidth)];
703
+ const blockHeight = lines.length * lineHeight;
704
+ const vertical = format.verticalAlignment;
705
+ const firstBaseline =
706
+ vertical === "top"
707
+ ? y + Math.min(height, padding + fontSize)
708
+ : vertical === "bottom"
709
+ ? y + height - padding - blockHeight + fontSize
710
+ : y + (height - blockHeight) / 2 + fontSize;
711
+ for (let index = 0; index < lines.length; index += 1) {
712
+ const line = lines[index]!;
713
+ const baseline = firstBaseline + index * lineHeight;
714
+ context.fillText(line, textX, baseline);
715
+ if (format.font?.underline && line) {
716
+ const metrics = context.measureText(line);
717
+ const textWidth = metrics.width;
718
+ const startX =
719
+ alignment === "center"
720
+ ? textX - textWidth / 2
721
+ : alignment === "right"
722
+ ? textX - textWidth
723
+ : textX;
724
+ setFill(context, format.font.color ?? input.theme.foreground, input.theme.foreground);
725
+ context.fillRect(startX, baseline + 1, textWidth, Math.max(1, fontSize / 14));
726
+ }
727
+ }
728
+ context.restore();
729
+ }
730
+
731
+ function wrapText(
732
+ context: CanvasRenderingContext2D,
733
+ input: string,
734
+ maximumWidth: number,
735
+ maximumLines: number,
736
+ ): string[] {
737
+ if (maximumWidth <= 0) return [""];
738
+ const output: string[] = [];
739
+ for (const paragraph of input.split(/\r?\n/)) {
740
+ const words = paragraph.split(/\s+/).filter(Boolean);
741
+ if (words.length === 0) output.push("");
742
+ let line = "";
743
+ for (const word of words) {
744
+ const next = line ? `${line} ${word}` : word;
745
+ if (!line || context.measureText(next).width <= maximumWidth) line = next;
746
+ else {
747
+ output.push(ellipsize(context, line, maximumWidth));
748
+ line = word;
749
+ }
750
+ if (output.length >= maximumLines) break;
751
+ }
752
+ if (output.length < maximumLines && line) output.push(ellipsize(context, line, maximumWidth));
753
+ if (output.length >= maximumLines) break;
754
+ }
755
+ return output.slice(0, maximumLines);
756
+ }
757
+
758
+ function ellipsize(context: CanvasRenderingContext2D, input: string, maximumWidth: number): string {
759
+ if (!input || maximumWidth <= 0) return "";
760
+ if (context.measureText(input).width <= maximumWidth) return input;
761
+ const suffix = "…";
762
+ const suffixWidth = context.measureText(suffix).width;
763
+ if (suffixWidth > maximumWidth) return "";
764
+ let low = 0;
765
+ let high = input.length;
766
+ while (low < high) {
767
+ const middle = Math.ceil((low + high) / 2);
768
+ if (context.measureText(`${input.slice(0, middle)}${suffix}`).width <= maximumWidth)
769
+ low = middle;
770
+ else high = middle - 1;
771
+ }
772
+ return `${input.slice(0, low)}${suffix}`;
773
+ }
774
+
775
+ function finitePositive(value: number | undefined, fallback: number): number {
776
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
777
+ }
778
+
779
+ function displayValue(value: unknown): string {
780
+ if (value === null || value === undefined) return "";
781
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : value.toLocaleDateString();
782
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
783
+ return String(value);
784
+ }
785
+
786
+ function columnName(index: number): string {
787
+ let value = index + 1;
788
+ let result = "";
789
+ while (value > 0) {
790
+ const remainder = (value - 1) % 26;
791
+ result = String.fromCharCode(65 + remainder) + result;
792
+ value = Math.floor((value - 1) / 26);
793
+ }
794
+ return result;
795
+ }
796
+
797
+ function clamp(value: number, minimum: number, maximum: number): number {
798
+ return Math.max(minimum, Math.min(maximum, value));
799
+ }