@gtkx/components 0.21.0 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +173 -0
  3. package/dist/cell.d.ts +30 -0
  4. package/dist/cell.d.ts.map +1 -0
  5. package/dist/cell.js +45 -0
  6. package/dist/cell.js.map +1 -0
  7. package/dist/column-view-column.d.ts +24 -14
  8. package/dist/column-view-column.d.ts.map +1 -1
  9. package/dist/column-view-column.js +19 -40
  10. package/dist/column-view-column.js.map +1 -1
  11. package/dist/column-view.d.ts +25 -11
  12. package/dist/column-view.d.ts.map +1 -1
  13. package/dist/column-view.js +53 -83
  14. package/dist/column-view.js.map +1 -1
  15. package/dist/constraint-layout-apply.d.ts +50 -1
  16. package/dist/constraint-layout-apply.d.ts.map +1 -1
  17. package/dist/constraint-layout-apply.js.map +1 -1
  18. package/dist/constraint-layout.d.ts +8 -1
  19. package/dist/constraint-layout.d.ts.map +1 -1
  20. package/dist/constraint-layout.js +12 -9
  21. package/dist/constraint-layout.js.map +1 -1
  22. package/dist/drop-down.d.ts +26 -36
  23. package/dist/drop-down.d.ts.map +1 -1
  24. package/dist/drop-down.js +47 -87
  25. package/dist/drop-down.js.map +1 -1
  26. package/dist/fixed.d.ts +26 -0
  27. package/dist/fixed.d.ts.map +1 -0
  28. package/dist/fixed.js +40 -0
  29. package/dist/fixed.js.map +1 -0
  30. package/dist/grid-view.d.ts +13 -10
  31. package/dist/grid-view.d.ts.map +1 -1
  32. package/dist/grid-view.js +30 -16
  33. package/dist/grid-view.js.map +1 -1
  34. package/dist/grid.d.ts +27 -0
  35. package/dist/grid.d.ts.map +1 -0
  36. package/dist/grid.js +48 -0
  37. package/dist/grid.js.map +1 -0
  38. package/dist/hooks/use-cell-containers.d.ts +18 -0
  39. package/dist/hooks/use-cell-containers.d.ts.map +1 -0
  40. package/dist/hooks/{use-realized-slots.js → use-cell-containers.js} +18 -17
  41. package/dist/hooks/use-cell-containers.js.map +1 -0
  42. package/dist/hooks/use-collection-model.d.ts +26 -0
  43. package/dist/hooks/use-collection-model.d.ts.map +1 -0
  44. package/dist/hooks/use-collection-model.js +35 -0
  45. package/dist/hooks/use-collection-model.js.map +1 -0
  46. package/dist/hooks/use-collection-widget.d.ts +28 -0
  47. package/dist/hooks/use-collection-widget.d.ts.map +1 -0
  48. package/dist/hooks/use-collection-widget.js +21 -0
  49. package/dist/hooks/use-collection-widget.js.map +1 -0
  50. package/dist/hooks/use-drop-down-selection.d.ts +7 -6
  51. package/dist/hooks/use-drop-down-selection.d.ts.map +1 -1
  52. package/dist/hooks/use-drop-down-selection.js +6 -6
  53. package/dist/hooks/use-drop-down-selection.js.map +1 -1
  54. package/dist/hooks/use-expansion-model.d.ts +12 -0
  55. package/dist/hooks/use-expansion-model.d.ts.map +1 -0
  56. package/dist/hooks/use-expansion-model.js +71 -0
  57. package/dist/hooks/use-expansion-model.js.map +1 -0
  58. package/dist/hooks/use-header-menu.d.ts.map +1 -1
  59. package/dist/hooks/use-header-menu.js +12 -24
  60. package/dist/hooks/use-header-menu.js.map +1 -1
  61. package/dist/hooks/use-installed-model.d.ts +4 -0
  62. package/dist/hooks/use-installed-model.d.ts.map +1 -0
  63. package/dist/hooks/use-installed-model.js +15 -0
  64. package/dist/hooks/use-installed-model.js.map +1 -0
  65. package/dist/hooks/use-list-model.d.ts +14 -15
  66. package/dist/hooks/use-list-model.d.ts.map +1 -1
  67. package/dist/hooks/use-list-model.js +61 -34
  68. package/dist/hooks/use-list-model.js.map +1 -1
  69. package/dist/hooks/use-placed-child.d.ts +18 -0
  70. package/dist/hooks/use-placed-child.d.ts.map +1 -0
  71. package/dist/hooks/use-placed-child.js +47 -0
  72. package/dist/hooks/use-placed-child.js.map +1 -0
  73. package/dist/hooks/use-selection-model.d.ts +4 -3
  74. package/dist/hooks/use-selection-model.d.ts.map +1 -1
  75. package/dist/hooks/use-selection-model.js +8 -16
  76. package/dist/hooks/use-selection-model.js.map +1 -1
  77. package/dist/hooks/use-sort-handler.d.ts +10 -5
  78. package/dist/hooks/use-sort-handler.d.ts.map +1 -1
  79. package/dist/hooks/use-sort-handler.js +2 -2
  80. package/dist/hooks/use-sort-handler.js.map +1 -1
  81. package/dist/index.d.ts +11 -17
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +6 -13
  84. package/dist/index.js.map +1 -1
  85. package/dist/list-view.d.ts +17 -9
  86. package/dist/list-view.d.ts.map +1 -1
  87. package/dist/list-view.js +49 -24
  88. package/dist/list-view.js.map +1 -1
  89. package/dist/menu-items.d.ts.map +1 -1
  90. package/dist/menu-items.js.map +1 -1
  91. package/dist/menu.d.ts +6 -10
  92. package/dist/menu.d.ts.map +1 -1
  93. package/dist/menu.js +7 -9
  94. package/dist/menu.js.map +1 -1
  95. package/dist/overlay.d.ts +24 -0
  96. package/dist/overlay.d.ts.map +1 -0
  97. package/dist/overlay.js +49 -0
  98. package/dist/overlay.js.map +1 -0
  99. package/dist/size-group.d.ts +20 -0
  100. package/dist/size-group.d.ts.map +1 -0
  101. package/dist/size-group.js +57 -0
  102. package/dist/size-group.js.map +1 -0
  103. package/dist/types.d.ts +43 -173
  104. package/dist/types.d.ts.map +1 -1
  105. package/dist/types.js.map +1 -1
  106. package/dist/utils/{realized-slot-store.d.ts → cell-container-store.d.ts} +7 -9
  107. package/dist/utils/cell-container-store.d.ts.map +1 -0
  108. package/dist/utils/{realized-slot-store.js → cell-container-store.js} +9 -14
  109. package/dist/utils/cell-container-store.js.map +1 -0
  110. package/dist/utils/item-models.d.ts +10 -0
  111. package/dist/utils/item-models.d.ts.map +1 -0
  112. package/dist/utils/{position-only-model.js → item-models.js} +13 -14
  113. package/dist/utils/item-models.js.map +1 -0
  114. package/dist/utils/item-resolver.d.ts +14 -17
  115. package/dist/utils/item-resolver.d.ts.map +1 -1
  116. package/dist/utils/item-resolver.js +34 -43
  117. package/dist/utils/item-resolver.js.map +1 -1
  118. package/dist/utils/list-item-flatten.d.ts +16 -24
  119. package/dist/utils/list-item-flatten.d.ts.map +1 -1
  120. package/dist/utils/list-item-flatten.js +16 -36
  121. package/dist/utils/list-item-flatten.js.map +1 -1
  122. package/package.json +52 -53
  123. package/src/cell.tsx +144 -0
  124. package/src/column-view-column.tsx +57 -48
  125. package/src/column-view.tsx +130 -146
  126. package/src/constraint-layout-apply.ts +53 -1
  127. package/src/constraint-layout.tsx +18 -13
  128. package/src/drop-down.tsx +129 -196
  129. package/src/fixed.tsx +86 -0
  130. package/src/grid-view.tsx +52 -44
  131. package/src/grid.tsx +88 -0
  132. package/src/hooks/{use-realized-slots.ts → use-cell-containers.ts} +30 -30
  133. package/src/hooks/use-collection-model.ts +60 -0
  134. package/src/hooks/use-collection-widget.ts +47 -0
  135. package/src/hooks/use-drop-down-selection.ts +12 -13
  136. package/src/hooks/use-expansion-model.ts +82 -0
  137. package/src/hooks/use-header-menu.ts +24 -23
  138. package/src/hooks/use-installed-model.ts +20 -0
  139. package/src/hooks/use-list-model.ts +103 -87
  140. package/src/hooks/use-placed-child.ts +79 -0
  141. package/src/hooks/use-selection-model.ts +11 -18
  142. package/src/hooks/use-sort-handler.ts +13 -8
  143. package/src/index.ts +25 -27
  144. package/src/list-view.tsx +120 -57
  145. package/src/menu.tsx +12 -14
  146. package/src/overlay.tsx +88 -0
  147. package/src/size-group.tsx +96 -0
  148. package/src/types.ts +55 -206
  149. package/src/utils/{realized-slot-store.ts → cell-container-store.ts} +13 -25
  150. package/src/utils/{position-only-model.ts → item-models.ts} +26 -28
  151. package/src/utils/item-resolver.ts +54 -59
  152. package/src/utils/list-item-flatten.ts +33 -56
  153. package/dist/collection-view.d.ts +0 -30
  154. package/dist/collection-view.d.ts.map +0 -1
  155. package/dist/collection-view.js +0 -52
  156. package/dist/collection-view.js.map +0 -1
  157. package/dist/contexts/column-view-context.d.ts +0 -17
  158. package/dist/contexts/column-view-context.d.ts.map +0 -1
  159. package/dist/contexts/column-view-context.js +0 -10
  160. package/dist/contexts/column-view-context.js.map +0 -1
  161. package/dist/hooks/use-model-installation.d.ts +0 -9
  162. package/dist/hooks/use-model-installation.d.ts.map +0 -1
  163. package/dist/hooks/use-model-installation.js +0 -20
  164. package/dist/hooks/use-model-installation.js.map +0 -1
  165. package/dist/hooks/use-realized-slots.d.ts +0 -20
  166. package/dist/hooks/use-realized-slots.d.ts.map +0 -1
  167. package/dist/hooks/use-realized-slots.js.map +0 -1
  168. package/dist/list-portal-host.d.ts +0 -11
  169. package/dist/list-portal-host.d.ts.map +0 -1
  170. package/dist/list-portal-host.js +0 -8
  171. package/dist/list-portal-host.js.map +0 -1
  172. package/dist/list-slot.d.ts +0 -14
  173. package/dist/list-slot.d.ts.map +0 -1
  174. package/dist/list-slot.js +0 -29
  175. package/dist/list-slot.js.map +0 -1
  176. package/dist/utils/position-only-model.d.ts +0 -10
  177. package/dist/utils/position-only-model.d.ts.map +0 -1
  178. package/dist/utils/position-only-model.js.map +0 -1
  179. package/dist/utils/realized-slot-store.d.ts.map +0 -1
  180. package/dist/utils/realized-slot-store.js.map +0 -1
  181. package/src/collection-view.tsx +0 -125
  182. package/src/contexts/column-view-context.ts +0 -28
  183. package/src/hooks/use-model-installation.ts +0 -25
  184. package/src/list-portal-host.tsx +0 -27
  185. package/src/list-slot.tsx +0 -56
@@ -1,72 +1,81 @@
1
1
  import type * as Gtk from "@gtkx/gi/gtk";
2
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";
3
+ import { useMergeRefs } from "@gtkx/react/internal";
4
+ import { type ReactNode, useLayoutEffect, useRef, useState } from "react";
5
+ import { type CellRenderer, CellRenderHost, itemRenderer, type TreeRenderContext } from "./cell.js";
6
+ import { makeFactoryInstaller, useCellContainers } from "./hooks/use-cell-containers.js";
5
7
  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";
8
+ import type { RenderItemProps } from "./types.js";
9
+ import type { ItemResolver } from "./utils/item-resolver.js";
10
10
 
11
- const factoryBinding: FactoryBinding<Gtk.ColumnViewColumn> = {
12
- install: (column, factory) => column.setFactory(factory),
13
- uninstall: (column) => column.setFactory(null),
11
+ const factoryInstaller = makeFactoryInstaller<Gtk.ColumnViewColumn>((column, factory) => column.setFactory(factory));
12
+
13
+ export type ColumnDefDeclarativeProps<T = unknown> = {
14
+ title: string;
15
+ expand?: boolean | undefined;
16
+ resizable?: boolean | undefined;
17
+ fixedWidth?: number | undefined;
18
+ id: string;
19
+ sortable?: boolean | undefined;
20
+ visible?: boolean | undefined;
21
+ renderCell: (props: RenderItemProps<T>) => ReactNode;
22
+ headerMenu?: ReactNode;
14
23
  };
15
24
 
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>;
25
+ export type ColumnDef<T = unknown> = Omit<GtkColumnViewColumnProps, "factory" | "sorter"> &
26
+ ColumnDefDeclarativeProps<T>;
27
+
28
+ type ColumnViewColumnProps<T = unknown> = ColumnDef<T> & {
29
+ resolver: ItemResolver<unknown, unknown>;
30
+ tree: TreeRenderContext;
31
+ estimatedItemHeight?: number | undefined;
32
+ onInstance: (id: string, column: Gtk.ColumnViewColumn | null) => void;
33
+ };
24
34
 
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();
35
+ export const ColumnViewColumn = <T = unknown>(props: ColumnViewColumnProps<T>): ReactNode => {
36
+ const {
37
+ id,
38
+ title,
39
+ sortable,
40
+ renderCell,
41
+ headerMenu,
42
+ resolver,
43
+ tree,
44
+ estimatedItemHeight,
45
+ onInstance,
46
+ ref,
47
+ ...intrinsicProps
48
+ } = props as ColumnViewColumnProps<T> & { [key: string]: unknown };
33
49
  const [column, setColumn] = useState<Gtk.ColumnViewColumn | null>(null);
34
50
 
35
- const captureColumn = useRef((value: Gtk.ColumnViewColumn | null) => {
36
- setColumn(value);
37
- }).current;
51
+ const captureColumn = useMergeRefs<Gtk.ColumnViewColumn>(ref, setColumn);
38
52
 
39
- const { store } = useRealizedSlots<Gtk.ColumnViewColumn>({
40
- target: column,
41
- binding: factoryBinding,
53
+ const store = useCellContainers<Gtk.ColumnViewColumn>({
54
+ object: column,
55
+ installer: factoryInstaller,
56
+ estimatedHeight: estimatedItemHeight,
42
57
  });
43
58
 
44
- const registerRef = useRef(context.register);
45
- registerRef.current = context.register;
46
- const unregisterRef = useRef(context.unregister);
47
- unregisterRef.current = context.unregister;
59
+ const onInstanceRef = useRef(onInstance);
60
+ onInstanceRef.current = onInstance;
48
61
 
49
62
  useLayoutEffect(() => {
50
63
  if (column === null) return;
51
- registerRef.current({ id, column, sortable: sortable ?? false });
52
- return () => unregisterRef.current(id);
53
- }, [column, id, sortable]);
64
+ onInstanceRef.current(id, column);
65
+ return () => onInstanceRef.current(id, null);
66
+ }, [column, id]);
54
67
 
55
68
  const headerMenuPortal = useHeaderMenu(column, headerMenu);
56
69
 
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;
70
+ const cellRenderer: CellRenderer<unknown, unknown> = itemRenderer<unknown, unknown>(
71
+ (cellProps) => renderCell({ ...cellProps, item: cellProps.item as T }),
72
+ tree,
73
+ );
65
74
 
66
75
  return (
67
76
  <>
68
- {createElement(GtkColumnViewColumn, intrinsicProps)}
69
- <ListPortalHost store={store} resolver={context.resolver} render={slotRenderer} />
77
+ <GtkColumnViewColumn {...intrinsicProps} id={id} title={title} ref={captureColumn} />
78
+ <CellRenderHost store={store} resolver={resolver} render={cellRenderer} />
70
79
  {headerMenuPortal}
71
80
  </>
72
81
  );
@@ -1,206 +1,190 @@
1
- import type * as Gio from "@gtkx/gi/gio";
2
1
  import type * as Gtk from "@gtkx/gi/gtk";
3
2
  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";
3
+ import { type ReactNode, type Ref, useCallback, useMemo, useState } from "react";
4
+ import { createTreeRenderContext, HeaderRenderHost, type TreeRenderContext } from "./cell.js";
5
+ import { type ColumnDef, ColumnViewColumn } from "./column-view-column.js";
6
+ import { makeFactoryInstaller, useCellContainers } from "./hooks/use-cell-containers.js";
7
+ import { useCollectionWidget } from "./hooks/use-collection-widget.js";
8
+ import { type ColumnRegistration, useSortHandler } from "./hooks/use-sort-handler.js";
9
+ import type {
10
+ CollectionItemSizeProps,
11
+ ControlledExpansionProps,
12
+ ControlledSelectionProps,
13
+ ItemNode,
14
+ SectionNode,
15
+ } from "./types.js";
16
+ import type { CellContainerStore } from "./utils/cell-container-store.js";
19
17
  import type { ItemResolver } from "./utils/item-resolver.js";
20
18
 
21
- const headerFactoryBinding: FactoryBinding<Gtk.ColumnView> = {
22
- install: (widget, factory) => widget.setHeaderFactory(factory),
23
- uninstall: (widget) => widget.setHeaderFactory(null),
24
- };
19
+ export type { ColumnDef, ColumnDefDeclarativeProps } from "./column-view-column.js";
25
20
 
26
- interface ColumnRegistry {
27
- columns: ColumnRegistration[];
28
- register(registration: ColumnRegistration): void;
29
- unregister(id: string): void;
30
- }
21
+ const headerFactoryInstaller = makeFactoryInstaller<Gtk.ColumnView>((widget, factory) =>
22
+ widget.setHeaderFactory(factory),
23
+ );
31
24
 
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 };
25
+ export type ColumnViewSortProps = {
26
+ sortColumn?: string | null | undefined;
27
+ sortOrder?: Gtk.SortType | null | undefined;
28
+ onSortChanged?: ((column: string | null, order: Gtk.SortType) => void) | null | undefined;
51
29
  };
52
30
 
31
+ export type ColumnViewDeclarativeProps<T = unknown, S = unknown> = ColumnViewSortProps &
32
+ Omit<CollectionItemSizeProps, "estimatedItemWidth"> &
33
+ ControlledSelectionProps &
34
+ ControlledExpansionProps & {
35
+ items?: ItemNode<T>[] | undefined;
36
+ sections?: SectionNode<S, T>[] | undefined;
37
+ renderHeader?: ((info: { section: S }) => ReactNode) | null | undefined;
38
+ columns: ColumnDef<T>[];
39
+ };
40
+
53
41
  /**
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.
42
+ * Props for {@link ColumnView}. Combines the underlying Gtk.ColumnView props with
43
+ * declarative collection props: flat items or grouped sections, controlled selection
44
+ * and expansion, sorting (sortColumn, sortOrder, onSortChanged), an optional section
45
+ * header renderer, and the columns to render.
58
46
  */
59
- export type ColumnViewComponentProps<T = unknown, S = unknown> = Omit<
47
+ export type ColumnViewProps<T = unknown, S = unknown> = Omit<
60
48
  GtkColumnViewProps,
61
- "columns" | keyof ColumnViewProps<T, S>
49
+ "columns" | "model" | "headerFactory" | keyof ColumnViewDeclarativeProps<T, S>
62
50
  > &
63
- ColumnViewProps<T, S>;
51
+ ColumnViewDeclarativeProps<T, S>;
64
52
 
65
- type NormalizedColumnViewProps<T, S> = ColumnViewProps<T, S> & {
53
+ type NormalizedColumnViewProps<T, S> = ColumnViewDeclarativeProps<T, S> & {
66
54
  ref?: Ref<Gtk.ColumnView | null>;
67
- renderHeader?: ((value: S) => ReactNode) | null;
68
- children?: ReactNode;
69
55
  [key: string]: unknown;
70
56
  };
71
57
 
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
58
  interface ColumnViewWiring<T, S> {
78
59
  setRef: (value: Gtk.ColumnView | null) => void;
79
- resolver: ItemResolver<T, S>;
80
- headerResolver: ItemResolver<T, S>;
81
- headerStore: ReturnType<typeof useRealizedSlots>["store"];
82
60
  useHeader: boolean;
83
- contextValue: ColumnViewContextValue;
61
+ headerStore: CellContainerStore;
62
+ headerResolver: ItemResolver<T, S>;
63
+ resolver: ItemResolver<unknown, unknown>;
64
+ tree: TreeRenderContext;
65
+ setInstance: (id: string, column: Gtk.ColumnViewColumn | null) => void;
84
66
  }
85
67
 
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;
68
+ const useColumnInstances = (): {
69
+ instances: Map<string, Gtk.ColumnViewColumn>;
70
+ setInstance: (id: string, column: Gtk.ColumnViewColumn | null) => void;
71
+ } => {
72
+ const [instances, setInstances] = useState<Map<string, Gtk.ColumnViewColumn>>(() => new Map());
73
+ const setInstance = useCallback((id: string, column: Gtk.ColumnViewColumn | null): void => {
74
+ setInstances((current) => {
75
+ if (column === null) {
76
+ if (!current.has(id)) return current;
77
+ const next = new Map(current);
78
+ next.delete(id);
79
+ return next;
80
+ }
81
+ if (current.get(id) === column) return current;
82
+ const next = new Map(current);
83
+ next.set(id, column);
84
+ return next;
85
+ });
93
86
  }, []);
94
- const [, setRef] = useForwardedRef<Gtk.ColumnView>(props.ref, captureWidget);
87
+ return { instances, setInstance };
88
+ };
95
89
 
96
- const externalModel = props.model as Gio.ListModel | undefined;
97
- const listModel = useListModel<T, S>(
98
- externalModel === undefined ? { items: props.items } : { model: externalModel },
99
- );
90
+ const buildRegistrations = <T,>(
91
+ columns: ColumnDef<T>[],
92
+ instances: Map<string, Gtk.ColumnViewColumn>,
93
+ ): ColumnRegistration[] => {
94
+ const result: ColumnRegistration[] = [];
95
+ for (const column of columns) {
96
+ const instance = instances.get(column.id);
97
+ if (instance) result.push({ id: column.id, column: instance, sortable: column.sortable ?? false });
98
+ }
99
+ return result;
100
+ };
100
101
 
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,
102
+ const useColumnViewWiring = <T, S>(props: NormalizedColumnViewProps<T, S>): ColumnViewWiring<T, S> => {
103
+ const { widgetRef, setRef, collection } = useCollectionWidget<Gtk.ColumnView, T, S>(props);
104
+
105
+ const useHeader = collection.useHeader;
106
+ const headerStore = useCellContainers<Gtk.ColumnView>({
107
+ object: useHeader ? widgetRef : null,
108
+ installer: headerFactoryInstaller,
109
+ estimatedHeight: props.estimatedItemHeight,
117
110
  });
118
111
 
112
+ const { instances, setInstance } = useColumnInstances();
113
+ const registrations = useMemo<ColumnRegistration[]>(
114
+ () => buildRegistrations(props.columns, instances),
115
+ [props.columns, instances],
116
+ );
117
+
119
118
  useSortHandler({
120
119
  columnView: widgetRef,
121
120
  sortColumn: props.sortColumn,
122
121
  sortOrder: props.sortOrder,
123
122
  onSortChanged: props.onSortChanged,
124
- columns: registry.columns,
123
+ columns: registrations,
125
124
  });
126
125
 
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],
126
+ const expandedIds = props.expandedIds;
127
+ const tree = useMemo<TreeRenderContext>(
128
+ () => createTreeRenderContext(expandedIds, collection.rowId),
129
+ [expandedIds, collection.rowId],
135
130
  );
136
131
 
137
132
  return {
138
133
  setRef,
139
- resolver: listModel.resolver,
140
- headerResolver: listModel.headerResolver,
141
- headerStore: headers.store,
142
134
  useHeader,
143
- contextValue,
135
+ headerStore,
136
+ headerResolver: collection.headerResolver,
137
+ resolver: collection.resolver as ItemResolver<unknown, unknown>,
138
+ tree,
139
+ setInstance,
144
140
  };
145
141
  };
146
142
 
147
143
  /**
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.
144
+ * Renders a Gtk.ColumnView: a multi-column, scrollable list backed by a collection
145
+ * model. Columns are declared through the columns prop, each a {@link ColumnDef}.
152
146
  */
153
- export const ColumnView = <T = unknown, S = unknown>(props: ColumnViewComponentProps<T, S>): ReactNode => {
147
+ export const ColumnView = <T = unknown, S = unknown>(props: ColumnViewProps<T, S>): ReactNode => {
148
+ const normalized = props as NormalizedColumnViewProps<T, S>;
149
+ const wiring = useColumnViewWiring<T, S>(normalized);
154
150
  const {
155
151
  ref,
156
152
  items,
157
- model,
158
- selected,
153
+ sections,
154
+ columns,
155
+ selectedIds,
159
156
  selectionMode,
160
157
  onSelectionChanged,
158
+ expandedIds,
159
+ onExpandedChange,
161
160
  sortColumn,
162
161
  sortOrder,
163
162
  onSortChanged,
164
163
  renderHeader,
165
- estimatedRowHeight,
166
- children,
164
+ estimatedItemHeight,
167
165
  ...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
- );
166
+ } = normalized;
193
167
 
194
168
  return (
195
169
  <>
196
- {intrinsic}
197
- {wiring.useHeader ? (
198
- <ListPortalHost
199
- store={wiring.headerStore}
200
- resolver={wiring.headerResolver}
201
- render={headerRenderer<T, S>(renderHeader)}
202
- />
203
- ) : null}
170
+ <GtkColumnView {...intrinsicProps} ref={wiring.setRef}>
171
+ {columns.map((column) => (
172
+ <ColumnViewColumn
173
+ key={column.id}
174
+ {...column}
175
+ resolver={wiring.resolver}
176
+ tree={wiring.tree}
177
+ estimatedItemHeight={estimatedItemHeight}
178
+ onInstance={wiring.setInstance}
179
+ />
180
+ ))}
181
+ </GtkColumnView>
182
+ <HeaderRenderHost
183
+ useHeader={wiring.useHeader}
184
+ store={wiring.headerStore}
185
+ resolver={wiring.headerResolver}
186
+ renderHeader={renderHeader}
187
+ />
204
188
  </>
205
189
  );
206
190
  };
@@ -1,6 +1,58 @@
1
1
  import * as GObject from "@gtkx/gi/gobject";
2
2
  import * as Gtk from "@gtkx/gi/gtk";
3
- import type { ConstraintGuideProps, ConstraintProps, ConstraintVflProps } from "./types.js";
3
+
4
+ /**
5
+ * Describes one constraint added by `<ConstraintLayout.Constraint>`, relating a
6
+ * target widget attribute to a source attribute of another widget or guide.
7
+ */
8
+ export type ConstraintProps = {
9
+ /** Name of the target widget or guide. Use "super" or omit to reference the layout's own widget. */
10
+ target?: string;
11
+ targetAttribute: Gtk.ConstraintAttribute;
12
+ /** Relation between the target and source attributes (defaults to equality). */
13
+ relation?: Gtk.ConstraintRelation;
14
+ /** Name of the source widget or guide. Use "super" or omit for a constant constraint. */
15
+ source?: string;
16
+ sourceAttribute?: Gtk.ConstraintAttribute;
17
+ /** Factor applied to the source attribute (defaults to 1). */
18
+ multiplier?: number;
19
+ /** Constant offset added to the relation (defaults to 0). */
20
+ constant?: number;
21
+ /** Constraint strength, higher values winning conflicts (defaults to required). */
22
+ strength?: number;
23
+ };
24
+
25
+ /**
26
+ * Describes an invisible spacing guide added by `<ConstraintLayout.Guide>`,
27
+ * usable as a constraint target under its id.
28
+ */
29
+ export type ConstraintGuideProps = {
30
+ /** Identifier used to reference this guide from constraints. */
31
+ id: string;
32
+ minWidth?: number;
33
+ minHeight?: number;
34
+ /** Preferred (natural) width. */
35
+ natWidth?: number;
36
+ /** Preferred (natural) height. */
37
+ natHeight?: number;
38
+ maxWidth?: number;
39
+ maxHeight?: number;
40
+ /** Strength of the guide's own size constraints. */
41
+ strength?: Gtk.ConstraintStrength;
42
+ };
43
+
44
+ /**
45
+ * Describes constraints authored with the Visual Format Language (VFL), applied by
46
+ * `<ConstraintLayout.Vfl>`.
47
+ */
48
+ export type ConstraintVflProps = {
49
+ /** VFL lines describing the constraints between named widgets and guides. */
50
+ lines: string[];
51
+ /** Default horizontal spacing used by the layout operator (defaults to 0). */
52
+ hspacing?: number;
53
+ /** Default vertical spacing used by the layout operator (defaults to 0). */
54
+ vspacing?: number;
55
+ };
4
56
 
5
57
  const SUPER_ID = "super";
6
58
 
@@ -1,21 +1,25 @@
1
1
  import type * as Gtk from "@gtkx/gi/gtk";
2
2
  import { GtkConstraintLayout } from "@gtkx/jsx/gtk";
3
- import { useForwardedRef } from "@gtkx/react";
3
+ import { useMergeRefs } from "@gtkx/react/internal";
4
4
  import {
5
5
  type Context,
6
6
  createContext,
7
- createElement,
8
7
  type ReactNode,
9
8
  type Ref,
10
9
  type RefObject,
11
- useCallback,
12
10
  useContext,
13
11
  useEffect,
14
12
  useLayoutEffect,
15
13
  useRef,
16
14
  } from "react";
17
- import { applyConstraint, applyGuide, applyVfl } from "./constraint-layout-apply.js";
18
- import type { ConstraintGuideProps, ConstraintProps, ConstraintVflProps } from "./types.js";
15
+ import {
16
+ applyConstraint,
17
+ applyGuide,
18
+ applyVfl,
19
+ type ConstraintGuideProps,
20
+ type ConstraintProps,
21
+ type ConstraintVflProps,
22
+ } from "./constraint-layout-apply.js";
19
23
 
20
24
  const ORPHAN_MESSAGE = "<ConstraintLayout.Constraint> / <Guide> / <Vfl> must be a child of <ConstraintLayout>";
21
25
 
@@ -28,6 +32,7 @@ const useConstraintLayoutRef = (): RefObject<Gtk.ConstraintLayout | null> => {
28
32
  return ref;
29
33
  };
30
34
 
35
+ /** Props for {@link ConstraintLayout}. */
31
36
  export type ConstraintLayoutProps = {
32
37
  children?: ReactNode;
33
38
  ref?: Ref<Gtk.ConstraintLayout | null>;
@@ -68,6 +73,12 @@ const useDeferredContribution = (apply: (layout: Gtk.ConstraintLayout) => () =>
68
73
  }, deps);
69
74
  };
70
75
 
76
+ /**
77
+ * Installs a Gtk.ConstraintLayout on a host widget and exposes child components to
78
+ * populate it: {@link ConstraintProps | ConstraintLayout.Constraint} for individual constraints,
79
+ * {@link ConstraintGuideProps | ConstraintLayout.Guide} for spacing guides, and {@link ConstraintVflProps | ConstraintLayout.Vfl}
80
+ * for constraints authored in the Visual Format Language.
81
+ */
71
82
  export const ConstraintLayout: ((props: ConstraintLayoutProps) => ReactNode) & {
72
83
  Guide: (props: ConstraintGuideProps) => ReactNode;
73
84
  Constraint: (props: ConstraintProps) => ReactNode;
@@ -75,16 +86,10 @@ export const ConstraintLayout: ((props: ConstraintLayoutProps) => ReactNode) & {
75
86
  } = Object.assign(
76
87
  ({ children, ref }: ConstraintLayoutProps): ReactNode => {
77
88
  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);
89
+ const mergedRef = useMergeRefs<Gtk.ConstraintLayout>(ref, layoutRef);
85
90
  return (
86
91
  <>
87
- {createElement(GtkConstraintLayout, { ref: mergedRef })}
92
+ <GtkConstraintLayout ref={mergedRef} />
88
93
  <ConstraintLayoutContext.Provider value={layoutRef}>{children}</ConstraintLayoutContext.Provider>
89
94
  </>
90
95
  );