@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/list-view.tsx CHANGED
@@ -1,85 +1,148 @@
1
- import type * as Gio from "@gtkx/gi/gio";
2
1
  import type * as Gtk from "@gtkx/gi/gtk";
3
2
  import { GtkListView, type GtkListViewProps } from "@gtkx/jsx/gtk";
4
3
  import type { ReactNode, Ref } from "react";
5
- import { CollectionView, type ModelBinding } from "./collection-view.js";
6
- import type { SlotRenderer } from "./list-slot.js";
7
- import type { ListViewProps } from "./types.js";
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";
8
24
 
9
- const factoryBinding = {
10
- install: (widget: Gtk.ListView, factory: Gtk.SignalListItemFactory) => widget.setFactory(factory),
11
- uninstall: (widget: Gtk.ListView) => widget.setFactory(null),
12
- };
25
+ const factoryInstaller = makeFactoryInstaller<Gtk.ListView>((widget, factory) => widget.setFactory(factory));
13
26
 
14
- const headerFactoryBinding = {
15
- install: (widget: Gtk.ListView, factory: Gtk.SignalListItemFactory) => widget.setHeaderFactory(factory),
16
- uninstall: (widget: Gtk.ListView) => widget.setHeaderFactory(null),
17
- };
27
+ const headerFactoryInstaller = makeFactoryInstaller<Gtk.ListView>((widget, factory) =>
28
+ widget.setHeaderFactory(factory),
29
+ );
18
30
 
19
- const modelBinding: ModelBinding<Gtk.ListView> = {
20
- install: (widget, model) => widget.setModel(model as Gtk.SelectionModel),
21
- };
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
+ };
22
39
 
23
40
  /**
24
- * Props for the {@link ListView} component: the raw `GtkListView` element
25
- * surface with its factory/model wiring replaced by the declarative
26
- * {@link ListViewProps} API.
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.
27
45
  */
28
- export type ListViewComponentProps<T = unknown, S = unknown> = Omit<GtkListViewProps, keyof ListViewProps<T, S>> &
29
- ListViewProps<T, S>;
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
+ };
30
97
 
31
98
  /**
32
- * A `GtkListView` driven by a declarative `items`/`renderItem` API with
33
- * optional controlled selection, section headers, and tree autoexpansion.
34
- * Supplying an external `model` switches to the uncontrolled form.
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.
35
101
  */
36
- export const ListView = <T = unknown, S = unknown>(props: ListViewComponentProps<T, S>): ReactNode => {
102
+ export const ListView = <T = unknown, S = unknown>(props: ListViewProps<T, S>): ReactNode => {
37
103
  const {
38
104
  ref,
39
105
  items,
40
- model,
106
+ sections,
41
107
  renderItem,
42
- autoexpand,
43
108
  renderHeader,
44
- selected,
109
+ selectedIds,
45
110
  selectionMode,
46
111
  onSelectionChanged,
112
+ expandedIds,
113
+ onExpandedChange,
47
114
  estimatedItemHeight,
48
115
  estimatedItemWidth,
49
116
  ...intrinsicProps
50
- } = props as ListViewProps<T, S> & {
51
- ref?: Ref<Gtk.ListView | null>;
52
- estimatedItemHeight?: number;
53
- estimatedItemWidth?: number;
54
- [key: string]: unknown;
55
- };
117
+ } = props as NormalizedListViewProps<T, S>;
56
118
 
57
- const renderItemFn = renderItem as (item: T, row?: Gtk.TreeListRow | null) => ReactNode;
58
- const slotRenderer: SlotRenderer<T, S> = (value, treeRow, isHeader) => {
59
- if (isHeader) return null;
60
- return treeRow === null ? renderItemFn(value as T) : renderItemFn(value as T, treeRow);
61
- };
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>);
62
132
 
63
- const useHeader = model === undefined && typeof renderHeader === "function";
133
+ const treeContext: TreeRenderContext = createTreeRenderContext(expandedIds, wiring.rowId);
134
+ const cellRenderer: CellRenderer<T, S> = itemRenderer<T, S>(renderItem, treeContext);
64
135
 
65
136
  return (
66
- <CollectionView<T, S, Gtk.ListView>
67
- element={GtkListView}
68
- intrinsicProps={intrinsicProps}
69
- ref={ref}
70
- items={model === undefined ? items : undefined}
71
- model={model as Gio.ListModel | undefined}
72
- renderItem={slotRenderer}
73
- autoexpand={autoexpand}
74
- renderHeader={useHeader ? (value) => renderHeader?.(value as S) ?? null : undefined}
75
- estimatedHeight={estimatedItemHeight}
76
- estimatedWidth={estimatedItemWidth}
77
- selected={selected}
78
- selectionMode={selectionMode}
79
- onSelectionChanged={onSelectionChanged}
80
- factoryBinding={factoryBinding}
81
- headerFactoryBinding={useHeader ? headerFactoryBinding : undefined}
82
- modelBinding={modelBinding}
83
- />
137
+ <>
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}
145
+ />
146
+ </>
84
147
  );
85
148
  };
package/src/menu.tsx CHANGED
@@ -1,24 +1,22 @@
1
1
  import type * as Gio from "@gtkx/gi/gio";
2
2
  import { GMenu, type GMenuProps } from "@gtkx/jsx/gio";
3
- import { useForwardedRef } from "@gtkx/react";
4
- import { createElement, type ReactNode, type Ref, useLayoutEffect, useRef } from "react";
3
+ import { useMergeRefs } from "@gtkx/react/internal";
4
+ import { type ReactNode, type Ref, useLayoutEffect, useRef } from "react";
5
5
  import { applyMenuItems, menuItemsEqual } from "./menu-items.js";
6
- import type { MenuEntry, MenuItemsProps } from "./types.js";
6
+ import type { MenuEntry } from "./types.js";
7
7
 
8
- /**
9
- * Props for the {@link Menu} component: the raw `GMenu` element surface with a
10
- * declarative `items` model layered on top.
11
- */
8
+ export type MenuItemsProps = {
9
+ items?: MenuEntry[] | null | undefined;
10
+ };
11
+
12
+ /** Props for {@link Menu}, combining Gio.Menu props with a declarative items array. */
12
13
  export type MenuProps = Omit<GMenuProps, keyof MenuItemsProps> & MenuItemsProps & { ref?: Ref<Gio.Menu | null> };
13
14
 
14
- /**
15
- * A declarative `Gio.Menu` built from an `items` tree of {@link MenuEntry}
16
- * values. Rebuilds the underlying menu model whenever the item structure
17
- * changes.
18
- */
15
+ /** Builds a Gio.Menu model from a declarative array of {@link MenuEntry} items. */
19
16
  export const Menu = (props: MenuProps): ReactNode => {
20
17
  const { items, ref, ...rest } = props;
21
- const [menuRef, mergedRef] = useForwardedRef(ref);
18
+ const menuRef = useRef<Gio.Menu | null>(null);
19
+ const mergedRef = useMergeRefs(ref, menuRef);
22
20
  const appliedRef = useRef<MenuEntry[] | null>(null);
23
21
 
24
22
  useLayoutEffect(() => {
@@ -30,5 +28,5 @@ export const Menu = (props: MenuProps): ReactNode => {
30
28
  applyMenuItems(menu, entries);
31
29
  });
32
30
 
33
- return createElement(GMenu, { ...rest, ref: mergedRef });
31
+ return <GMenu {...rest} ref={mergedRef} />;
34
32
  };
@@ -0,0 +1,88 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ import { GtkOverlay, type GtkOverlayProps } from "@gtkx/jsx/gtk";
3
+ import { useMergeRefs } from "@gtkx/react/internal";
4
+ import { Children, type ElementType, isValidElement, 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: OverlayContext, useParentRef: useOverlayRef } = createParentContext<Gtk.Overlay>(
9
+ "<Overlay.Child> must be a child of <Overlay>",
10
+ );
11
+
12
+ /** Props for {@link Overlay}. */
13
+ export type OverlayProps = GtkOverlayProps & { ref?: Ref<Gtk.Overlay | null> };
14
+
15
+ export type OverlayPlacementProps = {
16
+ /** Whether this overlay contributes to the Overlay's measured size. */
17
+ measure?: boolean | null | undefined;
18
+ /** Whether the overlay is clipped to the main child's allocation. */
19
+ clipOverlay?: boolean | null | undefined;
20
+ };
21
+
22
+ /** Adds a single widget as an overlay on top of an {@link Overlay}'s main child. */
23
+ export type OverlayChildProps<C extends ElementType> = ChildProps<C, OverlayPlacementProps>;
24
+
25
+ type OverlayPlacement = { measure: boolean; clipOverlay: boolean };
26
+
27
+ const placementOf = (props: OverlayPlacementProps): OverlayPlacement => ({
28
+ measure: props.measure ?? false,
29
+ clipOverlay: props.clipOverlay ?? false,
30
+ });
31
+
32
+ const samePlacement = (a: OverlayPlacement, b: OverlayPlacement): boolean =>
33
+ a.measure === b.measure && a.clipOverlay === b.clipOverlay;
34
+
35
+ const OverlayChild = <C extends ElementType>({
36
+ component,
37
+ measure,
38
+ clipOverlay,
39
+ ref,
40
+ ...rest
41
+ }: OverlayChildProps<C>): ReactNode => {
42
+ const overlayRef = useOverlayRef();
43
+ const Component: ElementType = component;
44
+ return usePlacedChild<Gtk.Widget, OverlayPlacement>({
45
+ render: (placeRef) => <Component {...rest} ref={placeRef} />,
46
+ ref,
47
+ placement: placementOf({ measure, clipOverlay }),
48
+ samePlacement,
49
+ place: (widget, placement) => {
50
+ const overlay = overlayRef.current;
51
+ if (!overlay) return;
52
+ if (widget.getParent() !== overlay) overlay.addOverlay(widget);
53
+ overlay.setMeasureOverlay(widget, placement.measure);
54
+ overlay.setClipOverlay(widget, placement.clipOverlay);
55
+ },
56
+ release: (widget) => {
57
+ const overlay = overlayRef.current;
58
+ if (overlay && widget.getParent() === overlay) overlay.removeOverlay(widget);
59
+ },
60
+ });
61
+ };
62
+
63
+ const isOverlayChild = (node: ReactNode): boolean => isValidElement(node) && node.type === OverlayChild;
64
+
65
+ /**
66
+ * Renders a Gtk.Overlay: a main child with one or more widgets stacked on top,
67
+ * declared via {@link Overlay.Child}. Non-Child children form the main content.
68
+ */
69
+ export const Overlay: ((props: OverlayProps) => ReactNode) & {
70
+ Child: <C extends ElementType>(props: OverlayChildProps<C>) => ReactNode;
71
+ } = Object.assign(
72
+ ({ children, ref, ...rest }: OverlayProps): ReactNode => {
73
+ const overlayRef = useRef<Gtk.Overlay | null>(null);
74
+ const mergedRef = useMergeRefs<Gtk.Overlay>(ref, overlayRef);
75
+ const items = Children.toArray(children);
76
+ const overlays = items.filter(isOverlayChild);
77
+ const base = items.filter((node) => !isOverlayChild(node));
78
+ return (
79
+ <>
80
+ <GtkOverlay {...rest} ref={mergedRef}>
81
+ {base}
82
+ </GtkOverlay>
83
+ <OverlayContext.Provider value={overlayRef}>{overlays}</OverlayContext.Provider>
84
+ </>
85
+ );
86
+ },
87
+ { Child: OverlayChild },
88
+ );
@@ -0,0 +1,96 @@
1
+ import type * as Gtk from "@gtkx/gi/gtk";
2
+ 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
+ };
21
+
22
+ const SizeGroupContext = createContext<SizeGroupRegistry | null>(null);
23
+
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;
28
+ };
29
+
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;
36
+ };
37
+
38
+ /** Adds a single widget, rendered by the given component, to the enclosing {@link SizeGroup}. */
39
+ export type SizeGroupChildProps<C extends ElementType> = ChildProps<C>;
40
+
41
+ const SizeGroupChild = <C extends ElementType>({ component, ref, ...rest }: SizeGroupChildProps<C>): ReactNode => {
42
+ const registry = useSizeGroupRegistry();
43
+ const joinGroup = useCallback<RefCallback<Gtk.Widget>>(
44
+ (widget) => {
45
+ if (widget === null) return;
46
+ registry.addWidget(widget);
47
+ return () => registry.removeWidget(widget);
48
+ },
49
+ [registry],
50
+ );
51
+ const Component: ElementType = component;
52
+ const setWidget = useMergeRefs<Gtk.Widget>(ref, joinGroup);
53
+ return <Component {...rest} ref={setWidget} />;
54
+ };
55
+
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
+ };
74
+
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
+ );