@gtkx/components 1.0.0-rc.4 → 1.0.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 +6 -10
  16. package/dist/internal/collection-index.d.ts.map +1 -1
  17. package/dist/internal/collection-index.js +32 -69
  18. package/dist/internal/collection-index.js.map +1 -1
  19. package/dist/internal/collection-model.d.ts +23 -2
  20. package/dist/internal/collection-model.d.ts.map +1 -1
  21. package/dist/internal/collection-model.js +210 -95
  22. package/dist/internal/collection-model.js.map +1 -1
  23. package/dist/internal/collection.d.ts +8 -11
  24. package/dist/internal/collection.d.ts.map +1 -1
  25. package/dist/internal/collection.js +38 -49
  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 +80 -47
  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 +20 -0
  51. package/dist/internal/tree-expansion.d.ts.map +1 -0
  52. package/dist/internal/tree-expansion.js +68 -0
  53. package/dist/internal/tree-expansion.js.map +1 -0
  54. package/dist/internal/tree-order.d.ts +27 -0
  55. package/dist/internal/tree-order.d.ts.map +1 -0
  56. package/dist/internal/tree-order.js +71 -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 +70 -21
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js.map +1 -1
  68. package/package.json +7 -7
  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 +44 -92
  74. package/src/internal/collection-model.ts +259 -107
  75. package/src/internal/collection.ts +45 -67
  76. package/src/internal/controlled-sync.ts +34 -0
  77. package/src/internal/drop-down.tsx +41 -20
  78. package/src/internal/expansion.ts +104 -67
  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 +110 -0
  83. package/src/internal/tree-order.ts +138 -0
  84. package/src/internal/use-collection.tsx +5 -5
  85. package/src/list-view.tsx +14 -10
  86. package/src/types.ts +76 -19
@@ -1,178 +1,283 @@
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 } from "./collection-index.js";
8
+ import { encodePart } from "./keys.js";
9
+ import { adoptIndex, createTreeExpansion, pruneSlots, resetExpansion } 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;
17
26
  };
18
27
 
19
28
  type ModelState = {
20
29
  root: Gio.ListStore;
21
30
  rootModels: GObject.Object[];
22
31
  model: Gtk.FlattenListModel;
23
- objects: Map<string, Gtk.StringObject>;
24
- levels: Map<string, LevelState>;
32
+ groupStores: LevelStore[];
25
33
  tree: Gtk.TreeListModel | null;
34
+ treeRoot: LevelStore | null;
35
+ expansion: TreeExpansion;
26
36
  };
27
37
 
28
38
  type CollectionModel = {
29
39
  model: Gtk.FlattenListModel;
40
+ expansion: TreeExpansion;
30
41
  treeModel: () => Gtk.TreeListModel | null;
31
42
  sync: (index: CollectionIndex) => void;
32
43
  };
33
44
 
34
- const OBJECT_CACHE_MAX = 1 << 20;
45
+ const STORE_CLASS_KEY = Symbol.for("gtkx.components.lazy-level-store");
46
+ const SLOTS_KEY = Symbol.for("gtkx.components.lazy-level-store.slots");
47
+ const EMPTY_INDEX = createCollectionIndex(undefined, undefined, true);
48
+ const SLOTS = sharedSlots();
35
49
 
36
- const newStore = (): Gio.ListStore => new Gio.ListStore({ itemType: GObject.TYPE_OBJECT });
50
+ const newRootStore = (): Gio.ListStore => new Gio.ListStore({ itemType: GObject.TYPE_OBJECT });
37
51
 
38
- function getId(value: GObject.Object | null): string | null {
39
- const item = value instanceof Gtk.TreeListRow ? value.getItem() : value;
52
+ function sharedSlots(): WeakMap<GObject.Object, SlotRef> {
53
+ const cached: unknown = Reflect.get(globalThis, SLOTS_KEY);
54
+
55
+ if (cached instanceof WeakMap) {
56
+ return cached as WeakMap<GObject.Object, SlotRef>;
57
+ }
58
+
59
+ const created: WeakMap<GObject.Object, SlotRef> = new WeakMap();
60
+ Reflect.set(globalThis, SLOTS_KEY, created);
40
61
 
41
- return item instanceof Gtk.StringObject ? item.getString() : null;
62
+ return created;
42
63
  }
43
64
 
44
- function objectFor(state: ModelState, id: string): Gtk.StringObject {
45
- const existing = state.objects.get(id);
65
+ function registeredStoreClass(): typeof LazyLevelStore {
66
+ const cached: unknown = Reflect.get(globalThis, STORE_CLASS_KEY);
46
67
 
47
- if (existing !== undefined) {
48
- return existing;
68
+ if (typeof cached === "function") {
69
+ return cached as typeof LazyLevelStore;
49
70
  }
50
71
 
51
- const created = Gtk.StringObject.new(id);
52
- state.objects.set(id, created);
72
+ registerClass(LazyLevelStore, { typeName: "GtkxLazyLevelStore", implements: [Gio.ListModel] });
73
+ Reflect.set(globalThis, STORE_CLASS_KEY, LazyLevelStore);
53
74
 
54
- return created;
75
+ return LazyLevelStore;
55
76
  }
56
77
 
57
- function commonPrefix(previous: string[], next: string[], max: number): number {
58
- let count = 0;
78
+ function slotRefFor(value: GObject.Object | null): SlotRef | null {
79
+ const item = value instanceof Gtk.TreeListRow ? value.getItem() : value;
59
80
 
60
- while (count < max && previous[count] === next[count]) {
61
- count += 1;
81
+ if (item === null) {
82
+ return null;
62
83
  }
63
84
 
64
- return count;
85
+ const ref = SLOTS.get(item);
86
+
87
+ return ref === undefined || ref.slot === -1 ? null : ref;
65
88
  }
66
89
 
67
- function commonSuffix(previous: string[], next: string[], max: number): number {
68
- let count = 0;
90
+ function slotPathAt(store: LevelStore, slot: number): string {
91
+ return store.path + encodePart(String(slot));
92
+ }
69
93
 
70
- while (count < max && previous[previous.length - 1 - count] === next[next.length - 1 - count]) {
71
- count += 1;
72
- }
94
+ function newLevelStore(path: string): LevelStore {
95
+ const store = new (registeredStoreClass())() as LevelStore;
96
+ store.path = path;
73
97
 
74
- return count;
98
+ return store;
75
99
  }
76
100
 
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);
101
+ function buildChildStore(context: SyncContext, path: string): LevelStore | null {
102
+ const level = context.index.children.get(path);
80
103
 
81
- if (start === previous.length && start === next.length) {
104
+ if (level === undefined) {
82
105
  return null;
83
106
  }
84
107
 
85
- const tail = commonSuffix(previous, next, shared - start);
108
+ const store = newLevelStore(path);
109
+ growStore(context, store, level);
86
110
 
87
- return { start, removed: previous.length - start - tail, added: next.slice(start, next.length - tail) };
111
+ return store;
88
112
  }
89
113
 
90
- function levelFor(state: ModelState, key: string): LevelState {
91
- const existing = state.levels.get(key);
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
+ }
92
121
 
93
- if (existing !== undefined) {
94
- return existing;
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);
95
125
  }
126
+ }
96
127
 
97
- const created: LevelState = { store: newStore(), ids: [], canExpand: new Map() };
98
- state.levels.set(key, created);
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;
132
+ }
99
133
 
100
- return created;
134
+ function collectFlips(store: LevelStore, level: Level): Set<number> {
135
+ const overlap = Math.min(store.refs.length, level.items.length);
136
+ const flipped: Set<number> = new Set();
137
+
138
+ for (let slot = 0; slot < overlap; slot++) {
139
+ if ((store.expandableFlags[slot] ?? false) !== (level.expandableFlags[slot] ?? false)) {
140
+ flipped.add(slot);
141
+ }
142
+ }
143
+
144
+ return flipped;
101
145
  }
102
146
 
103
- function refreshExpandable(current: LevelState, level: Level): void {
104
- const next: Map<string, boolean> = new Map();
147
+ function syncOverlap(context: SyncContext, store: LevelStore, level: Level): Set<number> {
148
+ const flipped = collectFlips(store, level);
149
+
150
+ if (flipped.size === 0) {
151
+ return flipped;
152
+ }
153
+
154
+ pruneSlots(context.expansion, store.path, (slot) => flipped.has(slot));
105
155
 
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);
156
+ for (const slot of flipped) {
157
+ flipSlot(context, store, slot);
158
+ }
159
+
160
+ return flipped;
161
+ }
110
162
 
111
- if (previous !== undefined && previous !== isWanted) {
112
- current.store.itemsChanged(index, 1, 1);
163
+ function detachRefs(store: LevelStore, nextLength: number): void {
164
+ for (let slot = nextLength; slot < store.refs.length; slot++) {
165
+ const ref = store.refs[slot];
166
+
167
+ if (ref !== undefined) {
168
+ ref.slot = -1;
113
169
  }
114
170
  }
171
+ }
115
172
 
116
- current.canExpand = next;
173
+ function shrinkStore(context: SyncContext, store: LevelStore, nextLength: number): void {
174
+ if (store.refs.length <= nextLength) {
175
+ return;
176
+ }
177
+
178
+ detachRefs(store, nextLength);
179
+ pruneSlots(context.expansion, store.path, (slot) => slot >= nextLength);
180
+ store.refs.length = nextLength;
181
+ store.objects.length = nextLength;
182
+ store.childStores.length = nextLength;
183
+ store.expandableFlags.length = nextLength;
117
184
  }
118
185
 
119
- function didSpliceLevel(state: ModelState, level: Level): boolean {
120
- const current = levelFor(state, level.key);
121
- const range = spliceRange(current.ids, level.ids);
186
+ function emitTailSplice(store: LevelStore, previousLength: number, nextLength: number): void {
187
+ if (nextLength < previousLength) {
188
+ store.itemsChanged(nextLength, previousLength - nextLength, 0);
122
189
 
123
- if (range === null) {
124
- return false;
190
+ return;
125
191
  }
126
192
 
127
- current.store.splice(range.start, range.removed, range.added.map((id) => objectFor(state, id)));
128
- current.ids = [...level.ids];
193
+ if (nextLength > previousLength) {
194
+ store.itemsChanged(previousLength, 0, nextLength - previousLength);
195
+ }
196
+ }
197
+
198
+ function extendRuns(runs: SlotRun[], slot: number): void {
199
+ const last = runs.at(-1);
129
200
 
130
- return true;
201
+ if (last !== undefined && last.start + last.length === slot) {
202
+ last.length += 1;
203
+
204
+ return;
205
+ }
206
+
207
+ runs.push({ start: slot, length: 1 });
131
208
  }
132
209
 
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);
137
- }
210
+ function collectFlipRuns(flipped: Set<number>): SlotRun[] {
211
+ const runs: SlotRun[] = [];
212
+
213
+ for (const slot of flipped) {
214
+ extendRuns(runs, slot);
215
+ }
216
+
217
+ return runs;
218
+ }
219
+
220
+ function emitFlips(store: LevelStore, flipped: Set<number>): void {
221
+ for (const run of collectFlipRuns(flipped)) {
222
+ store.itemsChanged(run.start, run.length, run.length);
138
223
  }
139
224
  }
140
225
 
141
- function pruneObjects(state: ModelState, index: CollectionIndex): void {
142
- if (state.objects.size <= OBJECT_CACHE_MAX) {
226
+ function stepChildStore(context: SyncContext, store: LevelStore, slot: number, flipped: Set<number>): void {
227
+ const child = store.childStores[slot];
228
+
229
+ if (child == null || flipped.has(slot)) {
143
230
  return;
144
231
  }
145
232
 
146
- for (const id of state.objects.keys()) {
147
- if (!index.hasId(id)) {
148
- state.objects.delete(id);
149
- }
233
+ const level = context.index.children.get(slotPathAt(store, slot));
234
+
235
+ if (level !== undefined) {
236
+ syncLevel(context, child, level);
150
237
  }
151
238
  }
152
239
 
153
- function childStoreFor(state: ModelState, object: GObject.Object): Gio.ListStore | null {
154
- const id = getId(object);
240
+ function syncLevel(context: SyncContext, store: LevelStore, level: Level): void {
241
+ const previousLength = store.refs.length;
242
+ const overlap = Math.min(previousLength, level.items.length);
243
+ const flipped = syncOverlap(context, store, level);
244
+ shrinkStore(context, store, level.items.length);
245
+ growStore(context, store, level);
246
+ emitTailSplice(store, previousLength, level.items.length);
247
+ emitFlips(store, flipped);
248
+
249
+ for (let slot = 0; slot < overlap; slot++) {
250
+ stepChildStore(context, store, slot, flipped);
251
+ }
252
+ }
155
253
 
156
- if (id === null) {
254
+ function childStoreFor(object: GObject.Object): Gio.ListModel | null {
255
+ const ref = slotRefFor(object);
256
+
257
+ if (ref === null) {
157
258
  return null;
158
259
  }
159
260
 
160
- return state.levels.get(childLevelKey(id))?.store ?? null;
261
+ return ref.store.childStores[ref.slot] ?? null;
161
262
  }
162
263
 
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));
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;
268
+ }
166
269
 
167
270
  return state.tree;
168
271
  }
169
272
 
170
273
  function desiredRootModels(state: ModelState, index: CollectionIndex): GObject.Object[] {
171
- if (index.isTree) {
172
- return [ensureTree(state)];
274
+ const [first] = state.groupStores;
275
+
276
+ if (first !== undefined && index.isTree) {
277
+ return [ensureTree(state, first)];
173
278
  }
174
279
 
175
- return index.groups.map((level) => levelFor(state, level.key).store);
280
+ return [...state.groupStores];
176
281
  }
177
282
 
178
283
  function hasSameModels(previous: GObject.Object[], next: GObject.Object[]): boolean {
@@ -188,51 +293,63 @@ function syncRoot(state: ModelState, index: CollectionIndex): void {
188
293
 
189
294
  state.root.splice(0, state.rootModels.length, next);
190
295
  state.rootModels = next;
296
+ resetExpansion(state.expansion);
191
297
  }
192
298
 
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;
299
+ function adjustGroupStores(state: ModelState, context: SyncContext): void {
300
+ const { groups } = context.index;
301
+ state.groupStores.length = Math.min(state.groupStores.length, groups.length);
197
302
 
198
- for (const level of levels) {
199
- didChange = didSpliceLevel(state, level) || didChange;
200
- visited.add(level.key);
303
+ 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);
201
307
  }
308
+ }
202
309
 
203
- if (didChange || visited.size !== state.levels.size) {
204
- dropUnvisited(state, visited);
205
- pruneObjects(state, index);
206
- }
310
+ function stepGroupStores(state: ModelState, context: SyncContext, surviving: number): void {
311
+ for (const [group, level] of context.index.groups.entries()) {
312
+ const store = state.groupStores[group];
207
313
 
208
- syncRoot(state, index);
209
- refreshExpandableLevels(state, index, levels);
314
+ if (store !== undefined && group < surviving) {
315
+ syncLevel(context, store, level);
316
+ }
317
+ }
210
318
  }
211
319
 
212
- function refreshExpandableLevels(state: ModelState, index: CollectionIndex, levels: Level[]): void {
213
- if (!index.isTree) {
214
- return;
320
+ function syncModel(state: ModelState, index: CollectionIndex): void {
321
+ const context: SyncContext = { index, expansion: state.expansion };
322
+ const surviving = Math.min(state.groupStores.length, index.groups.length);
323
+ state.expansion.isSyncing = true;
324
+
325
+ try {
326
+ adjustGroupStores(state, context);
327
+ syncRoot(state, index);
328
+ stepGroupStores(state, context, surviving);
329
+ } finally {
330
+ state.expansion.isSyncing = false;
215
331
  }
216
332
 
217
- for (const level of levels) {
218
- refreshExpandable(levelFor(state, level.key), level);
219
- }
333
+ adoptIndex(state.expansion, index);
220
334
  }
221
335
 
222
336
  function createCollectionModel(): CollectionModel {
223
- const root = newStore();
337
+ registeredStoreClass();
338
+ const root = newRootStore();
224
339
 
225
340
  const state: ModelState = {
226
341
  root,
227
342
  rootModels: [],
228
343
  model: Gtk.FlattenListModel.new(root),
229
- objects: new Map(),
230
- levels: new Map(),
344
+ groupStores: [],
231
345
  tree: null,
346
+ treeRoot: null,
347
+ expansion: createTreeExpansion(EMPTY_INDEX),
232
348
  };
233
349
 
234
350
  return {
235
351
  model: state.model,
352
+ expansion: state.expansion,
236
353
  treeModel: () => state.tree,
237
354
  sync: (index) => {
238
355
  syncModel(state, index);
@@ -240,4 +357,39 @@ function createCollectionModel(): CollectionModel {
240
357
  };
241
358
  }
242
359
 
243
- export { createCollectionModel, getId, type CollectionModel };
360
+ class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
361
+ declare itemsChanged: Gio.ListModel["itemsChanged"];
362
+ path = "";
363
+ refs: SlotRef[] = [];
364
+ objects: (Gtk.StringObject | null)[] = [];
365
+ childStores: (LevelStore | null)[] = [];
366
+ expandableFlags: boolean[] = [];
367
+
368
+ private createItem(position: number, ref: SlotRef): Gtk.StringObject {
369
+ const created = Gtk.StringObject.new("");
370
+ this.objects[position] = created;
371
+ SLOTS.set(created, ref);
372
+
373
+ return created;
374
+ }
375
+
376
+ vfuncGetItemType(): bigint {
377
+ return GObject.TYPE_OBJECT;
378
+ }
379
+
380
+ vfuncGetNItems(): number {
381
+ return this.refs.length;
382
+ }
383
+
384
+ vfuncGetItem(position: number): GObject.Object | null {
385
+ const ref = this.refs[position];
386
+
387
+ if (ref === undefined) {
388
+ return null;
389
+ }
390
+
391
+ return this.objects[position] ?? this.createItem(position, ref);
392
+ }
393
+ }
394
+
395
+ export { createCollectionModel, slotPathAt, slotRefFor, type CollectionModel, type SlotRef };
@@ -1,104 +1,82 @@
1
- import type * as GObject from "@gtkx/gi/gobject";
2
- import type * as Gtk from "@gtkx/gi/gtk";
3
1
  import type { ListItem } from "../types.js";
4
2
  import type { CollectionIndex } from "./collection-index.js";
5
- import type { CollectionModel } from "./collection-model.js";
6
- import { getId } from "./collection-model.js";
7
-
8
- type Collection = {
9
- isTree: boolean;
10
- model: Gtk.FlattenListModel;
11
- treeModel: () => Gtk.TreeListModel | null;
12
- itemFor: (id: string) => ListItem | undefined;
13
- sectionFor: (id: string) => unknown;
3
+ import type { CollectionModel, SlotRef } from "./collection-model.js";
4
+ import { slotPathAt, slotRefFor } from "./collection-model.js";
5
+ import { findPositions } from "./tree-order.js";
6
+
7
+ type Collection = Pick<CollectionModel, "model" | "expansion" | "treeModel"> & {
8
+ itemAt: (ref: SlotRef) => ListItem | undefined;
9
+ sectionFor: (levelPath: string) => unknown;
14
10
  idAt: (position: number) => string | null;
11
+ pathAt: (position: number) => string | null;
15
12
  positionFor: (id: string) => number;
16
13
  positionsFor: (ids: string[]) => number[];
17
14
  };
18
15
 
19
- const ascending = (a: number, b: number): number => a - b;
20
-
21
- function visitRow(
22
- tree: Gtk.TreeListModel,
23
- position: number,
24
- visit: (row: Gtk.TreeListRow, id: string | null) => void,
25
- ): void {
26
- const row = tree.getRow(position);
16
+ const NO_POSITIONS: number[] = [];
27
17
 
28
- if (row === null) {
29
- return;
18
+ function positionsFor(collectionModel: CollectionModel, ids: string[]): number[] {
19
+ if (ids.length === 0) {
20
+ return NO_POSITIONS;
30
21
  }
31
22
 
32
- visit(row, getId(row.getItem()));
33
- }
23
+ const { expansion } = collectionModel;
34
24
 
35
- function eachRow(tree: Gtk.TreeListModel, visit: (row: Gtk.TreeListRow, id: string | null) => void): void {
36
- for (let position = 0; position < tree.getNItems(); position++) {
37
- visitRow(tree, position, visit);
38
- }
25
+ return findPositions(expansion.index, expansion.slots, new Set(ids));
39
26
  }
40
27
 
41
- function collectWanted(item: GObject.Object | null, position: number, wanted: Set<string>, out: number[]): void {
42
- const id = getId(item);
28
+ function positionFor(collectionModel: CollectionModel, id: string): number {
29
+ const [first = -1] = positionsFor(collectionModel, [id]);
43
30
 
44
- if (id !== null && wanted.has(id)) {
45
- out.push(position);
46
- }
31
+ return first;
47
32
  }
48
33
 
49
- function scanPositions(tree: Gtk.TreeListModel, ids: string[]): number[] {
50
- const wanted = new Set(ids);
51
- const positions: number[] = [];
52
- const count = tree.getNItems();
53
-
54
- for (let position = 0; position < count; position++) {
55
- collectWanted(tree.getItem(position), position, wanted, positions);
34
+ function refAt(collectionModel: CollectionModel, position: number): SlotRef | null {
35
+ if (position < 0) {
36
+ return null;
56
37
  }
57
38
 
58
- return positions;
39
+ return slotRefFor(collectionModel.model.getItem(position));
59
40
  }
60
41
 
61
- function indexPositions(index: CollectionIndex, ids: string[]): number[] {
62
- const positions: number[] = [];
63
-
64
- for (const id of ids) {
65
- const position = index.positionFor(id);
42
+ function idAt(collectionModel: CollectionModel, index: CollectionIndex, position: number): string | null {
43
+ const ref = refAt(collectionModel, position);
66
44
 
67
- if (position >= 0) {
68
- positions.push(position);
69
- }
45
+ if (ref === null) {
46
+ return null;
70
47
  }
71
48
 
72
- return positions.toSorted(ascending);
49
+ return index.itemAt(ref.store.path, ref.slot)?.id ?? null;
73
50
  }
74
51
 
75
- function positionsFor(gtk: CollectionModel, index: CollectionIndex, ids: string[]): number[] {
76
- if (!index.isTree) {
77
- return indexPositions(index, ids);
78
- }
52
+ function pathAt(collectionModel: CollectionModel, position: number): string | null {
53
+ const ref = refAt(collectionModel, position);
79
54
 
80
- const tree = gtk.treeModel();
55
+ if (ref === null) {
56
+ return null;
57
+ }
81
58
 
82
- return tree === null ? [] : scanPositions(tree, ids);
59
+ return slotPathAt(ref.store, ref.slot);
83
60
  }
84
61
 
85
- function positionFor(gtk: CollectionModel, index: CollectionIndex, id: string): number {
86
- const [first = -1] = positionsFor(gtk, index, [id]);
62
+ function isCollectionIdle(collection: Collection): boolean {
63
+ const { expansion } = collection;
87
64
 
88
- return first;
65
+ return !expansion.isApplying && !expansion.isSyncing;
89
66
  }
90
67
 
91
- function createCollection(gtk: CollectionModel, index: CollectionIndex): Collection {
68
+ function createCollection(collectionModel: CollectionModel, index: CollectionIndex): Collection {
92
69
  return {
93
- isTree: index.isTree,
94
- model: gtk.model,
95
- treeModel: () => (index.isTree ? gtk.treeModel() : null),
96
- itemFor: index.itemFor,
70
+ model: collectionModel.model,
71
+ expansion: collectionModel.expansion,
72
+ treeModel: () => (index.isTree ? collectionModel.treeModel() : null),
73
+ itemAt: (ref) => index.itemAt(ref.store.path, ref.slot),
97
74
  sectionFor: index.sectionFor,
98
- idAt: (position) => getId(gtk.model.getItem(position)),
99
- positionFor: (id) => positionFor(gtk, index, id),
100
- positionsFor: (ids) => positionsFor(gtk, index, ids),
75
+ idAt: (position) => idAt(collectionModel, index, position),
76
+ pathAt: (position) => pathAt(collectionModel, position),
77
+ positionFor: (id) => positionFor(collectionModel, id),
78
+ positionsFor: (ids) => positionsFor(collectionModel, ids),
101
79
  };
102
80
  }
103
81
 
104
- export { createCollection, eachRow, type Collection };
82
+ export { createCollection, isCollectionIdle, type Collection };