@gtkx/components 2.0.0-beta.1 → 2.0.0-beta.11

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 (79) hide show
  1. package/README.md +8 -165
  2. package/dist/column-view.d.ts.map +1 -1
  3. package/dist/column-view.js +20 -9
  4. package/dist/column-view.js.map +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/internal/cells.d.ts +19 -2
  10. package/dist/internal/cells.d.ts.map +1 -1
  11. package/dist/internal/cells.js +202 -39
  12. package/dist/internal/cells.js.map +1 -1
  13. package/dist/internal/collection-index.d.ts +3 -2
  14. package/dist/internal/collection-index.d.ts.map +1 -1
  15. package/dist/internal/collection-index.js +3 -3
  16. package/dist/internal/collection-index.js.map +1 -1
  17. package/dist/internal/collection-model.d.ts +28 -7
  18. package/dist/internal/collection-model.d.ts.map +1 -1
  19. package/dist/internal/collection-model.js +147 -127
  20. package/dist/internal/collection-model.js.map +1 -1
  21. package/dist/internal/collection.d.ts +5 -4
  22. package/dist/internal/collection.d.ts.map +1 -1
  23. package/dist/internal/collection.js +20 -21
  24. package/dist/internal/collection.js.map +1 -1
  25. package/dist/internal/controlled-sync.d.ts +6 -8
  26. package/dist/internal/controlled-sync.d.ts.map +1 -1
  27. package/dist/internal/controlled-sync.js +3 -3
  28. package/dist/internal/controlled-sync.js.map +1 -1
  29. package/dist/internal/drop-down.d.ts.map +1 -1
  30. package/dist/internal/drop-down.js +43 -44
  31. package/dist/internal/drop-down.js.map +1 -1
  32. package/dist/internal/expansion.js +4 -4
  33. package/dist/internal/expansion.js.map +1 -1
  34. package/dist/internal/selection.d.ts +1 -0
  35. package/dist/internal/selection.d.ts.map +1 -1
  36. package/dist/internal/selection.js +6 -6
  37. package/dist/internal/selection.js.map +1 -1
  38. package/dist/internal/tree-expansion.d.ts.map +1 -1
  39. package/dist/internal/tree-expansion.js +2 -11
  40. package/dist/internal/tree-expansion.js.map +1 -1
  41. package/dist/internal/tree-order.d.ts.map +1 -1
  42. package/dist/internal/tree-order.js +32 -76
  43. package/dist/internal/tree-order.js.map +1 -1
  44. package/dist/internal/use-collection.d.ts +5 -1
  45. package/dist/internal/use-collection.d.ts.map +1 -1
  46. package/dist/internal/use-collection.js +21 -7
  47. package/dist/internal/use-collection.js.map +1 -1
  48. package/dist/list-item-factory.d.ts +10 -0
  49. package/dist/list-item-factory.d.ts.map +1 -0
  50. package/dist/list-item-factory.js +14 -0
  51. package/dist/list-item-factory.js.map +1 -0
  52. package/dist/list-view.d.ts.map +1 -1
  53. package/dist/list-view.js +1 -0
  54. package/dist/list-view.js.map +1 -1
  55. package/dist/toast.d.ts +3 -5
  56. package/dist/toast.d.ts.map +1 -1
  57. package/dist/toast.js +3 -12
  58. package/dist/toast.js.map +1 -1
  59. package/dist/types.d.ts +44 -52
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/types.js.map +1 -1
  62. package/package.json +13 -10
  63. package/src/column-view.tsx +24 -12
  64. package/src/index.ts +3 -2
  65. package/src/internal/cells.tsx +330 -53
  66. package/src/internal/collection-index.ts +8 -6
  67. package/src/internal/collection-model.ts +190 -168
  68. package/src/internal/collection.ts +28 -24
  69. package/src/internal/controlled-sync.ts +9 -12
  70. package/src/internal/drop-down.tsx +64 -60
  71. package/src/internal/expansion.ts +4 -4
  72. package/src/internal/selection.tsx +8 -8
  73. package/src/internal/tree-expansion.ts +2 -14
  74. package/src/internal/tree-order.ts +33 -121
  75. package/src/internal/use-collection.tsx +29 -8
  76. package/src/list-item-factory.tsx +27 -0
  77. package/src/list-view.tsx +1 -0
  78. package/src/toast.tsx +4 -18
  79. package/src/types.ts +48 -52
@@ -1,10 +1,9 @@
1
1
  import type * as GObject from "@gtkx/gi/gobject";
2
2
  import type { ReactElement, ReactNode } from "react";
3
3
  import * as Gtk from "@gtkx/gi/gtk";
4
- import { GtkSignalListItemFactory, GtkTreeExpander } from "@gtkx/jsx/gtk";
4
+ import { GtkBox, GtkSignalListItemFactory, GtkTreeExpander } from "@gtkx/jsx/gtk";
5
5
  import { createPortal, useProperty } from "@gtkx/react";
6
- import { setObjectProperty, t } from "@gtkx/runtime";
7
- import { memo, useLayoutEffect, useState, useSyncExternalStore } from "react";
6
+ import { Fragment, memo, useInsertionEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
8
7
  import type {
9
8
  ExpanderDescriptions,
10
9
  ListItem,
@@ -15,7 +14,8 @@ import type {
15
14
  ListSectionRenderer,
16
15
  } from "../types.js";
17
16
  import type { Collection } from "./collection.js";
18
- import { slotRefFor } from "./collection-model.js";
17
+ import { slotPathAt, type SlotRef, slotRefFor } from "./collection-model.js";
18
+ import { joinParts } from "./keys.js";
19
19
 
20
20
  type CellSize = {
21
21
  width: number;
@@ -34,6 +34,7 @@ type CellHost = FactoryHost & {
34
34
  type CellEntry<H extends FactoryHost> = {
35
35
  key: string;
36
36
  host: H;
37
+ identity: ItemIdentityStore;
37
38
  };
38
39
 
39
40
  type FactoryHandlers = {
@@ -83,19 +84,41 @@ type ItemSlotOptions = {
83
84
  type PositionedHost = Gtk.ListItem | Gtk.ColumnViewRow;
84
85
 
85
86
  type ItemSlot = {
87
+ itemKey: string;
86
88
  item: ListItem;
87
89
  row: Gtk.TreeListRow | null;
88
90
  args: ListItemRenderArgs<unknown>;
89
91
  };
90
92
 
93
+ type CollectionState = {
94
+ collection: Collection;
95
+ expandedIds: string[] | null | undefined;
96
+ };
97
+
98
+ type CollectionStateStore = {
99
+ read: () => CollectionState;
100
+ write: (state: CollectionState) => void;
101
+ };
102
+
103
+ type ItemIdentity = {
104
+ itemKey: string;
105
+ item: ListItem | undefined;
106
+ position: number;
107
+ isExpanded: boolean;
108
+ };
109
+
110
+ type ItemIdentityStore = {
111
+ read: () => ItemIdentity | null;
112
+ write: (identity: ItemIdentity) => void;
113
+ };
114
+
91
115
  type ItemCellProps = {
92
116
  entry: CellEntry<Gtk.ListItem>;
93
117
  render: ListItemRenderer<never>;
94
- collection: Collection;
118
+ state: CollectionStateStore;
95
119
  hasExpander: boolean;
96
- expandedIds: string[] | null | undefined;
97
120
  expanderDescriptions: ExpanderDescriptions | null | undefined;
98
- };
121
+ } & ItemIdentity;
99
122
 
100
123
  type ItemPortalsProps = {
101
124
  registry: CellRegistry<Gtk.ListItem>;
@@ -109,9 +132,8 @@ type ItemPortalsProps = {
109
132
  type ItemRowProps = {
110
133
  entry: CellEntry<Gtk.ColumnViewRow>;
111
134
  rowProps: ListRowPropsResolver<never>;
112
- collection: Collection;
113
- expandedIds: string[] | null | undefined;
114
- };
135
+ state: CollectionStateStore;
136
+ } & ItemIdentity;
115
137
 
116
138
  type RowPortalsProps = {
117
139
  registry: CellRegistry<Gtk.ColumnViewRow>;
@@ -132,6 +154,23 @@ type HeaderPortalsProps = {
132
154
  collection: Collection;
133
155
  };
134
156
 
157
+ type NativeItemKeyStore = {
158
+ keyFor: (item: GObject.Object) => string;
159
+ };
160
+
161
+ type NativeItemCellProps<T extends GObject.Object> = {
162
+ entry: CellEntry<Gtk.ListItem>;
163
+ render: ListItemRenderer<T>;
164
+ keys: NativeItemKeyStore;
165
+ size: CellSize;
166
+ };
167
+
168
+ type NativeItemPortalsProps<T extends GObject.Object> = {
169
+ registry: CellRegistry<Gtk.ListItem>;
170
+ render: ListItemRenderer<T>;
171
+ size: CellSize;
172
+ };
173
+
135
174
  type SectionHeaderSlot = {
136
175
  factoryProps: { headerFactory?: ReactElement };
137
176
  portals: ReactNode;
@@ -142,18 +181,22 @@ type RowSlot = {
142
181
  portals: ReactNode;
143
182
  };
144
183
 
145
- const ItemCell = memo(ItemCellImpl);
146
- const ItemRow = memo(ItemRowImpl);
184
+ const ItemCell = memo(ItemCellImpl, isSameItemCell);
185
+ const ItemRow = memo(ItemRowImpl, isSameItemRow);
147
186
  const HeaderCell = memo(HeaderCellImpl);
148
187
  const CELL_OPTIONS: CellRegistryOptions<Gtk.ListItem> = { isHost: isListItem, prepare: prepareCell };
149
188
  const HEADER_OPTIONS: CellRegistryOptions<Gtk.ListHeader> = { isHost: isListHeader, prepare: prepareCell };
150
189
  const ROW_OPTIONS: CellRegistryOptions<Gtk.ColumnViewRow> = { isHost: isColumnViewRow };
151
190
  const NO_SIZE: CellSize = { width: -1, height: -1 };
152
191
  const NO_ROW_PROPS: ListRowProps = {};
153
- const ROW_TEXT_DESCRIPTOR = t.string("borrowed");
192
+ const placeholders: WeakSet<Gtk.Widget> = new WeakSet();
154
193
 
155
- const placeholder = (size: CellSize): Gtk.Widget =>
156
- new Gtk.Box({ widthRequest: size.width, heightRequest: size.height });
194
+ const placeholder = (size: CellSize): Gtk.Widget => {
195
+ const widget = new Gtk.Box({ widthRequest: size.width, heightRequest: size.height });
196
+ placeholders.add(widget);
197
+
198
+ return widget;
199
+ };
157
200
 
158
201
  function isListItem(value: GObject.Object): value is Gtk.ListItem {
159
202
  return value instanceof Gtk.ListItem;
@@ -168,8 +211,12 @@ function isColumnViewRow(value: GObject.Object): value is Gtk.ColumnViewRow {
168
211
  }
169
212
 
170
213
  function prepareCell(host: CellHost, size: CellSize): void {
171
- if (host.getChild() === null) {
214
+ const child = host.getChild();
215
+
216
+ if (child === null) {
172
217
  host.setChild(placeholder(size));
218
+ } else if (placeholders.has(child)) {
219
+ child.setSizeRequest(size.width, size.height);
173
220
  }
174
221
  }
175
222
 
@@ -184,7 +231,7 @@ function notifyRegistry<H extends FactoryHost>(state: CellRegistryState<H>): voi
184
231
  function createEntry<H extends FactoryHost>(state: CellRegistryState<H>, host: H): CellEntry<H> {
185
232
  state.serial += 1;
186
233
 
187
- return { key: `gtkx-cell-${String(state.serial)}`, host };
234
+ return { key: `gtkx-cell-${String(state.serial)}`, host, identity: createItemIdentityStore() };
188
235
  }
189
236
 
190
237
  function setupHost<H extends FactoryHost>(state: CellRegistryState<H>, host: H): void {
@@ -254,7 +301,15 @@ function createCellRegistry<H extends FactoryHost>(options: CellRegistryOptions<
254
301
  return {
255
302
  handlers: createHandlers(state),
256
303
  setSize: (next) => {
304
+ if (state.size.width === next.width && state.size.height === next.height) {
305
+ return;
306
+ }
307
+
257
308
  state.size = next;
309
+
310
+ for (const host of state.entries.keys()) {
311
+ state.prepare?.(host, next);
312
+ }
258
313
  },
259
314
  subscribe: (onChange) => subscribeRegistry(state, onChange),
260
315
  getEntries: () => getRegistryEntries(state),
@@ -283,9 +338,13 @@ function isRowWanted(options: ItemSlotOptions, item: ListItem): boolean {
283
338
  return options.expandedIds?.includes(item.id) ?? false;
284
339
  }
285
340
 
286
- function itemArgs(item: ListItem, options: ItemSlotOptions): ListItemRenderArgs<unknown> {
287
- const args: ListItemRenderArgs<unknown> = { item: item.value, index: options.position ?? 0 };
288
- const row = options.row;
341
+ function itemArgs(
342
+ item: ListItem,
343
+ position: number | undefined,
344
+ row: Gtk.TreeListRow | null,
345
+ isExpanded: boolean,
346
+ ): ListItemRenderArgs<unknown> {
347
+ const args: ListItemRenderArgs<unknown> = { item: item.value, index: position ?? 0 };
289
348
 
290
349
  if (row === null) {
291
350
  return args;
@@ -294,35 +353,176 @@ function itemArgs(item: ListItem, options: ItemSlotOptions): ListItemRenderArgs<
294
353
  args.depth = row.getDepth();
295
354
 
296
355
  if (row.isExpandable()) {
297
- args.isExpanded = isRowWanted(options, item);
356
+ args.isExpanded = isExpanded;
298
357
  }
299
358
 
300
359
  return args;
301
360
  }
302
361
 
362
+ function keyForItem(ref: SlotRef, item: ListItem): string {
363
+ return joinParts([slotPathAt(ref.store, ref.slot), item.id]);
364
+ }
365
+
303
366
  function slotFor(options: ItemSlotOptions): ItemSlot | null {
304
367
  const ref = slotRefFor(options.item);
305
- const item = ref === null ? undefined : options.collection.itemAt(ref);
368
+
369
+ if (ref === null) {
370
+ return null;
371
+ }
372
+
373
+ const item = options.collection.itemAt(ref);
306
374
 
307
375
  if (item === undefined) {
308
376
  return null;
309
377
  }
310
378
 
311
- return { item, row: options.row, args: itemArgs(item, options) };
379
+ return {
380
+ itemKey: keyForItem(ref, item),
381
+ item,
382
+ row: options.row,
383
+ args: itemArgs(item, options.position, options.row, isRowWanted(options, item)),
384
+ };
312
385
  }
313
386
 
314
387
  function useItemSlot(
315
388
  entry: CellEntry<PositionedHost>,
316
- collection: Collection,
317
- expandedIds: string[] | null | undefined,
389
+ state: CollectionStateStore,
390
+ identity: ItemIdentity,
318
391
  ): ItemSlot | null {
319
392
  const position = useProperty(entry.host, "position");
320
393
  const item = useProperty(entry.host, "item") ?? null;
321
394
  const row = item instanceof Gtk.TreeListRow ? item : null;
322
395
 
396
+ if (position === identity.position) {
397
+ return identity.item === undefined
398
+ ? null
399
+ : {
400
+ itemKey: identity.itemKey,
401
+ item: identity.item,
402
+ row,
403
+ args: itemArgs(identity.item, position, row, identity.isExpanded),
404
+ };
405
+ }
406
+
407
+ const { collection, expandedIds } = state.read();
408
+
323
409
  return slotFor({ item, position, row, collection, expandedIds });
324
410
  }
325
411
 
412
+ function itemIdentity(
413
+ entry: CellEntry<PositionedHost>,
414
+ collection: Collection,
415
+ expandedIds: string[] | null | undefined,
416
+ ): ItemIdentity {
417
+ const ref = slotRefFor(entry.host.getItem());
418
+ const item = ref === null ? undefined : collection.itemAt(ref);
419
+
420
+ return {
421
+ itemKey: ref === null || item === undefined ? "" : keyForItem(ref, item),
422
+ item,
423
+ position: entry.host.getPosition(),
424
+ isExpanded: item !== undefined && (expandedIds?.includes(item.id) ?? false),
425
+ };
426
+ }
427
+
428
+ function isSameItemIdentity(previous: ItemIdentity, next: ItemIdentity): boolean {
429
+ return (
430
+ previous.item === next.item &&
431
+ previous.itemKey === next.itemKey &&
432
+ previous.position === next.position &&
433
+ previous.isExpanded === next.isExpanded
434
+ );
435
+ }
436
+
437
+ function isSameItemCell(previous: ItemCellProps, next: ItemCellProps): boolean {
438
+ const identity = previous.entry.identity.read() ?? previous;
439
+
440
+ return (
441
+ previous.entry === next.entry &&
442
+ previous.render === next.render &&
443
+ previous.hasExpander === next.hasExpander &&
444
+ previous.expanderDescriptions === next.expanderDescriptions &&
445
+ isSameItemIdentity(identity, next)
446
+ );
447
+ }
448
+
449
+ function isSameItemRow(previous: ItemRowProps, next: ItemRowProps): boolean {
450
+ const identity = previous.entry.identity.read() ?? previous;
451
+
452
+ return previous.entry === next.entry && previous.rowProps === next.rowProps && isSameItemIdentity(identity, next);
453
+ }
454
+
455
+ function createItemIdentityStore(): ItemIdentityStore {
456
+ let current: ItemIdentity | null = null;
457
+
458
+ return {
459
+ read: () => current,
460
+ write: (identity) => {
461
+ current = identity;
462
+ },
463
+ };
464
+ }
465
+
466
+ function createNativeItemKeyStore(): NativeItemKeyStore {
467
+ const keys: WeakMap<GObject.Object, string> = new WeakMap();
468
+ let serial = 0;
469
+
470
+ return {
471
+ keyFor: (item) => {
472
+ const current = keys.get(item);
473
+
474
+ if (current !== undefined) {
475
+ return current;
476
+ }
477
+
478
+ serial += 1;
479
+ const key = `gtkx-native-item-${String(serial)}`;
480
+ keys.set(item, key);
481
+
482
+ return key;
483
+ },
484
+ };
485
+ }
486
+
487
+ function renderedIdentity(entry: CellEntry<PositionedHost>, slot: ItemSlot | null): ItemIdentity {
488
+ return {
489
+ itemKey: slot?.itemKey ?? "",
490
+ item: slot?.item,
491
+ position: slot?.args.index ?? entry.host.getPosition(),
492
+ isExpanded: slot?.args.isExpanded ?? false,
493
+ };
494
+ }
495
+
496
+ function useRememberIdentity(entry: CellEntry<PositionedHost>, slot: ItemSlot | null): void {
497
+ useLayoutEffect(() => {
498
+ entry.identity.write(renderedIdentity(entry, slot));
499
+ });
500
+ }
501
+
502
+ function createCollectionStateStore(initial: CollectionState): CollectionStateStore {
503
+ let current = initial;
504
+
505
+ return {
506
+ read: () => current,
507
+ write: (state) => {
508
+ current = state;
509
+ },
510
+ };
511
+ }
512
+
513
+ function useCollectionState(
514
+ collection: Collection,
515
+ expandedIds: string[] | null | undefined,
516
+ ): CollectionStateStore {
517
+ const [state] = useState(() => createCollectionStateStore({ collection, expandedIds }));
518
+
519
+ useInsertionEffect(() => {
520
+ state.write({ collection, expandedIds });
521
+ }, [state, collection, expandedIds]);
522
+
523
+ return state;
524
+ }
525
+
326
526
  function expanderDescriptionFor(
327
527
  slot: ItemSlot,
328
528
  descriptions: ExpanderDescriptions | null | undefined,
@@ -376,7 +576,9 @@ function itemBody(
376
576
  const renderItem = render as ListItemRenderer<unknown>;
377
577
  const content = renderItem(slot.args);
378
578
 
379
- return hasExpander ? wrapExpander(slot, content, descriptions) : content;
579
+ return (
580
+ <Fragment key={slot.itemKey}>{hasExpander ? wrapExpander(slot, content, descriptions) : content}</Fragment>
581
+ );
380
582
  }
381
583
 
382
584
  function rowText(value: string | undefined): string | null {
@@ -401,52 +603,109 @@ function rowPropsFor(slot: ItemSlot | null, rowProps: ListRowPropsResolver<never
401
603
  };
402
604
  }
403
605
 
404
- function setRowText(host: Gtk.ColumnViewRow, name: string, value: string | null): void {
405
- setObjectProperty(host, name, ROW_TEXT_DESCRIPTOR, value);
406
- }
606
+ function applyRowProps(
607
+ host: Gtk.ColumnViewRow,
608
+ props: ResolvedRowProps,
609
+ previous: ResolvedRowProps | null,
610
+ ): void {
611
+ if (previous?.accessibleLabel !== props.accessibleLabel) {
612
+ host.accessibleLabel = props.accessibleLabel;
613
+ }
407
614
 
408
- function applyRowProps(host: Gtk.ColumnViewRow, props: ResolvedRowProps): void {
409
- setRowText(host, "accessible-label", props.accessibleLabel);
410
- setRowText(host, "accessible-description", props.accessibleDescription);
411
- host.setActivatable(props.isActivatable);
412
- host.setFocusable(props.isFocusable);
413
- host.setSelectable(props.isSelectable);
615
+ if (previous?.accessibleDescription !== props.accessibleDescription) {
616
+ host.accessibleDescription = props.accessibleDescription;
617
+ }
618
+
619
+ if (previous?.isActivatable !== props.isActivatable) {
620
+ host.setActivatable(props.isActivatable);
621
+ }
622
+
623
+ if (previous?.isFocusable !== props.isFocusable) {
624
+ host.setFocusable(props.isFocusable);
625
+ }
626
+
627
+ if (previous?.isSelectable !== props.isSelectable) {
628
+ host.setSelectable(props.isSelectable);
629
+ }
414
630
  }
415
631
 
416
632
  function ItemCellImpl({
417
633
  entry,
418
634
  render,
419
- collection,
635
+ state,
420
636
  hasExpander,
421
- expandedIds,
422
637
  expanderDescriptions,
638
+ ...identity
423
639
  }: ItemCellProps): ReactNode {
424
- const slot = useItemSlot(entry, collection, expandedIds);
640
+ const slot = useItemSlot(entry, state, identity);
641
+ useRememberIdentity(entry, slot);
425
642
  const body = itemBody(slot, render, hasExpander, expanderDescriptions);
426
643
 
427
644
  return createPortal(body, entry.host, entry.key);
428
645
  }
429
646
 
430
- function ItemRowImpl({ entry, rowProps, collection, expandedIds }: ItemRowProps): ReactNode {
431
- const slot = useItemSlot(entry, collection, expandedIds);
647
+ function ItemRowImpl({ entry, rowProps, state, ...identity }: ItemRowProps): ReactNode {
648
+ const slot = useItemSlot(entry, state, identity);
649
+ useRememberIdentity(entry, slot);
432
650
  const props = rowPropsFor(slot, rowProps);
651
+ const previous = useRef<ResolvedRowProps | null>(null);
433
652
 
434
653
  useLayoutEffect(() => {
435
- applyRowProps(entry.host, props);
436
- });
654
+ applyRowProps(entry.host, props, previous.current);
655
+ previous.current = props;
656
+ }, [entry.host, props]);
437
657
 
438
658
  return null;
439
659
  }
440
660
 
441
661
  function HeaderCellImpl({ entry, render, collection }: HeaderCellProps): ReactNode {
442
662
  const item = useProperty(entry.host, "item") ?? null;
443
- const ref = slotRefFor(item);
444
- const renderHeader = render as ListSectionRenderer<unknown>;
445
- const body = ref === null ? null : renderHeader({ section: collection.sectionFor(ref.store.level.path) });
663
+
664
+ return createPortal(headerBody(slotRefFor(item), render, collection), entry.host, entry.key);
665
+ }
666
+
667
+ function NativeItemCell<T extends GObject.Object>({
668
+ entry,
669
+ render,
670
+ keys,
671
+ size,
672
+ }: NativeItemCellProps<T>): ReactNode {
673
+ const position = useProperty(entry.host, "position");
674
+ const item = useProperty(entry.host, "item") ?? null;
675
+ let body: ReactNode = null;
676
+
677
+ if (item !== null && position !== undefined) {
678
+ const content = render({ item: item as T, index: position });
679
+ const child = content == null || typeof content === "boolean"
680
+ ? <GtkBox widthRequest={size.width} heightRequest={size.height} />
681
+ : content;
682
+ body = <Fragment key={keys.keyFor(item)}>{child}</Fragment>;
683
+ }
446
684
 
447
685
  return createPortal(body, entry.host, entry.key);
448
686
  }
449
687
 
688
+ function headerBody(
689
+ ref: SlotRef | null,
690
+ render: ListSectionRenderer<never>,
691
+ collection: Collection,
692
+ ): ReactNode {
693
+ if (ref === null) {
694
+ return null;
695
+ }
696
+
697
+ const section = collection.sectionFor(ref.store.level.path);
698
+
699
+ if (section === undefined) {
700
+ return null;
701
+ }
702
+
703
+ const renderHeader = render as ListSectionRenderer<unknown>;
704
+ const key = joinParts([ref.store.level.path, section.id]);
705
+
706
+ return <Fragment key={key}>{renderHeader({ section: section.value })}</Fragment>;
707
+ }
708
+
450
709
  function usePortalEntries<H extends FactoryHost>(registry: CellRegistry<H>): CellEntry<H>[] {
451
710
  return useSyncExternalStore(registry.subscribe, registry.getEntries);
452
711
  }
@@ -458,35 +717,52 @@ const ItemPortals = ({
458
717
  hasExpander,
459
718
  expandedIds,
460
719
  expanderDescriptions,
461
- }: ItemPortalsProps): ReactNode =>
462
- usePortalEntries(registry).map((entry) => (
720
+ }: ItemPortalsProps): ReactNode => {
721
+ const entries = usePortalEntries(registry);
722
+ const state = useCollectionState(collection, expandedIds);
723
+
724
+ return entries.map((entry) => (
463
725
  <ItemCell
464
726
  key={entry.key}
465
727
  entry={entry}
466
728
  render={render}
467
- collection={collection}
729
+ state={state}
468
730
  hasExpander={hasExpander ?? true}
469
- expandedIds={expandedIds}
470
731
  expanderDescriptions={expanderDescriptions}
732
+ {...itemIdentity(entry, collection, expandedIds)}
471
733
  />
472
734
  ));
735
+ };
473
736
 
474
- const RowPortals = ({ registry, rowProps, collection, expandedIds }: RowPortalsProps): ReactNode =>
475
- usePortalEntries(registry).map((entry) => (
737
+ const RowPortals = ({ registry, rowProps, collection, expandedIds }: RowPortalsProps): ReactNode => {
738
+ const entries = usePortalEntries(registry);
739
+ const state = useCollectionState(collection, expandedIds);
740
+
741
+ return entries.map((entry) => (
476
742
  <ItemRow
477
743
  key={entry.key}
478
744
  entry={entry}
479
745
  rowProps={rowProps}
480
- collection={collection}
481
- expandedIds={expandedIds}
746
+ state={state}
747
+ {...itemIdentity(entry, collection, expandedIds)}
482
748
  />
483
749
  ));
750
+ };
484
751
 
485
752
  const HeaderPortals = ({ registry, render, collection }: HeaderPortalsProps): ReactNode =>
486
753
  usePortalEntries(registry).map((entry) => (
487
754
  <HeaderCell key={entry.key} entry={entry} render={render} collection={collection} />
488
755
  ));
489
756
 
757
+ function NativeItemPortals<T extends GObject.Object>({ registry, render, size }: NativeItemPortalsProps<T>): ReactNode {
758
+ const entries = usePortalEntries(registry);
759
+ const [keys] = useState(createNativeItemKeyStore);
760
+
761
+ return entries.map((entry) => (
762
+ <NativeItemCell key={entry.key} entry={entry} render={render} keys={keys} size={size} />
763
+ ));
764
+ }
765
+
490
766
  const useSectionHeader = (
491
767
  render: ListSectionRenderer<never> | null | undefined,
492
768
  collection: Collection,
@@ -528,5 +804,6 @@ export {
528
804
  useRowProps,
529
805
  useSectionHeader,
530
806
  ItemPortals,
807
+ NativeItemPortals,
531
808
  type CellSize,
532
809
  };
@@ -7,20 +7,22 @@ type Level = {
7
7
  expandableFlags: boolean[];
8
8
  };
9
9
 
10
+ type SectionIdentity = Pick<ListSection, "id" | "value">;
11
+
10
12
  type CollectionIndex = {
11
13
  isTree: boolean;
12
14
  groups: Level[];
13
15
  childLevel: (level: Level, slot: number) => Level | undefined;
14
16
  levelFor: (levelPath: string) => Level | undefined;
15
17
  itemAt: (levelPath: string, slot: number) => ListItem | undefined;
16
- sectionFor: (levelPath: string) => unknown;
18
+ sectionFor: (levelPath: string) => SectionIdentity | undefined;
17
19
  };
18
20
 
19
21
  type IndexState = {
20
22
  isTree: boolean;
21
23
  groups: Level[];
22
24
  levels: Map<string, Level>;
23
- sectionValues: Map<string, unknown>;
25
+ sections: Map<string, SectionIdentity>;
24
26
  };
25
27
 
26
28
  type ParentItem = ListItem & { children: ListItem[] };
@@ -96,7 +98,7 @@ function buildGroups(state: IndexState, source: ListItem[], sections: ListSectio
96
98
 
97
99
  return sections.map((section, group) => {
98
100
  const path = encodePart(String(group));
99
- state.sectionValues.set(path, section.value);
101
+ state.sections.set(path, { id: section.id, value: section.value });
100
102
 
101
103
  return newLevel(state, path, section.data);
102
104
  });
@@ -125,7 +127,7 @@ function createCollectionIndex(
125
127
  isTree: isTreeSource(source, sections, isFlat),
126
128
  groups: [],
127
129
  levels: new Map(),
128
- sectionValues: new Map(),
130
+ sections: new Map(),
129
131
  };
130
132
 
131
133
  state.groups = buildGroups(state, source, sections);
@@ -136,8 +138,8 @@ function createCollectionIndex(
136
138
  childLevel: (level, slot) => childLevel(state, level, slot),
137
139
  levelFor: (levelPath) => levelFor(state, levelPath),
138
140
  itemAt: (levelPath, slot) => levelFor(state, levelPath)?.items[slot],
139
- sectionFor: (levelPath) => state.sectionValues.get(levelPath),
141
+ sectionFor: (levelPath) => state.sections.get(levelPath),
140
142
  };
141
143
  }
142
144
 
143
- export { createCollectionIndex, type CollectionIndex, type Level };
145
+ export { createCollectionIndex, type CollectionIndex, type Level, type SectionIdentity };