@gtkx/components 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/collection-view.d.ts +30 -0
- package/dist/collection-view.d.ts.map +1 -0
- package/dist/collection-view.js +52 -0
- package/dist/collection-view.js.map +1 -0
- package/dist/column-view-column.d.ts +17 -0
- package/dist/column-view-column.d.ts.map +1 -0
- package/dist/column-view-column.js +51 -0
- package/dist/column-view-column.js.map +1 -0
- package/dist/column-view.d.ts +18 -0
- package/dist/column-view.d.ts.map +1 -0
- package/dist/column-view.js +108 -0
- package/dist/column-view.js.map +1 -0
- package/dist/constraint-layout-apply.d.ts +6 -0
- package/dist/constraint-layout-apply.d.ts.map +1 -0
- package/dist/constraint-layout-apply.js +107 -0
- package/dist/constraint-layout-apply.js.map +1 -0
- package/dist/constraint-layout.d.ts +13 -0
- package/dist/constraint-layout.d.ts.map +1 -0
- package/dist/constraint-layout.js +85 -0
- package/dist/constraint-layout.js.map +1 -0
- package/dist/contexts/column-view-context.d.ts +17 -0
- package/dist/contexts/column-view-context.d.ts.map +1 -0
- package/dist/contexts/column-view-context.js +10 -0
- package/dist/contexts/column-view-context.js.map +1 -0
- package/dist/drop-down.d.ts +41 -0
- package/dist/drop-down.d.ts.map +1 -0
- package/dist/drop-down.js +129 -0
- package/dist/drop-down.js.map +1 -0
- package/dist/grid-view.d.ts +16 -0
- package/dist/grid-view.d.ts.map +1 -0
- package/dist/grid-view.js +22 -0
- package/dist/grid-view.js.map +1 -0
- package/dist/hooks/use-drop-down-selection.d.ts +15 -0
- package/dist/hooks/use-drop-down-selection.d.ts.map +1 -0
- package/dist/hooks/use-drop-down-selection.js +35 -0
- package/dist/hooks/use-drop-down-selection.js.map +1 -0
- package/dist/hooks/use-header-menu.d.ts +4 -0
- package/dist/hooks/use-header-menu.d.ts.map +1 -0
- package/dist/hooks/use-header-menu.js +32 -0
- package/dist/hooks/use-header-menu.js.map +1 -0
- package/dist/hooks/use-list-model.d.ts +20 -0
- package/dist/hooks/use-list-model.d.ts.map +1 -0
- package/dist/hooks/use-list-model.js +70 -0
- package/dist/hooks/use-list-model.js.map +1 -0
- package/dist/hooks/use-model-installation.d.ts +9 -0
- package/dist/hooks/use-model-installation.d.ts.map +1 -0
- package/dist/hooks/use-model-installation.js +20 -0
- package/dist/hooks/use-model-installation.js.map +1 -0
- package/dist/hooks/use-realized-slots.d.ts +20 -0
- package/dist/hooks/use-realized-slots.d.ts.map +1 -0
- package/dist/hooks/use-realized-slots.js +66 -0
- package/dist/hooks/use-realized-slots.js.map +1 -0
- package/dist/hooks/use-selection-model.d.ts +12 -0
- package/dist/hooks/use-selection-model.d.ts.map +1 -0
- package/dist/hooks/use-selection-model.js +111 -0
- package/dist/hooks/use-selection-model.js.map +1 -0
- package/dist/hooks/use-sort-handler.d.ts +12 -0
- package/dist/hooks/use-sort-handler.d.ts.map +1 -0
- package/dist/hooks/use-sort-handler.js +87 -0
- package/dist/hooks/use-sort-handler.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/list-portal-host.d.ts +11 -0
- package/dist/list-portal-host.d.ts.map +1 -0
- package/dist/list-portal-host.js +8 -0
- package/dist/list-portal-host.js.map +1 -0
- package/dist/list-slot.d.ts +14 -0
- package/dist/list-slot.d.ts.map +1 -0
- package/dist/list-slot.js +29 -0
- package/dist/list-slot.js.map +1 -0
- package/dist/list-view.d.ts +16 -0
- package/dist/list-view.d.ts.map +1 -0
- package/dist/list-view.js +31 -0
- package/dist/list-view.js.map +1 -0
- package/dist/menu-items.d.ts +5 -0
- package/dist/menu-items.d.ts.map +1 -0
- package/dist/menu-items.js +48 -0
- package/dist/menu-items.js.map +1 -0
- package/dist/menu.d.ts +18 -0
- package/dist/menu.d.ts.map +1 -0
- package/dist/menu.js +26 -0
- package/dist/menu.js.map +1 -0
- package/dist/types.d.ts +198 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/item-resolver.d.ts +28 -0
- package/dist/utils/item-resolver.d.ts.map +1 -0
- package/dist/utils/item-resolver.js +111 -0
- package/dist/utils/item-resolver.js.map +1 -0
- package/dist/utils/list-item-flatten.d.ts +32 -0
- package/dist/utils/list-item-flatten.d.ts.map +1 -0
- package/dist/utils/list-item-flatten.js +88 -0
- package/dist/utils/list-item-flatten.js.map +1 -0
- package/dist/utils/position-only-model.d.ts +10 -0
- package/dist/utils/position-only-model.d.ts.map +1 -0
- package/dist/utils/position-only-model.js +83 -0
- package/dist/utils/position-only-model.js.map +1 -0
- package/dist/utils/realized-slot-store.d.ts +30 -0
- package/dist/utils/realized-slot-store.d.ts.map +1 -0
- package/dist/utils/realized-slot-store.js +103 -0
- package/dist/utils/realized-slot-store.js.map +1 -0
- package/package.json +55 -0
- package/src/collection-view.tsx +125 -0
- package/src/column-view-column.tsx +73 -0
- package/src/column-view.tsx +206 -0
- package/src/constraint-layout-apply.ts +121 -0
- package/src/constraint-layout.tsx +130 -0
- package/src/contexts/column-view-context.ts +28 -0
- package/src/drop-down.tsx +259 -0
- package/src/grid-view.tsx +73 -0
- package/src/hooks/use-drop-down-selection.ts +53 -0
- package/src/hooks/use-header-menu.ts +33 -0
- package/src/hooks/use-list-model.ts +145 -0
- package/src/hooks/use-model-installation.ts +25 -0
- package/src/hooks/use-realized-slots.ts +93 -0
- package/src/hooks/use-selection-model.ts +121 -0
- package/src/hooks/use-sort-handler.ts +102 -0
- package/src/index.ts +31 -0
- package/src/list-portal-host.tsx +27 -0
- package/src/list-slot.tsx +56 -0
- package/src/list-view.tsx +85 -0
- package/src/menu-items.ts +48 -0
- package/src/menu.tsx +34 -0
- package/src/types.ts +227 -0
- package/src/utils/item-resolver.ts +143 -0
- package/src/utils/list-item-flatten.ts +113 -0
- package/src/utils/position-only-model.ts +115 -0
- package/src/utils/realized-slot-store.ts +126 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as GObject from "@gtkx/gi/gobject";
|
|
2
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
3
|
+
import type { ConstraintGuideProps, ConstraintProps, ConstraintVflProps } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const SUPER_ID = "super";
|
|
6
|
+
|
|
7
|
+
const unknownIdMessage = (role: string, id: string): string =>
|
|
8
|
+
`<ConstraintLayout.Constraint> references unknown id '${id}'. ` +
|
|
9
|
+
`Set name="${id}" on the ${role} widget, or add a <ConstraintLayout.Guide id="${id}">.`;
|
|
10
|
+
|
|
11
|
+
const guideRegistry = new WeakMap<Gtk.ConstraintLayout, Map<string, Gtk.ConstraintGuide>>();
|
|
12
|
+
|
|
13
|
+
const guidesFor = (layout: Gtk.ConstraintLayout): Map<string, Gtk.ConstraintGuide> => {
|
|
14
|
+
let map = guideRegistry.get(layout);
|
|
15
|
+
if (!map) {
|
|
16
|
+
map = new Map<string, Gtk.ConstraintGuide>();
|
|
17
|
+
guideRegistry.set(layout, map);
|
|
18
|
+
}
|
|
19
|
+
return map;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const explicitWidgetName = (widget: Gtk.Widget): string | null => {
|
|
23
|
+
const value = new GObject.Value();
|
|
24
|
+
value.init(GObject.TYPE_STRING);
|
|
25
|
+
widget.getProperty("name", value);
|
|
26
|
+
const name = value.getString();
|
|
27
|
+
return name !== null && name.length > 0 ? name : null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function* namedChildren(layout: Gtk.ConstraintLayout): Generator<[string, Gtk.Widget]> {
|
|
31
|
+
const host = layout.getWidget();
|
|
32
|
+
if (!host) return;
|
|
33
|
+
for (let child = host.getFirstChild(); child; child = child.getNextSibling()) {
|
|
34
|
+
const name = explicitWidgetName(child);
|
|
35
|
+
if (name !== null) yield [name, child];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const findChildByName = (layout: Gtk.ConstraintLayout, id: string): Gtk.Widget | undefined => {
|
|
40
|
+
for (const [name, child] of namedChildren(layout)) {
|
|
41
|
+
if (name === id) return child;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const resolveConstraintTarget = (
|
|
47
|
+
layout: Gtk.ConstraintLayout,
|
|
48
|
+
id: string | undefined,
|
|
49
|
+
): Gtk.ConstraintTarget | null | undefined => {
|
|
50
|
+
if (id === undefined || id === SUPER_ID) return null;
|
|
51
|
+
return findChildByName(layout, id) ?? guidesFor(layout).get(id);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const constraintViews = (layout: Gtk.ConstraintLayout): Map<string, Gtk.ConstraintTarget> => {
|
|
55
|
+
const views = new Map<string, Gtk.ConstraintTarget>();
|
|
56
|
+
for (const [name, child] of namedChildren(layout)) views.set(name, child);
|
|
57
|
+
for (const [name, guide] of guidesFor(layout)) {
|
|
58
|
+
if (!views.has(name)) views.set(name, guide);
|
|
59
|
+
}
|
|
60
|
+
return views;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const isLayoutLive = (layout: Gtk.ConstraintLayout): boolean => layout.getWidget() !== null;
|
|
64
|
+
|
|
65
|
+
export function applyConstraint(layout: Gtk.ConstraintLayout, props: ConstraintProps): () => void {
|
|
66
|
+
const target = resolveConstraintTarget(layout, props.target);
|
|
67
|
+
if (target === undefined) throw new Error(unknownIdMessage("target", String(props.target)));
|
|
68
|
+
const source = resolveConstraintTarget(layout, props.source);
|
|
69
|
+
if (source === undefined) throw new Error(unknownIdMessage("source", String(props.source)));
|
|
70
|
+
|
|
71
|
+
const constraint = new Gtk.Constraint({
|
|
72
|
+
target,
|
|
73
|
+
targetAttribute: props.targetAttribute,
|
|
74
|
+
relation: props.relation ?? Gtk.ConstraintRelation.EQ,
|
|
75
|
+
source,
|
|
76
|
+
sourceAttribute: props.sourceAttribute ?? Gtk.ConstraintAttribute.NONE,
|
|
77
|
+
multiplier: props.multiplier ?? 1,
|
|
78
|
+
constant: props.constant ?? 0,
|
|
79
|
+
strength: props.strength ?? Gtk.ConstraintStrength.REQUIRED,
|
|
80
|
+
});
|
|
81
|
+
layout.addConstraint(constraint);
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
if (isLayoutLive(layout)) layout.removeConstraint(constraint);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function applyVfl(layout: Gtk.ConstraintLayout, props: ConstraintVflProps): () => void {
|
|
89
|
+
const views = constraintViews(layout);
|
|
90
|
+
const constraints = layout.addConstraintsFromDescription(
|
|
91
|
+
props.lines,
|
|
92
|
+
props.hspacing ?? 0,
|
|
93
|
+
props.vspacing ?? 0,
|
|
94
|
+
views,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return () => {
|
|
98
|
+
if (!isLayoutLive(layout)) return;
|
|
99
|
+
for (const constraint of constraints) layout.removeConstraint(constraint);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function applyGuide(layout: Gtk.ConstraintLayout, props: ConstraintGuideProps): () => void {
|
|
104
|
+
const guide = new Gtk.ConstraintGuide({
|
|
105
|
+
name: props.id,
|
|
106
|
+
minWidth: props.minWidth,
|
|
107
|
+
minHeight: props.minHeight,
|
|
108
|
+
natWidth: props.natWidth,
|
|
109
|
+
natHeight: props.natHeight,
|
|
110
|
+
maxWidth: props.maxWidth,
|
|
111
|
+
maxHeight: props.maxHeight,
|
|
112
|
+
strength: props.strength,
|
|
113
|
+
});
|
|
114
|
+
layout.addGuide(guide);
|
|
115
|
+
guidesFor(layout).set(props.id, guide);
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
guidesFor(layout).delete(props.id);
|
|
119
|
+
if (isLayoutLive(layout)) layout.removeGuide(guide);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { GtkConstraintLayout } from "@gtkx/jsx/gtk";
|
|
3
|
+
import { useForwardedRef } from "@gtkx/react";
|
|
4
|
+
import {
|
|
5
|
+
type Context,
|
|
6
|
+
createContext,
|
|
7
|
+
createElement,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
type Ref,
|
|
10
|
+
type RefObject,
|
|
11
|
+
useCallback,
|
|
12
|
+
useContext,
|
|
13
|
+
useEffect,
|
|
14
|
+
useLayoutEffect,
|
|
15
|
+
useRef,
|
|
16
|
+
} from "react";
|
|
17
|
+
import { applyConstraint, applyGuide, applyVfl } from "./constraint-layout-apply.js";
|
|
18
|
+
import type { ConstraintGuideProps, ConstraintProps, ConstraintVflProps } from "./types.js";
|
|
19
|
+
|
|
20
|
+
const ORPHAN_MESSAGE = "<ConstraintLayout.Constraint> / <Guide> / <Vfl> must be a child of <ConstraintLayout>";
|
|
21
|
+
|
|
22
|
+
const ConstraintLayoutContext: Context<RefObject<Gtk.ConstraintLayout | null> | null> =
|
|
23
|
+
createContext<RefObject<Gtk.ConstraintLayout | null> | null>(null);
|
|
24
|
+
|
|
25
|
+
const useConstraintLayoutRef = (): RefObject<Gtk.ConstraintLayout | null> => {
|
|
26
|
+
const ref = useContext(ConstraintLayoutContext);
|
|
27
|
+
if (!ref) throw new Error(ORPHAN_MESSAGE);
|
|
28
|
+
return ref;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ConstraintLayoutProps = {
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
ref?: Ref<Gtk.ConstraintLayout | null>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type ContributionCleanupRef = { current: (() => void) | null };
|
|
37
|
+
|
|
38
|
+
const cleanupContribution = (cleanupRef: ContributionCleanupRef): void => {
|
|
39
|
+
const cleanup = cleanupRef.current;
|
|
40
|
+
if (!cleanup) return;
|
|
41
|
+
cleanupRef.current = null;
|
|
42
|
+
cleanup();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const useLayoutContribution = (apply: (layout: Gtk.ConstraintLayout) => () => void, deps: unknown[]): void => {
|
|
46
|
+
const layoutRef = useConstraintLayoutRef();
|
|
47
|
+
useLayoutEffect(() => {
|
|
48
|
+
const layout = layoutRef.current;
|
|
49
|
+
if (!layout) return;
|
|
50
|
+
return apply(layout);
|
|
51
|
+
}, deps);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const useDeferredContribution = (apply: (layout: Gtk.ConstraintLayout) => () => void, deps: unknown[]): void => {
|
|
55
|
+
const layoutRef = useConstraintLayoutRef();
|
|
56
|
+
const cleanupRef = useRef<(() => void) | null>(null);
|
|
57
|
+
|
|
58
|
+
useLayoutEffect(() => () => cleanupContribution(cleanupRef), deps);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const layout = layoutRef.current;
|
|
62
|
+
if (!layout) return;
|
|
63
|
+
const cleanup = apply(layout);
|
|
64
|
+
cleanupRef.current = cleanup;
|
|
65
|
+
return () => {
|
|
66
|
+
if (cleanupRef.current === cleanup) cleanupContribution(cleanupRef);
|
|
67
|
+
};
|
|
68
|
+
}, deps);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const ConstraintLayout: ((props: ConstraintLayoutProps) => ReactNode) & {
|
|
72
|
+
Guide: (props: ConstraintGuideProps) => ReactNode;
|
|
73
|
+
Constraint: (props: ConstraintProps) => ReactNode;
|
|
74
|
+
Vfl: (props: ConstraintVflProps) => ReactNode;
|
|
75
|
+
} = Object.assign(
|
|
76
|
+
({ children, ref }: ConstraintLayoutProps): ReactNode => {
|
|
77
|
+
const layoutRef = useRef<Gtk.ConstraintLayout | null>(null);
|
|
78
|
+
const captureLayout = useCallback(
|
|
79
|
+
(layout: Gtk.ConstraintLayout | null): void => {
|
|
80
|
+
layoutRef.current = layout;
|
|
81
|
+
},
|
|
82
|
+
[layoutRef],
|
|
83
|
+
);
|
|
84
|
+
const [, mergedRef] = useForwardedRef<Gtk.ConstraintLayout>(ref, captureLayout);
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
{createElement(GtkConstraintLayout, { ref: mergedRef })}
|
|
88
|
+
<ConstraintLayoutContext.Provider value={layoutRef}>{children}</ConstraintLayoutContext.Provider>
|
|
89
|
+
</>
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
Guide: (props: ConstraintGuideProps): ReactNode => {
|
|
94
|
+
useLayoutContribution(
|
|
95
|
+
(layout) => applyGuide(layout, props),
|
|
96
|
+
[
|
|
97
|
+
props.id,
|
|
98
|
+
props.minWidth,
|
|
99
|
+
props.minHeight,
|
|
100
|
+
props.natWidth,
|
|
101
|
+
props.natHeight,
|
|
102
|
+
props.maxWidth,
|
|
103
|
+
props.maxHeight,
|
|
104
|
+
props.strength,
|
|
105
|
+
],
|
|
106
|
+
);
|
|
107
|
+
return null;
|
|
108
|
+
},
|
|
109
|
+
Constraint: (props: ConstraintProps): ReactNode => {
|
|
110
|
+
useDeferredContribution(
|
|
111
|
+
(layout) => applyConstraint(layout, props),
|
|
112
|
+
[
|
|
113
|
+
props.target,
|
|
114
|
+
props.targetAttribute,
|
|
115
|
+
props.relation,
|
|
116
|
+
props.source,
|
|
117
|
+
props.sourceAttribute,
|
|
118
|
+
props.multiplier,
|
|
119
|
+
props.constant,
|
|
120
|
+
props.strength,
|
|
121
|
+
],
|
|
122
|
+
);
|
|
123
|
+
return null;
|
|
124
|
+
},
|
|
125
|
+
Vfl: (props: ConstraintVflProps): ReactNode => {
|
|
126
|
+
useDeferredContribution((layout) => applyVfl(layout, props), [props.lines, props.hspacing, props.vspacing]);
|
|
127
|
+
return null;
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import { type Context, createContext, type RefObject, useContext } from "react";
|
|
3
|
+
import type { ItemResolver } from "../utils/item-resolver.js";
|
|
4
|
+
|
|
5
|
+
export interface ColumnRegistration {
|
|
6
|
+
id: string;
|
|
7
|
+
column: Gtk.ColumnViewColumn;
|
|
8
|
+
sortable: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ColumnViewContextValue {
|
|
12
|
+
columnView: RefObject<Gtk.ColumnView | null>;
|
|
13
|
+
resolver: ItemResolver<unknown, unknown>;
|
|
14
|
+
register(registration: ColumnRegistration): void;
|
|
15
|
+
unregister(id: string): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ColumnViewContext: Context<ColumnViewContextValue | null> = createContext<ColumnViewContextValue | null>(
|
|
19
|
+
null,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export const useColumnViewContext = (): ColumnViewContextValue => {
|
|
23
|
+
const context = useContext(ColumnViewContext);
|
|
24
|
+
if (context === null) {
|
|
25
|
+
throw new Error("<ColumnViewColumn> must be rendered inside a <ColumnView>.");
|
|
26
|
+
}
|
|
27
|
+
return context;
|
|
28
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type * as Adw from "@gtkx/gi/adw";
|
|
2
|
+
import type * as Gio from "@gtkx/gi/gio";
|
|
3
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
4
|
+
import { AdwComboRow, type AdwComboRowProps } from "@gtkx/jsx/adw";
|
|
5
|
+
import { GtkDropDown, type GtkDropDownProps, GtkLabel } from "@gtkx/jsx/gtk";
|
|
6
|
+
import { useForwardedRef } from "@gtkx/react";
|
|
7
|
+
import {
|
|
8
|
+
createElement,
|
|
9
|
+
type ElementType,
|
|
10
|
+
type ReactElement,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
type Ref,
|
|
13
|
+
useCallback,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
} from "react";
|
|
17
|
+
import { useDropDownSelection } from "./hooks/use-drop-down-selection.js";
|
|
18
|
+
import { useListModel } from "./hooks/use-list-model.js";
|
|
19
|
+
import { useModelInstallation } from "./hooks/use-model-installation.js";
|
|
20
|
+
import { type FactoryBinding, useRealizedSlots } from "./hooks/use-realized-slots.js";
|
|
21
|
+
import { ListPortalHost } from "./list-portal-host.js";
|
|
22
|
+
import type { SlotRenderer } from "./list-slot.js";
|
|
23
|
+
import type { DropDownProps, ListItem } from "./types.js";
|
|
24
|
+
import type { ItemResolver } from "./utils/item-resolver.js";
|
|
25
|
+
import type { RealizedSlotStore } from "./utils/realized-slot-store.js";
|
|
26
|
+
|
|
27
|
+
export interface DropDownWidget extends Gtk.Widget {
|
|
28
|
+
getSelected(): number;
|
|
29
|
+
setSelected(position: number): void;
|
|
30
|
+
setModel(model: Gio.ListModel | null): void;
|
|
31
|
+
setFactory(factory: Gtk.ListItemFactory | null): void;
|
|
32
|
+
setListFactory(factory: Gtk.ListItemFactory | null): void;
|
|
33
|
+
setHeaderFactory(factory: Gtk.ListItemFactory | null): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const itemFactoryBinding: FactoryBinding<DropDownWidget> = {
|
|
37
|
+
install: (widget, factory) => widget.setFactory(factory),
|
|
38
|
+
uninstall: (widget) => widget.setFactory(null),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const listFactoryBinding: FactoryBinding<DropDownWidget> = {
|
|
42
|
+
install: (widget, factory) => widget.setListFactory(factory),
|
|
43
|
+
uninstall: (widget) => widget.setListFactory(null),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const headerFactoryBinding: FactoryBinding<DropDownWidget> = {
|
|
47
|
+
install: (widget, factory) => widget.setHeaderFactory(factory),
|
|
48
|
+
uninstall: (widget) => widget.setHeaderFactory(null),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const defaultRenderer: SlotRenderer<unknown, unknown> = (value, _treeRow, isHeader) => {
|
|
52
|
+
if (isHeader || value === undefined || value === null) return null;
|
|
53
|
+
return createElement(GtkLabel, { label: String(value) });
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
interface DropDownImplProps<T, S, W extends DropDownWidget> {
|
|
57
|
+
element: ElementType;
|
|
58
|
+
intrinsicProps: Record<string, unknown>;
|
|
59
|
+
ref: Ref<W | null> | undefined;
|
|
60
|
+
items: ListItem<T, S>[] | undefined;
|
|
61
|
+
model: Gio.ListModel | undefined;
|
|
62
|
+
renderItem: ((value: T) => ReactNode) | null | undefined;
|
|
63
|
+
renderListItem: ((value: T) => ReactNode) | null | undefined;
|
|
64
|
+
renderHeader: ((value: S) => ReactNode) | null | undefined;
|
|
65
|
+
selectedId: string | null | undefined;
|
|
66
|
+
onSelectionChanged: ((id: string) => void) | null | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const toItemRenderer = <T, S>(renderItem: ((value: T) => ReactNode) | null | undefined): SlotRenderer<T, S> => {
|
|
70
|
+
if (typeof renderItem !== "function") return defaultRenderer as SlotRenderer<T, S>;
|
|
71
|
+
return (value, _treeRow, isHeader) => (isHeader ? null : renderItem(value as T));
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const toPopupRenderer = <T, S>(
|
|
75
|
+
renderListItem: ((value: T) => ReactNode) | null | undefined,
|
|
76
|
+
renderItem: ((value: T) => ReactNode) | null | undefined,
|
|
77
|
+
): SlotRenderer<T, S> => {
|
|
78
|
+
if (typeof renderListItem === "function")
|
|
79
|
+
return (value, _treeRow, isHeader) => (isHeader ? null : renderListItem(value as T));
|
|
80
|
+
return toItemRenderer<T, S>(renderItem);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const toHeaderRenderer = <T, S>(renderHeader: ((value: S) => ReactNode) | null | undefined): SlotRenderer<T, S> => {
|
|
84
|
+
if (typeof renderHeader !== "function") return () => null;
|
|
85
|
+
return (value) => renderHeader(value as S);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
interface DropDownWiring<T, S, W extends DropDownWidget> {
|
|
89
|
+
setRef: (value: W | null) => void;
|
|
90
|
+
resolver: ItemResolver<T, S>;
|
|
91
|
+
faceResolver: ItemResolver<T, S>;
|
|
92
|
+
headerResolver: ItemResolver<T, S>;
|
|
93
|
+
faceStore: RealizedSlotStore;
|
|
94
|
+
popupStore: RealizedSlotStore;
|
|
95
|
+
headerStore: RealizedSlotStore;
|
|
96
|
+
useHeader: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const createFaceResolver = <T, S>(resolver: ItemResolver<T, S>, selectedPosition: number): ItemResolver<T, S> => ({
|
|
100
|
+
count: resolver.count,
|
|
101
|
+
positionOf: (id) => resolver.positionOf(id),
|
|
102
|
+
idOf: (position) => resolver.idOf(position),
|
|
103
|
+
resolve: (_position, treeRow) => resolver.resolve(selectedPosition, treeRow, null),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const useDropDownWiring = <T, S, W extends DropDownWidget>(
|
|
107
|
+
props: DropDownImplProps<T, S, W>,
|
|
108
|
+
): DropDownWiring<T, S, W> => {
|
|
109
|
+
const widgetRef = useRef<DropDownWidget | null>(null);
|
|
110
|
+
const [widget, setWidget] = useState<DropDownWidget | null>(null);
|
|
111
|
+
const captureWidget = useCallback((value: W | null) => {
|
|
112
|
+
widgetRef.current = value;
|
|
113
|
+
setWidget(value);
|
|
114
|
+
}, []);
|
|
115
|
+
const [, setRef] = useForwardedRef<W>(props.ref, captureWidget);
|
|
116
|
+
|
|
117
|
+
const externalModel = props.model;
|
|
118
|
+
const listModel = useListModel<T, S>(
|
|
119
|
+
externalModel === undefined ? { items: props.items ?? undefined } : { model: externalModel },
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const useHeader = externalModel === undefined && typeof props.renderHeader === "function";
|
|
123
|
+
|
|
124
|
+
const face = useRealizedSlots<DropDownWidget>({ target: widgetRef, binding: itemFactoryBinding });
|
|
125
|
+
const popup = useRealizedSlots<DropDownWidget>({ target: widgetRef, binding: listFactoryBinding });
|
|
126
|
+
const header = useRealizedSlots<DropDownWidget>({
|
|
127
|
+
target: useHeader ? widgetRef : null,
|
|
128
|
+
binding: headerFactoryBinding,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
useModelInstallation(widgetRef, listModel.model, (widget, model) => widget.setModel(model));
|
|
132
|
+
|
|
133
|
+
const selectedPosition = useDropDownSelection<T, S>({
|
|
134
|
+
widget,
|
|
135
|
+
resolver: listModel.resolver,
|
|
136
|
+
selectedId: props.selectedId,
|
|
137
|
+
onSelectionChanged: props.onSelectionChanged,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const controlledPosition =
|
|
141
|
+
props.selectedId === undefined || props.selectedId === null
|
|
142
|
+
? -1
|
|
143
|
+
: listModel.resolver.positionOf(props.selectedId);
|
|
144
|
+
const facePosition = controlledPosition >= 0 ? controlledPosition : selectedPosition < 0 ? 0 : selectedPosition;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
setRef,
|
|
148
|
+
resolver: listModel.resolver,
|
|
149
|
+
faceResolver: createFaceResolver(listModel.resolver, facePosition),
|
|
150
|
+
headerResolver: listModel.headerResolver,
|
|
151
|
+
faceStore: face.store,
|
|
152
|
+
popupStore: popup.store,
|
|
153
|
+
headerStore: header.store,
|
|
154
|
+
useHeader,
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const DropDownImpl = <T, S, W extends DropDownWidget>(props: DropDownImplProps<T, S, W>): ReactNode => {
|
|
159
|
+
const { setRef, resolver, faceResolver, headerResolver, faceStore, popupStore, headerStore, useHeader } =
|
|
160
|
+
useDropDownWiring(props);
|
|
161
|
+
const intrinsic: ReactElement = createElement(props.element, { ...props.intrinsicProps, ref: setRef });
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<>
|
|
165
|
+
{intrinsic}
|
|
166
|
+
<ListPortalHost store={faceStore} resolver={faceResolver} render={toItemRenderer<T, S>(props.renderItem)} />
|
|
167
|
+
<ListPortalHost
|
|
168
|
+
store={popupStore}
|
|
169
|
+
resolver={resolver}
|
|
170
|
+
render={toPopupRenderer<T, S>(props.renderListItem, props.renderItem)}
|
|
171
|
+
/>
|
|
172
|
+
{useHeader ? (
|
|
173
|
+
<ListPortalHost
|
|
174
|
+
store={headerStore}
|
|
175
|
+
resolver={headerResolver}
|
|
176
|
+
render={toHeaderRenderer<T, S>(props.renderHeader)}
|
|
177
|
+
/>
|
|
178
|
+
) : null}
|
|
179
|
+
</>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
type DropDownImplComponentProps<T, S, W extends DropDownWidget> = DropDownProps<T, S> & {
|
|
184
|
+
ref?: Ref<W | null> | undefined;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const extractDropDownProps = <T, S, W extends DropDownWidget>(
|
|
188
|
+
props: DropDownImplComponentProps<T, S, W>,
|
|
189
|
+
): {
|
|
190
|
+
impl: Omit<DropDownImplProps<T, S, W>, "element" | "intrinsicProps">;
|
|
191
|
+
intrinsicProps: Record<string, unknown>;
|
|
192
|
+
} => {
|
|
193
|
+
const {
|
|
194
|
+
ref,
|
|
195
|
+
items,
|
|
196
|
+
model,
|
|
197
|
+
renderItem,
|
|
198
|
+
renderListItem,
|
|
199
|
+
renderHeader,
|
|
200
|
+
selectedId,
|
|
201
|
+
onSelectionChanged,
|
|
202
|
+
...intrinsicProps
|
|
203
|
+
} = props as DropDownProps<T, S> & {
|
|
204
|
+
ref?: Ref<W | null>;
|
|
205
|
+
[key: string]: unknown;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
impl: {
|
|
210
|
+
ref,
|
|
211
|
+
items: items as ListItem<T, S>[] | undefined,
|
|
212
|
+
model: model as Gio.ListModel | undefined,
|
|
213
|
+
renderItem: renderItem as ((value: T) => ReactNode) | null | undefined,
|
|
214
|
+
renderListItem: renderListItem as ((value: T) => ReactNode) | null | undefined,
|
|
215
|
+
renderHeader: renderHeader as ((value: S) => ReactNode) | null | undefined,
|
|
216
|
+
selectedId: selectedId as string | null | undefined,
|
|
217
|
+
onSelectionChanged: onSelectionChanged as ((id: string) => void) | null | undefined,
|
|
218
|
+
},
|
|
219
|
+
intrinsicProps: intrinsicProps as Record<string, unknown>,
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Props for the {@link DropDown} component: the raw `GtkDropDown` element
|
|
225
|
+
* surface with its factory/model wiring replaced by the declarative
|
|
226
|
+
* {@link DropDownProps} API.
|
|
227
|
+
*/
|
|
228
|
+
export type DropDownComponentProps<T = unknown, S = unknown> = Omit<GtkDropDownProps, keyof DropDownProps<T, S>> &
|
|
229
|
+
DropDownProps<T, S>;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Props for the {@link ComboRow} component: the raw `AdwComboRow` element
|
|
233
|
+
* surface with its factory/model wiring replaced by the declarative
|
|
234
|
+
* {@link DropDownProps} API.
|
|
235
|
+
*/
|
|
236
|
+
export type ComboRowComponentProps<T = unknown, S = unknown> = Omit<AdwComboRowProps, keyof DropDownProps<T, S>> &
|
|
237
|
+
DropDownProps<T, S>;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A `GtkDropDown` driven by a declarative `items` model with a controlled
|
|
241
|
+
* `selectedId` and per-slot renderers for the selected face, the popup list,
|
|
242
|
+
* and section headers. Supplying an external `model` switches to the
|
|
243
|
+
* uncontrolled form.
|
|
244
|
+
*/
|
|
245
|
+
export const DropDown = <T = unknown, S = unknown>(props: DropDownComponentProps<T, S>): ReactNode => {
|
|
246
|
+
const { impl, intrinsicProps } = extractDropDownProps<T, S, Gtk.DropDown>(props);
|
|
247
|
+
return <DropDownImpl<T, S, Gtk.DropDown> element={GtkDropDown} intrinsicProps={intrinsicProps} {...impl} />;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* An `AdwComboRow` driven by a declarative `items` model with a controlled
|
|
252
|
+
* `selectedId` and per-slot renderers for the selected face, the popup list,
|
|
253
|
+
* and section headers. Supplying an external `model` switches to the
|
|
254
|
+
* uncontrolled form.
|
|
255
|
+
*/
|
|
256
|
+
export const ComboRow = <T = unknown, S = unknown>(props: ComboRowComponentProps<T, S>): ReactNode => {
|
|
257
|
+
const { impl, intrinsicProps } = extractDropDownProps<T, S, Adw.ComboRow>(props);
|
|
258
|
+
return <DropDownImpl<T, S, Adw.ComboRow> element={AdwComboRow} intrinsicProps={intrinsicProps} {...impl} />;
|
|
259
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type * as Gio from "@gtkx/gi/gio";
|
|
2
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
3
|
+
import { GtkGridView, type GtkGridViewProps } from "@gtkx/jsx/gtk";
|
|
4
|
+
import type { ReactNode, Ref } from "react";
|
|
5
|
+
import { CollectionView, type ModelBinding } from "./collection-view.js";
|
|
6
|
+
import type { SlotRenderer } from "./list-slot.js";
|
|
7
|
+
import type { GridViewProps } from "./types.js";
|
|
8
|
+
|
|
9
|
+
const factoryBinding = {
|
|
10
|
+
install: (widget: Gtk.GridView, factory: Gtk.SignalListItemFactory) => widget.setFactory(factory),
|
|
11
|
+
uninstall: (widget: Gtk.GridView) => widget.setFactory(null),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const modelBinding: ModelBinding<Gtk.GridView> = {
|
|
15
|
+
install: (widget, model) => widget.setModel(model as Gtk.SelectionModel),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Props for the {@link GridView} component: the raw `GtkGridView` element
|
|
20
|
+
* surface with its factory/model wiring replaced by the declarative
|
|
21
|
+
* {@link GridViewProps} API.
|
|
22
|
+
*/
|
|
23
|
+
export type GridViewComponentProps<T = unknown> = Omit<GtkGridViewProps, keyof GridViewProps<T>> & GridViewProps<T>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A `GtkGridView` driven by a declarative `items`/`renderItem` API with
|
|
27
|
+
* optional controlled selection. Supplying an external `model` switches to the
|
|
28
|
+
* uncontrolled form.
|
|
29
|
+
*/
|
|
30
|
+
export const GridView = <T = unknown>(props: GridViewComponentProps<T>): ReactNode => {
|
|
31
|
+
const {
|
|
32
|
+
ref,
|
|
33
|
+
items,
|
|
34
|
+
model,
|
|
35
|
+
renderItem,
|
|
36
|
+
selected,
|
|
37
|
+
selectionMode,
|
|
38
|
+
onSelectionChanged,
|
|
39
|
+
estimatedItemHeight,
|
|
40
|
+
estimatedItemWidth,
|
|
41
|
+
...intrinsicProps
|
|
42
|
+
} = props as GridViewProps<T> & {
|
|
43
|
+
ref?: Ref<Gtk.GridView | null>;
|
|
44
|
+
estimatedItemHeight?: number;
|
|
45
|
+
estimatedItemWidth?: number;
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const renderItemFn = renderItem as (item: T) => ReactNode;
|
|
50
|
+
const slotRenderer: SlotRenderer<T, unknown> = (value, _treeRow, isHeader) =>
|
|
51
|
+
isHeader ? null : renderItemFn(value as T);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<CollectionView<T, unknown, Gtk.GridView>
|
|
55
|
+
element={GtkGridView}
|
|
56
|
+
intrinsicProps={intrinsicProps}
|
|
57
|
+
ref={ref}
|
|
58
|
+
items={model === undefined ? items : undefined}
|
|
59
|
+
model={model as Gio.ListModel | undefined}
|
|
60
|
+
renderItem={slotRenderer}
|
|
61
|
+
autoexpand={undefined}
|
|
62
|
+
renderHeader={undefined}
|
|
63
|
+
estimatedHeight={estimatedItemHeight}
|
|
64
|
+
estimatedWidth={estimatedItemWidth}
|
|
65
|
+
selected={selected}
|
|
66
|
+
selectionMode={selectionMode}
|
|
67
|
+
onSelectionChanged={onSelectionChanged}
|
|
68
|
+
factoryBinding={factoryBinding}
|
|
69
|
+
headerFactoryBinding={undefined}
|
|
70
|
+
modelBinding={modelBinding}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
|
+
import { useSignal } from "@gtkx/react";
|
|
3
|
+
import { type GObjectTarget, resolveGobjectTarget, useGObjectSnapshot } from "@gtkx/react/internal";
|
|
4
|
+
import { useLayoutEffect, useRef } from "react";
|
|
5
|
+
import type { ItemResolver } from "../utils/item-resolver.js";
|
|
6
|
+
|
|
7
|
+
export interface DropDownSelectionTarget extends GObject.Object {
|
|
8
|
+
getSelected(): number;
|
|
9
|
+
setSelected(position: number): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DropDownSelectionOptions<T, S> {
|
|
13
|
+
widget: GObjectTarget<DropDownSelectionTarget>;
|
|
14
|
+
resolver: ItemResolver<T, S>;
|
|
15
|
+
selectedId: string | null | undefined;
|
|
16
|
+
onSelectionChanged: ((id: string) => void) | null | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const GTK_INVALID_LIST_POSITION = 4294967295;
|
|
20
|
+
|
|
21
|
+
const normalizeSelected = (position: number): number =>
|
|
22
|
+
position === GTK_INVALID_LIST_POSITION || position < 0 ? -1 : position;
|
|
23
|
+
|
|
24
|
+
export const useDropDownSelection = <T, S>(options: DropDownSelectionOptions<T, S>): number => {
|
|
25
|
+
const { widget, resolver, selectedId, onSelectionChanged } = options;
|
|
26
|
+
const resolved = resolveGobjectTarget(widget);
|
|
27
|
+
|
|
28
|
+
const selectedPosition = useGObjectSnapshot(widget, "notify::selected", (target) =>
|
|
29
|
+
target === null ? -1 : normalizeSelected(target.getSelected()),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const resolverRef = useRef(resolver);
|
|
33
|
+
resolverRef.current = resolver;
|
|
34
|
+
const onSelectionChangedRef = useRef(onSelectionChanged);
|
|
35
|
+
onSelectionChangedRef.current = onSelectionChanged;
|
|
36
|
+
|
|
37
|
+
useLayoutEffect(() => {
|
|
38
|
+
if (resolved === null || selectedId === undefined || selectedId === null) return;
|
|
39
|
+
const position = resolverRef.current.positionOf(selectedId);
|
|
40
|
+
if (position < 0) return;
|
|
41
|
+
if (normalizeSelected(resolved.getSelected()) !== position) resolved.setSelected(position);
|
|
42
|
+
}, [resolved, selectedId, resolver]);
|
|
43
|
+
|
|
44
|
+
useSignal(widget, "notify::selected", () => {
|
|
45
|
+
if (resolved === null) return;
|
|
46
|
+
const position = normalizeSelected(resolved.getSelected());
|
|
47
|
+
if (position < 0) return;
|
|
48
|
+
const id = resolverRef.current.idOf(position);
|
|
49
|
+
if (id !== undefined) onSelectionChangedRef.current?.(id);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return selectedPosition;
|
|
53
|
+
};
|