@gtkx/components 1.0.0-rc.2 → 1.0.0-rc.4
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 +4 -5
- package/dist/adw/combo-row.d.ts +12 -0
- package/dist/adw/combo-row.d.ts.map +1 -0
- package/dist/adw/combo-row.js +14 -0
- package/dist/adw/combo-row.js.map +1 -0
- package/dist/adw/index.d.ts +2 -1
- package/dist/adw/index.d.ts.map +1 -1
- package/dist/adw/index.js +1 -0
- package/dist/adw/index.js.map +1 -1
- package/dist/adw/types.d.ts +11 -3
- package/dist/adw/types.d.ts.map +1 -1
- package/dist/adw/types.js.map +1 -1
- package/dist/column-view.d.ts.map +1 -1
- package/dist/column-view.js +4 -4
- package/dist/column-view.js.map +1 -1
- package/dist/drop-down.d.ts +4 -5
- package/dist/drop-down.d.ts.map +1 -1
- package/dist/drop-down.js +5 -112
- package/dist/drop-down.js.map +1 -1
- package/dist/grid-view.js +1 -1
- package/dist/grid-view.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +3 -3
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/collection-index.d.ts +5 -5
- 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.map +1 -1
- package/dist/internal/collection-model.js +2 -5
- package/dist/internal/collection-model.js.map +1 -1
- package/dist/internal/collection.d.ts +2 -2
- package/dist/internal/collection.d.ts.map +1 -1
- package/dist/internal/collection.js.map +1 -1
- package/dist/internal/drop-down.d.ts +16 -0
- package/dist/internal/drop-down.d.ts.map +1 -0
- package/dist/internal/drop-down.js +110 -0
- package/dist/internal/drop-down.js.map +1 -0
- package/dist/internal/expansion.d.ts +1 -1
- package/dist/internal/expansion.d.ts.map +1 -1
- package/dist/internal/expansion.js +3 -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 +1 -0
- package/dist/internal/selection.js.map +1 -1
- package/dist/internal/use-collection.d.ts +5 -6
- package/dist/internal/use-collection.d.ts.map +1 -1
- package/dist/internal/use-collection.js +10 -18
- package/dist/internal/use-collection.js.map +1 -1
- package/dist/types.d.ts +70 -48
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -7
- package/src/adw/combo-row.tsx +21 -0
- package/src/adw/index.ts +8 -1
- package/src/adw/types.ts +17 -3
- package/src/column-view.tsx +13 -13
- package/src/drop-down.tsx +10 -195
- package/src/grid-view.tsx +1 -1
- package/src/index.ts +7 -12
- package/src/internal/cells.tsx +12 -12
- package/src/internal/collection-index.ts +16 -16
- package/src/internal/collection-model.ts +2 -5
- package/src/internal/collection.ts +2 -2
- package/src/internal/drop-down.tsx +186 -0
- package/src/internal/expansion.ts +5 -6
- package/src/internal/selection.tsx +3 -0
- package/src/internal/use-collection.tsx +16 -29
- package/src/types.ts +83 -66
- package/dist/size-group.d.ts +0 -12
- package/dist/size-group.d.ts.map +0 -1
- package/dist/size-group.js +0 -41
- package/dist/size-group.js.map +0 -1
- package/src/size-group.tsx +0 -77
package/src/column-view.tsx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
1
|
+
import type { ReactNode, RefObject } from "react";
|
|
2
2
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
3
3
|
import { GtkColumnView, GtkColumnViewColumn, GtkCustomSorter, GtkSignalListItemFactory } from "@gtkx/jsx/gtk";
|
|
4
|
-
import {
|
|
4
|
+
import { useSignal } from "@gtkx/react";
|
|
5
5
|
import { omit } from "@gtkx/utils";
|
|
6
6
|
import { useLayoutEffect, useRef } from "react";
|
|
7
7
|
import type { CellSize } from "./internal/cells.js";
|
|
8
8
|
import type { Collection } from "./internal/collection.js";
|
|
9
|
-
import type {
|
|
9
|
+
import type { ColumnViewColumn, ColumnViewProps } from "./types.js";
|
|
10
10
|
import { HeaderPortals, ItemPortals, useHeaderCells, useItemCells } from "./internal/cells.js";
|
|
11
11
|
import { useCollection } from "./internal/use-collection.js";
|
|
12
12
|
import { useWidgetRef } from "./internal/use-widget-ref.js";
|
|
@@ -20,14 +20,14 @@ type SortProps = {
|
|
|
20
20
|
type SortTarget = { view: Gtk.ColumnView; column: Gtk.ColumnViewColumn | null };
|
|
21
21
|
|
|
22
22
|
type ColumnCellsProps = {
|
|
23
|
-
column:
|
|
23
|
+
column: ColumnViewColumn<never>;
|
|
24
24
|
collection: Collection;
|
|
25
25
|
expandedIds: string[] | null | undefined;
|
|
26
26
|
size: CellSize;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
type ColumnListProps = Omit<ColumnCellsProps, "column"> & {
|
|
30
|
-
columns:
|
|
30
|
+
columns: ColumnViewColumn<never>[];
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const COLUMN_VIEW_PROPS = [
|
|
@@ -77,7 +77,7 @@ const getSortTarget = (view: Gtk.ColumnView | null, sortColumn: string | null |
|
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
const applySort = (
|
|
80
|
-
sorting:
|
|
80
|
+
sorting: RefObject<boolean>,
|
|
81
81
|
view: Gtk.ColumnView,
|
|
82
82
|
column: Gtk.ColumnViewColumn | null,
|
|
83
83
|
order: Gtk.SortType | null | undefined,
|
|
@@ -93,7 +93,7 @@ const applySort = (
|
|
|
93
93
|
|
|
94
94
|
const emitSortChanged = (
|
|
95
95
|
sorter: Gtk.ColumnViewSorter | null,
|
|
96
|
-
sorting:
|
|
96
|
+
sorting: RefObject<boolean>,
|
|
97
97
|
sort: SortProps,
|
|
98
98
|
): void => {
|
|
99
99
|
if (sorter === null || sorting.current) {
|
|
@@ -104,7 +104,7 @@ const emitSortChanged = (
|
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
const syncSort = (
|
|
107
|
-
sorting:
|
|
107
|
+
sorting: RefObject<boolean>,
|
|
108
108
|
view: Gtk.ColumnView | null,
|
|
109
109
|
sortColumn: string | null | undefined,
|
|
110
110
|
sortOrder: Gtk.SortType | null | undefined,
|
|
@@ -118,9 +118,9 @@ const syncSort = (
|
|
|
118
118
|
applySort(sorting, target.view, target.column, sortOrder);
|
|
119
119
|
};
|
|
120
120
|
|
|
121
|
-
const useColumnSorting = (view: Gtk.ColumnView | null, sort: SortProps, columns:
|
|
121
|
+
const useColumnSorting = (view: Gtk.ColumnView | null, sort: SortProps, columns: ColumnViewColumn<never>[]): void => {
|
|
122
122
|
const sorting = useRef(false);
|
|
123
|
-
const sorter =
|
|
123
|
+
const sorter = view?.getSorter() ?? null;
|
|
124
124
|
const columnSorter = sorter instanceof Gtk.ColumnViewSorter ? sorter : null;
|
|
125
125
|
|
|
126
126
|
useSignal(columnSorter, "changed", (): void => {
|
|
@@ -136,14 +136,14 @@ const useColumnSorting = (view: Gtk.ColumnView | null, sort: SortProps, columns:
|
|
|
136
136
|
|
|
137
137
|
const ColumnCells = ({ column, collection, expandedIds, size }: ColumnCellsProps): ReactNode => {
|
|
138
138
|
const cells = useItemCells(size);
|
|
139
|
-
const rest = omit(column, ["id", "renderCell", "
|
|
139
|
+
const rest = omit(column, ["id", "renderCell", "isSortable"]);
|
|
140
140
|
|
|
141
141
|
return (
|
|
142
142
|
<>
|
|
143
143
|
<GtkColumnViewColumn
|
|
144
144
|
id={column.id}
|
|
145
145
|
factory={<GtkSignalListItemFactory {...cells.handlers} />}
|
|
146
|
-
sorter={column.
|
|
146
|
+
sorter={column.isSortable === true ? <GtkCustomSorter /> : null}
|
|
147
147
|
{...rest}
|
|
148
148
|
/>
|
|
149
149
|
<ItemPortals
|
|
@@ -181,7 +181,7 @@ function ColumnView<T = unknown, S = unknown>(props: ColumnViewProps<T, S>): Rea
|
|
|
181
181
|
const size = { width: -1, height: estimatedItemHeight ?? -1 };
|
|
182
182
|
const { collection, selection } = useCollection(props);
|
|
183
183
|
const headerCells = useHeaderCells(size);
|
|
184
|
-
const columnList = columns as
|
|
184
|
+
const columnList = columns as ColumnViewColumn<never>[];
|
|
185
185
|
useColumnSorting(view, { sortColumn, sortOrder, onSortChanged }, columnList);
|
|
186
186
|
|
|
187
187
|
return (
|
package/src/drop-down.tsx
CHANGED
|
@@ -1,205 +1,20 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { useEffectEvent, useLayoutEffect, useRef } from "react";
|
|
7
|
-
import type { Collection } from "./internal/collection.js";
|
|
8
|
-
import type { DropDownProps, ItemRenderer, RenderItemArgs } from "./types.js";
|
|
9
|
-
import { HeaderPortals, ItemPortals, useHeaderCells, useItemCells } from "./internal/cells.js";
|
|
10
|
-
import { useCollectionData } from "./internal/use-collection.js";
|
|
11
|
-
import { useWidgetRef } from "./internal/use-widget-ref.js";
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { GtkDropDown } from "@gtkx/jsx/gtk";
|
|
3
|
+
import type { DropDownBaseProps } from "./internal/drop-down.js";
|
|
4
|
+
import type { DropDownProps } from "./types.js";
|
|
5
|
+
import { DropDownBase } from "./internal/drop-down.js";
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type DropDownRuntimeProps = DropDownProps & {
|
|
16
|
-
component?: ElementType | undefined;
|
|
17
|
-
ref?: Ref<SelectableWidget | null> | undefined;
|
|
18
|
-
} & Record<string, unknown>;
|
|
19
|
-
|
|
20
|
-
type SelectionOptions = {
|
|
21
|
-
widget: SelectableWidget | null;
|
|
22
|
-
collection: Collection;
|
|
23
|
-
applying: RefObject<boolean>;
|
|
24
|
-
props: DropDownRuntimeProps;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
type KnownSelection = {
|
|
28
|
-
known: { current: string | null };
|
|
29
|
-
onSelectionChanged: ((id: string) => void) | null | undefined;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
type DropDownComponent = <T = unknown, S = unknown, C extends ElementType = typeof GtkDropDown>(
|
|
33
|
-
props: DropDownProps<T, S, C>,
|
|
34
|
-
) => ReactNode;
|
|
35
|
-
|
|
36
|
-
const DROP_DOWN_PROPS: string[] = [
|
|
37
|
-
"component",
|
|
38
|
-
"items",
|
|
39
|
-
"sections",
|
|
40
|
-
"selectedId",
|
|
41
|
-
"onSelectionChanged",
|
|
42
|
-
"renderItem",
|
|
43
|
-
"renderListItem",
|
|
44
|
-
"renderHeader",
|
|
45
|
-
"ref",
|
|
46
|
-
];
|
|
7
|
+
/** Call signature of {@link DropDown}, generic in the item and section value types. */
|
|
8
|
+
type DropDownComponent = <T = unknown, S = unknown>(props: DropDownProps<T, S>) => ReactNode;
|
|
47
9
|
|
|
48
10
|
/**
|
|
49
|
-
* Renders a
|
|
11
|
+
* Renders a `Gtk.DropDown` backed by a collection model, with customizable renderers for the
|
|
50
12
|
* collapsed display, popup rows, and popup section headers, and controlled selection.
|
|
51
|
-
* The backing widget defaults to GtkDropDown and is swappable through `component`.
|
|
52
13
|
*/
|
|
53
14
|
const DropDown: DropDownComponent = DropDownImpl as DropDownComponent;
|
|
54
15
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return value;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
return JSON.stringify(value);
|
|
62
|
-
} catch {
|
|
63
|
-
return String(value);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const defaultItemContent = (value: unknown): ReactNode =>
|
|
68
|
-
value == null ? null : <GtkLabel>{describeValue(value)}</GtkLabel>;
|
|
69
|
-
|
|
70
|
-
const defaultRenderItem = ({ item }: RenderItemArgs<unknown>): ReactNode => defaultItemContent(item);
|
|
71
|
-
|
|
72
|
-
const faceRenderer = (props: DropDownRuntimeProps): ItemRenderer<never> =>
|
|
73
|
-
props.renderItem ?? defaultRenderItem;
|
|
74
|
-
|
|
75
|
-
const listRenderer = (props: DropDownRuntimeProps): ItemRenderer<never> =>
|
|
76
|
-
props.renderListItem ?? props.renderItem ?? defaultRenderItem;
|
|
77
|
-
|
|
78
|
-
const resolvePosition = (
|
|
79
|
-
widget: SelectableWidget,
|
|
80
|
-
collection: Collection,
|
|
81
|
-
selectedId: string | null | undefined,
|
|
82
|
-
): number => {
|
|
83
|
-
const requested = selectedId == null ? -1 : collection.positionFor(selectedId);
|
|
84
|
-
|
|
85
|
-
return requested >= 0 ? requested : widget.getSelected();
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const updateKnownSelection = (
|
|
89
|
-
tracker: KnownSelection,
|
|
90
|
-
effectiveId: string,
|
|
91
|
-
selectedId: string | null | undefined,
|
|
92
|
-
): void => {
|
|
93
|
-
const { known, onSelectionChanged } = tracker;
|
|
94
|
-
const expectedId = selectedId ?? known.current;
|
|
95
|
-
const isNew = effectiveId !== known.current;
|
|
96
|
-
known.current = effectiveId;
|
|
97
|
-
|
|
98
|
-
if (expectedId !== null && effectiveId !== expectedId && isNew) {
|
|
99
|
-
onSelectionChanged?.(effectiveId);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const reportKnownSelection = (tracker: KnownSelection, id: string | null): void => {
|
|
104
|
-
const { known, onSelectionChanged } = tracker;
|
|
105
|
-
|
|
106
|
-
if (id === null || id === known.current) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
known.current = id;
|
|
111
|
-
onSelectionChanged?.(id);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const reportSelectedNotify = (
|
|
115
|
-
options: SelectionOptions,
|
|
116
|
-
tracker: KnownSelection,
|
|
117
|
-
applying: RefObject<boolean>,
|
|
118
|
-
): void => {
|
|
119
|
-
const { widget, collection } = options;
|
|
120
|
-
|
|
121
|
-
if (widget === null || applying.current) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
reportKnownSelection(tracker, collection.idAt(widget.getSelected()));
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const applySelectedPosition = (widget: SelectableWidget, position: number, applying: RefObject<boolean>): void => {
|
|
129
|
-
applying.current = true;
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
widget.selected = position;
|
|
133
|
-
} finally {
|
|
134
|
-
applying.current = false;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const useDropDownSelection = (options: SelectionOptions): void => {
|
|
139
|
-
const { widget, collection, applying } = options;
|
|
140
|
-
const { selectedId } = options.props;
|
|
141
|
-
const known = useRef<string | null>(null);
|
|
142
|
-
|
|
143
|
-
const syncKnownSelection = useEffectEvent((position: number): void => {
|
|
144
|
-
const effectiveId = collection.idAt(position);
|
|
145
|
-
|
|
146
|
-
if (effectiveId === null) {
|
|
147
|
-
known.current = null;
|
|
148
|
-
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const { onSelectionChanged } = options.props;
|
|
153
|
-
updateKnownSelection({ known, onSelectionChanged }, effectiveId, selectedId);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
useLayoutEffect(() => {
|
|
157
|
-
if (widget === null) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const position = resolvePosition(widget, collection, selectedId);
|
|
162
|
-
applySelectedPosition(widget, position, applying);
|
|
163
|
-
syncKnownSelection(position);
|
|
164
|
-
}, [widget, collection, selectedId, applying]);
|
|
165
|
-
|
|
166
|
-
useSignal(widget, "notify::selected", (): void => {
|
|
167
|
-
reportSelectedNotify(options, { known, onSelectionChanged: options.props.onSelectionChanged }, applying);
|
|
168
|
-
});
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
function DropDownImpl(props: DropDownRuntimeProps): ReactNode {
|
|
172
|
-
const { component, items, sections, renderListItem, renderHeader, ref } = props;
|
|
173
|
-
const rest = omit(props, DROP_DOWN_PROPS);
|
|
174
|
-
const [widget, refCallback] = useWidgetRef<SelectableWidget>(ref);
|
|
175
|
-
const applying = useRef(false);
|
|
176
|
-
const collection = useCollectionData({ items, sections, applying });
|
|
177
|
-
const faceCells = useItemCells({ width: -1, height: -1 });
|
|
178
|
-
const listCells = useItemCells({ width: -1, height: -1 });
|
|
179
|
-
const headerCells = useHeaderCells({ width: -1, height: -1 });
|
|
180
|
-
useDropDownSelection({ widget, collection, applying, props });
|
|
181
|
-
const Component = component ?? GtkDropDown;
|
|
182
|
-
const hasHeader = typeof renderHeader === "function";
|
|
183
|
-
|
|
184
|
-
return (
|
|
185
|
-
<>
|
|
186
|
-
<Component
|
|
187
|
-
ref={refCallback}
|
|
188
|
-
model={collection.model}
|
|
189
|
-
factory={<GtkSignalListItemFactory {...faceCells.handlers} />}
|
|
190
|
-
{...(renderListItem != null && {
|
|
191
|
-
listFactory: <GtkSignalListItemFactory {...listCells.handlers} />,
|
|
192
|
-
})}
|
|
193
|
-
{...(hasHeader && { headerFactory: <GtkSignalListItemFactory {...headerCells.handlers} /> })}
|
|
194
|
-
{...rest}
|
|
195
|
-
/>
|
|
196
|
-
<ItemPortals store={faceCells} render={faceRenderer(props)} collection={collection} />
|
|
197
|
-
{renderListItem != null && (
|
|
198
|
-
<ItemPortals store={listCells} render={listRenderer(props)} collection={collection} />
|
|
199
|
-
)}
|
|
200
|
-
{hasHeader && <HeaderPortals store={headerCells} render={renderHeader} collection={collection} />}
|
|
201
|
-
</>
|
|
202
|
-
);
|
|
16
|
+
function DropDownImpl(props: DropDownBaseProps): ReactNode {
|
|
17
|
+
return <DropDownBase {...props} component={GtkDropDown} />;
|
|
203
18
|
}
|
|
204
19
|
|
|
205
20
|
export { DropDown };
|
package/src/grid-view.tsx
CHANGED
|
@@ -23,7 +23,7 @@ function GridView<T = unknown>(props: GridViewProps<T>): ReactNode {
|
|
|
23
23
|
const { renderItem, estimatedItemHeight, estimatedItemWidth } = props;
|
|
24
24
|
const rest = omit(props, GRID_VIEW_PROPS);
|
|
25
25
|
const size = { width: estimatedItemWidth ?? -1, height: estimatedItemHeight ?? -1 };
|
|
26
|
-
const { collection, selection } = useCollection({ ...props,
|
|
26
|
+
const { collection, selection } = useCollection({ ...props, isFlat: true });
|
|
27
27
|
const itemCells = useItemCells(size);
|
|
28
28
|
|
|
29
29
|
return (
|
package/src/index.ts
CHANGED
|
@@ -2,21 +2,16 @@ export { ColumnView } from "./column-view.js";
|
|
|
2
2
|
export { DropDown } from "./drop-down.js";
|
|
3
3
|
export { GridView } from "./grid-view.js";
|
|
4
4
|
export { ListView } from "./list-view.js";
|
|
5
|
-
export { SizeGroup } from "./size-group.js";
|
|
6
5
|
export type {
|
|
7
|
-
|
|
8
|
-
Column,
|
|
6
|
+
ColumnViewColumn,
|
|
9
7
|
ColumnViewProps,
|
|
10
8
|
DropDownProps,
|
|
11
9
|
GridViewProps,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
ListItem,
|
|
11
|
+
ListItemRenderArgs,
|
|
12
|
+
ListItemRenderer,
|
|
13
|
+
ListSection,
|
|
14
|
+
ListSectionRenderArgs,
|
|
15
|
+
ListSectionRenderer,
|
|
15
16
|
ListViewProps,
|
|
16
|
-
RenderHeaderArgs,
|
|
17
|
-
RenderItemArgs,
|
|
18
|
-
Section,
|
|
19
|
-
SizeGroupChildProps,
|
|
20
|
-
SizeGroupProps,
|
|
21
|
-
WidgetProps,
|
|
22
17
|
} from "./types.js";
|
package/src/internal/cells.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import * as Gtk from "@gtkx/gi/gtk";
|
|
|
4
4
|
import { GtkTreeExpander } from "@gtkx/jsx/gtk";
|
|
5
5
|
import { createPortal, useProperty } from "@gtkx/react";
|
|
6
6
|
import { memo, useLayoutEffect, useState, useSyncExternalStore } from "react";
|
|
7
|
-
import type {
|
|
7
|
+
import type { ListItem, ListItemRenderArgs, ListItemRenderer, ListSectionRenderer } from "../types.js";
|
|
8
8
|
import type { Collection } from "./collection.js";
|
|
9
9
|
import { getId } from "./collection-model.js";
|
|
10
10
|
|
|
@@ -56,34 +56,34 @@ type ItemBodyOptions = {
|
|
|
56
56
|
position: number | undefined;
|
|
57
57
|
row: Gtk.TreeListRow | null;
|
|
58
58
|
isRowExpanded: boolean | undefined;
|
|
59
|
-
render:
|
|
59
|
+
render: ListItemRenderer<never>;
|
|
60
60
|
collection: Collection;
|
|
61
61
|
expandedIds: string[] | null | undefined;
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
type ItemCellProps = {
|
|
65
65
|
entry: CellEntry<Gtk.ListItem>;
|
|
66
|
-
render:
|
|
66
|
+
render: ListItemRenderer<never>;
|
|
67
67
|
collection: Collection;
|
|
68
68
|
expandedIds: string[] | null | undefined;
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
type ItemPortalsProps = {
|
|
72
72
|
store: CellStore<Gtk.ListItem>;
|
|
73
|
-
render:
|
|
73
|
+
render: ListItemRenderer<never>;
|
|
74
74
|
collection: Collection;
|
|
75
75
|
expandedIds?: string[] | null | undefined;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
type HeaderCellProps = {
|
|
79
79
|
entry: CellEntry<Gtk.ListHeader>;
|
|
80
|
-
render:
|
|
80
|
+
render: ListSectionRenderer<never>;
|
|
81
81
|
collection: Collection;
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
type HeaderPortalsProps = {
|
|
85
85
|
store: CellStore<Gtk.ListHeader>;
|
|
86
|
-
render:
|
|
86
|
+
render: ListSectionRenderer<never>;
|
|
87
87
|
collection: Collection;
|
|
88
88
|
};
|
|
89
89
|
|
|
@@ -246,14 +246,14 @@ function useHeaderCells(size: CellSize): CellStore<Gtk.ListHeader> {
|
|
|
246
246
|
return useCellStore(isListHeader, size);
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
function isRowWanted(options: ItemBodyOptions, item:
|
|
249
|
+
function isRowWanted(options: ItemBodyOptions, item: ListItem): boolean {
|
|
250
250
|
const { expandedIds } = options;
|
|
251
251
|
|
|
252
252
|
return expandedIds == null ? options.isRowExpanded === true : expandedIds.includes(item.id);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
function itemArgs(item:
|
|
256
|
-
const args:
|
|
255
|
+
function itemArgs(item: ListItem, options: ItemBodyOptions): ListItemRenderArgs<unknown> {
|
|
256
|
+
const args: ListItemRenderArgs<unknown> = { item: item.value, index: options.position ?? 0 };
|
|
257
257
|
const row = options.row;
|
|
258
258
|
|
|
259
259
|
if (row === null) {
|
|
@@ -269,7 +269,7 @@ function itemArgs(item: Item, options: ItemBodyOptions): RenderItemArgs<unknown>
|
|
|
269
269
|
return args;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
function wrapExpander(item:
|
|
272
|
+
function wrapExpander(item: ListItem, row: Gtk.TreeListRow | null, content: ReactNode): ReactNode {
|
|
273
273
|
const expander: ReactNode =
|
|
274
274
|
row === null
|
|
275
275
|
? (
|
|
@@ -297,7 +297,7 @@ function itemBody(options: ItemBodyOptions): ReactNode {
|
|
|
297
297
|
return null;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
const render = options.render as
|
|
300
|
+
const render = options.render as ListItemRenderer<unknown>;
|
|
301
301
|
|
|
302
302
|
return wrapExpander(item, options.row, render(itemArgs(item, options)));
|
|
303
303
|
}
|
|
@@ -315,7 +315,7 @@ function ItemCellImpl({ entry, render, collection, expandedIds }: ItemCellProps)
|
|
|
315
315
|
function HeaderCellImpl({ entry, render, collection }: HeaderCellProps): ReactNode {
|
|
316
316
|
const item = useSyncExternalStore(entry.subscribe, entry.getItem);
|
|
317
317
|
const id = getId(item);
|
|
318
|
-
const renderHeader = render as
|
|
318
|
+
const renderHeader = render as ListSectionRenderer<unknown>;
|
|
319
319
|
const body = id === null ? null : renderHeader({ section: collection.sectionFor(id) });
|
|
320
320
|
|
|
321
321
|
return createPortal(body, entry.cell, entry.key);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ListItem, ListSection } from "../types.js";
|
|
2
2
|
|
|
3
3
|
type Level = {
|
|
4
4
|
key: string;
|
|
5
5
|
ids: string[];
|
|
6
|
-
|
|
6
|
+
expandableFlags: boolean[];
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
type CollectionIndex = {
|
|
@@ -11,8 +11,8 @@ type CollectionIndex = {
|
|
|
11
11
|
size: number;
|
|
12
12
|
groups: Level[];
|
|
13
13
|
children: Map<string, Level>;
|
|
14
|
-
|
|
15
|
-
itemFor: (id: string) =>
|
|
14
|
+
hasId: (id: string) => boolean;
|
|
15
|
+
itemFor: (id: string) => ListItem | undefined;
|
|
16
16
|
sectionFor: (id: string) => unknown;
|
|
17
17
|
positionFor: (id: string) => number;
|
|
18
18
|
};
|
|
@@ -21,24 +21,24 @@ type IndexState = {
|
|
|
21
21
|
isTree: boolean;
|
|
22
22
|
groups: Level[];
|
|
23
23
|
children: Map<string, Level>;
|
|
24
|
-
itemsById: Map<string,
|
|
25
|
-
sections:
|
|
24
|
+
itemsById: Map<string, ListItem>;
|
|
25
|
+
sections: ListSection[] | undefined;
|
|
26
26
|
positions: Map<string, number> | null;
|
|
27
27
|
starts: number[] | null;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
const ROOT_LEVEL_KEY = "";
|
|
31
31
|
|
|
32
|
-
const hasChildren = (item:
|
|
32
|
+
const hasChildren = (item: ListItem): boolean => item.children !== undefined && item.children.length > 0;
|
|
33
33
|
const childLevelKey = (id: string): string => `child:${id}`;
|
|
34
34
|
|
|
35
|
-
function collectChildren(state: IndexState, level: Level, item:
|
|
35
|
+
function collectChildren(state: IndexState, level: Level, item: ListItem): void {
|
|
36
36
|
if (!state.isTree) {
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const canExpand = hasChildren(item);
|
|
41
|
-
level.
|
|
41
|
+
level.expandableFlags.push(canExpand);
|
|
42
42
|
|
|
43
43
|
if (!canExpand) {
|
|
44
44
|
return;
|
|
@@ -48,8 +48,8 @@ function collectChildren(state: IndexState, level: Level, item: Item): void {
|
|
|
48
48
|
state.children.set(key, collectLevel(state, key, item.children ?? []));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function collectLevel(state: IndexState, key: string, items:
|
|
52
|
-
const level: Level = { key, ids: [],
|
|
51
|
+
function collectLevel(state: IndexState, key: string, items: ListItem[]): Level {
|
|
52
|
+
const level: Level = { key, ids: [], expandableFlags: [] };
|
|
53
53
|
|
|
54
54
|
for (const item of items) {
|
|
55
55
|
level.ids.push(item.id);
|
|
@@ -118,7 +118,7 @@ function sectionFor(state: IndexState, id: string): unknown {
|
|
|
118
118
|
return sections[sectionAt(state.starts, position)]?.value;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
function buildGroups(state: IndexState, source:
|
|
121
|
+
function buildGroups(state: IndexState, source: ListItem[], sections: ListSection[] | undefined): Level[] {
|
|
122
122
|
if (sections === undefined) {
|
|
123
123
|
return [collectLevel(state, ROOT_LEVEL_KEY, source)];
|
|
124
124
|
}
|
|
@@ -126,7 +126,7 @@ function buildGroups(state: IndexState, source: Item[], sections: Section[] | un
|
|
|
126
126
|
return sections.map((section) => collectLevel(state, section.id, section.data));
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
function isTreeSource(source:
|
|
129
|
+
function isTreeSource(source: ListItem[], sections: ListSection[] | undefined, isFlat: boolean): boolean {
|
|
130
130
|
if (sections !== undefined || isFlat) {
|
|
131
131
|
return false;
|
|
132
132
|
}
|
|
@@ -135,8 +135,8 @@ function isTreeSource(source: Item[], sections: Section[] | undefined, isFlat: b
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
function createCollectionIndex(
|
|
138
|
-
items:
|
|
139
|
-
sections:
|
|
138
|
+
items: ListItem[] | undefined,
|
|
139
|
+
sections: ListSection[] | undefined,
|
|
140
140
|
isFlat: boolean,
|
|
141
141
|
): CollectionIndex {
|
|
142
142
|
const source = items ?? [];
|
|
@@ -158,7 +158,7 @@ function createCollectionIndex(
|
|
|
158
158
|
size: state.itemsById.size,
|
|
159
159
|
groups: state.groups,
|
|
160
160
|
children: state.children,
|
|
161
|
-
|
|
161
|
+
hasId: (id) => state.itemsById.has(id),
|
|
162
162
|
itemFor: (id) => state.itemsById.get(id),
|
|
163
163
|
sectionFor: (id) => sectionFor(state, id),
|
|
164
164
|
positionFor: (id) => positionFor(state, id),
|
|
@@ -104,7 +104,7 @@ function refreshExpandable(current: LevelState, level: Level): void {
|
|
|
104
104
|
const next: Map<string, boolean> = new Map();
|
|
105
105
|
|
|
106
106
|
for (const [index, id] of level.ids.entries()) {
|
|
107
|
-
const isWanted = level.
|
|
107
|
+
const isWanted = level.expandableFlags[index] ?? false;
|
|
108
108
|
const previous = current.canExpand.get(id);
|
|
109
109
|
next.set(id, isWanted);
|
|
110
110
|
|
|
@@ -138,16 +138,13 @@ function dropUnvisited(state: ModelState, visited: Set<string>): void {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
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
141
|
function pruneObjects(state: ModelState, index: CollectionIndex): void {
|
|
145
142
|
if (state.objects.size <= OBJECT_CACHE_MAX) {
|
|
146
143
|
return;
|
|
147
144
|
}
|
|
148
145
|
|
|
149
146
|
for (const id of state.objects.keys()) {
|
|
150
|
-
if (!index.
|
|
147
|
+
if (!index.hasId(id)) {
|
|
151
148
|
state.objects.delete(id);
|
|
152
149
|
}
|
|
153
150
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
2
|
import type * as Gtk from "@gtkx/gi/gtk";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ListItem } from "../types.js";
|
|
4
4
|
import type { CollectionIndex } from "./collection-index.js";
|
|
5
5
|
import type { CollectionModel } from "./collection-model.js";
|
|
6
6
|
import { getId } from "./collection-model.js";
|
|
@@ -9,7 +9,7 @@ type Collection = {
|
|
|
9
9
|
isTree: boolean;
|
|
10
10
|
model: Gtk.FlattenListModel;
|
|
11
11
|
treeModel: () => Gtk.TreeListModel | null;
|
|
12
|
-
itemFor: (id: string) =>
|
|
12
|
+
itemFor: (id: string) => ListItem | undefined;
|
|
13
13
|
sectionFor: (id: string) => unknown;
|
|
14
14
|
idAt: (position: number) => string | null;
|
|
15
15
|
positionFor: (id: string) => number;
|