@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/src/drop-down.tsx CHANGED
@@ -1,259 +1,205 @@
1
- import type * as Adw from "@gtkx/gi/adw";
2
- import type * as Gio from "@gtkx/gi/gio";
3
- import type * as Gtk from "@gtkx/gi/gtk";
4
- import { AdwComboRow, type AdwComboRowProps } from "@gtkx/jsx/adw";
5
- import { GtkDropDown, type GtkDropDownProps, GtkLabel } from "@gtkx/jsx/gtk";
6
- import { useForwardedRef } from "@gtkx/react";
7
- import {
8
- createElement,
9
- type ElementType,
10
- type ReactElement,
11
- type ReactNode,
12
- type Ref,
13
- useCallback,
14
- useRef,
15
- useState,
16
- } from "react";
17
- import { useDropDownSelection } from "./hooks/use-drop-down-selection.js";
18
- import { useListModel } from "./hooks/use-list-model.js";
19
- import { useModelInstallation } from "./hooks/use-model-installation.js";
20
- import { type FactoryBinding, useRealizedSlots } from "./hooks/use-realized-slots.js";
21
- import { ListPortalHost } from "./list-portal-host.js";
22
- import type { SlotRenderer } from "./list-slot.js";
23
- import type { DropDownProps, ListItem } from "./types.js";
24
- import type { ItemResolver } from "./utils/item-resolver.js";
25
- import type { RealizedSlotStore } from "./utils/realized-slot-store.js";
26
-
27
- export interface DropDownWidget extends Gtk.Widget {
28
- getSelected(): number;
29
- setSelected(position: number): void;
30
- setModel(model: Gio.ListModel | null): void;
31
- setFactory(factory: Gtk.ListItemFactory | null): void;
32
- setListFactory(factory: Gtk.ListItemFactory | null): void;
33
- setHeaderFactory(factory: Gtk.ListItemFactory | null): void;
34
- }
1
+ import type * as GObject from "@gtkx/gi/gobject";
2
+ import type { ElementType, ReactNode, Ref, RefObject } from "react";
3
+ import { GtkDropDown, GtkLabel, GtkSignalListItemFactory } from "@gtkx/jsx/gtk";
4
+ import { useSignal } from "@gtkx/react";
5
+ import { omit } from "@gtkx/utils";
6
+ import { useEffectEvent, useLayoutEffect, useRef } from "react";
7
+ import type { Collection } from "./internal/collection.js";
8
+ import type { DropDownProps, ItemRenderer, RenderItemArgs } from "./types.js";
9
+ import { HeaderPortals, ItemPortals, useHeaderCells, useItemCells } from "./internal/cells.js";
10
+ import { useCollectionData } from "./internal/use-collection.js";
11
+ import { useWidgetRef } from "./internal/use-widget-ref.js";
12
+
13
+ type SelectableWidget = GObject.Object & { getSelected: () => number; selected: number };
14
+
15
+ type DropDownRuntimeProps = DropDownProps & {
16
+ component?: ElementType | undefined;
17
+ ref?: Ref<SelectableWidget | null> | undefined;
18
+ } & Record<string, unknown>;
19
+
20
+ type SelectionOptions = {
21
+ widget: SelectableWidget | null;
22
+ collection: Collection;
23
+ applying: RefObject<boolean>;
24
+ props: DropDownRuntimeProps;
25
+ };
35
26
 
36
- const itemFactoryBinding: FactoryBinding<DropDownWidget> = {
37
- install: (widget, factory) => widget.setFactory(factory),
38
- uninstall: (widget) => widget.setFactory(null),
27
+ type KnownSelection = {
28
+ known: { current: string | null };
29
+ onSelectionChanged: ((id: string) => void) | null | undefined;
39
30
  };
40
31
 
41
- const listFactoryBinding: FactoryBinding<DropDownWidget> = {
42
- install: (widget, factory) => widget.setListFactory(factory),
43
- uninstall: (widget) => widget.setListFactory(null),
32
+ type DropDownComponent = <T = unknown, S = unknown, C extends ElementType = typeof GtkDropDown>(
33
+ props: DropDownProps<T, S, C>,
34
+ ) => ReactNode;
35
+
36
+ const DROP_DOWN_PROPS: string[] = [
37
+ "component",
38
+ "items",
39
+ "sections",
40
+ "selectedId",
41
+ "onSelectionChanged",
42
+ "renderItem",
43
+ "renderListItem",
44
+ "renderHeader",
45
+ "ref",
46
+ ];
47
+
48
+ /**
49
+ * Renders a drop-down backed by a collection model, with customizable renderers for the
50
+ * collapsed display, popup rows, and popup section headers, and controlled selection.
51
+ * The backing widget defaults to GtkDropDown and is swappable through `component`.
52
+ */
53
+ const DropDown: DropDownComponent = DropDownImpl as DropDownComponent;
54
+
55
+ const describeValue = (value: unknown): string => {
56
+ if (typeof value === "string") {
57
+ return value;
58
+ }
59
+
60
+ try {
61
+ return JSON.stringify(value);
62
+ } catch {
63
+ return String(value);
64
+ }
44
65
  };
45
66
 
46
- const headerFactoryBinding: FactoryBinding<DropDownWidget> = {
47
- install: (widget, factory) => widget.setHeaderFactory(factory),
48
- uninstall: (widget) => widget.setHeaderFactory(null),
67
+ const defaultItemContent = (value: unknown): ReactNode =>
68
+ value == null ? null : <GtkLabel>{describeValue(value)}</GtkLabel>;
69
+
70
+ const defaultRenderItem = ({ item }: RenderItemArgs<unknown>): ReactNode => defaultItemContent(item);
71
+
72
+ const faceRenderer = (props: DropDownRuntimeProps): ItemRenderer<never> =>
73
+ props.renderItem ?? defaultRenderItem;
74
+
75
+ const listRenderer = (props: DropDownRuntimeProps): ItemRenderer<never> =>
76
+ props.renderListItem ?? props.renderItem ?? defaultRenderItem;
77
+
78
+ const resolvePosition = (
79
+ widget: SelectableWidget,
80
+ collection: Collection,
81
+ selectedId: string | null | undefined,
82
+ ): number => {
83
+ const requested = selectedId == null ? -1 : collection.positionFor(selectedId);
84
+
85
+ return requested >= 0 ? requested : widget.getSelected();
49
86
  };
50
87
 
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) });
88
+ const updateKnownSelection = (
89
+ tracker: KnownSelection,
90
+ effectiveId: string,
91
+ selectedId: string | null | undefined,
92
+ ): void => {
93
+ const { known, onSelectionChanged } = tracker;
94
+ const expectedId = selectedId ?? known.current;
95
+ const isNew = effectiveId !== known.current;
96
+ known.current = effectiveId;
97
+
98
+ if (expectedId !== null && effectiveId !== expectedId && isNew) {
99
+ onSelectionChanged?.(effectiveId);
100
+ }
54
101
  };
55
102
 
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
- }
103
+ const reportKnownSelection = (tracker: KnownSelection, id: string | null): void => {
104
+ const { known, onSelectionChanged } = tracker;
105
+
106
+ if (id === null || id === known.current) {
107
+ return;
108
+ }
68
109
 
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));
110
+ known.current = id;
111
+ onSelectionChanged?.(id);
72
112
  };
73
113
 
74
- const toPopupRenderer = <T, S>(
75
- renderListItem: ((value: T) => ReactNode) | null | undefined,
76
- renderItem: ((value: T) => ReactNode) | null | undefined,
77
- ): SlotRenderer<T, S> => {
78
- if (typeof renderListItem === "function")
79
- return (value, _treeRow, isHeader) => (isHeader ? null : renderListItem(value as T));
80
- return toItemRenderer<T, S>(renderItem);
114
+ const reportSelectedNotify = (
115
+ options: SelectionOptions,
116
+ tracker: KnownSelection,
117
+ applying: RefObject<boolean>,
118
+ ): void => {
119
+ const { widget, collection } = options;
120
+
121
+ if (widget === null || applying.current) {
122
+ return;
123
+ }
124
+
125
+ reportKnownSelection(tracker, collection.idAt(widget.getSelected()));
81
126
  };
82
127
 
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);
128
+ const applySelectedPosition = (widget: SelectableWidget, position: number, applying: RefObject<boolean>): void => {
129
+ applying.current = true;
130
+
131
+ try {
132
+ widget.selected = position;
133
+ } finally {
134
+ applying.current = false;
135
+ }
86
136
  };
87
137
 
88
- interface DropDownWiring<T, S, W extends DropDownWidget> {
89
- setRef: (value: W | null) => void;
90
- resolver: ItemResolver<T, S>;
91
- faceResolver: ItemResolver<T, S>;
92
- headerResolver: ItemResolver<T, S>;
93
- faceStore: RealizedSlotStore;
94
- popupStore: RealizedSlotStore;
95
- headerStore: RealizedSlotStore;
96
- useHeader: boolean;
97
- }
138
+ const useDropDownSelection = (options: SelectionOptions): void => {
139
+ const { widget, collection, applying } = options;
140
+ const { selectedId } = options.props;
141
+ const known = useRef<string | null>(null);
98
142
 
99
- const createFaceResolver = <T, S>(resolver: ItemResolver<T, S>, selectedPosition: number): ItemResolver<T, S> => ({
100
- count: resolver.count,
101
- positionOf: (id) => resolver.positionOf(id),
102
- idOf: (position) => resolver.idOf(position),
103
- resolve: (_position, treeRow) => resolver.resolve(selectedPosition, treeRow, null),
104
- });
105
-
106
- const useDropDownWiring = <T, S, W extends DropDownWidget>(
107
- props: DropDownImplProps<T, S, W>,
108
- ): DropDownWiring<T, S, W> => {
109
- const widgetRef = useRef<DropDownWidget | null>(null);
110
- const [widget, setWidget] = useState<DropDownWidget | null>(null);
111
- const captureWidget = useCallback((value: W | null) => {
112
- widgetRef.current = value;
113
- setWidget(value);
114
- }, []);
115
- const [, setRef] = useForwardedRef<W>(props.ref, captureWidget);
116
-
117
- const externalModel = props.model;
118
- const listModel = useListModel<T, S>(
119
- externalModel === undefined ? { items: props.items ?? undefined } : { model: externalModel },
120
- );
143
+ const syncKnownSelection = useEffectEvent((position: number): void => {
144
+ const effectiveId = collection.idAt(position);
121
145
 
122
- const useHeader = externalModel === undefined && typeof props.renderHeader === "function";
146
+ if (effectiveId === null) {
147
+ known.current = null;
123
148
 
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,
149
+ return;
150
+ }
151
+
152
+ const { onSelectionChanged } = options.props;
153
+ updateKnownSelection({ known, onSelectionChanged }, effectiveId, selectedId);
129
154
  });
130
155
 
131
- useModelInstallation(widgetRef, listModel.model, (widget, model) => widget.setModel(model));
156
+ useLayoutEffect(() => {
157
+ if (widget === null) {
158
+ return;
159
+ }
132
160
 
133
- const selectedPosition = useDropDownSelection<T, S>({
134
- widget,
135
- resolver: listModel.resolver,
136
- selectedId: props.selectedId,
137
- onSelectionChanged: props.onSelectionChanged,
138
- });
161
+ const position = resolvePosition(widget, collection, selectedId);
162
+ applySelectedPosition(widget, position, applying);
163
+ syncKnownSelection(position);
164
+ }, [widget, collection, selectedId, applying]);
139
165
 
140
- const controlledPosition =
141
- props.selectedId === undefined || props.selectedId === null
142
- ? -1
143
- : listModel.resolver.positionOf(props.selectedId);
144
- const facePosition = controlledPosition >= 0 ? controlledPosition : selectedPosition < 0 ? 0 : selectedPosition;
145
-
146
- return {
147
- setRef,
148
- resolver: listModel.resolver,
149
- faceResolver: createFaceResolver(listModel.resolver, facePosition),
150
- headerResolver: listModel.headerResolver,
151
- faceStore: face.store,
152
- popupStore: popup.store,
153
- headerStore: header.store,
154
- useHeader,
155
- };
166
+ useSignal(widget, "notify::selected", (): void => {
167
+ reportSelectedNotify(options, { known, onSelectionChanged: options.props.onSelectionChanged }, applying);
168
+ });
156
169
  };
157
170
 
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 });
171
+ function DropDownImpl(props: DropDownRuntimeProps): ReactNode {
172
+ const { component, items, sections, renderListItem, renderHeader, ref } = props;
173
+ const rest = omit(props, DROP_DOWN_PROPS);
174
+ const [widget, refCallback] = useWidgetRef<SelectableWidget>(ref);
175
+ const applying = useRef(false);
176
+ const collection = useCollectionData({ items, sections, applying });
177
+ const faceCells = useItemCells({ width: -1, height: -1 });
178
+ const listCells = useItemCells({ width: -1, height: -1 });
179
+ const headerCells = useHeaderCells({ width: -1, height: -1 });
180
+ useDropDownSelection({ widget, collection, applying, props });
181
+ const Component = component ?? GtkDropDown;
182
+ const hasHeader = typeof renderHeader === "function";
162
183
 
163
184
  return (
164
185
  <>
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)}
186
+ <Component
187
+ ref={refCallback}
188
+ model={collection.model}
189
+ factory={<GtkSignalListItemFactory {...faceCells.handlers} />}
190
+ {...(renderListItem != null && {
191
+ listFactory: <GtkSignalListItemFactory {...listCells.handlers} />,
192
+ })}
193
+ {...(hasHeader && { headerFactory: <GtkSignalListItemFactory {...headerCells.handlers} /> })}
194
+ {...rest}
171
195
  />
172
- {useHeader ? (
173
- <ListPortalHost
174
- store={headerStore}
175
- resolver={headerResolver}
176
- render={toHeaderRenderer<T, S>(props.renderHeader)}
177
- />
178
- ) : null}
196
+ <ItemPortals store={faceCells} render={faceRenderer(props)} collection={collection} />
197
+ {renderListItem != null && (
198
+ <ItemPortals store={listCells} render={listRenderer(props)} collection={collection} />
199
+ )}
200
+ {hasHeader && <HeaderPortals store={headerCells} render={renderHeader} collection={collection} />}
179
201
  </>
180
202
  );
181
- };
182
-
183
- type DropDownImplComponentProps<T, S, W extends DropDownWidget> = DropDownProps<T, S> & {
184
- ref?: Ref<W | null> | undefined;
185
- };
186
-
187
- const extractDropDownProps = <T, S, W extends DropDownWidget>(
188
- props: DropDownImplComponentProps<T, S, W>,
189
- ): {
190
- impl: Omit<DropDownImplProps<T, S, W>, "element" | "intrinsicProps">;
191
- intrinsicProps: Record<string, unknown>;
192
- } => {
193
- const {
194
- ref,
195
- items,
196
- model,
197
- renderItem,
198
- renderListItem,
199
- renderHeader,
200
- selectedId,
201
- onSelectionChanged,
202
- ...intrinsicProps
203
- } = props as DropDownProps<T, S> & {
204
- ref?: Ref<W | null>;
205
- [key: string]: unknown;
206
- };
207
-
208
- return {
209
- impl: {
210
- ref,
211
- items: items as ListItem<T, S>[] | undefined,
212
- model: model as Gio.ListModel | undefined,
213
- renderItem: renderItem as ((value: T) => ReactNode) | null | undefined,
214
- renderListItem: renderListItem as ((value: T) => ReactNode) | null | undefined,
215
- renderHeader: renderHeader as ((value: S) => ReactNode) | null | undefined,
216
- selectedId: selectedId as string | null | undefined,
217
- onSelectionChanged: onSelectionChanged as ((id: string) => void) | null | undefined,
218
- },
219
- intrinsicProps: intrinsicProps as Record<string, unknown>,
220
- };
221
- };
222
-
223
- /**
224
- * Props for the {@link DropDown} component: the raw `GtkDropDown` element
225
- * surface with its factory/model wiring replaced by the declarative
226
- * {@link DropDownProps} API.
227
- */
228
- export type DropDownComponentProps<T = unknown, S = unknown> = Omit<GtkDropDownProps, keyof DropDownProps<T, S>> &
229
- DropDownProps<T, S>;
230
-
231
- /**
232
- * Props for the {@link ComboRow} component: the raw `AdwComboRow` element
233
- * surface with its factory/model wiring replaced by the declarative
234
- * {@link DropDownProps} API.
235
- */
236
- export type ComboRowComponentProps<T = unknown, S = unknown> = Omit<AdwComboRowProps, keyof DropDownProps<T, S>> &
237
- DropDownProps<T, S>;
238
-
239
- /**
240
- * A `GtkDropDown` driven by a declarative `items` model with a controlled
241
- * `selectedId` and per-slot renderers for the selected face, the popup list,
242
- * and section headers. Supplying an external `model` switches to the
243
- * uncontrolled form.
244
- */
245
- export const DropDown = <T = unknown, S = unknown>(props: DropDownComponentProps<T, S>): ReactNode => {
246
- const { impl, intrinsicProps } = extractDropDownProps<T, S, Gtk.DropDown>(props);
247
- return <DropDownImpl<T, S, Gtk.DropDown> element={GtkDropDown} intrinsicProps={intrinsicProps} {...impl} />;
248
- };
203
+ }
249
204
 
250
- /**
251
- * An `AdwComboRow` driven by a declarative `items` model with a controlled
252
- * `selectedId` and per-slot renderers for the selected face, the popup list,
253
- * and section headers. Supplying an external `model` switches to the
254
- * uncontrolled form.
255
- */
256
- export const ComboRow = <T = unknown, S = unknown>(props: ComboRowComponentProps<T, S>): ReactNode => {
257
- const { impl, intrinsicProps } = extractDropDownProps<T, S, Adw.ComboRow>(props);
258
- return <DropDownImpl<T, S, Adw.ComboRow> element={AdwComboRow} intrinsicProps={intrinsicProps} {...impl} />;
259
- };
205
+ export { DropDown };
package/src/grid-view.tsx CHANGED
@@ -1,73 +1,37 @@
1
- import type * as Gio from "@gtkx/gi/gio";
2
- import type * as Gtk from "@gtkx/gi/gtk";
3
- import { GtkGridView, type GtkGridViewProps } from "@gtkx/jsx/gtk";
4
- import type { ReactNode, Ref } from "react";
5
- import { CollectionView, type ModelBinding } from "./collection-view.js";
6
- import type { SlotRenderer } from "./list-slot.js";
1
+ import type { ReactNode } from "react";
2
+ import { GtkGridView, GtkSignalListItemFactory } from "@gtkx/jsx/gtk";
3
+ import { omit } from "@gtkx/utils";
7
4
  import type { GridViewProps } from "./types.js";
5
+ import { ItemPortals, useItemCells } from "./internal/cells.js";
6
+ import { useCollection } from "./internal/use-collection.js";
8
7
 
9
- const factoryBinding = {
10
- install: (widget: Gtk.GridView, factory: Gtk.SignalListItemFactory) => widget.setFactory(factory),
11
- uninstall: (widget: Gtk.GridView) => widget.setFactory(null),
12
- };
13
-
14
- const modelBinding: ModelBinding<Gtk.GridView> = {
15
- install: (widget, model) => widget.setModel(model as Gtk.SelectionModel),
16
- };
17
-
18
- /**
19
- * Props for the {@link GridView} component: the raw `GtkGridView` element
20
- * surface with its factory/model wiring replaced by the declarative
21
- * {@link GridViewProps} API.
22
- */
23
- export type GridViewComponentProps<T = unknown> = Omit<GtkGridViewProps, keyof GridViewProps<T>> & GridViewProps<T>;
8
+ const GRID_VIEW_PROPS = [
9
+ "items",
10
+ "renderItem",
11
+ "selectedIds",
12
+ "onSelectionChanged",
13
+ "selectionMode",
14
+ "estimatedItemHeight",
15
+ "estimatedItemWidth",
16
+ ] as const satisfies (keyof GridViewProps)[];
24
17
 
25
18
  /**
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.
19
+ * Renders a Gtk.GridView of uniform cells from declarative items, with per-cell
20
+ * rendering, controlled selection, and estimated item sizing.
29
21
  */
30
- export const GridView = <T = unknown>(props: GridViewComponentProps<T>): ReactNode => {
31
- const {
32
- ref,
33
- items,
34
- model,
35
- renderItem,
36
- selected,
37
- selectionMode,
38
- onSelectionChanged,
39
- estimatedItemHeight,
40
- estimatedItemWidth,
41
- ...intrinsicProps
42
- } = props as GridViewProps<T> & {
43
- ref?: Ref<Gtk.GridView | null>;
44
- estimatedItemHeight?: number;
45
- estimatedItemWidth?: number;
46
- [key: string]: unknown;
47
- };
48
-
49
- const renderItemFn = renderItem as (item: T) => ReactNode;
50
- const slotRenderer: SlotRenderer<T, unknown> = (value, _treeRow, isHeader) =>
51
- isHeader ? null : renderItemFn(value as T);
22
+ function GridView<T = unknown>(props: GridViewProps<T>): ReactNode {
23
+ const { renderItem, estimatedItemHeight, estimatedItemWidth } = props;
24
+ const rest = omit(props, GRID_VIEW_PROPS);
25
+ const size = { width: estimatedItemWidth ?? -1, height: estimatedItemHeight ?? -1 };
26
+ const { collection, selection } = useCollection({ ...props, flat: true });
27
+ const itemCells = useItemCells(size);
52
28
 
53
29
  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
- />
30
+ <>
31
+ <GtkGridView model={selection} factory={<GtkSignalListItemFactory {...itemCells.handlers} />} {...rest} />
32
+ <ItemPortals store={itemCells} render={renderItem} collection={collection} />
33
+ </>
72
34
  );
73
- };
35
+ }
36
+
37
+ export { GridView };
package/src/index.ts CHANGED
@@ -1,31 +1,22 @@
1
- /**
2
- * Higher-level GTKX components: declarative, opinionated abstractions layered on
3
- * top of the generated `@gtkx/jsx` element bindings and the `@gtkx/react`
4
- * runtime. Each component pairs a GObject widget with a gtkx-authored prop
5
- * surface (item models, render callbacks, selection, sorting) and is exported
6
- * under an unprefixed name to distinguish it from the raw `Gtk*`/`Adw*`/`G*`
7
- * intrinsic element it builds upon.
8
- *
9
- * @packageDocumentation
10
- */
11
-
12
- export { ColumnView, type ColumnViewComponentProps } from "./column-view.js";
13
- export { ColumnViewColumn, type ColumnViewColumnComponentProps } from "./column-view-column.js";
14
- export { ConstraintLayout, type ConstraintLayoutProps } from "./constraint-layout.js";
15
- export { ComboRow, type ComboRowComponentProps, DropDown, type DropDownComponentProps } from "./drop-down.js";
16
- export { GridView, type GridViewComponentProps } from "./grid-view.js";
17
- export { ListView, type ListViewComponentProps } from "./list-view.js";
18
- export { Menu, type MenuProps } from "./menu.js";
1
+ export { ColumnView } from "./column-view.js";
2
+ export { DropDown } from "./drop-down.js";
3
+ export { GridView } from "./grid-view.js";
4
+ export { ListView } from "./list-view.js";
5
+ export { SizeGroup } from "./size-group.js";
19
6
  export type {
20
- ColumnViewColumnProps,
7
+ ChildProps,
8
+ Column,
21
9
  ColumnViewProps,
22
- ConstraintGuideProps,
23
- ConstraintProps,
24
- ConstraintVflProps,
25
10
  DropDownProps,
26
11
  GridViewProps,
27
- ListItem,
12
+ HeaderRenderer,
13
+ Item,
14
+ ItemRenderer,
28
15
  ListViewProps,
29
- MenuEntry,
30
- MenuItemsProps,
16
+ RenderHeaderArgs,
17
+ RenderItemArgs,
18
+ Section,
19
+ SizeGroupChildProps,
20
+ SizeGroupProps,
21
+ WidgetProps,
31
22
  } from "./types.js";