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

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 +8 -168
  2. package/dist/column-view.d.ts.map +1 -1
  3. package/dist/column-view.js +20 -10
  4. package/dist/column-view.js.map +1 -1
  5. package/dist/grid-view.js +1 -1
  6. package/dist/index.d.ts +4 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal/cells.d.ts +10 -7
  11. package/dist/internal/cells.d.ts.map +1 -1
  12. package/dist/internal/cells.js +88 -51
  13. package/dist/internal/cells.js.map +1 -1
  14. package/dist/internal/collection-index.d.ts +3 -2
  15. package/dist/internal/collection-index.d.ts.map +1 -1
  16. package/dist/internal/collection-index.js +3 -3
  17. package/dist/internal/collection-index.js.map +1 -1
  18. package/dist/internal/collection-model.d.ts +23 -2
  19. package/dist/internal/collection-model.d.ts.map +1 -1
  20. package/dist/internal/collection-model.js +118 -98
  21. package/dist/internal/collection-model.js.map +1 -1
  22. package/dist/internal/collection.d.ts +5 -4
  23. package/dist/internal/collection.d.ts.map +1 -1
  24. package/dist/internal/collection.js +20 -21
  25. package/dist/internal/collection.js.map +1 -1
  26. package/dist/internal/controlled-sync.d.ts +6 -8
  27. package/dist/internal/controlled-sync.d.ts.map +1 -1
  28. package/dist/internal/controlled-sync.js +3 -3
  29. package/dist/internal/controlled-sync.js.map +1 -1
  30. package/dist/internal/drop-down.d.ts.map +1 -1
  31. package/dist/internal/drop-down.js +44 -45
  32. package/dist/internal/drop-down.js.map +1 -1
  33. package/dist/internal/expansion.js +4 -4
  34. package/dist/internal/expansion.js.map +1 -1
  35. package/dist/internal/keys.d.ts.map +1 -1
  36. package/dist/internal/selection.d.ts +1 -0
  37. package/dist/internal/selection.d.ts.map +1 -1
  38. package/dist/internal/selection.js +6 -6
  39. package/dist/internal/selection.js.map +1 -1
  40. package/dist/internal/tree-expansion.d.ts +1 -1
  41. package/dist/internal/tree-expansion.d.ts.map +1 -1
  42. package/dist/internal/tree-expansion.js +3 -12
  43. package/dist/internal/tree-expansion.js.map +1 -1
  44. package/dist/internal/tree-order.d.ts +1 -1
  45. package/dist/internal/tree-order.d.ts.map +1 -1
  46. package/dist/internal/tree-order.js +33 -77
  47. package/dist/internal/tree-order.js.map +1 -1
  48. package/dist/internal/use-collection.d.ts +5 -1
  49. package/dist/internal/use-collection.d.ts.map +1 -1
  50. package/dist/internal/use-collection.js +21 -7
  51. package/dist/internal/use-collection.js.map +1 -1
  52. package/dist/internal/use-widget-ref.d.ts.map +1 -1
  53. package/dist/list-item-factory.d.ts +10 -0
  54. package/dist/list-item-factory.d.ts.map +1 -0
  55. package/dist/list-item-factory.js +14 -0
  56. package/dist/list-item-factory.js.map +1 -0
  57. package/dist/list-view.js +1 -1
  58. package/dist/tab-collection.d.ts +9 -0
  59. package/dist/tab-collection.d.ts.map +1 -0
  60. package/dist/tab-collection.js +114 -0
  61. package/dist/tab-collection.js.map +1 -0
  62. package/dist/toast.d.ts +3 -5
  63. package/dist/toast.d.ts.map +1 -1
  64. package/dist/toast.js +3 -12
  65. package/dist/toast.js.map +1 -1
  66. package/dist/types.d.ts +67 -51
  67. package/dist/types.d.ts.map +1 -1
  68. package/dist/types.js.map +1 -1
  69. package/package.json +13 -21
  70. package/src/column-view.tsx +23 -12
  71. package/src/index.ts +7 -2
  72. package/src/internal/cells.tsx +155 -103
  73. package/src/internal/collection-index.ts +8 -6
  74. package/src/internal/collection-model.ts +156 -134
  75. package/src/internal/collection.ts +28 -24
  76. package/src/internal/controlled-sync.ts +9 -12
  77. package/src/internal/drop-down.tsx +63 -63
  78. package/src/internal/expansion.ts +4 -4
  79. package/src/internal/selection.tsx +8 -8
  80. package/src/internal/tree-expansion.ts +3 -23
  81. package/src/internal/tree-order.ts +34 -130
  82. package/src/internal/use-collection.tsx +29 -8
  83. package/src/list-item-factory.tsx +27 -0
  84. package/src/tab-collection.tsx +124 -0
  85. package/src/toast.tsx +4 -18
  86. package/src/types.ts +81 -55
@@ -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";
@@ -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,7 +97,7 @@ 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
  }
@@ -194,44 +203,7 @@ function emitFlips(store: LevelStore, flipped: Set<number>): void {
194
203
  }
195
204
  }
196
205
 
197
- function childSync(
198
- context: SyncContext,
199
- store: LevelStore,
200
- slot: number,
201
- flipped: Set<number>,
202
- ): LevelSync | undefined {
203
- const child = store.childStores.get(slot) ?? null;
204
-
205
- if (child === null || flipped.has(slot)) {
206
- return undefined;
207
- }
208
-
209
- const level = context.index.childLevel(store.level, slot);
210
-
211
- return level === undefined ? undefined : { store: child, level };
212
- }
213
-
214
- function childSyncs(context: SyncContext, store: LevelStore, overlap: number, flipped: Set<number>): LevelSync[] {
215
- const syncs: LevelSync[] = [];
216
- const slots = store.childStores.keys().toArray().toSorted((left, right) => left - right);
217
-
218
- for (const slot of slots) {
219
- if (slot >= overlap) {
220
- continue;
221
- }
222
-
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;
206
+ function syncLevel(context: SyncContext, store: LevelStore, level: Level): void {
235
207
  const previous = store.level;
236
208
  const previousLength = previous.items.length;
237
209
  const overlap = Math.min(previousLength, level.items.length);
@@ -242,17 +214,15 @@ function syncEntry(context: SyncContext, entry: LevelSync): LevelSync[] {
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
  }
@@ -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()]);
266
+ const created = Gtk.TreeListModel.new(store, false, false, (object) => childStoreFor(state, object));
267
+ state.trees.set(store, created);
305
268
 
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
- }
312
-
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,31 +339,91 @@ 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;
396
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
+ }
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: [] };
@@ -427,7 +436,7 @@ class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
427
436
  const created = Gtk.StringObject.new("");
428
437
  this.refs.set(position, ref);
429
438
  this.objects.set(position, created);
430
- SLOTS.set(created, ref);
439
+ sharedSlots().set(created, ref);
431
440
 
432
441
  return created;
433
442
  }
@@ -449,4 +458,17 @@ class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
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
 
@@ -1,18 +1,15 @@
1
1
  import { useLatestRef } from "@gtkx/react/internal";
2
2
  import { startTransition, useLayoutEffect, useState } from "react";
3
- import type { Collection } from "./collection.js";
4
3
 
5
- type ControlledIds = string[] | null | undefined;
6
-
7
- type ControlledSyncOptions = {
8
- ids: ControlledIds;
9
- collection: Collection;
10
- widget?: object | null | undefined;
11
- apply: (ids: ControlledIds) => void;
4
+ type ControlledSyncOptions<T> = {
5
+ value: T;
6
+ source?: object | null | undefined;
7
+ target?: object | null | undefined;
8
+ apply: (value: T) => void;
12
9
  };
13
10
 
14
- function useControlledSync(options: ControlledSyncOptions): () => void {
15
- const { ids, collection, widget } = options;
11
+ function useControlledSync<T>(options: ControlledSyncOptions<T>): () => void {
12
+ const { value, source, target } = options;
16
13
  const [drift, setDrift] = useState(0);
17
14
 
18
15
  const markDrift = (): void => {
@@ -24,8 +21,8 @@ function useControlledSync(options: ControlledSyncOptions): () => void {
24
21
  const applyRef = useLatestRef(options.apply);
25
22
 
26
23
  useLayoutEffect(() => {
27
- applyRef.current(ids);
28
- }, [applyRef, widget, collection, ids, drift]);
24
+ applyRef.current(value);
25
+ }, [applyRef, target, source, value, drift]);
29
26
 
30
27
  return markDrift;
31
28
  }