@gtkx/components 1.0.0-rc.4 → 1.1.0

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 (86) hide show
  1. package/README.md +5 -5
  2. package/dist/column-view.d.ts +2 -2
  3. package/dist/column-view.d.ts.map +1 -1
  4. package/dist/column-view.js +14 -12
  5. package/dist/column-view.js.map +1 -1
  6. package/dist/grid-view.js +1 -1
  7. package/dist/grid-view.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/internal/cells.d.ts +31 -23
  12. package/dist/internal/cells.d.ts.map +1 -1
  13. package/dist/internal/cells.js +168 -77
  14. package/dist/internal/cells.js.map +1 -1
  15. package/dist/internal/collection-index.d.ts +8 -11
  16. package/dist/internal/collection-index.d.ts.map +1 -1
  17. package/dist/internal/collection-index.js +53 -73
  18. package/dist/internal/collection-index.js.map +1 -1
  19. package/dist/internal/collection-model.d.ts +24 -4
  20. package/dist/internal/collection-model.d.ts.map +1 -1
  21. package/dist/internal/collection-model.js +247 -96
  22. package/dist/internal/collection-model.js.map +1 -1
  23. package/dist/internal/collection.d.ts +8 -10
  24. package/dist/internal/collection.d.ts.map +1 -1
  25. package/dist/internal/collection.js +38 -48
  26. package/dist/internal/collection.js.map +1 -1
  27. package/dist/internal/controlled-sync.d.ts +11 -0
  28. package/dist/internal/controlled-sync.d.ts.map +1 -0
  29. package/dist/internal/controlled-sync.js +19 -0
  30. package/dist/internal/controlled-sync.js.map +1 -0
  31. package/dist/internal/drop-down.d.ts.map +1 -1
  32. package/dist/internal/drop-down.js +27 -15
  33. package/dist/internal/drop-down.js.map +1 -1
  34. package/dist/internal/expansion.d.ts +3 -2
  35. package/dist/internal/expansion.d.ts.map +1 -1
  36. package/dist/internal/expansion.js +73 -48
  37. package/dist/internal/expansion.js.map +1 -1
  38. package/dist/internal/keys.d.ts +5 -0
  39. package/dist/internal/keys.d.ts.map +1 -0
  40. package/dist/internal/keys.js +13 -0
  41. package/dist/internal/keys.js.map +1 -0
  42. package/dist/internal/selection.d.ts +3 -2
  43. package/dist/internal/selection.d.ts.map +1 -1
  44. package/dist/internal/selection.js +68 -35
  45. package/dist/internal/selection.js.map +1 -1
  46. package/dist/internal/slots.d.ts +10 -0
  47. package/dist/internal/slots.d.ts.map +1 -0
  48. package/dist/internal/slots.js +43 -0
  49. package/dist/internal/slots.js.map +1 -0
  50. package/dist/internal/tree-expansion.d.ts +19 -0
  51. package/dist/internal/tree-expansion.d.ts.map +1 -0
  52. package/dist/internal/tree-expansion.js +72 -0
  53. package/dist/internal/tree-expansion.js.map +1 -0
  54. package/dist/internal/tree-order.d.ts +26 -0
  55. package/dist/internal/tree-order.d.ts.map +1 -0
  56. package/dist/internal/tree-order.js +109 -0
  57. package/dist/internal/tree-order.js.map +1 -0
  58. package/dist/internal/use-collection.d.ts +1 -1
  59. package/dist/internal/use-collection.d.ts.map +1 -1
  60. package/dist/internal/use-collection.js +4 -4
  61. package/dist/internal/use-collection.js.map +1 -1
  62. package/dist/list-view.d.ts.map +1 -1
  63. package/dist/list-view.js +6 -6
  64. package/dist/list-view.js.map +1 -1
  65. package/dist/types.d.ts +72 -22
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js.map +1 -1
  68. package/package.json +17 -9
  69. package/src/column-view.tsx +28 -25
  70. package/src/grid-view.tsx +1 -1
  71. package/src/index.ts +3 -0
  72. package/src/internal/cells.tsx +339 -117
  73. package/src/internal/collection-index.ts +68 -92
  74. package/src/internal/collection-model.ts +327 -112
  75. package/src/internal/collection.ts +45 -65
  76. package/src/internal/controlled-sync.ts +34 -0
  77. package/src/internal/drop-down.tsx +41 -20
  78. package/src/internal/expansion.ts +95 -70
  79. package/src/internal/keys.ts +18 -0
  80. package/src/internal/selection.tsx +100 -51
  81. package/src/internal/slots.ts +67 -0
  82. package/src/internal/tree-expansion.ts +115 -0
  83. package/src/internal/tree-order.ts +208 -0
  84. package/src/internal/use-collection.tsx +5 -5
  85. package/src/list-view.tsx +14 -10
  86. package/src/types.ts +78 -20
@@ -1,26 +1,39 @@
1
1
  import type * as GObject from "@gtkx/gi/gobject";
2
- import type { ReactNode } from "react";
2
+ import type { ReactElement, ReactNode } from "react";
3
3
  import * as Gtk from "@gtkx/gi/gtk";
4
- import { GtkTreeExpander } from "@gtkx/jsx/gtk";
4
+ import { GtkSignalListItemFactory, GtkTreeExpander } from "@gtkx/jsx/gtk";
5
5
  import { createPortal, useProperty } from "@gtkx/react";
6
+ import { setObjectProperty, t } from "@gtkx/runtime";
6
7
  import { memo, useLayoutEffect, useState, useSyncExternalStore } from "react";
7
- import type { ListItem, ListItemRenderArgs, ListItemRenderer, ListSectionRenderer } from "../types.js";
8
+ import type {
9
+ ExpanderDescriptions,
10
+ ListItem,
11
+ ListItemRenderArgs,
12
+ ListItemRenderer,
13
+ ListRowProps,
14
+ ListRowPropsResolver,
15
+ ListSectionRenderer,
16
+ } from "../types.js";
8
17
  import type { Collection } from "./collection.js";
9
- import { getId } from "./collection-model.js";
18
+ import { slotRefFor } from "./collection-model.js";
10
19
 
11
20
  type CellSize = {
12
21
  width: number;
13
22
  height: number;
14
23
  };
15
24
 
16
- type CellHost = GObject.Object & {
25
+ type FactoryHost = GObject.Object & {
26
+ getItem: () => GObject.Object | null;
27
+ };
28
+
29
+ type CellHost = FactoryHost & {
17
30
  getChild: () => Gtk.Widget | null;
18
31
  setChild: (child: Gtk.Widget | null) => void;
19
32
  };
20
33
 
21
- type CellEntry<C extends CellHost> = {
34
+ type CellEntry<H extends FactoryHost> = {
22
35
  key: string;
23
- cell: C;
36
+ host: H;
24
37
  item: GObject.Object | null;
25
38
  listeners: Set<() => void>;
26
39
  subscribe: (onChange: () => void) => () => void;
@@ -28,51 +41,88 @@ type CellEntry<C extends CellHost> = {
28
41
  };
29
42
 
30
43
  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;
44
+ onSetup: (host: GObject.Object) => void;
45
+ onBind: (host: GObject.Object) => void;
46
+ onUnbind: (host: GObject.Object) => void;
47
+ onTeardown: (host: GObject.Object) => void;
35
48
  };
36
49
 
37
- type CellStore<C extends CellHost> = {
50
+ type CellRegistry<H extends FactoryHost> = {
38
51
  handlers: FactoryHandlers;
39
52
  setSize: (size: CellSize) => void;
40
53
  subscribe: (onChange: () => void) => () => void;
41
- getSnapshot: () => CellEntry<C>[];
54
+ getVersion: () => number;
55
+ getEntries: () => CellEntry<H>[];
42
56
  };
43
57
 
44
- type StoreState<C extends CellHost> = {
58
+ type HostGuard<H extends FactoryHost> = (value: GObject.Object) => value is H;
59
+
60
+ type CellRegistryOptions<H extends FactoryHost> = {
61
+ isHost: HostGuard<H>;
62
+ prepare?: ((host: H, size: CellSize) => void) | undefined;
63
+ };
64
+
65
+ type CellRegistryState<H extends FactoryHost> = CellRegistryOptions<H> & {
45
66
  size: CellSize;
46
- entries: Map<C, CellEntry<C>>;
47
- snapshot: CellEntry<C>[] | null;
67
+ entries: Map<H, CellEntry<H>>;
68
+ snapshot: CellEntry<H>[] | null;
48
69
  listeners: Set<() => void>;
49
70
  serial: number;
71
+ version: number;
50
72
  };
51
73
 
52
- type CellGuard<C extends CellHost> = (value: GObject.Object) => value is C;
74
+ type ResolvedRowProps = {
75
+ accessibleLabel: string | null;
76
+ accessibleDescription: string | null;
77
+ isActivatable: boolean;
78
+ isFocusable: boolean;
79
+ isSelectable: boolean;
80
+ };
53
81
 
54
- type ItemBodyOptions = {
82
+ type ItemSlotOptions = {
55
83
  item: GObject.Object | null;
56
84
  position: number | undefined;
57
85
  row: Gtk.TreeListRow | null;
58
- isRowExpanded: boolean | undefined;
59
- render: ListItemRenderer<never>;
60
86
  collection: Collection;
61
87
  expandedIds: string[] | null | undefined;
62
88
  };
63
89
 
90
+ type PositionedHost = Gtk.ListItem | Gtk.ColumnViewRow;
91
+
92
+ type ItemSlot = {
93
+ item: ListItem;
94
+ row: Gtk.TreeListRow | null;
95
+ args: ListItemRenderArgs<unknown>;
96
+ };
97
+
64
98
  type ItemCellProps = {
65
99
  entry: CellEntry<Gtk.ListItem>;
66
100
  render: ListItemRenderer<never>;
67
101
  collection: Collection;
68
102
  expandedIds: string[] | null | undefined;
103
+ expanderDescriptions: ExpanderDescriptions | null | undefined;
69
104
  };
70
105
 
71
106
  type ItemPortalsProps = {
72
- store: CellStore<Gtk.ListItem>;
107
+ registry: CellRegistry<Gtk.ListItem>;
73
108
  render: ListItemRenderer<never>;
74
109
  collection: Collection;
75
110
  expandedIds?: string[] | null | undefined;
111
+ expanderDescriptions?: ExpanderDescriptions | null | undefined;
112
+ };
113
+
114
+ type ItemRowProps = {
115
+ entry: CellEntry<Gtk.ColumnViewRow>;
116
+ rowProps: ListRowPropsResolver<never>;
117
+ collection: Collection;
118
+ expandedIds: string[] | null | undefined;
119
+ };
120
+
121
+ type RowPortalsProps = {
122
+ registry: CellRegistry<Gtk.ColumnViewRow>;
123
+ rowProps: ListRowPropsResolver<never>;
124
+ collection: Collection;
125
+ expandedIds: string[] | null | undefined;
76
126
  };
77
127
 
78
128
  type HeaderCellProps = {
@@ -82,40 +132,73 @@ type HeaderCellProps = {
82
132
  };
83
133
 
84
134
  type HeaderPortalsProps = {
85
- store: CellStore<Gtk.ListHeader>;
135
+ registry: CellRegistry<Gtk.ListHeader>;
86
136
  render: ListSectionRenderer<never>;
87
137
  collection: Collection;
88
138
  };
89
139
 
140
+ type SectionHeaderSlot = {
141
+ factoryProps: { headerFactory?: ReactElement };
142
+ portals: ReactNode;
143
+ };
144
+
145
+ type RowSlot = {
146
+ factoryProps: { rowFactory?: ReactElement };
147
+ portals: ReactNode;
148
+ };
149
+
90
150
  const ItemCell = memo(ItemCellImpl);
151
+ const ItemRow = memo(ItemRowImpl);
91
152
  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;
153
+ const CELL_OPTIONS: CellRegistryOptions<Gtk.ListItem> = { isHost: isListItem, prepare: prepareCell };
154
+ const HEADER_OPTIONS: CellRegistryOptions<Gtk.ListHeader> = { isHost: isListHeader, prepare: prepareCell };
155
+ const ROW_OPTIONS: CellRegistryOptions<Gtk.ColumnViewRow> = { isHost: isColumnViewRow };
156
+ const NO_SIZE: CellSize = { width: -1, height: -1 };
157
+ const NO_ROW_PROPS: ListRowProps = {};
158
+ const ROW_TEXT_DESCRIPTOR = t.string("borrowed");
95
159
 
96
160
  const placeholder = (size: CellSize): Gtk.Widget =>
97
161
  new Gtk.Box({ widthRequest: size.width, heightRequest: size.height });
98
162
 
99
- function notifyEntry<C extends CellHost>(entry: CellEntry<C>): void {
163
+ function isListItem(value: GObject.Object): value is Gtk.ListItem {
164
+ return value instanceof Gtk.ListItem;
165
+ }
166
+
167
+ function isListHeader(value: GObject.Object): value is Gtk.ListHeader {
168
+ return value instanceof Gtk.ListHeader;
169
+ }
170
+
171
+ function isColumnViewRow(value: GObject.Object): value is Gtk.ColumnViewRow {
172
+ return value instanceof Gtk.ColumnViewRow;
173
+ }
174
+
175
+ function prepareCell(host: CellHost, size: CellSize): void {
176
+ if (host.getChild() === null) {
177
+ host.setChild(placeholder(size));
178
+ }
179
+ }
180
+
181
+ function notifyEntry<H extends FactoryHost>(entry: CellEntry<H>): void {
100
182
  for (const listener of entry.listeners) {
101
183
  listener();
102
184
  }
103
185
  }
104
186
 
105
- function notifyRoster<C extends CellHost>(state: StoreState<C>): void {
187
+ function notifyRegistry<H extends FactoryHost>(state: CellRegistryState<H>): void {
106
188
  state.snapshot = null;
189
+ state.version += 1;
107
190
 
108
191
  for (const listener of state.listeners) {
109
192
  listener();
110
193
  }
111
194
  }
112
195
 
113
- function createEntry<C extends CellHost>(state: StoreState<C>, cell: C): CellEntry<C> {
196
+ function createEntry<H extends FactoryHost>(state: CellRegistryState<H>, host: H): CellEntry<H> {
114
197
  state.serial += 1;
115
198
 
116
- const entry: CellEntry<C> = {
199
+ const entry: CellEntry<H> = {
117
200
  key: `gtkx-cell-${String(state.serial)}`,
118
- cell,
201
+ host,
119
202
  item: null,
120
203
  listeners: new Set(),
121
204
  subscribe: (onChange) => {
@@ -131,20 +214,20 @@ function createEntry<C extends CellHost>(state: StoreState<C>, cell: C): CellEnt
131
214
  return entry;
132
215
  }
133
216
 
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);
217
+ function setupHost<H extends FactoryHost>(state: CellRegistryState<H>, host: H): void {
218
+ state.prepare?.(host, state.size);
219
+ state.entries.set(host, createEntry(state, host));
220
+ notifyRegistry(state);
138
221
  }
139
222
 
140
- function teardownCell<C extends CellHost>(state: StoreState<C>, cell: C): void {
141
- if (state.entries.delete(cell)) {
142
- notifyRoster(state);
223
+ function teardownHost<H extends FactoryHost>(state: CellRegistryState<H>, host: H): void {
224
+ if (state.entries.delete(host)) {
225
+ notifyRegistry(state);
143
226
  }
144
227
  }
145
228
 
146
- function writeBinding<C extends CellHost>(state: StoreState<C>, cell: C, item: GObject.Object | null): void {
147
- const entry = state.entries.get(cell);
229
+ function writeBinding<H extends FactoryHost>(state: CellRegistryState<H>, host: H, item: GObject.Object | null): void {
230
+ const entry = state.entries.get(host);
148
231
 
149
232
  if (entry === undefined) {
150
233
  return;
@@ -154,23 +237,12 @@ function writeBinding<C extends CellHost>(state: StoreState<C>, cell: C, item: G
154
237
  notifyEntry(entry);
155
238
  }
156
239
 
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;
240
+ function bindHost<H extends FactoryHost>(state: CellRegistryState<H>, host: H): void {
241
+ state.prepare?.(host, state.size);
242
+ writeBinding(state, host, host.getItem());
163
243
  }
164
244
 
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 {
245
+ function subscribeRegistry<H extends FactoryHost>(state: CellRegistryState<H>, onChange: () => void): () => void {
174
246
  state.listeners.add(onChange);
175
247
 
176
248
  return () => {
@@ -178,81 +250,84 @@ function subscribeRoster<C extends CellHost>(state: StoreState<C>, onChange: ()
178
250
  };
179
251
  }
180
252
 
181
- function rosterSnapshot<C extends CellHost>(state: StoreState<C>): CellEntry<C>[] {
253
+ function getRegistryEntries<H extends FactoryHost>(state: CellRegistryState<H>): CellEntry<H>[] {
182
254
  state.snapshot ??= state.entries.values().toArray();
183
255
 
184
256
  return state.snapshot;
185
257
  }
186
258
 
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);
259
+ function guarded<H extends FactoryHost>(isHost: HostGuard<H>, run: (host: H) => void): (host: GObject.Object) => void {
260
+ return (host) => {
261
+ if (isHost(host)) {
262
+ run(host);
191
263
  }
192
264
  };
193
265
  }
194
266
 
195
- function createHandlers<C extends CellHost>(state: StoreState<C>, isCell: CellGuard<C>): FactoryHandlers {
267
+ function createHandlers<H extends FactoryHost>(state: CellRegistryState<H>): FactoryHandlers {
268
+ const { isHost } = state;
269
+
196
270
  return {
197
- onSetup: guarded(isCell, (cell) => {
198
- setupCell(state, cell);
271
+ onSetup: guarded(isHost, (host) => {
272
+ setupHost(state, host);
199
273
  }),
200
- onBind: guarded(isCell, (cell) => {
201
- bindCell(state, cell);
274
+ onBind: guarded(isHost, (host) => {
275
+ bindHost(state, host);
202
276
  }),
203
- onUnbind: guarded(isCell, (cell) => {
204
- writeBinding(state, cell, null);
277
+ onUnbind: guarded(isHost, (host) => {
278
+ writeBinding(state, host, null);
205
279
  }),
206
- onTeardown: guarded(isCell, (cell) => {
207
- teardownCell(state, cell);
280
+ onTeardown: guarded(isHost, (host) => {
281
+ teardownHost(state, host);
208
282
  }),
209
283
  };
210
284
  }
211
285
 
212
- function createCellStore<C extends CellHost>(isCell: CellGuard<C>, size: CellSize): CellStore<C> {
213
- const state: StoreState<C> = {
286
+ function createCellRegistry<H extends FactoryHost>(options: CellRegistryOptions<H>, size: CellSize): CellRegistry<H> {
287
+ const state: CellRegistryState<H> = {
288
+ ...options,
214
289
  size,
215
290
  entries: new Map(),
216
291
  snapshot: null,
217
292
  listeners: new Set(),
218
293
  serial: 0,
294
+ version: 0,
219
295
  };
220
296
 
221
297
  return {
222
- handlers: createHandlers(state, isCell),
298
+ handlers: createHandlers(state),
223
299
  setSize: (next) => {
224
300
  state.size = next;
225
301
  },
226
- subscribe: (onChange) => subscribeRoster(state, onChange),
227
- getSnapshot: () => rosterSnapshot(state),
302
+ subscribe: (onChange) => subscribeRegistry(state, onChange),
303
+ getVersion: () => state.version,
304
+ getEntries: () => getRegistryEntries(state),
228
305
  };
229
306
  }
230
307
 
231
- function useCellStore<C extends CellHost>(isCell: CellGuard<C>, size: CellSize): CellStore<C> {
232
- const [store] = useState(() => createCellStore(isCell, size));
308
+ function useCellRegistry<H extends FactoryHost>(options: CellRegistryOptions<H>, size: CellSize): CellRegistry<H> {
309
+ const [registry] = useState(() => createCellRegistry(options, size));
233
310
 
234
311
  useLayoutEffect(() => {
235
- store.setSize(size);
312
+ registry.setSize(size);
236
313
  });
237
314
 
238
- return store;
315
+ return registry;
239
316
  }
240
317
 
241
- function useItemCells(size: CellSize): CellStore<Gtk.ListItem> {
242
- return useCellStore(isListItem, size);
318
+ function useItemCells(size: CellSize): CellRegistry<Gtk.ListItem> {
319
+ return useCellRegistry(CELL_OPTIONS, size);
243
320
  }
244
321
 
245
- function useHeaderCells(size: CellSize): CellStore<Gtk.ListHeader> {
246
- return useCellStore(isListHeader, size);
322
+ function useHeaderCells(size: CellSize): CellRegistry<Gtk.ListHeader> {
323
+ return useCellRegistry(HEADER_OPTIONS, size);
247
324
  }
248
325
 
249
- function isRowWanted(options: ItemBodyOptions, item: ListItem): boolean {
250
- const { expandedIds } = options;
251
-
252
- return expandedIds == null ? options.isRowExpanded === true : expandedIds.includes(item.id);
326
+ function isRowWanted(options: ItemSlotOptions, item: ListItem): boolean {
327
+ return options.expandedIds?.includes(item.id) ?? false;
253
328
  }
254
329
 
255
- function itemArgs(item: ListItem, options: ItemBodyOptions): ListItemRenderArgs<unknown> {
330
+ function itemArgs(item: ListItem, options: ItemSlotOptions): ListItemRenderArgs<unknown> {
256
331
  const args: ListItemRenderArgs<unknown> = { item: item.value, index: options.position ?? 0 };
257
332
  const row = options.row;
258
333
 
@@ -269,7 +344,49 @@ function itemArgs(item: ListItem, options: ItemBodyOptions): ListItemRenderArgs<
269
344
  return args;
270
345
  }
271
346
 
272
- function wrapExpander(item: ListItem, row: Gtk.TreeListRow | null, content: ReactNode): ReactNode {
347
+ function slotFor(options: ItemSlotOptions): ItemSlot | null {
348
+ const ref = slotRefFor(options.item);
349
+ const item = ref === null ? undefined : options.collection.itemAt(ref);
350
+
351
+ if (item === undefined) {
352
+ return null;
353
+ }
354
+
355
+ return { item, row: options.row, args: itemArgs(item, options) };
356
+ }
357
+
358
+ function useItemSlot(
359
+ entry: CellEntry<PositionedHost>,
360
+ collection: Collection,
361
+ expandedIds: string[] | null | undefined,
362
+ ): ItemSlot | null {
363
+ const position = useProperty(entry.host, "position");
364
+ const item = useSyncExternalStore(entry.subscribe, entry.getItem);
365
+ const row = item instanceof Gtk.TreeListRow ? item : null;
366
+
367
+ return slotFor({ item, position, row, collection, expandedIds });
368
+ }
369
+
370
+ function expanderDescriptionFor(
371
+ slot: ItemSlot,
372
+ descriptions: ExpanderDescriptions | null | undefined,
373
+ ): string | undefined {
374
+ const { isExpanded } = slot.args;
375
+
376
+ if (isExpanded === undefined || descriptions == null) {
377
+ return undefined;
378
+ }
379
+
380
+ return isExpanded ? descriptions.collapse : descriptions.expand;
381
+ }
382
+
383
+ function wrapExpander(
384
+ slot: ItemSlot,
385
+ content: ReactNode,
386
+ descriptions: ExpanderDescriptions | null | undefined,
387
+ ): ReactNode {
388
+ const { item, row } = slot;
389
+
273
390
  const expander: ReactNode =
274
391
  row === null
275
392
  ? (
@@ -278,9 +395,10 @@ function wrapExpander(item: ListItem, row: Gtk.TreeListRow | null, content: Reac
278
395
  : (
279
396
  <GtkTreeExpander
280
397
  listRow={row}
281
- hideExpander={item.hideExpander ?? false}
282
- indentForDepth={item.indentForDepth ?? true}
283
- indentForIcon={item.indentForIcon ?? true}
398
+ hideExpander={item.shouldHideExpander ?? false}
399
+ indentForDepth={item.shouldIndentForDepth ?? true}
400
+ indentForIcon={item.shouldIndentForIcon ?? true}
401
+ accessibleDescription={expanderDescriptionFor(slot, descriptions)}
284
402
  >
285
403
  {content}
286
404
  </GtkTreeExpander>
@@ -289,57 +407,161 @@ function wrapExpander(item: ListItem, row: Gtk.TreeListRow | null, content: Reac
289
407
  return expander;
290
408
  }
291
409
 
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) {
410
+ function itemBody(
411
+ slot: ItemSlot | null,
412
+ render: ListItemRenderer<never>,
413
+ descriptions: ExpanderDescriptions | null | undefined,
414
+ ): ReactNode {
415
+ if (slot === null) {
297
416
  return null;
298
417
  }
299
418
 
300
- const render = options.render as ListItemRenderer<unknown>;
419
+ const renderItem = render as ListItemRenderer<unknown>;
301
420
 
302
- return wrapExpander(item, options.row, render(itemArgs(item, options)));
421
+ return wrapExpander(slot, renderItem(slot.args), descriptions);
303
422
  }
304
423
 
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 });
424
+ function rowText(value: string | undefined): string | null {
425
+ return value ?? null;
426
+ }
427
+
428
+ /* eslint-disable-next-line unicorn/consistent-boolean-name -- reads the flag off a row, paired with rowText */
429
+ function rowFlag(value: boolean | undefined): boolean {
430
+ return value ?? true;
431
+ }
432
+
433
+ function rowPropsFor(slot: ItemSlot | null, rowProps: ListRowPropsResolver<never>): ResolvedRowProps {
434
+ const resolve = rowProps as ListRowPropsResolver<unknown>;
435
+ const props = slot === null ? NO_ROW_PROPS : resolve(slot.args);
436
+
437
+ return {
438
+ accessibleLabel: rowText(props.accessibleLabel),
439
+ accessibleDescription: rowText(props.accessibleDescription),
440
+ isActivatable: rowFlag(props.isActivatable),
441
+ isFocusable: rowFlag(props.isFocusable),
442
+ isSelectable: rowFlag(props.isSelectable),
443
+ };
444
+ }
445
+
446
+ function setRowText(host: Gtk.ColumnViewRow, name: string, value: string | null): void {
447
+ setObjectProperty(host, name, ROW_TEXT_DESCRIPTOR, value);
448
+ }
449
+
450
+ function applyRowProps(host: Gtk.ColumnViewRow, props: ResolvedRowProps): void {
451
+ setRowText(host, "accessible-label", props.accessibleLabel);
452
+ setRowText(host, "accessible-description", props.accessibleDescription);
453
+ host.setActivatable(props.isActivatable);
454
+ host.setFocusable(props.isFocusable);
455
+ host.setSelectable(props.isSelectable);
456
+ }
457
+
458
+ function ItemCellImpl({ entry, render, collection, expandedIds, expanderDescriptions }: ItemCellProps): ReactNode {
459
+ const slot = useItemSlot(entry, collection, expandedIds);
460
+ const body = itemBody(slot, render, expanderDescriptions);
311
461
 
312
- return createPortal(body, entry.cell, entry.key);
462
+ return createPortal(body, entry.host, entry.key);
463
+ }
464
+
465
+ function ItemRowImpl({ entry, rowProps, collection, expandedIds }: ItemRowProps): ReactNode {
466
+ const slot = useItemSlot(entry, collection, expandedIds);
467
+ const props = rowPropsFor(slot, rowProps);
468
+
469
+ useLayoutEffect(() => {
470
+ applyRowProps(entry.host, props);
471
+ });
472
+
473
+ return null;
313
474
  }
314
475
 
315
476
  function HeaderCellImpl({ entry, render, collection }: HeaderCellProps): ReactNode {
316
477
  const item = useSyncExternalStore(entry.subscribe, entry.getItem);
317
- const id = getId(item);
478
+ const ref = slotRefFor(item);
318
479
  const renderHeader = render as ListSectionRenderer<unknown>;
319
- const body = id === null ? null : renderHeader({ section: collection.sectionFor(id) });
480
+ const body = ref === null ? null : renderHeader({ section: collection.sectionFor(ref.store.level.path) });
320
481
 
321
- return createPortal(body, entry.cell, entry.key);
482
+ return createPortal(body, entry.host, entry.key);
322
483
  }
323
484
 
324
- function usePortalEntries<C extends CellHost>(store: CellStore<C>): CellEntry<C>[] {
325
- return useSyncExternalStore(store.subscribe, store.getSnapshot);
485
+ function usePortalEntries<H extends FactoryHost>(registry: CellRegistry<H>): CellEntry<H>[] {
486
+ useSyncExternalStore(registry.subscribe, registry.getVersion);
487
+
488
+ return registry.getEntries();
326
489
  }
327
490
 
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} />
491
+ const ItemPortals = ({
492
+ registry,
493
+ render,
494
+ collection,
495
+ expandedIds,
496
+ expanderDescriptions,
497
+ }: ItemPortalsProps): ReactNode =>
498
+ usePortalEntries(registry).map((entry) => (
499
+ <ItemCell
500
+ key={entry.key}
501
+ entry={entry}
502
+ render={render}
503
+ collection={collection}
504
+ expandedIds={expandedIds}
505
+ expanderDescriptions={expanderDescriptions}
506
+ />
507
+ ));
508
+
509
+ const RowPortals = ({ registry, rowProps, collection, expandedIds }: RowPortalsProps): ReactNode =>
510
+ usePortalEntries(registry).map((entry) => (
511
+ <ItemRow
512
+ key={entry.key}
513
+ entry={entry}
514
+ rowProps={rowProps}
515
+ collection={collection}
516
+ expandedIds={expandedIds}
517
+ />
331
518
  ));
332
519
 
333
- const HeaderPortals = ({ store, render, collection }: HeaderPortalsProps): ReactNode =>
334
- usePortalEntries(store).map((entry) => (
520
+ const HeaderPortals = ({ registry, render, collection }: HeaderPortalsProps): ReactNode =>
521
+ usePortalEntries(registry).map((entry) => (
335
522
  <HeaderCell key={entry.key} entry={entry} render={render} collection={collection} />
336
523
  ));
337
524
 
525
+ const useSectionHeader = (
526
+ render: ListSectionRenderer<never> | null | undefined,
527
+ collection: Collection,
528
+ size: CellSize,
529
+ ): SectionHeaderSlot => {
530
+ const registry = useHeaderCells(size);
531
+
532
+ if (render == null) {
533
+ return { factoryProps: {}, portals: null };
534
+ }
535
+
536
+ return {
537
+ factoryProps: { headerFactory: <GtkSignalListItemFactory {...registry.handlers} /> },
538
+ portals: <HeaderPortals registry={registry} render={render} collection={collection} />,
539
+ };
540
+ };
541
+
542
+ const useRowProps = (
543
+ rowProps: ListRowPropsResolver<never> | null | undefined,
544
+ collection: Collection,
545
+ expandedIds: string[] | null | undefined,
546
+ ): RowSlot => {
547
+ const registry = useCellRegistry(ROW_OPTIONS, NO_SIZE);
548
+
549
+ if (rowProps == null) {
550
+ return { factoryProps: {}, portals: null };
551
+ }
552
+
553
+ return {
554
+ factoryProps: { rowFactory: <GtkSignalListItemFactory {...registry.handlers} /> },
555
+ portals: (
556
+ <RowPortals registry={registry} rowProps={rowProps} collection={collection} expandedIds={expandedIds} />
557
+ ),
558
+ };
559
+ };
560
+
338
561
  export {
339
562
  useItemCells,
340
- useHeaderCells,
563
+ useRowProps,
564
+ useSectionHeader,
341
565
  ItemPortals,
342
- HeaderPortals,
343
566
  type CellSize,
344
- type CellStore,
345
567
  };