@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
package/src/drop-down.tsx CHANGED
@@ -1,134 +1,115 @@
1
- import type * as Adw from "@gtkx/gi/adw";
2
- import type * as Gio from "@gtkx/gi/gio";
3
1
  import type * as Gtk from "@gtkx/gi/gtk";
4
- import { AdwComboRow, type AdwComboRowProps } from "@gtkx/jsx/adw";
5
- import { GtkDropDown, type GtkDropDownProps, GtkLabel } from "@gtkx/jsx/gtk";
6
- import { useForwardedRef } from "@gtkx/react";
7
- import {
8
- createElement,
9
- type ElementType,
10
- type ReactElement,
11
- type ReactNode,
12
- type Ref,
13
- useCallback,
14
- useRef,
15
- useState,
16
- } from "react";
2
+ import { GtkDropDown, GtkLabel } from "@gtkx/jsx/gtk";
3
+ import { useMergeRefs } from "@gtkx/react/internal";
4
+ import { type ElementType, type ReactNode, type Ref, useCallback, useRef, useState } from "react";
5
+ import { type CellRenderer, CellRenderHost, HeaderRenderHost, itemRenderer } from "./cell.js";
6
+ import { makeFactoryInstaller, useCellContainers } from "./hooks/use-cell-containers.js";
17
7
  import { useDropDownSelection } from "./hooks/use-drop-down-selection.js";
8
+ import { useInstalledModel } from "./hooks/use-installed-model.js";
18
9
  import { useListModel } from "./hooks/use-list-model.js";
19
- import { useModelInstallation } from "./hooks/use-model-installation.js";
20
- import { type FactoryBinding, useRealizedSlots } from "./hooks/use-realized-slots.js";
21
- import { ListPortalHost } from "./list-portal-host.js";
22
- import type { SlotRenderer } from "./list-slot.js";
23
- import type { DropDownProps, ListItem } from "./types.js";
10
+ import type { ItemNode, RenderItemProps, SectionNode, WidgetProps } from "./types.js";
11
+ import type { CellContainerStore } from "./utils/cell-container-store.js";
24
12
  import type { ItemResolver } from "./utils/item-resolver.js";
25
- import type { RealizedSlotStore } from "./utils/realized-slot-store.js";
26
13
 
27
- export interface DropDownWidget extends Gtk.Widget {
28
- getSelected(): number;
29
- setSelected(position: number): void;
30
- setModel(model: Gio.ListModel | null): void;
31
- setFactory(factory: Gtk.ListItemFactory | null): void;
32
- setListFactory(factory: Gtk.ListItemFactory | null): void;
33
- setHeaderFactory(factory: Gtk.ListItemFactory | null): void;
34
- }
14
+ export type DropDownItemRenderer<T> = (props: RenderItemProps<T>) => ReactNode;
35
15
 
36
- const itemFactoryBinding: FactoryBinding<DropDownWidget> = {
37
- install: (widget, factory) => widget.setFactory(factory),
38
- uninstall: (widget) => widget.setFactory(null),
39
- };
16
+ const itemFactoryInstaller = makeFactoryInstaller<Gtk.DropDown>((widget, factory) => widget.setFactory(factory));
40
17
 
41
- const listFactoryBinding: FactoryBinding<DropDownWidget> = {
42
- install: (widget, factory) => widget.setListFactory(factory),
43
- uninstall: (widget) => widget.setListFactory(null),
44
- };
18
+ const listFactoryInstaller = makeFactoryInstaller<Gtk.DropDown>((widget, factory) => widget.setListFactory(factory));
45
19
 
46
- const headerFactoryBinding: FactoryBinding<DropDownWidget> = {
47
- install: (widget, factory) => widget.setHeaderFactory(factory),
48
- uninstall: (widget) => widget.setHeaderFactory(null),
49
- };
20
+ const headerFactoryInstaller = makeFactoryInstaller<Gtk.DropDown>((widget, factory) =>
21
+ widget.setHeaderFactory(factory),
22
+ );
50
23
 
51
- const defaultRenderer: SlotRenderer<unknown, unknown> = (value, _treeRow, isHeader) => {
52
- if (isHeader || value === undefined || value === null) return null;
53
- return createElement(GtkLabel, { label: String(value) });
24
+ const defaultRenderer: CellRenderer<unknown, unknown> = (value) => {
25
+ if (value === undefined || value === null) return null;
26
+ return <GtkLabel>{String(value)}</GtkLabel>;
54
27
  };
55
28
 
56
- interface DropDownImplProps<T, S, W extends DropDownWidget> {
57
- element: ElementType;
58
- intrinsicProps: Record<string, unknown>;
59
- ref: Ref<W | null> | undefined;
60
- items: ListItem<T, S>[] | undefined;
61
- model: Gio.ListModel | undefined;
62
- renderItem: ((value: T) => ReactNode) | null | undefined;
63
- renderListItem: ((value: T) => ReactNode) | null | undefined;
64
- renderHeader: ((value: S) => ReactNode) | null | undefined;
65
- selectedId: string | null | undefined;
66
- onSelectionChanged: ((id: string) => void) | null | undefined;
67
- }
68
-
69
- const toItemRenderer = <T, S>(renderItem: ((value: T) => ReactNode) | null | undefined): SlotRenderer<T, S> => {
70
- if (typeof renderItem !== "function") return defaultRenderer as SlotRenderer<T, S>;
71
- return (value, _treeRow, isHeader) => (isHeader ? null : renderItem(value as T));
29
+ const toItemRenderer = <T, S>(renderItem: DropDownItemRenderer<T> | null | undefined): CellRenderer<T, S> => {
30
+ if (typeof renderItem !== "function") return defaultRenderer as CellRenderer<T, S>;
31
+ return itemRenderer<T, S>(renderItem);
72
32
  };
73
33
 
74
- const toPopupRenderer = <T, S>(
75
- renderListItem: ((value: T) => ReactNode) | null | undefined,
76
- renderItem: ((value: T) => ReactNode) | null | undefined,
77
- ): SlotRenderer<T, S> => {
78
- if (typeof renderListItem === "function")
79
- return (value, _treeRow, isHeader) => (isHeader ? null : renderListItem(value as T));
34
+ const toListRenderer = <T, S>(
35
+ renderListItem: DropDownItemRenderer<T> | null | undefined,
36
+ renderItem: DropDownItemRenderer<T> | null | undefined,
37
+ ): CellRenderer<T, S> => {
38
+ if (typeof renderListItem === "function") return itemRenderer<T, S>(renderListItem);
80
39
  return toItemRenderer<T, S>(renderItem);
81
40
  };
82
41
 
83
- const toHeaderRenderer = <T, S>(renderHeader: ((value: S) => ReactNode) | null | undefined): SlotRenderer<T, S> => {
84
- if (typeof renderHeader !== "function") return () => null;
85
- return (value) => renderHeader(value as S);
42
+ const createSelectionResolver = <T, S>(resolver: ItemResolver<T, S>, selectedPosition: number): ItemResolver<T, S> => ({
43
+ positionOfId: (id) => resolver.positionOfId(id),
44
+ idOf: (position) => resolver.idOf(position),
45
+ resolve: (_position, treeRow) => resolver.resolve(selectedPosition, treeRow),
46
+ });
47
+
48
+ /** Declarative props for {@link DropDown}'s backing collection and cell rendering. */
49
+ export type DropDownDeclarativeProps<T = unknown, S = unknown> = {
50
+ items?: ItemNode<T>[] | undefined;
51
+ sections?: SectionNode<S, T>[] | undefined;
52
+ /** Id of the currently selected item, making the selection controlled. */
53
+ selectedId?: string | null | undefined;
54
+ onSelectionChanged?: ((id: string) => void) | null | undefined;
55
+ renderItem?: DropDownItemRenderer<T> | null | undefined;
56
+ /** Renderer for items in the open popup list, falling back to renderItem when omitted. */
57
+ renderListItem?: DropDownItemRenderer<T> | null | undefined;
58
+ /** Renderer for section headers in the popup list. */
59
+ renderHeader?: ((info: { section: S }) => ReactNode) | null | undefined;
86
60
  };
87
61
 
88
- interface DropDownWiring<T, S, W extends DropDownWidget> {
89
- setRef: (value: W | null) => void;
62
+ /**
63
+ * Props for {@link DropDown}. The backing widget is chosen through the `component` prop, defaulting to
64
+ * GtkDropDown, and its own props combine with {@link DropDownDeclarativeProps}.
65
+ */
66
+ export type DropDownProps<T = unknown, S = unknown, C extends ElementType = typeof GtkDropDown> = WidgetProps<
67
+ C,
68
+ DropDownDeclarativeProps<T, S>,
69
+ "model" | "factory" | "listFactory" | "headerFactory"
70
+ >;
71
+
72
+ interface NormalizedDropDownProps<T, S> {
73
+ ref: Ref<Gtk.DropDown | null> | undefined;
74
+ items: ItemNode<T>[] | undefined;
75
+ sections: SectionNode<S, T>[] | undefined;
76
+ selectedId: string | null | undefined;
77
+ onSelectionChanged: ((id: string) => void) | null | undefined;
78
+ renderHeader: ((info: { section: S }) => ReactNode) | null | undefined;
79
+ }
80
+
81
+ interface DropDownWiring<T, S> {
82
+ setRef: (value: Gtk.DropDown | null) => void;
90
83
  resolver: ItemResolver<T, S>;
91
- faceResolver: ItemResolver<T, S>;
84
+ selectionResolver: ItemResolver<T, S>;
92
85
  headerResolver: ItemResolver<T, S>;
93
- faceStore: RealizedSlotStore;
94
- popupStore: RealizedSlotStore;
95
- headerStore: RealizedSlotStore;
86
+ selectionStore: CellContainerStore;
87
+ listStore: CellContainerStore;
88
+ headerStore: CellContainerStore;
96
89
  useHeader: boolean;
97
90
  }
98
91
 
99
- const createFaceResolver = <T, S>(resolver: ItemResolver<T, S>, selectedPosition: number): ItemResolver<T, S> => ({
100
- count: resolver.count,
101
- positionOf: (id) => resolver.positionOf(id),
102
- idOf: (position) => resolver.idOf(position),
103
- resolve: (_position, treeRow) => resolver.resolve(selectedPosition, treeRow, null),
104
- });
105
-
106
- const useDropDownWiring = <T, S, W extends DropDownWidget>(
107
- props: DropDownImplProps<T, S, W>,
108
- ): DropDownWiring<T, S, W> => {
109
- const widgetRef = useRef<DropDownWidget | null>(null);
110
- const [widget, setWidget] = useState<DropDownWidget | null>(null);
111
- const captureWidget = useCallback((value: W | null) => {
92
+ const useDropDownWiring = <T, S>(props: NormalizedDropDownProps<T, S>): DropDownWiring<T, S> => {
93
+ const widgetRef = useRef<Gtk.DropDown | null>(null);
94
+ const [widget, setWidget] = useState<Gtk.DropDown | null>(null);
95
+ const captureWidget = useCallback((value: Gtk.DropDown | null) => {
112
96
  widgetRef.current = value;
113
97
  setWidget(value);
114
98
  }, []);
115
- const [, setRef] = useForwardedRef<W>(props.ref, captureWidget);
99
+ const setRef = useMergeRefs<Gtk.DropDown>(props.ref, captureWidget);
116
100
 
117
- const externalModel = props.model;
118
- const listModel = useListModel<T, S>(
119
- externalModel === undefined ? { items: props.items ?? undefined } : { model: externalModel },
120
- );
101
+ const listModel = useListModel<T, S>({ items: props.items, sections: props.sections });
121
102
 
122
- const useHeader = externalModel === undefined && typeof props.renderHeader === "function";
103
+ const useHeader = typeof props.renderHeader === "function";
123
104
 
124
- const face = useRealizedSlots<DropDownWidget>({ target: widgetRef, binding: itemFactoryBinding });
125
- const popup = useRealizedSlots<DropDownWidget>({ target: widgetRef, binding: listFactoryBinding });
126
- const header = useRealizedSlots<DropDownWidget>({
127
- target: useHeader ? widgetRef : null,
128
- binding: headerFactoryBinding,
105
+ const selectionStore = useCellContainers<Gtk.DropDown>({ object: widgetRef, installer: itemFactoryInstaller });
106
+ const listStore = useCellContainers<Gtk.DropDown>({ object: widgetRef, installer: listFactoryInstaller });
107
+ const headerStore = useCellContainers<Gtk.DropDown>({
108
+ object: useHeader ? widgetRef : null,
109
+ installer: headerFactoryInstaller,
129
110
  });
130
111
 
131
- useModelInstallation(widgetRef, listModel.model, (widget, model) => widget.setModel(model));
112
+ useInstalledModel(widgetRef, listModel.model, (widget, value) => widget.setModel(value));
132
113
 
133
114
  const selectedPosition = useDropDownSelection<T, S>({
134
115
  widget,
@@ -140,120 +121,72 @@ const useDropDownWiring = <T, S, W extends DropDownWidget>(
140
121
  const controlledPosition =
141
122
  props.selectedId === undefined || props.selectedId === null
142
123
  ? -1
143
- : listModel.resolver.positionOf(props.selectedId);
144
- const facePosition = controlledPosition >= 0 ? controlledPosition : selectedPosition < 0 ? 0 : selectedPosition;
124
+ : listModel.resolver.positionOfId(props.selectedId);
125
+ const selectionPosition =
126
+ controlledPosition >= 0 ? controlledPosition : selectedPosition < 0 ? 0 : selectedPosition;
145
127
 
146
128
  return {
147
129
  setRef,
148
130
  resolver: listModel.resolver,
149
- faceResolver: createFaceResolver(listModel.resolver, facePosition),
131
+ selectionResolver: createSelectionResolver(listModel.resolver, selectionPosition),
150
132
  headerResolver: listModel.headerResolver,
151
- faceStore: face.store,
152
- popupStore: popup.store,
153
- headerStore: header.store,
133
+ selectionStore,
134
+ listStore,
135
+ headerStore,
154
136
  useHeader,
155
137
  };
156
138
  };
157
139
 
158
- const DropDownImpl = <T, S, W extends DropDownWidget>(props: DropDownImplProps<T, S, W>): ReactNode => {
159
- const { setRef, resolver, faceResolver, headerResolver, faceStore, popupStore, headerStore, useHeader } =
160
- useDropDownWiring(props);
161
- const intrinsic: ReactElement = createElement(props.element, { ...props.intrinsicProps, ref: setRef });
162
-
163
- return (
164
- <>
165
- {intrinsic}
166
- <ListPortalHost store={faceStore} resolver={faceResolver} render={toItemRenderer<T, S>(props.renderItem)} />
167
- <ListPortalHost
168
- store={popupStore}
169
- resolver={resolver}
170
- render={toPopupRenderer<T, S>(props.renderListItem, props.renderItem)}
171
- />
172
- {useHeader ? (
173
- <ListPortalHost
174
- store={headerStore}
175
- resolver={headerResolver}
176
- render={toHeaderRenderer<T, S>(props.renderHeader)}
177
- />
178
- ) : null}
179
- </>
180
- );
181
- };
182
-
183
- type DropDownImplComponentProps<T, S, W extends DropDownWidget> = DropDownProps<T, S> & {
184
- ref?: Ref<W | null> | undefined;
185
- };
186
-
187
- const extractDropDownProps = <T, S, W extends DropDownWidget>(
188
- props: DropDownImplComponentProps<T, S, W>,
189
- ): {
190
- impl: Omit<DropDownImplProps<T, S, W>, "element" | "intrinsicProps">;
191
- intrinsicProps: Record<string, unknown>;
192
- } => {
140
+ /**
141
+ * Renders a drop-down widget (Gtk.DropDown by default, or the widget given as `component`) backed by a
142
+ * collection model, with customizable rendering for the selected item, the popup list rows, and section
143
+ * headers.
144
+ */
145
+ export const DropDown = <T = unknown, S = unknown, C extends ElementType = typeof GtkDropDown>(
146
+ props: DropDownProps<T, S, C>,
147
+ ): ReactNode => {
193
148
  const {
149
+ component,
194
150
  ref,
195
151
  items,
196
- model,
152
+ sections,
197
153
  renderItem,
198
154
  renderListItem,
199
155
  renderHeader,
200
156
  selectedId,
201
157
  onSelectionChanged,
202
158
  ...intrinsicProps
203
- } = props as DropDownProps<T, S> & {
204
- ref?: Ref<W | null>;
205
- [key: string]: unknown;
206
- };
207
-
208
- return {
209
- impl: {
210
- ref,
211
- items: items as ListItem<T, S>[] | undefined,
212
- model: model as Gio.ListModel | undefined,
213
- renderItem: renderItem as ((value: T) => ReactNode) | null | undefined,
214
- renderListItem: renderListItem as ((value: T) => ReactNode) | null | undefined,
215
- renderHeader: renderHeader as ((value: S) => ReactNode) | null | undefined,
216
- selectedId: selectedId as string | null | undefined,
217
- onSelectionChanged: onSelectionChanged as ((id: string) => void) | null | undefined,
218
- },
219
- intrinsicProps: intrinsicProps as Record<string, unknown>,
220
- };
221
- };
222
-
223
- /**
224
- * Props for the {@link DropDown} component: the raw `GtkDropDown` element
225
- * surface with its factory/model wiring replaced by the declarative
226
- * {@link DropDownProps} API.
227
- */
228
- export type DropDownComponentProps<T = unknown, S = unknown> = Omit<GtkDropDownProps, keyof DropDownProps<T, S>> &
229
- DropDownProps<T, S>;
230
-
231
- /**
232
- * Props for the {@link ComboRow} component: the raw `AdwComboRow` element
233
- * surface with its factory/model wiring replaced by the declarative
234
- * {@link DropDownProps} API.
235
- */
236
- export type ComboRowComponentProps<T = unknown, S = unknown> = Omit<AdwComboRowProps, keyof DropDownProps<T, S>> &
237
- DropDownProps<T, S>;
159
+ } = props;
160
+ const Component: ElementType = component ?? GtkDropDown;
238
161
 
239
- /**
240
- * A `GtkDropDown` driven by a declarative `items` model with a controlled
241
- * `selectedId` and per-slot renderers for the selected face, the popup list,
242
- * and section headers. Supplying an external `model` switches to the
243
- * uncontrolled form.
244
- */
245
- export const DropDown = <T = unknown, S = unknown>(props: DropDownComponentProps<T, S>): ReactNode => {
246
- const { impl, intrinsicProps } = extractDropDownProps<T, S, Gtk.DropDown>(props);
247
- return <DropDownImpl<T, S, Gtk.DropDown> element={GtkDropDown} intrinsicProps={intrinsicProps} {...impl} />;
248
- };
162
+ const wiring = useDropDownWiring<T, S>({
163
+ ref,
164
+ items,
165
+ sections,
166
+ selectedId,
167
+ onSelectionChanged,
168
+ renderHeader,
169
+ });
249
170
 
250
- /**
251
- * An `AdwComboRow` driven by a declarative `items` model with a controlled
252
- * `selectedId` and per-slot renderers for the selected face, the popup list,
253
- * and section headers. Supplying an external `model` switches to the
254
- * uncontrolled form.
255
- */
256
- export const ComboRow = <T = unknown, S = unknown>(props: ComboRowComponentProps<T, S>): ReactNode => {
257
- const { impl, intrinsicProps } = extractDropDownProps<T, S, Adw.ComboRow>(props);
258
- return <DropDownImpl<T, S, Adw.ComboRow> element={AdwComboRow} intrinsicProps={intrinsicProps} {...impl} />;
171
+ return (
172
+ <>
173
+ <Component {...intrinsicProps} ref={wiring.setRef} />
174
+ <CellRenderHost
175
+ store={wiring.selectionStore}
176
+ resolver={wiring.selectionResolver}
177
+ render={toItemRenderer<T, S>(renderItem)}
178
+ />
179
+ <CellRenderHost
180
+ store={wiring.listStore}
181
+ resolver={wiring.resolver}
182
+ render={toListRenderer<T, S>(renderListItem, renderItem)}
183
+ />
184
+ <HeaderRenderHost
185
+ useHeader={wiring.useHeader}
186
+ store={wiring.headerStore}
187
+ resolver={wiring.headerResolver}
188
+ renderHeader={renderHeader}
189
+ />
190
+ </>
191
+ );
259
192
  };
package/src/fixed.tsx ADDED
@@ -0,0 +1,86 @@
1
+ import * as Graphene from "@gtkx/gi/graphene";
2
+ import * as Gsk from "@gtkx/gi/gsk";
3
+ import type * as Gtk from "@gtkx/gi/gtk";
4
+ import { GtkFixed, type GtkFixedProps } from "@gtkx/jsx/gtk";
5
+ import { useMergeRefs } from "@gtkx/react/internal";
6
+ import {
7
+ createContext,
8
+ type ElementType,
9
+ type ReactNode,
10
+ type Ref,
11
+ type RefCallback,
12
+ useCallback,
13
+ useContext,
14
+ useLayoutEffect,
15
+ useRef,
16
+ useState,
17
+ } from "react";
18
+ import type { ChildProps } from "./types.js";
19
+
20
+ const FixedContext = createContext<Gtk.Fixed | null | undefined>(undefined);
21
+
22
+ const useFixedInstance = (): Gtk.Fixed | null => {
23
+ const fixed = useContext(FixedContext);
24
+ if (fixed === undefined) throw new Error("<Fixed.Child> must be a child of <Fixed>");
25
+ return fixed;
26
+ };
27
+
28
+ /** Props for {@link Fixed}. */
29
+ export type FixedProps = GtkFixedProps & { ref?: Ref<Gtk.Fixed | null>; children?: ReactNode };
30
+
31
+ export type FixedPlacementProps = {
32
+ x?: number | null | undefined;
33
+ y?: number | null | undefined;
34
+ /** Full transform applied to the child, overriding x and y when provided. */
35
+ transform?: Gsk.Transform | null | undefined;
36
+ };
37
+
38
+ /** Positions a single child inside a {@link Fixed} at coordinates x and y, or by an explicit transform. */
39
+ export type FixedChildProps<C extends ElementType> = ChildProps<C, FixedPlacementProps>;
40
+
41
+ const transformOf = (x: number, y: number, transform: Gsk.Transform | null | undefined): Gsk.Transform | null =>
42
+ transform !== undefined ? transform : Gsk.Transform.new().translate(Graphene.Point.create(x, y));
43
+
44
+ const FixedChild = <C extends ElementType>({
45
+ component,
46
+ x,
47
+ y,
48
+ transform,
49
+ ref,
50
+ ...rest
51
+ }: FixedChildProps<C>): ReactNode => {
52
+ const fixed = useFixedInstance();
53
+ const Component: ElementType = component;
54
+ const widgetRef = useRef<Gtk.Widget | null>(null);
55
+ const captureWidget = useCallback<RefCallback<Gtk.Widget>>((node) => {
56
+ widgetRef.current = node;
57
+ }, []);
58
+ const setWidget = useMergeRefs<Gtk.Widget>(ref, captureWidget);
59
+
60
+ useLayoutEffect(() => {
61
+ const widget = widgetRef.current;
62
+ if (widget === null || fixed === null) return;
63
+ fixed.setChildTransform(widget, transformOf(x ?? 0, y ?? 0, transform));
64
+ });
65
+
66
+ return <Component {...rest} ref={setWidget} />;
67
+ };
68
+
69
+ /**
70
+ * Renders a Gtk.Fixed container whose children are placed at explicit positions via
71
+ * {@link Fixed.Child}.
72
+ */
73
+ export const Fixed: ((props: FixedProps) => ReactNode) & {
74
+ Child: <C extends ElementType>(props: FixedChildProps<C>) => ReactNode;
75
+ } = Object.assign(
76
+ ({ children, ref, ...rest }: FixedProps): ReactNode => {
77
+ const [fixed, setFixed] = useState<Gtk.Fixed | null>(null);
78
+ const mergedRef = useMergeRefs<Gtk.Fixed>(ref, setFixed);
79
+ return (
80
+ <GtkFixed {...rest} ref={mergedRef}>
81
+ <FixedContext.Provider value={fixed}>{children}</FixedContext.Provider>
82
+ </GtkFixed>
83
+ );
84
+ },
85
+ { Child: FixedChild },
86
+ );
package/src/grid-view.tsx CHANGED
@@ -1,73 +1,81 @@
1
- import type * as Gio from "@gtkx/gi/gio";
2
1
  import type * as Gtk from "@gtkx/gi/gtk";
3
2
  import { GtkGridView, type GtkGridViewProps } from "@gtkx/jsx/gtk";
4
- import type { ReactNode, Ref } from "react";
5
- import { CollectionView, type ModelBinding } from "./collection-view.js";
6
- import type { SlotRenderer } from "./list-slot.js";
7
- import type { GridViewProps } from "./types.js";
3
+ import { useMergeRefs } from "@gtkx/react/internal";
4
+ import { type ReactNode, type Ref, useRef } from "react";
5
+ import { type CellRenderer, CellRenderHost, itemRenderer } from "./cell.js";
6
+ import { makeFactoryInstaller, useCellContainers } from "./hooks/use-cell-containers.js";
7
+ import { useCollectionModel } from "./hooks/use-collection-model.js";
8
+ import { useInstalledModel } from "./hooks/use-installed-model.js";
9
+ import type { CollectionItemSizeProps, ControlledSelectionProps, ItemNode, RenderItemProps } from "./types.js";
8
10
 
9
- const factoryBinding = {
10
- install: (widget: Gtk.GridView, factory: Gtk.SignalListItemFactory) => widget.setFactory(factory),
11
- uninstall: (widget: Gtk.GridView) => widget.setFactory(null),
12
- };
11
+ const factoryInstaller = makeFactoryInstaller<Gtk.GridView>((widget, factory) => widget.setFactory(factory));
13
12
 
14
- const modelBinding: ModelBinding<Gtk.GridView> = {
15
- install: (widget, model) => widget.setModel(model as Gtk.SelectionModel),
16
- };
13
+ export type GridViewDeclarativeProps<T = unknown> = CollectionItemSizeProps &
14
+ ControlledSelectionProps & {
15
+ items?: ItemNode<T>[] | undefined;
16
+ renderItem: (props: RenderItemProps<T>) => ReactNode;
17
+ };
17
18
 
18
19
  /**
19
- * Props for the {@link GridView} component: the raw `GtkGridView` element
20
- * surface with its factory/model wiring replaced by the declarative
21
- * {@link GridViewProps} API.
20
+ * Props for {@link GridView}. Combines the underlying Gtk.GridView props with
21
+ * declarative collection props: items, a per-cell renderItem, controlled selection,
22
+ * and estimated item sizing.
22
23
  */
23
- export type GridViewComponentProps<T = unknown> = Omit<GtkGridViewProps, keyof GridViewProps<T>> & GridViewProps<T>;
24
+ export type GridViewProps<T = unknown> = Omit<
25
+ GtkGridViewProps,
26
+ "model" | "factory" | keyof GridViewDeclarativeProps<T>
27
+ > &
28
+ GridViewDeclarativeProps<T>;
24
29
 
25
30
  /**
26
- * A `GtkGridView` driven by a declarative `items`/`renderItem` API with
27
- * optional controlled selection. Supplying an external `model` switches to the
28
- * uncontrolled form.
31
+ * Renders a Gtk.GridView: a scrollable grid of uniformly sized cells backed by a
32
+ * collection model, with each cell drawn by renderItem.
29
33
  */
30
- export const GridView = <T = unknown>(props: GridViewComponentProps<T>): ReactNode => {
34
+ export const GridView = <T = unknown>(props: GridViewProps<T>): ReactNode => {
31
35
  const {
32
36
  ref,
33
37
  items,
34
- model,
35
38
  renderItem,
36
- selected,
39
+ selectedIds,
37
40
  selectionMode,
38
41
  onSelectionChanged,
39
42
  estimatedItemHeight,
40
43
  estimatedItemWidth,
41
44
  ...intrinsicProps
42
- } = props as GridViewProps<T> & {
45
+ } = props as GridViewDeclarativeProps<T> & {
43
46
  ref?: Ref<Gtk.GridView | null>;
44
47
  estimatedItemHeight?: number;
45
48
  estimatedItemWidth?: number;
46
49
  [key: string]: unknown;
47
50
  };
48
51
 
49
- const renderItemFn = renderItem as (item: T) => ReactNode;
50
- const slotRenderer: SlotRenderer<T, unknown> = (value, _treeRow, isHeader) =>
51
- isHeader ? null : renderItemFn(value as T);
52
+ const cellRenderer: CellRenderer<T, unknown> = itemRenderer<T, unknown>(renderItem);
53
+
54
+ const widgetRef = useRef<Gtk.GridView | null>(null);
55
+ const setRef = useMergeRefs<Gtk.GridView>(ref, widgetRef);
56
+
57
+ const collection = useCollectionModel<T, unknown>({
58
+ items,
59
+ sections: undefined,
60
+ selectionMode,
61
+ selectedIds,
62
+ onSelectionChanged,
63
+ renderHeader: undefined,
64
+ });
65
+
66
+ const itemStore = useCellContainers<Gtk.GridView>({
67
+ object: widgetRef,
68
+ installer: factoryInstaller,
69
+ estimatedHeight: estimatedItemHeight,
70
+ estimatedWidth: estimatedItemWidth,
71
+ });
72
+
73
+ useInstalledModel(widgetRef, collection.installedModel, (widget, value) => widget.setModel(value));
52
74
 
53
75
  return (
54
- <CollectionView<T, unknown, Gtk.GridView>
55
- element={GtkGridView}
56
- intrinsicProps={intrinsicProps}
57
- ref={ref}
58
- items={model === undefined ? items : undefined}
59
- model={model as Gio.ListModel | undefined}
60
- renderItem={slotRenderer}
61
- autoexpand={undefined}
62
- renderHeader={undefined}
63
- estimatedHeight={estimatedItemHeight}
64
- estimatedWidth={estimatedItemWidth}
65
- selected={selected}
66
- selectionMode={selectionMode}
67
- onSelectionChanged={onSelectionChanged}
68
- factoryBinding={factoryBinding}
69
- headerFactoryBinding={undefined}
70
- modelBinding={modelBinding}
71
- />
76
+ <>
77
+ <GtkGridView {...intrinsicProps} ref={setRef} />
78
+ <CellRenderHost store={itemStore} resolver={collection.resolver} render={cellRenderer} />
79
+ </>
72
80
  );
73
81
  };