@gtkx/components 2.0.0-beta.1 → 2.0.0-beta.11

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 (79) hide show
  1. package/README.md +8 -165
  2. package/dist/column-view.d.ts.map +1 -1
  3. package/dist/column-view.js +20 -9
  4. package/dist/column-view.js.map +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/internal/cells.d.ts +19 -2
  10. package/dist/internal/cells.d.ts.map +1 -1
  11. package/dist/internal/cells.js +202 -39
  12. package/dist/internal/cells.js.map +1 -1
  13. package/dist/internal/collection-index.d.ts +3 -2
  14. package/dist/internal/collection-index.d.ts.map +1 -1
  15. package/dist/internal/collection-index.js +3 -3
  16. package/dist/internal/collection-index.js.map +1 -1
  17. package/dist/internal/collection-model.d.ts +28 -7
  18. package/dist/internal/collection-model.d.ts.map +1 -1
  19. package/dist/internal/collection-model.js +147 -127
  20. package/dist/internal/collection-model.js.map +1 -1
  21. package/dist/internal/collection.d.ts +5 -4
  22. package/dist/internal/collection.d.ts.map +1 -1
  23. package/dist/internal/collection.js +20 -21
  24. package/dist/internal/collection.js.map +1 -1
  25. package/dist/internal/controlled-sync.d.ts +6 -8
  26. package/dist/internal/controlled-sync.d.ts.map +1 -1
  27. package/dist/internal/controlled-sync.js +3 -3
  28. package/dist/internal/controlled-sync.js.map +1 -1
  29. package/dist/internal/drop-down.d.ts.map +1 -1
  30. package/dist/internal/drop-down.js +43 -44
  31. package/dist/internal/drop-down.js.map +1 -1
  32. package/dist/internal/expansion.js +4 -4
  33. package/dist/internal/expansion.js.map +1 -1
  34. package/dist/internal/selection.d.ts +1 -0
  35. package/dist/internal/selection.d.ts.map +1 -1
  36. package/dist/internal/selection.js +6 -6
  37. package/dist/internal/selection.js.map +1 -1
  38. package/dist/internal/tree-expansion.d.ts.map +1 -1
  39. package/dist/internal/tree-expansion.js +2 -11
  40. package/dist/internal/tree-expansion.js.map +1 -1
  41. package/dist/internal/tree-order.d.ts.map +1 -1
  42. package/dist/internal/tree-order.js +32 -76
  43. package/dist/internal/tree-order.js.map +1 -1
  44. package/dist/internal/use-collection.d.ts +5 -1
  45. package/dist/internal/use-collection.d.ts.map +1 -1
  46. package/dist/internal/use-collection.js +21 -7
  47. package/dist/internal/use-collection.js.map +1 -1
  48. package/dist/list-item-factory.d.ts +10 -0
  49. package/dist/list-item-factory.d.ts.map +1 -0
  50. package/dist/list-item-factory.js +14 -0
  51. package/dist/list-item-factory.js.map +1 -0
  52. package/dist/list-view.d.ts.map +1 -1
  53. package/dist/list-view.js +1 -0
  54. package/dist/list-view.js.map +1 -1
  55. package/dist/toast.d.ts +3 -5
  56. package/dist/toast.d.ts.map +1 -1
  57. package/dist/toast.js +3 -12
  58. package/dist/toast.js.map +1 -1
  59. package/dist/types.d.ts +44 -52
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/types.js.map +1 -1
  62. package/package.json +13 -10
  63. package/src/column-view.tsx +24 -12
  64. package/src/index.ts +3 -2
  65. package/src/internal/cells.tsx +330 -53
  66. package/src/internal/collection-index.ts +8 -6
  67. package/src/internal/collection-model.ts +190 -168
  68. package/src/internal/collection.ts +28 -24
  69. package/src/internal/controlled-sync.ts +9 -12
  70. package/src/internal/drop-down.tsx +64 -60
  71. package/src/internal/expansion.ts +4 -4
  72. package/src/internal/selection.tsx +8 -8
  73. package/src/internal/tree-expansion.ts +2 -14
  74. package/src/internal/tree-order.ts +33 -121
  75. package/src/internal/use-collection.tsx +29 -8
  76. package/src/list-item-factory.tsx +27 -0
  77. package/src/list-view.tsx +1 -0
  78. package/src/toast.tsx +4 -18
  79. package/src/types.ts +48 -52
@@ -1,6 +1,8 @@
1
+ import type { ReactNode, Ref } from "react";
1
2
  import * as Gio from "@gtkx/gi/gio";
2
3
  import * as GObject from "@gtkx/gi/gobject";
3
4
  import * as Gtk from "@gtkx/gi/gtk";
5
+ import { createElementComponent } from "@gtkx/react";
4
6
  import { registerClass } from "@gtkx/runtime";
5
7
  import type { CollectionIndex, Level } from "./collection-index.js";
6
8
  import type { TreeExpansion } from "./tree-expansion.js";
@@ -11,7 +13,7 @@ import { adoptIndex, createTreeExpansion, pruneSlots } from "./tree-expansion.js
11
13
  type LevelStore = Gio.ListModel & LazyLevelStore;
12
14
 
13
15
  type SlotRef = {
14
- store: LevelStore;
16
+ store: LazyLevelStore;
15
17
  slot: number;
16
18
  };
17
19
 
@@ -25,35 +27,29 @@ type SlotRun = {
25
27
  length: number;
26
28
  };
27
29
 
28
- type LevelSync = {
29
- store: LevelStore;
30
- level: Level;
31
- };
32
-
33
30
  type ModelState = {
34
- root: Gio.ListStore;
35
- rootModels: GObject.Object[];
36
- model: Gtk.FlattenListModel;
37
- groupStores: LevelStore[];
31
+ root: CollectionRootStore | null;
32
+ model: Gtk.FlattenListModel | null;
33
+ groupStores: Map<number, LevelStore>;
38
34
  trees: Map<LevelStore, Gtk.TreeListModel>;
39
- treeModels: Gtk.TreeListModel[];
40
35
  expansion: TreeExpansion;
41
36
  index: CollectionIndex;
42
37
  };
43
38
 
44
39
  type CollectionModel = {
45
- model: Gtk.FlattenListModel;
46
40
  expansion: TreeExpansion;
47
41
  rowAt: (position: number) => Gtk.TreeListRow | null;
42
+ bind: (root: CollectionRootStore, model: Gtk.FlattenListModel) => void;
43
+ clear: () => void;
48
44
  sync: (index: CollectionIndex) => void;
49
45
  };
50
46
 
47
+ type CollectionRootProps = { ref: Ref<CollectionRootStore> };
48
+
51
49
  const STORE_CLASS_KEY = Symbol.for("gtkx.components.lazy-level-store");
50
+ const ROOT_CLASS_KEY = Symbol.for("gtkx.components.collection-root-store");
52
51
  const SLOTS_KEY = Symbol.for("gtkx.components.lazy-level-store.slots");
53
- const EMPTY_INDEX = createCollectionIndex(undefined, undefined, true);
54
- const SLOTS = sharedSlots();
55
-
56
- const newRootStore = (): Gio.ListStore => new Gio.ListStore({ itemType: GObject.TYPE_OBJECT });
52
+ const EMPTY_INDEX = createCollectionIndex(undefined, [], true);
57
53
 
58
54
  function sharedSlots(): WeakMap<GObject.Object, SlotRef> {
59
55
  const cached: unknown = Reflect.get(globalThis, SLOTS_KEY);
@@ -81,6 +77,19 @@ function registeredStoreClass(): typeof LazyLevelStore {
81
77
  return LazyLevelStore;
82
78
  }
83
79
 
80
+ function registeredRootClass(): unknown {
81
+ const cached: unknown = Reflect.get(globalThis, ROOT_CLASS_KEY);
82
+
83
+ if (typeof cached === "function") {
84
+ return cached;
85
+ }
86
+
87
+ registerClass(CollectionRootStore, { typeName: "GtkxCollectionRootStore", implements: [Gio.ListModel] });
88
+ Reflect.set(globalThis, ROOT_CLASS_KEY, CollectionRootStore);
89
+
90
+ return CollectionRootStore;
91
+ }
92
+
84
93
  function slotRefFor(value: GObject.Object | null): SlotRef | null {
85
94
  const item = value instanceof Gtk.TreeListRow ? value.getItem() : value;
86
95
 
@@ -88,27 +97,18 @@ function slotRefFor(value: GObject.Object | null): SlotRef | null {
88
97
  return null;
89
98
  }
90
99
 
91
- const ref = SLOTS.get(item);
100
+ const ref = sharedSlots().get(item);
92
101
 
93
102
  return ref === undefined || ref.slot === -1 ? null : ref;
94
103
  }
95
104
 
96
- function slotPathAt(store: LevelStore, slot: number): string {
105
+ function slotPathAt(store: LazyLevelStore, slot: number): string {
97
106
  return store.level.path + encodePart(String(slot));
98
107
  }
99
108
 
100
- function growStore(store: LevelStore): void {
101
- for (let slot = store.refs.length; slot < store.level.items.length; slot++) {
102
- store.refs.push({ store, slot });
103
- store.objects.push(null);
104
- store.childStores.push(null);
105
- }
106
- }
107
-
108
109
  function newLevelStore(level: Level): LevelStore {
109
110
  const store = new (registeredStoreClass())() as LevelStore;
110
111
  store.level = level;
111
- growStore(store);
112
112
 
113
113
  return store;
114
114
  }
@@ -116,6 +116,10 @@ function newLevelStore(level: Level): LevelStore {
116
116
  function collectFlips(previous: Level, next: Level, overlap: number): Set<number> {
117
117
  const flipped: Set<number> = new Set();
118
118
 
119
+ if (previous.expandableFlags.length === 0 && next.expandableFlags.length === 0) {
120
+ return flipped;
121
+ }
122
+
119
123
  for (let slot = 0; slot < overlap; slot++) {
120
124
  if ((previous.expandableFlags[slot] ?? false) !== (next.expandableFlags[slot] ?? false)) {
121
125
  flipped.add(slot);
@@ -133,30 +137,30 @@ function pruneFlips(context: SyncContext, store: LevelStore, flipped: Set<number
133
137
  pruneSlots(context.expansion, store.level.path, (slot) => flipped.has(slot));
134
138
 
135
139
  for (const slot of flipped) {
136
- store.childStores[slot] = null;
140
+ store.childStores.delete(slot);
137
141
  }
138
142
  }
139
143
 
140
144
  function detachRefs(store: LevelStore, nextLength: number): void {
141
- for (let slot = nextLength; slot < store.refs.length; slot++) {
142
- const ref = store.refs[slot];
143
-
144
- if (ref !== undefined) {
145
- ref.slot = -1;
145
+ for (const [slot, ref] of store.refs) {
146
+ if (slot < nextLength) {
147
+ continue;
146
148
  }
149
+
150
+ ref.slot = -1;
151
+ store.refs.delete(slot);
152
+ store.objects.delete(slot);
153
+ store.childStores.delete(slot);
147
154
  }
148
155
  }
149
156
 
150
- function shrinkStore(context: SyncContext, store: LevelStore, nextLength: number): void {
151
- if (store.refs.length <= nextLength) {
157
+ function shrinkStore(context: SyncContext, store: LevelStore, previousLength: number, nextLength: number): void {
158
+ if (previousLength <= nextLength) {
152
159
  return;
153
160
  }
154
161
 
155
162
  detachRefs(store, nextLength);
156
163
  pruneSlots(context.expansion, store.level.path, (slot) => slot >= nextLength);
157
- store.refs.length = nextLength;
158
- store.objects.length = nextLength;
159
- store.childStores.length = nextLength;
160
164
  }
161
165
 
162
166
  function emitTailSplice(store: LevelStore, previousLength: number, nextLength: number): void {
@@ -199,66 +203,32 @@ function emitFlips(store: LevelStore, flipped: Set<number>): void {
199
203
  }
200
204
  }
201
205
 
202
- function childSync(
203
- context: SyncContext,
204
- store: LevelStore,
205
- slot: number,
206
- flipped: Set<number>,
207
- ): LevelSync | undefined {
208
- const child = store.childStores[slot] ?? null;
209
-
210
- if (child === null || flipped.has(slot)) {
211
- return undefined;
212
- }
213
-
214
- const level = context.index.childLevel(store.level, slot);
215
-
216
- return level === undefined ? undefined : { store: child, level };
217
- }
218
-
219
- function childSyncs(context: SyncContext, store: LevelStore, overlap: number, flipped: Set<number>): LevelSync[] {
220
- const syncs: LevelSync[] = [];
221
-
222
- for (let slot = 0; slot < overlap; slot++) {
223
- const sync = childSync(context, store, slot, flipped);
224
-
225
- if (sync !== undefined) {
226
- syncs.push(sync);
227
- }
228
- }
229
-
230
- return syncs;
231
- }
232
-
233
- function syncEntry(context: SyncContext, entry: LevelSync): LevelSync[] {
234
- const { store, level } = entry;
235
- const previousLength = store.refs.length;
206
+ function syncLevel(context: SyncContext, store: LevelStore, level: Level): void {
207
+ const previous = store.level;
208
+ const previousLength = previous.items.length;
236
209
  const overlap = Math.min(previousLength, level.items.length);
237
- const flipped = collectFlips(store.level, level, overlap);
210
+ const flipped = collectFlips(previous, level, overlap);
238
211
  store.level = level;
239
212
  pruneFlips(context, store, flipped);
240
- shrinkStore(context, store, level.items.length);
241
- growStore(store);
213
+ shrinkStore(context, store, previousLength, level.items.length);
242
214
  emitTailSplice(store, previousLength, level.items.length);
243
215
  emitFlips(store, flipped);
244
216
 
245
- return childSyncs(context, store, overlap, flipped);
246
- }
247
-
248
- function syncLevel(context: SyncContext, store: LevelStore, level: Level): void {
249
- const pending: LevelSync[] = [{ store, level }];
217
+ for (const [slot, child] of store.childStores) {
218
+ if (slot >= overlap || flipped.has(slot)) {
219
+ continue;
220
+ }
250
221
 
251
- while (pending.length > 0) {
252
- const entry = pending.pop();
222
+ const childLevel = context.index.childLevel(level, slot);
253
223
 
254
- if (entry !== undefined) {
255
- pending.push(...syncEntry(context, entry).toReversed());
224
+ if (childLevel !== undefined) {
225
+ syncLevel(context, child, childLevel);
256
226
  }
257
227
  }
258
228
  }
259
229
 
260
- function ensureChildStore(index: CollectionIndex, store: LevelStore, slot: number): LevelStore | null {
261
- const existing = store.childStores[slot] ?? null;
230
+ function ensureChildStore(index: CollectionIndex, store: LazyLevelStore, slot: number): LevelStore | null {
231
+ const existing = store.childStores.get(slot) ?? null;
262
232
 
263
233
  if (existing !== null || !(store.level.expandableFlags[slot] ?? false)) {
264
234
  return existing;
@@ -271,7 +241,7 @@ function ensureChildStore(index: CollectionIndex, store: LevelStore, slot: numbe
271
241
  }
272
242
 
273
243
  const created = newLevelStore(level);
274
- store.childStores[slot] = created;
244
+ store.childStores.set(slot, created);
275
245
 
276
246
  return created;
277
247
  }
@@ -287,102 +257,81 @@ function childStoreFor(state: ModelState, object: GObject.Object): Gio.ListModel
287
257
  }
288
258
 
289
259
  function treeFor(state: ModelState, store: LevelStore): Gtk.TreeListModel {
290
- return (
291
- state.trees.get(store) ?? Gtk.TreeListModel.new(store, false, false, (object) => childStoreFor(state, object))
292
- );
293
- }
260
+ const existing = state.trees.get(store);
294
261
 
295
- function nextTrees(state: ModelState, index: CollectionIndex): Map<LevelStore, Gtk.TreeListModel> {
296
- if (!index.isTree) {
297
- return new Map();
262
+ if (existing !== undefined) {
263
+ return existing;
298
264
  }
299
265
 
300
- return new Map(state.groupStores.map((store) => [store, treeFor(state, store)]));
301
- }
302
-
303
- function pruneRebuiltLevels(state: ModelState, next: Map<LevelStore, Gtk.TreeListModel>): void {
304
- const stores: Set<LevelStore> = new Set([...state.trees.keys(), ...next.keys()]);
305
-
306
- for (const store of stores) {
307
- if (state.trees.get(store) !== next.get(store)) {
308
- pruneSlots(state.expansion, store.level.path, () => true);
309
- }
310
- }
311
- }
266
+ const created = Gtk.TreeListModel.new(store, false, false, (object) => childStoreFor(state, object));
267
+ state.trees.set(store, created);
312
268
 
313
- function adoptTrees(state: ModelState, index: CollectionIndex): void {
314
- const next = nextTrees(state, index);
315
- pruneRebuiltLevels(state, next);
316
- state.trees = next;
317
- state.treeModels = next.values().toArray();
269
+ return created;
318
270
  }
319
271
 
320
- function desiredRootModels(state: ModelState, index: CollectionIndex): GObject.Object[] {
321
- adoptTrees(state, index);
272
+ function rowAt(state: ModelState, position: number): Gtk.TreeListRow | null {
273
+ const item = state.model?.getItem(position);
322
274
 
323
- return index.isTree ? [...state.treeModels] : [...state.groupStores];
275
+ return item instanceof Gtk.TreeListRow ? item : null;
324
276
  }
325
277
 
326
- function rowAt(state: ModelState, position: number): Gtk.TreeListRow | null {
327
- let offset = position;
278
+ function pruneGroups(state: ModelState, previous: CollectionIndex): void {
279
+ const hasModeChanged = previous.isTree !== state.index.isTree;
328
280
 
329
- for (const tree of state.treeModels) {
330
- const count = tree.getNItems();
281
+ for (const [group, store] of state.groupStores) {
282
+ const isRemoved = group >= state.index.groups.length;
331
283
 
332
- if (offset < count) {
333
- return tree.getRow(offset);
284
+ if (isRemoved || hasModeChanged) {
285
+ pruneSlots(state.expansion, store.level.path, () => true);
286
+ state.trees.delete(store);
334
287
  }
335
288
 
336
- offset -= count;
289
+ if (isRemoved) {
290
+ state.groupStores.delete(group);
291
+ }
337
292
  }
338
-
339
- return null;
340
293
  }
341
294
 
342
- function hasSameModels(previous: GObject.Object[], next: GObject.Object[]): boolean {
343
- return previous.length === next.length && next.every((model, index) => previous[index] === model);
344
- }
295
+ function syncRoot(state: ModelState, previous: CollectionIndex): void {
296
+ const before = previous.groups.length;
297
+ const after = state.index.groups.length;
345
298
 
346
- function syncRoot(state: ModelState, index: CollectionIndex): void {
347
- const next = desiredRootModels(state, index);
299
+ if (previous.isTree !== state.index.isTree) {
300
+ state.root?.itemsChanged(0, before, after);
348
301
 
349
- if (hasSameModels(state.rootModels, next)) {
350
302
  return;
351
303
  }
352
304
 
353
- state.root.splice(0, state.rootModels.length, next);
354
- state.rootModels = next;
355
- }
356
-
357
- function adjustGroupStores(state: ModelState, context: SyncContext): void {
358
- const { groups } = context.index;
359
- state.groupStores.length = Math.min(state.groupStores.length, groups.length);
360
-
361
- for (const level of groups.slice(state.groupStores.length)) {
362
- state.groupStores.push(newLevelStore(level));
305
+ if (before !== after) {
306
+ const start = Math.min(before, after);
307
+ state.root?.itemsChanged(start, before - start, after - start);
363
308
  }
364
309
  }
365
310
 
366
- function stepGroupStores(state: ModelState, context: SyncContext, surviving: number): void {
367
- for (const [group, level] of context.index.groups.entries()) {
368
- const store = state.groupStores[group];
311
+ function stepGroupStores(state: ModelState, context: SyncContext): void {
312
+ for (const [group, store] of state.groupStores) {
313
+ const level = context.index.groups[group];
369
314
 
370
- if (store !== undefined && group < surviving) {
315
+ if (level !== undefined && store.level !== level) {
371
316
  syncLevel(context, store, level);
372
317
  }
373
318
  }
374
319
  }
375
320
 
376
321
  function syncModel(state: ModelState, index: CollectionIndex): void {
322
+ if (state.root === null) {
323
+ return;
324
+ }
325
+
377
326
  const context: SyncContext = { index, expansion: state.expansion };
378
- const surviving = Math.min(state.groupStores.length, index.groups.length);
327
+ const previous = state.index;
379
328
  state.index = index;
380
329
  state.expansion.isSyncing = true;
381
330
 
382
331
  try {
383
- adjustGroupStores(state, context);
384
- syncRoot(state, index);
385
- stepGroupStores(state, context, surviving);
332
+ pruneGroups(state, previous);
333
+ syncRoot(state, previous);
334
+ stepGroupStores(state, context);
386
335
  } finally {
387
336
  state.expansion.isSyncing = false;
388
337
  }
@@ -390,42 +339,104 @@ function syncModel(state: ModelState, index: CollectionIndex): void {
390
339
  adoptIndex(state.expansion, index);
391
340
  }
392
341
 
393
- function createCollectionModel(): CollectionModel {
394
- registeredStoreClass();
395
- const root = newRootStore();
342
+ function clearModel(state: ModelState): void {
343
+ const root = state.root;
344
+ const previousLength = state.index.groups.length;
345
+ state.expansion.isSyncing = true;
346
+
347
+ try {
348
+ if (root !== null) {
349
+ root.state = null;
350
+ }
351
+
352
+ state.root = null;
353
+ state.model = null;
354
+ state.index = EMPTY_INDEX;
355
+ state.groupStores.clear();
356
+ state.trees.clear();
357
+ state.expansion.expanded.clear();
358
+ state.expansion.slots.clear();
359
+ adoptIndex(state.expansion, EMPTY_INDEX);
360
+
361
+ if (previousLength > 0) {
362
+ root?.itemsChanged(0, previousLength, 0);
363
+ }
364
+ } finally {
365
+ state.expansion.isSyncing = false;
366
+ }
367
+ }
396
368
 
369
+ function createCollectionModel(): CollectionModel {
397
370
  const state: ModelState = {
398
- root,
399
- rootModels: [],
400
- model: Gtk.FlattenListModel.new(root),
401
- groupStores: [],
371
+ root: null,
372
+ model: null,
373
+ groupStores: new Map(),
402
374
  trees: new Map(),
403
- treeModels: [],
404
375
  expansion: createTreeExpansion(EMPTY_INDEX),
405
376
  index: EMPTY_INDEX,
406
377
  };
407
378
 
408
379
  return {
409
- model: state.model,
410
380
  expansion: state.expansion,
411
381
  rowAt: (position) => rowAt(state, position),
382
+ bind: (root, model) => {
383
+ root.state = state;
384
+ state.root = root;
385
+ state.model = model;
386
+ },
387
+ clear: () => {
388
+ clearModel(state);
389
+ },
412
390
  sync: (index) => {
413
391
  syncModel(state, index);
414
392
  },
415
393
  };
416
394
  }
417
395
 
396
+ class CollectionRootStore extends GObject.Object implements Gio.ListModelImpl {
397
+ declare itemsChanged: Gio.ListModel["itemsChanged"];
398
+ state: ModelState | null = null;
399
+
400
+ vfuncGetItemType(): bigint {
401
+ return GObject.TYPE_OBJECT;
402
+ }
403
+
404
+ vfuncGetNItems(): number {
405
+ return this.state?.index.groups.length ?? 0;
406
+ }
407
+
408
+ vfuncGetItem(position: number): GObject.Object | null {
409
+ const state = this.state;
410
+ const level = state?.index.groups[position];
411
+
412
+ if (state === null || level === undefined) {
413
+ return null;
414
+ }
415
+
416
+ let store = state.groupStores.get(position);
417
+
418
+ if (store === undefined) {
419
+ store = newLevelStore(level);
420
+ state.groupStores.set(position, store);
421
+ }
422
+
423
+ return state.index.isTree ? treeFor(state, store) : store;
424
+ }
425
+ }
426
+
418
427
  class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
419
428
  declare itemsChanged: Gio.ListModel["itemsChanged"];
420
429
  level: Level = { path: "", items: [], expandableFlags: [] };
421
- refs: SlotRef[] = [];
422
- objects: (Gtk.StringObject | null)[] = [];
423
- childStores: (LevelStore | null)[] = [];
430
+ refs: Map<number, SlotRef> = new Map();
431
+ objects: Map<number, Gtk.StringObject> = new Map();
432
+ childStores: Map<number, LevelStore> = new Map();
424
433
 
425
- private createItem(position: number, ref: SlotRef): Gtk.StringObject {
434
+ private createItem(position: number): Gtk.StringObject {
435
+ const ref = { store: this, slot: position };
426
436
  const created = Gtk.StringObject.new("");
427
- this.objects[position] = created;
428
- SLOTS.set(created, ref);
437
+ this.refs.set(position, ref);
438
+ this.objects.set(position, created);
439
+ sharedSlots().set(created, ref);
429
440
 
430
441
  return created;
431
442
  }
@@ -435,18 +446,29 @@ class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
435
446
  }
436
447
 
437
448
  vfuncGetNItems(): number {
438
- return this.refs.length;
449
+ return this.level.items.length;
439
450
  }
440
451
 
441
452
  vfuncGetItem(position: number): GObject.Object | null {
442
- const ref = this.refs[position];
443
-
444
- if (ref === undefined) {
453
+ if (position < 0 || position >= this.level.items.length) {
445
454
  return null;
446
455
  }
447
456
 
448
- return this.objects[position] ?? this.createItem(position, ref);
457
+ return this.objects.get(position) ?? this.createItem(position);
449
458
  }
450
459
  }
451
460
 
452
- export { createCollectionModel, slotPathAt, slotRefFor, type CollectionModel, type SlotRef };
461
+ const CollectionRoot: (props: CollectionRootProps) => ReactNode = createElementComponent<CollectionRootProps>(
462
+ "GtkxCollectionRootStore",
463
+ registeredRootClass(),
464
+ );
465
+
466
+ export {
467
+ CollectionRoot,
468
+ createCollectionModel,
469
+ slotPathAt,
470
+ slotRefFor,
471
+ type CollectionModel,
472
+ type CollectionRootStore,
473
+ type SlotRef,
474
+ };
@@ -1,14 +1,15 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
1
2
  import type { ListItem } from "../types.js";
2
- import type { CollectionIndex } from "./collection-index.js";
3
+ import type { CollectionIndex, SectionIdentity } from "./collection-index.js";
3
4
  import type { CollectionModel, SlotRef } from "./collection-model.js";
4
5
  import type { MatchedRows } from "./tree-order.js";
5
6
  import { slotPathAt, slotRefFor } from "./collection-model.js";
6
7
  import { findIds, findRows } from "./tree-order.js";
7
8
 
8
- type Collection = Pick<CollectionModel, "model" | "expansion" | "rowAt"> & {
9
+ type Collection = Pick<CollectionModel, "expansion" | "rowAt"> & {
9
10
  isTree: boolean;
10
11
  itemAt: (ref: SlotRef) => ListItem | undefined;
11
- sectionFor: (levelPath: string) => unknown;
12
+ sectionFor: (levelPath: string) => SectionIdentity | undefined;
12
13
  idAt: (position: number) => string | null;
13
14
  idsAt: (positions: number[]) => string[];
14
15
  pathAt: (position: number) => string | null;
@@ -19,42 +20,42 @@ type Collection = Pick<CollectionModel, "model" | "expansion" | "rowAt"> & {
19
20
  const NO_ROWS: MatchedRows = { positions: [], ids: [] };
20
21
  const NO_IDS: string[] = [];
21
22
 
22
- function rowsFor(collectionModel: CollectionModel, ids: string[]): MatchedRows {
23
+ function rowsFor(gtkModel: CollectionModel, ids: string[]): MatchedRows {
23
24
  if (ids.length === 0) {
24
25
  return NO_ROWS;
25
26
  }
26
27
 
27
- const { expansion } = collectionModel;
28
+ const { expansion } = gtkModel;
28
29
 
29
30
  return findRows(expansion.index, expansion.slots, new Set(ids));
30
31
  }
31
32
 
32
- function positionFor(collectionModel: CollectionModel, id: string): number {
33
- const [first = -1] = rowsFor(collectionModel, [id]).positions;
33
+ function positionFor(gtkModel: CollectionModel, id: string): number {
34
+ const [first = -1] = rowsFor(gtkModel, [id]).positions;
34
35
 
35
36
  return first;
36
37
  }
37
38
 
38
- function refAt(collectionModel: CollectionModel, position: number): SlotRef | null {
39
+ function refAt(model: Gtk.FlattenListModel | null, position: number): SlotRef | null {
39
40
  if (position < 0) {
40
41
  return null;
41
42
  }
42
43
 
43
- return slotRefFor(collectionModel.model.getItem(position));
44
+ return slotRefFor(model?.getItem(position) ?? null);
44
45
  }
45
46
 
46
47
  function itemAt(index: CollectionIndex, ref: SlotRef): ListItem | undefined {
47
48
  return index.itemAt(ref.store.level.path, ref.slot);
48
49
  }
49
50
 
50
- function idAt(collectionModel: CollectionModel, index: CollectionIndex, position: number): string | null {
51
- const ref = refAt(collectionModel, position);
51
+ function idAt(model: Gtk.FlattenListModel | null, index: CollectionIndex, position: number): string | null {
52
+ const ref = refAt(model, position);
52
53
 
53
54
  return ref === null ? null : (itemAt(index, ref)?.id ?? null);
54
55
  }
55
56
 
56
- function pathAt(collectionModel: CollectionModel, position: number): string | null {
57
- const ref = refAt(collectionModel, position);
57
+ function pathAt(model: Gtk.FlattenListModel | null, position: number): string | null {
58
+ const ref = refAt(model, position);
58
59
 
59
60
  if (ref === null) {
60
61
  return null;
@@ -63,12 +64,12 @@ function pathAt(collectionModel: CollectionModel, position: number): string | nu
63
64
  return slotPathAt(ref.store, ref.slot);
64
65
  }
65
66
 
66
- function idsAt(collectionModel: CollectionModel, positions: number[]): string[] {
67
+ function idsAt(gtkModel: CollectionModel, positions: number[]): string[] {
67
68
  if (positions.length === 0) {
68
69
  return NO_IDS;
69
70
  }
70
71
 
71
- const { expansion } = collectionModel;
72
+ const { expansion } = gtkModel;
72
73
 
73
74
  return findIds(expansion.index, expansion.slots, new Set(positions));
74
75
  }
@@ -79,19 +80,22 @@ function isCollectionIdle(collection: Collection): boolean {
79
80
  return !expansion.isApplying && !expansion.isSyncing;
80
81
  }
81
82
 
82
- function createCollection(collectionModel: CollectionModel, index: CollectionIndex): Collection {
83
+ function createCollection(
84
+ gtkModel: CollectionModel,
85
+ index: CollectionIndex,
86
+ model: Gtk.FlattenListModel | null,
87
+ ): Collection {
83
88
  return {
84
- model: collectionModel.model,
85
- expansion: collectionModel.expansion,
89
+ expansion: gtkModel.expansion,
86
90
  isTree: index.isTree,
87
- rowAt: collectionModel.rowAt,
91
+ rowAt: gtkModel.rowAt,
88
92
  itemAt: (ref) => itemAt(index, ref),
89
93
  sectionFor: index.sectionFor,
90
- idAt: (position) => idAt(collectionModel, index, position),
91
- idsAt: (positions) => idsAt(collectionModel, positions),
92
- pathAt: (position) => pathAt(collectionModel, position),
93
- positionFor: (id) => positionFor(collectionModel, id),
94
- rowsFor: (ids) => rowsFor(collectionModel, ids),
94
+ idAt: (position) => idAt(model, index, position),
95
+ idsAt: (positions) => idsAt(gtkModel, positions),
96
+ pathAt: (position) => pathAt(model, position),
97
+ positionFor: (id) => positionFor(gtkModel, id),
98
+ rowsFor: (ids) => rowsFor(gtkModel, ids),
95
99
  };
96
100
  }
97
101