@gtkx/components 0.21.0 → 1.0.0-rc.2
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/LICENSE +373 -0
- package/README.md +173 -0
- package/dist/adw/index.d.ts +3 -0
- package/dist/adw/index.d.ts.map +1 -0
- package/dist/adw/index.js +2 -0
- package/dist/adw/index.js.map +1 -0
- package/dist/adw/toast.d.ts +10 -0
- package/dist/adw/toast.d.ts.map +1 -0
- package/dist/adw/toast.js +51 -0
- package/dist/adw/toast.js.map +1 -0
- package/dist/adw/types.d.ts +28 -0
- package/dist/adw/types.d.ts.map +1 -0
- package/dist/adw/types.js +2 -0
- package/dist/adw/types.js.map +1 -0
- package/dist/column-view.d.ts +6 -14
- package/dist/column-view.d.ts.map +1 -1
- package/dist/column-view.js +98 -100
- package/dist/column-view.js.map +1 -1
- package/dist/drop-down.d.ts +8 -37
- package/dist/drop-down.d.ts.map +1 -1
- package/dist/drop-down.js +109 -118
- package/dist/drop-down.js.map +1 -1
- package/dist/grid-view.d.ts +4 -11
- package/dist/grid-view.d.ts.map +1 -1
- package/dist/grid-view.js +25 -19
- package/dist/grid-view.js.map +1 -1
- package/dist/index.d.ts +6 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -14
- package/dist/index.js.map +1 -1
- package/dist/internal/cells.d.ts +50 -0
- package/dist/internal/cells.d.ts.map +1 -0
- package/dist/internal/cells.js +185 -0
- package/dist/internal/cells.js.map +1 -0
- package/dist/internal/collection-index.d.ts +21 -0
- package/dist/internal/collection-index.d.ts.map +1 -0
- package/dist/internal/collection-index.js +106 -0
- package/dist/internal/collection-index.js.map +1 -0
- package/dist/internal/collection-model.d.ts +12 -0
- package/dist/internal/collection-model.d.ts.map +1 -0
- package/dist/internal/collection-model.js +164 -0
- package/dist/internal/collection-model.js.map +1 -0
- package/dist/internal/collection.d.ts +18 -0
- package/dist/internal/collection.d.ts.map +1 -0
- package/dist/internal/collection.js +64 -0
- package/dist/internal/collection.js.map +1 -0
- package/dist/internal/expansion.d.ts +9 -0
- package/dist/internal/expansion.d.ts.map +1 -0
- package/dist/internal/expansion.js +75 -0
- package/dist/internal/expansion.js.map +1 -0
- package/dist/internal/selection.d.ts +12 -0
- package/dist/internal/selection.d.ts.map +1 -0
- package/dist/internal/selection.js +99 -0
- package/dist/internal/selection.js.map +1 -0
- package/dist/internal/use-collection.d.ts +18 -0
- package/dist/internal/use-collection.d.ts.map +1 -0
- package/dist/internal/use-collection.js +42 -0
- package/dist/internal/use-collection.js.map +1 -0
- package/dist/internal/use-widget-ref.d.ts +4 -0
- package/dist/internal/use-widget-ref.d.ts.map +1 -0
- package/dist/internal/use-widget-ref.js +8 -0
- package/dist/internal/use-widget-ref.js.map +1 -0
- package/dist/list-view.d.ts +4 -11
- package/dist/list-view.d.ts.map +1 -1
- package/dist/list-view.js +32 -28
- package/dist/list-view.js.map +1 -1
- package/dist/size-group.d.ts +12 -0
- package/dist/size-group.d.ts.map +1 -0
- package/dist/size-group.js +41 -0
- package/dist/size-group.js.map +1 -0
- package/dist/types.d.ts +104 -161
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +62 -52
- package/src/adw/index.ts +2 -0
- package/src/adw/toast.tsx +74 -0
- package/src/adw/types.ts +32 -0
- package/src/column-view.tsx +184 -184
- package/src/drop-down.tsx +173 -227
- package/src/grid-view.tsx +29 -65
- package/src/index.ts +16 -25
- package/src/internal/cells.tsx +345 -0
- package/src/internal/collection-index.ts +168 -0
- package/src/internal/collection-model.ts +246 -0
- package/src/internal/collection.ts +104 -0
- package/src/internal/expansion.ts +123 -0
- package/src/internal/selection.tsx +159 -0
- package/src/internal/use-collection.tsx +70 -0
- package/src/internal/use-widget-ref.ts +11 -0
- package/src/list-view.tsx +45 -78
- package/src/size-group.tsx +77 -0
- package/src/types.ts +171 -191
- package/dist/collection-view.d.ts +0 -30
- package/dist/collection-view.d.ts.map +0 -1
- package/dist/collection-view.js +0 -52
- package/dist/collection-view.js.map +0 -1
- package/dist/column-view-column.d.ts +0 -17
- package/dist/column-view-column.d.ts.map +0 -1
- package/dist/column-view-column.js +0 -51
- package/dist/column-view-column.js.map +0 -1
- package/dist/constraint-layout-apply.d.ts +0 -6
- package/dist/constraint-layout-apply.d.ts.map +0 -1
- package/dist/constraint-layout-apply.js +0 -107
- package/dist/constraint-layout-apply.js.map +0 -1
- package/dist/constraint-layout.d.ts +0 -13
- package/dist/constraint-layout.d.ts.map +0 -1
- package/dist/constraint-layout.js +0 -85
- package/dist/constraint-layout.js.map +0 -1
- package/dist/contexts/column-view-context.d.ts +0 -17
- package/dist/contexts/column-view-context.d.ts.map +0 -1
- package/dist/contexts/column-view-context.js +0 -10
- package/dist/contexts/column-view-context.js.map +0 -1
- package/dist/hooks/use-drop-down-selection.d.ts +0 -15
- package/dist/hooks/use-drop-down-selection.d.ts.map +0 -1
- package/dist/hooks/use-drop-down-selection.js +0 -35
- package/dist/hooks/use-drop-down-selection.js.map +0 -1
- package/dist/hooks/use-header-menu.d.ts +0 -4
- package/dist/hooks/use-header-menu.d.ts.map +0 -1
- package/dist/hooks/use-header-menu.js +0 -32
- package/dist/hooks/use-header-menu.js.map +0 -1
- package/dist/hooks/use-list-model.d.ts +0 -20
- package/dist/hooks/use-list-model.d.ts.map +0 -1
- package/dist/hooks/use-list-model.js +0 -70
- package/dist/hooks/use-list-model.js.map +0 -1
- package/dist/hooks/use-model-installation.d.ts +0 -9
- package/dist/hooks/use-model-installation.d.ts.map +0 -1
- package/dist/hooks/use-model-installation.js +0 -20
- package/dist/hooks/use-model-installation.js.map +0 -1
- package/dist/hooks/use-realized-slots.d.ts +0 -20
- package/dist/hooks/use-realized-slots.d.ts.map +0 -1
- package/dist/hooks/use-realized-slots.js +0 -66
- package/dist/hooks/use-realized-slots.js.map +0 -1
- package/dist/hooks/use-selection-model.d.ts +0 -12
- package/dist/hooks/use-selection-model.d.ts.map +0 -1
- package/dist/hooks/use-selection-model.js +0 -111
- package/dist/hooks/use-selection-model.js.map +0 -1
- package/dist/hooks/use-sort-handler.d.ts +0 -12
- package/dist/hooks/use-sort-handler.d.ts.map +0 -1
- package/dist/hooks/use-sort-handler.js +0 -87
- package/dist/hooks/use-sort-handler.js.map +0 -1
- package/dist/list-portal-host.d.ts +0 -11
- package/dist/list-portal-host.d.ts.map +0 -1
- package/dist/list-portal-host.js +0 -8
- package/dist/list-portal-host.js.map +0 -1
- package/dist/list-slot.d.ts +0 -14
- package/dist/list-slot.d.ts.map +0 -1
- package/dist/list-slot.js +0 -29
- package/dist/list-slot.js.map +0 -1
- package/dist/menu-items.d.ts +0 -5
- package/dist/menu-items.d.ts.map +0 -1
- package/dist/menu-items.js +0 -48
- package/dist/menu-items.js.map +0 -1
- package/dist/menu.d.ts +0 -18
- package/dist/menu.d.ts.map +0 -1
- package/dist/menu.js +0 -26
- package/dist/menu.js.map +0 -1
- package/dist/utils/item-resolver.d.ts +0 -28
- package/dist/utils/item-resolver.d.ts.map +0 -1
- package/dist/utils/item-resolver.js +0 -111
- package/dist/utils/item-resolver.js.map +0 -1
- package/dist/utils/list-item-flatten.d.ts +0 -32
- package/dist/utils/list-item-flatten.d.ts.map +0 -1
- package/dist/utils/list-item-flatten.js +0 -88
- package/dist/utils/list-item-flatten.js.map +0 -1
- package/dist/utils/position-only-model.d.ts +0 -10
- package/dist/utils/position-only-model.d.ts.map +0 -1
- package/dist/utils/position-only-model.js +0 -83
- package/dist/utils/position-only-model.js.map +0 -1
- package/dist/utils/realized-slot-store.d.ts +0 -30
- package/dist/utils/realized-slot-store.d.ts.map +0 -1
- package/dist/utils/realized-slot-store.js +0 -103
- package/dist/utils/realized-slot-store.js.map +0 -1
- package/src/collection-view.tsx +0 -125
- package/src/column-view-column.tsx +0 -73
- package/src/constraint-layout-apply.ts +0 -121
- package/src/constraint-layout.tsx +0 -130
- package/src/contexts/column-view-context.ts +0 -28
- package/src/hooks/use-drop-down-selection.ts +0 -53
- package/src/hooks/use-header-menu.ts +0 -33
- package/src/hooks/use-list-model.ts +0 -145
- package/src/hooks/use-model-installation.ts +0 -25
- package/src/hooks/use-realized-slots.ts +0 -93
- package/src/hooks/use-selection-model.ts +0 -121
- package/src/hooks/use-sort-handler.ts +0 -102
- package/src/list-portal-host.tsx +0 -27
- package/src/list-slot.tsx +0 -56
- package/src/menu-items.ts +0 -48
- package/src/menu.tsx +0 -34
- package/src/utils/item-resolver.ts +0 -143
- package/src/utils/list-item-flatten.ts +0 -113
- package/src/utils/position-only-model.ts +0 -115
- package/src/utils/realized-slot-store.ts +0 -126
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import type * as GObject from "@gtkx/gi/gobject";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
4
|
+
import { GtkTreeExpander } from "@gtkx/jsx/gtk";
|
|
5
|
+
import { createPortal, useProperty } from "@gtkx/react";
|
|
6
|
+
import { memo, useLayoutEffect, useState, useSyncExternalStore } from "react";
|
|
7
|
+
import type { HeaderRenderer, Item, ItemRenderer, RenderItemArgs } from "../types.js";
|
|
8
|
+
import type { Collection } from "./collection.js";
|
|
9
|
+
import { getId } from "./collection-model.js";
|
|
10
|
+
|
|
11
|
+
type CellSize = {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type CellHost = GObject.Object & {
|
|
17
|
+
getChild: () => Gtk.Widget | null;
|
|
18
|
+
setChild: (child: Gtk.Widget | null) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CellEntry<C extends CellHost> = {
|
|
22
|
+
key: string;
|
|
23
|
+
cell: C;
|
|
24
|
+
item: GObject.Object | null;
|
|
25
|
+
listeners: Set<() => void>;
|
|
26
|
+
subscribe: (onChange: () => void) => () => void;
|
|
27
|
+
getItem: () => GObject.Object | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type FactoryHandlers = {
|
|
31
|
+
onSetup: (cell: GObject.Object) => void;
|
|
32
|
+
onBind: (cell: GObject.Object) => void;
|
|
33
|
+
onUnbind: (cell: GObject.Object) => void;
|
|
34
|
+
onTeardown: (cell: GObject.Object) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type CellStore<C extends CellHost> = {
|
|
38
|
+
handlers: FactoryHandlers;
|
|
39
|
+
setSize: (size: CellSize) => void;
|
|
40
|
+
subscribe: (onChange: () => void) => () => void;
|
|
41
|
+
getSnapshot: () => CellEntry<C>[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type StoreState<C extends CellHost> = {
|
|
45
|
+
size: CellSize;
|
|
46
|
+
entries: Map<C, CellEntry<C>>;
|
|
47
|
+
snapshot: CellEntry<C>[] | null;
|
|
48
|
+
listeners: Set<() => void>;
|
|
49
|
+
serial: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type CellGuard<C extends CellHost> = (value: GObject.Object) => value is C;
|
|
53
|
+
|
|
54
|
+
type ItemBodyOptions = {
|
|
55
|
+
item: GObject.Object | null;
|
|
56
|
+
position: number | undefined;
|
|
57
|
+
row: Gtk.TreeListRow | null;
|
|
58
|
+
isRowExpanded: boolean | undefined;
|
|
59
|
+
render: ItemRenderer<never>;
|
|
60
|
+
collection: Collection;
|
|
61
|
+
expandedIds: string[] | null | undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type ItemCellProps = {
|
|
65
|
+
entry: CellEntry<Gtk.ListItem>;
|
|
66
|
+
render: ItemRenderer<never>;
|
|
67
|
+
collection: Collection;
|
|
68
|
+
expandedIds: string[] | null | undefined;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type ItemPortalsProps = {
|
|
72
|
+
store: CellStore<Gtk.ListItem>;
|
|
73
|
+
render: ItemRenderer<never>;
|
|
74
|
+
collection: Collection;
|
|
75
|
+
expandedIds?: string[] | null | undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type HeaderCellProps = {
|
|
79
|
+
entry: CellEntry<Gtk.ListHeader>;
|
|
80
|
+
render: HeaderRenderer<never>;
|
|
81
|
+
collection: Collection;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type HeaderPortalsProps = {
|
|
85
|
+
store: CellStore<Gtk.ListHeader>;
|
|
86
|
+
render: HeaderRenderer<never>;
|
|
87
|
+
collection: Collection;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const ItemCell = memo(ItemCellImpl);
|
|
91
|
+
const HeaderCell = memo(HeaderCellImpl);
|
|
92
|
+
|
|
93
|
+
const isListItem = (value: GObject.Object): value is Gtk.ListItem => value instanceof Gtk.ListItem;
|
|
94
|
+
const isListHeader = (value: GObject.Object): value is Gtk.ListHeader => value instanceof Gtk.ListHeader;
|
|
95
|
+
|
|
96
|
+
const placeholder = (size: CellSize): Gtk.Widget =>
|
|
97
|
+
new Gtk.Box({ widthRequest: size.width, heightRequest: size.height });
|
|
98
|
+
|
|
99
|
+
function notifyEntry<C extends CellHost>(entry: CellEntry<C>): void {
|
|
100
|
+
for (const listener of entry.listeners) {
|
|
101
|
+
listener();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function notifyRoster<C extends CellHost>(state: StoreState<C>): void {
|
|
106
|
+
state.snapshot = null;
|
|
107
|
+
|
|
108
|
+
for (const listener of state.listeners) {
|
|
109
|
+
listener();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function createEntry<C extends CellHost>(state: StoreState<C>, cell: C): CellEntry<C> {
|
|
114
|
+
state.serial += 1;
|
|
115
|
+
|
|
116
|
+
const entry: CellEntry<C> = {
|
|
117
|
+
key: `gtkx-cell-${String(state.serial)}`,
|
|
118
|
+
cell,
|
|
119
|
+
item: null,
|
|
120
|
+
listeners: new Set(),
|
|
121
|
+
subscribe: (onChange) => {
|
|
122
|
+
entry.listeners.add(onChange);
|
|
123
|
+
|
|
124
|
+
return () => {
|
|
125
|
+
entry.listeners.delete(onChange);
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
getItem: () => entry.item,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return entry;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function setupCell<C extends CellHost>(state: StoreState<C>, cell: C): void {
|
|
135
|
+
cell.setChild(placeholder(state.size));
|
|
136
|
+
state.entries.set(cell, createEntry(state, cell));
|
|
137
|
+
notifyRoster(state);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function teardownCell<C extends CellHost>(state: StoreState<C>, cell: C): void {
|
|
141
|
+
if (state.entries.delete(cell)) {
|
|
142
|
+
notifyRoster(state);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function writeBinding<C extends CellHost>(state: StoreState<C>, cell: C, item: GObject.Object | null): void {
|
|
147
|
+
const entry = state.entries.get(cell);
|
|
148
|
+
|
|
149
|
+
if (entry === undefined) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
entry.item = item;
|
|
154
|
+
notifyEntry(entry);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function boundItem(cell: CellHost): GObject.Object | null {
|
|
158
|
+
if (cell instanceof Gtk.ListItem) {
|
|
159
|
+
return cell.getItem();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return cell instanceof Gtk.ListHeader ? cell.getItem() : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function bindCell<C extends CellHost>(state: StoreState<C>, cell: C): void {
|
|
166
|
+
if (cell.getChild() === null) {
|
|
167
|
+
cell.setChild(placeholder(state.size));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
writeBinding(state, cell, boundItem(cell));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function subscribeRoster<C extends CellHost>(state: StoreState<C>, onChange: () => void): () => void {
|
|
174
|
+
state.listeners.add(onChange);
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
state.listeners.delete(onChange);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function rosterSnapshot<C extends CellHost>(state: StoreState<C>): CellEntry<C>[] {
|
|
182
|
+
state.snapshot ??= state.entries.values().toArray();
|
|
183
|
+
|
|
184
|
+
return state.snapshot;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function guarded<C extends CellHost>(isCell: CellGuard<C>, run: (cell: C) => void): (cell: GObject.Object) => void {
|
|
188
|
+
return (cell) => {
|
|
189
|
+
if (isCell(cell)) {
|
|
190
|
+
run(cell);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function createHandlers<C extends CellHost>(state: StoreState<C>, isCell: CellGuard<C>): FactoryHandlers {
|
|
196
|
+
return {
|
|
197
|
+
onSetup: guarded(isCell, (cell) => {
|
|
198
|
+
setupCell(state, cell);
|
|
199
|
+
}),
|
|
200
|
+
onBind: guarded(isCell, (cell) => {
|
|
201
|
+
bindCell(state, cell);
|
|
202
|
+
}),
|
|
203
|
+
onUnbind: guarded(isCell, (cell) => {
|
|
204
|
+
writeBinding(state, cell, null);
|
|
205
|
+
}),
|
|
206
|
+
onTeardown: guarded(isCell, (cell) => {
|
|
207
|
+
teardownCell(state, cell);
|
|
208
|
+
}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function createCellStore<C extends CellHost>(isCell: CellGuard<C>, size: CellSize): CellStore<C> {
|
|
213
|
+
const state: StoreState<C> = {
|
|
214
|
+
size,
|
|
215
|
+
entries: new Map(),
|
|
216
|
+
snapshot: null,
|
|
217
|
+
listeners: new Set(),
|
|
218
|
+
serial: 0,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
handlers: createHandlers(state, isCell),
|
|
223
|
+
setSize: (next) => {
|
|
224
|
+
state.size = next;
|
|
225
|
+
},
|
|
226
|
+
subscribe: (onChange) => subscribeRoster(state, onChange),
|
|
227
|
+
getSnapshot: () => rosterSnapshot(state),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function useCellStore<C extends CellHost>(isCell: CellGuard<C>, size: CellSize): CellStore<C> {
|
|
232
|
+
const [store] = useState(() => createCellStore(isCell, size));
|
|
233
|
+
|
|
234
|
+
useLayoutEffect(() => {
|
|
235
|
+
store.setSize(size);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return store;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function useItemCells(size: CellSize): CellStore<Gtk.ListItem> {
|
|
242
|
+
return useCellStore(isListItem, size);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function useHeaderCells(size: CellSize): CellStore<Gtk.ListHeader> {
|
|
246
|
+
return useCellStore(isListHeader, size);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isRowWanted(options: ItemBodyOptions, item: Item): boolean {
|
|
250
|
+
const { expandedIds } = options;
|
|
251
|
+
|
|
252
|
+
return expandedIds == null ? options.isRowExpanded === true : expandedIds.includes(item.id);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function itemArgs(item: Item, options: ItemBodyOptions): RenderItemArgs<unknown> {
|
|
256
|
+
const args: RenderItemArgs<unknown> = { item: item.value, index: options.position ?? 0 };
|
|
257
|
+
const row = options.row;
|
|
258
|
+
|
|
259
|
+
if (row === null) {
|
|
260
|
+
return args;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
args.depth = row.getDepth();
|
|
264
|
+
|
|
265
|
+
if (row.isExpandable()) {
|
|
266
|
+
args.isExpanded = isRowWanted(options, item);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return args;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function wrapExpander(item: Item, row: Gtk.TreeListRow | null, content: ReactNode): ReactNode {
|
|
273
|
+
const expander: ReactNode =
|
|
274
|
+
row === null
|
|
275
|
+
? (
|
|
276
|
+
content
|
|
277
|
+
)
|
|
278
|
+
: (
|
|
279
|
+
<GtkTreeExpander
|
|
280
|
+
listRow={row}
|
|
281
|
+
hideExpander={item.hideExpander ?? false}
|
|
282
|
+
indentForDepth={item.indentForDepth ?? true}
|
|
283
|
+
indentForIcon={item.indentForIcon ?? true}
|
|
284
|
+
>
|
|
285
|
+
{content}
|
|
286
|
+
</GtkTreeExpander>
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
return expander;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function itemBody(options: ItemBodyOptions): ReactNode {
|
|
293
|
+
const id = getId(options.item);
|
|
294
|
+
const item = id === null ? undefined : options.collection.itemFor(id);
|
|
295
|
+
|
|
296
|
+
if (item === undefined) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const render = options.render as ItemRenderer<unknown>;
|
|
301
|
+
|
|
302
|
+
return wrapExpander(item, options.row, render(itemArgs(item, options)));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function ItemCellImpl({ entry, render, collection, expandedIds }: ItemCellProps): ReactNode {
|
|
306
|
+
const item = useSyncExternalStore(entry.subscribe, entry.getItem);
|
|
307
|
+
const position = useProperty(entry.cell, "position");
|
|
308
|
+
const row = item instanceof Gtk.TreeListRow ? item : null;
|
|
309
|
+
const isRowExpanded = useProperty(row, "expanded");
|
|
310
|
+
const body = itemBody({ item, position, row, isRowExpanded, render, collection, expandedIds });
|
|
311
|
+
|
|
312
|
+
return createPortal(body, entry.cell, entry.key);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function HeaderCellImpl({ entry, render, collection }: HeaderCellProps): ReactNode {
|
|
316
|
+
const item = useSyncExternalStore(entry.subscribe, entry.getItem);
|
|
317
|
+
const id = getId(item);
|
|
318
|
+
const renderHeader = render as HeaderRenderer<unknown>;
|
|
319
|
+
const body = id === null ? null : renderHeader({ section: collection.sectionFor(id) });
|
|
320
|
+
|
|
321
|
+
return createPortal(body, entry.cell, entry.key);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function usePortalEntries<C extends CellHost>(store: CellStore<C>): CellEntry<C>[] {
|
|
325
|
+
return useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const ItemPortals = ({ store, render, collection, expandedIds }: ItemPortalsProps): ReactNode =>
|
|
329
|
+
usePortalEntries(store).map((entry) => (
|
|
330
|
+
<ItemCell key={entry.key} entry={entry} render={render} collection={collection} expandedIds={expandedIds} />
|
|
331
|
+
));
|
|
332
|
+
|
|
333
|
+
const HeaderPortals = ({ store, render, collection }: HeaderPortalsProps): ReactNode =>
|
|
334
|
+
usePortalEntries(store).map((entry) => (
|
|
335
|
+
<HeaderCell key={entry.key} entry={entry} render={render} collection={collection} />
|
|
336
|
+
));
|
|
337
|
+
|
|
338
|
+
export {
|
|
339
|
+
useItemCells,
|
|
340
|
+
useHeaderCells,
|
|
341
|
+
ItemPortals,
|
|
342
|
+
HeaderPortals,
|
|
343
|
+
type CellSize,
|
|
344
|
+
type CellStore,
|
|
345
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { Item, Section } from "../types.js";
|
|
2
|
+
|
|
3
|
+
type Level = {
|
|
4
|
+
key: string;
|
|
5
|
+
ids: string[];
|
|
6
|
+
isExpandable: boolean[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type CollectionIndex = {
|
|
10
|
+
isTree: boolean;
|
|
11
|
+
size: number;
|
|
12
|
+
groups: Level[];
|
|
13
|
+
children: Map<string, Level>;
|
|
14
|
+
has: (id: string) => boolean;
|
|
15
|
+
itemFor: (id: string) => Item | undefined;
|
|
16
|
+
sectionFor: (id: string) => unknown;
|
|
17
|
+
positionFor: (id: string) => number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type IndexState = {
|
|
21
|
+
isTree: boolean;
|
|
22
|
+
groups: Level[];
|
|
23
|
+
children: Map<string, Level>;
|
|
24
|
+
itemsById: Map<string, Item>;
|
|
25
|
+
sections: Section[] | undefined;
|
|
26
|
+
positions: Map<string, number> | null;
|
|
27
|
+
starts: number[] | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ROOT_LEVEL_KEY = "";
|
|
31
|
+
|
|
32
|
+
const hasChildren = (item: Item): boolean => item.children !== undefined && item.children.length > 0;
|
|
33
|
+
const childLevelKey = (id: string): string => `child:${id}`;
|
|
34
|
+
|
|
35
|
+
function collectChildren(state: IndexState, level: Level, item: Item): void {
|
|
36
|
+
if (!state.isTree) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const canExpand = hasChildren(item);
|
|
41
|
+
level.isExpandable.push(canExpand);
|
|
42
|
+
|
|
43
|
+
if (!canExpand) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const key = childLevelKey(item.id);
|
|
48
|
+
state.children.set(key, collectLevel(state, key, item.children ?? []));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collectLevel(state: IndexState, key: string, items: Item[]): Level {
|
|
52
|
+
const level: Level = { key, ids: [], isExpandable: [] };
|
|
53
|
+
|
|
54
|
+
for (const item of items) {
|
|
55
|
+
level.ids.push(item.id);
|
|
56
|
+
state.itemsById.set(item.id, item);
|
|
57
|
+
collectChildren(state, level, item);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return level;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildPositions(state: IndexState): void {
|
|
64
|
+
const positions: Map<string, number> = new Map();
|
|
65
|
+
const starts: number[] = [];
|
|
66
|
+
let offset = 0;
|
|
67
|
+
|
|
68
|
+
for (const level of state.groups) {
|
|
69
|
+
starts.push(offset);
|
|
70
|
+
|
|
71
|
+
for (const id of level.ids) {
|
|
72
|
+
positions.set(id, offset);
|
|
73
|
+
offset += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
state.positions = positions;
|
|
78
|
+
state.starts = starts;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function positionFor(state: IndexState, id: string): number {
|
|
82
|
+
if (state.isTree) {
|
|
83
|
+
return -1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (state.positions === null) {
|
|
87
|
+
buildPositions(state);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return state.positions?.get(id) ?? -1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sectionAt(starts: number[], position: number): number {
|
|
94
|
+
let low = 0;
|
|
95
|
+
let high = starts.length - 1;
|
|
96
|
+
|
|
97
|
+
while (low < high) {
|
|
98
|
+
const middle = Math.ceil((low + high) / 2);
|
|
99
|
+
|
|
100
|
+
if ((starts[middle] ?? 0) <= position) {
|
|
101
|
+
low = middle;
|
|
102
|
+
} else {
|
|
103
|
+
high = middle - 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return low;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sectionFor(state: IndexState, id: string): unknown {
|
|
111
|
+
const sections = state.sections;
|
|
112
|
+
const position = positionFor(state, id);
|
|
113
|
+
|
|
114
|
+
if (sections === undefined || position < 0 || state.starts === null) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return sections[sectionAt(state.starts, position)]?.value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildGroups(state: IndexState, source: Item[], sections: Section[] | undefined): Level[] {
|
|
122
|
+
if (sections === undefined) {
|
|
123
|
+
return [collectLevel(state, ROOT_LEVEL_KEY, source)];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return sections.map((section) => collectLevel(state, section.id, section.data));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isTreeSource(source: Item[], sections: Section[] | undefined, isFlat: boolean): boolean {
|
|
130
|
+
if (sections !== undefined || isFlat) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return source.some((item) => hasChildren(item));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function createCollectionIndex(
|
|
138
|
+
items: Item[] | undefined,
|
|
139
|
+
sections: Section[] | undefined,
|
|
140
|
+
isFlat: boolean,
|
|
141
|
+
): CollectionIndex {
|
|
142
|
+
const source = items ?? [];
|
|
143
|
+
|
|
144
|
+
const state: IndexState = {
|
|
145
|
+
isTree: isTreeSource(source, sections, isFlat),
|
|
146
|
+
groups: [],
|
|
147
|
+
children: new Map(),
|
|
148
|
+
itemsById: new Map(),
|
|
149
|
+
sections,
|
|
150
|
+
positions: null,
|
|
151
|
+
starts: null,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
state.groups = buildGroups(state, source, sections);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
isTree: state.isTree,
|
|
158
|
+
size: state.itemsById.size,
|
|
159
|
+
groups: state.groups,
|
|
160
|
+
children: state.children,
|
|
161
|
+
has: (id) => state.itemsById.has(id),
|
|
162
|
+
itemFor: (id) => state.itemsById.get(id),
|
|
163
|
+
sectionFor: (id) => sectionFor(state, id),
|
|
164
|
+
positionFor: (id) => positionFor(state, id),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { createCollectionIndex, childLevelKey, ROOT_LEVEL_KEY, type CollectionIndex, type Level };
|