@gtkx/components 2.0.0-beta.12 → 2.0.0-beta.13

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 (100) hide show
  1. package/README.md +1 -1
  2. package/dist/column-view.d.ts.map +1 -1
  3. package/dist/column-view.js +7 -3
  4. package/dist/column-view.js.map +1 -1
  5. package/dist/combo-row.d.ts.map +1 -1
  6. package/dist/combo-row.js +3 -1
  7. package/dist/combo-row.js.map +1 -1
  8. package/dist/grid-view.d.ts.map +1 -1
  9. package/dist/grid-view.js +3 -2
  10. package/dist/grid-view.js.map +1 -1
  11. package/dist/index.d.ts +1 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +0 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/internal/cells.d.ts +5 -2
  16. package/dist/internal/cells.d.ts.map +1 -1
  17. package/dist/internal/cells.js +86 -43
  18. package/dist/internal/cells.js.map +1 -1
  19. package/dist/internal/collection-index.d.ts +3 -5
  20. package/dist/internal/collection-index.d.ts.map +1 -1
  21. package/dist/internal/collection-index.js +3 -7
  22. package/dist/internal/collection-index.js.map +1 -1
  23. package/dist/internal/collection-model.d.ts +15 -4
  24. package/dist/internal/collection-model.d.ts.map +1 -1
  25. package/dist/internal/collection-model.js +154 -73
  26. package/dist/internal/collection-model.js.map +1 -1
  27. package/dist/internal/collection.d.ts +3 -3
  28. package/dist/internal/collection.d.ts.map +1 -1
  29. package/dist/internal/collection.js +11 -9
  30. package/dist/internal/collection.js.map +1 -1
  31. package/dist/internal/controlled-sync.d.ts +2 -1
  32. package/dist/internal/controlled-sync.d.ts.map +1 -1
  33. package/dist/internal/controlled-sync.js +6 -1
  34. package/dist/internal/controlled-sync.js.map +1 -1
  35. package/dist/internal/drop-down.d.ts +10 -4
  36. package/dist/internal/drop-down.d.ts.map +1 -1
  37. package/dist/internal/drop-down.js +60 -8
  38. package/dist/internal/drop-down.js.map +1 -1
  39. package/dist/internal/expansion.d.ts.map +1 -1
  40. package/dist/internal/expansion.js +46 -19
  41. package/dist/internal/expansion.js.map +1 -1
  42. package/dist/internal/first-header.d.ts +11 -0
  43. package/dist/internal/first-header.d.ts.map +1 -0
  44. package/dist/internal/first-header.js +21 -0
  45. package/dist/internal/first-header.js.map +1 -0
  46. package/dist/internal/level-splice.d.ts +27 -0
  47. package/dist/internal/level-splice.d.ts.map +1 -0
  48. package/dist/internal/level-splice.js +98 -0
  49. package/dist/internal/level-splice.js.map +1 -0
  50. package/dist/internal/selection.d.ts.map +1 -1
  51. package/dist/internal/selection.js +23 -16
  52. package/dist/internal/selection.js.map +1 -1
  53. package/dist/internal/tree-expansion.d.ts +3 -1
  54. package/dist/internal/tree-expansion.d.ts.map +1 -1
  55. package/dist/internal/tree-expansion.js +31 -2
  56. package/dist/internal/tree-expansion.js.map +1 -1
  57. package/dist/internal/tree-order.d.ts +1 -0
  58. package/dist/internal/tree-order.d.ts.map +1 -1
  59. package/dist/internal/tree-order.js +2 -1
  60. package/dist/internal/tree-order.js.map +1 -1
  61. package/dist/internal/use-collection.d.ts +1 -0
  62. package/dist/internal/use-collection.d.ts.map +1 -1
  63. package/dist/internal/use-collection.js +6 -2
  64. package/dist/internal/use-collection.js.map +1 -1
  65. package/dist/list-item-factory.d.ts +1 -1
  66. package/dist/list-item-factory.d.ts.map +1 -1
  67. package/dist/list-item-factory.js +2 -2
  68. package/dist/list-item-factory.js.map +1 -1
  69. package/dist/list-view.d.ts.map +1 -1
  70. package/dist/list-view.js +8 -3
  71. package/dist/list-view.js.map +1 -1
  72. package/dist/types.d.ts +37 -47
  73. package/dist/types.d.ts.map +1 -1
  74. package/dist/types.js.map +1 -1
  75. package/package.json +8 -8
  76. package/src/column-view.tsx +8 -4
  77. package/src/combo-row.tsx +4 -1
  78. package/src/grid-view.tsx +3 -2
  79. package/src/index.ts +0 -4
  80. package/src/internal/cells.tsx +141 -53
  81. package/src/internal/collection-index.ts +7 -14
  82. package/src/internal/collection-model.ts +211 -76
  83. package/src/internal/collection.ts +14 -13
  84. package/src/internal/controlled-sync.ts +8 -1
  85. package/src/internal/drop-down.tsx +111 -14
  86. package/src/internal/expansion.ts +52 -20
  87. package/src/internal/first-header.ts +23 -0
  88. package/src/internal/level-splice.ts +157 -0
  89. package/src/internal/selection.tsx +24 -19
  90. package/src/internal/tree-expansion.ts +52 -2
  91. package/src/internal/tree-order.ts +3 -1
  92. package/src/internal/use-collection.tsx +8 -2
  93. package/src/list-item-factory.tsx +2 -1
  94. package/src/list-view.tsx +10 -2
  95. package/src/types.ts +39 -51
  96. package/dist/tab-collection.d.ts +0 -9
  97. package/dist/tab-collection.d.ts.map +0 -1
  98. package/dist/tab-collection.js +0 -114
  99. package/dist/tab-collection.js.map +0 -1
  100. package/src/tab-collection.tsx +0 -124
@@ -7,16 +7,32 @@ import { registerClass } from "@gtkx/runtime";
7
7
  import type { CollectionIndex, Level } from "./collection-index.js";
8
8
  import type { TreeExpansion } from "./tree-expansion.js";
9
9
  import { createCollectionIndex } from "./collection-index.js";
10
+ import type { LevelKeys, Relocate, Splice } from "./level-splice.js";
10
11
  import { encodePart } from "./keys.js";
11
- import { adoptIndex, createTreeExpansion, pruneSlots } from "./tree-expansion.js";
12
+ import {
13
+ findSplice,
14
+ hasSplice,
15
+ matchMoves,
16
+ NO_SPLICE,
17
+ relocateEntries,
18
+ relocateSlot,
19
+ searchableSplice,
20
+ } from "./level-splice.js";
21
+ import { adoptIndex, createTreeExpansion, pruneSlots, relocateSlots } from "./tree-expansion.js";
12
22
 
13
23
  type LevelStore = Gio.ListModel & LazyLevelStore;
14
24
 
15
25
  type SlotRef = {
26
+ key: string;
16
27
  store: LazyLevelStore;
17
28
  slot: number;
18
29
  };
19
30
 
31
+ type SlotItem = {
32
+ ref: SlotRef;
33
+ object: Gtk.StringObject;
34
+ };
35
+
20
36
  type SyncContext = {
21
37
  index: CollectionIndex;
22
38
  expansion: TreeExpansion;
@@ -27,7 +43,13 @@ type SlotRun = {
27
43
  length: number;
28
44
  };
29
45
 
46
+ type Serial = {
47
+ value: number;
48
+ };
49
+
30
50
  type ModelState = {
51
+ version: number;
52
+ listeners: Set<() => void>;
31
53
  root: CollectionRootStore | null;
32
54
  model: Gtk.FlattenListModel | null;
33
55
  groupStores: Map<number, LevelStore>;
@@ -38,10 +60,13 @@ type ModelState = {
38
60
 
39
61
  type CollectionModel = {
40
62
  expansion: TreeExpansion;
63
+ subscribe: (onChange: () => void) => () => void;
64
+ getVersion: () => number;
65
+ isPending: (index: CollectionIndex) => boolean;
41
66
  rowAt: (position: number) => Gtk.TreeListRow | null;
42
67
  bind: (root: CollectionRootStore, model: Gtk.FlattenListModel) => void;
43
68
  clear: () => void;
44
- sync: (index: CollectionIndex) => void;
69
+ sync: (index: CollectionIndex, onSectionsFilled: () => void) => void;
45
70
  };
46
71
 
47
72
  type CollectionRootProps = { ref: Ref<CollectionRootStore> };
@@ -50,6 +75,13 @@ const STORE_CLASS_KEY = Symbol.for("gtkx.components.lazy-level-store");
50
75
  const ROOT_CLASS_KEY = Symbol.for("gtkx.components.collection-root-store");
51
76
  const SLOTS_KEY = Symbol.for("gtkx.components.lazy-level-store.slots");
52
77
  const EMPTY_INDEX = createCollectionIndex(undefined, [], true);
78
+ const serial: Serial = { value: 0 };
79
+
80
+ function nextKey(): string {
81
+ serial.value += 1;
82
+
83
+ return String(serial.value);
84
+ }
53
85
 
54
86
  function sharedSlots(): WeakMap<GObject.Object, SlotRef> {
55
87
  const cached: unknown = Reflect.get(globalThis, SLOTS_KEY);
@@ -90,16 +122,20 @@ function registeredRootClass(): unknown {
90
122
  return CollectionRootStore;
91
123
  }
92
124
 
93
- function slotRefFor(value: GObject.Object | null): SlotRef | null {
125
+ function anySlotRefFor(value: GObject.Object | null): SlotRef | null {
94
126
  const item = value instanceof Gtk.TreeListRow ? value.getItem() : value;
95
127
 
96
- if (item === null) {
97
- return null;
98
- }
128
+ return item === null ? null : (sharedSlots().get(item) ?? null);
129
+ }
99
130
 
100
- const ref = sharedSlots().get(item);
131
+ function slotRefFor(value: GObject.Object | null): SlotRef | null {
132
+ const ref = anySlotRefFor(value);
101
133
 
102
- return ref === undefined || ref.slot === -1 ? null : ref;
134
+ return ref === null || ref.slot === -1 ? null : ref;
135
+ }
136
+
137
+ function levelStoreFor(value: GObject.Object | null): LazyLevelStore | null {
138
+ return anySlotRefFor(value)?.store ?? null;
103
139
  }
104
140
 
105
141
  function slotPathAt(store: LazyLevelStore, slot: number): string {
@@ -113,22 +149,68 @@ function newLevelStore(level: Level): LevelStore {
113
149
  return store;
114
150
  }
115
151
 
116
- function collectFlips(previous: Level, next: Level, overlap: number): Set<number> {
152
+ function itemKeys(previous: Level, next: Level): LevelKeys {
153
+ return {
154
+ previousLength: previous.items.length,
155
+ nextLength: next.items.length,
156
+ previousKey: (slot) => previous.items[slot]?.id ?? "",
157
+ nextKey: (slot) => next.items[slot]?.id ?? "",
158
+ };
159
+ }
160
+
161
+ function groupKey(index: CollectionIndex, group: number): string {
162
+ const level = index.groups[group];
163
+
164
+ return level?.section?.id ?? "";
165
+ }
166
+
167
+ function groupKeys(previous: CollectionIndex, next: CollectionIndex): LevelKeys {
168
+ return {
169
+ previousLength: previous.groups.length,
170
+ nextLength: next.groups.length,
171
+ previousKey: (group) => groupKey(previous, group),
172
+ nextKey: (group) => groupKey(next, group),
173
+ };
174
+ }
175
+
176
+ function isExpandableAt(level: Level, slot: number): boolean {
177
+ return level.expandableFlags[slot] ?? false;
178
+ }
179
+
180
+ function collectFlips(previous: Level, next: Level, splice: Splice): Set<number> {
117
181
  const flipped: Set<number> = new Set();
118
182
 
119
183
  if (previous.expandableFlags.length === 0 && next.expandableFlags.length === 0) {
120
184
  return flipped;
121
185
  }
122
186
 
123
- for (let slot = 0; slot < overlap; slot++) {
124
- if ((previous.expandableFlags[slot] ?? false) !== (next.expandableFlags[slot] ?? false)) {
187
+ for (let slot = 0; slot < splice.start; slot++) {
188
+ if (isExpandableAt(previous, slot) !== isExpandableAt(next, slot)) {
125
189
  flipped.add(slot);
126
190
  }
127
191
  }
128
192
 
193
+ const shift = splice.added - splice.removed;
194
+
195
+ for (let slot = splice.start + splice.removed; slot < previous.items.length; slot++) {
196
+ if (isExpandableAt(previous, slot) !== isExpandableAt(next, slot + shift)) {
197
+ flipped.add(slot + shift);
198
+ }
199
+ }
200
+
129
201
  return flipped;
130
202
  }
131
203
 
204
+ function detachStore(store: LevelStore): void {
205
+ for (const { ref } of store.slots.values()) {
206
+ ref.slot = -1;
207
+ }
208
+
209
+ for (const child of store.childStores.values()) {
210
+ detachStore(child);
211
+ }
212
+ }
213
+
132
214
  function pruneFlips(context: SyncContext, store: LevelStore, flipped: Set<number>): void {
133
215
  if (flipped.size === 0) {
134
216
  return;
@@ -137,42 +219,45 @@ function pruneFlips(context: SyncContext, store: LevelStore, flipped: Set<number
137
219
  pruneSlots(context.expansion, store.level.path, (slot) => flipped.has(slot));
138
220
 
139
221
  for (const slot of flipped) {
140
- store.childStores.delete(slot);
141
- }
142
- }
222
+ const child = store.childStores.get(slot);
143
223
 
144
- function detachRefs(store: LevelStore, nextLength: number): void {
145
- for (const [slot, ref] of store.refs) {
146
- if (slot < nextLength) {
147
- continue;
224
+ if (child !== undefined) {
225
+ detachStore(child);
226
+ store.childStores.delete(slot);
148
227
  }
149
-
150
- ref.slot = -1;
151
- store.refs.delete(slot);
152
- store.objects.delete(slot);
153
- store.childStores.delete(slot);
154
228
  }
155
229
  }
156
230
 
157
- function shrinkStore(context: SyncContext, store: LevelStore, previousLength: number, nextLength: number): void {
158
- if (previousLength <= nextLength) {
231
+ function renewKeys(store: LevelStore, previous: Level, from: number, to: number): void {
232
+ if (from === to) {
159
233
  return;
160
234
  }
161
235
 
162
- detachRefs(store, nextLength);
163
- pruneSlots(context.expansion, store.level.path, (slot) => slot >= nextLength);
236
+ for (const [slot, { ref }] of store.slots) {
237
+ if (slot >= from && slot < to && previous.items[slot]?.id !== store.level.items[slot]?.id) {
238
+ ref.key = nextKey();
239
+ }
240
+ }
164
241
  }
165
242
 
166
- function emitTailSplice(store: LevelStore, previousLength: number, nextLength: number): void {
167
- if (nextLength < previousLength) {
168
- store.itemsChanged(nextLength, previousLength - nextLength, 0);
243
+ function relocateStore(context: SyncContext, store: LevelStore, keys: LevelKeys, splice: Splice): void {
244
+ const moves = matchMoves(keys, splice, store.slots.keys());
245
+ const shift: Relocate = (slot) => relocateSlot(splice, slot);
169
246
 
170
- return;
171
- }
247
+ store.slots = relocateEntries(
248
+ store.slots,
249
+ (slot) => relocateSlot(splice, slot, moves),
250
+ ({ ref }) => {
251
+ ref.slot = -1;
252
+ },
253
+ );
172
254
 
173
- if (nextLength > previousLength) {
174
- store.itemsChanged(previousLength, 0, nextLength - previousLength);
255
+ for (const [slot, { ref }] of store.slots) {
256
+ ref.slot = slot;
175
257
  }
258
+
259
+ store.childStores = relocateEntries(store.childStores, shift, detachStore);
260
+ relocateSlots(context.expansion, store.level.path, shift);
176
261
  }
177
262
 
178
263
  function extendRuns(runs: SlotRun[], slot: number): void {
@@ -205,20 +290,26 @@ function emitFlips(store: LevelStore, flipped: Set<number>): void {
205
290
 
206
291
  function syncLevel(context: SyncContext, store: LevelStore, level: Level): void {
207
292
  const previous = store.level;
208
- const previousLength = previous.items.length;
209
- const overlap = Math.min(previousLength, level.items.length);
210
- const flipped = collectFlips(previous, level, overlap);
293
+ const keys = itemKeys(previous, level);
294
+ const found = previous.items === level.items ? NO_SPLICE : findSplice(keys);
295
+ const splice = searchableSplice(found, found.added);
211
296
  store.level = level;
297
+ renewKeys(store, previous, found.start, splice.start);
298
+
299
+ if (hasSplice(splice)) {
300
+ relocateStore(context, store, keys, splice);
301
+ }
302
+
303
+ const flipped = collectFlips(previous, level, splice);
212
304
  pruneFlips(context, store, flipped);
213
- shrinkStore(context, store, previousLength, level.items.length);
214
- emitTailSplice(store, previousLength, level.items.length);
305
+
306
+ if (hasSplice(splice)) {
307
+ store.itemsChanged(splice.start, splice.removed, splice.added);
308
+ }
309
+
215
310
  emitFlips(store, flipped);
216
311
 
217
312
  for (const [slot, child] of store.childStores) {
218
- if (slot >= overlap || flipped.has(slot)) {
219
- continue;
220
- }
221
-
222
313
  const childLevel = context.index.childLevel(level, slot);
223
314
 
224
315
  if (childLevel !== undefined) {
@@ -230,7 +321,7 @@ function syncLevel(context: SyncContext, store: LevelStore, level: Level): void
230
321
  function ensureChildStore(index: CollectionIndex, store: LazyLevelStore, slot: number): LevelStore | null {
231
322
  const existing = store.childStores.get(slot) ?? null;
232
323
 
233
- if (existing !== null || !(store.level.expandableFlags[slot] ?? false)) {
324
+ if (existing !== null || !isExpandableAt(store.level, slot)) {
234
325
  return existing;
235
326
  }
236
327
 
@@ -275,37 +366,51 @@ function rowAt(state: ModelState, position: number): Gtk.TreeListRow | null {
275
366
  return item instanceof Gtk.TreeListRow ? item : null;
276
367
  }
277
368
 
278
- function pruneGroups(state: ModelState, previous: CollectionIndex): void {
279
- const hasModeChanged = previous.isTree !== state.index.isTree;
369
+ function dropGroup(state: ModelState, store: LevelStore): void {
370
+ detachStore(store);
371
+ state.trees.delete(store);
372
+ }
280
373
 
281
- for (const [group, store] of state.groupStores) {
282
- const isRemoved = group >= state.index.groups.length;
374
+ function replaceGroups(state: ModelState, before: number): void {
375
+ for (const store of state.groupStores.values()) {
376
+ dropGroup(state, store);
377
+ }
283
378
 
284
- if (isRemoved || hasModeChanged) {
285
- pruneSlots(state.expansion, store.level.path, () => true);
286
- state.trees.delete(store);
287
- }
379
+ state.groupStores.clear();
380
+ state.expansion.expanded.clear();
381
+ state.expansion.slots.clear();
382
+ state.root?.itemsChanged(0, before, state.index.groups.length);
383
+ }
288
384
 
289
- if (isRemoved) {
290
- state.groupStores.delete(group);
291
- }
292
- }
385
+ function addedRows(index: CollectionIndex, splice: Splice): number {
386
+ return index.groups
387
+ .slice(splice.start, splice.start + splice.added)
388
+ .reduce((rows, level) => rows + level.items.length, 0);
293
389
  }
294
390
 
295
391
  function syncRoot(state: ModelState, previous: CollectionIndex): void {
296
- const before = previous.groups.length;
297
- const after = state.index.groups.length;
298
-
299
392
  if (previous.isTree !== state.index.isTree) {
300
- state.root?.itemsChanged(0, before, after);
393
+ replaceGroups(state, previous.groups.length);
301
394
 
302
395
  return;
303
396
  }
304
397
 
305
- if (before !== after) {
306
- const start = Math.min(before, after);
307
- state.root?.itemsChanged(start, before - start, after - start);
398
+ const keys = groupKeys(previous, state.index);
399
+ const found = findSplice(keys);
400
+ const splice = searchableSplice(found, addedRows(state.index, found));
401
+
402
+ if (!hasSplice(splice)) {
403
+ return;
308
404
  }
405
+
406
+ const moves = matchMoves(keys, splice, state.groupStores.keys());
407
+ const relocate: Relocate = (group) => relocateSlot(splice, group, moves);
408
+ state.groupStores = relocateEntries(state.groupStores, relocate, (store) => {
409
+ dropGroup(state, store);
410
+ });
411
+ relocateSlots(state.expansion, "", relocate);
412
+
413
+ state.root?.itemsChanged(splice.start, splice.removed, splice.added);
309
414
  }
310
415
 
311
416
  function stepGroupStores(state: ModelState, context: SyncContext): void {
@@ -318,7 +423,17 @@ function stepGroupStores(state: ModelState, context: SyncContext): void {
318
423
  }
319
424
  }
320
425
 
321
- function syncModel(state: ModelState, index: CollectionIndex): void {
426
+ function rowCount(index: CollectionIndex): number {
427
+ return index.groups.reduce((rows, level) => rows + level.items.length, 0);
428
+ }
429
+
430
+ function hasFilledSections(previous: CollectionIndex, index: CollectionIndex): boolean {
431
+ return (
432
+ rowCount(previous) === 0 && index.groups.some((level) => level.section !== undefined && level.items.length > 0)
433
+ );
434
+ }
435
+
436
+ function syncModel(state: ModelState, index: CollectionIndex, onSectionsFilled: () => void): void {
322
437
  if (state.root === null) {
323
438
  return;
324
439
  }
@@ -329,14 +444,22 @@ function syncModel(state: ModelState, index: CollectionIndex): void {
329
444
  state.expansion.isSyncing = true;
330
445
 
331
446
  try {
332
- pruneGroups(state, previous);
333
447
  syncRoot(state, previous);
334
448
  stepGroupStores(state, context);
449
+
450
+ if (hasFilledSections(previous, index)) {
451
+ onSectionsFilled();
452
+ }
335
453
  } finally {
336
454
  state.expansion.isSyncing = false;
337
455
  }
338
456
 
339
457
  adoptIndex(state.expansion, index);
458
+ state.version += 1;
459
+
460
+ for (const listener of state.listeners) {
461
+ listener();
462
+ }
340
463
  }
341
464
 
342
465
  function clearModel(state: ModelState): void {
@@ -368,6 +491,8 @@ function clearModel(state: ModelState): void {
368
491
 
369
492
  function createCollectionModel(): CollectionModel {
370
493
  const state: ModelState = {
494
+ version: 0,
495
+ listeners: new Set(),
371
496
  root: null,
372
497
  model: null,
373
498
  groupStores: new Map(),
@@ -378,6 +503,15 @@ function createCollectionModel(): CollectionModel {
378
503
 
379
504
  return {
380
505
  expansion: state.expansion,
506
+ subscribe: (onChange) => {
507
+ state.listeners.add(onChange);
508
+
509
+ return () => {
510
+ state.listeners.delete(onChange);
511
+ };
512
+ },
513
+ getVersion: () => state.version,
514
+ isPending: (index) => state.root !== null && state.index !== index,
381
515
  rowAt: (position) => rowAt(state, position),
382
516
  bind: (root, model) => {
383
517
  root.state = state;
@@ -387,8 +521,8 @@ function createCollectionModel(): CollectionModel {
387
521
  clear: () => {
388
522
  clearModel(state);
389
523
  },
390
- sync: (index) => {
391
- syncModel(state, index);
524
+ sync: (index, onSectionsFilled) => {
525
+ syncModel(state, index, onSectionsFilled);
392
526
  },
393
527
  };
394
528
  }
@@ -426,19 +560,18 @@ class CollectionRootStore extends GObject.Object implements Gio.ListModelImpl {
426
560
 
427
561
  class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
428
562
  declare itemsChanged: Gio.ListModel["itemsChanged"];
563
+ key: string = nextKey();
429
564
  level: Level = { path: "", items: [], expandableFlags: [] };
430
- refs: Map<number, SlotRef> = new Map();
431
- objects: Map<number, Gtk.StringObject> = new Map();
565
+ slots: Map<number, SlotItem> = new Map();
432
566
  childStores: Map<number, LevelStore> = new Map();
433
567
 
434
568
  private createItem(position: number): Gtk.StringObject {
435
- const ref = { store: this, slot: position };
436
- const created = Gtk.StringObject.new("");
437
- this.refs.set(position, ref);
438
- this.objects.set(position, created);
439
- sharedSlots().set(created, ref);
569
+ const ref = { key: nextKey(), store: this, slot: position };
570
+ const object = Gtk.StringObject.new("");
571
+ this.slots.set(position, { ref, object });
572
+ sharedSlots().set(object, ref);
440
573
 
441
- return created;
574
+ return object;
442
575
  }
443
576
 
444
577
  vfuncGetItemType(): bigint {
@@ -454,7 +587,7 @@ class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
454
587
  return null;
455
588
  }
456
589
 
457
- return this.objects.get(position) ?? this.createItem(position);
590
+ return this.slots.get(position)?.object ?? this.createItem(position);
458
591
  }
459
592
  }
460
593
 
@@ -466,9 +599,11 @@ const CollectionRoot: (props: CollectionRootProps) => ReactNode = createElementC
466
599
  export {
467
600
  CollectionRoot,
468
601
  createCollectionModel,
602
+ levelStoreFor,
469
603
  slotPathAt,
470
604
  slotRefFor,
471
605
  type CollectionModel,
472
606
  type CollectionRootStore,
607
+ type LazyLevelStore,
473
608
  type SlotRef,
474
609
  };
@@ -1,18 +1,19 @@
1
1
  import type * as Gtk from "@gtkx/gi/gtk";
2
2
  import type { ListItem } from "../types.js";
3
- import type { CollectionIndex, SectionIdentity } from "./collection-index.js";
3
+ import type { CollectionIndex } from "./collection-index.js";
4
4
  import type { CollectionModel, SlotRef } from "./collection-model.js";
5
5
  import type { MatchedRows } from "./tree-order.js";
6
6
  import { slotPathAt, slotRefFor } from "./collection-model.js";
7
+ import { orderFor } from "./tree-expansion.js";
7
8
  import { findIds, findRows } from "./tree-order.js";
8
9
 
9
- type Collection = Pick<CollectionModel, "expansion" | "rowAt"> & {
10
+ type Collection = Pick<CollectionModel, "expansion" | "rowAt" | "subscribe" | "getVersion"> & {
10
11
  isTree: boolean;
11
12
  itemAt: (ref: SlotRef) => ListItem | undefined;
12
- sectionFor: (levelPath: string) => SectionIdentity | undefined;
13
13
  idAt: (position: number) => string | null;
14
14
  idsAt: (positions: number[]) => string[];
15
15
  pathAt: (position: number) => string | null;
16
+ isPending: () => boolean;
16
17
  positionFor: (id: string) => number;
17
18
  rowsFor: (ids: string[]) => MatchedRows;
18
19
  };
@@ -31,9 +32,7 @@ function rowsFor(gtkModel: CollectionModel, ids: string[]): MatchedRows {
31
32
  }
32
33
 
33
34
  function positionFor(gtkModel: CollectionModel, id: string): number {
34
- const [first = -1] = rowsFor(gtkModel, [id]).positions;
35
-
36
- return first;
35
+ return orderFor(gtkModel.expansion).positions.get(id) ?? -1;
37
36
  }
38
37
 
39
38
  function refAt(model: Gtk.FlattenListModel | null, position: number): SlotRef | null {
@@ -44,14 +43,14 @@ function refAt(model: Gtk.FlattenListModel | null, position: number): SlotRef |
44
43
  return slotRefFor(model?.getItem(position) ?? null);
45
44
  }
46
45
 
47
- function itemAt(index: CollectionIndex, ref: SlotRef): ListItem | undefined {
48
- return index.itemAt(ref.store.level.path, ref.slot);
46
+ function itemAt(ref: SlotRef): ListItem | undefined {
47
+ return ref.store.level.items[ref.slot];
49
48
  }
50
49
 
51
- function idAt(model: Gtk.FlattenListModel | null, index: CollectionIndex, position: number): string | null {
50
+ function idAt(model: Gtk.FlattenListModel | null, position: number): string | null {
52
51
  const ref = refAt(model, position);
53
52
 
54
- return ref === null ? null : (itemAt(index, ref)?.id ?? null);
53
+ return ref === null ? null : (itemAt(ref)?.id ?? null);
55
54
  }
56
55
 
57
56
  function pathAt(model: Gtk.FlattenListModel | null, position: number): string | null {
@@ -89,11 +88,13 @@ function createCollection(
89
88
  expansion: gtkModel.expansion,
90
89
  isTree: index.isTree,
91
90
  rowAt: gtkModel.rowAt,
92
- itemAt: (ref) => itemAt(index, ref),
93
- sectionFor: index.sectionFor,
94
- idAt: (position) => idAt(model, index, position),
91
+ subscribe: gtkModel.subscribe,
92
+ getVersion: gtkModel.getVersion,
93
+ itemAt,
94
+ idAt: (position) => idAt(model, position),
95
95
  idsAt: (positions) => idsAt(gtkModel, positions),
96
96
  pathAt: (position) => pathAt(model, position),
97
+ isPending: () => gtkModel.isPending(index),
97
98
  positionFor: (id) => positionFor(gtkModel, id),
98
99
  rowsFor: (ids) => rowsFor(gtkModel, ids),
99
100
  };
@@ -8,6 +8,13 @@ type ControlledSyncOptions<T> = {
8
8
  apply: (value: T) => void;
9
9
  };
10
10
 
11
+ function hasSameIds(reported: string[], requested: string[]): boolean {
12
+ const wanted = new Set(requested);
13
+ const actual = new Set(reported);
14
+
15
+ return actual.size === wanted.size && actual.isSubsetOf(wanted);
16
+ }
17
+
11
18
  function useControlledSync<T>(options: ControlledSyncOptions<T>): () => void {
12
19
  const { value, source, target } = options;
13
20
  const [drift, setDrift] = useState(0);
@@ -27,4 +34,4 @@ function useControlledSync<T>(options: ControlledSyncOptions<T>): () => void {
27
34
  return markDrift;
28
35
  }
29
36
 
30
- export { useControlledSync };
37
+ export { hasSameIds, useControlledSync };