@gtkx/components 1.0.0-rc.4 → 1.1.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 (86) hide show
  1. package/README.md +5 -5
  2. package/dist/column-view.d.ts +2 -2
  3. package/dist/column-view.d.ts.map +1 -1
  4. package/dist/column-view.js +14 -12
  5. package/dist/column-view.js.map +1 -1
  6. package/dist/grid-view.js +1 -1
  7. package/dist/grid-view.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/internal/cells.d.ts +31 -23
  12. package/dist/internal/cells.d.ts.map +1 -1
  13. package/dist/internal/cells.js +168 -77
  14. package/dist/internal/cells.js.map +1 -1
  15. package/dist/internal/collection-index.d.ts +8 -11
  16. package/dist/internal/collection-index.d.ts.map +1 -1
  17. package/dist/internal/collection-index.js +53 -73
  18. package/dist/internal/collection-index.js.map +1 -1
  19. package/dist/internal/collection-model.d.ts +24 -4
  20. package/dist/internal/collection-model.d.ts.map +1 -1
  21. package/dist/internal/collection-model.js +247 -96
  22. package/dist/internal/collection-model.js.map +1 -1
  23. package/dist/internal/collection.d.ts +8 -10
  24. package/dist/internal/collection.d.ts.map +1 -1
  25. package/dist/internal/collection.js +38 -48
  26. package/dist/internal/collection.js.map +1 -1
  27. package/dist/internal/controlled-sync.d.ts +11 -0
  28. package/dist/internal/controlled-sync.d.ts.map +1 -0
  29. package/dist/internal/controlled-sync.js +19 -0
  30. package/dist/internal/controlled-sync.js.map +1 -0
  31. package/dist/internal/drop-down.d.ts.map +1 -1
  32. package/dist/internal/drop-down.js +27 -15
  33. package/dist/internal/drop-down.js.map +1 -1
  34. package/dist/internal/expansion.d.ts +3 -2
  35. package/dist/internal/expansion.d.ts.map +1 -1
  36. package/dist/internal/expansion.js +73 -48
  37. package/dist/internal/expansion.js.map +1 -1
  38. package/dist/internal/keys.d.ts +5 -0
  39. package/dist/internal/keys.d.ts.map +1 -0
  40. package/dist/internal/keys.js +13 -0
  41. package/dist/internal/keys.js.map +1 -0
  42. package/dist/internal/selection.d.ts +3 -2
  43. package/dist/internal/selection.d.ts.map +1 -1
  44. package/dist/internal/selection.js +68 -35
  45. package/dist/internal/selection.js.map +1 -1
  46. package/dist/internal/slots.d.ts +10 -0
  47. package/dist/internal/slots.d.ts.map +1 -0
  48. package/dist/internal/slots.js +43 -0
  49. package/dist/internal/slots.js.map +1 -0
  50. package/dist/internal/tree-expansion.d.ts +19 -0
  51. package/dist/internal/tree-expansion.d.ts.map +1 -0
  52. package/dist/internal/tree-expansion.js +72 -0
  53. package/dist/internal/tree-expansion.js.map +1 -0
  54. package/dist/internal/tree-order.d.ts +26 -0
  55. package/dist/internal/tree-order.d.ts.map +1 -0
  56. package/dist/internal/tree-order.js +109 -0
  57. package/dist/internal/tree-order.js.map +1 -0
  58. package/dist/internal/use-collection.d.ts +1 -1
  59. package/dist/internal/use-collection.d.ts.map +1 -1
  60. package/dist/internal/use-collection.js +4 -4
  61. package/dist/internal/use-collection.js.map +1 -1
  62. package/dist/list-view.d.ts.map +1 -1
  63. package/dist/list-view.js +6 -6
  64. package/dist/list-view.js.map +1 -1
  65. package/dist/types.d.ts +72 -22
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js.map +1 -1
  68. package/package.json +17 -9
  69. package/src/column-view.tsx +28 -25
  70. package/src/grid-view.tsx +1 -1
  71. package/src/index.ts +3 -0
  72. package/src/internal/cells.tsx +339 -117
  73. package/src/internal/collection-index.ts +68 -92
  74. package/src/internal/collection-model.ts +327 -112
  75. package/src/internal/collection.ts +45 -65
  76. package/src/internal/controlled-sync.ts +34 -0
  77. package/src/internal/drop-down.tsx +41 -20
  78. package/src/internal/expansion.ts +95 -70
  79. package/src/internal/keys.ts +18 -0
  80. package/src/internal/selection.tsx +100 -51
  81. package/src/internal/slots.ts +67 -0
  82. package/src/internal/tree-expansion.ts +115 -0
  83. package/src/internal/tree-order.ts +208 -0
  84. package/src/internal/use-collection.tsx +5 -5
  85. package/src/list-view.tsx +14 -10
  86. package/src/types.ts +78 -20
@@ -1,178 +1,348 @@
1
1
  import * as Gio from "@gtkx/gi/gio";
2
2
  import * as GObject from "@gtkx/gi/gobject";
3
3
  import * as Gtk from "@gtkx/gi/gtk";
4
+ import { registerClass } from "@gtkx/runtime";
4
5
  import type { CollectionIndex, Level } from "./collection-index.js";
5
- import { childLevelKey, ROOT_LEVEL_KEY } from "./collection-index.js";
6
+ import type { TreeExpansion } from "./tree-expansion.js";
7
+ import { createCollectionIndex, emptyLevel } from "./collection-index.js";
8
+ import { encodePart } from "./keys.js";
9
+ import { adoptIndex, createTreeExpansion, pruneSlots } from "./tree-expansion.js";
6
10
 
7
- type LevelState = {
8
- store: Gio.ListStore;
9
- ids: string[];
10
- canExpand: Map<string, boolean>;
11
+ type LevelStore = Gio.ListModel & LazyLevelStore;
12
+
13
+ type SlotRef = {
14
+ store: LevelStore;
15
+ slot: number;
16
+ };
17
+
18
+ type SyncContext = {
19
+ index: CollectionIndex;
20
+ expansion: TreeExpansion;
11
21
  };
12
22
 
13
- type SpliceRange = {
23
+ type SlotRun = {
14
24
  start: number;
15
- removed: number;
16
- added: string[];
25
+ length: number;
26
+ };
27
+
28
+ type LevelSync = {
29
+ store: LevelStore;
30
+ level: Level;
17
31
  };
18
32
 
19
33
  type ModelState = {
20
34
  root: Gio.ListStore;
21
35
  rootModels: GObject.Object[];
22
36
  model: Gtk.FlattenListModel;
23
- objects: Map<string, Gtk.StringObject>;
24
- levels: Map<string, LevelState>;
25
- tree: Gtk.TreeListModel | null;
37
+ groupStores: LevelStore[];
38
+ trees: Map<LevelStore, Gtk.TreeListModel>;
39
+ treeModels: Gtk.TreeListModel[];
40
+ expansion: TreeExpansion;
41
+ index: CollectionIndex;
26
42
  };
27
43
 
28
44
  type CollectionModel = {
29
45
  model: Gtk.FlattenListModel;
30
- treeModel: () => Gtk.TreeListModel | null;
46
+ expansion: TreeExpansion;
47
+ rowAt: (position: number) => Gtk.TreeListRow | null;
31
48
  sync: (index: CollectionIndex) => void;
32
49
  };
33
50
 
34
- const OBJECT_CACHE_MAX = 1 << 20;
51
+ const STORE_CLASS_KEY = Symbol.for("gtkx.components.lazy-level-store");
52
+ const SLOTS_KEY = Symbol.for("gtkx.components.lazy-level-store.slots");
53
+ const EMPTY_INDEX = createCollectionIndex(undefined, undefined, true);
54
+ const SLOTS = sharedSlots();
35
55
 
36
- const newStore = (): Gio.ListStore => new Gio.ListStore({ itemType: GObject.TYPE_OBJECT });
56
+ const newRootStore = (): Gio.ListStore => new Gio.ListStore({ itemType: GObject.TYPE_OBJECT });
37
57
 
38
- function getId(value: GObject.Object | null): string | null {
39
- const item = value instanceof Gtk.TreeListRow ? value.getItem() : value;
58
+ function sharedSlots(): WeakMap<GObject.Object, SlotRef> {
59
+ const cached: unknown = Reflect.get(globalThis, SLOTS_KEY);
60
+
61
+ if (cached instanceof WeakMap) {
62
+ return cached as WeakMap<GObject.Object, SlotRef>;
63
+ }
64
+
65
+ const created: WeakMap<GObject.Object, SlotRef> = new WeakMap();
66
+ Reflect.set(globalThis, SLOTS_KEY, created);
40
67
 
41
- return item instanceof Gtk.StringObject ? item.getString() : null;
68
+ return created;
42
69
  }
43
70
 
44
- function objectFor(state: ModelState, id: string): Gtk.StringObject {
45
- const existing = state.objects.get(id);
71
+ function registeredStoreClass(): typeof LazyLevelStore {
72
+ const cached: unknown = Reflect.get(globalThis, STORE_CLASS_KEY);
46
73
 
47
- if (existing !== undefined) {
48
- return existing;
74
+ if (typeof cached === "function") {
75
+ return cached as typeof LazyLevelStore;
49
76
  }
50
77
 
51
- const created = Gtk.StringObject.new(id);
52
- state.objects.set(id, created);
78
+ registerClass(LazyLevelStore, { typeName: "GtkxLazyLevelStore", implements: [Gio.ListModel] });
79
+ Reflect.set(globalThis, STORE_CLASS_KEY, LazyLevelStore);
53
80
 
54
- return created;
81
+ return LazyLevelStore;
55
82
  }
56
83
 
57
- function commonPrefix(previous: string[], next: string[], max: number): number {
58
- let count = 0;
84
+ function slotRefFor(value: GObject.Object | null): SlotRef | null {
85
+ const item = value instanceof Gtk.TreeListRow ? value.getItem() : value;
59
86
 
60
- while (count < max && previous[count] === next[count]) {
61
- count += 1;
87
+ if (item === null) {
88
+ return null;
62
89
  }
63
90
 
64
- return count;
91
+ const ref = SLOTS.get(item);
92
+
93
+ return ref === undefined || ref.slot === -1 ? null : ref;
65
94
  }
66
95
 
67
- function commonSuffix(previous: string[], next: string[], max: number): number {
68
- let count = 0;
96
+ function slotPathAt(store: LevelStore, slot: number): string {
97
+ return store.level.path + encodePart(String(slot));
98
+ }
69
99
 
70
- while (count < max && previous[previous.length - 1 - count] === next[next.length - 1 - count]) {
71
- count += 1;
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);
72
105
  }
106
+ }
107
+
108
+ function newLevelStore(level: Level): LevelStore {
109
+ const store = new (registeredStoreClass())() as LevelStore;
110
+ store.level = level;
111
+ growStore(store);
73
112
 
74
- return count;
113
+ return store;
75
114
  }
76
115
 
77
- function spliceRange(previous: string[], next: string[]): SpliceRange | null {
78
- const shared = Math.min(previous.length, next.length);
79
- const start = commonPrefix(previous, next, shared);
116
+ function collectFlips(previous: Level, next: Level, overlap: number): Set<number> {
117
+ const flipped: Set<number> = new Set();
80
118
 
81
- if (start === previous.length && start === next.length) {
82
- return null;
119
+ for (let slot = 0; slot < overlap; slot++) {
120
+ if ((previous.expandableFlags[slot] ?? false) !== (next.expandableFlags[slot] ?? false)) {
121
+ flipped.add(slot);
122
+ }
123
+ }
124
+
125
+ return flipped;
126
+ }
127
+
128
+ function pruneFlips(context: SyncContext, store: LevelStore, flipped: Set<number>): void {
129
+ if (flipped.size === 0) {
130
+ return;
83
131
  }
84
132
 
85
- const tail = commonSuffix(previous, next, shared - start);
133
+ pruneSlots(context.expansion, store.level.path, (slot) => flipped.has(slot));
86
134
 
87
- return { start, removed: previous.length - start - tail, added: next.slice(start, next.length - tail) };
135
+ for (const slot of flipped) {
136
+ store.childStores[slot] = null;
137
+ }
88
138
  }
89
139
 
90
- function levelFor(state: ModelState, key: string): LevelState {
91
- const existing = state.levels.get(key);
140
+ function detachRefs(store: LevelStore, nextLength: number): void {
141
+ for (let slot = nextLength; slot < store.refs.length; slot++) {
142
+ const ref = store.refs[slot];
92
143
 
93
- if (existing !== undefined) {
94
- return existing;
144
+ if (ref !== undefined) {
145
+ ref.slot = -1;
146
+ }
95
147
  }
148
+ }
96
149
 
97
- const created: LevelState = { store: newStore(), ids: [], canExpand: new Map() };
98
- state.levels.set(key, created);
150
+ function shrinkStore(context: SyncContext, store: LevelStore, nextLength: number): void {
151
+ if (store.refs.length <= nextLength) {
152
+ return;
153
+ }
99
154
 
100
- return created;
155
+ detachRefs(store, nextLength);
156
+ pruneSlots(context.expansion, store.level.path, (slot) => slot >= nextLength);
157
+ store.refs.length = nextLength;
158
+ store.objects.length = nextLength;
159
+ store.childStores.length = nextLength;
101
160
  }
102
161
 
103
- function refreshExpandable(current: LevelState, level: Level): void {
104
- const next: Map<string, boolean> = new Map();
162
+ function emitTailSplice(store: LevelStore, previousLength: number, nextLength: number): void {
163
+ if (nextLength < previousLength) {
164
+ store.itemsChanged(nextLength, previousLength - nextLength, 0);
105
165
 
106
- for (const [index, id] of level.ids.entries()) {
107
- const isWanted = level.expandableFlags[index] ?? false;
108
- const previous = current.canExpand.get(id);
109
- next.set(id, isWanted);
166
+ return;
167
+ }
110
168
 
111
- if (previous !== undefined && previous !== isWanted) {
112
- current.store.itemsChanged(index, 1, 1);
113
- }
169
+ if (nextLength > previousLength) {
170
+ store.itemsChanged(previousLength, 0, nextLength - previousLength);
171
+ }
172
+ }
173
+
174
+ function extendRuns(runs: SlotRun[], slot: number): void {
175
+ const last = runs.at(-1);
176
+
177
+ if (last !== undefined && last.start + last.length === slot) {
178
+ last.length += 1;
179
+
180
+ return;
181
+ }
182
+
183
+ runs.push({ start: slot, length: 1 });
184
+ }
185
+
186
+ function collectFlipRuns(flipped: Set<number>): SlotRun[] {
187
+ const runs: SlotRun[] = [];
188
+
189
+ for (const slot of flipped) {
190
+ extendRuns(runs, slot);
114
191
  }
115
192
 
116
- current.canExpand = next;
193
+ return runs;
194
+ }
195
+
196
+ function emitFlips(store: LevelStore, flipped: Set<number>): void {
197
+ for (const run of collectFlipRuns(flipped)) {
198
+ store.itemsChanged(run.start, run.length, run.length);
199
+ }
117
200
  }
118
201
 
119
- function didSpliceLevel(state: ModelState, level: Level): boolean {
120
- const current = levelFor(state, level.key);
121
- const range = spliceRange(current.ids, level.ids);
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;
122
209
 
123
- if (range === null) {
124
- return false;
210
+ if (child === null || flipped.has(slot)) {
211
+ return undefined;
125
212
  }
126
213
 
127
- current.store.splice(range.start, range.removed, range.added.map((id) => objectFor(state, id)));
128
- current.ids = [...level.ids];
214
+ const level = context.index.childLevel(store.level, slot);
129
215
 
130
- return true;
216
+ return level === undefined ? undefined : { store: child, level };
131
217
  }
132
218
 
133
- function dropUnvisited(state: ModelState, visited: Set<string>): void {
134
- for (const key of state.levels.keys()) {
135
- if (!visited.has(key)) {
136
- state.levels.delete(key);
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);
137
227
  }
138
228
  }
229
+
230
+ return syncs;
139
231
  }
140
232
 
141
- function pruneObjects(state: ModelState, index: CollectionIndex): void {
142
- if (state.objects.size <= OBJECT_CACHE_MAX) {
143
- return;
233
+ function syncEntry(context: SyncContext, entry: LevelSync): LevelSync[] {
234
+ const { store, level } = entry;
235
+ const previousLength = store.refs.length;
236
+ const overlap = Math.min(previousLength, level.items.length);
237
+ const flipped = collectFlips(store.level, level, overlap);
238
+ store.level = level;
239
+ pruneFlips(context, store, flipped);
240
+ shrinkStore(context, store, level.items.length);
241
+ growStore(store);
242
+ emitTailSplice(store, previousLength, level.items.length);
243
+ emitFlips(store, flipped);
244
+
245
+ return childSyncs(context, store, overlap, flipped);
246
+ }
247
+
248
+ function pushSyncs(pending: LevelSync[], syncs: LevelSync[]): void {
249
+ for (const sync of syncs) {
250
+ pending.push(sync);
144
251
  }
252
+ }
145
253
 
146
- for (const id of state.objects.keys()) {
147
- if (!index.hasId(id)) {
148
- state.objects.delete(id);
254
+ function syncLevel(context: SyncContext, store: LevelStore, level: Level): void {
255
+ const pending: LevelSync[] = [{ store, level }];
256
+
257
+ while (pending.length > 0) {
258
+ const entry = pending.pop();
259
+
260
+ if (entry !== undefined) {
261
+ pushSyncs(pending, syncEntry(context, entry).toReversed());
149
262
  }
150
263
  }
151
264
  }
152
265
 
153
- function childStoreFor(state: ModelState, object: GObject.Object): Gio.ListStore | null {
154
- const id = getId(object);
266
+ function ensureChildStore(index: CollectionIndex, store: LevelStore, slot: number): LevelStore | null {
267
+ const existing = store.childStores[slot] ?? null;
268
+
269
+ if (existing !== null || !(store.level.expandableFlags[slot] ?? false)) {
270
+ return existing;
271
+ }
272
+
273
+ const level = index.childLevel(store.level, slot);
274
+
275
+ if (level === undefined) {
276
+ return null;
277
+ }
278
+
279
+ const created = newLevelStore(level);
280
+ store.childStores[slot] = created;
281
+
282
+ return created;
283
+ }
284
+
285
+ function childStoreFor(state: ModelState, object: GObject.Object): Gio.ListModel | null {
286
+ const ref = slotRefFor(object);
155
287
 
156
- if (id === null) {
288
+ if (ref === null) {
157
289
  return null;
158
290
  }
159
291
 
160
- return state.levels.get(childLevelKey(id))?.store ?? null;
292
+ return ensureChildStore(state.index, ref.store, ref.slot);
161
293
  }
162
294
 
163
- function ensureTree(state: ModelState): Gtk.TreeListModel {
164
- state.tree ??= Gtk.TreeListModel.new(levelFor(state, ROOT_LEVEL_KEY).store, false, false, (object) =>
165
- childStoreFor(state, object));
295
+ function treeFor(state: ModelState, store: LevelStore): Gtk.TreeListModel {
296
+ return (
297
+ state.trees.get(store) ?? Gtk.TreeListModel.new(store, false, false, (object) => childStoreFor(state, object))
298
+ );
299
+ }
300
+
301
+ function nextTrees(state: ModelState, index: CollectionIndex): Map<LevelStore, Gtk.TreeListModel> {
302
+ if (!index.isTree) {
303
+ return new Map();
304
+ }
305
+
306
+ return new Map(state.groupStores.map((store) => [store, treeFor(state, store)]));
307
+ }
308
+
309
+ function pruneRebuiltLevels(state: ModelState, next: Map<LevelStore, Gtk.TreeListModel>): void {
310
+ const stores: Set<LevelStore> = new Set([...state.trees.keys(), ...next.keys()]);
166
311
 
167
- return state.tree;
312
+ for (const store of stores) {
313
+ if (state.trees.get(store) !== next.get(store)) {
314
+ pruneSlots(state.expansion, store.level.path, () => true);
315
+ }
316
+ }
317
+ }
318
+
319
+ function adoptTrees(state: ModelState, index: CollectionIndex): void {
320
+ const next = nextTrees(state, index);
321
+ pruneRebuiltLevels(state, next);
322
+ state.trees = next;
323
+ state.treeModels = next.values().toArray();
168
324
  }
169
325
 
170
326
  function desiredRootModels(state: ModelState, index: CollectionIndex): GObject.Object[] {
171
- if (index.isTree) {
172
- return [ensureTree(state)];
327
+ adoptTrees(state, index);
328
+
329
+ return index.isTree ? [...state.treeModels] : [...state.groupStores];
330
+ }
331
+
332
+ function rowAt(state: ModelState, position: number): Gtk.TreeListRow | null {
333
+ let offset = position;
334
+
335
+ for (const tree of state.treeModels) {
336
+ const count = tree.getNItems();
337
+
338
+ if (offset < count) {
339
+ return tree.getRow(offset);
340
+ }
341
+
342
+ offset -= count;
173
343
  }
174
344
 
175
- return index.groups.map((level) => levelFor(state, level.key).store);
345
+ return null;
176
346
  }
177
347
 
178
348
  function hasSameModels(previous: GObject.Object[], next: GObject.Object[]): boolean {
@@ -190,54 +360,99 @@ function syncRoot(state: ModelState, index: CollectionIndex): void {
190
360
  state.rootModels = next;
191
361
  }
192
362
 
193
- function syncModel(state: ModelState, index: CollectionIndex): void {
194
- const levels = [...index.groups, ...index.children.values()];
195
- const visited: Set<string> = new Set();
196
- let didChange = false;
363
+ function adjustGroupStores(state: ModelState, context: SyncContext): void {
364
+ const { groups } = context.index;
365
+ state.groupStores.length = Math.min(state.groupStores.length, groups.length);
197
366
 
198
- for (const level of levels) {
199
- didChange = didSpliceLevel(state, level) || didChange;
200
- visited.add(level.key);
367
+ for (const level of groups.slice(state.groupStores.length)) {
368
+ state.groupStores.push(newLevelStore(level));
201
369
  }
370
+ }
202
371
 
203
- if (didChange || visited.size !== state.levels.size) {
204
- dropUnvisited(state, visited);
205
- pruneObjects(state, index);
206
- }
372
+ function stepGroupStores(state: ModelState, context: SyncContext, surviving: number): void {
373
+ for (const [group, level] of context.index.groups.entries()) {
374
+ const store = state.groupStores[group];
207
375
 
208
- syncRoot(state, index);
209
- refreshExpandableLevels(state, index, levels);
376
+ if (store !== undefined && group < surviving) {
377
+ syncLevel(context, store, level);
378
+ }
379
+ }
210
380
  }
211
381
 
212
- function refreshExpandableLevels(state: ModelState, index: CollectionIndex, levels: Level[]): void {
213
- if (!index.isTree) {
214
- return;
382
+ function syncModel(state: ModelState, index: CollectionIndex): void {
383
+ const context: SyncContext = { index, expansion: state.expansion };
384
+ const surviving = Math.min(state.groupStores.length, index.groups.length);
385
+ state.index = index;
386
+ state.expansion.isSyncing = true;
387
+
388
+ try {
389
+ adjustGroupStores(state, context);
390
+ syncRoot(state, index);
391
+ stepGroupStores(state, context, surviving);
392
+ } finally {
393
+ state.expansion.isSyncing = false;
215
394
  }
216
395
 
217
- for (const level of levels) {
218
- refreshExpandable(levelFor(state, level.key), level);
219
- }
396
+ adoptIndex(state.expansion, index);
220
397
  }
221
398
 
222
399
  function createCollectionModel(): CollectionModel {
223
- const root = newStore();
400
+ registeredStoreClass();
401
+ const root = newRootStore();
224
402
 
225
403
  const state: ModelState = {
226
404
  root,
227
405
  rootModels: [],
228
406
  model: Gtk.FlattenListModel.new(root),
229
- objects: new Map(),
230
- levels: new Map(),
231
- tree: null,
407
+ groupStores: [],
408
+ trees: new Map(),
409
+ treeModels: [],
410
+ expansion: createTreeExpansion(EMPTY_INDEX),
411
+ index: EMPTY_INDEX,
232
412
  };
233
413
 
234
414
  return {
235
415
  model: state.model,
236
- treeModel: () => state.tree,
416
+ expansion: state.expansion,
417
+ rowAt: (position) => rowAt(state, position),
237
418
  sync: (index) => {
238
419
  syncModel(state, index);
239
420
  },
240
421
  };
241
422
  }
242
423
 
243
- export { createCollectionModel, getId, type CollectionModel };
424
+ class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
425
+ declare itemsChanged: Gio.ListModel["itemsChanged"];
426
+ level: Level = emptyLevel();
427
+ refs: SlotRef[] = [];
428
+ objects: (Gtk.StringObject | null)[] = [];
429
+ childStores: (LevelStore | null)[] = [];
430
+
431
+ private createItem(position: number, ref: SlotRef): Gtk.StringObject {
432
+ const created = Gtk.StringObject.new("");
433
+ this.objects[position] = created;
434
+ SLOTS.set(created, ref);
435
+
436
+ return created;
437
+ }
438
+
439
+ vfuncGetItemType(): bigint {
440
+ return GObject.TYPE_OBJECT;
441
+ }
442
+
443
+ vfuncGetNItems(): number {
444
+ return this.refs.length;
445
+ }
446
+
447
+ vfuncGetItem(position: number): GObject.Object | null {
448
+ const ref = this.refs[position];
449
+
450
+ if (ref === undefined) {
451
+ return null;
452
+ }
453
+
454
+ return this.objects[position] ?? this.createItem(position, ref);
455
+ }
456
+ }
457
+
458
+ export { createCollectionModel, slotPathAt, slotRefFor, type CollectionModel, type SlotRef };