@gtkx/components 1.0.0-rc.1 → 1.0.0-rc.3

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 (220) hide show
  1. package/dist/adw/combo-row.d.ts +11 -0
  2. package/dist/adw/combo-row.d.ts.map +1 -0
  3. package/dist/adw/combo-row.js +14 -0
  4. package/dist/adw/combo-row.js.map +1 -0
  5. package/dist/adw/index.d.ts +4 -0
  6. package/dist/adw/index.d.ts.map +1 -0
  7. package/dist/adw/index.js +3 -0
  8. package/dist/adw/index.js.map +1 -0
  9. package/dist/adw/toast.d.ts +10 -0
  10. package/dist/adw/toast.d.ts.map +1 -0
  11. package/dist/adw/toast.js +51 -0
  12. package/dist/adw/toast.js.map +1 -0
  13. package/dist/adw/types.d.ts +35 -0
  14. package/dist/adw/types.d.ts.map +1 -0
  15. package/dist/adw/types.js +2 -0
  16. package/dist/adw/types.js.map +1 -0
  17. package/dist/column-view.d.ts +7 -29
  18. package/dist/column-view.d.ts.map +1 -1
  19. package/dist/column-view.js +98 -70
  20. package/dist/column-view.js.map +1 -1
  21. package/dist/drop-down.d.ts +7 -28
  22. package/dist/drop-down.d.ts.map +1 -1
  23. package/dist/drop-down.js +10 -86
  24. package/dist/drop-down.js.map +1 -1
  25. package/dist/grid-view.d.ts +6 -16
  26. package/dist/grid-view.d.ts.map +1 -1
  27. package/dist/grid-view.js +25 -33
  28. package/dist/grid-view.js.map +1 -1
  29. package/dist/index.d.ts +6 -12
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -6
  32. package/dist/index.js.map +1 -1
  33. package/dist/internal/cells.d.ts +50 -0
  34. package/dist/internal/cells.d.ts.map +1 -0
  35. package/dist/internal/cells.js +185 -0
  36. package/dist/internal/cells.js.map +1 -0
  37. package/dist/internal/collection-index.d.ts +21 -0
  38. package/dist/internal/collection-index.d.ts.map +1 -0
  39. package/dist/internal/collection-index.js +106 -0
  40. package/dist/internal/collection-index.js.map +1 -0
  41. package/dist/internal/collection-model.d.ts +12 -0
  42. package/dist/internal/collection-model.d.ts.map +1 -0
  43. package/dist/internal/collection-model.js +164 -0
  44. package/dist/internal/collection-model.js.map +1 -0
  45. package/dist/internal/collection.d.ts +18 -0
  46. package/dist/internal/collection.d.ts.map +1 -0
  47. package/dist/internal/collection.js +64 -0
  48. package/dist/internal/collection.js.map +1 -0
  49. package/dist/internal/drop-down.d.ts +16 -0
  50. package/dist/internal/drop-down.d.ts.map +1 -0
  51. package/dist/internal/drop-down.js +110 -0
  52. package/dist/internal/drop-down.js.map +1 -0
  53. package/dist/internal/expansion.d.ts +9 -0
  54. package/dist/internal/expansion.d.ts.map +1 -0
  55. package/dist/internal/expansion.js +74 -0
  56. package/dist/internal/expansion.js.map +1 -0
  57. package/dist/internal/selection.d.ts +13 -0
  58. package/dist/internal/selection.d.ts.map +1 -0
  59. package/dist/internal/selection.js +100 -0
  60. package/dist/internal/selection.js.map +1 -0
  61. package/dist/internal/use-collection.d.ts +17 -0
  62. package/dist/internal/use-collection.d.ts.map +1 -0
  63. package/dist/internal/use-collection.js +34 -0
  64. package/dist/internal/use-collection.js.map +1 -0
  65. package/dist/internal/use-widget-ref.d.ts +4 -0
  66. package/dist/internal/use-widget-ref.d.ts.map +1 -0
  67. package/dist/internal/use-widget-ref.js +8 -0
  68. package/dist/internal/use-widget-ref.js.map +1 -0
  69. package/dist/list-view.d.ts +5 -20
  70. package/dist/list-view.d.ts.map +1 -1
  71. package/dist/list-view.js +32 -53
  72. package/dist/list-view.js.map +1 -1
  73. package/dist/size-group.d.ts +8 -16
  74. package/dist/size-group.d.ts.map +1 -1
  75. package/dist/size-group.js +35 -51
  76. package/dist/size-group.js.map +1 -1
  77. package/dist/types.d.ts +98 -27
  78. package/dist/types.d.ts.map +1 -1
  79. package/dist/types.js.map +1 -1
  80. package/package.json +17 -6
  81. package/src/adw/combo-row.tsx +20 -0
  82. package/src/adw/index.ts +9 -0
  83. package/src/adw/toast.tsx +74 -0
  84. package/src/adw/types.ts +45 -0
  85. package/src/column-view.tsx +176 -160
  86. package/src/drop-down.tsx +12 -185
  87. package/src/grid-view.tsx +29 -73
  88. package/src/index.ts +18 -26
  89. package/src/internal/cells.tsx +345 -0
  90. package/src/internal/collection-index.ts +168 -0
  91. package/src/internal/collection-model.ts +246 -0
  92. package/src/internal/collection.ts +104 -0
  93. package/src/internal/drop-down.tsx +186 -0
  94. package/src/internal/expansion.ts +122 -0
  95. package/src/internal/selection.tsx +162 -0
  96. package/src/internal/use-collection.tsx +57 -0
  97. package/src/internal/use-widget-ref.ts +11 -0
  98. package/src/list-view.tsx +43 -139
  99. package/src/size-group.tsx +63 -82
  100. package/src/types.ts +158 -28
  101. package/dist/cell.d.ts +0 -30
  102. package/dist/cell.d.ts.map +0 -1
  103. package/dist/cell.js +0 -45
  104. package/dist/cell.js.map +0 -1
  105. package/dist/column-view-column.d.ts +0 -27
  106. package/dist/column-view-column.d.ts.map +0 -1
  107. package/dist/column-view-column.js +0 -30
  108. package/dist/column-view-column.js.map +0 -1
  109. package/dist/constraint-layout-apply.d.ts +0 -55
  110. package/dist/constraint-layout-apply.d.ts.map +0 -1
  111. package/dist/constraint-layout-apply.js +0 -107
  112. package/dist/constraint-layout-apply.js.map +0 -1
  113. package/dist/constraint-layout.d.ts +0 -20
  114. package/dist/constraint-layout.d.ts.map +0 -1
  115. package/dist/constraint-layout.js +0 -88
  116. package/dist/constraint-layout.js.map +0 -1
  117. package/dist/fixed.d.ts +0 -26
  118. package/dist/fixed.d.ts.map +0 -1
  119. package/dist/fixed.js +0 -40
  120. package/dist/fixed.js.map +0 -1
  121. package/dist/grid.d.ts +0 -27
  122. package/dist/grid.d.ts.map +0 -1
  123. package/dist/grid.js +0 -48
  124. package/dist/grid.js.map +0 -1
  125. package/dist/hooks/use-cell-containers.d.ts +0 -18
  126. package/dist/hooks/use-cell-containers.d.ts.map +0 -1
  127. package/dist/hooks/use-cell-containers.js +0 -67
  128. package/dist/hooks/use-cell-containers.js.map +0 -1
  129. package/dist/hooks/use-collection-model.d.ts +0 -26
  130. package/dist/hooks/use-collection-model.d.ts.map +0 -1
  131. package/dist/hooks/use-collection-model.js +0 -35
  132. package/dist/hooks/use-collection-model.js.map +0 -1
  133. package/dist/hooks/use-collection-widget.d.ts +0 -28
  134. package/dist/hooks/use-collection-widget.d.ts.map +0 -1
  135. package/dist/hooks/use-collection-widget.js +0 -21
  136. package/dist/hooks/use-collection-widget.js.map +0 -1
  137. package/dist/hooks/use-drop-down-selection.d.ts +0 -16
  138. package/dist/hooks/use-drop-down-selection.d.ts.map +0 -1
  139. package/dist/hooks/use-drop-down-selection.js +0 -35
  140. package/dist/hooks/use-drop-down-selection.js.map +0 -1
  141. package/dist/hooks/use-expansion-model.d.ts +0 -12
  142. package/dist/hooks/use-expansion-model.d.ts.map +0 -1
  143. package/dist/hooks/use-expansion-model.js +0 -71
  144. package/dist/hooks/use-expansion-model.js.map +0 -1
  145. package/dist/hooks/use-header-menu.d.ts +0 -4
  146. package/dist/hooks/use-header-menu.d.ts.map +0 -1
  147. package/dist/hooks/use-header-menu.js +0 -20
  148. package/dist/hooks/use-header-menu.js.map +0 -1
  149. package/dist/hooks/use-installed-model.d.ts +0 -4
  150. package/dist/hooks/use-installed-model.d.ts.map +0 -1
  151. package/dist/hooks/use-installed-model.js +0 -15
  152. package/dist/hooks/use-installed-model.js.map +0 -1
  153. package/dist/hooks/use-list-model.d.ts +0 -19
  154. package/dist/hooks/use-list-model.d.ts.map +0 -1
  155. package/dist/hooks/use-list-model.js +0 -97
  156. package/dist/hooks/use-list-model.js.map +0 -1
  157. package/dist/hooks/use-placed-child.d.ts +0 -18
  158. package/dist/hooks/use-placed-child.d.ts.map +0 -1
  159. package/dist/hooks/use-placed-child.js +0 -47
  160. package/dist/hooks/use-placed-child.js.map +0 -1
  161. package/dist/hooks/use-selection-model.d.ts +0 -13
  162. package/dist/hooks/use-selection-model.d.ts.map +0 -1
  163. package/dist/hooks/use-selection-model.js +0 -103
  164. package/dist/hooks/use-selection-model.js.map +0 -1
  165. package/dist/hooks/use-sort-handler.d.ts +0 -17
  166. package/dist/hooks/use-sort-handler.d.ts.map +0 -1
  167. package/dist/hooks/use-sort-handler.js +0 -87
  168. package/dist/hooks/use-sort-handler.js.map +0 -1
  169. package/dist/menu-items.d.ts +0 -5
  170. package/dist/menu-items.d.ts.map +0 -1
  171. package/dist/menu-items.js +0 -48
  172. package/dist/menu-items.js.map +0 -1
  173. package/dist/menu.d.ts +0 -14
  174. package/dist/menu.d.ts.map +0 -1
  175. package/dist/menu.js +0 -24
  176. package/dist/menu.js.map +0 -1
  177. package/dist/overlay.d.ts +0 -24
  178. package/dist/overlay.d.ts.map +0 -1
  179. package/dist/overlay.js +0 -49
  180. package/dist/overlay.js.map +0 -1
  181. package/dist/utils/cell-container-store.d.ts +0 -28
  182. package/dist/utils/cell-container-store.d.ts.map +0 -1
  183. package/dist/utils/cell-container-store.js +0 -98
  184. package/dist/utils/cell-container-store.js.map +0 -1
  185. package/dist/utils/item-models.d.ts +0 -10
  186. package/dist/utils/item-models.d.ts.map +0 -1
  187. package/dist/utils/item-models.js +0 -82
  188. package/dist/utils/item-models.js.map +0 -1
  189. package/dist/utils/item-resolver.d.ts +0 -25
  190. package/dist/utils/item-resolver.d.ts.map +0 -1
  191. package/dist/utils/item-resolver.js +0 -102
  192. package/dist/utils/item-resolver.js.map +0 -1
  193. package/dist/utils/list-item-flatten.d.ts +0 -24
  194. package/dist/utils/list-item-flatten.d.ts.map +0 -1
  195. package/dist/utils/list-item-flatten.js +0 -68
  196. package/dist/utils/list-item-flatten.js.map +0 -1
  197. package/src/cell.tsx +0 -144
  198. package/src/column-view-column.tsx +0 -82
  199. package/src/constraint-layout-apply.ts +0 -173
  200. package/src/constraint-layout.tsx +0 -135
  201. package/src/fixed.tsx +0 -86
  202. package/src/grid.tsx +0 -88
  203. package/src/hooks/use-cell-containers.ts +0 -93
  204. package/src/hooks/use-collection-model.ts +0 -60
  205. package/src/hooks/use-collection-widget.ts +0 -47
  206. package/src/hooks/use-drop-down-selection.ts +0 -52
  207. package/src/hooks/use-expansion-model.ts +0 -82
  208. package/src/hooks/use-header-menu.ts +0 -34
  209. package/src/hooks/use-installed-model.ts +0 -20
  210. package/src/hooks/use-list-model.ts +0 -161
  211. package/src/hooks/use-placed-child.ts +0 -79
  212. package/src/hooks/use-selection-model.ts +0 -114
  213. package/src/hooks/use-sort-handler.ts +0 -107
  214. package/src/menu-items.ts +0 -48
  215. package/src/menu.tsx +0 -32
  216. package/src/overlay.tsx +0 -88
  217. package/src/utils/cell-container-store.ts +0 -114
  218. package/src/utils/item-models.ts +0 -113
  219. package/src/utils/item-resolver.ts +0 -138
  220. package/src/utils/list-item-flatten.ts +0 -90
@@ -0,0 +1,162 @@
1
+ import type * as Gio from "@gtkx/gi/gio";
2
+ import type { ReactElement } from "react";
3
+ import * as Gtk from "@gtkx/gi/gtk";
4
+ import { GtkMultiSelection, GtkNoSelection, GtkSingleSelection } from "@gtkx/jsx/gtk";
5
+ import { useEffectEvent, useLayoutEffect, useState } from "react";
6
+ import type { Collection } from "./collection.js";
7
+
8
+ type SelectionOptions = {
9
+ collection: Collection;
10
+ selectedIds?: string[] | null | undefined;
11
+ onSelectionChanged?: ((ids: string[]) => void) | null | undefined;
12
+ selectionMode?: Gtk.SelectionMode | null | undefined;
13
+ onItemsChanged: () => void;
14
+ };
15
+
16
+ type LastSelection = {
17
+ selection: Gtk.SelectionModel | null;
18
+ key: string | null;
19
+ };
20
+
21
+ type SelectionElementProps = {
22
+ ref: (value: Gtk.SelectionModel | null) => void;
23
+ model: Gio.ListModel;
24
+ onSelectionChanged: () => void;
25
+ onItemsChanged: () => void;
26
+ };
27
+
28
+ function getSelectedIds(selection: Gtk.SelectionModel, collection: Collection): string[] {
29
+ const bitset = selection.getSelection();
30
+ const size = Number(bitset.getSize());
31
+ const ids: string[] = [];
32
+
33
+ for (let index = 0; index < size; index++) {
34
+ pushId(collection.idAt(bitset.getNth(index)), ids);
35
+ }
36
+
37
+ return ids;
38
+ }
39
+
40
+ function pushId(id: string | null, ids: string[]): void {
41
+ if (id !== null) {
42
+ ids.push(id);
43
+ }
44
+ }
45
+
46
+ function applySingleSelection(selection: Gtk.SingleSelection, positions: number[]): void {
47
+ const [first] = positions;
48
+
49
+ if (first === undefined) {
50
+ selection.unselectAll();
51
+
52
+ return;
53
+ }
54
+
55
+ selection.selectItem(first, true);
56
+ }
57
+
58
+ function applyMultiSelection(selection: Gtk.SelectionModel, positions: number[]): void {
59
+ const selected = Gtk.Bitset.newEmpty();
60
+
61
+ for (const position of positions) {
62
+ selected.add(position);
63
+ }
64
+
65
+ selection.setSelection(selected, Gtk.Bitset.newRange(0, selection.getNItems()));
66
+ }
67
+
68
+ function applySelection(selection: Gtk.SelectionModel, collection: Collection, ids: string[]): void {
69
+ if (selection instanceof Gtk.NoSelection) {
70
+ return;
71
+ }
72
+
73
+ const positions = collection.positionsFor(ids);
74
+
75
+ if (ids.length > 0 && positions.length === 0) {
76
+ return;
77
+ }
78
+
79
+ if (selection instanceof Gtk.SingleSelection) {
80
+ applySingleSelection(selection, positions);
81
+
82
+ return;
83
+ }
84
+
85
+ applyMultiSelection(selection, positions);
86
+ }
87
+
88
+ function reportSelection(
89
+ selection: Gtk.SelectionModel | null,
90
+ collection: Collection,
91
+ last: LastSelection,
92
+ onSelectionChanged: ((ids: string[]) => void) | null | undefined,
93
+ ): void {
94
+ if (selection === null) {
95
+ return;
96
+ }
97
+
98
+ const ids = getSelectedIds(selection, collection);
99
+ const key = ids.join(" ");
100
+
101
+ if (last.selection === selection && last.key === key) {
102
+ return;
103
+ }
104
+
105
+ last.selection = selection;
106
+ last.key = key;
107
+ onSelectionChanged?.(ids);
108
+ }
109
+
110
+ function selectionElement(mode: Gtk.SelectionMode | null | undefined, props: SelectionElementProps): ReactElement {
111
+ if (mode === Gtk.SelectionMode.MULTIPLE) {
112
+ return <GtkMultiSelection {...props} />;
113
+ }
114
+
115
+ if (mode === Gtk.SelectionMode.NONE) {
116
+ return <GtkNoSelection {...props} />;
117
+ }
118
+
119
+ return <GtkSingleSelection {...props} autoselect={false} canUnselect />;
120
+ }
121
+
122
+ function syncSelection(
123
+ selection: Gtk.SelectionModel | null,
124
+ collection: Collection,
125
+ selectedIds: string[] | null | undefined,
126
+ ): void {
127
+ if (selection === null || selectedIds == null) {
128
+ return;
129
+ }
130
+
131
+ applySelection(selection, collection, selectedIds);
132
+ }
133
+
134
+ function newLastSelection(): LastSelection {
135
+ return { selection: null, key: null };
136
+ }
137
+
138
+ function useSelection(options: SelectionOptions): ReactElement {
139
+ const { collection, selectedIds, onSelectionChanged, selectionMode } = options;
140
+ const [selection, setSelection] = useState<Gtk.SelectionModel | null>(null);
141
+ const [last] = useState<LastSelection>(newLastSelection);
142
+
143
+ const report = useEffectEvent((): void => {
144
+ reportSelection(selection, collection, last, onSelectionChanged);
145
+ });
146
+
147
+ useLayoutEffect(() => {
148
+ syncSelection(selection, collection, selectedIds);
149
+ report();
150
+ }, [selection, collection, selectedIds]);
151
+
152
+ return selectionElement(selectionMode, {
153
+ ref: setSelection,
154
+ model: collection.model,
155
+ onSelectionChanged: () => {
156
+ reportSelection(selection, collection, last, onSelectionChanged);
157
+ },
158
+ onItemsChanged: options.onItemsChanged,
159
+ });
160
+ }
161
+
162
+ export { useSelection, type SelectionOptions };
@@ -0,0 +1,57 @@
1
+ import type { ReactElement } from "react";
2
+ import { useLayoutEffect, useMemo, useState } from "react";
3
+ import type { ExpansionProps, Item, Section, SelectionProps } from "../types.js";
4
+ import type { Collection } from "./collection.js";
5
+ import { createCollectionIndex } from "./collection-index.js";
6
+ import { createCollectionModel } from "./collection-model.js";
7
+ import { createCollection } from "./collection.js";
8
+ import { useExpansion } from "./expansion.js";
9
+ import { useSelection } from "./selection.js";
10
+
11
+ type CollectionDataOptions = {
12
+ items?: Item[] | undefined;
13
+ sections?: Section[] | undefined;
14
+ flat?: boolean | undefined;
15
+ };
16
+
17
+ type CollectionOptions = CollectionDataOptions & SelectionProps & ExpansionProps;
18
+
19
+ type CollectionResult = {
20
+ collection: Collection;
21
+ selection: ReactElement;
22
+ };
23
+
24
+ function useCollectionData(options: CollectionDataOptions): Collection {
25
+ const { items, sections, flat } = options;
26
+ const [gtk] = useState(createCollectionModel);
27
+ const index = useMemo(() => createCollectionIndex(items, sections, flat === true), [items, sections, flat]);
28
+ const collection = useMemo(() => createCollection(gtk, index), [gtk, index]);
29
+
30
+ useLayoutEffect(() => {
31
+ gtk.sync(index);
32
+ }, [gtk, index]);
33
+
34
+ return collection;
35
+ }
36
+
37
+ function useCollection(options: CollectionOptions): CollectionResult {
38
+ const collection = useCollectionData(options);
39
+
40
+ const onItemsChanged = useExpansion({
41
+ collection,
42
+ expandedIds: options.expandedIds,
43
+ onExpandedChange: options.onExpandedChange,
44
+ });
45
+
46
+ const selection = useSelection({
47
+ collection,
48
+ selectedIds: options.selectedIds,
49
+ onSelectionChanged: options.onSelectionChanged,
50
+ selectionMode: options.selectionMode,
51
+ onItemsChanged,
52
+ });
53
+
54
+ return { collection, selection };
55
+ }
56
+
57
+ export { useCollection, useCollectionData, type CollectionDataOptions, type CollectionOptions };
@@ -0,0 +1,11 @@
1
+ import type { Ref, RefCallback } from "react";
2
+ import { useMergedRef } from "@gtkx/react/internal";
3
+ import { useState } from "react";
4
+
5
+ const useWidgetRef = <T>(external: Ref<T | null> | null | undefined): [T | null, RefCallback<T | null>] => {
6
+ const [widget, setWidget] = useState<T | null>(null);
7
+
8
+ return [widget, useMergedRef<T | null>(external, setWidget)];
9
+ };
10
+
11
+ export { useWidgetRef };
package/src/list-view.tsx CHANGED
@@ -1,148 +1,52 @@
1
- import type * as Gtk from "@gtkx/gi/gtk";
2
- import { GtkListView, type GtkListViewProps } from "@gtkx/jsx/gtk";
3
- import type { ReactNode, Ref } from "react";
4
- import {
5
- type CellRenderer,
6
- CellRenderHost,
7
- createTreeRenderContext,
8
- HeaderRenderHost,
9
- itemRenderer,
10
- type TreeRenderContext,
11
- } from "./cell.js";
12
- import { makeFactoryInstaller, useCellContainers } from "./hooks/use-cell-containers.js";
13
- import { useCollectionWidget } from "./hooks/use-collection-widget.js";
14
- import type {
15
- CollectionItemSizeProps,
16
- ControlledExpansionProps,
17
- ControlledSelectionProps,
18
- ItemNode,
19
- RenderItemProps,
20
- SectionNode,
21
- } from "./types.js";
22
- import type { CellContainerStore } from "./utils/cell-container-store.js";
23
- import type { ItemResolver } from "./utils/item-resolver.js";
24
-
25
- const factoryInstaller = makeFactoryInstaller<Gtk.ListView>((widget, factory) => widget.setFactory(factory));
26
-
27
- const headerFactoryInstaller = makeFactoryInstaller<Gtk.ListView>((widget, factory) =>
28
- widget.setHeaderFactory(factory),
29
- );
30
-
31
- export type ListViewDeclarativeProps<T = unknown, S = unknown> = CollectionItemSizeProps &
32
- ControlledSelectionProps &
33
- ControlledExpansionProps & {
34
- items?: ItemNode<T>[] | undefined;
35
- sections?: SectionNode<S, T>[] | undefined;
36
- renderItem: (props: RenderItemProps<T>) => ReactNode;
37
- renderHeader?: ((info: { section: S }) => ReactNode) | null | undefined;
38
- };
1
+ import type { ReactNode } from "react";
2
+ import { GtkListView, GtkSignalListItemFactory } from "@gtkx/jsx/gtk";
3
+ import { omit } from "@gtkx/utils";
4
+ import type { ListViewProps } from "./types.js";
5
+ import { HeaderPortals, ItemPortals, useHeaderCells, useItemCells } from "./internal/cells.js";
6
+ import { useCollection } from "./internal/use-collection.js";
7
+
8
+ const LIST_VIEW_PROPS = [
9
+ "items",
10
+ "sections",
11
+ "renderItem",
12
+ "renderHeader",
13
+ "selectedIds",
14
+ "onSelectionChanged",
15
+ "selectionMode",
16
+ "expandedIds",
17
+ "onExpandedChange",
18
+ "estimatedItemHeight",
19
+ "estimatedItemWidth",
20
+ ] as const satisfies (keyof ListViewProps)[];
39
21
 
40
22
  /**
41
- * Props for {@link ListView}. Combines the underlying Gtk.ListView props with
42
- * declarative collection props: flat items or grouped sections, a per-row renderItem,
43
- * an optional section header renderer, controlled selection and expansion, and
44
- * estimated item sizing.
23
+ * Renders a Gtk.ListView from declarative items or sections, with per-row rendering,
24
+ * controlled selection, controlled tree expansion, and estimated item sizing.
45
25
  */
46
- export type ListViewProps<T = unknown, S = unknown> = Omit<
47
- GtkListViewProps,
48
- "model" | "factory" | "headerFactory" | keyof ListViewDeclarativeProps<T, S>
49
- > &
50
- ListViewDeclarativeProps<T, S>;
51
-
52
- interface ListViewWiring<T, S> {
53
- setRef: (value: Gtk.ListView | null) => void;
54
- resolver: ItemResolver<T, S>;
55
- headerResolver: ItemResolver<T, S>;
56
- itemStore: CellContainerStore;
57
- headerStore: CellContainerStore;
58
- useHeader: boolean;
59
- rowId: (row: Gtk.TreeListRow) => string | undefined;
60
- }
61
-
62
- const useListViewWiring = <T, S>(props: NormalizedListViewProps<T, S>): ListViewWiring<T, S> => {
63
- const { widgetRef, setRef, collection } = useCollectionWidget<Gtk.ListView, T, S>(props);
64
-
65
- const useHeader = collection.useHeader;
66
-
67
- const itemStore = useCellContainers<Gtk.ListView>({
68
- object: widgetRef,
69
- installer: factoryInstaller,
70
- estimatedHeight: props.estimatedItemHeight,
71
- estimatedWidth: props.estimatedItemWidth,
72
- });
73
- const headerStore = useCellContainers<Gtk.ListView>({
74
- object: useHeader ? widgetRef : null,
75
- installer: headerFactoryInstaller,
76
- estimatedHeight: props.estimatedItemHeight,
77
- estimatedWidth: props.estimatedItemWidth,
78
- });
79
-
80
- return {
81
- setRef,
82
- resolver: collection.resolver,
83
- headerResolver: collection.headerResolver,
84
- itemStore,
85
- headerStore,
86
- useHeader,
87
- rowId: collection.rowId,
88
- };
89
- };
90
-
91
- type NormalizedListViewProps<T, S> = ListViewDeclarativeProps<T, S> & {
92
- ref?: Ref<Gtk.ListView | null>;
93
- estimatedItemHeight?: number;
94
- estimatedItemWidth?: number;
95
- [key: string]: unknown;
96
- };
97
-
98
- /**
99
- * Renders a Gtk.ListView: a scrollable, single-column list backed by a collection
100
- * model, with each row drawn by renderItem and optional section headers.
101
- */
102
- export const ListView = <T = unknown, S = unknown>(props: ListViewProps<T, S>): ReactNode => {
103
- const {
104
- ref,
105
- items,
106
- sections,
107
- renderItem,
108
- renderHeader,
109
- selectedIds,
110
- selectionMode,
111
- onSelectionChanged,
112
- expandedIds,
113
- onExpandedChange,
114
- estimatedItemHeight,
115
- estimatedItemWidth,
116
- ...intrinsicProps
117
- } = props as NormalizedListViewProps<T, S>;
118
-
119
- const wiring = useListViewWiring<T, S>({
120
- ref,
121
- items,
122
- sections,
123
- renderHeader,
124
- selectedIds,
125
- selectionMode,
126
- onSelectionChanged,
127
- expandedIds,
128
- onExpandedChange,
129
- estimatedItemHeight,
130
- estimatedItemWidth,
131
- } as NormalizedListViewProps<T, S>);
132
-
133
- const treeContext: TreeRenderContext = createTreeRenderContext(expandedIds, wiring.rowId);
134
- const cellRenderer: CellRenderer<T, S> = itemRenderer<T, S>(renderItem, treeContext);
26
+ function ListView<T = unknown, S = unknown>(props: ListViewProps<T, S>): ReactNode {
27
+ const { renderItem, renderHeader, expandedIds, estimatedItemHeight, estimatedItemWidth } = props;
28
+ const rest = omit(props, LIST_VIEW_PROPS);
29
+ const size = { width: estimatedItemWidth ?? -1, height: estimatedItemHeight ?? -1 };
30
+ const { collection, selection } = useCollection(props);
31
+ const itemCells = useItemCells(size);
32
+ const headerCells = useHeaderCells(size);
135
33
 
136
34
  return (
137
35
  <>
138
- <GtkListView {...intrinsicProps} ref={wiring.setRef} />
139
- <CellRenderHost store={wiring.itemStore} resolver={wiring.resolver} render={cellRenderer} />
140
- <HeaderRenderHost
141
- useHeader={wiring.useHeader}
142
- store={wiring.headerStore}
143
- resolver={wiring.headerResolver}
144
- renderHeader={renderHeader}
36
+ <GtkListView
37
+ model={selection}
38
+ factory={<GtkSignalListItemFactory {...itemCells.handlers} />}
39
+ {...(renderHeader != null && {
40
+ headerFactory: <GtkSignalListItemFactory {...headerCells.handlers} />,
41
+ })}
42
+ {...rest}
145
43
  />
44
+ <ItemPortals store={itemCells} render={renderItem} collection={collection} expandedIds={expandedIds} />
45
+ {renderHeader != null && (
46
+ <HeaderPortals store={headerCells} render={renderHeader} collection={collection} />
47
+ )}
146
48
  </>
147
49
  );
148
- };
50
+ }
51
+
52
+ export { ListView };
@@ -1,96 +1,77 @@
1
1
  import type * as Gtk from "@gtkx/gi/gtk";
2
+ import type { ElementType, ReactNode, Ref, RefCallback } from "react";
2
3
  import { GtkSizeGroup } from "@gtkx/jsx/gtk";
3
- import { useMergeRefs } from "@gtkx/react/internal";
4
- import {
5
- createContext,
6
- type ElementType,
7
- type ReactNode,
8
- type Ref,
9
- type RefCallback,
10
- useCallback,
11
- useContext,
12
- useRef,
13
- } from "react";
14
- import type { ChildProps } from "./types.js";
15
-
16
- type SizeGroupRegistry = {
17
- addWidget: (widget: Gtk.Widget) => void;
18
- removeWidget: (widget: Gtk.Widget) => void;
19
- setGroup: (group: Gtk.SizeGroup | null) => void;
20
- };
4
+ import { useMergedRef } from "@gtkx/react/internal";
5
+ import { createContext, useCallback, useContext, useState } from "react";
6
+ import type { SizeGroupChildProps, SizeGroupProps } from "./types.js";
7
+
8
+ type Register = RefCallback<Gtk.Widget | null>;
21
9
 
22
- const SizeGroupContext = createContext<SizeGroupRegistry | null>(null);
10
+ type SizeGroupChildRuntimeProps = {
11
+ component: ElementType;
12
+ ref?: Ref<Gtk.Widget | null> | undefined;
13
+ } & Record<string, unknown>;
23
14
 
24
- const useSizeGroupRegistry = (): SizeGroupRegistry => {
25
- const registry = useContext(SizeGroupContext);
26
- if (!registry) throw new Error("<SizeGroup.Child> must be a child of <SizeGroup>");
27
- return registry;
15
+ type SizeGroupComponent = ((props: SizeGroupProps) => ReactNode) & {
16
+ Child: <C extends ElementType>(props: SizeGroupChildProps<C>) => ReactNode;
28
17
  };
29
18
 
30
- /** Props for {@link SizeGroup}. */
31
- export type SizeGroupProps = {
32
- /** How the group equalizes sizes: horizontal, vertical, or both. */
33
- mode?: Gtk.SizeGroupMode | null | undefined;
34
- ref?: Ref<Gtk.SizeGroup | null>;
35
- children?: ReactNode;
19
+ const SizeGroupContext = createContext<Register | null>(null);
20
+ const SizeGroupChild = SizeGroupChildImpl as <C extends ElementType>(props: SizeGroupChildProps<C>) => ReactNode;
21
+ /**
22
+ * Creates a Gtk.SizeGroup that keeps widgets joined through `SizeGroup.Child`
23
+ * at a common size in the given mode, without contributing a widget of its own.
24
+ */
25
+ const SizeGroup: SizeGroupComponent = Object.assign(SizeGroupRoot, { Child: SizeGroupChild });
26
+
27
+ const useRegister = (): Register => {
28
+ const register = useContext(SizeGroupContext);
29
+
30
+ if (register === null) {
31
+ throw new Error("<SizeGroup.Child> must be a child of <SizeGroup>");
32
+ }
33
+
34
+ return register;
36
35
  };
37
36
 
38
- /** Adds a single widget, rendered by the given component, to the enclosing {@link SizeGroup}. */
39
- export type SizeGroupChildProps<C extends ElementType> = ChildProps<C>;
37
+ function SizeGroupChildImpl(props: SizeGroupChildRuntimeProps): ReactNode {
38
+ const { component: Component, ref, ...rest } = props;
39
+ const refCallback = useMergedRef<Gtk.Widget | null>(ref, useRegister());
40
+
41
+ return <Component {...rest} ref={refCallback} />;
42
+ }
43
+
44
+ const addWidget = (widgets: Gtk.Widget[], widget: Gtk.Widget): Gtk.Widget[] =>
45
+ widgets.includes(widget) ? widgets : [...widgets, widget];
46
+
47
+ const removeWidget = (widgets: Gtk.Widget[], widget: Gtk.Widget): Gtk.Widget[] =>
48
+ widgets.filter((entry) => entry !== widget);
49
+
50
+ function SizeGroupRoot(props: SizeGroupProps): ReactNode {
51
+ const { mode, ref, children } = props;
52
+ const [widgets, setWidgets] = useState<Gtk.Widget[]>([]);
40
53
 
41
- const SizeGroupChild = <C extends ElementType>({ component, ref, ...rest }: SizeGroupChildProps<C>): ReactNode => {
42
- const registry = useSizeGroupRegistry();
43
- const joinGroup = useCallback<RefCallback<Gtk.Widget>>(
54
+ const register = useCallback<Register>(
44
55
  (widget) => {
45
- if (widget === null) return;
46
- registry.addWidget(widget);
47
- return () => registry.removeWidget(widget);
56
+ if (widget === null) {
57
+ return;
58
+ }
59
+
60
+ setWidgets((previous) => addWidget(previous, widget));
61
+
62
+ return () => {
63
+ setWidgets((previous) => removeWidget(previous, widget));
64
+ };
48
65
  },
49
- [registry],
66
+ [setWidgets],
50
67
  );
51
- const Component: ElementType = component;
52
- const setWidget = useMergeRefs<Gtk.Widget>(ref, joinGroup);
53
- return <Component {...rest} ref={setWidget} />;
54
- };
55
68
 
56
- const createRegistry = (): SizeGroupRegistry => {
57
- const members = new Set<Gtk.Widget>();
58
- let group: Gtk.SizeGroup | null = null;
59
- return {
60
- addWidget: (widget) => {
61
- members.add(widget);
62
- group?.addWidget(widget);
63
- },
64
- removeWidget: (widget) => {
65
- members.delete(widget);
66
- group?.removeWidget(widget);
67
- },
68
- setGroup: (next) => {
69
- group = next;
70
- if (next) for (const member of members) next.addWidget(member);
71
- },
72
- };
73
- };
69
+ return (
70
+ <>
71
+ <GtkSizeGroup ref={ref} mode={mode} widgets={widgets} />
72
+ <SizeGroupContext.Provider value={register}>{children}</SizeGroupContext.Provider>
73
+ </>
74
+ );
75
+ }
74
76
 
75
- /**
76
- * Creates a Gtk.SizeGroup that keeps its member widgets at a common size. Members join the group
77
- * through {@link SizeGroup.Child}.
78
- */
79
- export const SizeGroup: ((props: SizeGroupProps) => ReactNode) & {
80
- Child: <C extends ElementType>(props: SizeGroupChildProps<C>) => ReactNode;
81
- } = Object.assign(
82
- ({ mode, ref, children }: SizeGroupProps): ReactNode => {
83
- const registryRef = useRef<SizeGroupRegistry | null>(null);
84
- if (registryRef.current === null) registryRef.current = createRegistry();
85
- const registry = registryRef.current;
86
- const setGroup = useCallback<RefCallback<Gtk.SizeGroup>>((group) => registry.setGroup(group), [registry]);
87
- const mergedRef = useMergeRefs<Gtk.SizeGroup>(ref, setGroup);
88
- return (
89
- <>
90
- <GtkSizeGroup ref={mergedRef} mode={mode} />
91
- <SizeGroupContext.Provider value={registry}>{children}</SizeGroupContext.Provider>
92
- </>
93
- );
94
- },
95
- { Child: SizeGroupChild },
96
- );
77
+ export { SizeGroup };