@gtkx/components 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/dist/adw/toast.d.ts.map +1 -1
  3. package/dist/adw/toast.js +6 -2
  4. package/dist/adw/toast.js.map +1 -1
  5. package/dist/internal/cells.js +1 -1
  6. package/dist/internal/cells.js.map +1 -1
  7. package/dist/internal/collection-index.d.ts +4 -3
  8. package/dist/internal/collection-index.d.ts.map +1 -1
  9. package/dist/internal/collection-index.js +50 -33
  10. package/dist/internal/collection-index.js.map +1 -1
  11. package/dist/internal/collection-model.d.ts +3 -4
  12. package/dist/internal/collection-model.d.ts.map +1 -1
  13. package/dist/internal/collection-model.js +110 -74
  14. package/dist/internal/collection-model.js.map +1 -1
  15. package/dist/internal/collection.d.ts +2 -1
  16. package/dist/internal/collection.d.ts.map +1 -1
  17. package/dist/internal/collection.js +7 -6
  18. package/dist/internal/collection.js.map +1 -1
  19. package/dist/internal/expansion.d.ts.map +1 -1
  20. package/dist/internal/expansion.js +11 -19
  21. package/dist/internal/expansion.js.map +1 -1
  22. package/dist/internal/tree-expansion.d.ts +1 -2
  23. package/dist/internal/tree-expansion.d.ts.map +1 -1
  24. package/dist/internal/tree-expansion.js +12 -8
  25. package/dist/internal/tree-expansion.js.map +1 -1
  26. package/dist/internal/tree-order.d.ts +3 -4
  27. package/dist/internal/tree-order.d.ts.map +1 -1
  28. package/dist/internal/tree-order.js +61 -23
  29. package/dist/internal/tree-order.js.map +1 -1
  30. package/dist/types.d.ts +3 -2
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/types.js.map +1 -1
  33. package/package.json +17 -9
  34. package/src/adw/toast.tsx +6 -2
  35. package/src/internal/cells.tsx +1 -1
  36. package/src/internal/collection-index.ts +63 -39
  37. package/src/internal/collection-model.ts +148 -85
  38. package/src/internal/collection.ts +10 -8
  39. package/src/internal/expansion.ts +12 -24
  40. package/src/internal/tree-expansion.ts +14 -9
  41. package/src/internal/tree-order.ts +98 -28
  42. package/src/types.ts +3 -2
@@ -1,4 +1,3 @@
1
- import type * as Gtk from "@gtkx/gi/gtk";
2
1
  import { useState } from "react";
3
2
  import type { Collection } from "./collection.js";
4
3
  import type { TreeExpansion } from "./tree-expansion.js";
@@ -8,7 +7,7 @@ import { useControlledSync } from "./controlled-sync.js";
8
7
  import { joinParts } from "./keys.js";
9
8
  import { trackPaths } from "./slots.js";
10
9
  import { adoptOrder, markExpanded, orderFor } from "./tree-expansion.js";
11
- import { walkVisible } from "./tree-order.js";
10
+ import { expandedPathsFor, walkVisible } from "./tree-order.js";
12
11
 
13
12
  type ExpansionOptions = {
14
13
  collection: Collection;
@@ -35,30 +34,20 @@ function newLastExpansion(): LastExpansion {
35
34
  return { key: "" };
36
35
  }
37
36
 
38
- function wantedSet(expansion: TreeExpansion, expandedIds: string[]): Set<string> {
39
- const wanted: Set<string> = new Set();
40
-
41
- for (const id of expandedIds) {
42
- for (const path of expansion.index.expandablePathsFor(id)) {
43
- wanted.add(path);
44
- }
45
- }
46
-
47
- return wanted;
48
- }
49
-
50
37
  function hasSameExpansion(expanded: Set<string>, wanted: Set<string>): boolean {
51
38
  return expanded.size === wanted.size && wanted.isSubsetOf(expanded);
52
39
  }
53
40
 
54
- function toggleOptions(expansion: TreeExpansion, tree: Gtk.TreeListModel, wanted: Set<string>): WalkOptions {
41
+ function toggleOptions(collection: Collection, wanted: Set<string>): WalkOptions {
42
+ const { expansion } = collection;
43
+
55
44
  return {
56
45
  index: expansion.index,
57
46
  slots: trackPaths(wanted),
58
47
  marks: trackPaths(wanted.symmetricDifference(expansion.expanded)),
59
48
  visit: (row) => {
60
49
  if (row.isMarked) {
61
- tree.getRow(row.position)?.setExpanded(row.isOpen);
50
+ collection.rowAt(row.position)?.setExpanded(row.isOpen);
62
51
  }
63
52
  },
64
53
  };
@@ -74,14 +63,15 @@ function walkApplying(expansion: TreeExpansion, options: WalkOptions): VisibleOr
74
63
  }
75
64
  }
76
65
 
77
- function applyWanted(expansion: TreeExpansion, tree: Gtk.TreeListModel, expandedIds: string[]): void {
78
- const wanted = wantedSet(expansion, expandedIds);
66
+ function applyWanted(collection: Collection, expandedIds: string[]): void {
67
+ const { expansion } = collection;
68
+ const wanted = expandedPathsFor(expansion.index, new Set(expandedIds));
79
69
 
80
70
  if (hasSameExpansion(expansion.expanded, wanted)) {
81
71
  return;
82
72
  }
83
73
 
84
- adoptOrder(expansion, walkApplying(expansion, toggleOptions(expansion, tree, wanted)));
74
+ adoptOrder(expansion, walkApplying(expansion, toggleOptions(collection, wanted)));
85
75
  }
86
76
 
87
77
  function reportExpansion(context: ExpansionContext): void {
@@ -97,18 +87,16 @@ function reportExpansion(context: ExpansionContext): void {
97
87
  }
98
88
 
99
89
  function applyControlledExpansion(context: ExpansionContext, expandedIds: string[] | null | undefined): void {
100
- const tree = context.collection.treeModel();
101
-
102
- if (tree === null) {
90
+ if (!context.collection.isTree) {
103
91
  return;
104
92
  }
105
93
 
106
- applyWanted(context.collection.expansion, tree, expandedIds ?? []);
94
+ applyWanted(context.collection, expandedIds ?? []);
107
95
  reportExpansion(context);
108
96
  }
109
97
 
110
98
  function isExpansionIdle(collection: Collection): boolean {
111
- return collection.treeModel() !== null && isCollectionIdle(collection);
99
+ return collection.isTree && isCollectionIdle(collection);
112
100
  }
113
101
 
114
102
  function didRowDrift(collection: Collection, change: ItemsChange): boolean {
@@ -14,7 +14,7 @@ type TreeExpansion = {
14
14
  isSyncing: boolean;
15
15
  };
16
16
 
17
- function dropSubtree(expansion: TreeExpansion, path: string): void {
17
+ function dropLevel(expansion: TreeExpansion, path: string, pending: string[]): void {
18
18
  expansion.expanded.delete(path);
19
19
  const level = expansion.slots.get(path);
20
20
 
@@ -25,7 +25,19 @@ function dropSubtree(expansion: TreeExpansion, path: string): void {
25
25
  expansion.slots.delete(path);
26
26
 
27
27
  for (const slot of level) {
28
- dropSubtree(expansion, path + encodePart(String(slot)));
28
+ pending.push(path + encodePart(String(slot)));
29
+ }
30
+ }
31
+
32
+ function dropSubtree(expansion: TreeExpansion, path: string): void {
33
+ const pending: string[] = [path];
34
+
35
+ while (pending.length > 0) {
36
+ const current = pending.pop();
37
+
38
+ if (current !== undefined) {
39
+ dropLevel(expansion, current, pending);
40
+ }
29
41
  }
30
42
  }
31
43
 
@@ -87,12 +99,6 @@ function markExpanded(expansion: TreeExpansion, path: string, isExpanded: boolea
87
99
  prunePath(expansion, path);
88
100
  }
89
101
 
90
- function resetExpansion(expansion: TreeExpansion): void {
91
- expansion.expanded = new Set();
92
- expansion.slots = new Map();
93
- expansion.order = null;
94
- }
95
-
96
102
  function adoptIndex(expansion: TreeExpansion, index: CollectionIndex): void {
97
103
  expansion.index = index;
98
104
  expansion.order = null;
@@ -105,6 +111,5 @@ export {
105
111
  markExpanded,
106
112
  orderFor,
107
113
  pruneSlots,
108
- resetExpansion,
109
114
  type TreeExpansion,
110
115
  };
@@ -1,11 +1,8 @@
1
1
  import type { ListItem } from "../types.js";
2
2
  import type { CollectionIndex, Level } from "./collection-index.js";
3
3
  import type { SlotMap } from "./slots.js";
4
- import { encodePart } from "./keys.js";
5
4
 
6
5
  type VisibleRow = {
7
- level: Level;
8
- slot: number;
9
6
  item: ListItem;
10
7
  position: number;
11
8
  isOpen: boolean;
@@ -26,9 +23,12 @@ type WalkOptions = {
26
23
  visit?: RowVisitor | undefined;
27
24
  };
28
25
 
29
- type WalkFrame = {
26
+ type LevelFrame = {
30
27
  level: Level;
31
28
  cursor: number;
29
+ };
30
+
31
+ type WalkFrame = LevelFrame & {
32
32
  open: Set<number> | undefined;
33
33
  marked: Set<number> | undefined;
34
34
  };
@@ -40,23 +40,55 @@ type WalkState = {
40
40
  order: VisibleOrder;
41
41
  };
42
42
 
43
- const NO_LEVEL: Level = { path: "", items: [], expandableFlags: [] };
43
+ type OpenFrame = LevelFrame & {
44
+ owner: ListItem | null;
45
+ };
46
+
47
+ type OpenWalk = {
48
+ index: CollectionIndex;
49
+ ids: Set<string>;
50
+ paths: Set<string>;
51
+ stack: OpenFrame[];
52
+ ancestors: Set<ListItem>;
53
+ };
54
+
44
55
  const NO_ITEM: ListItem = { id: "", value: undefined };
45
56
 
46
- const getRowPath = (row: VisibleRow): string => row.level.path + encodePart(String(row.slot));
57
+ function advanceStack<F extends LevelFrame>(
58
+ stack: F[],
59
+ onSlot: (frame: F, slot: number) => void,
60
+ onLeave?: (frame: F) => void,
61
+ ): void {
62
+ const frame = stack.at(-1);
63
+
64
+ if (frame === undefined) {
65
+ return;
66
+ }
67
+
68
+ if (frame.cursor >= frame.level.items.length) {
69
+ stack.pop();
70
+ onLeave?.(frame);
71
+
72
+ return;
73
+ }
74
+
75
+ const slot = frame.cursor;
76
+ frame.cursor += 1;
77
+ onSlot(frame, slot);
78
+ }
47
79
 
48
80
  function frameFor(options: WalkOptions, level: Level): WalkFrame {
49
81
  return { level, cursor: 0, open: options.slots.get(level.path), marked: options.marks?.get(level.path) };
50
82
  }
51
83
 
52
- function descend(state: WalkState, path: string): void {
53
- const child = state.options.index.children.get(path);
84
+ function descend(state: WalkState, level: Level, slot: number): void {
85
+ const child = state.options.index.childLevel(level, slot);
54
86
 
55
- if (child === undefined || child.items.length === 0) {
87
+ if (child === undefined) {
56
88
  return;
57
89
  }
58
90
 
59
- state.order.expandedPaths.push(path);
91
+ state.order.expandedPaths.push(child.path);
60
92
  state.order.expandedIds.push(state.row.item.id);
61
93
  state.stack.push(frameFor(state.options, child));
62
94
  }
@@ -69,8 +101,6 @@ function visitSlot(state: WalkState, frame: WalkFrame, slot: number): void {
69
101
  }
70
102
 
71
103
  const { row } = state;
72
- row.level = frame.level;
73
- row.slot = slot;
74
104
  row.item = item;
75
105
  row.isOpen = frame.open?.has(slot) ?? false;
76
106
  row.isMarked = frame.marked?.has(slot) ?? false;
@@ -78,35 +108,74 @@ function visitSlot(state: WalkState, frame: WalkFrame, slot: number): void {
78
108
  row.position += 1;
79
109
 
80
110
  if (row.isOpen) {
81
- descend(state, getRowPath(row));
111
+ descend(state, frame.level, slot);
82
112
  }
83
113
  }
84
114
 
85
- function advanceFrame(state: WalkState): void {
86
- const frame = state.stack.at(-1);
115
+ function walkVisible(options: WalkOptions): VisibleOrder {
116
+ const order: VisibleOrder = { expandedPaths: [], expandedIds: [] };
117
+ const row: VisibleRow = { item: NO_ITEM, position: 0, isOpen: false, isMarked: false };
118
+ const frames = options.index.groups.map((level) => frameFor(options, level));
119
+ const state: WalkState = { options, stack: frames.toReversed(), row, order };
87
120
 
88
- if (frame === undefined || frame.cursor >= frame.level.items.length) {
89
- state.stack.pop();
121
+ const onSlot = (frame: WalkFrame, slot: number): void => {
122
+ visitSlot(state, frame, slot);
123
+ };
124
+
125
+ while (state.stack.length > 0) {
126
+ advanceStack(state.stack, onSlot);
127
+ }
90
128
 
129
+ return order;
130
+ }
131
+
132
+ function openSlot(walk: OpenWalk, frame: OpenFrame, slot: number): void {
133
+ const item = frame.level.items[slot];
134
+
135
+ if (item === undefined || !walk.ids.has(item.id) || walk.ancestors.has(item)) {
91
136
  return;
92
137
  }
93
138
 
94
- const slot = frame.cursor;
95
- frame.cursor += 1;
96
- visitSlot(state, frame, slot);
139
+ const child = walk.index.childLevel(frame.level, slot);
140
+
141
+ if (child === undefined) {
142
+ return;
143
+ }
144
+
145
+ walk.paths.add(child.path);
146
+ walk.ancestors.add(item);
147
+ walk.stack.push({ level: child, cursor: 0, owner: item });
97
148
  }
98
149
 
99
- function walkVisible(options: WalkOptions): VisibleOrder {
100
- const order: VisibleOrder = { expandedPaths: [], expandedIds: [] };
101
- const row: VisibleRow = { level: NO_LEVEL, slot: 0, item: NO_ITEM, position: 0, isOpen: false, isMarked: false };
102
- const frames = options.index.groups.map((level) => frameFor(options, level));
103
- const state: WalkState = { options, stack: frames.toReversed(), row, order };
150
+ function leaveOpenFrame(walk: OpenWalk, frame: OpenFrame): void {
151
+ if (frame.owner !== null) {
152
+ walk.ancestors.delete(frame.owner);
153
+ }
154
+ }
104
155
 
105
- while (state.stack.length > 0) {
106
- advanceFrame(state);
156
+ function expandedPathsFor(index: CollectionIndex, ids: Set<string>): Set<string> {
157
+ const paths: Set<string> = new Set();
158
+
159
+ if (ids.size === 0 || !index.isTree) {
160
+ return paths;
107
161
  }
108
162
 
109
- return order;
163
+ const frames = index.groups.map((level) => ({ level, cursor: 0, owner: null }));
164
+ const walk: OpenWalk = { index, ids, paths, stack: frames.toReversed(), ancestors: new Set() };
165
+
166
+ const onSlot = (frame: OpenFrame, slot: number): void => {
167
+ openSlot(walk, frame, slot);
168
+ };
169
+
170
+ const onLeave = (frame: OpenFrame): void => {
171
+ leaveOpenFrame(walk, frame);
172
+ };
173
+
174
+ while (walk.stack.length > 0) {
175
+ advanceStack(walk.stack, onSlot, onLeave);
176
+ }
177
+
178
+ return paths;
110
179
  }
111
180
 
112
181
  function buildVisibleOrder(index: CollectionIndex, slots: SlotMap): VisibleOrder {
@@ -131,6 +200,7 @@ function findPositions(index: CollectionIndex, slots: SlotMap, ids: Set<string>)
131
200
 
132
201
  export {
133
202
  buildVisibleOrder,
203
+ expandedPathsFor,
134
204
  findPositions,
135
205
  walkVisible,
136
206
  type VisibleOrder,
package/src/types.ts CHANGED
@@ -17,7 +17,7 @@ type ListItem<T = unknown> = {
17
17
  id: string;
18
18
  /** Payload handed to the cell renderer as `ListItemRenderArgs.item`. */
19
19
  value: T;
20
- /** Child items nested under this one, which turn a plain item list into a tree. */
20
+ /** Child items nested under this one, which turn a plain item list into a tree, read only as rows are drawn. */
21
21
  children?: ListItem<T>[] | undefined;
22
22
  /** Hides the tree expander arrow even when the item has children, through `hide-expander`. */
23
23
  shouldHideExpander?: boolean | undefined;
@@ -119,7 +119,8 @@ type ExpansionProps = {
119
119
  /**
120
120
  * Ids of the items to keep expanded; omitting it keeps every row collapsed, and `onExpandedChange` is how a
121
121
  * user's expansion is adopted into it. An id repeated in several branches names every matching row, so all of
122
- * them expand together.
122
+ * them expand together. An item whose children lead back to itself expands one level at a time, since a row
123
+ * repeating an item already expanded above it stays collapsed.
123
124
  */
124
125
  expandedIds?: string[] | null | undefined;
125
126
  /** Called with one id per expanded row, in visible order, whenever expansion changes. */