@gtkx/components 2.0.0-beta.10 → 2.0.0-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -168
- package/dist/column-view.d.ts.map +1 -1
- package/dist/column-view.js +20 -10
- package/dist/column-view.js.map +1 -1
- package/dist/grid-view.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +10 -7
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +88 -51
- 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 +23 -2
- package/dist/internal/collection-model.d.ts.map +1 -1
- package/dist/internal/collection-model.js +118 -98
- 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 +44 -45
- 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/keys.d.ts.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 +1 -1
- package/dist/internal/tree-expansion.d.ts.map +1 -1
- package/dist/internal/tree-expansion.js +3 -12
- package/dist/internal/tree-expansion.js.map +1 -1
- package/dist/internal/tree-order.d.ts +1 -1
- package/dist/internal/tree-order.d.ts.map +1 -1
- package/dist/internal/tree-order.js +33 -77
- 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/internal/use-widget-ref.d.ts.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.js +1 -1
- package/dist/tab-collection.d.ts +9 -0
- package/dist/tab-collection.d.ts.map +1 -0
- package/dist/tab-collection.js +114 -0
- package/dist/tab-collection.js.map +1 -0
- 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 +67 -51
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +13 -21
- package/src/column-view.tsx +23 -12
- package/src/index.ts +7 -2
- package/src/internal/cells.tsx +155 -103
- package/src/internal/collection-index.ts +8 -6
- package/src/internal/collection-model.ts +156 -134
- package/src/internal/collection.ts +28 -24
- package/src/internal/controlled-sync.ts +9 -12
- package/src/internal/drop-down.tsx +63 -63
- package/src/internal/expansion.ts +4 -4
- package/src/internal/selection.tsx +8 -8
- package/src/internal/tree-expansion.ts +3 -23
- package/src/internal/tree-order.ts +34 -130
- package/src/internal/use-collection.tsx +29 -8
- package/src/list-item-factory.tsx +27 -0
- package/src/tab-collection.tsx +124 -0
- package/src/toast.tsx +4 -18
- package/src/types.ts +81 -55
|
@@ -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,27 @@ 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
|
-
|
|
60
|
-
}
|
|
58
|
+
const defaultRenderItem = ({ item }: ListItemRenderArgs<Primitive>): ReactNode =>
|
|
59
|
+
item == null ? null : <GtkLabel>{String(item)}</GtkLabel>;
|
|
60
|
+
const faceRenderer = (props: DropDownBaseProps): ListItemRenderer<never> => props.renderItem ?? defaultRenderItem;
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return String(value);
|
|
62
|
+
const resolvePosition = (widget: SelectableWidget, collection: Collection, selectedId: string | undefined): number => {
|
|
63
|
+
if (selectedId === undefined) {
|
|
64
|
+
return widget.getSelected();
|
|
66
65
|
}
|
|
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);
|
|
73
|
-
const faceRenderer = (props: DropDownBaseProps): ListItemRenderer<never> => props.renderItem ?? defaultRenderItem;
|
|
74
66
|
|
|
75
|
-
const
|
|
76
|
-
widget: SelectableWidget,
|
|
77
|
-
collection: Collection,
|
|
78
|
-
selectedId: string | null | undefined,
|
|
79
|
-
): number => {
|
|
80
|
-
const requested = selectedId == null ? -1 : collection.positionFor(selectedId);
|
|
67
|
+
const requested = collection.positionFor(selectedId);
|
|
81
68
|
|
|
82
69
|
return requested >= 0 ? requested : widget.getSelected();
|
|
83
70
|
};
|
|
84
71
|
|
|
85
72
|
const updateKnownSelection = (
|
|
86
73
|
tracker: KnownSelection,
|
|
87
|
-
effectiveId: string,
|
|
88
|
-
selectedId: string |
|
|
74
|
+
effectiveId: string | null,
|
|
75
|
+
selectedId: string | undefined,
|
|
89
76
|
): void => {
|
|
90
77
|
const { known, onSelectionChanged } = tracker;
|
|
91
78
|
const expectedId = selectedId ?? known.current;
|
|
@@ -100,7 +87,7 @@ const updateKnownSelection = (
|
|
|
100
87
|
const reportKnownSelection = (tracker: KnownSelection, id: string | null): void => {
|
|
101
88
|
const { known, onSelectionChanged } = tracker;
|
|
102
89
|
|
|
103
|
-
if (id ===
|
|
90
|
+
if (id === known.current) {
|
|
104
91
|
return;
|
|
105
92
|
}
|
|
106
93
|
|
|
@@ -108,14 +95,18 @@ const reportKnownSelection = (tracker: KnownSelection, id: string | null): void
|
|
|
108
95
|
onSelectionChanged?.(id);
|
|
109
96
|
};
|
|
110
97
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
98
|
+
const hasSelectionDrifted = (
|
|
99
|
+
widget: SelectableWidget,
|
|
100
|
+
collection: Collection,
|
|
101
|
+
selectedId: string | undefined,
|
|
102
|
+
): boolean => {
|
|
103
|
+
if (selectedId === undefined) {
|
|
104
|
+
return false;
|
|
116
105
|
}
|
|
117
106
|
|
|
118
|
-
|
|
107
|
+
const requested = collection.positionFor(selectedId);
|
|
108
|
+
|
|
109
|
+
return requested >= 0 && widget.getSelected() !== requested;
|
|
119
110
|
};
|
|
120
111
|
|
|
121
112
|
const applySelectedPosition = (widget: SelectableWidget, position: number, state: ApplyState): void => {
|
|
@@ -129,49 +120,58 @@ const applySelectedPosition = (widget: SelectableWidget, position: number, state
|
|
|
129
120
|
};
|
|
130
121
|
|
|
131
122
|
const dispatchSelectedNotify = (context: NotifyContext, value: number | null, self: SelectableWidget): void => {
|
|
132
|
-
const { options, known, state } = context;
|
|
123
|
+
const { options, known, state, markDrift } = context;
|
|
133
124
|
|
|
134
125
|
if (state.isApplying) {
|
|
135
126
|
return;
|
|
136
127
|
}
|
|
137
128
|
|
|
138
|
-
reportSelectedNotify(options, { known, onSelectionChanged: options.props.onSelectionChanged });
|
|
139
129
|
options.props.onNotifySelected?.(value, self);
|
|
130
|
+
|
|
131
|
+
if (!state.isReady || !isCollectionIdle(options.collection)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
reportKnownSelection(
|
|
136
|
+
{ known, onSelectionChanged: options.props.onSelectionChanged },
|
|
137
|
+
options.collection.idAt(self.getSelected()),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (hasSelectionDrifted(self, options.collection, options.props.selectedId)) {
|
|
141
|
+
markDrift();
|
|
142
|
+
}
|
|
140
143
|
};
|
|
141
144
|
|
|
142
145
|
const useDropDownSelection = (options: SelectionOptions): NotifySelectedHandler => {
|
|
143
146
|
const { widget, collection } = options;
|
|
144
147
|
const { selectedId } = options.props;
|
|
145
148
|
const known = useRef<string | null>(null);
|
|
146
|
-
const
|
|
149
|
+
const applyState = useRef<ApplyState>(newApplyState());
|
|
147
150
|
|
|
148
|
-
const syncKnownSelection = (position: number): void => {
|
|
151
|
+
const syncKnownSelection = (position: number, requestedId: string | undefined): void => {
|
|
149
152
|
const effectiveId = collection.idAt(position);
|
|
150
|
-
|
|
151
|
-
if (effectiveId === null) {
|
|
152
|
-
known.current = null;
|
|
153
|
-
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
153
|
const { onSelectionChanged } = options.props;
|
|
158
|
-
updateKnownSelection({ known, onSelectionChanged }, effectiveId,
|
|
154
|
+
updateKnownSelection({ known, onSelectionChanged }, effectiveId, requestedId);
|
|
155
|
+
applyState.current.isReady = true;
|
|
159
156
|
};
|
|
160
157
|
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
158
|
+
const markDrift = useControlledSync({
|
|
159
|
+
value: selectedId,
|
|
160
|
+
source: collection,
|
|
161
|
+
target: widget,
|
|
162
|
+
apply: (value) => {
|
|
163
|
+
if (widget === null) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const position = resolvePosition(widget, collection, value);
|
|
168
|
+
applySelectedPosition(widget, position, applyState.current);
|
|
169
|
+
syncKnownSelection(position, value);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
172
|
|
|
173
173
|
return (value, self) => {
|
|
174
|
-
dispatchSelectedNotify({ options, known, state: applyState }, value, self);
|
|
174
|
+
dispatchSelectedNotify({ options, known, state: applyState.current, markDrift }, value, self);
|
|
175
175
|
};
|
|
176
176
|
};
|
|
177
177
|
|
|
@@ -179,7 +179,7 @@ function DropDownBase(props: DropDownBaseProps & { component: ElementType }): Re
|
|
|
179
179
|
const { component: Component, items, sections, renderListItem, renderHeader, ref } = props;
|
|
180
180
|
const rest = omit(props, DROP_DOWN_PROPS);
|
|
181
181
|
const [widget, refCallback] = useWidgetRef<SelectableWidget>(ref);
|
|
182
|
-
const collection = useCollectionData({ items, sections, isFlat: true });
|
|
182
|
+
const { collection, model } = useCollectionData({ items, sections, isFlat: true });
|
|
183
183
|
const faceCells = useItemCells({ width: -1, height: -1 });
|
|
184
184
|
const listCells = useItemCells({ width: -1, height: -1 });
|
|
185
185
|
const header = useSectionHeader(renderHeader, collection, { width: -1, height: -1 });
|
|
@@ -189,7 +189,7 @@ function DropDownBase(props: DropDownBaseProps & { component: ElementType }): Re
|
|
|
189
189
|
<>
|
|
190
190
|
<Component
|
|
191
191
|
ref={refCallback}
|
|
192
|
-
model={
|
|
192
|
+
model={model}
|
|
193
193
|
factory={<GtkSignalListItemFactory {...faceCells.handlers} />}
|
|
194
194
|
{...(renderListItem != null && {
|
|
195
195
|
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
|
|
|
@@ -104,12 +92,4 @@ function adoptIndex(expansion: TreeExpansion, index: CollectionIndex): void {
|
|
|
104
92
|
expansion.order = null;
|
|
105
93
|
}
|
|
106
94
|
|
|
107
|
-
export {
|
|
108
|
-
adoptIndex,
|
|
109
|
-
adoptOrder,
|
|
110
|
-
createTreeExpansion,
|
|
111
|
-
markExpanded,
|
|
112
|
-
orderFor,
|
|
113
|
-
pruneSlots,
|
|
114
|
-
type TreeExpansion,
|
|
115
|
-
};
|
|
95
|
+
export { adoptIndex, adoptOrder, createTreeExpansion, markExpanded, orderFor, pruneSlots, type TreeExpansion };
|
|
@@ -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
|
-
|
|
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);
|
|
51
39
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
stack: OpenFrame[];
|
|
57
|
-
ancestors: Set<ListItem>;
|
|
58
|
-
};
|
|
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;
|
|
59
44
|
|
|
60
|
-
|
|
45
|
+
if (!isOpen) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
61
48
|
|
|
62
|
-
|
|
63
|
-
stack: F[],
|
|
64
|
-
onSlot: (frame: F, slot: number) => void,
|
|
65
|
-
onLeave?: (frame: F) => void,
|
|
66
|
-
): void {
|
|
67
|
-
const frame = stack.at(-1);
|
|
49
|
+
const child = options.index.childLevel(level, slot);
|
|
68
50
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
}
|
|
88
|
-
|
|
89
|
-
function descend(state: WalkState, level: Level, slot: number): void {
|
|
90
|
-
const child = state.options.index.childLevel(level, slot);
|
|
91
|
-
|
|
92
|
-
if (child === undefined) {
|
|
93
|
-
return;
|
|
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
|
-
}
|
|
100
|
-
|
|
101
|
-
function visitSlot(state: WalkState, frame: WalkFrame, slot: number): void {
|
|
102
|
-
const item = frame.level.items[slot];
|
|
103
|
-
|
|
104
|
-
if (item === undefined) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const { row } = state;
|
|
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 };
|
|
60
|
+
const state: WalkState = { position: 0, order: { expandedPaths: [], expandedIds: [] } };
|
|
125
61
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
while (state.stack.length > 0) {
|
|
131
|
-
advanceStack(state.stack, onSlot);
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const child = walk.index.childLevel(frame.level, slot);
|
|
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
|
+
}
|
|
145
74
|
|
|
146
|
-
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
walk.paths.add(child.path);
|
|
151
|
-
walk.ancestors.add(item);
|
|
152
|
-
walk.stack.push({ level: child, cursor: 0, owner: item });
|
|
153
|
-
}
|
|
75
|
+
const child = index.childLevel(level, slot);
|
|
154
76
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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;
|
|
@@ -221,12 +133,4 @@ function findIds(index: CollectionIndex, slots: SlotMap, positions: Set<number>)
|
|
|
221
133
|
return found;
|
|
222
134
|
}
|
|
223
135
|
|
|
224
|
-
export {
|
|
225
|
-
expandedPathsFor,
|
|
226
|
-
findIds,
|
|
227
|
-
findRows,
|
|
228
|
-
walkVisible,
|
|
229
|
-
type MatchedRows,
|
|
230
|
-
type VisibleOrder,
|
|
231
|
-
type WalkOptions,
|
|
232
|
-
};
|
|
136
|
+
export { expandedPathsFor, findIds, findRows, walkVisible, type MatchedRows, type VisibleOrder, type WalkOptions };
|
|
@@ -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 };
|