@gtkx/components 1.6.0 → 2.0.0-beta.10
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 +47 -44
- package/dist/column-view.d.ts.map +1 -1
- package/dist/column-view.js +1 -0
- package/dist/column-view.js.map +1 -1
- package/dist/combo-row.d.ts.map +1 -0
- package/dist/{adw/combo-row.js → combo-row.js} +1 -1
- package/dist/combo-row.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +11 -1
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +124 -26
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/collection-index.d.ts +1 -2
- package/dist/internal/collection-index.d.ts.map +1 -1
- package/dist/internal/collection-index.js +1 -2
- package/dist/internal/collection-index.js.map +1 -1
- package/dist/internal/collection-model.d.ts +5 -5
- package/dist/internal/collection-model.d.ts.map +1 -1
- package/dist/internal/collection-model.js +38 -43
- package/dist/internal/collection-model.js.map +1 -1
- package/dist/internal/expansion.d.ts.map +1 -1
- package/dist/internal/expansion.js +2 -8
- package/dist/internal/expansion.js.map +1 -1
- package/dist/internal/slots.d.ts.map +1 -1
- package/dist/internal/slots.js +1 -6
- package/dist/internal/slots.js.map +1 -1
- package/dist/internal/tree-expansion.js +2 -2
- package/dist/internal/tree-expansion.js.map +1 -1
- package/dist/internal/tree-order.d.ts +1 -2
- package/dist/internal/tree-order.d.ts.map +1 -1
- package/dist/internal/tree-order.js +1 -4
- package/dist/internal/tree-order.js.map +1 -1
- package/dist/internal/use-widget-ref.d.ts +2 -2
- package/dist/internal/use-widget-ref.d.ts.map +1 -1
- package/dist/internal/use-widget-ref.js.map +1 -1
- 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.map +1 -0
- package/dist/toast.js.map +1 -0
- package/dist/types.d.ts +45 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +13 -14
- package/src/column-view.tsx +1 -0
- package/src/{adw/combo-row.tsx → combo-row.tsx} +2 -2
- package/src/index.ts +7 -0
- package/src/internal/cells.tsx +200 -40
- package/src/internal/collection-index.ts +1 -2
- package/src/internal/collection-model.ts +44 -50
- package/src/internal/expansion.ts +2 -10
- package/src/internal/slots.ts +1 -9
- package/src/internal/tree-expansion.ts +2 -2
- package/src/internal/tree-order.ts +0 -5
- package/src/internal/use-widget-ref.ts +2 -2
- package/src/list-view.tsx +1 -0
- package/src/types.ts +55 -3
- package/dist/adw/combo-row.d.ts.map +0 -1
- package/dist/adw/combo-row.js.map +0 -1
- package/dist/adw/index.d.ts +0 -4
- package/dist/adw/index.d.ts.map +0 -1
- package/dist/adw/index.js +0 -3
- package/dist/adw/index.js.map +0 -1
- package/dist/adw/toast.d.ts.map +0 -1
- package/dist/adw/toast.js.map +0 -1
- package/dist/adw/types.d.ts +0 -36
- package/dist/adw/types.d.ts.map +0 -1
- package/dist/adw/types.js +0 -2
- package/dist/adw/types.js.map +0 -1
- package/src/adw/index.ts +0 -9
- package/src/adw/types.ts +0 -46
- /package/dist/{adw/combo-row.d.ts → combo-row.d.ts} +0 -0
- /package/dist/{adw/toast.d.ts → toast.d.ts} +0 -0
- /package/dist/{adw/toast.js → toast.js} +0 -0
- /package/src/{adw/toast.tsx → toast.tsx} +0 -0
package/src/internal/cells.tsx
CHANGED
|
@@ -3,8 +3,8 @@ import type { ReactElement, ReactNode } from "react";
|
|
|
3
3
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
4
4
|
import { 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 { setProperty, t } from "@gtkx/runtime";
|
|
7
|
+
import { memo, useInsertionEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
8
8
|
import type {
|
|
9
9
|
ExpanderDescriptions,
|
|
10
10
|
ListItem,
|
|
@@ -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 = {
|
|
@@ -88,14 +89,34 @@ type ItemSlot = {
|
|
|
88
89
|
args: ListItemRenderArgs<unknown>;
|
|
89
90
|
};
|
|
90
91
|
|
|
92
|
+
type CollectionState = {
|
|
93
|
+
collection: Collection;
|
|
94
|
+
expandedIds: string[] | null | undefined;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type CollectionStateStore = {
|
|
98
|
+
read: () => CollectionState;
|
|
99
|
+
write: (state: CollectionState) => void;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
type ItemIdentity = {
|
|
103
|
+
item: ListItem | undefined;
|
|
104
|
+
position: number;
|
|
105
|
+
isExpanded: boolean;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
type ItemIdentityStore = {
|
|
109
|
+
read: () => ItemIdentity | null;
|
|
110
|
+
write: (identity: ItemIdentity) => void;
|
|
111
|
+
};
|
|
112
|
+
|
|
91
113
|
type ItemCellProps = {
|
|
92
114
|
entry: CellEntry<Gtk.ListItem>;
|
|
93
115
|
render: ListItemRenderer<never>;
|
|
94
|
-
|
|
116
|
+
state: CollectionStateStore;
|
|
95
117
|
hasExpander: boolean;
|
|
96
|
-
expandedIds: string[] | null | undefined;
|
|
97
118
|
expanderDescriptions: ExpanderDescriptions | null | undefined;
|
|
98
|
-
};
|
|
119
|
+
} & ItemIdentity;
|
|
99
120
|
|
|
100
121
|
type ItemPortalsProps = {
|
|
101
122
|
registry: CellRegistry<Gtk.ListItem>;
|
|
@@ -109,9 +130,8 @@ type ItemPortalsProps = {
|
|
|
109
130
|
type ItemRowProps = {
|
|
110
131
|
entry: CellEntry<Gtk.ColumnViewRow>;
|
|
111
132
|
rowProps: ListRowPropsResolver<never>;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
};
|
|
133
|
+
state: CollectionStateStore;
|
|
134
|
+
} & ItemIdentity;
|
|
115
135
|
|
|
116
136
|
type RowPortalsProps = {
|
|
117
137
|
registry: CellRegistry<Gtk.ColumnViewRow>;
|
|
@@ -142,8 +162,8 @@ type RowSlot = {
|
|
|
142
162
|
portals: ReactNode;
|
|
143
163
|
};
|
|
144
164
|
|
|
145
|
-
const ItemCell = memo(ItemCellImpl);
|
|
146
|
-
const ItemRow = memo(ItemRowImpl);
|
|
165
|
+
const ItemCell = memo(ItemCellImpl, isSameItemCell);
|
|
166
|
+
const ItemRow = memo(ItemRowImpl, isSameItemRow);
|
|
147
167
|
const HeaderCell = memo(HeaderCellImpl);
|
|
148
168
|
const CELL_OPTIONS: CellRegistryOptions<Gtk.ListItem> = { isHost: isListItem, prepare: prepareCell };
|
|
149
169
|
const HEADER_OPTIONS: CellRegistryOptions<Gtk.ListHeader> = { isHost: isListHeader, prepare: prepareCell };
|
|
@@ -184,7 +204,7 @@ function notifyRegistry<H extends FactoryHost>(state: CellRegistryState<H>): voi
|
|
|
184
204
|
function createEntry<H extends FactoryHost>(state: CellRegistryState<H>, host: H): CellEntry<H> {
|
|
185
205
|
state.serial += 1;
|
|
186
206
|
|
|
187
|
-
return { key: `gtkx-cell-${String(state.serial)}`, host };
|
|
207
|
+
return { key: `gtkx-cell-${String(state.serial)}`, host, identity: createItemIdentityStore() };
|
|
188
208
|
}
|
|
189
209
|
|
|
190
210
|
function setupHost<H extends FactoryHost>(state: CellRegistryState<H>, host: H): void {
|
|
@@ -283,9 +303,13 @@ function isRowWanted(options: ItemSlotOptions, item: ListItem): boolean {
|
|
|
283
303
|
return options.expandedIds?.includes(item.id) ?? false;
|
|
284
304
|
}
|
|
285
305
|
|
|
286
|
-
function itemArgs(
|
|
287
|
-
|
|
288
|
-
|
|
306
|
+
function itemArgs(
|
|
307
|
+
item: ListItem,
|
|
308
|
+
position: number | undefined,
|
|
309
|
+
row: Gtk.TreeListRow | null,
|
|
310
|
+
isExpanded: boolean,
|
|
311
|
+
): ListItemRenderArgs<unknown> {
|
|
312
|
+
const args: ListItemRenderArgs<unknown> = { item: item.value, index: position ?? 0 };
|
|
289
313
|
|
|
290
314
|
if (row === null) {
|
|
291
315
|
return args;
|
|
@@ -294,7 +318,7 @@ function itemArgs(item: ListItem, options: ItemSlotOptions): ListItemRenderArgs<
|
|
|
294
318
|
args.depth = row.getDepth();
|
|
295
319
|
|
|
296
320
|
if (row.isExpandable()) {
|
|
297
|
-
args.isExpanded =
|
|
321
|
+
args.isExpanded = isExpanded;
|
|
298
322
|
}
|
|
299
323
|
|
|
300
324
|
return args;
|
|
@@ -308,21 +332,127 @@ function slotFor(options: ItemSlotOptions): ItemSlot | null {
|
|
|
308
332
|
return null;
|
|
309
333
|
}
|
|
310
334
|
|
|
311
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
item,
|
|
337
|
+
row: options.row,
|
|
338
|
+
args: itemArgs(item, options.position, options.row, isRowWanted(options, item)),
|
|
339
|
+
};
|
|
312
340
|
}
|
|
313
341
|
|
|
314
342
|
function useItemSlot(
|
|
315
343
|
entry: CellEntry<PositionedHost>,
|
|
316
|
-
|
|
317
|
-
|
|
344
|
+
state: CollectionStateStore,
|
|
345
|
+
identity: ItemIdentity,
|
|
318
346
|
): ItemSlot | null {
|
|
319
347
|
const position = useProperty(entry.host, "position");
|
|
320
348
|
const item = useProperty(entry.host, "item") ?? null;
|
|
321
349
|
const row = item instanceof Gtk.TreeListRow ? item : null;
|
|
322
350
|
|
|
351
|
+
if (position === identity.position) {
|
|
352
|
+
return identity.item === undefined
|
|
353
|
+
? null
|
|
354
|
+
: {
|
|
355
|
+
item: identity.item,
|
|
356
|
+
row,
|
|
357
|
+
args: itemArgs(identity.item, position, row, identity.isExpanded),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const { collection, expandedIds } = state.read();
|
|
362
|
+
|
|
323
363
|
return slotFor({ item, position, row, collection, expandedIds });
|
|
324
364
|
}
|
|
325
365
|
|
|
366
|
+
function itemIdentity(
|
|
367
|
+
entry: CellEntry<PositionedHost>,
|
|
368
|
+
collection: Collection,
|
|
369
|
+
expandedIds: string[] | null | undefined,
|
|
370
|
+
): ItemIdentity {
|
|
371
|
+
const ref = slotRefFor(entry.host.getItem());
|
|
372
|
+
const item = ref === null ? undefined : collection.itemAt(ref);
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
item,
|
|
376
|
+
position: entry.host.getPosition(),
|
|
377
|
+
isExpanded: item === undefined ? false : (expandedIds?.includes(item.id) ?? false),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isSameItemIdentity(previous: ItemIdentity, next: ItemIdentity): boolean {
|
|
382
|
+
return (
|
|
383
|
+
previous.item === next.item &&
|
|
384
|
+
previous.position === next.position &&
|
|
385
|
+
previous.isExpanded === next.isExpanded
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function isSameItemCell(previous: ItemCellProps, next: ItemCellProps): boolean {
|
|
390
|
+
const identity = previous.entry.identity.read() ?? previous;
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
previous.entry === next.entry &&
|
|
394
|
+
previous.render === next.render &&
|
|
395
|
+
previous.hasExpander === next.hasExpander &&
|
|
396
|
+
previous.expanderDescriptions === next.expanderDescriptions &&
|
|
397
|
+
isSameItemIdentity(identity, next)
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function isSameItemRow(previous: ItemRowProps, next: ItemRowProps): boolean {
|
|
402
|
+
const identity = previous.entry.identity.read() ?? previous;
|
|
403
|
+
|
|
404
|
+
return previous.entry === next.entry && previous.rowProps === next.rowProps && isSameItemIdentity(identity, next);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function createItemIdentityStore(): ItemIdentityStore {
|
|
408
|
+
let current: ItemIdentity | null = null;
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
read: () => current,
|
|
412
|
+
write: (identity) => {
|
|
413
|
+
current = identity;
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function renderedIdentity(entry: CellEntry<PositionedHost>, slot: ItemSlot | null): ItemIdentity {
|
|
419
|
+
return {
|
|
420
|
+
item: slot?.item,
|
|
421
|
+
position: slot?.args.index ?? entry.host.getPosition(),
|
|
422
|
+
isExpanded: slot?.args.isExpanded ?? false,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function useRememberIdentity(entry: CellEntry<PositionedHost>, slot: ItemSlot | null): void {
|
|
427
|
+
useLayoutEffect(() => {
|
|
428
|
+
entry.identity.write(renderedIdentity(entry, slot));
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function createCollectionStateStore(initial: CollectionState): CollectionStateStore {
|
|
433
|
+
let current = initial;
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
read: () => current,
|
|
437
|
+
write: (state) => {
|
|
438
|
+
current = state;
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function useCollectionState(
|
|
444
|
+
collection: Collection,
|
|
445
|
+
expandedIds: string[] | null | undefined,
|
|
446
|
+
): CollectionStateStore {
|
|
447
|
+
const [state] = useState(() => createCollectionStateStore({ collection, expandedIds }));
|
|
448
|
+
|
|
449
|
+
useInsertionEffect(() => {
|
|
450
|
+
state.write({ collection, expandedIds });
|
|
451
|
+
}, [state, collection, expandedIds]);
|
|
452
|
+
|
|
453
|
+
return state;
|
|
454
|
+
}
|
|
455
|
+
|
|
326
456
|
function expanderDescriptionFor(
|
|
327
457
|
slot: ItemSlot,
|
|
328
458
|
descriptions: ExpanderDescriptions | null | undefined,
|
|
@@ -402,38 +532,60 @@ function rowPropsFor(slot: ItemSlot | null, rowProps: ListRowPropsResolver<never
|
|
|
402
532
|
}
|
|
403
533
|
|
|
404
534
|
function setRowText(host: Gtk.ColumnViewRow, name: string, value: string | null): void {
|
|
405
|
-
|
|
535
|
+
setProperty(host, name, ROW_TEXT_DESCRIPTOR, value);
|
|
406
536
|
}
|
|
407
537
|
|
|
408
|
-
function applyRowProps(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
538
|
+
function applyRowProps(
|
|
539
|
+
host: Gtk.ColumnViewRow,
|
|
540
|
+
props: ResolvedRowProps,
|
|
541
|
+
previous: ResolvedRowProps | null,
|
|
542
|
+
): void {
|
|
543
|
+
if (previous?.accessibleLabel !== props.accessibleLabel) {
|
|
544
|
+
setRowText(host, "accessible-label", props.accessibleLabel);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (previous?.accessibleDescription !== props.accessibleDescription) {
|
|
548
|
+
setRowText(host, "accessible-description", props.accessibleDescription);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (previous?.isActivatable !== props.isActivatable) {
|
|
552
|
+
host.setActivatable(props.isActivatable);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (previous?.isFocusable !== props.isFocusable) {
|
|
556
|
+
host.setFocusable(props.isFocusable);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (previous?.isSelectable !== props.isSelectable) {
|
|
560
|
+
host.setSelectable(props.isSelectable);
|
|
561
|
+
}
|
|
414
562
|
}
|
|
415
563
|
|
|
416
564
|
function ItemCellImpl({
|
|
417
565
|
entry,
|
|
418
566
|
render,
|
|
419
|
-
|
|
567
|
+
state,
|
|
420
568
|
hasExpander,
|
|
421
|
-
expandedIds,
|
|
422
569
|
expanderDescriptions,
|
|
570
|
+
...identity
|
|
423
571
|
}: ItemCellProps): ReactNode {
|
|
424
|
-
const slot = useItemSlot(entry,
|
|
572
|
+
const slot = useItemSlot(entry, state, identity);
|
|
573
|
+
useRememberIdentity(entry, slot);
|
|
425
574
|
const body = itemBody(slot, render, hasExpander, expanderDescriptions);
|
|
426
575
|
|
|
427
576
|
return createPortal(body, entry.host, entry.key);
|
|
428
577
|
}
|
|
429
578
|
|
|
430
|
-
function ItemRowImpl({ entry, rowProps,
|
|
431
|
-
const slot = useItemSlot(entry,
|
|
579
|
+
function ItemRowImpl({ entry, rowProps, state, ...identity }: ItemRowProps): ReactNode {
|
|
580
|
+
const slot = useItemSlot(entry, state, identity);
|
|
581
|
+
useRememberIdentity(entry, slot);
|
|
432
582
|
const props = rowPropsFor(slot, rowProps);
|
|
583
|
+
const previous = useRef<ResolvedRowProps | null>(null);
|
|
433
584
|
|
|
434
585
|
useLayoutEffect(() => {
|
|
435
|
-
applyRowProps(entry.host, props);
|
|
436
|
-
|
|
586
|
+
applyRowProps(entry.host, props, previous.current);
|
|
587
|
+
previous.current = props;
|
|
588
|
+
}, [entry.host, props]);
|
|
437
589
|
|
|
438
590
|
return null;
|
|
439
591
|
}
|
|
@@ -458,29 +610,37 @@ const ItemPortals = ({
|
|
|
458
610
|
hasExpander,
|
|
459
611
|
expandedIds,
|
|
460
612
|
expanderDescriptions,
|
|
461
|
-
}: ItemPortalsProps): ReactNode =>
|
|
462
|
-
usePortalEntries(registry)
|
|
613
|
+
}: ItemPortalsProps): ReactNode => {
|
|
614
|
+
const entries = usePortalEntries(registry);
|
|
615
|
+
const state = useCollectionState(collection, expandedIds);
|
|
616
|
+
|
|
617
|
+
return entries.map((entry) => (
|
|
463
618
|
<ItemCell
|
|
464
619
|
key={entry.key}
|
|
465
620
|
entry={entry}
|
|
466
621
|
render={render}
|
|
467
|
-
|
|
622
|
+
state={state}
|
|
468
623
|
hasExpander={hasExpander ?? true}
|
|
469
|
-
expandedIds={expandedIds}
|
|
470
624
|
expanderDescriptions={expanderDescriptions}
|
|
625
|
+
{...itemIdentity(entry, collection, expandedIds)}
|
|
471
626
|
/>
|
|
472
627
|
));
|
|
628
|
+
};
|
|
473
629
|
|
|
474
|
-
const RowPortals = ({ registry, rowProps, collection, expandedIds }: RowPortalsProps): ReactNode =>
|
|
475
|
-
usePortalEntries(registry)
|
|
630
|
+
const RowPortals = ({ registry, rowProps, collection, expandedIds }: RowPortalsProps): ReactNode => {
|
|
631
|
+
const entries = usePortalEntries(registry);
|
|
632
|
+
const state = useCollectionState(collection, expandedIds);
|
|
633
|
+
|
|
634
|
+
return entries.map((entry) => (
|
|
476
635
|
<ItemRow
|
|
477
636
|
key={entry.key}
|
|
478
637
|
entry={entry}
|
|
479
638
|
rowProps={rowProps}
|
|
480
|
-
|
|
481
|
-
|
|
639
|
+
state={state}
|
|
640
|
+
{...itemIdentity(entry, collection, expandedIds)}
|
|
482
641
|
/>
|
|
483
642
|
));
|
|
643
|
+
};
|
|
484
644
|
|
|
485
645
|
const HeaderPortals = ({ registry, render, collection }: HeaderPortalsProps): ReactNode =>
|
|
486
646
|
usePortalEntries(registry).map((entry) => (
|
|
@@ -25,7 +25,6 @@ type IndexState = {
|
|
|
25
25
|
|
|
26
26
|
type ParentItem = ListItem & { children: ListItem[] };
|
|
27
27
|
|
|
28
|
-
const emptyLevel = (): Level => ({ path: "", items: [], expandableFlags: [] });
|
|
29
28
|
const hasChildren = (item: ListItem): item is ParentItem => item.children !== undefined && item.children.length > 0;
|
|
30
29
|
|
|
31
30
|
function newLevel(state: IndexState, path: string, items: ListItem[]): Level {
|
|
@@ -141,4 +140,4 @@ function createCollectionIndex(
|
|
|
141
140
|
};
|
|
142
141
|
}
|
|
143
142
|
|
|
144
|
-
export { createCollectionIndex,
|
|
143
|
+
export { createCollectionIndex, type CollectionIndex, type Level };
|
|
@@ -4,14 +4,14 @@ import * as Gtk from "@gtkx/gi/gtk";
|
|
|
4
4
|
import { registerClass } from "@gtkx/runtime";
|
|
5
5
|
import type { CollectionIndex, Level } from "./collection-index.js";
|
|
6
6
|
import type { TreeExpansion } from "./tree-expansion.js";
|
|
7
|
-
import { createCollectionIndex
|
|
7
|
+
import { createCollectionIndex } from "./collection-index.js";
|
|
8
8
|
import { encodePart } from "./keys.js";
|
|
9
9
|
import { adoptIndex, createTreeExpansion, pruneSlots } from "./tree-expansion.js";
|
|
10
10
|
|
|
11
11
|
type LevelStore = Gio.ListModel & LazyLevelStore;
|
|
12
12
|
|
|
13
13
|
type SlotRef = {
|
|
14
|
-
store:
|
|
14
|
+
store: LazyLevelStore;
|
|
15
15
|
slot: number;
|
|
16
16
|
};
|
|
17
17
|
|
|
@@ -93,22 +93,13 @@ function slotRefFor(value: GObject.Object | null): SlotRef | null {
|
|
|
93
93
|
return ref === undefined || ref.slot === -1 ? null : ref;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function slotPathAt(store:
|
|
96
|
+
function slotPathAt(store: LazyLevelStore, slot: number): string {
|
|
97
97
|
return store.level.path + encodePart(String(slot));
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function growStore(store: LevelStore): void {
|
|
101
|
-
for (let slot = store.refs.length; slot < store.level.items.length; slot++) {
|
|
102
|
-
store.refs.push({ store, slot });
|
|
103
|
-
store.objects.push(null);
|
|
104
|
-
store.childStores.push(null);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
100
|
function newLevelStore(level: Level): LevelStore {
|
|
109
101
|
const store = new (registeredStoreClass())() as LevelStore;
|
|
110
102
|
store.level = level;
|
|
111
|
-
growStore(store);
|
|
112
103
|
|
|
113
104
|
return store;
|
|
114
105
|
}
|
|
@@ -116,6 +107,10 @@ function newLevelStore(level: Level): LevelStore {
|
|
|
116
107
|
function collectFlips(previous: Level, next: Level, overlap: number): Set<number> {
|
|
117
108
|
const flipped: Set<number> = new Set();
|
|
118
109
|
|
|
110
|
+
if (previous.expandableFlags.length === 0 && next.expandableFlags.length === 0) {
|
|
111
|
+
return flipped;
|
|
112
|
+
}
|
|
113
|
+
|
|
119
114
|
for (let slot = 0; slot < overlap; slot++) {
|
|
120
115
|
if ((previous.expandableFlags[slot] ?? false) !== (next.expandableFlags[slot] ?? false)) {
|
|
121
116
|
flipped.add(slot);
|
|
@@ -133,30 +128,30 @@ function pruneFlips(context: SyncContext, store: LevelStore, flipped: Set<number
|
|
|
133
128
|
pruneSlots(context.expansion, store.level.path, (slot) => flipped.has(slot));
|
|
134
129
|
|
|
135
130
|
for (const slot of flipped) {
|
|
136
|
-
store.childStores
|
|
131
|
+
store.childStores.delete(slot);
|
|
137
132
|
}
|
|
138
133
|
}
|
|
139
134
|
|
|
140
135
|
function detachRefs(store: LevelStore, nextLength: number): void {
|
|
141
|
-
for (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (ref !== undefined) {
|
|
145
|
-
ref.slot = -1;
|
|
136
|
+
for (const [slot, ref] of store.refs) {
|
|
137
|
+
if (slot < nextLength) {
|
|
138
|
+
continue;
|
|
146
139
|
}
|
|
140
|
+
|
|
141
|
+
ref.slot = -1;
|
|
142
|
+
store.refs.delete(slot);
|
|
143
|
+
store.objects.delete(slot);
|
|
144
|
+
store.childStores.delete(slot);
|
|
147
145
|
}
|
|
148
146
|
}
|
|
149
147
|
|
|
150
|
-
function shrinkStore(context: SyncContext, store: LevelStore, nextLength: number): void {
|
|
151
|
-
if (
|
|
148
|
+
function shrinkStore(context: SyncContext, store: LevelStore, previousLength: number, nextLength: number): void {
|
|
149
|
+
if (previousLength <= nextLength) {
|
|
152
150
|
return;
|
|
153
151
|
}
|
|
154
152
|
|
|
155
153
|
detachRefs(store, nextLength);
|
|
156
154
|
pruneSlots(context.expansion, store.level.path, (slot) => slot >= nextLength);
|
|
157
|
-
store.refs.length = nextLength;
|
|
158
|
-
store.objects.length = nextLength;
|
|
159
|
-
store.childStores.length = nextLength;
|
|
160
155
|
}
|
|
161
156
|
|
|
162
157
|
function emitTailSplice(store: LevelStore, previousLength: number, nextLength: number): void {
|
|
@@ -205,7 +200,7 @@ function childSync(
|
|
|
205
200
|
slot: number,
|
|
206
201
|
flipped: Set<number>,
|
|
207
202
|
): LevelSync | undefined {
|
|
208
|
-
const child = store.childStores
|
|
203
|
+
const child = store.childStores.get(slot) ?? null;
|
|
209
204
|
|
|
210
205
|
if (child === null || flipped.has(slot)) {
|
|
211
206
|
return undefined;
|
|
@@ -218,8 +213,13 @@ function childSync(
|
|
|
218
213
|
|
|
219
214
|
function childSyncs(context: SyncContext, store: LevelStore, overlap: number, flipped: Set<number>): LevelSync[] {
|
|
220
215
|
const syncs: LevelSync[] = [];
|
|
216
|
+
const slots = store.childStores.keys().toArray().toSorted((left, right) => left - right);
|
|
217
|
+
|
|
218
|
+
for (const slot of slots) {
|
|
219
|
+
if (slot >= overlap) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
221
222
|
|
|
222
|
-
for (let slot = 0; slot < overlap; slot++) {
|
|
223
223
|
const sync = childSync(context, store, slot, flipped);
|
|
224
224
|
|
|
225
225
|
if (sync !== undefined) {
|
|
@@ -232,25 +232,19 @@ function childSyncs(context: SyncContext, store: LevelStore, overlap: number, fl
|
|
|
232
232
|
|
|
233
233
|
function syncEntry(context: SyncContext, entry: LevelSync): LevelSync[] {
|
|
234
234
|
const { store, level } = entry;
|
|
235
|
-
const
|
|
235
|
+
const previous = store.level;
|
|
236
|
+
const previousLength = previous.items.length;
|
|
236
237
|
const overlap = Math.min(previousLength, level.items.length);
|
|
237
|
-
const flipped = collectFlips(
|
|
238
|
+
const flipped = collectFlips(previous, level, overlap);
|
|
238
239
|
store.level = level;
|
|
239
240
|
pruneFlips(context, store, flipped);
|
|
240
|
-
shrinkStore(context, store, level.items.length);
|
|
241
|
-
growStore(store);
|
|
241
|
+
shrinkStore(context, store, previousLength, level.items.length);
|
|
242
242
|
emitTailSplice(store, previousLength, level.items.length);
|
|
243
243
|
emitFlips(store, flipped);
|
|
244
244
|
|
|
245
245
|
return childSyncs(context, store, overlap, flipped);
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
function pushSyncs(pending: LevelSync[], syncs: LevelSync[]): void {
|
|
249
|
-
for (const sync of syncs) {
|
|
250
|
-
pending.push(sync);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
248
|
function syncLevel(context: SyncContext, store: LevelStore, level: Level): void {
|
|
255
249
|
const pending: LevelSync[] = [{ store, level }];
|
|
256
250
|
|
|
@@ -258,13 +252,13 @@ function syncLevel(context: SyncContext, store: LevelStore, level: Level): void
|
|
|
258
252
|
const entry = pending.pop();
|
|
259
253
|
|
|
260
254
|
if (entry !== undefined) {
|
|
261
|
-
|
|
255
|
+
pending.push(...syncEntry(context, entry).toReversed());
|
|
262
256
|
}
|
|
263
257
|
}
|
|
264
258
|
}
|
|
265
259
|
|
|
266
|
-
function ensureChildStore(index: CollectionIndex, store:
|
|
267
|
-
const existing = store.childStores
|
|
260
|
+
function ensureChildStore(index: CollectionIndex, store: LazyLevelStore, slot: number): LevelStore | null {
|
|
261
|
+
const existing = store.childStores.get(slot) ?? null;
|
|
268
262
|
|
|
269
263
|
if (existing !== null || !(store.level.expandableFlags[slot] ?? false)) {
|
|
270
264
|
return existing;
|
|
@@ -277,7 +271,7 @@ function ensureChildStore(index: CollectionIndex, store: LevelStore, slot: numbe
|
|
|
277
271
|
}
|
|
278
272
|
|
|
279
273
|
const created = newLevelStore(level);
|
|
280
|
-
store.childStores
|
|
274
|
+
store.childStores.set(slot, created);
|
|
281
275
|
|
|
282
276
|
return created;
|
|
283
277
|
}
|
|
@@ -423,14 +417,16 @@ function createCollectionModel(): CollectionModel {
|
|
|
423
417
|
|
|
424
418
|
class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
|
|
425
419
|
declare itemsChanged: Gio.ListModel["itemsChanged"];
|
|
426
|
-
level: Level =
|
|
427
|
-
refs: SlotRef
|
|
428
|
-
objects:
|
|
429
|
-
childStores:
|
|
420
|
+
level: Level = { path: "", items: [], expandableFlags: [] };
|
|
421
|
+
refs: Map<number, SlotRef> = new Map();
|
|
422
|
+
objects: Map<number, Gtk.StringObject> = new Map();
|
|
423
|
+
childStores: Map<number, LevelStore> = new Map();
|
|
430
424
|
|
|
431
|
-
private createItem(position: number
|
|
425
|
+
private createItem(position: number): Gtk.StringObject {
|
|
426
|
+
const ref = { store: this, slot: position };
|
|
432
427
|
const created = Gtk.StringObject.new("");
|
|
433
|
-
this.
|
|
428
|
+
this.refs.set(position, ref);
|
|
429
|
+
this.objects.set(position, created);
|
|
434
430
|
SLOTS.set(created, ref);
|
|
435
431
|
|
|
436
432
|
return created;
|
|
@@ -441,17 +437,15 @@ class LazyLevelStore extends GObject.Object implements Gio.ListModelImpl {
|
|
|
441
437
|
}
|
|
442
438
|
|
|
443
439
|
vfuncGetNItems(): number {
|
|
444
|
-
return this.
|
|
440
|
+
return this.level.items.length;
|
|
445
441
|
}
|
|
446
442
|
|
|
447
443
|
vfuncGetItem(position: number): GObject.Object | null {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (ref === undefined) {
|
|
444
|
+
if (position < 0 || position >= this.level.items.length) {
|
|
451
445
|
return null;
|
|
452
446
|
}
|
|
453
447
|
|
|
454
|
-
return this.objects
|
|
448
|
+
return this.objects.get(position) ?? this.createItem(position);
|
|
455
449
|
}
|
|
456
450
|
}
|
|
457
451
|
|
|
@@ -30,10 +30,6 @@ type ExpansionContext = {
|
|
|
30
30
|
onExpandedChange?: ((ids: string[]) => void) | null | undefined;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
function newLastExpansion(): LastExpansion {
|
|
34
|
-
return { key: "" };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
33
|
function hasSameExpansion(expanded: Set<string>, wanted: Set<string>): boolean {
|
|
38
34
|
return expanded.size === wanted.size && wanted.isSubsetOf(expanded);
|
|
39
35
|
}
|
|
@@ -95,10 +91,6 @@ function applyControlledExpansion(context: ExpansionContext, expandedIds: string
|
|
|
95
91
|
reportExpansion(context);
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
function isExpansionIdle(collection: Collection): boolean {
|
|
99
|
-
return collection.isTree && isCollectionIdle(collection);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
94
|
function didRowDrift(collection: Collection, change: ItemsChange): boolean {
|
|
103
95
|
const path = collection.pathAt(change.position - 1);
|
|
104
96
|
const isExpanded = change.removed === 0 && change.added > 0;
|
|
@@ -114,7 +106,7 @@ function didRowDrift(collection: Collection, change: ItemsChange): boolean {
|
|
|
114
106
|
}
|
|
115
107
|
|
|
116
108
|
function observeExpansion(context: ExpansionContext, change: ItemsChange, onDrift: () => void): void {
|
|
117
|
-
if (!
|
|
109
|
+
if (!context.collection.isTree || !isCollectionIdle(context.collection)) {
|
|
118
110
|
return;
|
|
119
111
|
}
|
|
120
112
|
|
|
@@ -128,7 +120,7 @@ function observeExpansion(context: ExpansionContext, change: ItemsChange, onDrif
|
|
|
128
120
|
|
|
129
121
|
function useExpansion(options: ExpansionOptions): ItemsChangeHandler {
|
|
130
122
|
const { collection, expandedIds, onExpandedChange } = options;
|
|
131
|
-
const [last] = useState<LastExpansion>(
|
|
123
|
+
const [last] = useState<LastExpansion>(() => ({ key: "" }));
|
|
132
124
|
const context: ExpansionContext = { collection, last, onExpandedChange };
|
|
133
125
|
|
|
134
126
|
const markDrift = useControlledSync({
|
package/src/internal/slots.ts
CHANGED
|
@@ -43,15 +43,7 @@ function trackPath(slots: SlotMap, path: string): void {
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (level === undefined) {
|
|
49
|
-
slots.set(key.levelPath, new Set([key.slot]));
|
|
50
|
-
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
level.add(key.slot);
|
|
46
|
+
slots.getOrInsertComputed(key.levelPath, () => new Set()).add(key.slot);
|
|
55
47
|
}
|
|
56
48
|
|
|
57
49
|
function trackPaths(paths: Iterable<string>): SlotMap {
|
|
@@ -3,7 +3,7 @@ import type { SlotMap } from "./slots.js";
|
|
|
3
3
|
import type { VisibleOrder } from "./tree-order.js";
|
|
4
4
|
import { encodePart } from "./keys.js";
|
|
5
5
|
import { getSlotKey, trackPath, trackPaths } from "./slots.js";
|
|
6
|
-
import {
|
|
6
|
+
import { walkVisible } from "./tree-order.js";
|
|
7
7
|
|
|
8
8
|
type TreeExpansion = {
|
|
9
9
|
index: CollectionIndex;
|
|
@@ -46,7 +46,7 @@ function createTreeExpansion(index: CollectionIndex): TreeExpansion {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function orderFor(expansion: TreeExpansion): VisibleOrder {
|
|
49
|
-
expansion.order ??=
|
|
49
|
+
expansion.order ??= walkVisible({ index: expansion.index, slots: expansion.slots });
|
|
50
50
|
|
|
51
51
|
return expansion.order;
|
|
52
52
|
}
|
|
@@ -183,10 +183,6 @@ function expandedPathsFor(index: CollectionIndex, ids: Set<string>): Set<string>
|
|
|
183
183
|
return paths;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
function buildVisibleOrder(index: CollectionIndex, slots: SlotMap): VisibleOrder {
|
|
187
|
-
return walkVisible({ index, slots });
|
|
188
|
-
}
|
|
189
|
-
|
|
190
186
|
function findRows(index: CollectionIndex, slots: SlotMap, ids: Set<string>): MatchedRows {
|
|
191
187
|
const positions: number[] = [];
|
|
192
188
|
const found: string[] = [];
|
|
@@ -226,7 +222,6 @@ function findIds(index: CollectionIndex, slots: SlotMap, positions: Set<number>)
|
|
|
226
222
|
}
|
|
227
223
|
|
|
228
224
|
export {
|
|
229
|
-
buildVisibleOrder,
|
|
230
225
|
expandedPathsFor,
|
|
231
226
|
findIds,
|
|
232
227
|
findRows,
|