@gtkx/components 2.0.0-beta.1 → 2.0.0-beta.11
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.
- package/README.md +8 -165
- package/dist/column-view.d.ts.map +1 -1
- package/dist/column-view.js +20 -9
- package/dist/column-view.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +19 -2
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +202 -39
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/collection-index.d.ts +3 -2
- package/dist/internal/collection-index.d.ts.map +1 -1
- package/dist/internal/collection-index.js +3 -3
- package/dist/internal/collection-index.js.map +1 -1
- package/dist/internal/collection-model.d.ts +28 -7
- package/dist/internal/collection-model.d.ts.map +1 -1
- package/dist/internal/collection-model.js +147 -127
- package/dist/internal/collection-model.js.map +1 -1
- package/dist/internal/collection.d.ts +5 -4
- package/dist/internal/collection.d.ts.map +1 -1
- package/dist/internal/collection.js +20 -21
- package/dist/internal/collection.js.map +1 -1
- package/dist/internal/controlled-sync.d.ts +6 -8
- package/dist/internal/controlled-sync.d.ts.map +1 -1
- package/dist/internal/controlled-sync.js +3 -3
- package/dist/internal/controlled-sync.js.map +1 -1
- package/dist/internal/drop-down.d.ts.map +1 -1
- package/dist/internal/drop-down.js +43 -44
- package/dist/internal/drop-down.js.map +1 -1
- package/dist/internal/expansion.js +4 -4
- package/dist/internal/expansion.js.map +1 -1
- package/dist/internal/selection.d.ts +1 -0
- package/dist/internal/selection.d.ts.map +1 -1
- package/dist/internal/selection.js +6 -6
- package/dist/internal/selection.js.map +1 -1
- package/dist/internal/tree-expansion.d.ts.map +1 -1
- package/dist/internal/tree-expansion.js +2 -11
- package/dist/internal/tree-expansion.js.map +1 -1
- package/dist/internal/tree-order.d.ts.map +1 -1
- package/dist/internal/tree-order.js +32 -76
- package/dist/internal/tree-order.js.map +1 -1
- package/dist/internal/use-collection.d.ts +5 -1
- package/dist/internal/use-collection.d.ts.map +1 -1
- package/dist/internal/use-collection.js +21 -7
- package/dist/internal/use-collection.js.map +1 -1
- package/dist/list-item-factory.d.ts +10 -0
- package/dist/list-item-factory.d.ts.map +1 -0
- package/dist/list-item-factory.js +14 -0
- package/dist/list-item-factory.js.map +1 -0
- package/dist/list-view.d.ts.map +1 -1
- package/dist/list-view.js +1 -0
- package/dist/list-view.js.map +1 -1
- package/dist/toast.d.ts +3 -5
- package/dist/toast.d.ts.map +1 -1
- package/dist/toast.js +3 -12
- package/dist/toast.js.map +1 -1
- package/dist/types.d.ts +44 -52
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +13 -10
- package/src/column-view.tsx +24 -12
- package/src/index.ts +3 -2
- package/src/internal/cells.tsx +330 -53
- package/src/internal/collection-index.ts +8 -6
- package/src/internal/collection-model.ts +190 -168
- package/src/internal/collection.ts +28 -24
- package/src/internal/controlled-sync.ts +9 -12
- package/src/internal/drop-down.tsx +64 -60
- package/src/internal/expansion.ts +4 -4
- package/src/internal/selection.tsx +8 -8
- package/src/internal/tree-expansion.ts +2 -14
- package/src/internal/tree-order.ts +33 -121
- package/src/internal/use-collection.tsx +29 -8
- package/src/list-item-factory.tsx +27 -0
- package/src/list-view.tsx +1 -0
- package/src/toast.tsx +4 -18
- package/src/types.ts +48 -52
|
@@ -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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 {
|
|
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(
|
|
28
|
-
}, [applyRef,
|
|
24
|
+
applyRef.current(value);
|
|
25
|
+
}, [applyRef, target, source, value, drift]);
|
|
29
26
|
|
|
30
27
|
return markDrift;
|
|
31
28
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
2
|
import type { ElementType, ReactNode, Ref } from "react";
|
|
3
3
|
import { GtkLabel, GtkSignalListItemFactory } from "@gtkx/jsx/gtk";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { omit, type Primitive } from "@gtkx/utils";
|
|
5
|
+
import { useRef } from "react";
|
|
7
6
|
import type { DropDownOwnProps, ListItemRenderArgs, ListItemRenderer } from "../types.js";
|
|
8
|
-
import type { Collection } from "./collection.js";
|
|
9
7
|
import { ItemPortals, useItemCells, useSectionHeader } from "./cells.js";
|
|
8
|
+
import { type Collection, isCollectionIdle } from "./collection.js";
|
|
9
|
+
import { useControlledSync } from "./controlled-sync.js";
|
|
10
10
|
import { useCollectionData } from "./use-collection.js";
|
|
11
11
|
import { useWidgetRef } from "./use-widget-ref.js";
|
|
12
12
|
|
|
@@ -24,17 +24,18 @@ type SelectionOptions = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
type NotifySelectedHandler = NonNullable<DropDownBaseProps["onNotifySelected"]>;
|
|
27
|
-
type ApplyState = { isApplying: boolean };
|
|
27
|
+
type ApplyState = { isApplying: boolean; isReady: boolean };
|
|
28
28
|
|
|
29
29
|
type NotifyContext = {
|
|
30
30
|
options: SelectionOptions;
|
|
31
31
|
known: { current: string | null };
|
|
32
32
|
state: ApplyState;
|
|
33
|
+
markDrift: () => void;
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
type KnownSelection = {
|
|
36
37
|
known: { current: string | null };
|
|
37
|
-
onSelectionChanged: ((id: string) => void) | null | undefined;
|
|
38
|
+
onSelectionChanged: ((id: string | null) => void) | null | undefined;
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
const DROP_DOWN_PROPS: string[] = [
|
|
@@ -51,41 +52,31 @@ const DROP_DOWN_PROPS: string[] = [
|
|
|
51
52
|
];
|
|
52
53
|
|
|
53
54
|
function newApplyState(): ApplyState {
|
|
54
|
-
return { isApplying: false };
|
|
55
|
+
return { isApplying: false, isReady: false };
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
return value;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
return JSON.stringify(value);
|
|
64
|
-
} catch {
|
|
65
|
-
return String(value);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const defaultItemContent = (value: unknown): ReactNode =>
|
|
70
|
-
value == null ? null : <GtkLabel>{describeValue(value)}</GtkLabel>;
|
|
71
|
-
|
|
72
|
-
const defaultRenderItem = ({ item }: ListItemRenderArgs<unknown>): ReactNode => defaultItemContent(item);
|
|
58
|
+
const defaultRenderItem = ({ item }: ListItemRenderArgs<Primitive>): ReactNode =>
|
|
59
|
+
item == null ? null : <GtkLabel>{String(item)}</GtkLabel>;
|
|
73
60
|
const faceRenderer = (props: DropDownBaseProps): ListItemRenderer<never> => props.renderItem ?? defaultRenderItem;
|
|
74
61
|
|
|
75
62
|
const resolvePosition = (
|
|
76
63
|
widget: SelectableWidget,
|
|
77
64
|
collection: Collection,
|
|
78
|
-
selectedId: string |
|
|
65
|
+
selectedId: string | undefined,
|
|
79
66
|
): number => {
|
|
80
|
-
|
|
67
|
+
if (selectedId === undefined) {
|
|
68
|
+
return widget.getSelected();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const requested = collection.positionFor(selectedId);
|
|
81
72
|
|
|
82
73
|
return requested >= 0 ? requested : widget.getSelected();
|
|
83
74
|
};
|
|
84
75
|
|
|
85
76
|
const updateKnownSelection = (
|
|
86
77
|
tracker: KnownSelection,
|
|
87
|
-
effectiveId: string,
|
|
88
|
-
selectedId: string |
|
|
78
|
+
effectiveId: string | null,
|
|
79
|
+
selectedId: string | undefined,
|
|
89
80
|
): void => {
|
|
90
81
|
const { known, onSelectionChanged } = tracker;
|
|
91
82
|
const expectedId = selectedId ?? known.current;
|
|
@@ -100,7 +91,7 @@ const updateKnownSelection = (
|
|
|
100
91
|
const reportKnownSelection = (tracker: KnownSelection, id: string | null): void => {
|
|
101
92
|
const { known, onSelectionChanged } = tracker;
|
|
102
93
|
|
|
103
|
-
if (id ===
|
|
94
|
+
if (id === known.current) {
|
|
104
95
|
return;
|
|
105
96
|
}
|
|
106
97
|
|
|
@@ -108,14 +99,18 @@ const reportKnownSelection = (tracker: KnownSelection, id: string | null): void
|
|
|
108
99
|
onSelectionChanged?.(id);
|
|
109
100
|
};
|
|
110
101
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
102
|
+
const hasSelectionDrifted = (
|
|
103
|
+
widget: SelectableWidget,
|
|
104
|
+
collection: Collection,
|
|
105
|
+
selectedId: string | undefined,
|
|
106
|
+
): boolean => {
|
|
107
|
+
if (selectedId === undefined) {
|
|
108
|
+
return false;
|
|
116
109
|
}
|
|
117
110
|
|
|
118
|
-
|
|
111
|
+
const requested = collection.positionFor(selectedId);
|
|
112
|
+
|
|
113
|
+
return requested >= 0 && widget.getSelected() !== requested;
|
|
119
114
|
};
|
|
120
115
|
|
|
121
116
|
const applySelectedPosition = (widget: SelectableWidget, position: number, state: ApplyState): void => {
|
|
@@ -129,49 +124,58 @@ const applySelectedPosition = (widget: SelectableWidget, position: number, state
|
|
|
129
124
|
};
|
|
130
125
|
|
|
131
126
|
const dispatchSelectedNotify = (context: NotifyContext, value: number | null, self: SelectableWidget): void => {
|
|
132
|
-
const { options, known, state } = context;
|
|
127
|
+
const { options, known, state, markDrift } = context;
|
|
133
128
|
|
|
134
129
|
if (state.isApplying) {
|
|
135
130
|
return;
|
|
136
131
|
}
|
|
137
132
|
|
|
138
|
-
reportSelectedNotify(options, { known, onSelectionChanged: options.props.onSelectionChanged });
|
|
139
133
|
options.props.onNotifySelected?.(value, self);
|
|
134
|
+
|
|
135
|
+
if (!state.isReady || !isCollectionIdle(options.collection)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
reportKnownSelection(
|
|
140
|
+
{ known, onSelectionChanged: options.props.onSelectionChanged },
|
|
141
|
+
options.collection.idAt(self.getSelected()),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (hasSelectionDrifted(self, options.collection, options.props.selectedId)) {
|
|
145
|
+
markDrift();
|
|
146
|
+
}
|
|
140
147
|
};
|
|
141
148
|
|
|
142
149
|
const useDropDownSelection = (options: SelectionOptions): NotifySelectedHandler => {
|
|
143
150
|
const { widget, collection } = options;
|
|
144
151
|
const { selectedId } = options.props;
|
|
145
152
|
const known = useRef<string | null>(null);
|
|
146
|
-
const
|
|
153
|
+
const applyState = useRef<ApplyState>(newApplyState());
|
|
147
154
|
|
|
148
|
-
const syncKnownSelection = (position: number): void => {
|
|
155
|
+
const syncKnownSelection = (position: number, requestedId: string | undefined): void => {
|
|
149
156
|
const effectiveId = collection.idAt(position);
|
|
150
|
-
|
|
151
|
-
if (effectiveId === null) {
|
|
152
|
-
known.current = null;
|
|
153
|
-
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
157
|
const { onSelectionChanged } = options.props;
|
|
158
|
-
updateKnownSelection({ known, onSelectionChanged }, effectiveId,
|
|
158
|
+
updateKnownSelection({ known, onSelectionChanged }, effectiveId, requestedId);
|
|
159
|
+
applyState.current.isReady = true;
|
|
159
160
|
};
|
|
160
161
|
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
162
|
+
const markDrift = useControlledSync({
|
|
163
|
+
value: selectedId,
|
|
164
|
+
source: collection,
|
|
165
|
+
target: widget,
|
|
166
|
+
apply: (value) => {
|
|
167
|
+
if (widget === null) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const position = resolvePosition(widget, collection, value);
|
|
172
|
+
applySelectedPosition(widget, position, applyState.current);
|
|
173
|
+
syncKnownSelection(position, value);
|
|
174
|
+
},
|
|
175
|
+
});
|
|
172
176
|
|
|
173
177
|
return (value, self) => {
|
|
174
|
-
dispatchSelectedNotify({ options, known, state: applyState }, value, self);
|
|
178
|
+
dispatchSelectedNotify({ options, known, state: applyState.current, markDrift }, value, self);
|
|
175
179
|
};
|
|
176
180
|
};
|
|
177
181
|
|
|
@@ -179,7 +183,7 @@ function DropDownBase(props: DropDownBaseProps & { component: ElementType }): Re
|
|
|
179
183
|
const { component: Component, items, sections, renderListItem, renderHeader, ref } = props;
|
|
180
184
|
const rest = omit(props, DROP_DOWN_PROPS);
|
|
181
185
|
const [widget, refCallback] = useWidgetRef<SelectableWidget>(ref);
|
|
182
|
-
const collection = useCollectionData({ items, sections, isFlat: true });
|
|
186
|
+
const { collection, model } = useCollectionData({ items, sections, isFlat: true });
|
|
183
187
|
const faceCells = useItemCells({ width: -1, height: -1 });
|
|
184
188
|
const listCells = useItemCells({ width: -1, height: -1 });
|
|
185
189
|
const header = useSectionHeader(renderHeader, collection, { width: -1, height: -1 });
|
|
@@ -189,7 +193,7 @@ function DropDownBase(props: DropDownBaseProps & { component: ElementType }): Re
|
|
|
189
193
|
<>
|
|
190
194
|
<Component
|
|
191
195
|
ref={refCallback}
|
|
192
|
-
model={
|
|
196
|
+
model={model}
|
|
193
197
|
factory={<GtkSignalListItemFactory {...faceCells.handlers} />}
|
|
194
198
|
{...(renderListItem != null && {
|
|
195
199
|
listFactory: <GtkSignalListItemFactory {...listCells.handlers} />,
|
|
@@ -124,10 +124,10 @@ function useExpansion(options: ExpansionOptions): ItemsChangeHandler {
|
|
|
124
124
|
const context: ExpansionContext = { collection, last, onExpandedChange };
|
|
125
125
|
|
|
126
126
|
const markDrift = useControlledSync({
|
|
127
|
-
|
|
128
|
-
collection,
|
|
129
|
-
apply: (
|
|
130
|
-
applyControlledExpansion(context,
|
|
127
|
+
value: expandedIds,
|
|
128
|
+
source: collection,
|
|
129
|
+
apply: (value) => {
|
|
130
|
+
applyControlledExpansion(context, value);
|
|
131
131
|
},
|
|
132
132
|
});
|
|
133
133
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type * as Gio from "@gtkx/gi/gio";
|
|
2
1
|
import type { ReactElement } from "react";
|
|
3
2
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
4
3
|
import { GtkMultiSelection, GtkNoSelection, GtkSingleSelection } from "@gtkx/jsx/gtk";
|
|
@@ -12,6 +11,7 @@ import { joinParts } from "./keys.js";
|
|
|
12
11
|
|
|
13
12
|
type SelectionOptions = {
|
|
14
13
|
collection: Collection;
|
|
14
|
+
model: ReactElement;
|
|
15
15
|
selectedIds?: string[] | null | undefined;
|
|
16
16
|
onSelectionChanged?: ((ids: string[]) => void) | null | undefined;
|
|
17
17
|
selectionMode?: Gtk.SelectionMode | null | undefined;
|
|
@@ -42,7 +42,7 @@ type PositionRun = {
|
|
|
42
42
|
|
|
43
43
|
type SelectionElementProps = {
|
|
44
44
|
ref: (value: Gtk.SelectionModel | null) => void;
|
|
45
|
-
model:
|
|
45
|
+
model: ReactElement;
|
|
46
46
|
onSelectionChanged: () => void;
|
|
47
47
|
onItemsChanged: ItemsChangeHandler;
|
|
48
48
|
};
|
|
@@ -257,17 +257,17 @@ function useSelection(options: SelectionOptions): ReactElement {
|
|
|
257
257
|
const context: SelectionContext = { selection, collection, last, state, onSelectionChanged };
|
|
258
258
|
|
|
259
259
|
const markDrift = useControlledSync({
|
|
260
|
-
|
|
261
|
-
collection,
|
|
262
|
-
|
|
263
|
-
apply: (
|
|
264
|
-
applyControlledSelection(context,
|
|
260
|
+
value: selectedIds,
|
|
261
|
+
source: collection,
|
|
262
|
+
target: selection,
|
|
263
|
+
apply: (value) => {
|
|
264
|
+
applyControlledSelection(context, value);
|
|
265
265
|
},
|
|
266
266
|
});
|
|
267
267
|
|
|
268
268
|
return selectionElement(selectionMode, {
|
|
269
269
|
ref: setSelection,
|
|
270
|
-
model:
|
|
270
|
+
model: options.model,
|
|
271
271
|
onSelectionChanged: () => {
|
|
272
272
|
observeSelection(context, selectedIds, markDrift);
|
|
273
273
|
},
|
|
@@ -14,7 +14,7 @@ type TreeExpansion = {
|
|
|
14
14
|
isSyncing: boolean;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
function
|
|
17
|
+
function dropSubtree(expansion: TreeExpansion, path: string): void {
|
|
18
18
|
expansion.expanded.delete(path);
|
|
19
19
|
const level = expansion.slots.get(path);
|
|
20
20
|
|
|
@@ -25,19 +25,7 @@ function dropLevel(expansion: TreeExpansion, path: string, pending: string[]): v
|
|
|
25
25
|
expansion.slots.delete(path);
|
|
26
26
|
|
|
27
27
|
for (const slot of level) {
|
|
28
|
-
|
|
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
|
-
}
|
|
28
|
+
dropSubtree(expansion, path + encodePart(String(slot)));
|
|
41
29
|
}
|
|
42
30
|
}
|
|
43
31
|
|
|
@@ -28,133 +28,56 @@ type WalkOptions = {
|
|
|
28
28
|
visit?: RowVisitor | undefined;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
type LevelFrame = {
|
|
32
|
-
level: Level;
|
|
33
|
-
cursor: number;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
type WalkFrame = LevelFrame & {
|
|
37
|
-
open: Set<number> | undefined;
|
|
38
|
-
marked: Set<number> | undefined;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
31
|
type WalkState = {
|
|
42
|
-
|
|
43
|
-
stack: WalkFrame[];
|
|
44
|
-
row: VisibleRow;
|
|
32
|
+
position: number;
|
|
45
33
|
order: VisibleOrder;
|
|
46
34
|
};
|
|
47
35
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
type OpenWalk = {
|
|
53
|
-
index: CollectionIndex;
|
|
54
|
-
ids: Set<string>;
|
|
55
|
-
paths: Set<string>;
|
|
56
|
-
stack: OpenFrame[];
|
|
57
|
-
ancestors: Set<ListItem>;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const NO_ITEM: ListItem = { id: "", value: undefined };
|
|
61
|
-
|
|
62
|
-
function advanceStack<F extends LevelFrame>(
|
|
63
|
-
stack: F[],
|
|
64
|
-
onSlot: (frame: F, slot: number) => void,
|
|
65
|
-
onLeave?: (frame: F) => void,
|
|
66
|
-
): void {
|
|
67
|
-
const frame = stack.at(-1);
|
|
68
|
-
|
|
69
|
-
if (frame === undefined) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (frame.cursor >= frame.level.items.length) {
|
|
74
|
-
stack.pop();
|
|
75
|
-
onLeave?.(frame);
|
|
76
|
-
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const slot = frame.cursor;
|
|
81
|
-
frame.cursor += 1;
|
|
82
|
-
onSlot(frame, slot);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function frameFor(options: WalkOptions, level: Level): WalkFrame {
|
|
86
|
-
return { level, cursor: 0, open: options.slots.get(level.path), marked: options.marks?.get(level.path) };
|
|
87
|
-
}
|
|
36
|
+
function walkLevel(options: WalkOptions, level: Level, state: WalkState): void {
|
|
37
|
+
const open = options.slots.get(level.path);
|
|
38
|
+
const marked = options.marks?.get(level.path);
|
|
88
39
|
|
|
89
|
-
|
|
90
|
-
|
|
40
|
+
for (const [slot, item] of level.items.entries()) {
|
|
41
|
+
const isOpen = open?.has(slot) ?? false;
|
|
42
|
+
options.visit?.({ item, position: state.position, isOpen, isMarked: marked?.has(slot) ?? false });
|
|
43
|
+
state.position += 1;
|
|
91
44
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
state.order.expandedPaths.push(child.path);
|
|
97
|
-
state.order.expandedIds.push(state.row.item.id);
|
|
98
|
-
state.stack.push(frameFor(state.options, child));
|
|
99
|
-
}
|
|
45
|
+
if (!isOpen) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
100
48
|
|
|
101
|
-
|
|
102
|
-
const item = frame.level.items[slot];
|
|
49
|
+
const child = options.index.childLevel(level, slot);
|
|
103
50
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
row.item = item;
|
|
110
|
-
row.isOpen = frame.open?.has(slot) ?? false;
|
|
111
|
-
row.isMarked = frame.marked?.has(slot) ?? false;
|
|
112
|
-
state.options.visit?.(row);
|
|
113
|
-
row.position += 1;
|
|
114
|
-
|
|
115
|
-
if (row.isOpen) {
|
|
116
|
-
descend(state, frame.level, slot);
|
|
51
|
+
if (child !== undefined) {
|
|
52
|
+
state.order.expandedPaths.push(child.path);
|
|
53
|
+
state.order.expandedIds.push(item.id);
|
|
54
|
+
walkLevel(options, child, state);
|
|
55
|
+
}
|
|
117
56
|
}
|
|
118
57
|
}
|
|
119
58
|
|
|
120
59
|
function walkVisible(options: WalkOptions): VisibleOrder {
|
|
121
|
-
const
|
|
122
|
-
const row: VisibleRow = { item: NO_ITEM, position: 0, isOpen: false, isMarked: false };
|
|
123
|
-
const frames = options.index.groups.map((level) => frameFor(options, level));
|
|
124
|
-
const state: WalkState = { options, stack: frames.toReversed(), row, order };
|
|
125
|
-
|
|
126
|
-
const onSlot = (frame: WalkFrame, slot: number): void => {
|
|
127
|
-
visitSlot(state, frame, slot);
|
|
128
|
-
};
|
|
60
|
+
const state: WalkState = { position: 0, order: { expandedPaths: [], expandedIds: [] } };
|
|
129
61
|
|
|
130
|
-
|
|
131
|
-
|
|
62
|
+
for (const level of options.index.groups) {
|
|
63
|
+
walkLevel(options, level, state);
|
|
132
64
|
}
|
|
133
65
|
|
|
134
|
-
return order;
|
|
66
|
+
return state.order;
|
|
135
67
|
}
|
|
136
68
|
|
|
137
|
-
function
|
|
138
|
-
const item
|
|
69
|
+
function expandLevel(index: CollectionIndex, level: Level, ids: Set<string>, paths: Set<string>): void {
|
|
70
|
+
for (const [slot, item] of level.items.entries()) {
|
|
71
|
+
if (!ids.has(item.id)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
139
74
|
|
|
140
|
-
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const child = walk.index.childLevel(frame.level, slot);
|
|
75
|
+
const child = index.childLevel(level, slot);
|
|
145
76
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
walk.paths.add(child.path);
|
|
151
|
-
walk.ancestors.add(item);
|
|
152
|
-
walk.stack.push({ level: child, cursor: 0, owner: item });
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function leaveOpenFrame(walk: OpenWalk, frame: OpenFrame): void {
|
|
156
|
-
if (frame.owner !== null) {
|
|
157
|
-
walk.ancestors.delete(frame.owner);
|
|
77
|
+
if (child !== undefined) {
|
|
78
|
+
paths.add(child.path);
|
|
79
|
+
expandLevel(index, child, ids, paths);
|
|
80
|
+
}
|
|
158
81
|
}
|
|
159
82
|
}
|
|
160
83
|
|
|
@@ -165,19 +88,8 @@ function expandedPathsFor(index: CollectionIndex, ids: Set<string>): Set<string>
|
|
|
165
88
|
return paths;
|
|
166
89
|
}
|
|
167
90
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const onSlot = (frame: OpenFrame, slot: number): void => {
|
|
172
|
-
openSlot(walk, frame, slot);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const onLeave = (frame: OpenFrame): void => {
|
|
176
|
-
leaveOpenFrame(walk, frame);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
while (walk.stack.length > 0) {
|
|
180
|
-
advanceStack(walk.stack, onSlot, onLeave);
|
|
91
|
+
for (const level of index.groups) {
|
|
92
|
+
expandLevel(index, level, ids, paths);
|
|
181
93
|
}
|
|
182
94
|
|
|
183
95
|
return paths;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
1
2
|
import type { ReactElement } from "react";
|
|
3
|
+
import { GtkFlattenListModel } from "@gtkx/jsx/gtk";
|
|
2
4
|
import { useLayoutEffect, useMemo, useState } from "react";
|
|
3
5
|
import type { ExpansionProps, ListItem, ListSection, SelectionProps } from "../types.js";
|
|
4
6
|
import type { Collection } from "./collection.js";
|
|
5
7
|
import { createCollectionIndex } from "./collection-index.js";
|
|
6
|
-
import { createCollectionModel } from "./collection-model.js";
|
|
8
|
+
import { CollectionRoot, type CollectionRootStore, createCollectionModel } from "./collection-model.js";
|
|
7
9
|
import { createCollection } from "./collection.js";
|
|
8
10
|
import { useExpansion } from "./expansion.js";
|
|
9
11
|
import { useSelection } from "./selection.js";
|
|
@@ -21,21 +23,39 @@ type CollectionResult = {
|
|
|
21
23
|
selection: ReactElement;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
type CollectionDataResult = {
|
|
27
|
+
collection: Collection;
|
|
28
|
+
model: ReactElement;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function useCollectionData(options: CollectionDataOptions): CollectionDataResult {
|
|
25
32
|
const { items, sections, isFlat } = options;
|
|
26
|
-
const [
|
|
33
|
+
const [gtkModel] = useState(createCollectionModel);
|
|
34
|
+
const [root, setRoot] = useState<CollectionRootStore | null>(null);
|
|
35
|
+
const [model, setModel] = useState<Gtk.FlattenListModel | null>(null);
|
|
27
36
|
const index = useMemo(() => createCollectionIndex(items, sections, isFlat === true), [items, sections, isFlat]);
|
|
28
|
-
const collection = useMemo(() => createCollection(
|
|
37
|
+
const collection = useMemo(() => createCollection(gtkModel, index, model), [gtkModel, index, model]);
|
|
29
38
|
|
|
30
39
|
useLayoutEffect(() => {
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
if (root !== null && model !== null) {
|
|
41
|
+
gtkModel.bind(root, model);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return gtkModel.clear;
|
|
45
|
+
}, [gtkModel, root, model]);
|
|
33
46
|
|
|
34
|
-
|
|
47
|
+
useLayoutEffect(() => {
|
|
48
|
+
gtkModel.sync(index);
|
|
49
|
+
}, [gtkModel, index, root, model]);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
collection,
|
|
53
|
+
model: <GtkFlattenListModel ref={setModel} model={<CollectionRoot ref={setRoot} />} />,
|
|
54
|
+
};
|
|
35
55
|
}
|
|
36
56
|
|
|
37
57
|
function useCollection(options: CollectionOptions): CollectionResult {
|
|
38
|
-
const collection = useCollectionData(options);
|
|
58
|
+
const { collection, model } = useCollectionData(options);
|
|
39
59
|
|
|
40
60
|
const onItemsChanged = useExpansion({
|
|
41
61
|
collection,
|
|
@@ -45,6 +65,7 @@ function useCollection(options: CollectionOptions): CollectionResult {
|
|
|
45
65
|
|
|
46
66
|
const selection = useSelection({
|
|
47
67
|
collection,
|
|
68
|
+
model,
|
|
48
69
|
selectedIds: options.selectedIds,
|
|
49
70
|
onSelectionChanged: options.onSelectionChanged,
|
|
50
71
|
selectionMode: options.selectionMode,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { GtkSignalListItemFactory } from "@gtkx/jsx/gtk";
|
|
4
|
+
import type { ListItemFactoryProps } from "./types.js";
|
|
5
|
+
import { NativeItemPortals, useItemCells } from "./internal/cells.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a Gtk.SignalListItemFactory that renders objects from an existing native model with React.
|
|
9
|
+
* Estimated dimensions keep cells stable until their content mounts.
|
|
10
|
+
*/
|
|
11
|
+
function ListItemFactory<T extends GObject.Object>({
|
|
12
|
+
renderItem,
|
|
13
|
+
estimatedItemHeight = -1,
|
|
14
|
+
estimatedItemWidth = -1,
|
|
15
|
+
}: ListItemFactoryProps<T>): ReactNode {
|
|
16
|
+
const size = { width: estimatedItemWidth, height: estimatedItemHeight };
|
|
17
|
+
const cells = useItemCells(size);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<GtkSignalListItemFactory {...cells.handlers} />
|
|
22
|
+
<NativeItemPortals registry={cells} render={renderItem} size={size} />
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { ListItemFactory };
|