virtual-tree-canvas 0.3.9 → 0.4.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.
@@ -6,6 +6,8 @@ export class TreeWorkerClient {
6
6
  this.pending = new Map();
7
7
  this.ready = Promise.resolve();
8
8
  this.worker.addEventListener('message', this.#onMessage);
9
+ this.worker.addEventListener('error', this.#onError);
10
+ this.worker.addEventListener('messageerror', this.#onMessageError);
9
11
  }
10
12
 
11
13
  setData(nodes) {
@@ -27,6 +29,8 @@ export class TreeWorkerClient {
27
29
  for (const { reject } of this.pending.values()) reject(new Error('Tree worker destroyed'));
28
30
  this.pending.clear();
29
31
  this.worker.removeEventListener('message', this.#onMessage);
32
+ this.worker.removeEventListener('error', this.#onError);
33
+ this.worker.removeEventListener('messageerror', this.#onMessageError);
30
34
  this.worker.terminate();
31
35
  }
32
36
 
@@ -47,4 +51,17 @@ export class TreeWorkerClient {
47
51
  if (ok) pending.resolve(result);
48
52
  else pending.reject(new Error(error || 'Tree worker request failed'));
49
53
  };
54
+
55
+ #onError = (event) => {
56
+ this.#rejectPending(new Error(event.message || 'Tree worker failed'));
57
+ };
58
+
59
+ #onMessageError = () => {
60
+ this.#rejectPending(new Error('Tree worker returned an unreadable message'));
61
+ };
62
+
63
+ #rejectPending(error) {
64
+ for (const { reject } of this.pending.values()) reject(error);
65
+ this.pending.clear();
66
+ }
50
67
  }
package/src/index.d.ts CHANGED
@@ -1,5 +1,17 @@
1
1
  export type TreeViewAlign = 'start' | 'center' | 'end' | 'nearest';
2
2
 
3
+ export type IconDrawFunction = (ctx: CanvasRenderingContext2D, x: number, y: number, size: number, color: string) => void;
4
+ export type IconSource = string | CanvasImageSource | IconDrawFunction;
5
+
6
+ export class IconRegistry {
7
+ constructor(options?: { pixelRatio?: number });
8
+ register(name: string, icon: IconSource): any;
9
+ get(name: string): any;
10
+ onChange(listener: () => void): () => void;
11
+ prepare(options?: { icons?: Iterable<string>; size?: number; color?: string; pixelRatio?: number }): Promise<any[]>;
12
+ draw(ctx: CanvasRenderingContext2D, name: string, x: number, y: number, size: number, color: string): void;
13
+ }
14
+
3
15
  export type TreeNode = {
4
16
  id: string;
5
17
  parentId?: string | null;
@@ -86,6 +98,7 @@ export class TreeViewController {
86
98
  setDynamicState(patches: DynamicPatch[]): void;
87
99
  setTheme(theme: any): void;
88
100
  setLayoutMetrics(options?: { rowHeight?: number; indentWidth?: number; headerHeight?: number }): void;
101
+ registerIcon(name: string, icon: IconSource): any;
89
102
  resize(width: number, height: number): void;
90
103
  render(time?: number): void;
91
104
  renderMeasured(time?: number): any;
@@ -9,6 +9,9 @@ export class TreeRowRenderer {
9
9
  this.scene = null;
10
10
  this.iconRegistry = iconRegistry ?? new IconRegistry();
11
11
  this.renderedRows = 0;
12
+ this.iconRenderQueued = false;
13
+ this.preparedIconThemeKey = null;
14
+ this.stopIconListener = this.iconRegistry.onChange?.(() => this.#scheduleIconRender()) ?? null;
12
15
  }
13
16
 
14
17
  /** @param {HTMLCanvasElement} canvas */
@@ -22,6 +25,14 @@ export class TreeRowRenderer {
22
25
  this.scene = scene;
23
26
  }
24
27
 
28
+ destroy() {
29
+ this.stopIconListener?.();
30
+ this.stopIconListener = null;
31
+ this.canvas = null;
32
+ this.ctx = null;
33
+ this.scene = null;
34
+ }
35
+
25
36
  updateDynamicState(_patches) {
26
37
  // Canvas2D reads the state map directly; patches still stay on the hot path.
27
38
  }
@@ -43,6 +54,7 @@ export class TreeRowRenderer {
43
54
 
44
55
  const ctx = this.ctx;
45
56
  const colors = theme.colors;
57
+ this.#prepareThemeIcons(theme, dpr);
46
58
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
47
59
  ctx.fillStyle = colors.background;
48
60
  ctx.fillRect(0, 0, viewportWidth, viewportHeight);
@@ -55,6 +67,29 @@ export class TreeRowRenderer {
55
67
  ctx.restore();
56
68
  }
57
69
 
70
+ #scheduleIconRender() {
71
+ if (this.iconRenderQueued) return;
72
+ this.iconRenderQueued = true;
73
+ const schedule = globalThis.requestAnimationFrame ?? ((callback) => setTimeout(callback, 0));
74
+ schedule(() => {
75
+ this.iconRenderQueued = false;
76
+ this.render();
77
+ });
78
+ }
79
+
80
+ #prepareThemeIcons(theme, pixelRatio) {
81
+ const iconColors = new Map([['placeholder', theme.colors.textMuted]]);
82
+ for (const style of Object.values(theme.types ?? {})) {
83
+ if (style?.icon) iconColors.set(style.icon, style.color ?? theme.colors.progressFill);
84
+ }
85
+ const key = `${pixelRatio}|${[...iconColors].map(([icon, color]) => `${icon}:${color}`).join(',')}`;
86
+ if (key === this.preparedIconThemeKey) return;
87
+ this.preparedIconThemeKey = key;
88
+ for (const [icon, color] of iconColors) {
89
+ this.iconRegistry.prepare?.({ icons: [icon], size: 15, color, pixelRatio });
90
+ }
91
+ }
92
+
58
93
  #drawHeader(ctx) {
59
94
  const { viewport, columns, theme, sort, headerFilter, filterQuery } = this.scene;
60
95
  if (viewport.headerHeight <= 0) return;
@@ -113,6 +113,7 @@ export class TreeViewController {
113
113
  this.#nativeScroll = null;
114
114
  this.inputController = null;
115
115
  this.cellEditor = null;
116
+ this.renderer?.destroy?.();
116
117
  this.canvas = null;
117
118
  }
118
119
 
@@ -212,8 +213,14 @@ export class TreeViewController {
212
213
 
213
214
  enableWorkers(workerUrl) {
214
215
  if (this.workerClient) return Promise.resolve(this);
215
- this.workerClient = new TreeWorkerClient(workerUrl);
216
- return this.workerClient.setData(this.model.nodes).then(() => this);
216
+ const client = new TreeWorkerClient(workerUrl);
217
+ this.workerClient = client;
218
+ return client.setData(this.model.nodes)
219
+ .then(() => this)
220
+ .catch((error) => {
221
+ if (this.workerClient === client) this.disableWorkers();
222
+ throw error;
223
+ });
217
224
  }
218
225
 
219
226
  disableWorkers() {