@gtkx/components 1.0.0-rc.3 → 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 (104) hide show
  1. package/README.md +8 -9
  2. package/dist/adw/combo-row.d.ts +1 -0
  3. package/dist/adw/combo-row.d.ts.map +1 -1
  4. package/dist/adw/combo-row.js.map +1 -1
  5. package/dist/adw/types.d.ts +2 -1
  6. package/dist/adw/types.d.ts.map +1 -1
  7. package/dist/adw/types.js.map +1 -1
  8. package/dist/column-view.d.ts +2 -2
  9. package/dist/column-view.d.ts.map +1 -1
  10. package/dist/column-view.js +15 -13
  11. package/dist/column-view.js.map +1 -1
  12. package/dist/drop-down.d.ts +1 -0
  13. package/dist/drop-down.d.ts.map +1 -1
  14. package/dist/drop-down.js.map +1 -1
  15. package/dist/grid-view.js +2 -2
  16. package/dist/grid-view.js.map +1 -1
  17. package/dist/index.d.ts +1 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +0 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/internal/cells.d.ts +32 -24
  22. package/dist/internal/cells.d.ts.map +1 -1
  23. package/dist/internal/cells.js +168 -77
  24. package/dist/internal/cells.js.map +1 -1
  25. package/dist/internal/collection-index.d.ts +9 -13
  26. package/dist/internal/collection-index.d.ts.map +1 -1
  27. package/dist/internal/collection-index.js +33 -70
  28. package/dist/internal/collection-index.js.map +1 -1
  29. package/dist/internal/collection-model.d.ts +23 -2
  30. package/dist/internal/collection-model.d.ts.map +1 -1
  31. package/dist/internal/collection-model.js +210 -98
  32. package/dist/internal/collection-model.js.map +1 -1
  33. package/dist/internal/collection.d.ts +9 -12
  34. package/dist/internal/collection.d.ts.map +1 -1
  35. package/dist/internal/collection.js +38 -49
  36. package/dist/internal/collection.js.map +1 -1
  37. package/dist/internal/controlled-sync.d.ts +11 -0
  38. package/dist/internal/controlled-sync.d.ts.map +1 -0
  39. package/dist/internal/controlled-sync.js +19 -0
  40. package/dist/internal/controlled-sync.js.map +1 -0
  41. package/dist/internal/drop-down.d.ts.map +1 -1
  42. package/dist/internal/drop-down.js +27 -15
  43. package/dist/internal/drop-down.js.map +1 -1
  44. package/dist/internal/expansion.d.ts +3 -2
  45. package/dist/internal/expansion.d.ts.map +1 -1
  46. package/dist/internal/expansion.js +80 -47
  47. package/dist/internal/expansion.js.map +1 -1
  48. package/dist/internal/keys.d.ts +5 -0
  49. package/dist/internal/keys.d.ts.map +1 -0
  50. package/dist/internal/keys.js +13 -0
  51. package/dist/internal/keys.js.map +1 -0
  52. package/dist/internal/selection.d.ts +3 -2
  53. package/dist/internal/selection.d.ts.map +1 -1
  54. package/dist/internal/selection.js +68 -35
  55. package/dist/internal/selection.js.map +1 -1
  56. package/dist/internal/slots.d.ts +10 -0
  57. package/dist/internal/slots.d.ts.map +1 -0
  58. package/dist/internal/slots.js +43 -0
  59. package/dist/internal/slots.js.map +1 -0
  60. package/dist/internal/tree-expansion.d.ts +20 -0
  61. package/dist/internal/tree-expansion.d.ts.map +1 -0
  62. package/dist/internal/tree-expansion.js +68 -0
  63. package/dist/internal/tree-expansion.js.map +1 -0
  64. package/dist/internal/tree-order.d.ts +27 -0
  65. package/dist/internal/tree-order.d.ts.map +1 -0
  66. package/dist/internal/tree-order.js +71 -0
  67. package/dist/internal/tree-order.js.map +1 -0
  68. package/dist/internal/use-collection.d.ts +5 -5
  69. package/dist/internal/use-collection.d.ts.map +1 -1
  70. package/dist/internal/use-collection.js +6 -6
  71. package/dist/internal/use-collection.js.map +1 -1
  72. package/dist/list-view.d.ts.map +1 -1
  73. package/dist/list-view.js +6 -6
  74. package/dist/list-view.js.map +1 -1
  75. package/dist/types.d.ts +127 -54
  76. package/dist/types.d.ts.map +1 -1
  77. package/dist/types.js.map +1 -1
  78. package/package.json +7 -7
  79. package/src/adw/combo-row.tsx +1 -0
  80. package/src/adw/types.ts +2 -1
  81. package/src/column-view.tsx +38 -35
  82. package/src/drop-down.tsx +1 -0
  83. package/src/grid-view.tsx +2 -2
  84. package/src/index.ts +10 -11
  85. package/src/internal/cells.tsx +345 -123
  86. package/src/internal/collection-index.ts +51 -99
  87. package/src/internal/collection-model.ts +259 -110
  88. package/src/internal/collection.ts +46 -68
  89. package/src/internal/controlled-sync.ts +34 -0
  90. package/src/internal/drop-down.tsx +44 -23
  91. package/src/internal/expansion.ts +104 -67
  92. package/src/internal/keys.ts +18 -0
  93. package/src/internal/selection.tsx +100 -51
  94. package/src/internal/slots.ts +67 -0
  95. package/src/internal/tree-expansion.ts +110 -0
  96. package/src/internal/tree-order.ts +138 -0
  97. package/src/internal/use-collection.tsx +11 -11
  98. package/src/list-view.tsx +14 -10
  99. package/src/types.ts +140 -65
  100. package/dist/size-group.d.ts +0 -12
  101. package/dist/size-group.d.ts.map +0 -1
  102. package/dist/size-group.js +0 -41
  103. package/dist/size-group.js.map +0 -1
  104. package/src/size-group.tsx +0 -77
@@ -1,181 +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.isExpandable[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
- // Items keep their identity by id, so a collection that shrinks keeps its objects cached: filtering a long
142
- // list down and back up reuses them instead of rebuilding one GObject per row. Incremental filters shrink and
143
- // grow the list many times per keystroke, so the cache is only swept once it passes an absolute bound.
144
- function pruneObjects(state: ModelState, index: CollectionIndex): void {
145
- 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)) {
146
230
  return;
147
231
  }
148
232
 
149
- for (const id of state.objects.keys()) {
150
- if (!index.has(id)) {
151
- state.objects.delete(id);
152
- }
233
+ const level = context.index.children.get(slotPathAt(store, slot));
234
+
235
+ if (level !== undefined) {
236
+ syncLevel(context, child, level);
153
237
  }
154
238
  }
155
239
 
156
- function childStoreFor(state: ModelState, object: GObject.Object): Gio.ListStore | null {
157
- 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
+ }
158
253
 
159
- if (id === null) {
254
+ function childStoreFor(object: GObject.Object): Gio.ListModel | null {
255
+ const ref = slotRefFor(object);
256
+
257
+ if (ref === null) {
160
258
  return null;
161
259
  }
162
260
 
163
- return state.levels.get(childLevelKey(id))?.store ?? null;
261
+ return ref.store.childStores[ref.slot] ?? null;
164
262
  }
165
263
 
166
- function ensureTree(state: ModelState): Gtk.TreeListModel {
167
- state.tree ??= Gtk.TreeListModel.new(levelFor(state, ROOT_LEVEL_KEY).store, false, false, (object) =>
168
- 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
+ }
169
269
 
170
270
  return state.tree;
171
271
  }
172
272
 
173
273
  function desiredRootModels(state: ModelState, index: CollectionIndex): GObject.Object[] {
174
- if (index.isTree) {
175
- return [ensureTree(state)];
274
+ const [first] = state.groupStores;
275
+
276
+ if (first !== undefined && index.isTree) {
277
+ return [ensureTree(state, first)];
176
278
  }
177
279
 
178
- return index.groups.map((level) => levelFor(state, level.key).store);
280
+ return [...state.groupStores];
179
281
  }
180
282
 
181
283
  function hasSameModels(previous: GObject.Object[], next: GObject.Object[]): boolean {
@@ -191,51 +293,63 @@ function syncRoot(state: ModelState, index: CollectionIndex): void {
191
293
 
192
294
  state.root.splice(0, state.rootModels.length, next);
193
295
  state.rootModels = next;
296
+ resetExpansion(state.expansion);
194
297
  }
195
298
 
196
- function syncModel(state: ModelState, index: CollectionIndex): void {
197
- const levels = [...index.groups, ...index.children.values()];
198
- const visited: Set<string> = new Set();
199
- 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);
200
302
 
201
- for (const level of levels) {
202
- didChange = didSpliceLevel(state, level) || didChange;
203
- 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);
204
307
  }
308
+ }
205
309
 
206
- if (didChange || visited.size !== state.levels.size) {
207
- dropUnvisited(state, visited);
208
- pruneObjects(state, index);
209
- }
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];
210
313
 
211
- syncRoot(state, index);
212
- refreshExpandableLevels(state, index, levels);
314
+ if (store !== undefined && group < surviving) {
315
+ syncLevel(context, store, level);
316
+ }
317
+ }
213
318
  }
214
319
 
215
- function refreshExpandableLevels(state: ModelState, index: CollectionIndex, levels: Level[]): void {
216
- if (!index.isTree) {
217
- 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;
218
331
  }
219
332
 
220
- for (const level of levels) {
221
- refreshExpandable(levelFor(state, level.key), level);
222
- }
333
+ adoptIndex(state.expansion, index);
223
334
  }
224
335
 
225
336
  function createCollectionModel(): CollectionModel {
226
- const root = newStore();
337
+ registeredStoreClass();
338
+ const root = newRootStore();
227
339
 
228
340
  const state: ModelState = {
229
341
  root,
230
342
  rootModels: [],
231
343
  model: Gtk.FlattenListModel.new(root),
232
- objects: new Map(),
233
- levels: new Map(),
344
+ groupStores: [],
234
345
  tree: null,
346
+ treeRoot: null,
347
+ expansion: createTreeExpansion(EMPTY_INDEX),
235
348
  };
236
349
 
237
350
  return {
238
351
  model: state.model,
352
+ expansion: state.expansion,
239
353
  treeModel: () => state.tree,
240
354
  sync: (index) => {
241
355
  syncModel(state, index);
@@ -243,4 +357,39 @@ function createCollectionModel(): CollectionModel {
243
357
  };
244
358
  }
245
359
 
246
- 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
- import type { Item } from "../types.js";
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) => Item | 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 };