@gtkx/react 0.18.9 → 0.19.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 (174) hide show
  1. package/dist/generated/internal.d.ts +6 -0
  2. package/dist/generated/internal.d.ts.map +1 -1
  3. package/dist/generated/internal.js +331 -44
  4. package/dist/generated/internal.js.map +1 -1
  5. package/dist/generated/jsx.d.ts +178 -2
  6. package/dist/generated/jsx.d.ts.map +1 -1
  7. package/dist/generated/jsx.js.map +1 -1
  8. package/dist/host-config.d.ts.map +1 -1
  9. package/dist/host-config.js +46 -10
  10. package/dist/host-config.js.map +1 -1
  11. package/dist/jsx.d.ts +133 -13
  12. package/dist/jsx.d.ts.map +1 -1
  13. package/dist/jsx.js +41 -2
  14. package/dist/jsx.js.map +1 -1
  15. package/dist/metadata.d.ts +1 -0
  16. package/dist/metadata.d.ts.map +1 -1
  17. package/dist/metadata.js +3 -1
  18. package/dist/metadata.js.map +1 -1
  19. package/dist/node.d.ts +2 -0
  20. package/dist/node.d.ts.map +1 -1
  21. package/dist/node.js +22 -6
  22. package/dist/node.js.map +1 -1
  23. package/dist/nodes/column-view-column.d.ts +4 -1
  24. package/dist/nodes/column-view-column.d.ts.map +1 -1
  25. package/dist/nodes/column-view-column.js +29 -8
  26. package/dist/nodes/column-view-column.js.map +1 -1
  27. package/dist/nodes/column-view.d.ts +4 -3
  28. package/dist/nodes/column-view.d.ts.map +1 -1
  29. package/dist/nodes/column-view.js +44 -14
  30. package/dist/nodes/column-view.js.map +1 -1
  31. package/dist/nodes/drop-down.d.ts +12 -2
  32. package/dist/nodes/drop-down.d.ts.map +1 -1
  33. package/dist/nodes/drop-down.js +151 -5
  34. package/dist/nodes/drop-down.js.map +1 -1
  35. package/dist/nodes/event-controller.d.ts +1 -0
  36. package/dist/nodes/event-controller.d.ts.map +1 -1
  37. package/dist/nodes/event-controller.js +11 -3
  38. package/dist/nodes/event-controller.js.map +1 -1
  39. package/dist/nodes/fixed-child.d.ts +1 -2
  40. package/dist/nodes/fixed-child.d.ts.map +1 -1
  41. package/dist/nodes/fixed-child.js +21 -21
  42. package/dist/nodes/fixed-child.js.map +1 -1
  43. package/dist/nodes/font-dialog-button.d.ts +1 -1
  44. package/dist/nodes/font-dialog-button.d.ts.map +1 -1
  45. package/dist/nodes/font-dialog-button.js +8 -0
  46. package/dist/nodes/font-dialog-button.js.map +1 -1
  47. package/dist/nodes/grid-view.d.ts +6 -5
  48. package/dist/nodes/grid-view.d.ts.map +1 -1
  49. package/dist/nodes/grid-view.js +23 -18
  50. package/dist/nodes/grid-view.js.map +1 -1
  51. package/dist/nodes/internal/accessible.d.ts +5 -0
  52. package/dist/nodes/internal/accessible.d.ts.map +1 -0
  53. package/dist/nodes/internal/accessible.js +119 -0
  54. package/dist/nodes/internal/accessible.js.map +1 -0
  55. package/dist/nodes/internal/base-item-renderer.d.ts.map +1 -1
  56. package/dist/nodes/internal/base-item-renderer.js +0 -1
  57. package/dist/nodes/internal/base-item-renderer.js.map +1 -1
  58. package/dist/nodes/internal/construct.d.ts +10 -0
  59. package/dist/nodes/internal/construct.d.ts.map +1 -0
  60. package/dist/nodes/internal/construct.js +68 -0
  61. package/dist/nodes/internal/construct.js.map +1 -0
  62. package/dist/nodes/internal/header-item-renderer.d.ts +23 -0
  63. package/dist/nodes/internal/header-item-renderer.d.ts.map +1 -0
  64. package/dist/nodes/internal/header-item-renderer.js +87 -0
  65. package/dist/nodes/internal/header-item-renderer.js.map +1 -0
  66. package/dist/nodes/internal/header-renderer-manager.d.ts +13 -0
  67. package/dist/nodes/internal/header-renderer-manager.d.ts.map +1 -0
  68. package/dist/nodes/internal/header-renderer-manager.js +20 -0
  69. package/dist/nodes/internal/header-renderer-manager.js.map +1 -0
  70. package/dist/nodes/internal/list-store.d.ts +10 -11
  71. package/dist/nodes/internal/list-store.d.ts.map +1 -1
  72. package/dist/nodes/internal/list-store.js +28 -29
  73. package/dist/nodes/internal/list-store.js.map +1 -1
  74. package/dist/nodes/internal/sectioned-list-store.d.ts +50 -0
  75. package/dist/nodes/internal/sectioned-list-store.d.ts.map +1 -0
  76. package/dist/nodes/internal/sectioned-list-store.js +250 -0
  77. package/dist/nodes/internal/sectioned-list-store.js.map +1 -0
  78. package/dist/nodes/internal/selection-helpers.d.ts +12 -0
  79. package/dist/nodes/internal/selection-helpers.d.ts.map +1 -0
  80. package/dist/nodes/internal/selection-helpers.js +25 -0
  81. package/dist/nodes/internal/selection-helpers.js.map +1 -0
  82. package/dist/nodes/internal/selection-model-controller.d.ts.map +1 -1
  83. package/dist/nodes/internal/selection-model-controller.js +3 -0
  84. package/dist/nodes/internal/selection-model-controller.js.map +1 -1
  85. package/dist/nodes/internal/simple-list-store.d.ts +7 -12
  86. package/dist/nodes/internal/simple-list-store.d.ts.map +1 -1
  87. package/dist/nodes/internal/simple-list-store.js +58 -35
  88. package/dist/nodes/internal/simple-list-store.js.map +1 -1
  89. package/dist/nodes/internal/text-buffer-controller.d.ts +4 -0
  90. package/dist/nodes/internal/text-buffer-controller.d.ts.map +1 -1
  91. package/dist/nodes/internal/text-buffer-controller.js +49 -9
  92. package/dist/nodes/internal/text-buffer-controller.js.map +1 -1
  93. package/dist/nodes/internal/tree-store.d.ts +3 -0
  94. package/dist/nodes/internal/tree-store.d.ts.map +1 -1
  95. package/dist/nodes/internal/tree-store.js +55 -10
  96. package/dist/nodes/internal/tree-store.js.map +1 -1
  97. package/dist/nodes/list-section.d.ts +27 -0
  98. package/dist/nodes/list-section.d.ts.map +1 -0
  99. package/dist/nodes/list-section.js +43 -0
  100. package/dist/nodes/list-section.js.map +1 -0
  101. package/dist/nodes/list-view.d.ts +6 -3
  102. package/dist/nodes/list-view.d.ts.map +1 -1
  103. package/dist/nodes/list-view.js +54 -14
  104. package/dist/nodes/list-view.js.map +1 -1
  105. package/dist/nodes/models/list.d.ts +13 -5
  106. package/dist/nodes/models/list.d.ts.map +1 -1
  107. package/dist/nodes/models/list.js +135 -21
  108. package/dist/nodes/models/list.js.map +1 -1
  109. package/dist/nodes/shortcut.d.ts +3 -2
  110. package/dist/nodes/shortcut.d.ts.map +1 -1
  111. package/dist/nodes/shortcut.js +19 -4
  112. package/dist/nodes/shortcut.js.map +1 -1
  113. package/dist/nodes/text-anchor.d.ts.map +1 -1
  114. package/dist/nodes/text-anchor.js +7 -1
  115. package/dist/nodes/text-anchor.js.map +1 -1
  116. package/dist/nodes/text-tag.d.ts.map +1 -1
  117. package/dist/nodes/text-tag.js +5 -1
  118. package/dist/nodes/text-tag.js.map +1 -1
  119. package/dist/nodes/text-view.d.ts +1 -0
  120. package/dist/nodes/text-view.d.ts.map +1 -1
  121. package/dist/nodes/text-view.js +4 -0
  122. package/dist/nodes/text-view.js.map +1 -1
  123. package/dist/nodes/widget.d.ts +0 -2
  124. package/dist/nodes/widget.d.ts.map +1 -1
  125. package/dist/nodes/widget.js +44 -61
  126. package/dist/nodes/widget.js.map +1 -1
  127. package/dist/registry.d.ts.map +1 -1
  128. package/dist/registry.js +2 -2
  129. package/dist/registry.js.map +1 -1
  130. package/package.json +3 -3
  131. package/src/generated/internal.ts +333 -44
  132. package/src/generated/jsx.ts +178 -2
  133. package/src/host-config.ts +41 -10
  134. package/src/jsx.ts +166 -15
  135. package/src/metadata.ts +5 -1
  136. package/src/node.ts +20 -6
  137. package/src/nodes/column-view-column.ts +32 -8
  138. package/src/nodes/column-view.ts +59 -14
  139. package/src/nodes/drop-down.ts +182 -6
  140. package/src/nodes/event-controller.ts +11 -3
  141. package/src/nodes/fixed-child.ts +24 -23
  142. package/src/nodes/font-dialog-button.ts +10 -0
  143. package/src/nodes/grid-view.ts +29 -19
  144. package/src/nodes/internal/accessible.ts +156 -0
  145. package/src/nodes/internal/base-item-renderer.ts +0 -1
  146. package/src/nodes/internal/construct.ts +90 -0
  147. package/src/nodes/internal/header-item-renderer.ts +105 -0
  148. package/src/nodes/internal/header-renderer-manager.ts +33 -0
  149. package/src/nodes/internal/list-store.ts +32 -30
  150. package/src/nodes/internal/sectioned-list-store.ts +287 -0
  151. package/src/nodes/internal/selection-helpers.ts +35 -0
  152. package/src/nodes/internal/selection-model-controller.ts +4 -0
  153. package/src/nodes/internal/simple-list-store.ts +60 -43
  154. package/src/nodes/internal/text-buffer-controller.ts +51 -8
  155. package/src/nodes/internal/tree-store.ts +61 -9
  156. package/src/nodes/list-section.ts +64 -0
  157. package/src/nodes/list-view.ts +65 -14
  158. package/src/nodes/models/list.ts +147 -37
  159. package/src/nodes/shortcut.ts +22 -5
  160. package/src/nodes/text-anchor.ts +6 -1
  161. package/src/nodes/text-tag.ts +7 -1
  162. package/src/nodes/text-view.ts +5 -0
  163. package/src/nodes/widget.ts +45 -62
  164. package/src/registry.ts +4 -2
  165. package/dist/nodes/models/grid.d.ts +0 -28
  166. package/dist/nodes/models/grid.d.ts.map +0 -1
  167. package/dist/nodes/models/grid.js +0 -69
  168. package/dist/nodes/models/grid.js.map +0 -1
  169. package/dist/nodes/shortcut-controller.d.ts +0 -10
  170. package/dist/nodes/shortcut-controller.d.ts.map +0 -1
  171. package/dist/nodes/shortcut-controller.js +0 -23
  172. package/dist/nodes/shortcut-controller.js.map +0 -1
  173. package/src/nodes/models/grid.ts +0 -105
  174. package/src/nodes/shortcut-controller.ts +0 -27
@@ -1,24 +1,44 @@
1
- import * as Gtk from "@gtkx/ffi/gtk";
1
+ import { SectionedListStore } from "./sectioned-list-store.js";
2
2
 
3
- export class SimpleListStore {
4
- private ids: string[] = [];
5
- private idToIndex = new Map<string, number>();
6
- private model = new Gtk.StringList();
7
- private pendingBatch: string[] | null = null;
3
+ export class SimpleListStore extends SectionedListStore {
4
+ private batchMode = false;
8
5
 
9
- public beginBatch(): void {
10
- this.pendingBatch = [];
6
+ public override beginBatch(): void {
7
+ this.batchMode = true;
8
+ super.beginBatch();
11
9
  }
12
10
 
13
- public flushBatch(): void {
14
- const batch = this.pendingBatch;
15
- this.pendingBatch = null;
16
- if (batch && batch.length > 0) {
17
- this.model.splice(0, 0, batch);
11
+ public override flushBatch(): void {
12
+ this.batchMode = false;
13
+ super.flushBatch();
14
+ }
15
+
16
+ protected override getInitialPendingBatch(): string[] | null {
17
+ return this.batchMode ? [] : null;
18
+ }
19
+
20
+ protected override getModelString(_itemId: string, item: unknown): string {
21
+ return item as string;
22
+ }
23
+
24
+ public getHeaderValueByLabel(label: string): unknown {
25
+ for (const section of this.sections) {
26
+ if (section.model.getNItems() > 0) {
27
+ const firstLabel = section.model.getString(0);
28
+ if (firstLabel === label) {
29
+ return this.headerValues.get(section.id);
30
+ }
31
+ }
18
32
  }
33
+ return undefined;
19
34
  }
20
35
 
21
36
  public addItem(id: string, label: string): void {
37
+ this.flushRemovals();
38
+ if (this.sectioned) {
39
+ return;
40
+ }
41
+
22
42
  this.idToIndex.set(id, this.ids.length);
23
43
  this.ids.push(id);
24
44
 
@@ -30,6 +50,7 @@ export class SimpleListStore {
30
50
  }
31
51
 
32
52
  public appendItem(id: string, label: string): void {
53
+ this.flushRemovals();
33
54
  const existingIndex = this.idToIndex.get(id);
34
55
 
35
56
  if (existingIndex !== undefined) {
@@ -43,17 +64,8 @@ export class SimpleListStore {
43
64
  this.model.append(label);
44
65
  }
45
66
 
46
- public removeItem(id: string): void {
47
- const index = this.idToIndex.get(id);
48
- if (index === undefined) return;
49
-
50
- this.model.remove(index);
51
- this.ids.splice(index, 1);
52
- this.idToIndex.delete(id);
53
- this.rebuildIndices(index);
54
- }
55
-
56
67
  public insertItemBefore(id: string, beforeId: string, label: string): void {
68
+ this.flushRemovals();
57
69
  const beforeIndex = this.idToIndex.get(beforeId);
58
70
  if (beforeIndex === undefined) {
59
71
  this.addItem(id, label);
@@ -64,7 +76,20 @@ export class SimpleListStore {
64
76
  }
65
77
  }
66
78
 
67
- public updateItem(id: string, label: string): void {
79
+ public updateItem(id: string, item: unknown): void {
80
+ this.flushRemovals();
81
+ const label = item as string;
82
+ if (this.sectioned) {
83
+ const sectionId = this.itemToSection.get(id);
84
+ if (!sectionId) return;
85
+ const section = this.sectionById.get(sectionId);
86
+ if (!section) return;
87
+ const indexInSection = section.itemIds.indexOf(id);
88
+ if (indexInSection >= 0) {
89
+ section.model.splice(indexInSection, 1, [label]);
90
+ }
91
+ return;
92
+ }
68
93
  const index = this.idToIndex.get(id);
69
94
  if (index === undefined) {
70
95
  this.addItem(id, label);
@@ -73,27 +98,19 @@ export class SimpleListStore {
73
98
  this.model.splice(index, 1, [label]);
74
99
  }
75
100
 
76
- public getItem(id: string) {
101
+ public getItem(id: string): string | null {
102
+ this.flushRemovals();
103
+ if (this.sectioned) {
104
+ const sectionId = this.itemToSection.get(id);
105
+ if (!sectionId) return null;
106
+ const section = this.sectionById.get(sectionId);
107
+ if (!section) return null;
108
+ const indexInSection = section.itemIds.indexOf(id);
109
+ if (indexInSection < 0) return null;
110
+ return section.model.getString(indexInSection);
111
+ }
77
112
  const index = this.idToIndex.get(id);
78
113
  if (index === undefined) return null;
79
114
  return this.model.getString(index);
80
115
  }
81
-
82
- public getIdAtIndex(index: number): string | null {
83
- return this.ids[index] ?? null;
84
- }
85
-
86
- public getIndexById(id: string): number | null {
87
- return this.idToIndex.get(id) ?? null;
88
- }
89
-
90
- public getModel(): Gtk.StringList {
91
- return this.model;
92
- }
93
-
94
- private rebuildIndices(fromIndex: number): void {
95
- for (let i = fromIndex; i < this.ids.length; i++) {
96
- this.idToIndex.set(this.ids[i] as string, i);
97
- }
98
- }
99
116
  }
@@ -18,6 +18,8 @@ type BufferProps = Pick<GtkTextViewProps, "enableUndo"> & BufferCallbackProps;
18
18
  export class TextBufferController<TBuffer extends Gtk.TextBuffer = Gtk.TextBuffer> {
19
19
  private buffer: TBuffer | null = null;
20
20
  private textChildren: TextContentChild[] = [];
21
+ private initialMount = true;
22
+ private irreversibleStarted = false;
21
23
 
22
24
  constructor(
23
25
  private readonly owner: Node & TextContentParent,
@@ -41,6 +43,14 @@ export class TextBufferController<TBuffer extends Gtk.TextBuffer = Gtk.TextBuffe
41
43
  return this.buffer;
42
44
  }
43
45
 
46
+ finalizeInitialMount(): void {
47
+ if (this.irreversibleStarted) {
48
+ this.buffer?.endIrreversibleAction();
49
+ this.irreversibleStarted = false;
50
+ }
51
+ this.initialMount = false;
52
+ }
53
+
44
54
  applyOwnProps(oldProps: BufferProps | null, newProps: BufferProps): void {
45
55
  const hasBufferProps =
46
56
  newProps.enableUndo !== undefined ||
@@ -130,6 +140,11 @@ export class TextBufferController<TBuffer extends Gtk.TextBuffer = Gtk.TextBuffe
130
140
  appendChild(child: TextContentChild): void {
131
141
  const buffer = this.ensureBuffer();
132
142
 
143
+ if (this.initialMount && !this.irreversibleStarted) {
144
+ buffer.beginIrreversibleAction();
145
+ this.irreversibleStarted = true;
146
+ }
147
+
133
148
  const wasMoved = this.textChildren.indexOf(child) !== -1;
134
149
  if (wasMoved) {
135
150
  const existingIndex = this.textChildren.indexOf(child);
@@ -158,6 +173,7 @@ export class TextBufferController<TBuffer extends Gtk.TextBuffer = Gtk.TextBuffe
158
173
  if (!child.hasBuffer()) {
159
174
  child.setBuffer(buffer);
160
175
  }
176
+ this.setupEmbeddedObjects(child);
161
177
  } else if (child instanceof TextAnchorNode) {
162
178
  child.setTextViewAndBuffer(this.container, buffer);
163
179
  } else if (child instanceof TextPaintableNode) {
@@ -173,6 +189,11 @@ export class TextBufferController<TBuffer extends Gtk.TextBuffer = Gtk.TextBuffe
173
189
  insertBefore(child: TextContentChild, before: TextContentChild): void {
174
190
  const buffer = this.ensureBuffer();
175
191
 
192
+ if (this.initialMount && !this.irreversibleStarted) {
193
+ buffer.beginIrreversibleAction();
194
+ this.irreversibleStarted = true;
195
+ }
196
+
176
197
  const existingIndex = this.textChildren.indexOf(child);
177
198
  if (existingIndex !== -1) {
178
199
  const oldOffset = child.getBufferOffset();
@@ -207,6 +228,7 @@ export class TextBufferController<TBuffer extends Gtk.TextBuffer = Gtk.TextBuffe
207
228
  if (!child.hasBuffer()) {
208
229
  child.setBuffer(buffer);
209
230
  }
231
+ this.setupEmbeddedObjects(child);
210
232
  } else if (child instanceof TextAnchorNode) {
211
233
  child.setTextViewAndBuffer(this.container, buffer);
212
234
  } else if (child instanceof TextPaintableNode) {
@@ -234,6 +256,21 @@ export class TextBufferController<TBuffer extends Gtk.TextBuffer = Gtk.TextBuffe
234
256
  this.reapplyTagsFromOffset(offset);
235
257
  }
236
258
 
259
+ private setupEmbeddedObjects(tag: TextTagNode): void {
260
+ if (!this.buffer) return;
261
+
262
+ for (const child of tag.children) {
263
+ if (child instanceof TextPaintableNode || child instanceof TextAnchorNode) {
264
+ this.deleteTextAtRange(child.getBufferOffset(), child.getBufferOffset() + 1);
265
+ child.setTextViewAndBuffer(this.container, this.buffer);
266
+ } else if (child instanceof TextTagNode) {
267
+ this.setupEmbeddedObjects(child);
268
+ }
269
+ }
270
+
271
+ tag.reapplyTag();
272
+ }
273
+
237
274
  private getTotalLength(): number {
238
275
  let length = 0;
239
276
  for (const child of this.textChildren) {
@@ -248,7 +285,7 @@ export class TextBufferController<TBuffer extends Gtk.TextBuffer = Gtk.TextBuffe
248
285
 
249
286
  const iter = new Gtk.TextIter();
250
287
  buffer.getIterAtOffset(iter, offset);
251
- buffer.insert(iter, text, text.length);
288
+ buffer.insert(iter, text, -1);
252
289
  }
253
290
 
254
291
  private deleteTextAtRange(start: number, end: number): void {
@@ -292,10 +329,7 @@ export class TextBufferController<TBuffer extends Gtk.TextBuffer = Gtk.TextBuffe
292
329
  private reapplyTagsFromOffset(fromOffset: number): void {
293
330
  for (const child of this.textChildren) {
294
331
  if (child instanceof TextTagNode) {
295
- if (child.getBufferOffset() >= fromOffset) {
296
- child.reapplyTag();
297
- this.reapplyAllTagsRecursive(child.children);
298
- } else if (child.getBufferOffset() + child.getLength() > fromOffset) {
332
+ if (child.getBufferOffset() + child.getLength() > fromOffset) {
299
333
  child.reapplyTag();
300
334
  this.reapplyAllTagsRecursive(child.children);
301
335
  }
@@ -320,9 +354,18 @@ export class TextBufferController<TBuffer extends Gtk.TextBuffer = Gtk.TextBuffe
320
354
  onChildInserted(child: TextContentChild): void {
321
355
  if (!this.buffer) return;
322
356
 
323
- const text = child.getText();
324
- if (text.length > 0) {
325
- this.insertTextAtOffset(text, child.getBufferOffset());
357
+ if (child instanceof TextPaintableNode) {
358
+ child.setTextViewAndBuffer(this.container, this.buffer);
359
+ } else if (child instanceof TextAnchorNode) {
360
+ child.setTextViewAndBuffer(this.container, this.buffer);
361
+ } else {
362
+ const text = child.getText();
363
+ if (text.length > 0) {
364
+ this.insertTextAtOffset(text, child.getBufferOffset());
365
+ }
366
+ if (child instanceof TextTagNode) {
367
+ this.setupEmbeddedObjects(child);
368
+ }
326
369
  }
327
370
 
328
371
  const containingIndex = this.findDirectChildContaining(child.getBufferOffset());
@@ -19,6 +19,8 @@ export class TreeStore {
19
19
  private items = new Map<string, TreeItemData>();
20
20
  private onItemUpdated: TreeItemUpdatedCallback | null = null;
21
21
  private pendingBatch: string[] | null = null;
22
+ private pendingRemovals: Set<string> | null = null;
23
+ private flushScheduled = false;
22
24
 
23
25
  public setOnItemUpdated(callback: TreeItemUpdatedCallback | null): void {
24
26
  this.onItemUpdated = callback;
@@ -46,6 +48,7 @@ export class TreeStore {
46
48
  }
47
49
 
48
50
  public addItem(id: string, data: TreeItemData, parentId?: string): void {
51
+ if (parentId === undefined) this.flushRemovals();
49
52
  this.items.set(id, data);
50
53
 
51
54
  if (parentId === undefined) {
@@ -104,15 +107,7 @@ export class TreeStore {
104
107
  this.childIdToIndex.delete(id);
105
108
  this.childModels.delete(id);
106
109
 
107
- if (parentId === undefined) {
108
- const index = this.rootIdToIndex.get(id);
109
- if (index !== undefined) {
110
- this.rootIds.splice(index, 1);
111
- this.rootIdToIndex.delete(id);
112
- this.rebuildRootIndices(index);
113
- this.rootModel.remove(index);
114
- }
115
- } else {
110
+ if (parentId !== undefined) {
116
111
  const siblings = this.children.get(parentId);
117
112
  const indexMap = this.childIdToIndex.get(parentId);
118
113
  if (siblings && indexMap) {
@@ -131,10 +126,67 @@ export class TreeStore {
131
126
  this.childIdToIndex.delete(parentId);
132
127
  }
133
128
  }
129
+ return;
130
+ }
131
+
132
+ if (!this.rootIdToIndex.has(id)) return;
133
+
134
+ if (!this.pendingRemovals) {
135
+ this.pendingRemovals = new Set();
136
+ if (!this.flushScheduled) {
137
+ this.flushScheduled = true;
138
+ queueMicrotask(() => this.flushRemovals());
139
+ }
140
+ }
141
+ this.pendingRemovals.add(id);
142
+ }
143
+
144
+ public flushRemovals(): void {
145
+ this.flushScheduled = false;
146
+ const removals = this.pendingRemovals;
147
+ if (!removals || removals.size === 0) {
148
+ this.pendingRemovals = null;
149
+ return;
150
+ }
151
+ this.pendingRemovals = null;
152
+
153
+ const indices: number[] = [];
154
+ for (const id of removals) {
155
+ const index = this.rootIdToIndex.get(id);
156
+ if (index !== undefined) {
157
+ indices.push(index);
158
+ this.rootIdToIndex.delete(id);
159
+ }
160
+ }
161
+
162
+ if (indices.length === 0) return;
163
+
164
+ indices.sort((a, b) => a - b);
165
+
166
+ let i = indices.length - 1;
167
+ while (i >= 0) {
168
+ let rangeStart = indices[i] ?? 0;
169
+ const rangeEnd = rangeStart;
170
+
171
+ while (i > 0) {
172
+ const prev = indices[i - 1];
173
+ if (prev !== rangeStart - 1) break;
174
+ i--;
175
+ rangeStart = prev ?? 0;
176
+ }
177
+
178
+ const count = rangeEnd - rangeStart + 1;
179
+ this.rootModel.splice(rangeStart, count);
180
+ this.rootIds.splice(rangeStart, count);
181
+
182
+ i--;
134
183
  }
184
+
185
+ this.rebuildRootIndices(0);
135
186
  }
136
187
 
137
188
  public insertItemBefore(id: string, beforeId: string, data: TreeItemData, parentId?: string): void {
189
+ if (parentId === undefined) this.flushRemovals();
138
190
  this.items.set(id, data);
139
191
 
140
192
  if (parentId === undefined) {
@@ -0,0 +1,64 @@
1
+ import type { ListSectionProps } from "../jsx.js";
2
+ import type { Node } from "../node.js";
3
+ import { hasChanged } from "./internal/props.js";
4
+ import { ListItemNode } from "./list-item.js";
5
+ import { VirtualNode } from "./virtual.js";
6
+
7
+ type SectionStore = {
8
+ addItemToSection(sectionId: string, itemId: string, item: unknown): void;
9
+ addItemsToSection(sectionId: string, items: { itemId: string; item: unknown }[]): void;
10
+ removeItemFromSection(itemId: string): void;
11
+ updateHeaderValue(sectionId: string, value: unknown): void;
12
+ insertItemToSectionBefore(sectionId: string, itemId: string, beforeId: string, item: unknown): void;
13
+ updateItem(id: string, item: unknown): void;
14
+ };
15
+
16
+ export class ListSectionNode extends VirtualNode<ListSectionProps, Node, ListItemNode> {
17
+ private store: SectionStore | null = null;
18
+
19
+ public override isValidChild(child: Node): boolean {
20
+ return child instanceof ListItemNode;
21
+ }
22
+
23
+ public override appendChild(child: ListItemNode): void {
24
+ super.appendChild(child);
25
+ if (this.store) {
26
+ this.store.addItemToSection(this.props.id, child.props.id, child.props.value);
27
+ }
28
+ }
29
+
30
+ public override insertBefore(child: ListItemNode, before: ListItemNode): void {
31
+ super.insertBefore(child, before);
32
+ if (this.store) {
33
+ this.store.insertItemToSectionBefore(this.props.id, child.props.id, before.props.id, child.props.value);
34
+ }
35
+ }
36
+
37
+ public override removeChild(child: ListItemNode): void {
38
+ if (this.store) {
39
+ this.store.removeItemFromSection(child.props.id);
40
+ }
41
+ super.removeChild(child);
42
+ }
43
+
44
+ public override commitUpdate(oldProps: ListSectionProps | null, newProps: ListSectionProps): void {
45
+ super.commitUpdate(oldProps, newProps);
46
+ if (this.store && hasChanged(oldProps, newProps, "value")) {
47
+ this.store.updateHeaderValue(newProps.id, newProps.value);
48
+ }
49
+ }
50
+
51
+ public setStore(store: SectionStore | null): void {
52
+ this.store = store;
53
+ if (store && this.children.length > 0) {
54
+ store.addItemsToSection(
55
+ this.props.id,
56
+ this.children.map((child) => ({ itemId: child.props.id, item: child.props.value })),
57
+ );
58
+ }
59
+ }
60
+
61
+ public getChildNodes(): readonly ListItemNode[] {
62
+ return this.children;
63
+ }
64
+ }
@@ -4,27 +4,34 @@ import type { Node } from "../node.js";
4
4
  import type { Container } from "../types.js";
5
5
  import { ContainerSlotNode } from "./container-slot.js";
6
6
  import { EventControllerNode } from "./event-controller.js";
7
+ import { GridItemRenderer } from "./internal/grid-item-renderer.js";
8
+ import type { HeaderItemRenderer } from "./internal/header-item-renderer.js";
9
+ import { updateHeaderRenderer } from "./internal/header-renderer-manager.js";
7
10
  import { ListItemRenderer } from "./internal/list-item-renderer.js";
8
11
  import { filterProps, hasChanged } from "./internal/props.js";
9
12
  import { ListItemNode } from "./list-item.js";
13
+ import { ListSectionNode } from "./list-section.js";
10
14
  import { ListModel, type ListModelProps } from "./models/list.js";
11
15
  import { SlotNode } from "./slot.js";
12
16
  import { WidgetNode } from "./widget.js";
13
17
 
14
- const RENDERER_PROPS = ["renderItem", "estimatedItemHeight"] as const;
18
+ const RENDERER_PROPS = ["renderItem", "renderHeader", "estimatedItemHeight"] as const;
15
19
  const OWN_PROPS = [...RENDERER_PROPS, "autoexpand", "selectionMode", "selected", "onSelectionChanged"] as const;
16
20
 
17
21
  type ListViewProps = Pick<GtkListViewProps, (typeof RENDERER_PROPS)[number]> & ListModelProps;
18
22
 
19
- type ListViewChild = ListItemNode | EventControllerNode | SlotNode | ContainerSlotNode;
23
+ type ListViewChild = ListItemNode | ListSectionNode | EventControllerNode | SlotNode | ContainerSlotNode;
20
24
 
21
25
  export class ListViewNode extends WidgetNode<Gtk.ListView, ListViewProps, ListViewChild> {
22
- private itemRenderer: ListItemRenderer;
26
+ private treeRenderer: ListItemRenderer | null = null;
27
+ private flatRenderer: GridItemRenderer | null = null;
28
+ private headerRenderer: HeaderItemRenderer | null = null;
23
29
  private list: ListModel;
24
30
 
25
31
  public override isValidChild(child: Node): boolean {
26
32
  return (
27
33
  child instanceof ListItemNode ||
34
+ child instanceof ListSectionNode ||
28
35
  child instanceof EventControllerNode ||
29
36
  child instanceof SlotNode ||
30
37
  child instanceof ContainerSlotNode
@@ -33,37 +40,54 @@ export class ListViewNode extends WidgetNode<Gtk.ListView, ListViewProps, ListVi
33
40
 
34
41
  constructor(typeName: string, props: ListViewProps, container: Gtk.ListView, rootContainer: Container) {
35
42
  super(typeName, props, container, rootContainer);
43
+
44
+ const flat = props.renderHeader != null;
45
+
36
46
  this.list = new ListModel(
37
47
  { owner: this, signalStore: this.signalStore },
38
48
  {
39
- autoexpand: props.autoexpand,
49
+ autoexpand: flat ? undefined : props.autoexpand,
40
50
  selectionMode: props.selectionMode,
41
51
  selected: props.selected,
42
52
  onSelectionChanged: props.onSelectionChanged,
43
53
  },
54
+ flat,
44
55
  );
45
- this.itemRenderer = new ListItemRenderer(this.signalStore);
46
- this.itemRenderer.setStore(this.list.getStore());
47
- this.list.getStore().setOnItemUpdated((id) => this.itemRenderer.rebindItem(id));
48
- this.container.setFactory(this.itemRenderer.getFactory());
56
+
57
+ if (flat) {
58
+ const store = this.list.getFlatStore();
59
+ this.flatRenderer = new GridItemRenderer(this.signalStore);
60
+ this.flatRenderer.setStore(store);
61
+ store.setOnItemUpdated((id) => this.flatRenderer?.rebindItem(id));
62
+ this.container.setFactory(this.flatRenderer.getFactory());
63
+ } else {
64
+ const store = this.list.getStore();
65
+ this.treeRenderer = new ListItemRenderer(this.signalStore);
66
+ this.treeRenderer.setStore(store);
67
+ store.setOnItemUpdated((id) => this.treeRenderer?.rebindItem(id));
68
+ this.container.setFactory(this.treeRenderer.getFactory());
69
+ }
49
70
  }
50
71
 
51
72
  public override appendChild(child: ListViewChild): void {
52
73
  super.appendChild(child);
53
- if (child instanceof ListItemNode) {
74
+ if (child instanceof ListItemNode || child instanceof ListSectionNode) {
54
75
  this.list.appendChild(child);
55
76
  }
56
77
  }
57
78
 
58
79
  public override insertBefore(child: ListViewChild, before: ListViewChild): void {
59
80
  super.insertBefore(child, before);
60
- if (child instanceof ListItemNode && before instanceof ListItemNode) {
81
+ if (
82
+ (child instanceof ListItemNode || child instanceof ListSectionNode) &&
83
+ (before instanceof ListItemNode || before instanceof ListSectionNode)
84
+ ) {
61
85
  this.list.insertBefore(child, before);
62
86
  }
63
87
  }
64
88
 
65
89
  public override removeChild(child: ListViewChild): void {
66
- if (child instanceof ListItemNode) {
90
+ if (child instanceof ListItemNode || child instanceof ListSectionNode) {
67
91
  this.list.removeChild(child);
68
92
  }
69
93
  super.removeChild(child);
@@ -86,17 +110,44 @@ export class ListViewNode extends WidgetNode<Gtk.ListView, ListViewProps, ListVi
86
110
  }
87
111
 
88
112
  public override detachDeletedInstance(): void {
89
- this.itemRenderer.dispose();
113
+ this.treeRenderer?.dispose();
114
+ this.flatRenderer?.dispose();
115
+ this.headerRenderer?.dispose();
90
116
  super.detachDeletedInstance();
91
117
  }
92
118
 
93
119
  private applyOwnProps(oldProps: ListViewProps | null, newProps: ListViewProps): void {
94
120
  if (hasChanged(oldProps, newProps, "renderItem")) {
95
- this.itemRenderer.setRenderFn(newProps.renderItem ?? null);
121
+ if (this.treeRenderer) {
122
+ this.treeRenderer.setRenderFn(newProps.renderItem ?? null);
123
+ }
124
+ if (this.flatRenderer) {
125
+ const renderItem = newProps.renderItem;
126
+ this.flatRenderer.setRenderFn(renderItem ? (item: unknown) => renderItem(item, null) : null);
127
+ }
96
128
  }
97
129
 
98
130
  if (hasChanged(oldProps, newProps, "estimatedItemHeight")) {
99
- this.itemRenderer.setEstimatedItemHeight(newProps.estimatedItemHeight ?? null);
131
+ const height = newProps.estimatedItemHeight ?? null;
132
+ if (this.treeRenderer) {
133
+ this.treeRenderer.setEstimatedItemHeight(height);
134
+ }
135
+ if (this.flatRenderer) {
136
+ this.flatRenderer.setEstimatedItemHeight(height);
137
+ }
138
+ }
139
+
140
+ if (hasChanged(oldProps, newProps, "renderHeader")) {
141
+ this.headerRenderer = updateHeaderRenderer(
142
+ this.headerRenderer,
143
+ {
144
+ signalStore: this.signalStore,
145
+ isEnabled: () => this.list.isFlatMode(),
146
+ resolveItem: (id) => this.list.getFlatStore().getHeaderValue(id),
147
+ setFactory: (factory) => this.container.setHeaderFactory(factory),
148
+ },
149
+ newProps.renderHeader,
150
+ );
100
151
  }
101
152
 
102
153
  const previousModel = this.list.getSelectionModel();