@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.
Files changed (131) hide show
  1. package/dist/collection-view.d.ts +30 -0
  2. package/dist/collection-view.d.ts.map +1 -0
  3. package/dist/collection-view.js +52 -0
  4. package/dist/collection-view.js.map +1 -0
  5. package/dist/column-view-column.d.ts +17 -0
  6. package/dist/column-view-column.d.ts.map +1 -0
  7. package/dist/column-view-column.js +51 -0
  8. package/dist/column-view-column.js.map +1 -0
  9. package/dist/column-view.d.ts +18 -0
  10. package/dist/column-view.d.ts.map +1 -0
  11. package/dist/column-view.js +108 -0
  12. package/dist/column-view.js.map +1 -0
  13. package/dist/constraint-layout-apply.d.ts +6 -0
  14. package/dist/constraint-layout-apply.d.ts.map +1 -0
  15. package/dist/constraint-layout-apply.js +107 -0
  16. package/dist/constraint-layout-apply.js.map +1 -0
  17. package/dist/constraint-layout.d.ts +13 -0
  18. package/dist/constraint-layout.d.ts.map +1 -0
  19. package/dist/constraint-layout.js +85 -0
  20. package/dist/constraint-layout.js.map +1 -0
  21. package/dist/contexts/column-view-context.d.ts +17 -0
  22. package/dist/contexts/column-view-context.d.ts.map +1 -0
  23. package/dist/contexts/column-view-context.js +10 -0
  24. package/dist/contexts/column-view-context.js.map +1 -0
  25. package/dist/drop-down.d.ts +41 -0
  26. package/dist/drop-down.d.ts.map +1 -0
  27. package/dist/drop-down.js +129 -0
  28. package/dist/drop-down.js.map +1 -0
  29. package/dist/grid-view.d.ts +16 -0
  30. package/dist/grid-view.d.ts.map +1 -0
  31. package/dist/grid-view.js +22 -0
  32. package/dist/grid-view.js.map +1 -0
  33. package/dist/hooks/use-drop-down-selection.d.ts +15 -0
  34. package/dist/hooks/use-drop-down-selection.d.ts.map +1 -0
  35. package/dist/hooks/use-drop-down-selection.js +35 -0
  36. package/dist/hooks/use-drop-down-selection.js.map +1 -0
  37. package/dist/hooks/use-header-menu.d.ts +4 -0
  38. package/dist/hooks/use-header-menu.d.ts.map +1 -0
  39. package/dist/hooks/use-header-menu.js +32 -0
  40. package/dist/hooks/use-header-menu.js.map +1 -0
  41. package/dist/hooks/use-list-model.d.ts +20 -0
  42. package/dist/hooks/use-list-model.d.ts.map +1 -0
  43. package/dist/hooks/use-list-model.js +70 -0
  44. package/dist/hooks/use-list-model.js.map +1 -0
  45. package/dist/hooks/use-model-installation.d.ts +9 -0
  46. package/dist/hooks/use-model-installation.d.ts.map +1 -0
  47. package/dist/hooks/use-model-installation.js +20 -0
  48. package/dist/hooks/use-model-installation.js.map +1 -0
  49. package/dist/hooks/use-realized-slots.d.ts +20 -0
  50. package/dist/hooks/use-realized-slots.d.ts.map +1 -0
  51. package/dist/hooks/use-realized-slots.js +66 -0
  52. package/dist/hooks/use-realized-slots.js.map +1 -0
  53. package/dist/hooks/use-selection-model.d.ts +12 -0
  54. package/dist/hooks/use-selection-model.d.ts.map +1 -0
  55. package/dist/hooks/use-selection-model.js +111 -0
  56. package/dist/hooks/use-selection-model.js.map +1 -0
  57. package/dist/hooks/use-sort-handler.d.ts +12 -0
  58. package/dist/hooks/use-sort-handler.d.ts.map +1 -0
  59. package/dist/hooks/use-sort-handler.js +87 -0
  60. package/dist/hooks/use-sort-handler.js.map +1 -0
  61. package/dist/index.d.ts +19 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +18 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/list-portal-host.d.ts +11 -0
  66. package/dist/list-portal-host.d.ts.map +1 -0
  67. package/dist/list-portal-host.js +8 -0
  68. package/dist/list-portal-host.js.map +1 -0
  69. package/dist/list-slot.d.ts +14 -0
  70. package/dist/list-slot.d.ts.map +1 -0
  71. package/dist/list-slot.js +29 -0
  72. package/dist/list-slot.js.map +1 -0
  73. package/dist/list-view.d.ts +16 -0
  74. package/dist/list-view.d.ts.map +1 -0
  75. package/dist/list-view.js +31 -0
  76. package/dist/list-view.js.map +1 -0
  77. package/dist/menu-items.d.ts +5 -0
  78. package/dist/menu-items.d.ts.map +1 -0
  79. package/dist/menu-items.js +48 -0
  80. package/dist/menu-items.js.map +1 -0
  81. package/dist/menu.d.ts +18 -0
  82. package/dist/menu.d.ts.map +1 -0
  83. package/dist/menu.js +26 -0
  84. package/dist/menu.js.map +1 -0
  85. package/dist/types.d.ts +198 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +2 -0
  88. package/dist/types.js.map +1 -0
  89. package/dist/utils/item-resolver.d.ts +28 -0
  90. package/dist/utils/item-resolver.d.ts.map +1 -0
  91. package/dist/utils/item-resolver.js +111 -0
  92. package/dist/utils/item-resolver.js.map +1 -0
  93. package/dist/utils/list-item-flatten.d.ts +32 -0
  94. package/dist/utils/list-item-flatten.d.ts.map +1 -0
  95. package/dist/utils/list-item-flatten.js +88 -0
  96. package/dist/utils/list-item-flatten.js.map +1 -0
  97. package/dist/utils/position-only-model.d.ts +10 -0
  98. package/dist/utils/position-only-model.d.ts.map +1 -0
  99. package/dist/utils/position-only-model.js +83 -0
  100. package/dist/utils/position-only-model.js.map +1 -0
  101. package/dist/utils/realized-slot-store.d.ts +30 -0
  102. package/dist/utils/realized-slot-store.d.ts.map +1 -0
  103. package/dist/utils/realized-slot-store.js +103 -0
  104. package/dist/utils/realized-slot-store.js.map +1 -0
  105. package/package.json +55 -0
  106. package/src/collection-view.tsx +125 -0
  107. package/src/column-view-column.tsx +73 -0
  108. package/src/column-view.tsx +206 -0
  109. package/src/constraint-layout-apply.ts +121 -0
  110. package/src/constraint-layout.tsx +130 -0
  111. package/src/contexts/column-view-context.ts +28 -0
  112. package/src/drop-down.tsx +259 -0
  113. package/src/grid-view.tsx +73 -0
  114. package/src/hooks/use-drop-down-selection.ts +53 -0
  115. package/src/hooks/use-header-menu.ts +33 -0
  116. package/src/hooks/use-list-model.ts +145 -0
  117. package/src/hooks/use-model-installation.ts +25 -0
  118. package/src/hooks/use-realized-slots.ts +93 -0
  119. package/src/hooks/use-selection-model.ts +121 -0
  120. package/src/hooks/use-sort-handler.ts +102 -0
  121. package/src/index.ts +31 -0
  122. package/src/list-portal-host.tsx +27 -0
  123. package/src/list-slot.tsx +56 -0
  124. package/src/list-view.tsx +85 -0
  125. package/src/menu-items.ts +48 -0
  126. package/src/menu.tsx +34 -0
  127. package/src/types.ts +227 -0
  128. package/src/utils/item-resolver.ts +143 -0
  129. package/src/utils/list-item-flatten.ts +113 -0
  130. package/src/utils/position-only-model.ts +115 -0
  131. package/src/utils/realized-slot-store.ts +126 -0
@@ -0,0 +1,103 @@
1
+ let nextKey = 0;
2
+ export class RealizedSlotStore {
3
+ entries = new Map();
4
+ published = new Map();
5
+ setListeners = new Set();
6
+ positionListeners = new Map();
7
+ positionFlushers = new Map();
8
+ keys = new WeakMap();
9
+ snapshot = [];
10
+ subscribeSet = (onChange) => {
11
+ this.setListeners.add(onChange);
12
+ return () => {
13
+ this.setListeners.delete(onChange);
14
+ };
15
+ };
16
+ getSlotsSnapshot = () => this.snapshot;
17
+ subscribePosition = (container, onChange) => {
18
+ let listeners = this.positionListeners.get(container);
19
+ if (listeners === undefined) {
20
+ listeners = new Set();
21
+ this.positionListeners.set(container, listeners);
22
+ }
23
+ listeners.add(onChange);
24
+ return () => {
25
+ const current = this.positionListeners.get(container);
26
+ if (current === undefined)
27
+ return;
28
+ current.delete(onChange);
29
+ if (current.size === 0)
30
+ this.positionListeners.delete(container);
31
+ };
32
+ };
33
+ getPosition = (container) => {
34
+ let entry = this.published.get(container);
35
+ if (entry === undefined) {
36
+ entry = { container, position: -1, treeRow: null, item: null };
37
+ this.published.set(container, entry);
38
+ }
39
+ return entry;
40
+ };
41
+ keyFor = (container) => {
42
+ const existing = this.keys.get(container);
43
+ if (existing !== undefined)
44
+ return existing;
45
+ const key = `slot-${nextKey++}`;
46
+ this.keys.set(container, key);
47
+ return key;
48
+ };
49
+ addContainer = (container) => {
50
+ if (this.entries.has(container))
51
+ return;
52
+ this.entries.set(container, { container, position: -1, treeRow: null, item: null });
53
+ this.notifySet();
54
+ };
55
+ bind = (container, position, treeRow, item) => {
56
+ if (!this.entries.has(container)) {
57
+ this.entries.set(container, { container, position, treeRow, item });
58
+ this.notifySet();
59
+ return;
60
+ }
61
+ this.entries.set(container, { container, position, treeRow, item });
62
+ this.notifyPosition(container);
63
+ };
64
+ unbind = (container) => {
65
+ if (!this.entries.has(container))
66
+ return;
67
+ this.entries.set(container, { container, position: -1, treeRow: null, item: null });
68
+ this.notifyPosition(container);
69
+ };
70
+ removeContainer = (container) => {
71
+ if (!this.entries.delete(container))
72
+ return;
73
+ this.published.delete(container);
74
+ this.positionFlushers.delete(container);
75
+ this.notifySet();
76
+ };
77
+ flushSet = () => {
78
+ this.snapshot = [...this.entries.keys()];
79
+ for (const listener of [...this.setListeners])
80
+ listener();
81
+ };
82
+ notifySet() {
83
+ this.flushSet();
84
+ }
85
+ notifyPosition(container) {
86
+ let flush = this.positionFlushers.get(container);
87
+ if (flush === undefined) {
88
+ flush = () => {
89
+ const live = this.entries.get(container);
90
+ if (live !== undefined)
91
+ this.published.set(container, live);
92
+ const listeners = this.positionListeners.get(container);
93
+ if (listeners === undefined)
94
+ return;
95
+ for (const listener of [...listeners])
96
+ listener();
97
+ };
98
+ this.positionFlushers.set(container, flush);
99
+ }
100
+ flush();
101
+ }
102
+ }
103
+ //# sourceMappingURL=realized-slot-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realized-slot-store.js","sourceRoot":"","sources":["../../src/utils/realized-slot-store.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@gtkx/components",
3
+ "version": "0.21.0",
4
+ "description": "Hand-written higher-level GTKX components: list, grid, column, drop-down, menu, and constraint-layout abstractions over the generated bindings",
5
+ "keywords": [
6
+ "gtkx",
7
+ "gtk",
8
+ "gtk4",
9
+ "adwaita",
10
+ "react",
11
+ "components",
12
+ "linux",
13
+ "desktop"
14
+ ],
15
+ "homepage": "https://gtkx.dev",
16
+ "bugs": {
17
+ "url": "https://github.com/gtkx-org/gtkx/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/gtkx-org/gtkx.git",
22
+ "directory": "packages/components"
23
+ },
24
+ "license": "MPL-2.0",
25
+ "author": "Eugenio Depalo <eugeniodepalo@gmail.com>",
26
+ "type": "module",
27
+ "exports": {
28
+ "./package.json": "./package.json",
29
+ ".": {
30
+ "source": "./src/index.ts",
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ }
34
+ },
35
+ "sideEffects": false,
36
+ "files": [
37
+ "dist",
38
+ "src"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc -b"
42
+ },
43
+ "engines": {
44
+ "node": ">=24"
45
+ },
46
+ "dependencies": {
47
+ "@gtkx/react": "workspace:*"
48
+ },
49
+ "devDependencies": {
50
+ "@gtkx/vitest": "workspace:*"
51
+ },
52
+ "peerDependencies": {
53
+ "react": "^19"
54
+ }
55
+ }
@@ -0,0 +1,125 @@
1
+ import type * as Gio from "@gtkx/gi/gio";
2
+ import type * as Gtk from "@gtkx/gi/gtk";
3
+ import { useForwardedRef } from "@gtkx/react";
4
+ import {
5
+ createElement,
6
+ type ElementType,
7
+ type ReactElement,
8
+ type ReactNode,
9
+ type Ref,
10
+ useCallback,
11
+ useRef,
12
+ } from "react";
13
+ import { useListModel } from "./hooks/use-list-model.js";
14
+ import { useModelInstallation } from "./hooks/use-model-installation.js";
15
+ import { type FactoryBinding, useRealizedSlots } from "./hooks/use-realized-slots.js";
16
+ import { useSelectionModel } from "./hooks/use-selection-model.js";
17
+ import { ListPortalHost } from "./list-portal-host.js";
18
+ import type { SlotRenderer } from "./list-slot.js";
19
+ import type { ListItem } from "./types.js";
20
+ import type { ItemResolver } from "./utils/item-resolver.js";
21
+ import type { RealizedSlotStore } from "./utils/realized-slot-store.js";
22
+
23
+ export interface ModelBinding<W extends Gtk.Widget> {
24
+ install(widget: W, model: Gio.ListModel): void;
25
+ }
26
+
27
+ export interface CollectionViewProps<T, S, W extends Gtk.Widget> {
28
+ element: ElementType;
29
+ intrinsicProps: Record<string, unknown>;
30
+ ref: Ref<W | null> | undefined;
31
+ items: ListItem<T, S>[] | undefined;
32
+ model: Gio.ListModel | undefined;
33
+ renderItem: SlotRenderer<T, S>;
34
+ autoexpand: boolean | undefined;
35
+ renderHeader: ((value: S | undefined) => ReactNode) | undefined;
36
+ estimatedHeight: number | undefined;
37
+ estimatedWidth: number | undefined;
38
+ selected: string[] | null | undefined;
39
+ selectionMode: Gtk.SelectionMode | null | undefined;
40
+ onSelectionChanged: ((ids: string[]) => void) | null | undefined;
41
+ factoryBinding: FactoryBinding<W>;
42
+ headerFactoryBinding: FactoryBinding<W> | undefined;
43
+ modelBinding: ModelBinding<W>;
44
+ children?: ReactNode;
45
+ }
46
+
47
+ const headerRenderer =
48
+ <T, S>(renderHeader: ((value: S | undefined) => ReactNode) | undefined): SlotRenderer<T, S> =>
49
+ (value) =>
50
+ renderHeader ? renderHeader(value as S | undefined) : null;
51
+
52
+ interface CollectionWiring<T, S, W extends Gtk.Widget> {
53
+ setRef: (value: W | null) => void;
54
+ resolver: ItemResolver<T, S>;
55
+ headerResolver: ItemResolver<T, S>;
56
+ itemStore: RealizedSlotStore;
57
+ headerStore: RealizedSlotStore;
58
+ }
59
+
60
+ const useCollectionWiring = <T, S, W extends Gtk.Widget>(
61
+ props: CollectionViewProps<T, S, W>,
62
+ ): CollectionWiring<T, S, W> => {
63
+ const widgetRef = useRef<W | null>(null);
64
+ const captureWidget = useCallback((value: W | null) => {
65
+ widgetRef.current = value;
66
+ }, []);
67
+ const [, setRef] = useForwardedRef<W>(props.ref, captureWidget);
68
+
69
+ const externalModel = props.model;
70
+ const listModel = useListModel<T, S>(
71
+ externalModel === undefined ? { items: props.items, autoexpand: props.autoexpand } : { model: externalModel },
72
+ );
73
+
74
+ const controlledSelection = useSelectionModel<T, S>({
75
+ base: listModel.model,
76
+ selectionMode: props.selectionMode,
77
+ selected: props.selected,
78
+ onSelectionChanged: props.onSelectionChanged,
79
+ resolver: listModel.resolver,
80
+ });
81
+ const installedModel: Gio.ListModel = externalModel === undefined ? controlledSelection : externalModel;
82
+
83
+ const items = useRealizedSlots<W>({
84
+ target: widgetRef,
85
+ binding: props.factoryBinding,
86
+ estimatedHeight: props.estimatedHeight,
87
+ estimatedWidth: props.estimatedWidth,
88
+ });
89
+ const headers = useRealizedSlots<W>({
90
+ target: props.headerFactoryBinding ? widgetRef : null,
91
+ binding: props.headerFactoryBinding ?? props.factoryBinding,
92
+ estimatedHeight: props.estimatedHeight,
93
+ estimatedWidth: props.estimatedWidth,
94
+ });
95
+
96
+ useModelInstallation(widgetRef, installedModel, (widget, model) => props.modelBinding.install(widget, model));
97
+
98
+ return {
99
+ setRef,
100
+ resolver: listModel.resolver,
101
+ headerResolver: listModel.headerResolver,
102
+ itemStore: items.store,
103
+ headerStore: headers.store,
104
+ };
105
+ };
106
+
107
+ export const CollectionView = <T, S, W extends Gtk.Widget>(props: CollectionViewProps<T, S, W>): ReactNode => {
108
+ const { setRef, resolver, headerResolver, itemStore, headerStore } = useCollectionWiring(props);
109
+ const intrinsic: ReactElement = createElement(props.element, { ...props.intrinsicProps, ref: setRef });
110
+
111
+ return (
112
+ <>
113
+ {intrinsic}
114
+ <ListPortalHost store={itemStore} resolver={resolver} render={props.renderItem} />
115
+ {props.headerFactoryBinding ? (
116
+ <ListPortalHost
117
+ store={headerStore}
118
+ resolver={headerResolver}
119
+ render={headerRenderer<T, S>(props.renderHeader)}
120
+ />
121
+ ) : null}
122
+ {props.children}
123
+ </>
124
+ );
125
+ };
@@ -0,0 +1,73 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { GtkColumnViewColumn, type GtkColumnViewColumnProps } from "@gtkx/jsx/gtk";
3
+ import { createElement, type ReactNode, useLayoutEffect, useRef, useState } from "react";
4
+ import { useColumnViewContext } from "./contexts/column-view-context.js";
5
+ import { useHeaderMenu } from "./hooks/use-header-menu.js";
6
+ import { type FactoryBinding, useRealizedSlots } from "./hooks/use-realized-slots.js";
7
+ import { ListPortalHost } from "./list-portal-host.js";
8
+ import type { SlotRenderer } from "./list-slot.js";
9
+ import type { ColumnViewColumnProps } from "./types.js";
10
+
11
+ const factoryBinding: FactoryBinding<Gtk.ColumnViewColumn> = {
12
+ install: (column, factory) => column.setFactory(factory),
13
+ uninstall: (column) => column.setFactory(null),
14
+ };
15
+
16
+ /**
17
+ * Props for the {@link ColumnViewColumn} component: the raw
18
+ * `GtkColumnViewColumn` element surface (minus its imperative `factory` and
19
+ * `sorter` properties) with its cell wiring replaced by the declarative
20
+ * {@link ColumnViewColumnProps} API.
21
+ */
22
+ export type ColumnViewColumnComponentProps<T = unknown> = Omit<GtkColumnViewColumnProps, "factory" | "sorter"> &
23
+ ColumnViewColumnProps<T>;
24
+
25
+ /**
26
+ * A single column of a {@link ColumnView}, rendering each row's cell through
27
+ * the declarative `renderCell` callback and optionally attaching a header
28
+ * context menu.
29
+ */
30
+ export const ColumnViewColumn = <T = unknown>(props: ColumnViewColumnComponentProps<T>): ReactNode => {
31
+ const { id, title, expand, resizable, fixedWidth, visible, sortable, renderCell, headerMenu } = props;
32
+ const context = useColumnViewContext();
33
+ const [column, setColumn] = useState<Gtk.ColumnViewColumn | null>(null);
34
+
35
+ const captureColumn = useRef((value: Gtk.ColumnViewColumn | null) => {
36
+ setColumn(value);
37
+ }).current;
38
+
39
+ const { store } = useRealizedSlots<Gtk.ColumnViewColumn>({
40
+ target: column,
41
+ binding: factoryBinding,
42
+ });
43
+
44
+ const registerRef = useRef(context.register);
45
+ registerRef.current = context.register;
46
+ const unregisterRef = useRef(context.unregister);
47
+ unregisterRef.current = context.unregister;
48
+
49
+ useLayoutEffect(() => {
50
+ if (column === null) return;
51
+ registerRef.current({ id, column, sortable: sortable ?? false });
52
+ return () => unregisterRef.current(id);
53
+ }, [column, id, sortable]);
54
+
55
+ const headerMenuPortal = useHeaderMenu(column, headerMenu);
56
+
57
+ const slotRenderer: SlotRenderer<unknown, unknown> = (value, _treeRow, isHeader) =>
58
+ isHeader ? null : renderCell(value as T);
59
+
60
+ const intrinsicProps: Record<string, unknown> = { id, title, ref: captureColumn };
61
+ if (expand !== undefined) intrinsicProps["expand"] = expand;
62
+ if (resizable !== undefined) intrinsicProps["resizable"] = resizable;
63
+ if (fixedWidth !== undefined) intrinsicProps["fixedWidth"] = fixedWidth;
64
+ if (visible !== undefined) intrinsicProps["visible"] = visible;
65
+
66
+ return (
67
+ <>
68
+ {createElement(GtkColumnViewColumn, intrinsicProps)}
69
+ <ListPortalHost store={store} resolver={context.resolver} render={slotRenderer} />
70
+ {headerMenuPortal}
71
+ </>
72
+ );
73
+ };
@@ -0,0 +1,206 @@
1
+ import type * as Gio from "@gtkx/gi/gio";
2
+ import type * as Gtk from "@gtkx/gi/gtk";
3
+ import { GtkColumnView, type GtkColumnViewProps } from "@gtkx/jsx/gtk";
4
+ import { useForwardedRef } from "@gtkx/react";
5
+ import { createElement, type ReactNode, type Ref, useCallback, useMemo, useRef, useState } from "react";
6
+ import {
7
+ type ColumnRegistration,
8
+ ColumnViewContext,
9
+ type ColumnViewContextValue,
10
+ } from "./contexts/column-view-context.js";
11
+ import { useListModel } from "./hooks/use-list-model.js";
12
+ import { useModelInstallation } from "./hooks/use-model-installation.js";
13
+ import { type FactoryBinding, useRealizedSlots } from "./hooks/use-realized-slots.js";
14
+ import { useSelectionModel } from "./hooks/use-selection-model.js";
15
+ import { useSortHandler } from "./hooks/use-sort-handler.js";
16
+ import { ListPortalHost } from "./list-portal-host.js";
17
+ import type { SlotRenderer } from "./list-slot.js";
18
+ import type { ColumnViewProps } from "./types.js";
19
+ import type { ItemResolver } from "./utils/item-resolver.js";
20
+
21
+ const headerFactoryBinding: FactoryBinding<Gtk.ColumnView> = {
22
+ install: (widget, factory) => widget.setHeaderFactory(factory),
23
+ uninstall: (widget) => widget.setHeaderFactory(null),
24
+ };
25
+
26
+ interface ColumnRegistry {
27
+ columns: ColumnRegistration[];
28
+ register(registration: ColumnRegistration): void;
29
+ unregister(id: string): void;
30
+ }
31
+
32
+ const useColumnRegistry = (): ColumnRegistry => {
33
+ const [registrations, setRegistrations] = useState<Map<string, ColumnRegistration>>(() => new Map());
34
+ const register = useCallback((registration: ColumnRegistration): void => {
35
+ setRegistrations((current) => {
36
+ const next = new Map(current);
37
+ next.set(registration.id, registration);
38
+ return next;
39
+ });
40
+ }, []);
41
+ const unregister = useCallback((id: string): void => {
42
+ setRegistrations((current) => {
43
+ if (!current.has(id)) return current;
44
+ const next = new Map(current);
45
+ next.delete(id);
46
+ return next;
47
+ });
48
+ }, []);
49
+ const columns = useMemo(() => [...registrations.values()], [registrations]);
50
+ return { columns, register, unregister };
51
+ };
52
+
53
+ /**
54
+ * Props for the {@link ColumnView} component: the raw `GtkColumnView` element
55
+ * surface (minus its imperative `columns` property) with its model/sort wiring
56
+ * replaced by the declarative {@link ColumnViewProps} API. Columns are declared
57
+ * as {@link ColumnViewColumn} children.
58
+ */
59
+ export type ColumnViewComponentProps<T = unknown, S = unknown> = Omit<
60
+ GtkColumnViewProps,
61
+ "columns" | keyof ColumnViewProps<T, S>
62
+ > &
63
+ ColumnViewProps<T, S>;
64
+
65
+ type NormalizedColumnViewProps<T, S> = ColumnViewProps<T, S> & {
66
+ ref?: Ref<Gtk.ColumnView | null>;
67
+ renderHeader?: ((value: S) => ReactNode) | null;
68
+ children?: ReactNode;
69
+ [key: string]: unknown;
70
+ };
71
+
72
+ const headerRenderer =
73
+ <T, S>(renderHeader: ((value: S) => ReactNode) | null | undefined): SlotRenderer<T, S> =>
74
+ (value) =>
75
+ renderHeader ? renderHeader(value as S) : null;
76
+
77
+ interface ColumnViewWiring<T, S> {
78
+ setRef: (value: Gtk.ColumnView | null) => void;
79
+ resolver: ItemResolver<T, S>;
80
+ headerResolver: ItemResolver<T, S>;
81
+ headerStore: ReturnType<typeof useRealizedSlots>["store"];
82
+ useHeader: boolean;
83
+ contextValue: ColumnViewContextValue;
84
+ }
85
+
86
+ const useColumnViewWiring = <T, S>(
87
+ props: NormalizedColumnViewProps<T, S>,
88
+ registry: ColumnRegistry,
89
+ ): ColumnViewWiring<T, S> => {
90
+ const widgetRef = useRef<Gtk.ColumnView | null>(null);
91
+ const captureWidget = useCallback((value: Gtk.ColumnView | null) => {
92
+ widgetRef.current = value;
93
+ }, []);
94
+ const [, setRef] = useForwardedRef<Gtk.ColumnView>(props.ref, captureWidget);
95
+
96
+ const externalModel = props.model as Gio.ListModel | undefined;
97
+ const listModel = useListModel<T, S>(
98
+ externalModel === undefined ? { items: props.items } : { model: externalModel },
99
+ );
100
+
101
+ const controlledSelection = useSelectionModel<T, S>({
102
+ base: listModel.model,
103
+ selectionMode: props.selectionMode,
104
+ selected: props.selected,
105
+ onSelectionChanged: props.onSelectionChanged,
106
+ resolver: listModel.resolver,
107
+ });
108
+ const installedModel: Gtk.SelectionModel =
109
+ externalModel === undefined ? controlledSelection : (externalModel as Gtk.SelectionModel);
110
+ useModelInstallation(widgetRef, installedModel, (widget, model) => widget.setModel(model));
111
+
112
+ const useHeader = externalModel === undefined && typeof props.renderHeader === "function";
113
+ const headers = useRealizedSlots<Gtk.ColumnView>({
114
+ target: useHeader ? widgetRef : null,
115
+ binding: headerFactoryBinding,
116
+ estimatedHeight: props.estimatedRowHeight ?? undefined,
117
+ });
118
+
119
+ useSortHandler({
120
+ columnView: widgetRef,
121
+ sortColumn: props.sortColumn,
122
+ sortOrder: props.sortOrder,
123
+ onSortChanged: props.onSortChanged,
124
+ columns: registry.columns,
125
+ });
126
+
127
+ const contextValue = useMemo<ColumnViewContextValue>(
128
+ () => ({
129
+ columnView: widgetRef,
130
+ resolver: listModel.resolver as ItemResolver<unknown, unknown>,
131
+ register: registry.register,
132
+ unregister: registry.unregister,
133
+ }),
134
+ [listModel.resolver, registry.register, registry.unregister],
135
+ );
136
+
137
+ return {
138
+ setRef,
139
+ resolver: listModel.resolver,
140
+ headerResolver: listModel.headerResolver,
141
+ headerStore: headers.store,
142
+ useHeader,
143
+ contextValue,
144
+ };
145
+ };
146
+
147
+ /**
148
+ * A `GtkColumnView` driven by a declarative `items` model with optional
149
+ * controlled selection, controlled sorting, and section headers. Columns are
150
+ * declared as {@link ColumnViewColumn} children. Supplying an external `model`
151
+ * switches to the uncontrolled form.
152
+ */
153
+ export const ColumnView = <T = unknown, S = unknown>(props: ColumnViewComponentProps<T, S>): ReactNode => {
154
+ const {
155
+ ref,
156
+ items,
157
+ model,
158
+ selected,
159
+ selectionMode,
160
+ onSelectionChanged,
161
+ sortColumn,
162
+ sortOrder,
163
+ onSortChanged,
164
+ renderHeader,
165
+ estimatedRowHeight,
166
+ children,
167
+ ...intrinsicProps
168
+ } = props as NormalizedColumnViewProps<T, S>;
169
+
170
+ const registry = useColumnRegistry();
171
+ const wiring = useColumnViewWiring<T, S>(
172
+ {
173
+ ref,
174
+ items,
175
+ model,
176
+ selected,
177
+ selectionMode,
178
+ onSelectionChanged,
179
+ sortColumn,
180
+ sortOrder,
181
+ onSortChanged,
182
+ renderHeader,
183
+ estimatedRowHeight,
184
+ } as NormalizedColumnViewProps<T, S>,
185
+ registry,
186
+ );
187
+
188
+ const intrinsic = createElement(
189
+ GtkColumnView,
190
+ { ...intrinsicProps, ref: wiring.setRef },
191
+ <ColumnViewContext.Provider value={wiring.contextValue}>{children}</ColumnViewContext.Provider>,
192
+ );
193
+
194
+ return (
195
+ <>
196
+ {intrinsic}
197
+ {wiring.useHeader ? (
198
+ <ListPortalHost
199
+ store={wiring.headerStore}
200
+ resolver={wiring.headerResolver}
201
+ render={headerRenderer<T, S>(renderHeader)}
202
+ />
203
+ ) : null}
204
+ </>
205
+ );
206
+ };