vastgrid 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/README.md +175 -13
  2. package/dist/canvas/geometry.d.ts +92 -0
  3. package/dist/canvas/geometry.d.ts.map +1 -0
  4. package/dist/canvas/geometry.js +197 -0
  5. package/dist/canvas/geometry.js.map +1 -0
  6. package/dist/canvas/grid-canvas.d.ts +161 -0
  7. package/dist/canvas/grid-canvas.d.ts.map +1 -0
  8. package/dist/canvas/grid-canvas.js +1146 -0
  9. package/dist/canvas/grid-canvas.js.map +1 -0
  10. package/dist/canvas/hooks.d.ts +61 -0
  11. package/dist/canvas/hooks.d.ts.map +1 -0
  12. package/dist/canvas/hooks.js +2 -0
  13. package/dist/canvas/hooks.js.map +1 -0
  14. package/dist/canvas/keys.d.ts +26 -0
  15. package/dist/canvas/keys.d.ts.map +1 -0
  16. package/dist/canvas/keys.js +133 -0
  17. package/dist/canvas/keys.js.map +1 -0
  18. package/dist/canvas/layout.d.ts +26 -0
  19. package/dist/canvas/layout.d.ts.map +1 -0
  20. package/dist/canvas/layout.js +73 -0
  21. package/dist/canvas/layout.js.map +1 -0
  22. package/dist/canvas/scroll.d.ts +31 -0
  23. package/dist/canvas/scroll.d.ts.map +1 -0
  24. package/dist/canvas/scroll.js +111 -0
  25. package/dist/canvas/scroll.js.map +1 -0
  26. package/dist/canvas/text.d.ts +23 -0
  27. package/dist/canvas/text.d.ts.map +1 -0
  28. package/dist/canvas/text.js +142 -0
  29. package/dist/canvas/text.js.map +1 -0
  30. package/dist/{theme.d.ts → canvas/theme.d.ts} +8 -0
  31. package/dist/canvas/theme.d.ts.map +1 -0
  32. package/dist/canvas/theme.js +62 -0
  33. package/dist/canvas/theme.js.map +1 -0
  34. package/dist/core/cache.d.ts +48 -0
  35. package/dist/core/cache.d.ts.map +1 -0
  36. package/dist/core/cache.js +222 -0
  37. package/dist/core/cache.js.map +1 -0
  38. package/dist/core/checked.d.ts +20 -0
  39. package/dist/core/checked.d.ts.map +1 -0
  40. package/dist/core/checked.js +61 -0
  41. package/dist/core/checked.js.map +1 -0
  42. package/dist/core/client.d.ts +33 -0
  43. package/dist/core/client.d.ts.map +1 -0
  44. package/dist/core/client.js +103 -0
  45. package/dist/core/client.js.map +1 -0
  46. package/dist/core/conditions.d.ts +48 -0
  47. package/dist/core/conditions.d.ts.map +1 -0
  48. package/dist/core/conditions.js +105 -0
  49. package/dist/core/conditions.js.map +1 -0
  50. package/dist/core/evaluate.d.ts +20 -0
  51. package/dist/core/evaluate.d.ts.map +1 -0
  52. package/dist/core/evaluate.js +210 -0
  53. package/dist/core/evaluate.js.map +1 -0
  54. package/dist/core/filters.d.ts +51 -0
  55. package/dist/core/filters.d.ts.map +1 -0
  56. package/dist/core/filters.js +303 -0
  57. package/dist/core/filters.js.map +1 -0
  58. package/dist/core/format.d.ts +14 -0
  59. package/dist/core/format.d.ts.map +1 -0
  60. package/dist/core/format.js +303 -0
  61. package/dist/core/format.js.map +1 -0
  62. package/dist/core/measures.d.ts +26 -0
  63. package/dist/core/measures.d.ts.map +1 -0
  64. package/dist/core/measures.js +105 -0
  65. package/dist/core/measures.js.map +1 -0
  66. package/dist/core/request-ids.d.ts +20 -0
  67. package/dist/core/request-ids.d.ts.map +1 -0
  68. package/dist/core/request-ids.js +55 -0
  69. package/dist/core/request-ids.js.map +1 -0
  70. package/dist/core/rows.d.ts +79 -0
  71. package/dist/core/rows.d.ts.map +1 -0
  72. package/dist/core/rows.js +173 -0
  73. package/dist/core/rows.js.map +1 -0
  74. package/dist/core/store.d.ts +233 -0
  75. package/dist/core/store.d.ts.map +1 -0
  76. package/dist/core/store.js +1162 -0
  77. package/dist/core/store.js.map +1 -0
  78. package/dist/core/totals.d.ts +24 -0
  79. package/dist/core/totals.d.ts.map +1 -0
  80. package/dist/core/totals.js +49 -0
  81. package/dist/core/totals.js.map +1 -0
  82. package/dist/core/trace.d.ts +7 -0
  83. package/dist/core/trace.d.ts.map +1 -0
  84. package/dist/core/trace.js +26 -0
  85. package/dist/core/trace.js.map +1 -0
  86. package/dist/core/transports.d.ts +50 -0
  87. package/dist/core/transports.d.ts.map +1 -0
  88. package/dist/core/transports.js +160 -0
  89. package/dist/core/transports.js.map +1 -0
  90. package/dist/core/types.d.ts +150 -0
  91. package/dist/core/types.d.ts.map +1 -0
  92. package/dist/core/types.js +45 -0
  93. package/dist/core/types.js.map +1 -0
  94. package/dist/core.d.ts +12 -0
  95. package/dist/core.d.ts.map +1 -0
  96. package/dist/core.js +11 -0
  97. package/dist/core.js.map +1 -0
  98. package/dist/index.d.ts +15 -6
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +13 -7
  101. package/dist/index.js.map +1 -1
  102. package/dist/react/accessible-rows.d.ts +13 -0
  103. package/dist/react/accessible-rows.d.ts.map +1 -0
  104. package/dist/react/accessible-rows.js +320 -0
  105. package/dist/react/accessible-rows.js.map +1 -0
  106. package/dist/react/cell-editor.d.ts +22 -0
  107. package/dist/react/cell-editor.d.ts.map +1 -0
  108. package/dist/react/cell-editor.js +431 -0
  109. package/dist/react/cell-editor.js.map +1 -0
  110. package/dist/react/cell-menu.d.ts +18 -0
  111. package/dist/react/cell-menu.d.ts.map +1 -0
  112. package/dist/react/cell-menu.js +299 -0
  113. package/dist/react/cell-menu.js.map +1 -0
  114. package/dist/react/column-menu.d.ts +16 -0
  115. package/dist/react/column-menu.d.ts.map +1 -0
  116. package/dist/react/column-menu.js +1311 -0
  117. package/dist/react/column-menu.js.map +1 -0
  118. package/dist/react/columns-menu.d.ts +7 -0
  119. package/dist/react/columns-menu.d.ts.map +1 -0
  120. package/dist/react/columns-menu.js +249 -0
  121. package/dist/react/columns-menu.js.map +1 -0
  122. package/dist/react/conditions-menu.d.ts +10 -0
  123. package/dist/react/conditions-menu.d.ts.map +1 -0
  124. package/dist/react/conditions-menu.js +795 -0
  125. package/dist/react/conditions-menu.js.map +1 -0
  126. package/dist/react/context.d.ts +29 -0
  127. package/dist/react/context.d.ts.map +1 -0
  128. package/dist/react/context.js +18 -0
  129. package/dist/react/context.js.map +1 -0
  130. package/dist/react/data-grid.d.ts +79 -0
  131. package/dist/react/data-grid.d.ts.map +1 -0
  132. package/dist/react/data-grid.js +653 -0
  133. package/dist/react/data-grid.js.map +1 -0
  134. package/dist/react/detail-panel.d.ts +22 -0
  135. package/dist/react/detail-panel.d.ts.map +1 -0
  136. package/dist/react/detail-panel.js +242 -0
  137. package/dist/react/detail-panel.js.map +1 -0
  138. package/dist/react/export-menu.d.ts +11 -0
  139. package/dist/react/export-menu.d.ts.map +1 -0
  140. package/dist/react/export-menu.js +304 -0
  141. package/dist/react/export-menu.js.map +1 -0
  142. package/dist/react/find-bar.d.ts +11 -0
  143. package/dist/react/find-bar.d.ts.map +1 -0
  144. package/dist/react/find-bar.js +303 -0
  145. package/dist/react/find-bar.js.map +1 -0
  146. package/dist/react/format-form.d.ts +12 -0
  147. package/dist/react/format-form.d.ts.map +1 -0
  148. package/dist/react/format-form.js +863 -0
  149. package/dist/react/format-form.js.map +1 -0
  150. package/dist/react/formula-menu.d.ts +6 -0
  151. package/dist/react/formula-menu.d.ts.map +1 -0
  152. package/dist/react/formula-menu.js +343 -0
  153. package/dist/react/formula-menu.js.map +1 -0
  154. package/dist/react/grid-body.d.ts +33 -0
  155. package/dist/react/grid-body.d.ts.map +1 -0
  156. package/dist/react/grid-body.js +1389 -0
  157. package/dist/react/grid-body.js.map +1 -0
  158. package/dist/react/group-menu.d.ts +7 -0
  159. package/dist/react/group-menu.d.ts.map +1 -0
  160. package/dist/react/group-menu.js +508 -0
  161. package/dist/react/group-menu.js.map +1 -0
  162. package/dist/react/hooks.d.ts +33 -0
  163. package/dist/react/hooks.d.ts.map +1 -0
  164. package/dist/react/hooks.js +161 -0
  165. package/dist/react/hooks.js.map +1 -0
  166. package/dist/react/messages.d.ts +180 -0
  167. package/dist/react/messages.d.ts.map +1 -0
  168. package/dist/react/messages.js +414 -0
  169. package/dist/react/messages.js.map +1 -0
  170. package/dist/react/save.d.ts +9 -0
  171. package/dist/react/save.d.ts.map +1 -0
  172. package/dist/react/save.js +102 -0
  173. package/dist/react/save.js.map +1 -0
  174. package/dist/react/static-rows.d.ts +9 -0
  175. package/dist/react/static-rows.d.ts.map +1 -0
  176. package/dist/react/static-rows.js +176 -0
  177. package/dist/react/static-rows.js.map +1 -0
  178. package/dist/react/status-bar.d.ts +12 -0
  179. package/dist/react/status-bar.d.ts.map +1 -0
  180. package/dist/react/status-bar.js +413 -0
  181. package/dist/react/status-bar.js.map +1 -0
  182. package/dist/react/toolbar-popover.d.ts +14 -0
  183. package/dist/react/toolbar-popover.d.ts.map +1 -0
  184. package/dist/react/toolbar-popover.js +143 -0
  185. package/dist/react/toolbar-popover.js.map +1 -0
  186. package/dist/react/toolbar.d.ts +13 -0
  187. package/dist/react/toolbar.d.ts.map +1 -0
  188. package/dist/react/toolbar.js +344 -0
  189. package/dist/react/toolbar.js.map +1 -0
  190. package/dist/server.d.ts +22 -0
  191. package/dist/server.d.ts.map +1 -0
  192. package/dist/server.js +45 -0
  193. package/dist/server.js.map +1 -0
  194. package/dist/styles.css +496 -316
  195. package/package.json +43 -18
  196. package/src/canvas/geometry.ts +197 -0
  197. package/src/canvas/grid-canvas.ts +1222 -0
  198. package/src/canvas/hooks.ts +61 -0
  199. package/src/canvas/keys.ts +96 -0
  200. package/src/canvas/layout.ts +77 -0
  201. package/src/canvas/scroll.ts +120 -0
  202. package/src/canvas/text.ts +155 -0
  203. package/src/{theme.ts → canvas/theme.ts} +14 -1
  204. package/src/core/cache.ts +247 -0
  205. package/src/core/checked.ts +64 -0
  206. package/src/core/client.ts +103 -0
  207. package/src/core/conditions.ts +89 -0
  208. package/src/core/evaluate.ts +221 -0
  209. package/src/core/filters.ts +268 -0
  210. package/src/core/format.ts +309 -0
  211. package/src/core/measures.ts +99 -0
  212. package/src/core/request-ids.ts +54 -0
  213. package/src/core/rows.ts +213 -0
  214. package/src/core/store.ts +1122 -0
  215. package/src/core/totals.ts +59 -0
  216. package/src/core/trace.ts +44 -0
  217. package/src/core/transports.ts +177 -0
  218. package/src/core/types.ts +168 -0
  219. package/src/core.ts +39 -0
  220. package/src/index.ts +24 -12
  221. package/src/react/accessible-rows.tsx +242 -0
  222. package/src/react/cell-editor.tsx +216 -0
  223. package/src/react/cell-menu.tsx +166 -0
  224. package/src/react/column-menu.tsx +619 -0
  225. package/src/react/columns-menu.tsx +116 -0
  226. package/src/react/conditions-menu.tsx +289 -0
  227. package/src/react/context.ts +33 -0
  228. package/src/react/data-grid.tsx +286 -0
  229. package/src/react/detail-panel.tsx +92 -0
  230. package/src/react/export-menu.tsx +112 -0
  231. package/src/react/find-bar.tsx +112 -0
  232. package/src/react/format-form.tsx +421 -0
  233. package/src/react/formula-menu.tsx +163 -0
  234. package/src/react/grid-body.tsx +744 -0
  235. package/src/react/group-menu.tsx +214 -0
  236. package/src/react/hooks.ts +104 -0
  237. package/src/react/messages.ts +556 -0
  238. package/src/react/save.ts +42 -0
  239. package/src/react/static-rows.tsx +59 -0
  240. package/src/react/status-bar.tsx +175 -0
  241. package/src/react/toolbar-popover.tsx +69 -0
  242. package/src/react/toolbar.tsx +152 -0
  243. package/src/server.ts +58 -0
  244. package/src/styles.css +496 -316
  245. package/dist/components/bars.d.ts +0 -14
  246. package/dist/components/bars.d.ts.map +0 -1
  247. package/dist/components/bars.js +0 -67
  248. package/dist/components/bars.js.map +0 -1
  249. package/dist/components/cell-inspector.d.ts +0 -11
  250. package/dist/components/cell-inspector.d.ts.map +0 -1
  251. package/dist/components/cell-inspector.js +0 -37
  252. package/dist/components/cell-inspector.js.map +0 -1
  253. package/dist/components/columns-panel.d.ts +0 -12
  254. package/dist/components/columns-panel.d.ts.map +0 -1
  255. package/dist/components/columns-panel.js +0 -11
  256. package/dist/components/columns-panel.js.map +0 -1
  257. package/dist/components/filter-panel.d.ts +0 -17
  258. package/dist/components/filter-panel.d.ts.map +0 -1
  259. package/dist/components/filter-panel.js +0 -165
  260. package/dist/components/filter-panel.js.map +0 -1
  261. package/dist/components/find-bar.d.ts +0 -9
  262. package/dist/components/find-bar.d.ts.map +0 -1
  263. package/dist/components/find-bar.js +0 -46
  264. package/dist/components/find-bar.js.map +0 -1
  265. package/dist/components/popover.d.ts +0 -38
  266. package/dist/components/popover.d.ts.map +0 -1
  267. package/dist/components/popover.js +0 -104
  268. package/dist/components/popover.js.map +0 -1
  269. package/dist/components/stats-panel.d.ts +0 -12
  270. package/dist/components/stats-panel.d.ts.map +0 -1
  271. package/dist/components/stats-panel.js +0 -40
  272. package/dist/components/stats-panel.js.map +0 -1
  273. package/dist/controller.d.ts +0 -218
  274. package/dist/controller.d.ts.map +0 -1
  275. package/dist/controller.js +0 -788
  276. package/dist/controller.js.map +0 -1
  277. package/dist/data-grid.d.ts +0 -51
  278. package/dist/data-grid.d.ts.map +0 -1
  279. package/dist/data-grid.js +0 -698
  280. package/dist/data-grid.js.map +0 -1
  281. package/dist/labels.d.ts +0 -9
  282. package/dist/labels.d.ts.map +0 -1
  283. package/dist/labels.js +0 -38
  284. package/dist/labels.js.map +0 -1
  285. package/dist/messages.d.ts +0 -99
  286. package/dist/messages.d.ts.map +0 -1
  287. package/dist/messages.js +0 -276
  288. package/dist/messages.js.map +0 -1
  289. package/dist/renderer.d.ts +0 -52
  290. package/dist/renderer.d.ts.map +0 -1
  291. package/dist/renderer.js +0 -522
  292. package/dist/renderer.js.map +0 -1
  293. package/dist/text.d.ts +0 -14
  294. package/dist/text.d.ts.map +0 -1
  295. package/dist/text.js +0 -63
  296. package/dist/text.js.map +0 -1
  297. package/dist/theme.d.ts.map +0 -1
  298. package/dist/theme.js +0 -54
  299. package/dist/theme.js.map +0 -1
  300. package/src/components/bars.tsx +0 -179
  301. package/src/components/cell-inspector.tsx +0 -70
  302. package/src/components/columns-panel.tsx +0 -59
  303. package/src/components/filter-panel.tsx +0 -290
  304. package/src/components/find-bar.tsx +0 -104
  305. package/src/components/popover.tsx +0 -194
  306. package/src/components/stats-panel.tsx +0 -153
  307. package/src/controller.ts +0 -887
  308. package/src/data-grid.tsx +0 -1029
  309. package/src/labels.ts +0 -46
  310. package/src/messages.ts +0 -390
  311. package/src/renderer.ts +0 -535
  312. package/src/text.ts +0 -55
package/src/renderer.ts DELETED
@@ -1,535 +0,0 @@
1
- import {
2
- type CacheBlock,
3
- type CellRange,
4
- type CellValue,
5
- type ColumnType,
6
- isNumericType,
7
- rangeContains,
8
- } from "vastgrid-core";
9
- import type { GridController } from "./controller.js";
10
- import { TextFitter } from "./text.js";
11
- import type { GridTheme } from "./theme.js";
12
-
13
- const PADDING = 8;
14
- const ICON_SPACE = 18;
15
-
16
- /** A short mark for the column type, drawn before the column name. */
17
- function typeMark(type: ColumnType) {
18
- switch (type) {
19
- case "integer":
20
- case "bigint":
21
- case "float":
22
- case "decimal":
23
- return "#";
24
- case "boolean":
25
- return "✓";
26
- case "date":
27
- case "time":
28
- case "timestamp":
29
- case "timestamptz":
30
- case "interval":
31
- return "◷";
32
- case "list":
33
- case "struct":
34
- case "map":
35
- case "json":
36
- return "{}";
37
- case "string":
38
- return "A";
39
- default:
40
- return "·";
41
- }
42
- }
43
-
44
- /**
45
- * Draws a `GridController` onto a canvas: cells, headers, row numbers, grid lines and the
46
- * selection. Draws at most once per animation frame, only what is visible.
47
- */
48
- export class CanvasRenderer {
49
- private readonly context: CanvasRenderingContext2D;
50
- private theme: GridTheme;
51
- private cellText!: TextFitter;
52
- private headerText!: TextFitter;
53
- private smallText!: TextFitter;
54
- private frame = 0;
55
- private ratio = 1;
56
- private formatKey = "";
57
- /** Widths numeric columns needed in the last frame. */
58
- private readonly needed = new Map<number, number>();
59
- /** Durations of the last frames in milliseconds, for diagnostics (`timings()`). */
60
- private readonly durations: number[] = [];
61
-
62
- constructor(
63
- private readonly canvas: HTMLCanvasElement,
64
- private readonly controller: GridController,
65
- theme: GridTheme,
66
- ) {
67
- const context = canvas.getContext("2d", { alpha: false });
68
- if (!context) throw new Error("Canvas 2D is not available.");
69
- this.context = context;
70
- this.theme = theme;
71
- this.setTheme(theme);
72
- controller.measure = (text, header) => {
73
- const fitter = header ? this.headerText : this.cellText;
74
- this.context.font = fitter.font;
75
- return fitter.width(text);
76
- };
77
- }
78
-
79
- setTheme(theme: GridTheme) {
80
- this.theme = theme;
81
- this.cellText = new TextFitter(this.context, theme.font);
82
- this.headerText = new TextFitter(this.context, theme.headerFont);
83
- this.smallText = new TextFitter(this.context, theme.smallFont);
84
- this.formatKey = "";
85
- this.controller.updateLayout();
86
- this.schedule();
87
- }
88
-
89
- schedule() {
90
- if (this.frame) return;
91
- this.frame = requestAnimationFrame(() => {
92
- this.frame = 0;
93
- this.draw();
94
- });
95
- }
96
-
97
- dispose() {
98
- if (this.frame) cancelAnimationFrame(this.frame);
99
- this.frame = 0;
100
- this.controller.measure = null;
101
- }
102
-
103
- /** A 1-device-pixel line position. */
104
- private snap(value: number) {
105
- return (Math.round(value * this.ratio) + 0.5) / this.ratio;
106
- }
107
-
108
- private resize() {
109
- const { width, height } = this.controller;
110
- const ratio = window.devicePixelRatio || 1;
111
- const pixelWidth = Math.max(1, Math.round(width * ratio));
112
- const pixelHeight = Math.max(1, Math.round(height * ratio));
113
- if (this.canvas.width !== pixelWidth || this.canvas.height !== pixelHeight || this.ratio !== ratio) {
114
- this.canvas.width = pixelWidth;
115
- this.canvas.height = pixelHeight;
116
- this.canvas.style.width = `${width}px`;
117
- this.canvas.style.height = `${height}px`;
118
- this.ratio = ratio;
119
- }
120
- this.context.setTransform(ratio, 0, 0, ratio, 0, 0);
121
- }
122
-
123
- /** Average and slowest draw time of the recent frames, in milliseconds. */
124
- timings() {
125
- const count = this.durations.length;
126
- const total = this.durations.reduce((sum, value) => sum + value, 0);
127
- return { frames: count, average: count ? total / count : 0, slowest: count ? Math.max(...this.durations) : 0 };
128
- }
129
-
130
- draw() {
131
- const started = performance.now();
132
- this.paint();
133
- this.durations.push(performance.now() - started);
134
- if (this.durations.length > 120) this.durations.shift();
135
- }
136
-
137
- private paint() {
138
- const c = this.controller;
139
- const ctx = this.context;
140
- const t = this.theme;
141
- this.resize();
142
- ctx.fillStyle = t.background;
143
- ctx.fillRect(0, 0, c.width, c.height);
144
- if (!c.info || c.visible.length === 0) return;
145
- this.formatKey = String(c.formatVersion);
146
-
147
- const [firstRow, lastRow] = c.visibleRows();
148
- const [firstCol, lastCol] = c.visibleScrollColumns();
149
- const wanted: number[] = [];
150
- for (let col = 0; col < c.frozen; col++) wanted.push(c.visible[col].index);
151
- for (let col = firstCol; col <= lastCol; col++) wanted.push(c.visible[col].index);
152
- c.cache.want(firstRow, lastRow, wanted);
153
-
154
- const headerHeight = c.options.headerHeight;
155
- const scrollLeft = c.gutterWidth + c.frozenWidth;
156
- // Scrollable columns.
157
- this.clipped(scrollLeft, headerHeight, c.width - scrollLeft, c.height - headerHeight, () => {
158
- for (let col = firstCol; col <= lastCol; col++) this.drawCells(col, firstRow, lastRow);
159
- this.drawLines(firstCol, lastCol, firstRow, lastRow);
160
- this.drawSelection(false);
161
- });
162
- // Frozen columns.
163
- if (c.frozen > 0) {
164
- this.clipped(c.gutterWidth, headerHeight, c.frozenWidth, c.height - headerHeight, () => {
165
- for (let col = 0; col < c.frozen; col++) this.drawCells(col, firstRow, lastRow);
166
- this.drawLines(0, c.frozen - 1, firstRow, lastRow);
167
- this.drawSelection(true);
168
- });
169
- }
170
- this.drawHeaders(firstCol, lastCol);
171
- this.drawGutter(firstRow, lastRow);
172
- this.drawCorner();
173
- if (this.needed.size > 0) {
174
- const needed = new Map(this.needed);
175
- this.needed.clear();
176
- c.growColumns(needed);
177
- }
178
- if (c.frozen > 0) {
179
- ctx.strokeStyle = t.lineStrong;
180
- ctx.lineWidth = 1;
181
- ctx.beginPath();
182
- const x = this.snap(scrollLeft) - 0.5 / this.ratio;
183
- ctx.moveTo(x, 0);
184
- ctx.lineTo(x, c.height);
185
- ctx.stroke();
186
- }
187
- if (c.resizing >= 0) {
188
- const x = c.columnX(c.resizing) + c.layout.width(c.resizing);
189
- ctx.fillStyle = t.accent;
190
- ctx.fillRect(x - 1, 0, 2, c.height);
191
- }
192
- }
193
-
194
- private clipped(x: number, y: number, width: number, height: number, draw: () => void) {
195
- if (width <= 0 || height <= 0) return;
196
- const ctx = this.context;
197
- ctx.save();
198
- ctx.beginPath();
199
- ctx.rect(x, y, width, height);
200
- ctx.clip();
201
- draw();
202
- ctx.restore();
203
- }
204
-
205
- private textOf(block: CacheBlock, column: number, row: number, value: CellValue, type: ColumnType) {
206
- if (block.textKey !== this.formatKey || !block.text) {
207
- block.text = block.columns.map(() => []);
208
- block.textKey = this.formatKey;
209
- }
210
- const list = block.text[column - block.firstColumn];
211
- const offset = row - block.start;
212
- let text = list[offset];
213
- if (text === undefined) {
214
- text = this.controller.formatter.format(value, type);
215
- list[offset] = text;
216
- }
217
- return text;
218
- }
219
-
220
- private drawCells(col: number, firstRow: number, lastRow: number) {
221
- const c = this.controller;
222
- const ctx = this.context;
223
- const t = this.theme;
224
- const state = c.visible[col];
225
- const schema = c.schema[state.index];
226
- if (!schema) return;
227
- const x = c.columnX(col);
228
- const width = c.layout.width(col);
229
- const rowHeight = c.options.rowHeight;
230
- const numeric = isNumericType(schema.type);
231
- const textWidth = width - PADDING * 2;
232
- const find = c.findText ? (c.findCaseSensitive ? c.findText : c.findText.toLowerCase()) : "";
233
- ctx.save();
234
- ctx.beginPath();
235
- ctx.rect(x, c.options.headerHeight, width, c.height);
236
- ctx.clip();
237
- ctx.font = this.cellText.font;
238
- ctx.textBaseline = "middle";
239
- ctx.textAlign = numeric ? "right" : "left";
240
- const textX = numeric ? x + width - PADDING : x + PADDING;
241
- for (let row = firstRow; row < lastRow; row++) {
242
- const y = c.rowY(row);
243
- const block = c.cache.blockAt(row, state.index);
244
- if (block?.state !== "ready" || !block.values || row >= block.end) {
245
- if (row < c.readableRows && textWidth > 12) {
246
- ctx.fillStyle = block?.state === "error" ? t.error : t.placeholder;
247
- const length = Math.min(textWidth, 24 + ((row * 7919 + state.index * 104729) % 48));
248
- ctx.fillRect(numeric ? x + width - PADDING - length : x + PADDING, y + rowHeight / 2 - 3, length, 6);
249
- }
250
- continue;
251
- }
252
- const value = block.values[state.index - block.firstColumn]?.[row - block.start] ?? null;
253
- const text = this.textOf(block, state.index, row, value, schema.type);
254
- if (find && text && (c.findCaseSensitive ? text : text.toLowerCase()).includes(find)) {
255
- ctx.fillStyle = t.match;
256
- ctx.fillRect(x, y, width, rowHeight);
257
- }
258
- if (text === "") continue;
259
- ctx.fillStyle = value === null ? t.nullText : t.text;
260
- if (numeric && value !== null && !state.sized) {
261
- const needed = this.cellText.width(text) + PADDING * 2 + 2;
262
- if (needed > width && needed > (this.needed.get(col) ?? 0)) this.needed.set(col, needed);
263
- }
264
- ctx.fillText(this.cellText.fit(text, textWidth), textX, y + rowHeight / 2 + 0.5);
265
- }
266
- ctx.restore();
267
- }
268
-
269
- private drawLines(firstCol: number, lastCol: number, firstRow: number, lastRow: number) {
270
- const c = this.controller;
271
- const ctx = this.context;
272
- const top = c.options.headerHeight;
273
- const bottom = Math.min(c.height, c.rowY(c.rowCount));
274
- const left = firstCol < c.frozen ? c.gutterWidth : c.gutterWidth + c.frozenWidth;
275
- const right = lastCol >= 0 ? Math.min(c.width, c.columnX(lastCol) + c.layout.width(lastCol)) : left;
276
- ctx.strokeStyle = this.theme.line;
277
- ctx.lineWidth = 1 / this.ratio;
278
- ctx.beginPath();
279
- for (let row = firstRow; row < lastRow; row++) {
280
- const y = this.snap(c.rowY(row + 1)) - 1 / this.ratio;
281
- ctx.moveTo(left, y);
282
- ctx.lineTo(right, y);
283
- }
284
- for (let col = firstCol; col <= lastCol; col++) {
285
- const x = this.snap(c.columnX(col) + c.layout.width(col)) - 1 / this.ratio;
286
- ctx.moveTo(x, top);
287
- ctx.lineTo(x, bottom);
288
- }
289
- ctx.stroke();
290
- }
291
-
292
- /** The screen rectangle of a range within the frozen or the scrollable columns, or null. */
293
- private rangeRect(range: CellRange, frozen: boolean) {
294
- const c = this.controller;
295
- const left = frozen ? range.left : Math.max(range.left, c.frozen);
296
- const right = frozen ? Math.min(range.right, c.frozen - 1) : range.right;
297
- if (left > right) return null;
298
- const x0 = c.columnX(left);
299
- const x1 = c.columnX(right) + c.layout.width(right);
300
- const y0 = Math.max(c.options.headerHeight - 2, c.rowY(range.top));
301
- const y1 = Math.min(c.height + 2, c.rowY(range.bottom + 1));
302
- if (y1 <= y0) return null;
303
- return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
304
- }
305
-
306
- private drawSelection(frozen: boolean) {
307
- const c = this.controller;
308
- const ctx = this.context;
309
- const t = this.theme;
310
- if (c.rowCount === 0) return;
311
- const ranges = c.selection.ranges;
312
- const active = c.selection.active;
313
- const single = c.selection.single;
314
- const activeRect = this.rangeRect(
315
- { top: active.row, bottom: active.row, left: active.col, right: active.col },
316
- frozen,
317
- );
318
- if (!single) {
319
- ctx.fillStyle = t.selection;
320
- ranges.forEach((range) => {
321
- const rect = this.rangeRect(range, frozen);
322
- if (!rect) return;
323
- ctx.beginPath();
324
- ctx.rect(rect.x, rect.y, rect.width, rect.height);
325
- // The active cell stays unshaded inside the selection, as in spreadsheets.
326
- if (activeRect && rangeContains(range, active.row, active.col)) {
327
- ctx.rect(activeRect.x, activeRect.y, activeRect.width, activeRect.height);
328
- }
329
- ctx.fill("evenodd");
330
- });
331
- ctx.strokeStyle = t.selectionBorder;
332
- ranges.forEach((range, index) => {
333
- const rect = this.rangeRect(range, frozen);
334
- if (!rect) return;
335
- ctx.lineWidth = index === ranges.length - 1 ? 2 : 1;
336
- ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.width - 1, rect.height - 1);
337
- });
338
- }
339
- if (activeRect) {
340
- ctx.strokeStyle = t.activeBorder;
341
- ctx.lineWidth = 2;
342
- ctx.strokeRect(activeRect.x + 1, activeRect.y + 1, activeRect.width - 2, activeRect.height - 2);
343
- }
344
- }
345
-
346
- private drawHeaders(firstCol: number, lastCol: number) {
347
- const c = this.controller;
348
- const ctx = this.context;
349
- const t = this.theme;
350
- const height = c.options.headerHeight;
351
- ctx.fillStyle = t.headerBackground;
352
- ctx.fillRect(0, 0, c.width, height);
353
- const scrollLeft = c.gutterWidth + c.frozenWidth;
354
- this.clipped(scrollLeft, 0, c.width - scrollLeft, height, () => {
355
- for (let col = firstCol; col <= lastCol; col++) this.drawHeader(col);
356
- });
357
- this.clipped(c.gutterWidth, 0, c.frozenWidth, height, () => {
358
- for (let col = 0; col < c.frozen; col++) this.drawHeader(col);
359
- });
360
- ctx.strokeStyle = t.lineStrong;
361
- ctx.lineWidth = 1 / this.ratio;
362
- ctx.beginPath();
363
- const y = this.snap(height) - 1 / this.ratio;
364
- ctx.moveTo(0, y);
365
- ctx.lineTo(c.width, y);
366
- ctx.stroke();
367
- }
368
-
369
- private drawHeader(col: number) {
370
- const c = this.controller;
371
- const ctx = this.context;
372
- const t = this.theme;
373
- const state = c.visible[col];
374
- const schema = c.schema[state.index];
375
- if (!schema) return;
376
- const x = c.columnX(col);
377
- const width = c.layout.width(col);
378
- const height = c.options.headerHeight;
379
- const hovered = c.hover?.zone === "header" && c.hover.col === col;
380
- if (c.selection.colSelected(col)) {
381
- ctx.fillStyle = t.selection;
382
- ctx.fillRect(x, 0, width, height);
383
- } else if (c.selection.colTouched(col) || hovered) {
384
- ctx.fillStyle = t.headerActive;
385
- ctx.fillRect(x, 0, width, height);
386
- }
387
- ctx.textBaseline = "middle";
388
- ctx.textAlign = "left";
389
- // Indicators on the right: menu (while hovered or active), filter, sort.
390
- let right = x + width - 6;
391
- const sort = c.sortOf(state.index);
392
- const filtered = c.filterOf(state.index) !== null;
393
- if (hovered || c.selection.active.col === col) {
394
- if (hovered && c.hover?.menu) {
395
- ctx.fillStyle = t.headerActive;
396
- ctx.fillRect(right - 18, height / 2 - 9, 18, 18);
397
- }
398
- this.chevron(right - 9, height / 2, t.muted);
399
- right -= 20;
400
- }
401
- if (filtered) {
402
- this.funnel(right - 6, height / 2, t.accent);
403
- right -= 16;
404
- }
405
- if (sort) {
406
- this.triangle(right - 5, height / 2, sort.direction === "asc", t.accent);
407
- right -= 14;
408
- if ((c.query.sort?.length ?? 0) > 1) {
409
- ctx.font = this.smallText.font;
410
- ctx.fillStyle = t.accent;
411
- ctx.textAlign = "right";
412
- ctx.fillText(String(sort.order), right, height / 2 + 0.5);
413
- ctx.textAlign = "left";
414
- right -= 8;
415
- }
416
- right -= 2;
417
- }
418
- // The type mark gives way to the name in narrow columns.
419
- let start = x + PADDING;
420
- if (right - start - ICON_SPACE >= 28) {
421
- ctx.font = this.smallText.font;
422
- ctx.fillStyle = t.muted;
423
- ctx.fillText(typeMark(schema.type), start, height / 2 + 0.5);
424
- start += ICON_SPACE;
425
- }
426
- ctx.font = this.headerText.font;
427
- ctx.fillStyle = t.headerText;
428
- ctx.fillText(this.headerText.fit(schema.name, right - start), start, height / 2 + 0.5);
429
- // Separator.
430
- ctx.strokeStyle = t.line;
431
- ctx.lineWidth = 1 / this.ratio;
432
- ctx.beginPath();
433
- const line = this.snap(x + width) - 1 / this.ratio;
434
- ctx.moveTo(line, 6);
435
- ctx.lineTo(line, height - 6);
436
- ctx.stroke();
437
- }
438
-
439
- private chevron(x: number, y: number, color: string) {
440
- const ctx = this.context;
441
- ctx.strokeStyle = color;
442
- ctx.lineWidth = 1.5;
443
- ctx.beginPath();
444
- ctx.moveTo(x - 4, y - 2);
445
- ctx.lineTo(x, y + 2);
446
- ctx.lineTo(x + 4, y - 2);
447
- ctx.stroke();
448
- }
449
-
450
- private triangle(x: number, y: number, up: boolean, color: string) {
451
- const ctx = this.context;
452
- ctx.fillStyle = color;
453
- ctx.beginPath();
454
- if (up) {
455
- ctx.moveTo(x - 4.5, y + 3);
456
- ctx.lineTo(x + 4.5, y + 3);
457
- ctx.lineTo(x, y - 4);
458
- } else {
459
- ctx.moveTo(x - 4.5, y - 3);
460
- ctx.lineTo(x + 4.5, y - 3);
461
- ctx.lineTo(x, y + 4);
462
- }
463
- ctx.closePath();
464
- ctx.fill();
465
- }
466
-
467
- private funnel(x: number, y: number, color: string) {
468
- const ctx = this.context;
469
- ctx.fillStyle = color;
470
- ctx.beginPath();
471
- ctx.moveTo(x - 5.5, y - 5);
472
- ctx.lineTo(x + 5.5, y - 5);
473
- ctx.lineTo(x + 1.2, y);
474
- ctx.lineTo(x + 1.2, y + 5);
475
- ctx.lineTo(x - 1.2, y + 3.5);
476
- ctx.lineTo(x - 1.2, y);
477
- ctx.closePath();
478
- ctx.fill();
479
- }
480
-
481
- private drawGutter(firstRow: number, lastRow: number) {
482
- const c = this.controller;
483
- const ctx = this.context;
484
- const t = this.theme;
485
- const width = c.gutterWidth;
486
- const top = c.options.headerHeight;
487
- const rowHeight = c.options.rowHeight;
488
- ctx.fillStyle = t.gutterBackground;
489
- ctx.fillRect(0, top, width, c.height - top);
490
- this.clipped(0, top, width, c.height - top, () => {
491
- ctx.font = this.smallText.font;
492
- ctx.textBaseline = "middle";
493
- ctx.textAlign = "right";
494
- const viewed = c.view.id !== null;
495
- for (let row = firstRow; row < lastRow; row++) {
496
- const y = c.rowY(row);
497
- if (c.selection.rowSelected(row)) {
498
- ctx.fillStyle = t.selection;
499
- ctx.fillRect(0, y, width, rowHeight);
500
- } else if (c.selection.rowTouched(row)) {
501
- ctx.fillStyle = t.headerActive;
502
- ctx.fillRect(0, y, width, rowHeight);
503
- }
504
- const label = c.rowLabel(row);
505
- if (label === null) continue;
506
- ctx.fillStyle = viewed ? t.gutterViewText : t.gutterText;
507
- ctx.fillText(String(label), width - 8, y + rowHeight / 2 + 0.5);
508
- }
509
- });
510
- ctx.strokeStyle = t.lineStrong;
511
- ctx.lineWidth = 1 / this.ratio;
512
- ctx.beginPath();
513
- const x = this.snap(width) - 1 / this.ratio;
514
- ctx.moveTo(x, 0);
515
- ctx.lineTo(x, c.height);
516
- ctx.stroke();
517
- }
518
-
519
- private drawCorner() {
520
- const c = this.controller;
521
- const ctx = this.context;
522
- const t = this.theme;
523
- const width = c.gutterWidth;
524
- const height = c.options.headerHeight;
525
- ctx.fillStyle = c.hover?.zone === "corner" ? t.headerActive : t.headerBackground;
526
- ctx.fillRect(0, 0, width, height);
527
- ctx.fillStyle = t.muted;
528
- ctx.beginPath();
529
- ctx.moveTo(width - 6, height - 14);
530
- ctx.lineTo(width - 6, height - 6);
531
- ctx.lineTo(width - 14, height - 6);
532
- ctx.closePath();
533
- ctx.fill();
534
- }
535
- }
package/src/text.ts DELETED
@@ -1,55 +0,0 @@
1
- const ELLIPSIS = "…";
2
- const MAX_ENTRIES = 20_000;
3
-
4
- /** Measures and shortens text for one font, caching results because the same strings repeat a lot. */
5
- export class TextFitter {
6
- private readonly widths = new Map<string, number>();
7
- private readonly fits = new Map<string, string>();
8
- private ellipsisWidth = -1;
9
-
10
- constructor(
11
- private readonly context: CanvasRenderingContext2D,
12
- readonly font: string,
13
- ) {}
14
-
15
- /** Width of `text`. The caller must have set `context.font` to this fitter's font. */
16
- width(text: string): number {
17
- let width = this.widths.get(text);
18
- if (width === undefined) {
19
- width = this.context.measureText(text).width;
20
- if (this.widths.size > MAX_ENTRIES) this.widths.clear();
21
- this.widths.set(text, width);
22
- }
23
- return width;
24
- }
25
-
26
- /** `text`, shortened with "…" to fit in `maxWidth`. */
27
- fit(text: string, maxWidth: number): string {
28
- if (maxWidth <= 0 || text === "") return "";
29
- // Cells never show more than a few hundred characters; skip measuring the rest.
30
- const source = text.length > 400 ? text.slice(0, 400) : text;
31
- if (source === text && this.width(text) <= maxWidth) return text;
32
- const key = `${Math.floor(maxWidth)}\u0001${source}`;
33
- const cached = this.fits.get(key);
34
- if (cached !== undefined) return cached;
35
- if (this.ellipsisWidth < 0) this.ellipsisWidth = this.context.measureText(ELLIPSIS).width;
36
- let result = "";
37
- if (this.ellipsisWidth <= maxWidth) {
38
- const full = this.context.measureText(source).width;
39
- let low = 0;
40
- let high = Math.min(source.length, Math.ceil((source.length * maxWidth) / Math.max(1, full)) + 4);
41
- while (low < high) {
42
- const middle = (low + high + 1) >> 1;
43
- if (this.context.measureText(source.slice(0, middle)).width + this.ellipsisWidth <= maxWidth) low = middle;
44
- else high = middle - 1;
45
- }
46
- // Do not split a surrogate pair.
47
- const code = source.charCodeAt(low - 1);
48
- if (low > 0 && code >= 0xd800 && code <= 0xdbff) low--;
49
- result = `${source.slice(0, low)}${ELLIPSIS}`;
50
- }
51
- if (this.fits.size > MAX_ENTRIES) this.fits.clear();
52
- this.fits.set(key, result);
53
- return result;
54
- }
55
- }