@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/grid.tsx ADDED
@@ -0,0 +1,88 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { GtkGrid, type GtkGridProps } from "@gtkx/jsx/gtk";
3
+ import { useMergeRefs } from "@gtkx/react/internal";
4
+ import { Children, type ElementType, type ReactNode, type Ref, useRef } from "react";
5
+ import { createParentContext, usePlacedChild } from "./hooks/use-placed-child.js";
6
+ import type { ChildProps } from "./types.js";
7
+
8
+ const { Context: GridContext, useParentRef: useGridRef } = createParentContext<Gtk.Grid>(
9
+ "<Grid.Child> must be a child of <Grid>",
10
+ );
11
+
12
+ /** Props for {@link Grid}. */
13
+ export type GridProps = GtkGridProps & { ref?: Ref<Gtk.Grid | null>; children?: ReactNode };
14
+
15
+ export type GridPlacement = {
16
+ column?: number | null | undefined;
17
+ row?: number | null | undefined;
18
+ /** Number of columns the child spans (defaults to 1). */
19
+ columnSpan?: number | null | undefined;
20
+ /** Number of rows the child spans (defaults to 1). */
21
+ rowSpan?: number | null | undefined;
22
+ };
23
+
24
+ /** Places a single child inside a {@link Grid} at a column and row, optionally spanning multiple cells. */
25
+ export type GridChildProps<C extends ElementType> = ChildProps<C, GridPlacement>;
26
+
27
+ type Placement = { column: number; row: number; columnSpan: number; rowSpan: number };
28
+
29
+ const placementOf = (props: GridPlacement): Placement => ({
30
+ column: props.column ?? 0,
31
+ row: props.row ?? 0,
32
+ columnSpan: props.columnSpan ?? 1,
33
+ rowSpan: props.rowSpan ?? 1,
34
+ });
35
+
36
+ const samePlacement = (a: Placement, b: Placement): boolean =>
37
+ a.column === b.column && a.row === b.row && a.columnSpan === b.columnSpan && a.rowSpan === b.rowSpan;
38
+
39
+ const GridChild = <C extends ElementType>({
40
+ component,
41
+ column,
42
+ row,
43
+ columnSpan,
44
+ rowSpan,
45
+ ref,
46
+ ...rest
47
+ }: GridChildProps<C>): ReactNode => {
48
+ const gridRef = useGridRef();
49
+ const Component: ElementType = component;
50
+ return usePlacedChild<Gtk.Widget, Placement>({
51
+ render: (placeRef) => <Component {...rest} ref={placeRef} />,
52
+ ref,
53
+ placement: placementOf({ column, row, columnSpan, rowSpan }),
54
+ samePlacement,
55
+ place: (widget, placement, previous) => {
56
+ const grid = gridRef.current;
57
+ if (!grid) return;
58
+ if (previous !== undefined && widget.getParent() === grid) grid.remove(widget);
59
+ if (widget.getParent() !== grid) {
60
+ grid.attach(widget, placement.column, placement.row, placement.columnSpan, placement.rowSpan);
61
+ }
62
+ },
63
+ release: (widget) => {
64
+ const grid = gridRef.current;
65
+ if (grid && widget.getParent() === grid) grid.remove(widget);
66
+ },
67
+ });
68
+ };
69
+
70
+ /**
71
+ * Renders a Gtk.Grid whose children are attached at explicit column/row positions via
72
+ * {@link Grid.Child}.
73
+ */
74
+ export const Grid: ((props: GridProps) => ReactNode) & {
75
+ Child: <C extends ElementType>(props: GridChildProps<C>) => ReactNode;
76
+ } = Object.assign(
77
+ ({ children, ref, ...rest }: GridProps): ReactNode => {
78
+ const gridRef = useRef<Gtk.Grid | null>(null);
79
+ const mergedRef = useMergeRefs<Gtk.Grid>(ref, gridRef);
80
+ return (
81
+ <>
82
+ <GtkGrid {...rest} ref={mergedRef} />
83
+ <GridContext.Provider value={gridRef}>{Children.toArray(children)}</GridContext.Provider>
84
+ </>
85
+ );
86
+ },
87
+ { Child: GridChild },
88
+ );
@@ -1,14 +1,14 @@
1
1
  import type * as GObject from "@gtkx/gi/gobject";
2
2
  import * as Gtk from "@gtkx/gi/gtk";
3
- import { type GObjectTarget, useTargetRegistration } from "@gtkx/react/internal";
3
+ import { type ObjectProp, useObjectAttachment } from "@gtkx/react/internal";
4
4
  import { useRef } from "react";
5
- import { RealizedSlotStore } from "../utils/realized-slot-store.js";
5
+ import { CellContainerStore } from "../utils/cell-container-store.js";
6
6
 
7
- interface ChildContainer {
7
+ type ChildContainer = {
8
8
  getChild(): Gtk.Widget | null;
9
9
  setChild(child: Gtk.Widget | null): void;
10
10
  getItem(): GObject.Object | null;
11
- }
11
+ };
12
12
 
13
13
  const isChildContainer = (container: GObject.Object): container is GObject.Object & ChildContainer =>
14
14
  container instanceof Gtk.ListItem || container instanceof Gtk.ListHeader;
@@ -19,36 +19,38 @@ const positionOf = (container: GObject.Object & ChildContainer): number => {
19
19
  return -1;
20
20
  };
21
21
 
22
- export interface FactoryBinding<W extends GObject.Object> {
22
+ export type FactoryInstaller<W extends GObject.Object> = {
23
23
  install(widget: W, factory: Gtk.SignalListItemFactory): void;
24
- uninstall(widget: W, factory: Gtk.SignalListItemFactory): void;
25
- }
24
+ uninstall(widget: W): void;
25
+ };
26
+
27
+ export const makeFactoryInstaller = <W extends GObject.Object>(
28
+ setFactory: (widget: W, factory: Gtk.SignalListItemFactory | null) => void,
29
+ ): FactoryInstaller<W> => ({
30
+ install: (widget, factory) => setFactory(widget, factory),
31
+ uninstall: (widget) => setFactory(widget, null),
32
+ });
26
33
 
27
- export interface RealizedSlotsOptions<W extends GObject.Object> {
28
- target: GObjectTarget<W>;
29
- binding: FactoryBinding<W>;
34
+ type CellContainersOptions<W extends GObject.Object> = {
35
+ object: ObjectProp<W>;
36
+ installer: FactoryInstaller<W>;
30
37
  estimatedHeight?: number | undefined;
31
38
  estimatedWidth?: number | undefined;
32
- }
33
-
34
- export interface RealizedSlots {
35
- factory: Gtk.SignalListItemFactory;
36
- store: RealizedSlotStore;
37
- }
39
+ };
38
40
 
39
41
  const applyEstimatedSize = (child: Gtk.Widget, height: number | undefined, width: number | undefined): void => {
40
42
  if (height === undefined && width === undefined) return;
41
43
  child.setSizeRequest(width ?? -1, height ?? -1);
42
44
  };
43
45
 
44
- export const useRealizedSlots = <W extends GObject.Object>(options: RealizedSlotsOptions<W>): RealizedSlots => {
45
- const { target, binding, estimatedHeight, estimatedWidth } = options;
46
- const storeRef = useRef<RealizedSlotStore | null>(null);
47
- if (storeRef.current === null) storeRef.current = new RealizedSlotStore();
46
+ export const useCellContainers = <W extends GObject.Object>(options: CellContainersOptions<W>): CellContainerStore => {
47
+ const { object, installer, estimatedHeight, estimatedWidth } = options;
48
+ const storeRef = useRef<CellContainerStore | null>(null);
49
+ if (storeRef.current === null) storeRef.current = new CellContainerStore();
48
50
  const store = storeRef.current;
49
51
 
50
- const bindingRef = useRef(binding);
51
- bindingRef.current = binding;
52
+ const installerRef = useRef(installer);
53
+ installerRef.current = installer;
52
54
 
53
55
  const factoryRef = useRef<Gtk.SignalListItemFactory | null>(null);
54
56
  if (factoryRef.current === null) {
@@ -65,9 +67,7 @@ export const useRealizedSlots = <W extends GObject.Object>(options: RealizedSlot
65
67
  if (isChildContainer(container)) {
66
68
  const item = container.getItem();
67
69
  const treeRow = item instanceof Gtk.TreeListRow ? item : null;
68
- store.bind(container, positionOf(container), treeRow, item);
69
- } else {
70
- store.bind(container, -1, null, null);
70
+ store.bind(container, positionOf(container), treeRow);
71
71
  }
72
72
  });
73
73
  factory.on("unbind", (container) => {
@@ -80,14 +80,14 @@ export const useRealizedSlots = <W extends GObject.Object>(options: RealizedSlot
80
80
  }
81
81
  const factory = factoryRef.current;
82
82
 
83
- useTargetRegistration<W, { widget: W }>(target, {
83
+ useObjectAttachment<W, { widget: W }>(object, {
84
84
  attach: (widget) => {
85
- bindingRef.current.install(widget, factory);
85
+ installerRef.current.install(widget, factory);
86
86
  return { widget };
87
87
  },
88
- detach: (registration) => bindingRef.current.uninstall(registration.widget, factory),
89
- isSame: (registration, widget) => registration.widget === widget,
88
+ detach: (attachment) => installerRef.current.uninstall(attachment.widget),
89
+ isSame: (attachment, widget) => attachment.widget === widget,
90
90
  });
91
91
 
92
- return { factory, store };
92
+ return store;
93
93
  };
@@ -0,0 +1,60 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { type ReactNode, useCallback } from "react";
3
+ import type { ItemNode, SectionNode } from "../types.js";
4
+ import { type ItemResolver, rowIdOf } from "../utils/item-resolver.js";
5
+ import { useExpansionModel } from "./use-expansion-model.js";
6
+ import { useListModel } from "./use-list-model.js";
7
+ import { useSelectionModel } from "./use-selection-model.js";
8
+
9
+ type CollectionModelInput<T, S> = {
10
+ items: ItemNode<T>[] | undefined;
11
+ sections: SectionNode<S, T>[] | undefined;
12
+ selectionMode: Gtk.SelectionMode | null | undefined;
13
+ selectedIds: string[] | null | undefined;
14
+ onSelectionChanged: ((ids: string[]) => void) | null | undefined;
15
+ expandedIds?: string[] | null | undefined;
16
+ onExpandedChange?: ((ids: string[]) => void) | null | undefined;
17
+ renderHeader: ((info: { section: S }) => ReactNode) | null | undefined;
18
+ };
19
+
20
+ export type CollectionModelResult<T, S> = {
21
+ resolver: ItemResolver<T, S>;
22
+ headerResolver: ItemResolver<T, S>;
23
+ installedModel: Gtk.SelectionModel;
24
+ useHeader: boolean;
25
+ rowId: (row: Gtk.TreeListRow) => string | undefined;
26
+ };
27
+
28
+ export const useCollectionModel = <T, S>(input: CollectionModelInput<T, S>): CollectionModelResult<T, S> => {
29
+ const listModel = useListModel<T, S>({
30
+ items: input.items,
31
+ sections: input.sections,
32
+ });
33
+
34
+ useExpansionModel<T>({
35
+ treeModel: listModel.treeModel,
36
+ rowValues: listModel.rowValues,
37
+ expandedIds: input.expandedIds,
38
+ onExpandedChange: input.onExpandedChange,
39
+ });
40
+
41
+ const installedModel = useSelectionModel<T, S>({
42
+ base: listModel.model,
43
+ resolver: listModel.resolver,
44
+ selectionMode: input.selectionMode,
45
+ selectedIds: input.selectedIds,
46
+ onSelectionChanged: input.onSelectionChanged,
47
+ });
48
+
49
+ const useHeader = typeof input.renderHeader === "function";
50
+ const { rowValues } = listModel;
51
+ const rowId = useCallback((row: Gtk.TreeListRow): string | undefined => rowIdOf(rowValues, row), [rowValues]);
52
+
53
+ return {
54
+ resolver: listModel.resolver,
55
+ headerResolver: listModel.headerResolver,
56
+ installedModel,
57
+ useHeader,
58
+ rowId,
59
+ };
60
+ };
@@ -0,0 +1,47 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { useMergeRefs } from "@gtkx/react/internal";
3
+ import { type ReactNode, type Ref, type RefCallback, type RefObject, useRef } from "react";
4
+ import type { ItemNode, SectionNode } from "../types.js";
5
+ import { type CollectionModelResult, useCollectionModel } from "./use-collection-model.js";
6
+ import { useInstalledModel } from "./use-installed-model.js";
7
+
8
+ type CollectionWidget = Gtk.Widget & {
9
+ setModel(model: Gtk.SelectionModel | null): void;
10
+ };
11
+
12
+ type CollectionWidgetInput<W extends CollectionWidget, T, S> = {
13
+ ref?: Ref<W | null> | undefined;
14
+ items?: ItemNode<T>[] | undefined;
15
+ sections?: SectionNode<S, T>[] | undefined;
16
+ selectionMode?: Gtk.SelectionMode | null | undefined;
17
+ selectedIds?: string[] | null | undefined;
18
+ onSelectionChanged?: ((ids: string[]) => void) | null | undefined;
19
+ expandedIds?: string[] | null | undefined;
20
+ onExpandedChange?: ((ids: string[]) => void) | null | undefined;
21
+ renderHeader?: ((info: { section: S }) => ReactNode) | null | undefined;
22
+ };
23
+
24
+ type CollectionWidgetResult<W extends CollectionWidget, T, S> = {
25
+ widgetRef: RefObject<W | null>;
26
+ setRef: RefCallback<W>;
27
+ collection: CollectionModelResult<T, S>;
28
+ };
29
+
30
+ export const useCollectionWidget = <W extends CollectionWidget, T, S>(
31
+ props: CollectionWidgetInput<W, T, S>,
32
+ ): CollectionWidgetResult<W, T, S> => {
33
+ const widgetRef = useRef<W | null>(null);
34
+ const setRef = useMergeRefs<W>(props.ref, widgetRef);
35
+ const collection = useCollectionModel<T, S>({
36
+ items: props.items,
37
+ sections: props.sections,
38
+ selectionMode: props.selectionMode,
39
+ selectedIds: props.selectedIds,
40
+ onSelectionChanged: props.onSelectionChanged,
41
+ expandedIds: props.expandedIds,
42
+ onExpandedChange: props.onExpandedChange,
43
+ renderHeader: props.renderHeader,
44
+ });
45
+ useInstalledModel(widgetRef, collection.installedModel, (widget, model) => widget.setModel(model));
46
+ return { widgetRef, setRef, collection };
47
+ };
@@ -1,32 +1,31 @@
1
1
  import type * as GObject from "@gtkx/gi/gobject";
2
+ import * as Gtk from "@gtkx/gi/gtk";
2
3
  import { useSignal } from "@gtkx/react";
3
- import { type GObjectTarget, resolveGobjectTarget, useGObjectSnapshot } from "@gtkx/react/internal";
4
+ import { type ObjectProp, resolveObjectProp, useObjectValue } from "@gtkx/react/internal";
4
5
  import { useLayoutEffect, useRef } from "react";
5
6
  import type { ItemResolver } from "../utils/item-resolver.js";
6
7
 
7
- export interface DropDownSelectionTarget extends GObject.Object {
8
+ type DropDownSelectionObject = GObject.Object & {
8
9
  getSelected(): number;
9
10
  setSelected(position: number): void;
10
- }
11
+ };
11
12
 
12
- export interface DropDownSelectionOptions<T, S> {
13
- widget: GObjectTarget<DropDownSelectionTarget>;
13
+ type DropDownSelectionOptions<T, S> = {
14
+ widget: ObjectProp<DropDownSelectionObject>;
14
15
  resolver: ItemResolver<T, S>;
15
16
  selectedId: string | null | undefined;
16
17
  onSelectionChanged: ((id: string) => void) | null | undefined;
17
- }
18
-
19
- const GTK_INVALID_LIST_POSITION = 4294967295;
18
+ };
20
19
 
21
20
  const normalizeSelected = (position: number): number =>
22
- position === GTK_INVALID_LIST_POSITION || position < 0 ? -1 : position;
21
+ position === Gtk.INVALID_LIST_POSITION || position < 0 ? -1 : position;
23
22
 
24
23
  export const useDropDownSelection = <T, S>(options: DropDownSelectionOptions<T, S>): number => {
25
24
  const { widget, resolver, selectedId, onSelectionChanged } = options;
26
- const resolved = resolveGobjectTarget(widget);
25
+ const resolved = resolveObjectProp(widget);
27
26
 
28
- const selectedPosition = useGObjectSnapshot(widget, "notify::selected", (target) =>
29
- target === null ? -1 : normalizeSelected(target.getSelected()),
27
+ const selectedPosition = useObjectValue(widget, "notify::selected", (object) =>
28
+ object === null ? -1 : normalizeSelected(object.getSelected()),
30
29
  );
31
30
 
32
31
  const resolverRef = useRef(resolver);
@@ -36,7 +35,7 @@ export const useDropDownSelection = <T, S>(options: DropDownSelectionOptions<T,
36
35
 
37
36
  useLayoutEffect(() => {
38
37
  if (resolved === null || selectedId === undefined || selectedId === null) return;
39
- const position = resolverRef.current.positionOf(selectedId);
38
+ const position = resolverRef.current.positionOfId(selectedId);
40
39
  if (position < 0) return;
41
40
  if (normalizeSelected(resolved.getSelected()) !== position) resolved.setSelected(position);
42
41
  }, [resolved, selectedId, resolver]);
@@ -0,0 +1,82 @@
1
+ import type * as GObject from "@gtkx/gi/gobject";
2
+ import type * as Gtk from "@gtkx/gi/gtk";
3
+ import { useSignal } from "@gtkx/react";
4
+ import { isSameArray } from "@gtkx/utils";
5
+ import { useLayoutEffect, useRef } from "react";
6
+ import { type RowValue, rowIdOf } from "../utils/item-resolver.js";
7
+
8
+ type ExpansionModelOptions<T> = {
9
+ treeModel: Gtk.TreeListModel | null;
10
+ rowValues: WeakMap<GObject.Object, RowValue<T>>;
11
+ expandedIds: string[] | null | undefined;
12
+ onExpandedChange: ((ids: string[]) => void) | null | undefined;
13
+ };
14
+
15
+ const readExpandedIds = <T>(model: Gtk.TreeListModel, rowValues: WeakMap<GObject.Object, RowValue<T>>): string[] => {
16
+ const ids: string[] = [];
17
+ const count = model.getNItems();
18
+ for (let position = 0; position < count; position++) {
19
+ const row = model.getRow(position);
20
+ if (row?.getExpanded()) {
21
+ const id = rowIdOf(rowValues, row);
22
+ if (id !== undefined) ids.push(id);
23
+ }
24
+ }
25
+ return ids;
26
+ };
27
+
28
+ const applyExpanded = <T>(
29
+ model: Gtk.TreeListModel,
30
+ rowValues: WeakMap<GObject.Object, RowValue<T>>,
31
+ ids: string[],
32
+ ): void => {
33
+ const target = new Set(ids);
34
+ let changed = true;
35
+ while (changed) {
36
+ changed = false;
37
+ const count = model.getNItems();
38
+ for (let position = 0; position < count; position++) {
39
+ const row = model.getRow(position);
40
+ if (row === null || !row.isExpandable()) continue;
41
+ const id = rowIdOf(rowValues, row);
42
+ if (id === undefined) continue;
43
+ const desired = target.has(id);
44
+ if (row.getExpanded() !== desired) {
45
+ row.setExpanded(desired);
46
+ changed = true;
47
+ }
48
+ }
49
+ }
50
+ };
51
+
52
+ export const useExpansionModel = <T>(options: ExpansionModelOptions<T>): void => {
53
+ const { treeModel, rowValues, expandedIds, onExpandedChange } = options;
54
+ const onChangeRef = useRef(onExpandedChange);
55
+ onChangeRef.current = onExpandedChange;
56
+ const rowValuesRef = useRef(rowValues);
57
+ rowValuesRef.current = rowValues;
58
+ const lastReportedRef = useRef<string[] | null>(null);
59
+ const applyingRef = useRef(false);
60
+
61
+ const report = (): void => {
62
+ if (treeModel === null || applyingRef.current) return;
63
+ const callback = onChangeRef.current;
64
+ if (!callback) return;
65
+ const ids = readExpandedIds(treeModel, rowValuesRef.current);
66
+ if (lastReportedRef.current !== null && isSameArray(lastReportedRef.current, ids)) return;
67
+ lastReportedRef.current = ids;
68
+ callback(ids);
69
+ };
70
+
71
+ useSignal(treeModel, "items-changed", report);
72
+
73
+ useLayoutEffect(() => {
74
+ if (treeModel === null) return;
75
+ if (expandedIds !== undefined && expandedIds !== null) {
76
+ applyingRef.current = true;
77
+ applyExpanded(treeModel, rowValuesRef.current, expandedIds);
78
+ applyingRef.current = false;
79
+ }
80
+ report();
81
+ }, [treeModel, expandedIds]);
82
+ };
@@ -1,33 +1,34 @@
1
- import * as Gio from "@gtkx/gi/gio";
2
- import * as GObject from "@gtkx/gi/gobject";
1
+ import type * as Gio from "@gtkx/gi/gio";
3
2
  import type * as Gtk from "@gtkx/gi/gtk";
4
- import { createPortal } from "@gtkx/react";
5
- import { stateOf } from "@gtkx/react/internal";
6
- import { type ReactNode, useLayoutEffect, useRef } from "react";
3
+ import { createPortal, rootElement } from "@gtkx/react";
4
+ import { useMergeRefs } from "@gtkx/react/internal";
5
+ import {
6
+ cloneElement,
7
+ isValidElement,
8
+ type ReactElement,
9
+ type ReactNode,
10
+ type Ref,
11
+ type RefCallback,
12
+ useCallback,
13
+ useLayoutEffect,
14
+ useRef,
15
+ } from "react";
7
16
 
8
- const findMenuModel = (container: GObject.Object): Gio.MenuModel | null => {
9
- for (const child of stateOf(container).children) {
10
- if (child instanceof Gio.MenuModel) return child;
11
- }
12
- return null;
13
- };
17
+ type MenuElement = ReactElement<{ ref?: Ref<Gio.MenuModel | null> }>;
14
18
 
15
19
  export const useHeaderMenu = (column: Gtk.ColumnViewColumn | null, headerMenu: ReactNode): ReactNode => {
16
- const containerRef = useRef<GObject.Object | null>(null);
17
- if (containerRef.current === null) {
18
- containerRef.current = new GObject.Object();
19
- }
20
- const container = containerRef.current;
20
+ const modelRef = useRef<Gio.MenuModel | null>(null);
21
+ const setModel = useCallback<RefCallback<Gio.MenuModel>>((model) => {
22
+ modelRef.current = model;
23
+ }, []);
24
+ const element = isValidElement(headerMenu) ? (headerMenu as MenuElement) : null;
25
+ const mergedRef = useMergeRefs<Gio.MenuModel>(setModel, element?.props.ref);
21
26
 
22
27
  useLayoutEffect(() => {
23
28
  if (column === null) return;
24
- if (headerMenu === undefined || headerMenu === null) {
25
- column.setHeaderMenu(null);
26
- return;
27
- }
28
- column.setHeaderMenu(findMenuModel(container));
29
+ column.setHeaderMenu(headerMenu == null ? null : modelRef.current);
29
30
  });
30
31
 
31
- if (headerMenu === undefined || headerMenu === null) return null;
32
- return createPortal(headerMenu, container, "header-menu");
32
+ if (element === null) return null;
33
+ return createPortal(cloneElement(element, { ref: mergedRef }), rootElement);
33
34
  };
@@ -0,0 +1,20 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { useObjectAttachment } from "@gtkx/react/internal";
3
+ import { type RefObject, useRef } from "react";
4
+
5
+ export const useInstalledModel = <W extends Gtk.Widget, M>(
6
+ object: RefObject<W | null>,
7
+ model: M,
8
+ install: (widget: W, model: M) => void,
9
+ ): void => {
10
+ const installRef = useRef(install);
11
+ installRef.current = install;
12
+ useObjectAttachment<W, { widget: W; model: M }>(object, {
13
+ attach: (widget) => {
14
+ installRef.current(widget, model);
15
+ return { widget, model };
16
+ },
17
+ detach: () => {},
18
+ isSame: (attachment, widget) => attachment.widget === widget && attachment.model === model,
19
+ });
20
+ };