@gtkx/components 1.0.0 → 1.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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/adw/toast.d.ts.map +1 -1
  3. package/dist/adw/toast.js +6 -2
  4. package/dist/adw/toast.js.map +1 -1
  5. package/dist/internal/cells.js +1 -1
  6. package/dist/internal/cells.js.map +1 -1
  7. package/dist/internal/collection-index.d.ts +4 -3
  8. package/dist/internal/collection-index.d.ts.map +1 -1
  9. package/dist/internal/collection-index.js +50 -33
  10. package/dist/internal/collection-index.js.map +1 -1
  11. package/dist/internal/collection-model.d.ts +3 -4
  12. package/dist/internal/collection-model.d.ts.map +1 -1
  13. package/dist/internal/collection-model.js +110 -74
  14. package/dist/internal/collection-model.js.map +1 -1
  15. package/dist/internal/collection.d.ts +2 -1
  16. package/dist/internal/collection.d.ts.map +1 -1
  17. package/dist/internal/collection.js +7 -6
  18. package/dist/internal/collection.js.map +1 -1
  19. package/dist/internal/expansion.d.ts.map +1 -1
  20. package/dist/internal/expansion.js +11 -19
  21. package/dist/internal/expansion.js.map +1 -1
  22. package/dist/internal/tree-expansion.d.ts +1 -2
  23. package/dist/internal/tree-expansion.d.ts.map +1 -1
  24. package/dist/internal/tree-expansion.js +12 -8
  25. package/dist/internal/tree-expansion.js.map +1 -1
  26. package/dist/internal/tree-order.d.ts +3 -4
  27. package/dist/internal/tree-order.d.ts.map +1 -1
  28. package/dist/internal/tree-order.js +61 -23
  29. package/dist/internal/tree-order.js.map +1 -1
  30. package/dist/types.d.ts +3 -2
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/types.js.map +1 -1
  33. package/package.json +17 -9
  34. package/src/adw/toast.tsx +6 -2
  35. package/src/internal/cells.tsx +1 -1
  36. package/src/internal/collection-index.ts +63 -39
  37. package/src/internal/collection-model.ts +148 -85
  38. package/src/internal/collection.ts +10 -8
  39. package/src/internal/expansion.ts +12 -24
  40. package/src/internal/tree-expansion.ts +14 -9
  41. package/src/internal/tree-order.ts +98 -28
  42. package/src/types.ts +3 -2
@@ -1,5 +1,5 @@
1
1
  import type { ListItem, ListSection } from "../types.js";
2
- import { encodePart } from "./keys.js";
2
+ import { decodePartAt, encodePart } from "./keys.js";
3
3
 
4
4
  type Level = {
5
5
  path: string;
@@ -10,83 +10,109 @@ type Level = {
10
10
  type CollectionIndex = {
11
11
  isTree: boolean;
12
12
  groups: Level[];
13
- children: Map<string, Level>;
13
+ childLevel: (level: Level, slot: number) => Level | undefined;
14
+ levelFor: (levelPath: string) => Level | undefined;
14
15
  itemAt: (levelPath: string, slot: number) => ListItem | undefined;
15
16
  sectionFor: (levelPath: string) => unknown;
16
- expandablePathsFor: (id: string) => string[];
17
17
  };
18
18
 
19
19
  type IndexState = {
20
20
  isTree: boolean;
21
21
  groups: Level[];
22
- children: Map<string, Level>;
23
22
  levels: Map<string, Level>;
24
- expandablePaths: Map<string, string[]>;
25
23
  sectionValues: Map<string, unknown>;
26
24
  };
27
25
 
28
- const NO_PATHS: string[] = [];
26
+ type ParentItem = ListItem & { children: ListItem[] };
29
27
 
30
- const hasChildren = (item: ListItem): boolean => item.children !== undefined && item.children.length > 0;
28
+ const emptyLevel = (): Level => ({ path: "", items: [], expandableFlags: [] });
29
+ const hasChildren = (item: ListItem): item is ParentItem => item.children !== undefined && item.children.length > 0;
31
30
 
32
- function pushPath(table: Map<string, string[]>, id: string, path: string): void {
33
- const existing = table.get(id);
31
+ function newLevel(state: IndexState, path: string, items: ListItem[]): Level {
32
+ const expandableFlags = state.isTree ? items.map((item) => hasChildren(item)) : [];
33
+ const level: Level = { path, items, expandableFlags };
34
+ state.levels.set(path, level);
34
35
 
35
- if (existing === undefined) {
36
- table.set(id, [path]);
36
+ return level;
37
+ }
37
38
 
38
- return;
39
- }
39
+ function slotItems(state: IndexState, parent: Level, slot: number): ListItem[] | undefined {
40
+ const owner = state.levels.get(parent.path) === parent ? parent : levelFor(state, parent.path);
41
+ const item = owner?.items[slot];
40
42
 
41
- existing.push(path);
43
+ return item !== undefined && hasChildren(item) ? item.children : undefined;
42
44
  }
43
45
 
44
- function collectChildren(state: IndexState, level: Level, slotPath: string, item: ListItem): void {
45
- const canExpand = hasChildren(item);
46
- level.expandableFlags.push(canExpand);
46
+ function childLevel(state: IndexState, parent: Level, slot: number): Level | undefined {
47
+ if (!state.isTree) {
48
+ return undefined;
49
+ }
47
50
 
48
- if (!canExpand) {
49
- return;
51
+ const path = parent.path + encodePart(String(slot));
52
+ const cached = state.levels.get(path);
53
+
54
+ if (cached !== undefined) {
55
+ return cached;
50
56
  }
51
57
 
52
- pushPath(state.expandablePaths, item.id, slotPath);
53
- state.children.set(slotPath, collectLevel(state, slotPath, item.children ?? []));
58
+ const items = slotItems(state, parent, slot);
59
+
60
+ return items === undefined ? undefined : newLevel(state, path, items);
54
61
  }
55
62
 
56
- function collectLevel(state: IndexState, path: string, items: ListItem[]): Level {
57
- const level: Level = { path, items, expandableFlags: [] };
58
- state.levels.set(path, level);
63
+ function descendPath(state: IndexState, levelPath: string, group: string): Level | undefined {
64
+ let level = state.levels.get(encodePart(group));
65
+ let offset = encodePart(group).length;
59
66
 
60
- if (!state.isTree) {
61
- return level;
62
- }
67
+ while (level !== undefined && offset < levelPath.length) {
68
+ const part = decodePartAt(levelPath, offset);
63
69
 
64
- for (const [slot, item] of items.entries()) {
65
- collectChildren(state, level, path + encodePart(String(slot)), item);
70
+ if (part === null) {
71
+ return undefined;
72
+ }
73
+
74
+ level = childLevel(state, level, Number(part));
75
+ offset += encodePart(part).length;
66
76
  }
67
77
 
68
78
  return level;
69
79
  }
70
80
 
81
+ function levelFor(state: IndexState, levelPath: string): Level | undefined {
82
+ const cached = state.levels.get(levelPath);
83
+
84
+ if (cached !== undefined) {
85
+ return cached;
86
+ }
87
+
88
+ const group = decodePartAt(levelPath, 0);
89
+
90
+ return group === null ? undefined : descendPath(state, levelPath, group);
91
+ }
92
+
71
93
  function buildGroups(state: IndexState, source: ListItem[], sections: ListSection[] | undefined): Level[] {
72
94
  if (sections === undefined) {
73
- return [collectLevel(state, encodePart("0"), source)];
95
+ return [newLevel(state, encodePart("0"), source)];
74
96
  }
75
97
 
76
98
  return sections.map((section, group) => {
77
99
  const path = encodePart(String(group));
78
100
  state.sectionValues.set(path, section.value);
79
101
 
80
- return collectLevel(state, path, section.data);
102
+ return newLevel(state, path, section.data);
81
103
  });
82
104
  }
83
105
 
84
106
  function isTreeSource(source: ListItem[], sections: ListSection[] | undefined, isFlat: boolean): boolean {
85
- if (sections !== undefined || isFlat) {
107
+ if (isFlat) {
86
108
  return false;
87
109
  }
88
110
 
89
- return source.some((item) => hasChildren(item));
111
+ if (sections === undefined) {
112
+ return source.some((item) => hasChildren(item));
113
+ }
114
+
115
+ return sections.some((section) => section.data.some((item) => hasChildren(item)));
90
116
  }
91
117
 
92
118
  function createCollectionIndex(
@@ -99,9 +125,7 @@ function createCollectionIndex(
99
125
  const state: IndexState = {
100
126
  isTree: isTreeSource(source, sections, isFlat),
101
127
  groups: [],
102
- children: new Map(),
103
128
  levels: new Map(),
104
- expandablePaths: new Map(),
105
129
  sectionValues: new Map(),
106
130
  };
107
131
 
@@ -110,11 +134,11 @@ function createCollectionIndex(
110
134
  return {
111
135
  isTree: state.isTree,
112
136
  groups: state.groups,
113
- children: state.children,
114
- itemAt: (levelPath, slot) => state.levels.get(levelPath)?.items[slot],
137
+ childLevel: (level, slot) => childLevel(state, level, slot),
138
+ levelFor: (levelPath) => levelFor(state, levelPath),
139
+ itemAt: (levelPath, slot) => levelFor(state, levelPath)?.items[slot],
115
140
  sectionFor: (levelPath) => state.sectionValues.get(levelPath),
116
- expandablePathsFor: (id) => state.expandablePaths.get(id) ?? NO_PATHS,
117
141
  };
118
142
  }
119
143
 
120
- export { createCollectionIndex, type CollectionIndex, type Level };
144
+ export { createCollectionIndex, emptyLevel, type CollectionIndex, type Level };
@@ -4,9 +4,9 @@ import * as Gtk from "@gtkx/gi/gtk";
4
4
  import { registerClass } from "@gtkx/runtime";
5
5
  import type { CollectionIndex, Level } from "./collection-index.js";
6
6
  import type { TreeExpansion } from "./tree-expansion.js";
7
- import { createCollectionIndex } from "./collection-index.js";
7
+ import { createCollectionIndex, emptyLevel } from "./collection-index.js";
8
8
  import { encodePart } from "./keys.js";
9
- import { adoptIndex, createTreeExpansion, pruneSlots, resetExpansion } from "./tree-expansion.js";
9
+ import { adoptIndex, createTreeExpansion, pruneSlots } from "./tree-expansion.js";
10
10
 
11
11
  type LevelStore = Gio.ListModel & LazyLevelStore;
12
12
 
@@ -25,20 +25,26 @@ type SlotRun = {
25
25
  length: number;
26
26
  };
27
27
 
28
+ type LevelSync = {
29
+ store: LevelStore;
30
+ level: Level;
31
+ };
32
+
28
33
  type ModelState = {
29
34
  root: Gio.ListStore;
30
35
  rootModels: GObject.Object[];
31
36
  model: Gtk.FlattenListModel;
32
37
  groupStores: LevelStore[];
33
- tree: Gtk.TreeListModel | null;
34
- treeRoot: LevelStore | null;
38
+ trees: Map<LevelStore, Gtk.TreeListModel>;
39
+ treeModels: Gtk.TreeListModel[];
35
40
  expansion: TreeExpansion;
41
+ index: CollectionIndex;
36
42
  };
37
43
 
38
44
  type CollectionModel = {
39
45
  model: Gtk.FlattenListModel;
40
46
  expansion: TreeExpansion;
41
- treeModel: () => Gtk.TreeListModel | null;
47
+ rowAt: (position: number) => Gtk.TreeListRow | null;
42
48
  sync: (index: CollectionIndex) => void;
43
49
  };
44
50
 
@@ -88,55 +94,30 @@ function slotRefFor(value: GObject.Object | null): SlotRef | null {
88
94
  }
89
95
 
90
96
  function slotPathAt(store: LevelStore, slot: number): string {
91
- return store.path + encodePart(String(slot));
92
- }
93
-
94
- function newLevelStore(path: string): LevelStore {
95
- const store = new (registeredStoreClass())() as LevelStore;
96
- store.path = path;
97
-
98
- return store;
97
+ return store.level.path + encodePart(String(slot));
99
98
  }
100
99
 
101
- function buildChildStore(context: SyncContext, path: string): LevelStore | null {
102
- const level = context.index.children.get(path);
103
-
104
- if (level === undefined) {
105
- return null;
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);
106
105
  }
107
-
108
- const store = newLevelStore(path);
109
- growStore(context, store, level);
110
-
111
- return store;
112
- }
113
-
114
- function pushSlot(context: SyncContext, store: LevelStore, level: Level, slot: number): void {
115
- const canExpand = level.expandableFlags[slot] ?? false;
116
- store.refs.push({ store, slot });
117
- store.objects.push(null);
118
- store.expandableFlags.push(canExpand);
119
- store.childStores.push(canExpand ? buildChildStore(context, slotPathAt(store, slot)) : null);
120
106
  }
121
107
 
122
- function growStore(context: SyncContext, store: LevelStore, level: Level): void {
123
- for (let slot = store.refs.length; slot < level.items.length; slot++) {
124
- pushSlot(context, store, level, slot);
125
- }
126
- }
108
+ function newLevelStore(level: Level): LevelStore {
109
+ const store = new (registeredStoreClass())() as LevelStore;
110
+ store.level = level;
111
+ growStore(store);
127
112
 
128
- function flipSlot(context: SyncContext, store: LevelStore, slot: number): void {
129
- const canExpand = !(store.expandableFlags[slot] ?? false);
130
- store.expandableFlags[slot] = canExpand;
131
- store.childStores[slot] = canExpand ? buildChildStore(context, slotPathAt(store, slot)) : null;
113
+ return store;
132
114
  }
133
115
 
134
- function collectFlips(store: LevelStore, level: Level): Set<number> {
135
- const overlap = Math.min(store.refs.length, level.items.length);
116
+ function collectFlips(previous: Level, next: Level, overlap: number): Set<number> {
136
117
  const flipped: Set<number> = new Set();
137
118
 
138
119
  for (let slot = 0; slot < overlap; slot++) {
139
- if ((store.expandableFlags[slot] ?? false) !== (level.expandableFlags[slot] ?? false)) {
120
+ if ((previous.expandableFlags[slot] ?? false) !== (next.expandableFlags[slot] ?? false)) {
140
121
  flipped.add(slot);
141
122
  }
142
123
  }
@@ -144,20 +125,16 @@ function collectFlips(store: LevelStore, level: Level): Set<number> {
144
125
  return flipped;
145
126
  }
146
127
 
147
- function syncOverlap(context: SyncContext, store: LevelStore, level: Level): Set<number> {
148
- const flipped = collectFlips(store, level);
149
-
128
+ function pruneFlips(context: SyncContext, store: LevelStore, flipped: Set<number>): void {
150
129
  if (flipped.size === 0) {
151
- return flipped;
130
+ return;
152
131
  }
153
132
 
154
- pruneSlots(context.expansion, store.path, (slot) => flipped.has(slot));
133
+ pruneSlots(context.expansion, store.level.path, (slot) => flipped.has(slot));
155
134
 
156
135
  for (const slot of flipped) {
157
- flipSlot(context, store, slot);
136
+ store.childStores[slot] = null;
158
137
  }
159
-
160
- return flipped;
161
138
  }
162
139
 
163
140
  function detachRefs(store: LevelStore, nextLength: number): void {
@@ -176,11 +153,10 @@ function shrinkStore(context: SyncContext, store: LevelStore, nextLength: number
176
153
  }
177
154
 
178
155
  detachRefs(store, nextLength);
179
- pruneSlots(context.expansion, store.path, (slot) => slot >= nextLength);
156
+ pruneSlots(context.expansion, store.level.path, (slot) => slot >= nextLength);
180
157
  store.refs.length = nextLength;
181
158
  store.objects.length = nextLength;
182
159
  store.childStores.length = nextLength;
183
- store.expandableFlags.length = nextLength;
184
160
  }
185
161
 
186
162
  function emitTailSplice(store: LevelStore, previousLength: number, nextLength: number): void {
@@ -223,61 +199,150 @@ function emitFlips(store: LevelStore, flipped: Set<number>): void {
223
199
  }
224
200
  }
225
201
 
226
- function stepChildStore(context: SyncContext, store: LevelStore, slot: number, flipped: Set<number>): void {
227
- const child = store.childStores[slot];
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;
228
209
 
229
- if (child == null || flipped.has(slot)) {
230
- return;
210
+ if (child === null || flipped.has(slot)) {
211
+ return undefined;
231
212
  }
232
213
 
233
- const level = context.index.children.get(slotPathAt(store, slot));
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[] = [];
234
221
 
235
- if (level !== undefined) {
236
- syncLevel(context, child, level);
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
+ }
237
228
  }
229
+
230
+ return syncs;
238
231
  }
239
232
 
240
- function syncLevel(context: SyncContext, store: LevelStore, level: Level): void {
233
+ function syncEntry(context: SyncContext, entry: LevelSync): LevelSync[] {
234
+ const { store, level } = entry;
241
235
  const previousLength = store.refs.length;
242
236
  const overlap = Math.min(previousLength, level.items.length);
243
- const flipped = syncOverlap(context, store, level);
237
+ const flipped = collectFlips(store.level, level, overlap);
238
+ store.level = level;
239
+ pruneFlips(context, store, flipped);
244
240
  shrinkStore(context, store, level.items.length);
245
- growStore(context, store, level);
241
+ growStore(store);
246
242
  emitTailSplice(store, previousLength, level.items.length);
247
243
  emitFlips(store, flipped);
248
244
 
249
- for (let slot = 0; slot < overlap; slot++) {
250
- stepChildStore(context, store, slot, flipped);
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);
251
251
  }
252
252
  }
253
253
 
254
- function childStoreFor(object: GObject.Object): Gio.ListModel | null {
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());
262
+ }
263
+ }
264
+ }
265
+
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 {
255
286
  const ref = slotRefFor(object);
256
287
 
257
288
  if (ref === null) {
258
289
  return null;
259
290
  }
260
291
 
261
- return ref.store.childStores[ref.slot] ?? null;
292
+ return ensureChildStore(state.index, ref.store, ref.slot);
262
293
  }
263
294
 
264
- function ensureTree(state: ModelState, first: LevelStore): Gtk.TreeListModel {
265
- if (state.tree === null || state.treeRoot !== first) {
266
- state.tree = Gtk.TreeListModel.new(first, false, false, (object) => childStoreFor(object));
267
- state.treeRoot = first;
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();
268
304
  }
269
305
 
270
- return state.tree;
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()]);
311
+
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();
271
324
  }
272
325
 
273
326
  function desiredRootModels(state: ModelState, index: CollectionIndex): GObject.Object[] {
274
- const [first] = state.groupStores;
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
+ }
275
341
 
276
- if (first !== undefined && index.isTree) {
277
- return [ensureTree(state, first)];
342
+ offset -= count;
278
343
  }
279
344
 
280
- return [...state.groupStores];
345
+ return null;
281
346
  }
282
347
 
283
348
  function hasSameModels(previous: GObject.Object[], next: GObject.Object[]): boolean {
@@ -293,7 +358,6 @@ function syncRoot(state: ModelState, index: CollectionIndex): void {
293
358
 
294
359
  state.root.splice(0, state.rootModels.length, next);
295
360
  state.rootModels = next;
296
- resetExpansion(state.expansion);
297
361
  }
298
362
 
299
363
  function adjustGroupStores(state: ModelState, context: SyncContext): void {
@@ -301,9 +365,7 @@ function adjustGroupStores(state: ModelState, context: SyncContext): void {
301
365
  state.groupStores.length = Math.min(state.groupStores.length, groups.length);
302
366
 
303
367
  for (const level of groups.slice(state.groupStores.length)) {
304
- const store = newLevelStore(level.path);
305
- growStore(context, store, level);
306
- state.groupStores.push(store);
368
+ state.groupStores.push(newLevelStore(level));
307
369
  }
308
370
  }
309
371
 
@@ -320,6 +382,7 @@ function stepGroupStores(state: ModelState, context: SyncContext, surviving: num
320
382
  function syncModel(state: ModelState, index: CollectionIndex): void {
321
383
  const context: SyncContext = { index, expansion: state.expansion };
322
384
  const surviving = Math.min(state.groupStores.length, index.groups.length);
385
+ state.index = index;
323
386
  state.expansion.isSyncing = true;
324
387
 
325
388
  try {
@@ -342,15 +405,16 @@ function createCollectionModel(): CollectionModel {
342
405
  rootModels: [],
343
406
  model: Gtk.FlattenListModel.new(root),
344
407
  groupStores: [],
345
- tree: null,
346
- treeRoot: null,
408
+ trees: new Map(),
409
+ treeModels: [],
347
410
  expansion: createTreeExpansion(EMPTY_INDEX),
411
+ index: EMPTY_INDEX,
348
412
  };
349
413
 
350
414
  return {
351
415
  model: state.model,
352
416
  expansion: state.expansion,
353
- treeModel: () => state.tree,
417
+ rowAt: (position) => rowAt(state, position),
354
418
  sync: (index) => {
355
419
  syncModel(state, index);
356
420
  },
@@ -359,11 +423,10 @@ function createCollectionModel(): CollectionModel {
359
423
 
360
424
  class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
361
425
  declare itemsChanged: Gio.ListModel["itemsChanged"];
362
- path = "";
426
+ level: Level = emptyLevel();
363
427
  refs: SlotRef[] = [];
364
428
  objects: (Gtk.StringObject | null)[] = [];
365
429
  childStores: (LevelStore | null)[] = [];
366
- expandableFlags: boolean[] = [];
367
430
 
368
431
  private createItem(position: number, ref: SlotRef): Gtk.StringObject {
369
432
  const created = Gtk.StringObject.new("");
@@ -4,7 +4,8 @@ import type { CollectionModel, SlotRef } from "./collection-model.js";
4
4
  import { slotPathAt, slotRefFor } from "./collection-model.js";
5
5
  import { findPositions } from "./tree-order.js";
6
6
 
7
- type Collection = Pick<CollectionModel, "model" | "expansion" | "treeModel"> & {
7
+ type Collection = Pick<CollectionModel, "model" | "expansion" | "rowAt"> & {
8
+ isTree: boolean;
8
9
  itemAt: (ref: SlotRef) => ListItem | undefined;
9
10
  sectionFor: (levelPath: string) => unknown;
10
11
  idAt: (position: number) => string | null;
@@ -39,14 +40,14 @@ function refAt(collectionModel: CollectionModel, position: number): SlotRef | nu
39
40
  return slotRefFor(collectionModel.model.getItem(position));
40
41
  }
41
42
 
43
+ function itemAt(index: CollectionIndex, ref: SlotRef): ListItem | undefined {
44
+ return index.itemAt(ref.store.level.path, ref.slot);
45
+ }
46
+
42
47
  function idAt(collectionModel: CollectionModel, index: CollectionIndex, position: number): string | null {
43
48
  const ref = refAt(collectionModel, position);
44
49
 
45
- if (ref === null) {
46
- return null;
47
- }
48
-
49
- return index.itemAt(ref.store.path, ref.slot)?.id ?? null;
50
+ return ref === null ? null : (itemAt(index, ref)?.id ?? null);
50
51
  }
51
52
 
52
53
  function pathAt(collectionModel: CollectionModel, position: number): string | null {
@@ -69,8 +70,9 @@ function createCollection(collectionModel: CollectionModel, index: CollectionInd
69
70
  return {
70
71
  model: collectionModel.model,
71
72
  expansion: collectionModel.expansion,
72
- treeModel: () => (index.isTree ? collectionModel.treeModel() : null),
73
- itemAt: (ref) => index.itemAt(ref.store.path, ref.slot),
73
+ isTree: index.isTree,
74
+ rowAt: collectionModel.rowAt,
75
+ itemAt: (ref) => itemAt(index, ref),
74
76
  sectionFor: index.sectionFor,
75
77
  idAt: (position) => idAt(collectionModel, index, position),
76
78
  pathAt: (position) => pathAt(collectionModel, position),