@gtkx/components 0.21.0 → 1.0.0-rc.2

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 (192) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +173 -0
  3. package/dist/adw/index.d.ts +3 -0
  4. package/dist/adw/index.d.ts.map +1 -0
  5. package/dist/adw/index.js +2 -0
  6. package/dist/adw/index.js.map +1 -0
  7. package/dist/adw/toast.d.ts +10 -0
  8. package/dist/adw/toast.d.ts.map +1 -0
  9. package/dist/adw/toast.js +51 -0
  10. package/dist/adw/toast.js.map +1 -0
  11. package/dist/adw/types.d.ts +28 -0
  12. package/dist/adw/types.d.ts.map +1 -0
  13. package/dist/adw/types.js +2 -0
  14. package/dist/adw/types.js.map +1 -0
  15. package/dist/column-view.d.ts +6 -14
  16. package/dist/column-view.d.ts.map +1 -1
  17. package/dist/column-view.js +98 -100
  18. package/dist/column-view.js.map +1 -1
  19. package/dist/drop-down.d.ts +8 -37
  20. package/dist/drop-down.d.ts.map +1 -1
  21. package/dist/drop-down.js +109 -118
  22. package/dist/drop-down.js.map +1 -1
  23. package/dist/grid-view.d.ts +4 -11
  24. package/dist/grid-view.d.ts.map +1 -1
  25. package/dist/grid-view.js +25 -19
  26. package/dist/grid-view.js.map +1 -1
  27. package/dist/index.d.ts +6 -18
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -14
  30. package/dist/index.js.map +1 -1
  31. package/dist/internal/cells.d.ts +50 -0
  32. package/dist/internal/cells.d.ts.map +1 -0
  33. package/dist/internal/cells.js +185 -0
  34. package/dist/internal/cells.js.map +1 -0
  35. package/dist/internal/collection-index.d.ts +21 -0
  36. package/dist/internal/collection-index.d.ts.map +1 -0
  37. package/dist/internal/collection-index.js +106 -0
  38. package/dist/internal/collection-index.js.map +1 -0
  39. package/dist/internal/collection-model.d.ts +12 -0
  40. package/dist/internal/collection-model.d.ts.map +1 -0
  41. package/dist/internal/collection-model.js +164 -0
  42. package/dist/internal/collection-model.js.map +1 -0
  43. package/dist/internal/collection.d.ts +18 -0
  44. package/dist/internal/collection.d.ts.map +1 -0
  45. package/dist/internal/collection.js +64 -0
  46. package/dist/internal/collection.js.map +1 -0
  47. package/dist/internal/expansion.d.ts +9 -0
  48. package/dist/internal/expansion.d.ts.map +1 -0
  49. package/dist/internal/expansion.js +75 -0
  50. package/dist/internal/expansion.js.map +1 -0
  51. package/dist/internal/selection.d.ts +12 -0
  52. package/dist/internal/selection.d.ts.map +1 -0
  53. package/dist/internal/selection.js +99 -0
  54. package/dist/internal/selection.js.map +1 -0
  55. package/dist/internal/use-collection.d.ts +18 -0
  56. package/dist/internal/use-collection.d.ts.map +1 -0
  57. package/dist/internal/use-collection.js +42 -0
  58. package/dist/internal/use-collection.js.map +1 -0
  59. package/dist/internal/use-widget-ref.d.ts +4 -0
  60. package/dist/internal/use-widget-ref.d.ts.map +1 -0
  61. package/dist/internal/use-widget-ref.js +8 -0
  62. package/dist/internal/use-widget-ref.js.map +1 -0
  63. package/dist/list-view.d.ts +4 -11
  64. package/dist/list-view.d.ts.map +1 -1
  65. package/dist/list-view.js +32 -28
  66. package/dist/list-view.js.map +1 -1
  67. package/dist/size-group.d.ts +12 -0
  68. package/dist/size-group.d.ts.map +1 -0
  69. package/dist/size-group.js +41 -0
  70. package/dist/size-group.js.map +1 -0
  71. package/dist/types.d.ts +104 -161
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/types.js.map +1 -1
  74. package/package.json +62 -52
  75. package/src/adw/index.ts +2 -0
  76. package/src/adw/toast.tsx +74 -0
  77. package/src/adw/types.ts +32 -0
  78. package/src/column-view.tsx +184 -184
  79. package/src/drop-down.tsx +173 -227
  80. package/src/grid-view.tsx +29 -65
  81. package/src/index.ts +16 -25
  82. package/src/internal/cells.tsx +345 -0
  83. package/src/internal/collection-index.ts +168 -0
  84. package/src/internal/collection-model.ts +246 -0
  85. package/src/internal/collection.ts +104 -0
  86. package/src/internal/expansion.ts +123 -0
  87. package/src/internal/selection.tsx +159 -0
  88. package/src/internal/use-collection.tsx +70 -0
  89. package/src/internal/use-widget-ref.ts +11 -0
  90. package/src/list-view.tsx +45 -78
  91. package/src/size-group.tsx +77 -0
  92. package/src/types.ts +171 -191
  93. package/dist/collection-view.d.ts +0 -30
  94. package/dist/collection-view.d.ts.map +0 -1
  95. package/dist/collection-view.js +0 -52
  96. package/dist/collection-view.js.map +0 -1
  97. package/dist/column-view-column.d.ts +0 -17
  98. package/dist/column-view-column.d.ts.map +0 -1
  99. package/dist/column-view-column.js +0 -51
  100. package/dist/column-view-column.js.map +0 -1
  101. package/dist/constraint-layout-apply.d.ts +0 -6
  102. package/dist/constraint-layout-apply.d.ts.map +0 -1
  103. package/dist/constraint-layout-apply.js +0 -107
  104. package/dist/constraint-layout-apply.js.map +0 -1
  105. package/dist/constraint-layout.d.ts +0 -13
  106. package/dist/constraint-layout.d.ts.map +0 -1
  107. package/dist/constraint-layout.js +0 -85
  108. package/dist/constraint-layout.js.map +0 -1
  109. package/dist/contexts/column-view-context.d.ts +0 -17
  110. package/dist/contexts/column-view-context.d.ts.map +0 -1
  111. package/dist/contexts/column-view-context.js +0 -10
  112. package/dist/contexts/column-view-context.js.map +0 -1
  113. package/dist/hooks/use-drop-down-selection.d.ts +0 -15
  114. package/dist/hooks/use-drop-down-selection.d.ts.map +0 -1
  115. package/dist/hooks/use-drop-down-selection.js +0 -35
  116. package/dist/hooks/use-drop-down-selection.js.map +0 -1
  117. package/dist/hooks/use-header-menu.d.ts +0 -4
  118. package/dist/hooks/use-header-menu.d.ts.map +0 -1
  119. package/dist/hooks/use-header-menu.js +0 -32
  120. package/dist/hooks/use-header-menu.js.map +0 -1
  121. package/dist/hooks/use-list-model.d.ts +0 -20
  122. package/dist/hooks/use-list-model.d.ts.map +0 -1
  123. package/dist/hooks/use-list-model.js +0 -70
  124. package/dist/hooks/use-list-model.js.map +0 -1
  125. package/dist/hooks/use-model-installation.d.ts +0 -9
  126. package/dist/hooks/use-model-installation.d.ts.map +0 -1
  127. package/dist/hooks/use-model-installation.js +0 -20
  128. package/dist/hooks/use-model-installation.js.map +0 -1
  129. package/dist/hooks/use-realized-slots.d.ts +0 -20
  130. package/dist/hooks/use-realized-slots.d.ts.map +0 -1
  131. package/dist/hooks/use-realized-slots.js +0 -66
  132. package/dist/hooks/use-realized-slots.js.map +0 -1
  133. package/dist/hooks/use-selection-model.d.ts +0 -12
  134. package/dist/hooks/use-selection-model.d.ts.map +0 -1
  135. package/dist/hooks/use-selection-model.js +0 -111
  136. package/dist/hooks/use-selection-model.js.map +0 -1
  137. package/dist/hooks/use-sort-handler.d.ts +0 -12
  138. package/dist/hooks/use-sort-handler.d.ts.map +0 -1
  139. package/dist/hooks/use-sort-handler.js +0 -87
  140. package/dist/hooks/use-sort-handler.js.map +0 -1
  141. package/dist/list-portal-host.d.ts +0 -11
  142. package/dist/list-portal-host.d.ts.map +0 -1
  143. package/dist/list-portal-host.js +0 -8
  144. package/dist/list-portal-host.js.map +0 -1
  145. package/dist/list-slot.d.ts +0 -14
  146. package/dist/list-slot.d.ts.map +0 -1
  147. package/dist/list-slot.js +0 -29
  148. package/dist/list-slot.js.map +0 -1
  149. package/dist/menu-items.d.ts +0 -5
  150. package/dist/menu-items.d.ts.map +0 -1
  151. package/dist/menu-items.js +0 -48
  152. package/dist/menu-items.js.map +0 -1
  153. package/dist/menu.d.ts +0 -18
  154. package/dist/menu.d.ts.map +0 -1
  155. package/dist/menu.js +0 -26
  156. package/dist/menu.js.map +0 -1
  157. package/dist/utils/item-resolver.d.ts +0 -28
  158. package/dist/utils/item-resolver.d.ts.map +0 -1
  159. package/dist/utils/item-resolver.js +0 -111
  160. package/dist/utils/item-resolver.js.map +0 -1
  161. package/dist/utils/list-item-flatten.d.ts +0 -32
  162. package/dist/utils/list-item-flatten.d.ts.map +0 -1
  163. package/dist/utils/list-item-flatten.js +0 -88
  164. package/dist/utils/list-item-flatten.js.map +0 -1
  165. package/dist/utils/position-only-model.d.ts +0 -10
  166. package/dist/utils/position-only-model.d.ts.map +0 -1
  167. package/dist/utils/position-only-model.js +0 -83
  168. package/dist/utils/position-only-model.js.map +0 -1
  169. package/dist/utils/realized-slot-store.d.ts +0 -30
  170. package/dist/utils/realized-slot-store.d.ts.map +0 -1
  171. package/dist/utils/realized-slot-store.js +0 -103
  172. package/dist/utils/realized-slot-store.js.map +0 -1
  173. package/src/collection-view.tsx +0 -125
  174. package/src/column-view-column.tsx +0 -73
  175. package/src/constraint-layout-apply.ts +0 -121
  176. package/src/constraint-layout.tsx +0 -130
  177. package/src/contexts/column-view-context.ts +0 -28
  178. package/src/hooks/use-drop-down-selection.ts +0 -53
  179. package/src/hooks/use-header-menu.ts +0 -33
  180. package/src/hooks/use-list-model.ts +0 -145
  181. package/src/hooks/use-model-installation.ts +0 -25
  182. package/src/hooks/use-realized-slots.ts +0 -93
  183. package/src/hooks/use-selection-model.ts +0 -121
  184. package/src/hooks/use-sort-handler.ts +0 -102
  185. package/src/list-portal-host.tsx +0 -27
  186. package/src/list-slot.tsx +0 -56
  187. package/src/menu-items.ts +0 -48
  188. package/src/menu.tsx +0 -34
  189. package/src/utils/item-resolver.ts +0 -143
  190. package/src/utils/list-item-flatten.ts +0 -113
  191. package/src/utils/position-only-model.ts +0 -115
  192. package/src/utils/realized-slot-store.ts +0 -126
package/package.json CHANGED
@@ -1,55 +1,65 @@
1
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"
2
+ "name": "@gtkx/components",
3
+ "version": "1.0.0-rc.2",
4
+ "description": "High-level GTKX components for declarative collection views, menus, and containers with attach-time placement",
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": "git+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
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
18
32
  },
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"
33
+ "./adw": {
34
+ "types": "./dist/adw/index.d.ts",
35
+ "default": "./dist/adw/index.js"
54
36
  }
55
- }
37
+ },
38
+ "sideEffects": false,
39
+ "files": [
40
+ "dist",
41
+ "src"
42
+ ],
43
+ "engines": {
44
+ "node": ">=24"
45
+ },
46
+ "dependencies": {
47
+ "@gtkx/react": "1.0.0-rc.2",
48
+ "@gtkx/utils": "1.0.0-rc.2"
49
+ },
50
+ "peerDependencies": {
51
+ "react": "^19.2"
52
+ },
53
+ "devDependencies": {
54
+ "@codspeed/vitest-plugin": "^5.7.1",
55
+ "vitest": "^4.1.10",
56
+ "@gtkx/config": "1.0.0-rc.2",
57
+ "@gtkx/runtime": "1.0.0-rc.2",
58
+ "@gtkx/testing": "1.0.0-rc.2",
59
+ "@gtkx/vitest": "1.0.0-rc.2"
60
+ },
61
+ "scripts": {
62
+ "bench": "NODE_ENV=production vitest bench --run --config vitest.bench.config.ts",
63
+ "release": "tsx ../../scripts/release-package.ts"
64
+ }
65
+ }
@@ -0,0 +1,2 @@
1
+ export { ToastProvider, useToast, useToastOverlay } from "./toast.js";
2
+ export type { ToastController, ToastOptions, ToastOverlayController, ToastProviderProps } from "./types.js";
@@ -0,0 +1,74 @@
1
+ import type { ReactNode, RefObject } from "react";
2
+ import * as Adw from "@gtkx/gi/adw";
3
+ import { createContext, useContext, useMemo } from "react";
4
+ import type { ToastController, ToastOptions, ToastOverlayController, ToastProviderProps } from "./types.js";
5
+
6
+ type OverlayRef = RefObject<Adw.ToastOverlay | null>;
7
+
8
+ const ToastContext = createContext<OverlayRef | null>(null);
9
+
10
+ const useOverlayRef = (): OverlayRef => {
11
+ const overlayRef = useContext(ToastContext);
12
+
13
+ if (overlayRef === null) {
14
+ throw new Error("useToast and useToastOverlay must be used within a ToastProvider");
15
+ }
16
+
17
+ return overlayRef;
18
+ };
19
+
20
+ const buildToast = (options: ToastOptions): Adw.Toast => {
21
+ const { onButtonClicked, onDismissed, ...props } = options;
22
+ const toast = new Adw.Toast(props);
23
+
24
+ if (onButtonClicked != null) {
25
+ toast.on("button-clicked", onButtonClicked);
26
+ }
27
+
28
+ if (onDismissed != null) {
29
+ toast.on("dismissed", onDismissed);
30
+ }
31
+
32
+ return toast;
33
+ };
34
+
35
+ /** Shares an Adw.ToastOverlay reference with the toast hooks in every descendant. */
36
+ function ToastProvider(props: ToastProviderProps): ReactNode {
37
+ return <ToastContext.Provider value={props.overlayRef}>{props.children}</ToastContext.Provider>;
38
+ }
39
+
40
+ /** Returns a {@link ToastController} that shows and dismisses toasts on the nearest provider's overlay. */
41
+ function useToast(): ToastController {
42
+ const overlayRef = useOverlayRef();
43
+
44
+ return useMemo<ToastController>(
45
+ () => ({
46
+ show: (options?: ToastOptions) => {
47
+ const toast = buildToast(options ?? {});
48
+ overlayRef.current?.addToast(toast);
49
+
50
+ return toast;
51
+ },
52
+ dismiss: (toast: Adw.Toast) => {
53
+ toast.dismiss();
54
+ },
55
+ }),
56
+ [overlayRef],
57
+ );
58
+ }
59
+
60
+ /** Returns a {@link ToastOverlayController} for the nearest provider's overlay as a whole. */
61
+ function useToastOverlay(): ToastOverlayController {
62
+ const overlayRef = useOverlayRef();
63
+
64
+ return useMemo<ToastOverlayController>(
65
+ () => ({
66
+ dismissAll: () => {
67
+ overlayRef.current?.dismissAll();
68
+ },
69
+ }),
70
+ [overlayRef],
71
+ );
72
+ }
73
+
74
+ export { ToastProvider, useToast, useToastOverlay };
@@ -0,0 +1,32 @@
1
+ import type * as Adw from "@gtkx/gi/adw";
2
+ import type { AdwToastProps } from "@gtkx/jsx/adw";
3
+ import type { ReactNode, RefObject } from "react";
4
+
5
+ /**
6
+ * Describes a toast raised through {@link useToast}: the construct-time properties of an
7
+ * `Adw.Toast` plus its `button-clicked` and `dismissed` handlers.
8
+ */
9
+ type ToastOptions = Adw.ToastConstructorProps & Pick<AdwToastProps, "onButtonClicked" | "onDismissed">;
10
+
11
+ /** Imperative controls for individual toasts, returned by {@link useToast}. */
12
+ type ToastController = {
13
+ /** Builds a toast, shows it through the overlay, and returns it. */
14
+ show: (options?: ToastOptions) => Adw.Toast;
15
+ /** Dismisses a single toast, typically one returned by {@link ToastController.show}. */
16
+ dismiss: (toast: Adw.Toast) => void;
17
+ };
18
+
19
+ /** Imperative controls for the overlay as a whole, returned by {@link useToastOverlay}. */
20
+ type ToastOverlayController = {
21
+ /** Dismisses the shown toast and every queued one. */
22
+ dismissAll: () => void;
23
+ };
24
+
25
+ /** Props for {@link ToastProvider}. */
26
+ type ToastProviderProps = {
27
+ /** Ref also given to the `AdwToastOverlay` the toasts appear over. */
28
+ overlayRef: RefObject<Adw.ToastOverlay | null>;
29
+ children?: ReactNode | undefined;
30
+ };
31
+
32
+ export { type ToastOptions, type ToastController, type ToastOverlayController, type ToastProviderProps };
@@ -1,206 +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),
1
+ import type { ReactNode } from "react";
2
+ import * as Gtk from "@gtkx/gi/gtk";
3
+ import { GtkColumnView, GtkColumnViewColumn, GtkCustomSorter, GtkSignalListItemFactory } from "@gtkx/jsx/gtk";
4
+ import { useProperty, useSignal } from "@gtkx/react";
5
+ import { omit } from "@gtkx/utils";
6
+ import { useLayoutEffect, useRef } from "react";
7
+ import type { CellSize } from "./internal/cells.js";
8
+ import type { Collection } from "./internal/collection.js";
9
+ import type { Column, ColumnViewProps } from "./types.js";
10
+ import { HeaderPortals, ItemPortals, useHeaderCells, useItemCells } from "./internal/cells.js";
11
+ import { useCollection } from "./internal/use-collection.js";
12
+ import { useWidgetRef } from "./internal/use-widget-ref.js";
13
+
14
+ type SortProps = {
15
+ sortColumn?: string | null | undefined;
16
+ sortOrder?: Gtk.SortType | null | undefined;
17
+ onSortChanged?: ((column: string | null, order: Gtk.SortType) => void) | null | undefined;
24
18
  };
25
19
 
26
- interface ColumnRegistry {
27
- columns: ColumnRegistration[];
28
- register(registration: ColumnRegistration): void;
29
- unregister(id: string): void;
30
- }
20
+ type SortTarget = { view: Gtk.ColumnView; column: Gtk.ColumnViewColumn | null };
31
21
 
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 };
22
+ type ColumnCellsProps = {
23
+ column: Column<never>;
24
+ collection: Collection;
25
+ expandedIds: string[] | null | undefined;
26
+ size: CellSize;
51
27
  };
52
28
 
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;
29
+ type ColumnListProps = Omit<ColumnCellsProps, "column"> & {
30
+ columns: Column<never>[];
70
31
  };
71
32
 
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
- }
33
+ const COLUMN_VIEW_PROPS = [
34
+ "items",
35
+ "sections",
36
+ "renderHeader",
37
+ "columns",
38
+ "selectedIds",
39
+ "onSelectionChanged",
40
+ "selectionMode",
41
+ "expandedIds",
42
+ "onExpandedChange",
43
+ "sortColumn",
44
+ "sortOrder",
45
+ "onSortChanged",
46
+ "estimatedItemHeight",
47
+ "children",
48
+ "ref",
49
+ ] as const satisfies (keyof ColumnViewProps)[];
50
+
51
+ const columnById = (view: Gtk.ColumnView, id: string): Gtk.ColumnViewColumn | null => {
52
+ const columns = view.getColumns();
53
+
54
+ for (let index = 0; index < columns.getNItems(); index++) {
55
+ const column = columns.getItem(index);
56
+
57
+ if (column instanceof Gtk.ColumnViewColumn && column.getId() === id) {
58
+ return column;
59
+ }
60
+ }
61
+
62
+ return null;
63
+ };
85
64
 
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
- );
65
+ const getSortTarget = (view: Gtk.ColumnView | null, sortColumn: string | null | undefined): SortTarget | null => {
66
+ if (view === null || sortColumn === undefined) {
67
+ return null;
68
+ }
100
69
 
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
- });
70
+ if (sortColumn === null) {
71
+ return { view, column: null };
72
+ }
73
+
74
+ const column = columnById(view, sortColumn);
75
+
76
+ return column === null ? null : { view, column };
77
+ };
78
+
79
+ const applySort = (
80
+ sorting: { current: boolean },
81
+ view: Gtk.ColumnView,
82
+ column: Gtk.ColumnViewColumn | null,
83
+ order: Gtk.SortType | null | undefined,
84
+ ): void => {
85
+ sorting.current = true;
86
+
87
+ try {
88
+ view.sortByColumn(column, order ?? Gtk.SortType.ASCENDING);
89
+ } finally {
90
+ sorting.current = false;
91
+ }
92
+ };
93
+
94
+ const emitSortChanged = (
95
+ sorter: Gtk.ColumnViewSorter | null,
96
+ sorting: { current: boolean },
97
+ sort: SortProps,
98
+ ): void => {
99
+ if (sorter === null || sorting.current) {
100
+ return;
101
+ }
102
+
103
+ sort.onSortChanged?.(sorter.getPrimarySortColumn()?.getId() ?? null, sorter.getPrimarySortOrder());
104
+ };
105
+
106
+ const syncSort = (
107
+ sorting: { current: boolean },
108
+ view: Gtk.ColumnView | null,
109
+ sortColumn: string | null | undefined,
110
+ sortOrder: Gtk.SortType | null | undefined,
111
+ ): void => {
112
+ const target = getSortTarget(view, sortColumn);
113
+
114
+ if (target === null) {
115
+ return;
116
+ }
118
117
 
119
- useSortHandler({
120
- columnView: widgetRef,
121
- sortColumn: props.sortColumn,
122
- sortOrder: props.sortOrder,
123
- onSortChanged: props.onSortChanged,
124
- columns: registry.columns,
118
+ applySort(sorting, target.view, target.column, sortOrder);
119
+ };
120
+
121
+ const useColumnSorting = (view: Gtk.ColumnView | null, sort: SortProps, columns: Column<never>[]): void => {
122
+ const sorting = useRef(false);
123
+ const sorter = useProperty(view, "sorter");
124
+ const columnSorter = sorter instanceof Gtk.ColumnViewSorter ? sorter : null;
125
+
126
+ useSignal(columnSorter, "changed", (): void => {
127
+ emitSortChanged(columnSorter, sorting, sort);
125
128
  });
126
129
 
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
- );
130
+ const { sortColumn, sortOrder } = sort;
136
131
 
137
- return {
138
- setRef,
139
- resolver: listModel.resolver,
140
- headerResolver: listModel.headerResolver,
141
- headerStore: headers.store,
142
- useHeader,
143
- contextValue,
144
- };
132
+ useLayoutEffect(() => {
133
+ syncSort(sorting, view, sortColumn, sortOrder);
134
+ }, [view, sortColumn, sortOrder, columns]);
145
135
  };
146
136
 
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
- );
137
+ const ColumnCells = ({ column, collection, expandedIds, size }: ColumnCellsProps): ReactNode => {
138
+ const cells = useItemCells(size);
139
+ const rest = omit(column, ["id", "renderCell", "sortable"]);
187
140
 
188
- const intrinsic = createElement(
189
- GtkColumnView,
190
- { ...intrinsicProps, ref: wiring.setRef },
191
- <ColumnViewContext.Provider value={wiring.contextValue}>{children}</ColumnViewContext.Provider>,
141
+ return (
142
+ <>
143
+ <GtkColumnViewColumn
144
+ id={column.id}
145
+ factory={<GtkSignalListItemFactory {...cells.handlers} />}
146
+ sorter={column.sortable === true ? <GtkCustomSorter /> : null}
147
+ {...rest}
148
+ />
149
+ <ItemPortals
150
+ store={cells}
151
+ render={column.renderCell}
152
+ collection={collection}
153
+ expandedIds={expandedIds}
154
+ />
155
+ </>
192
156
  );
157
+ };
158
+
159
+ const ColumnList = ({ columns, collection, expandedIds, size }: ColumnListProps): ReactNode =>
160
+ columns.map((column) => (
161
+ <ColumnCells
162
+ key={column.id}
163
+ column={column}
164
+ collection={collection}
165
+ expandedIds={expandedIds}
166
+ size={size}
167
+ />
168
+ ));
169
+
170
+ /**
171
+ * Renders a Gtk.ColumnView from declarative items or sections and a columns array,
172
+ * with controlled selection, expansion, and sorting, per-column header menus, and
173
+ * section header rendering.
174
+ */
175
+ function ColumnView<T = unknown, S = unknown>(props: ColumnViewProps<T, S>): ReactNode {
176
+ const { renderHeader, columns, expandedIds, sortColumn, sortOrder, onSortChanged, estimatedItemHeight, ref } =
177
+ props;
178
+
179
+ const rest = omit(props, COLUMN_VIEW_PROPS);
180
+ const [view, refCallback] = useWidgetRef<Gtk.ColumnView>(ref);
181
+ const size = { width: -1, height: estimatedItemHeight ?? -1 };
182
+ const { collection, selection } = useCollection(props);
183
+ const headerCells = useHeaderCells(size);
184
+ const columnList = columns as Column<never>[];
185
+ useColumnSorting(view, { sortColumn, sortOrder, onSortChanged }, columnList);
193
186
 
194
187
  return (
195
188
  <>
196
- {intrinsic}
197
- {wiring.useHeader ? (
198
- <ListPortalHost
199
- store={wiring.headerStore}
200
- resolver={wiring.headerResolver}
201
- render={headerRenderer<T, S>(renderHeader)}
202
- />
203
- ) : null}
189
+ <GtkColumnView
190
+ ref={refCallback}
191
+ model={selection}
192
+ {...(renderHeader != null && {
193
+ headerFactory: <GtkSignalListItemFactory {...headerCells.handlers} />,
194
+ })}
195
+ {...rest}
196
+ >
197
+ <ColumnList columns={columnList} collection={collection} expandedIds={expandedIds} size={size} />
198
+ </GtkColumnView>
199
+ {renderHeader != null && (
200
+ <HeaderPortals store={headerCells} render={renderHeader} collection={collection} />
201
+ )}
204
202
  </>
205
203
  );
206
- };
204
+ }
205
+
206
+ export { ColumnView };