@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
package/src/grid-view.tsx CHANGED
@@ -1,81 +1,37 @@
1
- import type * as Gtk from "@gtkx/gi/gtk";
2
- import { GtkGridView, type GtkGridViewProps } from "@gtkx/jsx/gtk";
3
- import { useMergeRefs } from "@gtkx/react/internal";
4
- import { type ReactNode, type Ref, useRef } from "react";
5
- import { type CellRenderer, CellRenderHost, itemRenderer } from "./cell.js";
6
- import { makeFactoryInstaller, useCellContainers } from "./hooks/use-cell-containers.js";
7
- import { useCollectionModel } from "./hooks/use-collection-model.js";
8
- import { useInstalledModel } from "./hooks/use-installed-model.js";
9
- import type { CollectionItemSizeProps, ControlledSelectionProps, ItemNode, RenderItemProps } from "./types.js";
10
-
11
- const factoryInstaller = makeFactoryInstaller<Gtk.GridView>((widget, factory) => widget.setFactory(factory));
12
-
13
- export type GridViewDeclarativeProps<T = unknown> = CollectionItemSizeProps &
14
- ControlledSelectionProps & {
15
- items?: ItemNode<T>[] | undefined;
16
- renderItem: (props: RenderItemProps<T>) => ReactNode;
17
- };
18
-
19
- /**
20
- * Props for {@link GridView}. Combines the underlying Gtk.GridView props with
21
- * declarative collection props: items, a per-cell renderItem, controlled selection,
22
- * and estimated item sizing.
23
- */
24
- export type GridViewProps<T = unknown> = Omit<
25
- GtkGridViewProps,
26
- "model" | "factory" | keyof GridViewDeclarativeProps<T>
27
- > &
28
- GridViewDeclarativeProps<T>;
1
+ import type { ReactNode } from "react";
2
+ import { GtkGridView, GtkSignalListItemFactory } from "@gtkx/jsx/gtk";
3
+ import { omit } from "@gtkx/utils";
4
+ import type { GridViewProps } from "./types.js";
5
+ import { ItemPortals, useItemCells } from "./internal/cells.js";
6
+ import { useCollection } from "./internal/use-collection.js";
7
+
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)[];
29
17
 
30
18
  /**
31
- * Renders a Gtk.GridView: a scrollable grid of uniformly sized cells backed by a
32
- * collection model, with each cell drawn by renderItem.
19
+ * Renders a Gtk.GridView of uniform cells from declarative items, with per-cell
20
+ * rendering, controlled selection, and estimated item sizing.
33
21
  */
34
- export const GridView = <T = unknown>(props: GridViewProps<T>): ReactNode => {
35
- const {
36
- ref,
37
- items,
38
- renderItem,
39
- selectedIds,
40
- selectionMode,
41
- onSelectionChanged,
42
- estimatedItemHeight,
43
- estimatedItemWidth,
44
- ...intrinsicProps
45
- } = props as GridViewDeclarativeProps<T> & {
46
- ref?: Ref<Gtk.GridView | null>;
47
- estimatedItemHeight?: number;
48
- estimatedItemWidth?: number;
49
- [key: string]: unknown;
50
- };
51
-
52
- const cellRenderer: CellRenderer<T, unknown> = itemRenderer<T, unknown>(renderItem);
53
-
54
- const widgetRef = useRef<Gtk.GridView | null>(null);
55
- const setRef = useMergeRefs<Gtk.GridView>(ref, widgetRef);
56
-
57
- const collection = useCollectionModel<T, unknown>({
58
- items,
59
- sections: undefined,
60
- selectionMode,
61
- selectedIds,
62
- onSelectionChanged,
63
- renderHeader: undefined,
64
- });
65
-
66
- const itemStore = useCellContainers<Gtk.GridView>({
67
- object: widgetRef,
68
- installer: factoryInstaller,
69
- estimatedHeight: estimatedItemHeight,
70
- estimatedWidth: estimatedItemWidth,
71
- });
72
-
73
- useInstalledModel(widgetRef, collection.installedModel, (widget, value) => widget.setModel(value));
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);
74
28
 
75
29
  return (
76
30
  <>
77
- <GtkGridView {...intrinsicProps} ref={setRef} />
78
- <CellRenderHost store={itemStore} resolver={collection.resolver} render={cellRenderer} />
31
+ <GtkGridView model={selection} factory={<GtkSignalListItemFactory {...itemCells.handlers} />} {...rest} />
32
+ <ItemPortals store={itemCells} render={renderItem} collection={collection} />
79
33
  </>
80
34
  );
81
- };
35
+ }
36
+
37
+ export { GridView };
package/src/index.ts CHANGED
@@ -1,29 +1,21 @@
1
- export {
2
- type ColumnDef,
3
- type ColumnDefDeclarativeProps,
4
- ColumnView,
5
- type ColumnViewDeclarativeProps,
6
- type ColumnViewProps,
7
- type ColumnViewSortProps,
8
- } from "./column-view.js";
9
- export { ConstraintLayout, type ConstraintLayoutProps } from "./constraint-layout.js";
10
- export type { ConstraintGuideProps, ConstraintProps, ConstraintVflProps } from "./constraint-layout-apply.js";
11
- export { DropDown, type DropDownDeclarativeProps, type DropDownItemRenderer, type DropDownProps } from "./drop-down.js";
12
- export { Fixed, type FixedChildProps, type FixedPlacementProps, type FixedProps } from "./fixed.js";
13
- export { Grid, type GridChildProps, type GridPlacement, type GridProps } from "./grid.js";
14
- export { GridView, type GridViewDeclarativeProps, type GridViewProps } from "./grid-view.js";
15
- export { ListView, type ListViewDeclarativeProps, type ListViewProps } from "./list-view.js";
16
- export { Menu, type MenuItemsProps, type MenuProps } from "./menu.js";
17
- export { Overlay, type OverlayChildProps, type OverlayPlacementProps, type OverlayProps } from "./overlay.js";
18
- export { SizeGroup, type SizeGroupChildProps, type SizeGroupProps } from "./size-group.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
7
  ChildProps,
21
- CollectionItemSizeProps,
22
- ControlledExpansionProps,
23
- ControlledSelectionProps,
24
- ItemNode,
25
- MenuEntry,
26
- RenderItemProps,
27
- SectionNode,
28
- WidgetProps,
8
+ Column,
9
+ ColumnViewProps,
10
+ DropDownProps,
11
+ GridViewProps,
12
+ HeaderRenderer,
13
+ Item,
14
+ ItemRenderer,
15
+ ListViewProps,
16
+ RenderHeaderArgs,
17
+ RenderItemArgs,
18
+ Section,
19
+ SizeGroupChildProps,
20
+ SizeGroupProps,
29
21
  } from "./types.js";
@@ -0,0 +1,345 @@
1
+ import type * as GObject from "@gtkx/gi/gobject";
2
+ import type { ReactNode } from "react";
3
+ import * as Gtk from "@gtkx/gi/gtk";
4
+ import { GtkTreeExpander } from "@gtkx/jsx/gtk";
5
+ import { createPortal, useProperty } from "@gtkx/react";
6
+ import { memo, useLayoutEffect, useState, useSyncExternalStore } from "react";
7
+ import type { HeaderRenderer, Item, ItemRenderer, RenderItemArgs } from "../types.js";
8
+ import type { Collection } from "./collection.js";
9
+ import { getId } from "./collection-model.js";
10
+
11
+ type CellSize = {
12
+ width: number;
13
+ height: number;
14
+ };
15
+
16
+ type CellHost = GObject.Object & {
17
+ getChild: () => Gtk.Widget | null;
18
+ setChild: (child: Gtk.Widget | null) => void;
19
+ };
20
+
21
+ type CellEntry<C extends CellHost> = {
22
+ key: string;
23
+ cell: C;
24
+ item: GObject.Object | null;
25
+ listeners: Set<() => void>;
26
+ subscribe: (onChange: () => void) => () => void;
27
+ getItem: () => GObject.Object | null;
28
+ };
29
+
30
+ type FactoryHandlers = {
31
+ onSetup: (cell: GObject.Object) => void;
32
+ onBind: (cell: GObject.Object) => void;
33
+ onUnbind: (cell: GObject.Object) => void;
34
+ onTeardown: (cell: GObject.Object) => void;
35
+ };
36
+
37
+ type CellStore<C extends CellHost> = {
38
+ handlers: FactoryHandlers;
39
+ setSize: (size: CellSize) => void;
40
+ subscribe: (onChange: () => void) => () => void;
41
+ getSnapshot: () => CellEntry<C>[];
42
+ };
43
+
44
+ type StoreState<C extends CellHost> = {
45
+ size: CellSize;
46
+ entries: Map<C, CellEntry<C>>;
47
+ snapshot: CellEntry<C>[] | null;
48
+ listeners: Set<() => void>;
49
+ serial: number;
50
+ };
51
+
52
+ type CellGuard<C extends CellHost> = (value: GObject.Object) => value is C;
53
+
54
+ type ItemBodyOptions = {
55
+ item: GObject.Object | null;
56
+ position: number | undefined;
57
+ row: Gtk.TreeListRow | null;
58
+ isRowExpanded: boolean | undefined;
59
+ render: ItemRenderer<never>;
60
+ collection: Collection;
61
+ expandedIds: string[] | null | undefined;
62
+ };
63
+
64
+ type ItemCellProps = {
65
+ entry: CellEntry<Gtk.ListItem>;
66
+ render: ItemRenderer<never>;
67
+ collection: Collection;
68
+ expandedIds: string[] | null | undefined;
69
+ };
70
+
71
+ type ItemPortalsProps = {
72
+ store: CellStore<Gtk.ListItem>;
73
+ render: ItemRenderer<never>;
74
+ collection: Collection;
75
+ expandedIds?: string[] | null | undefined;
76
+ };
77
+
78
+ type HeaderCellProps = {
79
+ entry: CellEntry<Gtk.ListHeader>;
80
+ render: HeaderRenderer<never>;
81
+ collection: Collection;
82
+ };
83
+
84
+ type HeaderPortalsProps = {
85
+ store: CellStore<Gtk.ListHeader>;
86
+ render: HeaderRenderer<never>;
87
+ collection: Collection;
88
+ };
89
+
90
+ const ItemCell = memo(ItemCellImpl);
91
+ const HeaderCell = memo(HeaderCellImpl);
92
+
93
+ const isListItem = (value: GObject.Object): value is Gtk.ListItem => value instanceof Gtk.ListItem;
94
+ const isListHeader = (value: GObject.Object): value is Gtk.ListHeader => value instanceof Gtk.ListHeader;
95
+
96
+ const placeholder = (size: CellSize): Gtk.Widget =>
97
+ new Gtk.Box({ widthRequest: size.width, heightRequest: size.height });
98
+
99
+ function notifyEntry<C extends CellHost>(entry: CellEntry<C>): void {
100
+ for (const listener of entry.listeners) {
101
+ listener();
102
+ }
103
+ }
104
+
105
+ function notifyRoster<C extends CellHost>(state: StoreState<C>): void {
106
+ state.snapshot = null;
107
+
108
+ for (const listener of state.listeners) {
109
+ listener();
110
+ }
111
+ }
112
+
113
+ function createEntry<C extends CellHost>(state: StoreState<C>, cell: C): CellEntry<C> {
114
+ state.serial += 1;
115
+
116
+ const entry: CellEntry<C> = {
117
+ key: `gtkx-cell-${String(state.serial)}`,
118
+ cell,
119
+ item: null,
120
+ listeners: new Set(),
121
+ subscribe: (onChange) => {
122
+ entry.listeners.add(onChange);
123
+
124
+ return () => {
125
+ entry.listeners.delete(onChange);
126
+ };
127
+ },
128
+ getItem: () => entry.item,
129
+ };
130
+
131
+ return entry;
132
+ }
133
+
134
+ function setupCell<C extends CellHost>(state: StoreState<C>, cell: C): void {
135
+ cell.setChild(placeholder(state.size));
136
+ state.entries.set(cell, createEntry(state, cell));
137
+ notifyRoster(state);
138
+ }
139
+
140
+ function teardownCell<C extends CellHost>(state: StoreState<C>, cell: C): void {
141
+ if (state.entries.delete(cell)) {
142
+ notifyRoster(state);
143
+ }
144
+ }
145
+
146
+ function writeBinding<C extends CellHost>(state: StoreState<C>, cell: C, item: GObject.Object | null): void {
147
+ const entry = state.entries.get(cell);
148
+
149
+ if (entry === undefined) {
150
+ return;
151
+ }
152
+
153
+ entry.item = item;
154
+ notifyEntry(entry);
155
+ }
156
+
157
+ function boundItem(cell: CellHost): GObject.Object | null {
158
+ if (cell instanceof Gtk.ListItem) {
159
+ return cell.getItem();
160
+ }
161
+
162
+ return cell instanceof Gtk.ListHeader ? cell.getItem() : null;
163
+ }
164
+
165
+ function bindCell<C extends CellHost>(state: StoreState<C>, cell: C): void {
166
+ if (cell.getChild() === null) {
167
+ cell.setChild(placeholder(state.size));
168
+ }
169
+
170
+ writeBinding(state, cell, boundItem(cell));
171
+ }
172
+
173
+ function subscribeRoster<C extends CellHost>(state: StoreState<C>, onChange: () => void): () => void {
174
+ state.listeners.add(onChange);
175
+
176
+ return () => {
177
+ state.listeners.delete(onChange);
178
+ };
179
+ }
180
+
181
+ function rosterSnapshot<C extends CellHost>(state: StoreState<C>): CellEntry<C>[] {
182
+ state.snapshot ??= state.entries.values().toArray();
183
+
184
+ return state.snapshot;
185
+ }
186
+
187
+ function guarded<C extends CellHost>(isCell: CellGuard<C>, run: (cell: C) => void): (cell: GObject.Object) => void {
188
+ return (cell) => {
189
+ if (isCell(cell)) {
190
+ run(cell);
191
+ }
192
+ };
193
+ }
194
+
195
+ function createHandlers<C extends CellHost>(state: StoreState<C>, isCell: CellGuard<C>): FactoryHandlers {
196
+ return {
197
+ onSetup: guarded(isCell, (cell) => {
198
+ setupCell(state, cell);
199
+ }),
200
+ onBind: guarded(isCell, (cell) => {
201
+ bindCell(state, cell);
202
+ }),
203
+ onUnbind: guarded(isCell, (cell) => {
204
+ writeBinding(state, cell, null);
205
+ }),
206
+ onTeardown: guarded(isCell, (cell) => {
207
+ teardownCell(state, cell);
208
+ }),
209
+ };
210
+ }
211
+
212
+ function createCellStore<C extends CellHost>(isCell: CellGuard<C>, size: CellSize): CellStore<C> {
213
+ const state: StoreState<C> = {
214
+ size,
215
+ entries: new Map(),
216
+ snapshot: null,
217
+ listeners: new Set(),
218
+ serial: 0,
219
+ };
220
+
221
+ return {
222
+ handlers: createHandlers(state, isCell),
223
+ setSize: (next) => {
224
+ state.size = next;
225
+ },
226
+ subscribe: (onChange) => subscribeRoster(state, onChange),
227
+ getSnapshot: () => rosterSnapshot(state),
228
+ };
229
+ }
230
+
231
+ function useCellStore<C extends CellHost>(isCell: CellGuard<C>, size: CellSize): CellStore<C> {
232
+ const [store] = useState(() => createCellStore(isCell, size));
233
+
234
+ useLayoutEffect(() => {
235
+ store.setSize(size);
236
+ });
237
+
238
+ return store;
239
+ }
240
+
241
+ function useItemCells(size: CellSize): CellStore<Gtk.ListItem> {
242
+ return useCellStore(isListItem, size);
243
+ }
244
+
245
+ function useHeaderCells(size: CellSize): CellStore<Gtk.ListHeader> {
246
+ return useCellStore(isListHeader, size);
247
+ }
248
+
249
+ function isRowWanted(options: ItemBodyOptions, item: Item): boolean {
250
+ const { expandedIds } = options;
251
+
252
+ return expandedIds == null ? options.isRowExpanded === true : expandedIds.includes(item.id);
253
+ }
254
+
255
+ function itemArgs(item: Item, options: ItemBodyOptions): RenderItemArgs<unknown> {
256
+ const args: RenderItemArgs<unknown> = { item: item.value, index: options.position ?? 0 };
257
+ const row = options.row;
258
+
259
+ if (row === null) {
260
+ return args;
261
+ }
262
+
263
+ args.depth = row.getDepth();
264
+
265
+ if (row.isExpandable()) {
266
+ args.isExpanded = isRowWanted(options, item);
267
+ }
268
+
269
+ return args;
270
+ }
271
+
272
+ function wrapExpander(item: Item, row: Gtk.TreeListRow | null, content: ReactNode): ReactNode {
273
+ const expander: ReactNode =
274
+ row === null
275
+ ? (
276
+ content
277
+ )
278
+ : (
279
+ <GtkTreeExpander
280
+ listRow={row}
281
+ hideExpander={item.hideExpander ?? false}
282
+ indentForDepth={item.indentForDepth ?? true}
283
+ indentForIcon={item.indentForIcon ?? true}
284
+ >
285
+ {content}
286
+ </GtkTreeExpander>
287
+ );
288
+
289
+ return expander;
290
+ }
291
+
292
+ function itemBody(options: ItemBodyOptions): ReactNode {
293
+ const id = getId(options.item);
294
+ const item = id === null ? undefined : options.collection.itemFor(id);
295
+
296
+ if (item === undefined) {
297
+ return null;
298
+ }
299
+
300
+ const render = options.render as ItemRenderer<unknown>;
301
+
302
+ return wrapExpander(item, options.row, render(itemArgs(item, options)));
303
+ }
304
+
305
+ function ItemCellImpl({ entry, render, collection, expandedIds }: ItemCellProps): ReactNode {
306
+ const item = useSyncExternalStore(entry.subscribe, entry.getItem);
307
+ const position = useProperty(entry.cell, "position");
308
+ const row = item instanceof Gtk.TreeListRow ? item : null;
309
+ const isRowExpanded = useProperty(row, "expanded");
310
+ const body = itemBody({ item, position, row, isRowExpanded, render, collection, expandedIds });
311
+
312
+ return createPortal(body, entry.cell, entry.key);
313
+ }
314
+
315
+ function HeaderCellImpl({ entry, render, collection }: HeaderCellProps): ReactNode {
316
+ const item = useSyncExternalStore(entry.subscribe, entry.getItem);
317
+ const id = getId(item);
318
+ const renderHeader = render as HeaderRenderer<unknown>;
319
+ const body = id === null ? null : renderHeader({ section: collection.sectionFor(id) });
320
+
321
+ return createPortal(body, entry.cell, entry.key);
322
+ }
323
+
324
+ function usePortalEntries<C extends CellHost>(store: CellStore<C>): CellEntry<C>[] {
325
+ return useSyncExternalStore(store.subscribe, store.getSnapshot);
326
+ }
327
+
328
+ const ItemPortals = ({ store, render, collection, expandedIds }: ItemPortalsProps): ReactNode =>
329
+ usePortalEntries(store).map((entry) => (
330
+ <ItemCell key={entry.key} entry={entry} render={render} collection={collection} expandedIds={expandedIds} />
331
+ ));
332
+
333
+ const HeaderPortals = ({ store, render, collection }: HeaderPortalsProps): ReactNode =>
334
+ usePortalEntries(store).map((entry) => (
335
+ <HeaderCell key={entry.key} entry={entry} render={render} collection={collection} />
336
+ ));
337
+
338
+ export {
339
+ useItemCells,
340
+ useHeaderCells,
341
+ ItemPortals,
342
+ HeaderPortals,
343
+ type CellSize,
344
+ type CellStore,
345
+ };
@@ -0,0 +1,168 @@
1
+ import type { Item, Section } from "../types.js";
2
+
3
+ type Level = {
4
+ key: string;
5
+ ids: string[];
6
+ isExpandable: boolean[];
7
+ };
8
+
9
+ type CollectionIndex = {
10
+ isTree: boolean;
11
+ size: number;
12
+ groups: Level[];
13
+ children: Map<string, Level>;
14
+ has: (id: string) => boolean;
15
+ itemFor: (id: string) => Item | undefined;
16
+ sectionFor: (id: string) => unknown;
17
+ positionFor: (id: string) => number;
18
+ };
19
+
20
+ type IndexState = {
21
+ isTree: boolean;
22
+ groups: Level[];
23
+ children: Map<string, Level>;
24
+ itemsById: Map<string, Item>;
25
+ sections: Section[] | undefined;
26
+ positions: Map<string, number> | null;
27
+ starts: number[] | null;
28
+ };
29
+
30
+ const ROOT_LEVEL_KEY = "";
31
+
32
+ const hasChildren = (item: Item): boolean => item.children !== undefined && item.children.length > 0;
33
+ const childLevelKey = (id: string): string => `child:${id}`;
34
+
35
+ function collectChildren(state: IndexState, level: Level, item: Item): void {
36
+ if (!state.isTree) {
37
+ return;
38
+ }
39
+
40
+ const canExpand = hasChildren(item);
41
+ level.isExpandable.push(canExpand);
42
+
43
+ if (!canExpand) {
44
+ return;
45
+ }
46
+
47
+ const key = childLevelKey(item.id);
48
+ state.children.set(key, collectLevel(state, key, item.children ?? []));
49
+ }
50
+
51
+ function collectLevel(state: IndexState, key: string, items: Item[]): Level {
52
+ const level: Level = { key, ids: [], isExpandable: [] };
53
+
54
+ for (const item of items) {
55
+ level.ids.push(item.id);
56
+ state.itemsById.set(item.id, item);
57
+ collectChildren(state, level, item);
58
+ }
59
+
60
+ return level;
61
+ }
62
+
63
+ function buildPositions(state: IndexState): void {
64
+ const positions: Map<string, number> = new Map();
65
+ const starts: number[] = [];
66
+ let offset = 0;
67
+
68
+ for (const level of state.groups) {
69
+ starts.push(offset);
70
+
71
+ for (const id of level.ids) {
72
+ positions.set(id, offset);
73
+ offset += 1;
74
+ }
75
+ }
76
+
77
+ state.positions = positions;
78
+ state.starts = starts;
79
+ }
80
+
81
+ function positionFor(state: IndexState, id: string): number {
82
+ if (state.isTree) {
83
+ return -1;
84
+ }
85
+
86
+ if (state.positions === null) {
87
+ buildPositions(state);
88
+ }
89
+
90
+ return state.positions?.get(id) ?? -1;
91
+ }
92
+
93
+ function sectionAt(starts: number[], position: number): number {
94
+ let low = 0;
95
+ let high = starts.length - 1;
96
+
97
+ while (low < high) {
98
+ const middle = Math.ceil((low + high) / 2);
99
+
100
+ if ((starts[middle] ?? 0) <= position) {
101
+ low = middle;
102
+ } else {
103
+ high = middle - 1;
104
+ }
105
+ }
106
+
107
+ return low;
108
+ }
109
+
110
+ function sectionFor(state: IndexState, id: string): unknown {
111
+ const sections = state.sections;
112
+ const position = positionFor(state, id);
113
+
114
+ if (sections === undefined || position < 0 || state.starts === null) {
115
+ return undefined;
116
+ }
117
+
118
+ return sections[sectionAt(state.starts, position)]?.value;
119
+ }
120
+
121
+ function buildGroups(state: IndexState, source: Item[], sections: Section[] | undefined): Level[] {
122
+ if (sections === undefined) {
123
+ return [collectLevel(state, ROOT_LEVEL_KEY, source)];
124
+ }
125
+
126
+ return sections.map((section) => collectLevel(state, section.id, section.data));
127
+ }
128
+
129
+ function isTreeSource(source: Item[], sections: Section[] | undefined, isFlat: boolean): boolean {
130
+ if (sections !== undefined || isFlat) {
131
+ return false;
132
+ }
133
+
134
+ return source.some((item) => hasChildren(item));
135
+ }
136
+
137
+ function createCollectionIndex(
138
+ items: Item[] | undefined,
139
+ sections: Section[] | undefined,
140
+ isFlat: boolean,
141
+ ): CollectionIndex {
142
+ const source = items ?? [];
143
+
144
+ const state: IndexState = {
145
+ isTree: isTreeSource(source, sections, isFlat),
146
+ groups: [],
147
+ children: new Map(),
148
+ itemsById: new Map(),
149
+ sections,
150
+ positions: null,
151
+ starts: null,
152
+ };
153
+
154
+ state.groups = buildGroups(state, source, sections);
155
+
156
+ return {
157
+ isTree: state.isTree,
158
+ size: state.itemsById.size,
159
+ groups: state.groups,
160
+ children: state.children,
161
+ has: (id) => state.itemsById.has(id),
162
+ itemFor: (id) => state.itemsById.get(id),
163
+ sectionFor: (id) => sectionFor(state, id),
164
+ positionFor: (id) => positionFor(state, id),
165
+ };
166
+ }
167
+
168
+ export { createCollectionIndex, childLevelKey, ROOT_LEVEL_KEY, type CollectionIndex, type Level };