@gtkx/components 0.21.0 → 1.0.0-rc.1
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/LICENSE +373 -0
- package/README.md +173 -0
- package/dist/cell.d.ts +30 -0
- package/dist/cell.d.ts.map +1 -0
- package/dist/cell.js +45 -0
- package/dist/cell.js.map +1 -0
- package/dist/column-view-column.d.ts +24 -14
- package/dist/column-view-column.d.ts.map +1 -1
- package/dist/column-view-column.js +19 -40
- package/dist/column-view-column.js.map +1 -1
- package/dist/column-view.d.ts +25 -11
- package/dist/column-view.d.ts.map +1 -1
- package/dist/column-view.js +53 -83
- package/dist/column-view.js.map +1 -1
- package/dist/constraint-layout-apply.d.ts +50 -1
- package/dist/constraint-layout-apply.d.ts.map +1 -1
- package/dist/constraint-layout-apply.js.map +1 -1
- package/dist/constraint-layout.d.ts +8 -1
- package/dist/constraint-layout.d.ts.map +1 -1
- package/dist/constraint-layout.js +12 -9
- package/dist/constraint-layout.js.map +1 -1
- package/dist/drop-down.d.ts +26 -36
- package/dist/drop-down.d.ts.map +1 -1
- package/dist/drop-down.js +47 -87
- package/dist/drop-down.js.map +1 -1
- package/dist/fixed.d.ts +26 -0
- package/dist/fixed.d.ts.map +1 -0
- package/dist/fixed.js +40 -0
- package/dist/fixed.js.map +1 -0
- package/dist/grid-view.d.ts +13 -10
- package/dist/grid-view.d.ts.map +1 -1
- package/dist/grid-view.js +30 -16
- package/dist/grid-view.js.map +1 -1
- package/dist/grid.d.ts +27 -0
- package/dist/grid.d.ts.map +1 -0
- package/dist/grid.js +48 -0
- package/dist/grid.js.map +1 -0
- package/dist/hooks/use-cell-containers.d.ts +18 -0
- package/dist/hooks/use-cell-containers.d.ts.map +1 -0
- package/dist/hooks/{use-realized-slots.js → use-cell-containers.js} +18 -17
- package/dist/hooks/use-cell-containers.js.map +1 -0
- package/dist/hooks/use-collection-model.d.ts +26 -0
- package/dist/hooks/use-collection-model.d.ts.map +1 -0
- package/dist/hooks/use-collection-model.js +35 -0
- package/dist/hooks/use-collection-model.js.map +1 -0
- package/dist/hooks/use-collection-widget.d.ts +28 -0
- package/dist/hooks/use-collection-widget.d.ts.map +1 -0
- package/dist/hooks/use-collection-widget.js +21 -0
- package/dist/hooks/use-collection-widget.js.map +1 -0
- package/dist/hooks/use-drop-down-selection.d.ts +7 -6
- package/dist/hooks/use-drop-down-selection.d.ts.map +1 -1
- package/dist/hooks/use-drop-down-selection.js +6 -6
- package/dist/hooks/use-drop-down-selection.js.map +1 -1
- package/dist/hooks/use-expansion-model.d.ts +12 -0
- package/dist/hooks/use-expansion-model.d.ts.map +1 -0
- package/dist/hooks/use-expansion-model.js +71 -0
- package/dist/hooks/use-expansion-model.js.map +1 -0
- package/dist/hooks/use-header-menu.d.ts.map +1 -1
- package/dist/hooks/use-header-menu.js +12 -24
- package/dist/hooks/use-header-menu.js.map +1 -1
- package/dist/hooks/use-installed-model.d.ts +4 -0
- package/dist/hooks/use-installed-model.d.ts.map +1 -0
- package/dist/hooks/use-installed-model.js +15 -0
- package/dist/hooks/use-installed-model.js.map +1 -0
- package/dist/hooks/use-list-model.d.ts +14 -15
- package/dist/hooks/use-list-model.d.ts.map +1 -1
- package/dist/hooks/use-list-model.js +61 -34
- package/dist/hooks/use-list-model.js.map +1 -1
- package/dist/hooks/use-placed-child.d.ts +18 -0
- package/dist/hooks/use-placed-child.d.ts.map +1 -0
- package/dist/hooks/use-placed-child.js +47 -0
- package/dist/hooks/use-placed-child.js.map +1 -0
- package/dist/hooks/use-selection-model.d.ts +4 -3
- package/dist/hooks/use-selection-model.d.ts.map +1 -1
- package/dist/hooks/use-selection-model.js +8 -16
- package/dist/hooks/use-selection-model.js.map +1 -1
- package/dist/hooks/use-sort-handler.d.ts +10 -5
- package/dist/hooks/use-sort-handler.d.ts.map +1 -1
- package/dist/hooks/use-sort-handler.js +2 -2
- package/dist/hooks/use-sort-handler.js.map +1 -1
- package/dist/index.d.ts +11 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -13
- package/dist/index.js.map +1 -1
- package/dist/list-view.d.ts +17 -9
- package/dist/list-view.d.ts.map +1 -1
- package/dist/list-view.js +49 -24
- package/dist/list-view.js.map +1 -1
- package/dist/menu-items.d.ts.map +1 -1
- package/dist/menu-items.js.map +1 -1
- package/dist/menu.d.ts +6 -10
- package/dist/menu.d.ts.map +1 -1
- package/dist/menu.js +7 -9
- package/dist/menu.js.map +1 -1
- package/dist/overlay.d.ts +24 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +49 -0
- package/dist/overlay.js.map +1 -0
- package/dist/size-group.d.ts +20 -0
- package/dist/size-group.d.ts.map +1 -0
- package/dist/size-group.js +57 -0
- package/dist/size-group.js.map +1 -0
- package/dist/types.d.ts +43 -173
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/{realized-slot-store.d.ts → cell-container-store.d.ts} +7 -9
- package/dist/utils/cell-container-store.d.ts.map +1 -0
- package/dist/utils/{realized-slot-store.js → cell-container-store.js} +9 -14
- package/dist/utils/cell-container-store.js.map +1 -0
- package/dist/utils/item-models.d.ts +10 -0
- package/dist/utils/item-models.d.ts.map +1 -0
- package/dist/utils/{position-only-model.js → item-models.js} +13 -14
- package/dist/utils/item-models.js.map +1 -0
- package/dist/utils/item-resolver.d.ts +14 -17
- package/dist/utils/item-resolver.d.ts.map +1 -1
- package/dist/utils/item-resolver.js +34 -43
- package/dist/utils/item-resolver.js.map +1 -1
- package/dist/utils/list-item-flatten.d.ts +16 -24
- package/dist/utils/list-item-flatten.d.ts.map +1 -1
- package/dist/utils/list-item-flatten.js +16 -36
- package/dist/utils/list-item-flatten.js.map +1 -1
- package/package.json +52 -53
- package/src/cell.tsx +144 -0
- package/src/column-view-column.tsx +57 -48
- package/src/column-view.tsx +130 -146
- package/src/constraint-layout-apply.ts +53 -1
- package/src/constraint-layout.tsx +18 -13
- package/src/drop-down.tsx +129 -196
- package/src/fixed.tsx +86 -0
- package/src/grid-view.tsx +52 -44
- package/src/grid.tsx +88 -0
- package/src/hooks/{use-realized-slots.ts → use-cell-containers.ts} +30 -30
- package/src/hooks/use-collection-model.ts +60 -0
- package/src/hooks/use-collection-widget.ts +47 -0
- package/src/hooks/use-drop-down-selection.ts +12 -13
- package/src/hooks/use-expansion-model.ts +82 -0
- package/src/hooks/use-header-menu.ts +24 -23
- package/src/hooks/use-installed-model.ts +20 -0
- package/src/hooks/use-list-model.ts +103 -87
- package/src/hooks/use-placed-child.ts +79 -0
- package/src/hooks/use-selection-model.ts +11 -18
- package/src/hooks/use-sort-handler.ts +13 -8
- package/src/index.ts +25 -27
- package/src/list-view.tsx +120 -57
- package/src/menu.tsx +12 -14
- package/src/overlay.tsx +88 -0
- package/src/size-group.tsx +96 -0
- package/src/types.ts +55 -206
- package/src/utils/{realized-slot-store.ts → cell-container-store.ts} +13 -25
- package/src/utils/{position-only-model.ts → item-models.ts} +26 -28
- package/src/utils/item-resolver.ts +54 -59
- package/src/utils/list-item-flatten.ts +33 -56
- package/dist/collection-view.d.ts +0 -30
- package/dist/collection-view.d.ts.map +0 -1
- package/dist/collection-view.js +0 -52
- package/dist/collection-view.js.map +0 -1
- package/dist/contexts/column-view-context.d.ts +0 -17
- package/dist/contexts/column-view-context.d.ts.map +0 -1
- package/dist/contexts/column-view-context.js +0 -10
- package/dist/contexts/column-view-context.js.map +0 -1
- package/dist/hooks/use-model-installation.d.ts +0 -9
- package/dist/hooks/use-model-installation.d.ts.map +0 -1
- package/dist/hooks/use-model-installation.js +0 -20
- package/dist/hooks/use-model-installation.js.map +0 -1
- package/dist/hooks/use-realized-slots.d.ts +0 -20
- package/dist/hooks/use-realized-slots.d.ts.map +0 -1
- package/dist/hooks/use-realized-slots.js.map +0 -1
- package/dist/list-portal-host.d.ts +0 -11
- package/dist/list-portal-host.d.ts.map +0 -1
- package/dist/list-portal-host.js +0 -8
- package/dist/list-portal-host.js.map +0 -1
- package/dist/list-slot.d.ts +0 -14
- package/dist/list-slot.d.ts.map +0 -1
- package/dist/list-slot.js +0 -29
- package/dist/list-slot.js.map +0 -1
- package/dist/utils/position-only-model.d.ts +0 -10
- package/dist/utils/position-only-model.d.ts.map +0 -1
- package/dist/utils/position-only-model.js.map +0 -1
- package/dist/utils/realized-slot-store.d.ts.map +0 -1
- package/dist/utils/realized-slot-store.js.map +0 -1
- package/src/collection-view.tsx +0 -125
- package/src/contexts/column-view-context.ts +0 -28
- package/src/hooks/use-model-installation.ts +0 -25
- package/src/list-portal-host.tsx +0 -27
- package/src/list-slot.tsx +0 -56
package/src/grid.tsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { GtkGrid, type GtkGridProps } from "@gtkx/jsx/gtk";
|
|
3
|
+
import { useMergeRefs } from "@gtkx/react/internal";
|
|
4
|
+
import { Children, type ElementType, type ReactNode, type Ref, useRef } from "react";
|
|
5
|
+
import { createParentContext, usePlacedChild } from "./hooks/use-placed-child.js";
|
|
6
|
+
import type { ChildProps } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const { Context: GridContext, useParentRef: useGridRef } = createParentContext<Gtk.Grid>(
|
|
9
|
+
"<Grid.Child> must be a child of <Grid>",
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
/** Props for {@link Grid}. */
|
|
13
|
+
export type GridProps = GtkGridProps & { ref?: Ref<Gtk.Grid | null>; children?: ReactNode };
|
|
14
|
+
|
|
15
|
+
export type GridPlacement = {
|
|
16
|
+
column?: number | null | undefined;
|
|
17
|
+
row?: number | null | undefined;
|
|
18
|
+
/** Number of columns the child spans (defaults to 1). */
|
|
19
|
+
columnSpan?: number | null | undefined;
|
|
20
|
+
/** Number of rows the child spans (defaults to 1). */
|
|
21
|
+
rowSpan?: number | null | undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Places a single child inside a {@link Grid} at a column and row, optionally spanning multiple cells. */
|
|
25
|
+
export type GridChildProps<C extends ElementType> = ChildProps<C, GridPlacement>;
|
|
26
|
+
|
|
27
|
+
type Placement = { column: number; row: number; columnSpan: number; rowSpan: number };
|
|
28
|
+
|
|
29
|
+
const placementOf = (props: GridPlacement): Placement => ({
|
|
30
|
+
column: props.column ?? 0,
|
|
31
|
+
row: props.row ?? 0,
|
|
32
|
+
columnSpan: props.columnSpan ?? 1,
|
|
33
|
+
rowSpan: props.rowSpan ?? 1,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const samePlacement = (a: Placement, b: Placement): boolean =>
|
|
37
|
+
a.column === b.column && a.row === b.row && a.columnSpan === b.columnSpan && a.rowSpan === b.rowSpan;
|
|
38
|
+
|
|
39
|
+
const GridChild = <C extends ElementType>({
|
|
40
|
+
component,
|
|
41
|
+
column,
|
|
42
|
+
row,
|
|
43
|
+
columnSpan,
|
|
44
|
+
rowSpan,
|
|
45
|
+
ref,
|
|
46
|
+
...rest
|
|
47
|
+
}: GridChildProps<C>): ReactNode => {
|
|
48
|
+
const gridRef = useGridRef();
|
|
49
|
+
const Component: ElementType = component;
|
|
50
|
+
return usePlacedChild<Gtk.Widget, Placement>({
|
|
51
|
+
render: (placeRef) => <Component {...rest} ref={placeRef} />,
|
|
52
|
+
ref,
|
|
53
|
+
placement: placementOf({ column, row, columnSpan, rowSpan }),
|
|
54
|
+
samePlacement,
|
|
55
|
+
place: (widget, placement, previous) => {
|
|
56
|
+
const grid = gridRef.current;
|
|
57
|
+
if (!grid) return;
|
|
58
|
+
if (previous !== undefined && widget.getParent() === grid) grid.remove(widget);
|
|
59
|
+
if (widget.getParent() !== grid) {
|
|
60
|
+
grid.attach(widget, placement.column, placement.row, placement.columnSpan, placement.rowSpan);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
release: (widget) => {
|
|
64
|
+
const grid = gridRef.current;
|
|
65
|
+
if (grid && widget.getParent() === grid) grid.remove(widget);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Renders a Gtk.Grid whose children are attached at explicit column/row positions via
|
|
72
|
+
* {@link Grid.Child}.
|
|
73
|
+
*/
|
|
74
|
+
export const Grid: ((props: GridProps) => ReactNode) & {
|
|
75
|
+
Child: <C extends ElementType>(props: GridChildProps<C>) => ReactNode;
|
|
76
|
+
} = Object.assign(
|
|
77
|
+
({ children, ref, ...rest }: GridProps): ReactNode => {
|
|
78
|
+
const gridRef = useRef<Gtk.Grid | null>(null);
|
|
79
|
+
const mergedRef = useMergeRefs<Gtk.Grid>(ref, gridRef);
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<GtkGrid {...rest} ref={mergedRef} />
|
|
83
|
+
<GridContext.Provider value={gridRef}>{Children.toArray(children)}</GridContext.Provider>
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
{ Child: GridChild },
|
|
88
|
+
);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
2
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
3
|
-
import { type
|
|
3
|
+
import { type ObjectProp, useObjectAttachment } from "@gtkx/react/internal";
|
|
4
4
|
import { useRef } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import { CellContainerStore } from "../utils/cell-container-store.js";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type ChildContainer = {
|
|
8
8
|
getChild(): Gtk.Widget | null;
|
|
9
9
|
setChild(child: Gtk.Widget | null): void;
|
|
10
10
|
getItem(): GObject.Object | null;
|
|
11
|
-
}
|
|
11
|
+
};
|
|
12
12
|
|
|
13
13
|
const isChildContainer = (container: GObject.Object): container is GObject.Object & ChildContainer =>
|
|
14
14
|
container instanceof Gtk.ListItem || container instanceof Gtk.ListHeader;
|
|
@@ -19,36 +19,38 @@ const positionOf = (container: GObject.Object & ChildContainer): number => {
|
|
|
19
19
|
return -1;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
export
|
|
22
|
+
export type FactoryInstaller<W extends GObject.Object> = {
|
|
23
23
|
install(widget: W, factory: Gtk.SignalListItemFactory): void;
|
|
24
|
-
uninstall(widget: W
|
|
25
|
-
}
|
|
24
|
+
uninstall(widget: W): void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const makeFactoryInstaller = <W extends GObject.Object>(
|
|
28
|
+
setFactory: (widget: W, factory: Gtk.SignalListItemFactory | null) => void,
|
|
29
|
+
): FactoryInstaller<W> => ({
|
|
30
|
+
install: (widget, factory) => setFactory(widget, factory),
|
|
31
|
+
uninstall: (widget) => setFactory(widget, null),
|
|
32
|
+
});
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
type CellContainersOptions<W extends GObject.Object> = {
|
|
35
|
+
object: ObjectProp<W>;
|
|
36
|
+
installer: FactoryInstaller<W>;
|
|
30
37
|
estimatedHeight?: number | undefined;
|
|
31
38
|
estimatedWidth?: number | undefined;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface RealizedSlots {
|
|
35
|
-
factory: Gtk.SignalListItemFactory;
|
|
36
|
-
store: RealizedSlotStore;
|
|
37
|
-
}
|
|
39
|
+
};
|
|
38
40
|
|
|
39
41
|
const applyEstimatedSize = (child: Gtk.Widget, height: number | undefined, width: number | undefined): void => {
|
|
40
42
|
if (height === undefined && width === undefined) return;
|
|
41
43
|
child.setSizeRequest(width ?? -1, height ?? -1);
|
|
42
44
|
};
|
|
43
45
|
|
|
44
|
-
export const
|
|
45
|
-
const {
|
|
46
|
-
const storeRef = useRef<
|
|
47
|
-
if (storeRef.current === null) storeRef.current = new
|
|
46
|
+
export const useCellContainers = <W extends GObject.Object>(options: CellContainersOptions<W>): CellContainerStore => {
|
|
47
|
+
const { object, installer, estimatedHeight, estimatedWidth } = options;
|
|
48
|
+
const storeRef = useRef<CellContainerStore | null>(null);
|
|
49
|
+
if (storeRef.current === null) storeRef.current = new CellContainerStore();
|
|
48
50
|
const store = storeRef.current;
|
|
49
51
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
+
const installerRef = useRef(installer);
|
|
53
|
+
installerRef.current = installer;
|
|
52
54
|
|
|
53
55
|
const factoryRef = useRef<Gtk.SignalListItemFactory | null>(null);
|
|
54
56
|
if (factoryRef.current === null) {
|
|
@@ -65,9 +67,7 @@ export const useRealizedSlots = <W extends GObject.Object>(options: RealizedSlot
|
|
|
65
67
|
if (isChildContainer(container)) {
|
|
66
68
|
const item = container.getItem();
|
|
67
69
|
const treeRow = item instanceof Gtk.TreeListRow ? item : null;
|
|
68
|
-
store.bind(container, positionOf(container), treeRow
|
|
69
|
-
} else {
|
|
70
|
-
store.bind(container, -1, null, null);
|
|
70
|
+
store.bind(container, positionOf(container), treeRow);
|
|
71
71
|
}
|
|
72
72
|
});
|
|
73
73
|
factory.on("unbind", (container) => {
|
|
@@ -80,14 +80,14 @@ export const useRealizedSlots = <W extends GObject.Object>(options: RealizedSlot
|
|
|
80
80
|
}
|
|
81
81
|
const factory = factoryRef.current;
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
useObjectAttachment<W, { widget: W }>(object, {
|
|
84
84
|
attach: (widget) => {
|
|
85
|
-
|
|
85
|
+
installerRef.current.install(widget, factory);
|
|
86
86
|
return { widget };
|
|
87
87
|
},
|
|
88
|
-
detach: (
|
|
89
|
-
isSame: (
|
|
88
|
+
detach: (attachment) => installerRef.current.uninstall(attachment.widget),
|
|
89
|
+
isSame: (attachment, widget) => attachment.widget === widget,
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
return
|
|
92
|
+
return store;
|
|
93
93
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { type ReactNode, useCallback } from "react";
|
|
3
|
+
import type { ItemNode, SectionNode } from "../types.js";
|
|
4
|
+
import { type ItemResolver, rowIdOf } from "../utils/item-resolver.js";
|
|
5
|
+
import { useExpansionModel } from "./use-expansion-model.js";
|
|
6
|
+
import { useListModel } from "./use-list-model.js";
|
|
7
|
+
import { useSelectionModel } from "./use-selection-model.js";
|
|
8
|
+
|
|
9
|
+
type CollectionModelInput<T, S> = {
|
|
10
|
+
items: ItemNode<T>[] | undefined;
|
|
11
|
+
sections: SectionNode<S, T>[] | undefined;
|
|
12
|
+
selectionMode: Gtk.SelectionMode | null | undefined;
|
|
13
|
+
selectedIds: string[] | null | undefined;
|
|
14
|
+
onSelectionChanged: ((ids: string[]) => void) | null | undefined;
|
|
15
|
+
expandedIds?: string[] | null | undefined;
|
|
16
|
+
onExpandedChange?: ((ids: string[]) => void) | null | undefined;
|
|
17
|
+
renderHeader: ((info: { section: S }) => ReactNode) | null | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CollectionModelResult<T, S> = {
|
|
21
|
+
resolver: ItemResolver<T, S>;
|
|
22
|
+
headerResolver: ItemResolver<T, S>;
|
|
23
|
+
installedModel: Gtk.SelectionModel;
|
|
24
|
+
useHeader: boolean;
|
|
25
|
+
rowId: (row: Gtk.TreeListRow) => string | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const useCollectionModel = <T, S>(input: CollectionModelInput<T, S>): CollectionModelResult<T, S> => {
|
|
29
|
+
const listModel = useListModel<T, S>({
|
|
30
|
+
items: input.items,
|
|
31
|
+
sections: input.sections,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
useExpansionModel<T>({
|
|
35
|
+
treeModel: listModel.treeModel,
|
|
36
|
+
rowValues: listModel.rowValues,
|
|
37
|
+
expandedIds: input.expandedIds,
|
|
38
|
+
onExpandedChange: input.onExpandedChange,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const installedModel = useSelectionModel<T, S>({
|
|
42
|
+
base: listModel.model,
|
|
43
|
+
resolver: listModel.resolver,
|
|
44
|
+
selectionMode: input.selectionMode,
|
|
45
|
+
selectedIds: input.selectedIds,
|
|
46
|
+
onSelectionChanged: input.onSelectionChanged,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const useHeader = typeof input.renderHeader === "function";
|
|
50
|
+
const { rowValues } = listModel;
|
|
51
|
+
const rowId = useCallback((row: Gtk.TreeListRow): string | undefined => rowIdOf(rowValues, row), [rowValues]);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
resolver: listModel.resolver,
|
|
55
|
+
headerResolver: listModel.headerResolver,
|
|
56
|
+
installedModel,
|
|
57
|
+
useHeader,
|
|
58
|
+
rowId,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { useMergeRefs } from "@gtkx/react/internal";
|
|
3
|
+
import { type ReactNode, type Ref, type RefCallback, type RefObject, useRef } from "react";
|
|
4
|
+
import type { ItemNode, SectionNode } from "../types.js";
|
|
5
|
+
import { type CollectionModelResult, useCollectionModel } from "./use-collection-model.js";
|
|
6
|
+
import { useInstalledModel } from "./use-installed-model.js";
|
|
7
|
+
|
|
8
|
+
type CollectionWidget = Gtk.Widget & {
|
|
9
|
+
setModel(model: Gtk.SelectionModel | null): void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type CollectionWidgetInput<W extends CollectionWidget, T, S> = {
|
|
13
|
+
ref?: Ref<W | null> | undefined;
|
|
14
|
+
items?: ItemNode<T>[] | undefined;
|
|
15
|
+
sections?: SectionNode<S, T>[] | undefined;
|
|
16
|
+
selectionMode?: Gtk.SelectionMode | null | undefined;
|
|
17
|
+
selectedIds?: string[] | null | undefined;
|
|
18
|
+
onSelectionChanged?: ((ids: string[]) => void) | null | undefined;
|
|
19
|
+
expandedIds?: string[] | null | undefined;
|
|
20
|
+
onExpandedChange?: ((ids: string[]) => void) | null | undefined;
|
|
21
|
+
renderHeader?: ((info: { section: S }) => ReactNode) | null | undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type CollectionWidgetResult<W extends CollectionWidget, T, S> = {
|
|
25
|
+
widgetRef: RefObject<W | null>;
|
|
26
|
+
setRef: RefCallback<W>;
|
|
27
|
+
collection: CollectionModelResult<T, S>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const useCollectionWidget = <W extends CollectionWidget, T, S>(
|
|
31
|
+
props: CollectionWidgetInput<W, T, S>,
|
|
32
|
+
): CollectionWidgetResult<W, T, S> => {
|
|
33
|
+
const widgetRef = useRef<W | null>(null);
|
|
34
|
+
const setRef = useMergeRefs<W>(props.ref, widgetRef);
|
|
35
|
+
const collection = useCollectionModel<T, S>({
|
|
36
|
+
items: props.items,
|
|
37
|
+
sections: props.sections,
|
|
38
|
+
selectionMode: props.selectionMode,
|
|
39
|
+
selectedIds: props.selectedIds,
|
|
40
|
+
onSelectionChanged: props.onSelectionChanged,
|
|
41
|
+
expandedIds: props.expandedIds,
|
|
42
|
+
onExpandedChange: props.onExpandedChange,
|
|
43
|
+
renderHeader: props.renderHeader,
|
|
44
|
+
});
|
|
45
|
+
useInstalledModel(widgetRef, collection.installedModel, (widget, model) => widget.setModel(model));
|
|
46
|
+
return { widgetRef, setRef, collection };
|
|
47
|
+
};
|
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
2
3
|
import { useSignal } from "@gtkx/react";
|
|
3
|
-
import { type
|
|
4
|
+
import { type ObjectProp, resolveObjectProp, useObjectValue } from "@gtkx/react/internal";
|
|
4
5
|
import { useLayoutEffect, useRef } from "react";
|
|
5
6
|
import type { ItemResolver } from "../utils/item-resolver.js";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
type DropDownSelectionObject = GObject.Object & {
|
|
8
9
|
getSelected(): number;
|
|
9
10
|
setSelected(position: number): void;
|
|
10
|
-
}
|
|
11
|
+
};
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
widget:
|
|
13
|
+
type DropDownSelectionOptions<T, S> = {
|
|
14
|
+
widget: ObjectProp<DropDownSelectionObject>;
|
|
14
15
|
resolver: ItemResolver<T, S>;
|
|
15
16
|
selectedId: string | null | undefined;
|
|
16
17
|
onSelectionChanged: ((id: string) => void) | null | undefined;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const GTK_INVALID_LIST_POSITION = 4294967295;
|
|
18
|
+
};
|
|
20
19
|
|
|
21
20
|
const normalizeSelected = (position: number): number =>
|
|
22
|
-
position ===
|
|
21
|
+
position === Gtk.INVALID_LIST_POSITION || position < 0 ? -1 : position;
|
|
23
22
|
|
|
24
23
|
export const useDropDownSelection = <T, S>(options: DropDownSelectionOptions<T, S>): number => {
|
|
25
24
|
const { widget, resolver, selectedId, onSelectionChanged } = options;
|
|
26
|
-
const resolved =
|
|
25
|
+
const resolved = resolveObjectProp(widget);
|
|
27
26
|
|
|
28
|
-
const selectedPosition =
|
|
29
|
-
|
|
27
|
+
const selectedPosition = useObjectValue(widget, "notify::selected", (object) =>
|
|
28
|
+
object === null ? -1 : normalizeSelected(object.getSelected()),
|
|
30
29
|
);
|
|
31
30
|
|
|
32
31
|
const resolverRef = useRef(resolver);
|
|
@@ -36,7 +35,7 @@ export const useDropDownSelection = <T, S>(options: DropDownSelectionOptions<T,
|
|
|
36
35
|
|
|
37
36
|
useLayoutEffect(() => {
|
|
38
37
|
if (resolved === null || selectedId === undefined || selectedId === null) return;
|
|
39
|
-
const position = resolverRef.current.
|
|
38
|
+
const position = resolverRef.current.positionOfId(selectedId);
|
|
40
39
|
if (position < 0) return;
|
|
41
40
|
if (normalizeSelected(resolved.getSelected()) !== position) resolved.setSelected(position);
|
|
42
41
|
}, [resolved, selectedId, resolver]);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
3
|
+
import { useSignal } from "@gtkx/react";
|
|
4
|
+
import { isSameArray } from "@gtkx/utils";
|
|
5
|
+
import { useLayoutEffect, useRef } from "react";
|
|
6
|
+
import { type RowValue, rowIdOf } from "../utils/item-resolver.js";
|
|
7
|
+
|
|
8
|
+
type ExpansionModelOptions<T> = {
|
|
9
|
+
treeModel: Gtk.TreeListModel | null;
|
|
10
|
+
rowValues: WeakMap<GObject.Object, RowValue<T>>;
|
|
11
|
+
expandedIds: string[] | null | undefined;
|
|
12
|
+
onExpandedChange: ((ids: string[]) => void) | null | undefined;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const readExpandedIds = <T>(model: Gtk.TreeListModel, rowValues: WeakMap<GObject.Object, RowValue<T>>): string[] => {
|
|
16
|
+
const ids: string[] = [];
|
|
17
|
+
const count = model.getNItems();
|
|
18
|
+
for (let position = 0; position < count; position++) {
|
|
19
|
+
const row = model.getRow(position);
|
|
20
|
+
if (row?.getExpanded()) {
|
|
21
|
+
const id = rowIdOf(rowValues, row);
|
|
22
|
+
if (id !== undefined) ids.push(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return ids;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const applyExpanded = <T>(
|
|
29
|
+
model: Gtk.TreeListModel,
|
|
30
|
+
rowValues: WeakMap<GObject.Object, RowValue<T>>,
|
|
31
|
+
ids: string[],
|
|
32
|
+
): void => {
|
|
33
|
+
const target = new Set(ids);
|
|
34
|
+
let changed = true;
|
|
35
|
+
while (changed) {
|
|
36
|
+
changed = false;
|
|
37
|
+
const count = model.getNItems();
|
|
38
|
+
for (let position = 0; position < count; position++) {
|
|
39
|
+
const row = model.getRow(position);
|
|
40
|
+
if (row === null || !row.isExpandable()) continue;
|
|
41
|
+
const id = rowIdOf(rowValues, row);
|
|
42
|
+
if (id === undefined) continue;
|
|
43
|
+
const desired = target.has(id);
|
|
44
|
+
if (row.getExpanded() !== desired) {
|
|
45
|
+
row.setExpanded(desired);
|
|
46
|
+
changed = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const useExpansionModel = <T>(options: ExpansionModelOptions<T>): void => {
|
|
53
|
+
const { treeModel, rowValues, expandedIds, onExpandedChange } = options;
|
|
54
|
+
const onChangeRef = useRef(onExpandedChange);
|
|
55
|
+
onChangeRef.current = onExpandedChange;
|
|
56
|
+
const rowValuesRef = useRef(rowValues);
|
|
57
|
+
rowValuesRef.current = rowValues;
|
|
58
|
+
const lastReportedRef = useRef<string[] | null>(null);
|
|
59
|
+
const applyingRef = useRef(false);
|
|
60
|
+
|
|
61
|
+
const report = (): void => {
|
|
62
|
+
if (treeModel === null || applyingRef.current) return;
|
|
63
|
+
const callback = onChangeRef.current;
|
|
64
|
+
if (!callback) return;
|
|
65
|
+
const ids = readExpandedIds(treeModel, rowValuesRef.current);
|
|
66
|
+
if (lastReportedRef.current !== null && isSameArray(lastReportedRef.current, ids)) return;
|
|
67
|
+
lastReportedRef.current = ids;
|
|
68
|
+
callback(ids);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
useSignal(treeModel, "items-changed", report);
|
|
72
|
+
|
|
73
|
+
useLayoutEffect(() => {
|
|
74
|
+
if (treeModel === null) return;
|
|
75
|
+
if (expandedIds !== undefined && expandedIds !== null) {
|
|
76
|
+
applyingRef.current = true;
|
|
77
|
+
applyExpanded(treeModel, rowValuesRef.current, expandedIds);
|
|
78
|
+
applyingRef.current = false;
|
|
79
|
+
}
|
|
80
|
+
report();
|
|
81
|
+
}, [treeModel, expandedIds]);
|
|
82
|
+
};
|
|
@@ -1,33 +1,34 @@
|
|
|
1
|
-
import * as Gio from "@gtkx/gi/gio";
|
|
2
|
-
import * as GObject from "@gtkx/gi/gobject";
|
|
1
|
+
import type * as Gio from "@gtkx/gi/gio";
|
|
3
2
|
import type * as Gtk from "@gtkx/gi/gtk";
|
|
4
|
-
import { createPortal } from "@gtkx/react";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { createPortal, rootElement } from "@gtkx/react";
|
|
4
|
+
import { useMergeRefs } from "@gtkx/react/internal";
|
|
5
|
+
import {
|
|
6
|
+
cloneElement,
|
|
7
|
+
isValidElement,
|
|
8
|
+
type ReactElement,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
type Ref,
|
|
11
|
+
type RefCallback,
|
|
12
|
+
useCallback,
|
|
13
|
+
useLayoutEffect,
|
|
14
|
+
useRef,
|
|
15
|
+
} from "react";
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
for (const child of stateOf(container).children) {
|
|
10
|
-
if (child instanceof Gio.MenuModel) return child;
|
|
11
|
-
}
|
|
12
|
-
return null;
|
|
13
|
-
};
|
|
17
|
+
type MenuElement = ReactElement<{ ref?: Ref<Gio.MenuModel | null> }>;
|
|
14
18
|
|
|
15
19
|
export const useHeaderMenu = (column: Gtk.ColumnViewColumn | null, headerMenu: ReactNode): ReactNode => {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
const
|
|
20
|
+
const modelRef = useRef<Gio.MenuModel | null>(null);
|
|
21
|
+
const setModel = useCallback<RefCallback<Gio.MenuModel>>((model) => {
|
|
22
|
+
modelRef.current = model;
|
|
23
|
+
}, []);
|
|
24
|
+
const element = isValidElement(headerMenu) ? (headerMenu as MenuElement) : null;
|
|
25
|
+
const mergedRef = useMergeRefs<Gio.MenuModel>(setModel, element?.props.ref);
|
|
21
26
|
|
|
22
27
|
useLayoutEffect(() => {
|
|
23
28
|
if (column === null) return;
|
|
24
|
-
|
|
25
|
-
column.setHeaderMenu(null);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
column.setHeaderMenu(findMenuModel(container));
|
|
29
|
+
column.setHeaderMenu(headerMenu == null ? null : modelRef.current);
|
|
29
30
|
});
|
|
30
31
|
|
|
31
|
-
if (
|
|
32
|
-
return createPortal(
|
|
32
|
+
if (element === null) return null;
|
|
33
|
+
return createPortal(cloneElement(element, { ref: mergedRef }), rootElement);
|
|
33
34
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { useObjectAttachment } from "@gtkx/react/internal";
|
|
3
|
+
import { type RefObject, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
export const useInstalledModel = <W extends Gtk.Widget, M>(
|
|
6
|
+
object: RefObject<W | null>,
|
|
7
|
+
model: M,
|
|
8
|
+
install: (widget: W, model: M) => void,
|
|
9
|
+
): void => {
|
|
10
|
+
const installRef = useRef(install);
|
|
11
|
+
installRef.current = install;
|
|
12
|
+
useObjectAttachment<W, { widget: W; model: M }>(object, {
|
|
13
|
+
attach: (widget) => {
|
|
14
|
+
installRef.current(widget, model);
|
|
15
|
+
return { widget, model };
|
|
16
|
+
},
|
|
17
|
+
detach: () => {},
|
|
18
|
+
isSame: (attachment, widget) => attachment.widget === widget && attachment.model === model,
|
|
19
|
+
});
|
|
20
|
+
};
|