@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.
- package/README.md +8 -165
- package/dist/column-view.d.ts.map +1 -1
- package/dist/column-view.js +20 -9
- package/dist/column-view.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +19 -2
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +202 -39
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/collection-index.d.ts +3 -2
- package/dist/internal/collection-index.d.ts.map +1 -1
- package/dist/internal/collection-index.js +3 -3
- package/dist/internal/collection-index.js.map +1 -1
- package/dist/internal/collection-model.d.ts +28 -7
- package/dist/internal/collection-model.d.ts.map +1 -1
- package/dist/internal/collection-model.js +147 -127
- package/dist/internal/collection-model.js.map +1 -1
- package/dist/internal/collection.d.ts +5 -4
- package/dist/internal/collection.d.ts.map +1 -1
- package/dist/internal/collection.js +20 -21
- package/dist/internal/collection.js.map +1 -1
- package/dist/internal/controlled-sync.d.ts +6 -8
- package/dist/internal/controlled-sync.d.ts.map +1 -1
- package/dist/internal/controlled-sync.js +3 -3
- package/dist/internal/controlled-sync.js.map +1 -1
- package/dist/internal/drop-down.d.ts.map +1 -1
- package/dist/internal/drop-down.js +43 -44
- package/dist/internal/drop-down.js.map +1 -1
- package/dist/internal/expansion.js +4 -4
- package/dist/internal/expansion.js.map +1 -1
- package/dist/internal/selection.d.ts +1 -0
- package/dist/internal/selection.d.ts.map +1 -1
- package/dist/internal/selection.js +6 -6
- package/dist/internal/selection.js.map +1 -1
- package/dist/internal/tree-expansion.d.ts.map +1 -1
- package/dist/internal/tree-expansion.js +2 -11
- package/dist/internal/tree-expansion.js.map +1 -1
- package/dist/internal/tree-order.d.ts.map +1 -1
- package/dist/internal/tree-order.js +32 -76
- package/dist/internal/tree-order.js.map +1 -1
- package/dist/internal/use-collection.d.ts +5 -1
- package/dist/internal/use-collection.d.ts.map +1 -1
- package/dist/internal/use-collection.js +21 -7
- package/dist/internal/use-collection.js.map +1 -1
- package/dist/list-item-factory.d.ts +10 -0
- package/dist/list-item-factory.d.ts.map +1 -0
- package/dist/list-item-factory.js +14 -0
- package/dist/list-item-factory.js.map +1 -0
- package/dist/list-view.d.ts.map +1 -1
- package/dist/list-view.js +1 -0
- package/dist/list-view.js.map +1 -1
- package/dist/toast.d.ts +3 -5
- package/dist/toast.d.ts.map +1 -1
- package/dist/toast.js +3 -12
- package/dist/toast.js.map +1 -1
- package/dist/types.d.ts +44 -52
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +13 -10
- package/src/column-view.tsx +24 -12
- package/src/index.ts +3 -2
- package/src/internal/cells.tsx +330 -53
- package/src/internal/collection-index.ts +8 -6
- package/src/internal/collection-model.ts +190 -168
- package/src/internal/collection.ts +28 -24
- package/src/internal/controlled-sync.ts +9 -12
- package/src/internal/drop-down.tsx +64 -60
- package/src/internal/expansion.ts +4 -4
- package/src/internal/selection.tsx +8 -8
- package/src/internal/tree-expansion.ts +2 -14
- package/src/internal/tree-order.ts +33 -121
- package/src/internal/use-collection.tsx +29 -8
- package/src/list-item-factory.tsx +27 -0
- package/src/list-view.tsx +1 -0
- package/src/toast.tsx +4 -18
- package/src/types.ts +48 -52
package/src/internal/cells.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
287
|
-
|
|
288
|
-
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
317
|
-
|
|
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
|
|
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
|
|
405
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
635
|
+
state,
|
|
420
636
|
hasExpander,
|
|
421
|
-
expandedIds,
|
|
422
637
|
expanderDescriptions,
|
|
638
|
+
...identity
|
|
423
639
|
}: ItemCellProps): ReactNode {
|
|
424
|
-
const slot = useItemSlot(entry,
|
|
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,
|
|
431
|
-
const slot = useItemSlot(entry,
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
481
|
-
|
|
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) =>
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 };
|