@gtkx/components 2.0.0-beta.11 → 2.0.0-beta.12
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 +4 -4
- package/dist/column-view.js +1 -1
- package/dist/grid-view.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +3 -7
- package/dist/internal/cells.d.ts.map +1 -1
- package/dist/internal/cells.js +15 -43
- package/dist/internal/cells.js.map +1 -1
- package/dist/internal/collection-model.d.ts.map +1 -1
- package/dist/internal/drop-down.d.ts.map +1 -1
- package/dist/internal/drop-down.js +1 -1
- package/dist/internal/drop-down.js.map +1 -1
- package/dist/internal/keys.d.ts.map +1 -1
- package/dist/internal/tree-expansion.d.ts +1 -1
- package/dist/internal/tree-expansion.d.ts.map +1 -1
- package/dist/internal/tree-expansion.js +1 -1
- package/dist/internal/tree-expansion.js.map +1 -1
- package/dist/internal/tree-order.d.ts +1 -1
- package/dist/internal/tree-order.d.ts.map +1 -1
- package/dist/internal/tree-order.js +1 -1
- package/dist/internal/tree-order.js.map +1 -1
- package/dist/internal/use-widget-ref.d.ts.map +1 -1
- package/dist/list-item-factory.js +1 -1
- package/dist/list-view.js +1 -1
- package/dist/tab-collection.d.ts +9 -0
- package/dist/tab-collection.d.ts.map +1 -0
- package/dist/tab-collection.js +114 -0
- package/dist/tab-collection.js.map +1 -0
- package/dist/types.d.ts +35 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +10 -20
- package/src/index.ts +4 -0
- package/src/internal/cells.tsx +44 -109
- package/src/internal/drop-down.tsx +1 -5
- package/src/internal/tree-expansion.ts +1 -9
- package/src/internal/tree-order.ts +1 -9
- package/src/tab-collection.tsx +124 -0
- package/src/types.ts +56 -14
package/src/internal/cells.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
|
-
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { ReactElement, ReactNode, RefObject } from "react";
|
|
3
3
|
import * as Gtk from "@gtkx/gi/gtk";
|
|
4
4
|
import { GtkBox, GtkSignalListItemFactory, GtkTreeExpander } from "@gtkx/jsx/gtk";
|
|
5
5
|
import { createPortal, useProperty } from "@gtkx/react";
|
|
6
|
-
import {
|
|
6
|
+
import { useLatestRef } from "@gtkx/react/internal";
|
|
7
|
+
import { createRef, Fragment, memo, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
7
8
|
import type {
|
|
8
9
|
ExpanderDescriptions,
|
|
9
10
|
ListItem,
|
|
@@ -34,7 +35,7 @@ type CellHost = FactoryHost & {
|
|
|
34
35
|
type CellEntry<H extends FactoryHost> = {
|
|
35
36
|
key: string;
|
|
36
37
|
host: H;
|
|
37
|
-
identity:
|
|
38
|
+
identity: RefObject<ItemIdentity | null>;
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
type FactoryHandlers = {
|
|
@@ -95,11 +96,6 @@ type CollectionState = {
|
|
|
95
96
|
expandedIds: string[] | null | undefined;
|
|
96
97
|
};
|
|
97
98
|
|
|
98
|
-
type CollectionStateStore = {
|
|
99
|
-
read: () => CollectionState;
|
|
100
|
-
write: (state: CollectionState) => void;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
99
|
type ItemIdentity = {
|
|
104
100
|
itemKey: string;
|
|
105
101
|
item: ListItem | undefined;
|
|
@@ -107,15 +103,10 @@ type ItemIdentity = {
|
|
|
107
103
|
isExpanded: boolean;
|
|
108
104
|
};
|
|
109
105
|
|
|
110
|
-
type ItemIdentityStore = {
|
|
111
|
-
read: () => ItemIdentity | null;
|
|
112
|
-
write: (identity: ItemIdentity) => void;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
106
|
type ItemCellProps = {
|
|
116
107
|
entry: CellEntry<Gtk.ListItem>;
|
|
117
108
|
render: ListItemRenderer<never>;
|
|
118
|
-
state:
|
|
109
|
+
state: RefObject<CollectionState>;
|
|
119
110
|
hasExpander: boolean;
|
|
120
111
|
expanderDescriptions: ExpanderDescriptions | null | undefined;
|
|
121
112
|
} & ItemIdentity;
|
|
@@ -132,7 +123,7 @@ type ItemPortalsProps = {
|
|
|
132
123
|
type ItemRowProps = {
|
|
133
124
|
entry: CellEntry<Gtk.ColumnViewRow>;
|
|
134
125
|
rowProps: ListRowPropsResolver<never>;
|
|
135
|
-
state:
|
|
126
|
+
state: RefObject<CollectionState>;
|
|
136
127
|
} & ItemIdentity;
|
|
137
128
|
|
|
138
129
|
type RowPortalsProps = {
|
|
@@ -231,7 +222,7 @@ function notifyRegistry<H extends FactoryHost>(state: CellRegistryState<H>): voi
|
|
|
231
222
|
function createEntry<H extends FactoryHost>(state: CellRegistryState<H>, host: H): CellEntry<H> {
|
|
232
223
|
state.serial += 1;
|
|
233
224
|
|
|
234
|
-
return { key: `gtkx-cell-${String(state.serial)}`, host, identity:
|
|
225
|
+
return { key: `gtkx-cell-${String(state.serial)}`, host, identity: createRef<ItemIdentity>() };
|
|
235
226
|
}
|
|
236
227
|
|
|
237
228
|
function setupHost<H extends FactoryHost>(state: CellRegistryState<H>, host: H): void {
|
|
@@ -386,7 +377,7 @@ function slotFor(options: ItemSlotOptions): ItemSlot | null {
|
|
|
386
377
|
|
|
387
378
|
function useItemSlot(
|
|
388
379
|
entry: CellEntry<PositionedHost>,
|
|
389
|
-
state:
|
|
380
|
+
state: RefObject<CollectionState>,
|
|
390
381
|
identity: ItemIdentity,
|
|
391
382
|
): ItemSlot | null {
|
|
392
383
|
const position = useProperty(entry.host, "position");
|
|
@@ -397,14 +388,14 @@ function useItemSlot(
|
|
|
397
388
|
return identity.item === undefined
|
|
398
389
|
? null
|
|
399
390
|
: {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
391
|
+
itemKey: identity.itemKey,
|
|
392
|
+
item: identity.item,
|
|
393
|
+
row,
|
|
394
|
+
args: itemArgs(identity.item, position, row, identity.isExpanded),
|
|
395
|
+
};
|
|
405
396
|
}
|
|
406
397
|
|
|
407
|
-
const { collection, expandedIds } = state.
|
|
398
|
+
const { collection, expandedIds } = state.current;
|
|
408
399
|
|
|
409
400
|
return slotFor({ item, position, row, collection, expandedIds });
|
|
410
401
|
}
|
|
@@ -435,7 +426,7 @@ function isSameItemIdentity(previous: ItemIdentity, next: ItemIdentity): boolean
|
|
|
435
426
|
}
|
|
436
427
|
|
|
437
428
|
function isSameItemCell(previous: ItemCellProps, next: ItemCellProps): boolean {
|
|
438
|
-
const identity = previous.entry.identity.
|
|
429
|
+
const identity = previous.entry.identity.current ?? previous;
|
|
439
430
|
|
|
440
431
|
return (
|
|
441
432
|
previous.entry === next.entry &&
|
|
@@ -447,22 +438,11 @@ function isSameItemCell(previous: ItemCellProps, next: ItemCellProps): boolean {
|
|
|
447
438
|
}
|
|
448
439
|
|
|
449
440
|
function isSameItemRow(previous: ItemRowProps, next: ItemRowProps): boolean {
|
|
450
|
-
const identity = previous.entry.identity.
|
|
441
|
+
const identity = previous.entry.identity.current ?? previous;
|
|
451
442
|
|
|
452
443
|
return previous.entry === next.entry && previous.rowProps === next.rowProps && isSameItemIdentity(identity, next);
|
|
453
444
|
}
|
|
454
445
|
|
|
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
446
|
function createNativeItemKeyStore(): NativeItemKeyStore {
|
|
467
447
|
const keys: WeakMap<GObject.Object, string> = new WeakMap();
|
|
468
448
|
let serial = 0;
|
|
@@ -494,35 +474,12 @@ function renderedIdentity(entry: CellEntry<PositionedHost>, slot: ItemSlot | nul
|
|
|
494
474
|
}
|
|
495
475
|
|
|
496
476
|
function useRememberIdentity(entry: CellEntry<PositionedHost>, slot: ItemSlot | null): void {
|
|
477
|
+
const identityRef = entry.identity;
|
|
497
478
|
useLayoutEffect(() => {
|
|
498
|
-
|
|
479
|
+
identityRef.current = renderedIdentity(entry, slot);
|
|
499
480
|
});
|
|
500
481
|
}
|
|
501
482
|
|
|
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
|
-
|
|
526
483
|
function expanderDescriptionFor(
|
|
527
484
|
slot: ItemSlot,
|
|
528
485
|
descriptions: ExpanderDescriptions | null | undefined,
|
|
@@ -544,21 +501,19 @@ function wrapExpander(
|
|
|
544
501
|
const { item, row } = slot;
|
|
545
502
|
|
|
546
503
|
const expander: ReactNode =
|
|
547
|
-
row === null
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
</GtkTreeExpander>
|
|
561
|
-
);
|
|
504
|
+
row === null ? (
|
|
505
|
+
content
|
|
506
|
+
) : (
|
|
507
|
+
<GtkTreeExpander
|
|
508
|
+
listRow={row}
|
|
509
|
+
hideExpander={item.shouldHideExpander ?? false}
|
|
510
|
+
indentForDepth={item.shouldIndentForDepth ?? true}
|
|
511
|
+
indentForIcon={item.shouldIndentForIcon ?? true}
|
|
512
|
+
accessibleDescription={expanderDescriptionFor(slot, descriptions)}
|
|
513
|
+
>
|
|
514
|
+
{content}
|
|
515
|
+
</GtkTreeExpander>
|
|
516
|
+
);
|
|
562
517
|
|
|
563
518
|
return expander;
|
|
564
519
|
}
|
|
@@ -576,16 +531,13 @@ function itemBody(
|
|
|
576
531
|
const renderItem = render as ListItemRenderer<unknown>;
|
|
577
532
|
const content = renderItem(slot.args);
|
|
578
533
|
|
|
579
|
-
return (
|
|
580
|
-
<Fragment key={slot.itemKey}>{hasExpander ? wrapExpander(slot, content, descriptions) : content}</Fragment>
|
|
581
|
-
);
|
|
534
|
+
return <Fragment key={slot.itemKey}>{hasExpander ? wrapExpander(slot, content, descriptions) : content}</Fragment>;
|
|
582
535
|
}
|
|
583
536
|
|
|
584
537
|
function rowText(value: string | undefined): string | null {
|
|
585
538
|
return value ?? null;
|
|
586
539
|
}
|
|
587
540
|
|
|
588
|
-
/* eslint-disable-next-line unicorn/consistent-boolean-name -- reads the flag off a row, paired with rowText */
|
|
589
541
|
function rowFlag(value: boolean | undefined): boolean {
|
|
590
542
|
return value ?? true;
|
|
591
543
|
}
|
|
@@ -603,11 +555,7 @@ function rowPropsFor(slot: ItemSlot | null, rowProps: ListRowPropsResolver<never
|
|
|
603
555
|
};
|
|
604
556
|
}
|
|
605
557
|
|
|
606
|
-
function applyRowProps(
|
|
607
|
-
host: Gtk.ColumnViewRow,
|
|
608
|
-
props: ResolvedRowProps,
|
|
609
|
-
previous: ResolvedRowProps | null,
|
|
610
|
-
): void {
|
|
558
|
+
function applyRowProps(host: Gtk.ColumnViewRow, props: ResolvedRowProps, previous: ResolvedRowProps | null): void {
|
|
611
559
|
if (previous?.accessibleLabel !== props.accessibleLabel) {
|
|
612
560
|
host.accessibleLabel = props.accessibleLabel;
|
|
613
561
|
}
|
|
@@ -664,32 +612,26 @@ function HeaderCellImpl({ entry, render, collection }: HeaderCellProps): ReactNo
|
|
|
664
612
|
return createPortal(headerBody(slotRefFor(item), render, collection), entry.host, entry.key);
|
|
665
613
|
}
|
|
666
614
|
|
|
667
|
-
function NativeItemCell<T extends GObject.Object>({
|
|
668
|
-
entry,
|
|
669
|
-
render,
|
|
670
|
-
keys,
|
|
671
|
-
size,
|
|
672
|
-
}: NativeItemCellProps<T>): ReactNode {
|
|
615
|
+
function NativeItemCell<T extends GObject.Object>({ entry, render, keys, size }: NativeItemCellProps<T>): ReactNode {
|
|
673
616
|
const position = useProperty(entry.host, "position");
|
|
674
617
|
const item = useProperty(entry.host, "item") ?? null;
|
|
675
618
|
let body: ReactNode = null;
|
|
676
619
|
|
|
677
620
|
if (item !== null && position !== undefined) {
|
|
678
621
|
const content = render({ item: item as T, index: position });
|
|
679
|
-
const child =
|
|
680
|
-
|
|
681
|
-
|
|
622
|
+
const child =
|
|
623
|
+
content == null || typeof content === "boolean" ? (
|
|
624
|
+
<GtkBox widthRequest={size.width} heightRequest={size.height} />
|
|
625
|
+
) : (
|
|
626
|
+
content
|
|
627
|
+
);
|
|
682
628
|
body = <Fragment key={keys.keyFor(item)}>{child}</Fragment>;
|
|
683
629
|
}
|
|
684
630
|
|
|
685
631
|
return createPortal(body, entry.host, entry.key);
|
|
686
632
|
}
|
|
687
633
|
|
|
688
|
-
function headerBody(
|
|
689
|
-
ref: SlotRef | null,
|
|
690
|
-
render: ListSectionRenderer<never>,
|
|
691
|
-
collection: Collection,
|
|
692
|
-
): ReactNode {
|
|
634
|
+
function headerBody(ref: SlotRef | null, render: ListSectionRenderer<never>, collection: Collection): ReactNode {
|
|
693
635
|
if (ref === null) {
|
|
694
636
|
return null;
|
|
695
637
|
}
|
|
@@ -719,7 +661,7 @@ const ItemPortals = ({
|
|
|
719
661
|
expanderDescriptions,
|
|
720
662
|
}: ItemPortalsProps): ReactNode => {
|
|
721
663
|
const entries = usePortalEntries(registry);
|
|
722
|
-
const state =
|
|
664
|
+
const state = useLatestRef({ collection, expandedIds });
|
|
723
665
|
|
|
724
666
|
return entries.map((entry) => (
|
|
725
667
|
<ItemCell
|
|
@@ -736,7 +678,7 @@ const ItemPortals = ({
|
|
|
736
678
|
|
|
737
679
|
const RowPortals = ({ registry, rowProps, collection, expandedIds }: RowPortalsProps): ReactNode => {
|
|
738
680
|
const entries = usePortalEntries(registry);
|
|
739
|
-
const state =
|
|
681
|
+
const state = useLatestRef({ collection, expandedIds });
|
|
740
682
|
|
|
741
683
|
return entries.map((entry) => (
|
|
742
684
|
<ItemRow
|
|
@@ -799,11 +741,4 @@ const useRowProps = (
|
|
|
799
741
|
};
|
|
800
742
|
};
|
|
801
743
|
|
|
802
|
-
export {
|
|
803
|
-
useItemCells,
|
|
804
|
-
useRowProps,
|
|
805
|
-
useSectionHeader,
|
|
806
|
-
ItemPortals,
|
|
807
|
-
NativeItemPortals,
|
|
808
|
-
type CellSize,
|
|
809
|
-
};
|
|
744
|
+
export { useItemCells, useRowProps, useSectionHeader, ItemPortals, NativeItemPortals, type CellSize };
|
|
@@ -59,11 +59,7 @@ const defaultRenderItem = ({ item }: ListItemRenderArgs<Primitive>): ReactNode =
|
|
|
59
59
|
item == null ? null : <GtkLabel>{String(item)}</GtkLabel>;
|
|
60
60
|
const faceRenderer = (props: DropDownBaseProps): ListItemRenderer<never> => props.renderItem ?? defaultRenderItem;
|
|
61
61
|
|
|
62
|
-
const resolvePosition = (
|
|
63
|
-
widget: SelectableWidget,
|
|
64
|
-
collection: Collection,
|
|
65
|
-
selectedId: string | undefined,
|
|
66
|
-
): number => {
|
|
62
|
+
const resolvePosition = (widget: SelectableWidget, collection: Collection, selectedId: string | undefined): number => {
|
|
67
63
|
if (selectedId === undefined) {
|
|
68
64
|
return widget.getSelected();
|
|
69
65
|
}
|
|
@@ -92,12 +92,4 @@ function adoptIndex(expansion: TreeExpansion, index: CollectionIndex): void {
|
|
|
92
92
|
expansion.order = null;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export {
|
|
96
|
-
adoptIndex,
|
|
97
|
-
adoptOrder,
|
|
98
|
-
createTreeExpansion,
|
|
99
|
-
markExpanded,
|
|
100
|
-
orderFor,
|
|
101
|
-
pruneSlots,
|
|
102
|
-
type TreeExpansion,
|
|
103
|
-
};
|
|
95
|
+
export { adoptIndex, adoptOrder, createTreeExpansion, markExpanded, orderFor, pruneSlots, type TreeExpansion };
|
|
@@ -133,12 +133,4 @@ function findIds(index: CollectionIndex, slots: SlotMap, positions: Set<number>)
|
|
|
133
133
|
return found;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
export {
|
|
137
|
-
expandedPathsFor,
|
|
138
|
-
findIds,
|
|
139
|
-
findRows,
|
|
140
|
-
walkVisible,
|
|
141
|
-
type MatchedRows,
|
|
142
|
-
type VisibleOrder,
|
|
143
|
-
type WalkOptions,
|
|
144
|
-
};
|
|
136
|
+
export { expandedPathsFor, findIds, findRows, walkVisible, type MatchedRows, type VisibleOrder, type WalkOptions };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as Adw from "@gtkx/gi/adw";
|
|
2
|
+
import { AdwTabPage } from "@gtkx/jsx/adw";
|
|
3
|
+
import { createRoot, flushSync, type Root } from "@gtkx/react";
|
|
4
|
+
import { getClassType } from "@gtkx/runtime";
|
|
5
|
+
import { createRef, type RefObject, useEffect } from "react";
|
|
6
|
+
import type { TabCollection, TabCollectionOptions } from "./types.js";
|
|
7
|
+
|
|
8
|
+
type TabHandle<T> = {
|
|
9
|
+
root: Root;
|
|
10
|
+
page: RefObject<Adw.TabPage | null>;
|
|
11
|
+
getPage: () => Adw.TabPage | null;
|
|
12
|
+
parent: Adw.TabPage | undefined;
|
|
13
|
+
item: T;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type ManagedTabProps<T> = {
|
|
17
|
+
handle: TabHandle<T>;
|
|
18
|
+
item: T;
|
|
19
|
+
options: TabCollectionOptions<T>;
|
|
20
|
+
forget: (handle: TabHandle<T>) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const ManagedTab = <T,>({ handle, item, options, forget }: ManagedTabProps<T>) => {
|
|
24
|
+
const { page: pageRef, parent, getPage } = handle;
|
|
25
|
+
useEffect(() => () => forget(handle), [handle, forget]);
|
|
26
|
+
return <AdwTabPage {...options.pageProps(item, getPage)} ref={pageRef} parent={parent} />;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ownerOf = (page: Adw.TabPage): Adw.TabView => {
|
|
30
|
+
const owner = page.getChild().getAncestor(getClassType(Adw.TabView));
|
|
31
|
+
if (!(owner instanceof Adw.TabView)) throw new Error("The tab has no native view");
|
|
32
|
+
return owner;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a collection of JSX pages whose React state survives native transfers and source-window
|
|
37
|
+
* closure. Call append from an event handler because it commits synchronously. Page content lives in
|
|
38
|
+
* independent roots: pass context-dependent values through the item or wrap the content in providers.
|
|
39
|
+
*/
|
|
40
|
+
const createTabCollection = <T,>(options: TabCollectionOptions<T>): TabCollection<T> => {
|
|
41
|
+
const handles = new Set<TabHandle<T>>();
|
|
42
|
+
const forget = (handle: TabHandle<T>) => {
|
|
43
|
+
handles.delete(handle);
|
|
44
|
+
};
|
|
45
|
+
const find = (page: Adw.TabPage) => [...handles].find((handle) => handle.page.current === page);
|
|
46
|
+
const render = (handle: TabHandle<T>) => {
|
|
47
|
+
handle.root.render(<ManagedTab handle={handle} item={handle.item} options={options} forget={forget} />);
|
|
48
|
+
};
|
|
49
|
+
const remove = (handle: TabHandle<T>) => {
|
|
50
|
+
handles.delete(handle);
|
|
51
|
+
handle.root.unmount();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
append: (view, item, appendOptions = {}) => {
|
|
56
|
+
if (appendOptions.parent && ownerOf(appendOptions.parent) !== view) {
|
|
57
|
+
throw new RangeError("The parent page belongs to another tab view");
|
|
58
|
+
}
|
|
59
|
+
const pageRef = createRef<Adw.TabPage>();
|
|
60
|
+
const handle: TabHandle<T> = {
|
|
61
|
+
root: createRoot(view),
|
|
62
|
+
page: pageRef,
|
|
63
|
+
getPage: () => pageRef.current,
|
|
64
|
+
parent: appendOptions.parent,
|
|
65
|
+
item,
|
|
66
|
+
};
|
|
67
|
+
handles.add(handle);
|
|
68
|
+
try {
|
|
69
|
+
flushSync(() => render(handle));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
remove(handle);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
const page = handle.page.current;
|
|
75
|
+
if (page === null) {
|
|
76
|
+
remove(handle);
|
|
77
|
+
throw new Error("The tab page did not render");
|
|
78
|
+
}
|
|
79
|
+
if (appendOptions.parent === undefined) {
|
|
80
|
+
view.reorderPage(page, page.getPinned() ? view.getNPinnedPages() - 1 : view.getNPages() - 1);
|
|
81
|
+
}
|
|
82
|
+
if (appendOptions.select !== false) view.setSelectedPage(page);
|
|
83
|
+
return page;
|
|
84
|
+
},
|
|
85
|
+
get: (page) => find(page)?.item,
|
|
86
|
+
update: (page, item) => {
|
|
87
|
+
const handle = find(page);
|
|
88
|
+
if (handle === undefined) throw new Error("The tab is not managed by this collection");
|
|
89
|
+
handle.item = item;
|
|
90
|
+
render(handle);
|
|
91
|
+
},
|
|
92
|
+
remove: (page) => {
|
|
93
|
+
const handle = find(page);
|
|
94
|
+
if (handle) remove(handle);
|
|
95
|
+
},
|
|
96
|
+
removeView: (view) => {
|
|
97
|
+
const pages = Array.from({ length: view.getNPages() }, (_, index) => view.getNthPage(index));
|
|
98
|
+
for (const page of pages) {
|
|
99
|
+
const handle = find(page);
|
|
100
|
+
if (handle) remove(handle);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
transfer: (page, target, position) => {
|
|
104
|
+
if (find(page) === undefined) throw new Error("The tab is not managed by this collection");
|
|
105
|
+
const source = ownerOf(page);
|
|
106
|
+
const minimum = page.getPinned() ? 0 : target.getNPinnedPages();
|
|
107
|
+
const maximum = page.getPinned() ? target.getNPinnedPages() : target.getNPages();
|
|
108
|
+
const index = position ?? maximum;
|
|
109
|
+
if (!Number.isInteger(index) || index < minimum || index > maximum) {
|
|
110
|
+
throw new RangeError("The position is outside the tab's pinned or unpinned group");
|
|
111
|
+
}
|
|
112
|
+
if (source === target) {
|
|
113
|
+
target.reorderPage(page, Math.min(index, maximum - 1));
|
|
114
|
+
} else {
|
|
115
|
+
source.transferPage(page, target, index);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
dispose: () => {
|
|
119
|
+
for (const handle of [...handles]) remove(handle);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export { createTabCollection };
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type * as Adw from "@gtkx/gi/adw";
|
|
2
2
|
import type * as GObject from "@gtkx/gi/gobject";
|
|
3
3
|
import type * as Gtk from "@gtkx/gi/gtk";
|
|
4
|
-
import type { AdwComboRowProps, AdwToastProps } from "@gtkx/jsx/adw";
|
|
4
|
+
import type { AdwComboRowProps, AdwTabPageProps, AdwToastProps } from "@gtkx/jsx/adw";
|
|
5
5
|
import type {
|
|
6
6
|
GtkColumnViewColumnProps,
|
|
7
7
|
GtkColumnViewProps,
|
|
@@ -159,15 +159,18 @@ type SortProps = {
|
|
|
159
159
|
};
|
|
160
160
|
|
|
161
161
|
/** The data a collection view renders, either as a plain item list or grouped into sections. */
|
|
162
|
-
type SourceProps<T, S> =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
162
|
+
type SourceProps<T, S> =
|
|
163
|
+
| ({
|
|
164
|
+
/** Items to render, nesting through `ListItem.children` for a tree. */
|
|
165
|
+
items?: ListItem<T>[] | undefined;
|
|
166
|
+
} & Partial<Record<"sections", undefined>> &
|
|
167
|
+
Partial<Record<"renderHeader", null | undefined>>)
|
|
168
|
+
| ({
|
|
169
|
+
/** Items grouped under section headers. */
|
|
170
|
+
sections: ListSection<S, T>[];
|
|
171
|
+
/** Renders the header shown above each section. */
|
|
172
|
+
renderHeader?: ListSectionRenderer<S> | null | undefined;
|
|
173
|
+
} & Partial<Record<"items", undefined>>);
|
|
171
174
|
|
|
172
175
|
/** One column of a {@link ColumnView}, pairing Gtk.ColumnViewColumn props with a cell renderer. */
|
|
173
176
|
type ColumnViewColumn<T = unknown> = Omit<GtkColumnViewColumnProps, "factory" | "sorter" | "id" | "title"> & {
|
|
@@ -203,7 +206,7 @@ type ColumnViewProps<T = unknown, S = unknown> = Omit<
|
|
|
203
206
|
GtkColumnViewProps,
|
|
204
207
|
"children" | "columns" | "model" | "headerFactory" | "rowFactory" | keyof ColumnViewOwnProps<T, S>
|
|
205
208
|
> &
|
|
206
|
-
ColumnViewOwnProps<T, S>;
|
|
209
|
+
ColumnViewOwnProps<T, S>;
|
|
207
210
|
|
|
208
211
|
/** The declarative collection props {@link DropDown} and `ComboRow` add on top of their widget's own. */
|
|
209
212
|
type DropDownOwnProps<T, S> = SourceProps<T, S> & {
|
|
@@ -222,8 +225,8 @@ type DropDownWidgetProps<Widget, T, S> = Omit<
|
|
|
222
225
|
Widget,
|
|
223
226
|
"model" | "factory" | "listFactory" | "headerFactory" | keyof DropDownOwnProps<T, S>
|
|
224
227
|
> &
|
|
225
|
-
DropDownOwnProps<T, S> &
|
|
226
|
-
(SourceProps<T & Primitive, S> | Record<"renderItem", ListItemRenderer<T>>);
|
|
228
|
+
DropDownOwnProps<T, S> &
|
|
229
|
+
(SourceProps<T & Primitive, S> | Record<"renderItem", ListItemRenderer<T>>);
|
|
227
230
|
|
|
228
231
|
/**
|
|
229
232
|
* Native `Gtk.DropDown` props with declarative collections, controlled selection, and display/popup renderers.
|
|
@@ -263,7 +266,7 @@ type ListViewProps<T = unknown, S = unknown> = Omit<
|
|
|
263
266
|
GtkListViewProps,
|
|
264
267
|
"model" | "factory" | "headerFactory" | keyof ListViewOwnProps<T, S>
|
|
265
268
|
> &
|
|
266
|
-
ListViewOwnProps<T, S>;
|
|
269
|
+
ListViewOwnProps<T, S>;
|
|
267
270
|
|
|
268
271
|
/**
|
|
269
272
|
* Native `Adw.ComboRow` props with declarative collections, controlled selection, and display/popup renderers.
|
|
@@ -291,6 +294,42 @@ type ToastProviderProps = {
|
|
|
291
294
|
children?: ReactNode | undefined;
|
|
292
295
|
};
|
|
293
296
|
|
|
297
|
+
/** Describes the JSX page rendered for each value in a {@link TabCollection}. */
|
|
298
|
+
type TabCollectionOptions<T> = {
|
|
299
|
+
/** Native page props and JSX content. Call getPage in event handlers to access the mounted page. The collection owns its ref and parent relationship. */
|
|
300
|
+
pageProps: (item: T, getPage: () => Adw.TabPage | null) => Omit<AdwTabPageProps, "ref" | "parent">;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/** Options for adding a page to a {@link TabCollection}. */
|
|
304
|
+
type TabAppendOptions = {
|
|
305
|
+
/** Inserts a related page after this parent and its descendants, using native tab ancestry. */
|
|
306
|
+
parent?: Adw.TabPage | undefined;
|
|
307
|
+
/** Selects the new page by default. */
|
|
308
|
+
select?: boolean | undefined;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Owns independent JSX tab roots across native view transfers. Its lifetime can span several windows.
|
|
313
|
+
* Remove a page after accepting its native close request, remove a view's pages before destroying that
|
|
314
|
+
* view, and dispose the collection when its owner is finished.
|
|
315
|
+
*/
|
|
316
|
+
type TabCollection<T> = {
|
|
317
|
+
/** Renders synchronously and returns the native page, including from an overview's create-tab handler. */
|
|
318
|
+
append: (view: Adw.TabView, item: T, options?: TabAppendOptions) => Adw.TabPage;
|
|
319
|
+
/** Returns a managed page's current value, or undefined for an unmanaged page. */
|
|
320
|
+
get: (page: Adw.TabPage) => T | undefined;
|
|
321
|
+
/** Updates the value and rerenders the existing JSX root, preserving component state and native identity. */
|
|
322
|
+
update: (page: Adw.TabPage, item: T) => void;
|
|
323
|
+
/** Unmounts a managed page from its current native owner; already removed pages are ignored. */
|
|
324
|
+
remove: (page: Adw.TabPage) => void;
|
|
325
|
+
/** Unmounts this collection's pages currently belonging to the given view, including transferred pages. */
|
|
326
|
+
removeView: (view: Adw.TabView) => void;
|
|
327
|
+
/** Moves a managed page to another view, appending within its pinned or unpinned group by default. */
|
|
328
|
+
transfer: (page: Adw.TabPage, target: Adw.TabView, position?: number) => void;
|
|
329
|
+
/** Unmounts every managed page. The collection may subsequently be reused. */
|
|
330
|
+
dispose: () => void;
|
|
331
|
+
};
|
|
332
|
+
|
|
294
333
|
export {
|
|
295
334
|
type SelectionProps,
|
|
296
335
|
type ExpanderDescriptions,
|
|
@@ -315,4 +354,7 @@ export {
|
|
|
315
354
|
type ToastOptions,
|
|
316
355
|
type ToastController,
|
|
317
356
|
type ToastProviderProps,
|
|
357
|
+
type TabAppendOptions,
|
|
358
|
+
type TabCollection,
|
|
359
|
+
type TabCollectionOptions,
|
|
318
360
|
};
|