@decky.fx/raven-tsx 0.0.0-stage → 0.1.1
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 +202 -0
- package/NOTICE +12 -0
- package/RAVEN_VERSION +1 -0
- package/README.md +444 -2
- package/package.json +75 -4
- package/src/core/errors.ts +20 -0
- package/src/core/events.ts +925 -0
- package/src/core/frame.ts +76 -0
- package/src/core/index.ts +28 -0
- package/src/core/library-check.ts +89 -0
- package/src/core/native.ts +82 -0
- package/src/core/plot.ts +179 -0
- package/src/core/props.generated.ts +172 -0
- package/src/core/props.ts +158 -0
- package/src/core/raven.ts +10 -0
- package/src/core/subscriptions.ts +78 -0
- package/src/core/tags.ts +307 -0
- package/src/core/theme.ts +207 -0
- package/src/core/web.ts +84 -0
- package/src/core/widget-instance.ts +1057 -0
- package/src/core/window-instance.ts +671 -0
- package/src/react/components.generated.ts +179 -0
- package/src/react/hooks.ts +51 -0
- package/src/react/host-config.ts +138 -0
- package/src/react/index.ts +26 -0
- package/src/react/root.ts +119 -0
- package/src/react/tags.ts +50 -0
- package/src/react/web.ts +5 -0
- package/src/solid/babel-preset-solid.d.ts +7 -0
- package/src/solid/components.generated.ts +163 -0
- package/src/solid/index.ts +29 -0
- package/src/solid/node.ts +405 -0
- package/src/solid/plugin.ts +108 -0
- package/src/solid/portal.ts +27 -0
- package/src/solid/preload.ts +14 -0
- package/src/solid/primitives.ts +43 -0
- package/src/solid/renderer.ts +37 -0
- package/src/solid/root.ts +137 -0
- package/src/solid/tag.ts +37 -0
- package/src/solid/tags.ts +101 -0
- package/src/solid/teardown.ts +50 -0
- package/src/solid/web.ts +5 -0
- package/src/vendor/raven.ts +12652 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import * as r from "./raven";
|
|
2
|
+
import { RavenError } from "./errors";
|
|
3
|
+
import { closeEvents, WINDOW_EVENTS } from "./events";
|
|
4
|
+
import type { EventHandler } from "./props";
|
|
5
|
+
import { escapeXml, resolveProps, type RavenProps, type ResolvedProps } from "./props";
|
|
6
|
+
import { WidgetInstance, type HostNode } from "./widget-instance";
|
|
7
|
+
|
|
8
|
+
/** Where the widgets of a window go. Its size is the size of the window. */
|
|
9
|
+
export const CONTENT_NAME = "raven-tsx.content";
|
|
10
|
+
|
|
11
|
+
/** What each way of writing `Backdrop` means, as the XML loader reads it. */
|
|
12
|
+
const BACKDROPS: Readonly<Record<string, r.RBackdrop>> = {
|
|
13
|
+
None: r.R_BACKDROP_NONE,
|
|
14
|
+
Opaque: r.R_BACKDROP_NONE,
|
|
15
|
+
Transparent: r.R_BACKDROP_TRANSPARENT,
|
|
16
|
+
Blur: r.R_BACKDROP_BLUR,
|
|
17
|
+
Acrylic: r.R_BACKDROP_ACRYLIC,
|
|
18
|
+
Mica: r.R_BACKDROP_MICA,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** What a window can take while it is open, by calling Raven. */
|
|
22
|
+
const SET_ON_OPEN_WINDOW: Readonly<Record<string, (window: r.Rid, value: string) => void>> = {
|
|
23
|
+
Title: (window, value) => r.Rwindow_set_title(window, value),
|
|
24
|
+
Theme: (window, value) => void r.Rwindow_set_theme(window, value),
|
|
25
|
+
Backdrop: (window, value) => {
|
|
26
|
+
const backdrop = BACKDROPS[value];
|
|
27
|
+
if (backdrop === undefined || !r.Rwindow_set_backdrop(window, backdrop)) {
|
|
28
|
+
console.warn(`raven-tsx: <Window> did not take Backdrop="${value}": it needs Transparent, and a window system that does it`);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
Opacity: (window, value) => {
|
|
32
|
+
if (!r.Rwindow_set_opacity(window, Number(value))) {
|
|
33
|
+
console.warn("raven-tsx: this window system cannot make a window see-through with Opacity");
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
ContinuousRedraw: (window, value) => r.Rwindow_set_continuous_redraw(window, value === "true" ? r.R_TRUE : r.R_FALSE),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** What the XML of a window does not take: it is set by calling Raven, once the window is there. */
|
|
40
|
+
const NOT_IN_XML: ReadonlySet<string> = new Set(["Opacity", "ContinuousRedraw"]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Makes a window from attributes, with a place for widgets. That place is an `Overlay`, as for the
|
|
44
|
+
* content of a window in C: a child fills the window and follows its `Slot`. A `VBox` would not let
|
|
45
|
+
* it, and children that should stack go in one.
|
|
46
|
+
*/
|
|
47
|
+
export function makeWindow(attributes: ReadonlyMap<string, string>): { window: r.Rid; content: r.Rid } {
|
|
48
|
+
const text = [...attributes].filter(([name]) => !NOT_IN_XML.has(name)).map(([name, value]) => `${name}="${escapeXml(value)}"`).join(" ");
|
|
49
|
+
const window = r.Rwindow_create_from_xml(`<Window ${text}><Overlay Name="${CONTENT_NAME}"/></Window>`);
|
|
50
|
+
if (window === r.R_INVALID_ID) {
|
|
51
|
+
RavenError.fromLibrary("Raven cannot make the window");
|
|
52
|
+
}
|
|
53
|
+
for (const [name, value] of attributes) {
|
|
54
|
+
if (NOT_IN_XML.has(name)) {
|
|
55
|
+
SET_ON_OPEN_WINDOW[name]?.(window, value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { window, content: r.Rwidget_find(window, CONTENT_NAME) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Sets or removes the events of a window, from what its props say now. */
|
|
62
|
+
function setWindowEvents(window: r.Rid, handlers: ReadonlyMap<string, EventHandler>): void {
|
|
63
|
+
for (const [name, event] of Object.entries(WINDOW_EVENTS)) {
|
|
64
|
+
const handler = handlers.get(name);
|
|
65
|
+
if (handler) {
|
|
66
|
+
event.on(window, handler);
|
|
67
|
+
} else {
|
|
68
|
+
event.off(window, true);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A `<Window>` under a framework, and what a `ref` receives. It holds widgets but stands by itself:
|
|
75
|
+
* it can be anywhere in the tree, and goes away when its element does. It opens once it is placed:
|
|
76
|
+
* a render the framework throws away opens nothing.
|
|
77
|
+
*/
|
|
78
|
+
/** Throws the first of some failures, once all that could run has run. */
|
|
79
|
+
function throwFirst(failures: readonly unknown[]): void {
|
|
80
|
+
if (failures.length > 0) {
|
|
81
|
+
throw failures[0];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Runs a function, and keeps its failure with the others, to go on with what must be done. */
|
|
86
|
+
function attempt(failures: unknown[], run: () => void): void {
|
|
87
|
+
try {
|
|
88
|
+
run();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
failures.push(error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A function that runs when a window opens, and what it gave to run when it closes. */
|
|
95
|
+
interface Opening {
|
|
96
|
+
readonly run: () => (() => void) | void;
|
|
97
|
+
cleanup: (() => void) | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class WindowInstance implements HostNode {
|
|
101
|
+
readonly tag = "Window";
|
|
102
|
+
readonly nativeWidget = null;
|
|
103
|
+
|
|
104
|
+
get placed(): boolean {
|
|
105
|
+
return this.state === "live" && this.isOpen;
|
|
106
|
+
}
|
|
107
|
+
private id: r.Rid = r.R_INVALID_ID;
|
|
108
|
+
/** Where its widgets go, once it is open. */
|
|
109
|
+
private content: WidgetInstance | null = null;
|
|
110
|
+
/** The children rendered with it, which go in when it opens. */
|
|
111
|
+
private readonly initial: HostNode[] = [];
|
|
112
|
+
/** Its `<Island>`: it floats above the content, as in XML, and the window owns it. */
|
|
113
|
+
private island: WidgetInstance | null = null;
|
|
114
|
+
private state: "pending" | "live" | "closing" | "gone" = "pending";
|
|
115
|
+
/** A dispose asked for while the window closes: done once it is closed. */
|
|
116
|
+
private disposeAsked = false;
|
|
117
|
+
/** Closed because what holds it left the tree (`closeWindows`): `openWindows` opens it again. */
|
|
118
|
+
private hidden = false;
|
|
119
|
+
/** What runs each time the window opens, with what it returned to run when it closes. */
|
|
120
|
+
private readonly opening = new Set<Opening>();
|
|
121
|
+
/** How many times it opened: a cleanup returned for an opening that is over runs at once. */
|
|
122
|
+
private openings = 0;
|
|
123
|
+
/** While it opens, before its opening pass: a function `whenOpen` gets then runs in that pass, once. */
|
|
124
|
+
private draining = false;
|
|
125
|
+
/** Its handlers were forgotten (`forgetTree`): placed again alive, it gets them back. */
|
|
126
|
+
private forgotten = false;
|
|
127
|
+
/** The widget that holds it in the tree, if any: one at the top of a root, or in a window, has none. */
|
|
128
|
+
private holder: WidgetInstance | null = null;
|
|
129
|
+
/** What the application put in its content itself, kept alive while it is closed. */
|
|
130
|
+
private readonly keptNative: r.Rid[] = [];
|
|
131
|
+
/** The widgets of its content when it closed, in order: what the application put there goes back to its place. */
|
|
132
|
+
private keptOrder: r.Rid[] = [];
|
|
133
|
+
|
|
134
|
+
private constructor(private resolved: ResolvedProps) {}
|
|
135
|
+
|
|
136
|
+
/** A new window, as a framework renders it: it opens at `materialize`, with what its props say. */
|
|
137
|
+
static create<Child>(props: RavenProps<Child>): WindowInstance {
|
|
138
|
+
return new WindowInstance(resolveProps(props));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Raven's handle of the window, for the functions of the `raven` export: `R_INVALID_ID` until it is placed. */
|
|
142
|
+
get window(): r.Rid {
|
|
143
|
+
return this.id;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Opens the window, with its children. If any of it fails, the window is deleted. */
|
|
147
|
+
materialize(): void {
|
|
148
|
+
if (this.state !== "pending") {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// (what holds it was taken out of the tree, alive: it opens once that is placed again, at `openWindows`)
|
|
152
|
+
if (this.holder && !this.holder.attached) {
|
|
153
|
+
this.hidden = true;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const { window, content } = makeWindow(this.resolved.attributes);
|
|
157
|
+
// (once it is made: one that could not be made still opens with its tree)
|
|
158
|
+
this.hidden = false;
|
|
159
|
+
this.id = window;
|
|
160
|
+
this.content = WidgetInstance.forContainer(content);
|
|
161
|
+
this.state = "live";
|
|
162
|
+
// (user code run by a child may close the window, and open it again: that is another window,
|
|
163
|
+
// whose opening is done; this one is over)
|
|
164
|
+
const current = (): boolean => this.state === "live" && this.id === window;
|
|
165
|
+
this.draining = true;
|
|
166
|
+
try {
|
|
167
|
+
closeEvents.set(window, this.resolved.handlers.get("OnClose"));
|
|
168
|
+
// (with no OnClose, the user closes it at any time: what its openings gave runs then)
|
|
169
|
+
closeEvents.notice(window, () => this.closedByUser());
|
|
170
|
+
setWindowEvents(window, this.resolved.handlers);
|
|
171
|
+
// (each child goes in, also past one that fails: then the window goes, with all of them)
|
|
172
|
+
// (one at a time: one that user code released meanwhile is no longer waiting here. And not
|
|
173
|
+
// once user code closed the window: what is left waits for the next opening)
|
|
174
|
+
const failures: unknown[] = [];
|
|
175
|
+
while (current()) {
|
|
176
|
+
const child = this.initial.shift();
|
|
177
|
+
if (!child) {
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
attempt(failures, () => this.append(child));
|
|
181
|
+
}
|
|
182
|
+
throwFirst(failures);
|
|
183
|
+
if (current()) {
|
|
184
|
+
this.restoreNative(content);
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// (only this opening goes: one a child opened again meanwhile, after closing this one, stays)
|
|
188
|
+
if (!(this.state === "live" && this.id !== window)) {
|
|
189
|
+
this.forgetTree();
|
|
190
|
+
this.dispose();
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
// (closed while it opened: that was no opening, and the next placing opens it)
|
|
195
|
+
if (!current()) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this.draining = false;
|
|
199
|
+
this.openings++;
|
|
200
|
+
this.runOpening();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Puts back what the application put in the content itself, each just before the first widget
|
|
205
|
+
* that came after it when the window closed: its place among the children of the framework.
|
|
206
|
+
*/
|
|
207
|
+
private restoreNative(content: r.Rid): void {
|
|
208
|
+
const order = this.keptOrder;
|
|
209
|
+
this.keptOrder = [];
|
|
210
|
+
for (const widget of this.keptNative.splice(0)) {
|
|
211
|
+
// (one the application placed elsewhere meanwhile is that place's)
|
|
212
|
+
if (!this.stillKept(widget)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const at = order.indexOf(widget);
|
|
216
|
+
const after = new Set(at < 0 ? [] : order.slice(at + 1));
|
|
217
|
+
const count = r.Rwidget_get_child_count(content);
|
|
218
|
+
let position = count;
|
|
219
|
+
for (let index = 0; index < count; index++) {
|
|
220
|
+
if (after.has(r.Rwidget_get_child(content, index))) {
|
|
221
|
+
position = index;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
r.Rwidget_insert_child(content, widget, position);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Destroys what the application put in the content, kept while the window was closed. */
|
|
230
|
+
private destroyKeptNative(): void {
|
|
231
|
+
this.keptOrder = [];
|
|
232
|
+
for (const widget of this.keptNative.splice(0)) {
|
|
233
|
+
if (this.stillKept(widget)) {
|
|
234
|
+
r.Rwidget_destroy(widget);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Whether a widget kept while the window is closed is still its own: alive, and in no other parent. */
|
|
240
|
+
private stillKept(widget: r.Rid): boolean {
|
|
241
|
+
return r.Rwidget_is_valid(widget) !== 0 && r.Rwidget_get_parent(widget) === r.R_INVALID_ID;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Runs what waits for the window to open, each once: the first that threw says so after. */
|
|
245
|
+
private runOpening(): void {
|
|
246
|
+
// (a failure of one is not a failure of the window, which stays. One cancelled meanwhile, or
|
|
247
|
+
// all once the window is gone, disposed or destroyed in Raven, do not run)
|
|
248
|
+
const failures: unknown[] = [];
|
|
249
|
+
const openings = this.openings;
|
|
250
|
+
for (const opening of [...this.opening]) {
|
|
251
|
+
// (a function that closed the window and opened it again started a pass of its own)
|
|
252
|
+
if (this.openings !== openings) {
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (!this.opening.has(opening) || !this.isOpen) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
this.runOpeningOf(opening);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
failures.push(error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
throwFirst(failures);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Runs one function of an opening, and keeps what it returns for the closing. If the function
|
|
269
|
+
* cancelled itself or closed the window, that opening is over: what it returned runs now.
|
|
270
|
+
*/
|
|
271
|
+
private runOpeningOf(opening: Opening): void {
|
|
272
|
+
const openings = this.openings;
|
|
273
|
+
const cleanup = opening.run() ?? null;
|
|
274
|
+
if (this.opening.has(opening) && this.state === "live" && this.isOpen && this.openings === openings) {
|
|
275
|
+
opening.cleanup = cleanup;
|
|
276
|
+
} else {
|
|
277
|
+
cleanup?.();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Takes the cleanups of the opening that ends, to run once the window is closed. */
|
|
282
|
+
private takeClosing(): (() => void)[] {
|
|
283
|
+
const cleanups: (() => void)[] = [];
|
|
284
|
+
for (const opening of this.opening) {
|
|
285
|
+
if (opening.cleanup) {
|
|
286
|
+
cleanups.push(opening.cleanup);
|
|
287
|
+
opening.cleanup = null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return cleanups;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Runs a function each time the window opens: now if it is open, and again if it is closed and
|
|
295
|
+
* placed again (`close`). What it returns runs when the window closes. Returns what cancels it,
|
|
296
|
+
* running that too. A framework's ref gives the window before it opens.
|
|
297
|
+
*/
|
|
298
|
+
whenOpen(run: () => (() => void) | void): () => void {
|
|
299
|
+
if (this.state === "gone") {
|
|
300
|
+
return () => {};
|
|
301
|
+
}
|
|
302
|
+
const opening: Opening = { run, cleanup: null };
|
|
303
|
+
this.opening.add(opening);
|
|
304
|
+
// (a window the user closed, or Raven destroyed, does not open again. One that opens runs it with the rest)
|
|
305
|
+
if (this.state === "live" && this.isOpen && !this.draining) {
|
|
306
|
+
try {
|
|
307
|
+
this.runOpeningOf(opening);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
// (the caller gets no way to cancel it: it is not kept. What a run of it gave meanwhile,
|
|
310
|
+
// opening the window again, runs now: nothing would run it after)
|
|
311
|
+
this.opening.delete(opening);
|
|
312
|
+
const cleanup = opening.cleanup;
|
|
313
|
+
opening.cleanup = null;
|
|
314
|
+
try {
|
|
315
|
+
cleanup?.();
|
|
316
|
+
} catch (cleanupError) {
|
|
317
|
+
console.error("raven-tsx: a cleanup of whenOpen failed:", cleanupError);
|
|
318
|
+
}
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return () => {
|
|
323
|
+
this.opening.delete(opening);
|
|
324
|
+
const cleanup = opening.cleanup;
|
|
325
|
+
opening.cleanup = null;
|
|
326
|
+
cleanup?.();
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Closes the window and keeps what it holds, alive, to open again when it is placed again: the
|
|
332
|
+
* same instance, as its `ref` gave it. A framework that takes a window out of its tree, and may
|
|
333
|
+
* put it back, closes it. (`window` is then another handle)
|
|
334
|
+
*/
|
|
335
|
+
close(): void {
|
|
336
|
+
if (this.state !== "live") {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (!this.isOpen) {
|
|
340
|
+
// (closed by the user, or destroyed in Raven, with what it held: it does not open again)
|
|
341
|
+
this.forgetTree();
|
|
342
|
+
this.dispose();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// (while it closes, what user code does to it waits: whenOpen for the next opening, dispose for the end)
|
|
346
|
+
this.state = "closing";
|
|
347
|
+
const failures: unknown[] = [];
|
|
348
|
+
const cleanups = this.takeClosing();
|
|
349
|
+
// (what it holds is not shown from now: a window a cleanup adds in it opens at the next opening)
|
|
350
|
+
this.content?.setShown(false);
|
|
351
|
+
this.island?.setShown(false);
|
|
352
|
+
const content = this.content?.widget ?? r.R_INVALID_ID;
|
|
353
|
+
// (the order of the content, before the children of the framework leave it)
|
|
354
|
+
const order = r.Rwidget_is_valid(content) ? Array.from({ length: r.Rwidget_get_child_count(content) }, (_, index) => r.Rwidget_get_child(content, index)) : [];
|
|
355
|
+
// (the windows in it close too, kept to open with it)
|
|
356
|
+
attempt(failures, () => this.content?.closeWindows());
|
|
357
|
+
attempt(failures, () => this.island?.closeWindows());
|
|
358
|
+
const children = this.content?.releaseChildren() ?? [];
|
|
359
|
+
const island = this.island;
|
|
360
|
+
if (island) {
|
|
361
|
+
this.detachIsland(island);
|
|
362
|
+
}
|
|
363
|
+
// (what the application put in the content itself stays alive too. What is still kept from
|
|
364
|
+
// before, not put back since, keeps its place after)
|
|
365
|
+
this.keptOrder = [...order, ...this.keptOrder.filter((widget) => !order.includes(widget))];
|
|
366
|
+
while (r.Rwidget_is_valid(content) && r.Rwidget_get_child_count(content) > 0) {
|
|
367
|
+
const widget = r.Rwidget_get_child(content, 0);
|
|
368
|
+
r.Rwidget_remove_child(content, widget);
|
|
369
|
+
this.keptNative.push(widget);
|
|
370
|
+
}
|
|
371
|
+
closeEvents.set(this.id, undefined, false);
|
|
372
|
+
closeEvents.notice(this.id, undefined, false);
|
|
373
|
+
for (const event of Object.values(WINDOW_EVENTS)) {
|
|
374
|
+
event.off(this.id, false);
|
|
375
|
+
}
|
|
376
|
+
if (this.isOpen) {
|
|
377
|
+
r.Rwindow_destroy(this.id);
|
|
378
|
+
}
|
|
379
|
+
this.id = r.R_INVALID_ID;
|
|
380
|
+
this.content = null;
|
|
381
|
+
this.state = "pending";
|
|
382
|
+
// (before those that still waited: closed while it opened, it had placed these first. The island
|
|
383
|
+
// first: another Island among the children would take its place)
|
|
384
|
+
this.initial.unshift(...(island ? [island] : []), ...children);
|
|
385
|
+
// (last: a cleanup sees the window closed)
|
|
386
|
+
for (const cleanup of cleanups) {
|
|
387
|
+
attempt(failures, cleanup);
|
|
388
|
+
}
|
|
389
|
+
if (this.disposeAsked) {
|
|
390
|
+
this.disposeAsked = false;
|
|
391
|
+
attempt(failures, () => this.dispose());
|
|
392
|
+
}
|
|
393
|
+
throwFirst(failures);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Whether the window still exists: without an `OnClose`, the user can close it at any time. */
|
|
397
|
+
get isOpen(): boolean {
|
|
398
|
+
return this.id !== r.R_INVALID_ID && r.Rwindow_is_valid(this.id) !== 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Gives back what `forgetTree` took: a cleanup placed it elsewhere, alive, as its tree was deleted. */
|
|
402
|
+
rememberTree(): void {
|
|
403
|
+
if (!this.forgotten) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
this.forgotten = false;
|
|
407
|
+
if (this.state !== "live" || !this.isOpen) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
this.content?.rememberTree();
|
|
411
|
+
this.island?.rememberTree();
|
|
412
|
+
closeEvents.set(this.id, this.resolved.handlers.get("OnClose"));
|
|
413
|
+
closeEvents.notice(this.id, () => this.closedByUser());
|
|
414
|
+
setWindowEvents(this.id, this.resolved.handlers);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** The user closed it, with no OnClose to ask: Raven destroys it now. What its openings gave runs. */
|
|
418
|
+
private closedByUser(): void {
|
|
419
|
+
const failures: unknown[] = [];
|
|
420
|
+
for (const cleanup of this.takeClosing()) {
|
|
421
|
+
attempt(failures, cleanup);
|
|
422
|
+
}
|
|
423
|
+
throwFirst(failures);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** What holds it in the tree, to know whether that is shown: a window opens only then. */
|
|
427
|
+
setParent(parent: WidgetInstance | null): void {
|
|
428
|
+
this.holder = parent;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
closeWindows(): void {
|
|
432
|
+
if (this.state === "live") {
|
|
433
|
+
this.hidden = true;
|
|
434
|
+
this.close();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
openWindows(): void {
|
|
439
|
+
// (only one closed with its tree: one closed by other code stays closed)
|
|
440
|
+
if (this.hidden) {
|
|
441
|
+
this.materialize();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
applyParentAttributes(): void {}
|
|
446
|
+
|
|
447
|
+
/** What can change on a window that is open. The other attributes only count when it is made. */
|
|
448
|
+
update<Child>(props: RavenProps<Child>): void {
|
|
449
|
+
const next = resolveProps(props);
|
|
450
|
+
if (this.state === "live" && this.isOpen) {
|
|
451
|
+
this.applyChanges(next);
|
|
452
|
+
}
|
|
453
|
+
this.resolved = next;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Applies what changed in the props. */
|
|
457
|
+
private applyChanges(next: ResolvedProps): void {
|
|
458
|
+
const previous = this.resolved.attributes;
|
|
459
|
+
for (const [name, value] of next.attributes) {
|
|
460
|
+
if (value !== previous.get(name)) {
|
|
461
|
+
SET_ON_OPEN_WINDOW[name]?.(this.id, value);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const width = next.attributes.get("Width");
|
|
465
|
+
const height = next.attributes.get("Height");
|
|
466
|
+
if (width !== undefined && height !== undefined && (width !== previous.get("Width") || height !== previous.get("Height"))) {
|
|
467
|
+
r.Rwindow_set_size(this.id, Number(width), Number(height));
|
|
468
|
+
}
|
|
469
|
+
for (const [name, value] of next.attributes) {
|
|
470
|
+
if (previous.get(name) !== value && !SET_ON_OPEN_WINDOW[name] && name !== "Width" && name !== "Height") {
|
|
471
|
+
console.warn(`raven-tsx: <Window> ${name} only counts when the window is made`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
closeEvents.set(this.id, next.handlers.get("OnClose"));
|
|
475
|
+
setWindowEvents(this.id, next.handlers);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** The place of its widgets: children are added and removed only once the window is placed. */
|
|
479
|
+
private get open(): WidgetInstance {
|
|
480
|
+
if (!this.content) {
|
|
481
|
+
return RavenError.throw("raven-tsx: a <Window> got a child before it was placed");
|
|
482
|
+
}
|
|
483
|
+
return this.content;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Makes a child the island of this window, if it is one and there is none yet. */
|
|
487
|
+
private takeIsland(child: HostNode): boolean {
|
|
488
|
+
if (child.tag !== "Island" || this.island || !(child instanceof WidgetInstance)) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
// (shown again: closing the window said it was not)
|
|
492
|
+
child.setShown(true);
|
|
493
|
+
child.materialize();
|
|
494
|
+
const set = (widget: r.Rid): void => r.Rwindow_set_island(this.id, widget);
|
|
495
|
+
this.island = child;
|
|
496
|
+
child.setHolder(set);
|
|
497
|
+
set(child.widget);
|
|
498
|
+
// (an island kept while the window was closed opens the windows in it again; one a deleted
|
|
499
|
+
// tree let go, alive, gets back its handlers first)
|
|
500
|
+
child.rememberTree();
|
|
501
|
+
child.openWindows();
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
append(child: HostNode): void {
|
|
506
|
+
if (this.state !== "live") {
|
|
507
|
+
this.appendInitial(child);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (child !== this.island && !this.takeIsland(child)) {
|
|
511
|
+
this.open.append(child);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
appendInitial(child: HostNode): void {
|
|
516
|
+
if (this.state === "live") {
|
|
517
|
+
this.append(child);
|
|
518
|
+
} else {
|
|
519
|
+
this.releaseInitial(child);
|
|
520
|
+
this.initial.push(child);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Takes a child out of those that go in when the window opens. */
|
|
525
|
+
private releaseInitial(child: HostNode): void {
|
|
526
|
+
const index = this.initial.indexOf(child);
|
|
527
|
+
if (index >= 0) {
|
|
528
|
+
this.initial.splice(index, 1);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
release(child: HostNode): void {
|
|
533
|
+
// (also while it opens: what waits to go in is let go too)
|
|
534
|
+
this.releaseInitial(child);
|
|
535
|
+
// (while it closes too, it has its content: what it keeps to open again is read from there)
|
|
536
|
+
if (!this.content) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (child !== this.island) {
|
|
540
|
+
this.content.release(child);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
this.detachIsland(child);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Takes the island out of the window, alive. */
|
|
547
|
+
private detachIsland(child: HostNode): void {
|
|
548
|
+
this.island = null;
|
|
549
|
+
(child as WidgetInstance).setHolder(null);
|
|
550
|
+
const widget = child.nativeWidget;
|
|
551
|
+
if (this.isOpen && widget !== null && r.Rwidget_is_valid(widget)) {
|
|
552
|
+
// (Rwindow_set_island would delete it: a widget added elsewhere leaves its window alive)
|
|
553
|
+
const holder = r.Rwidget_create_from_xml("<VBox/>");
|
|
554
|
+
r.Rwidget_add_child(holder, widget);
|
|
555
|
+
r.Rwidget_remove_child(holder, widget);
|
|
556
|
+
r.Rwidget_destroy(holder);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
insertBefore(child: HostNode, before: HostNode): void {
|
|
561
|
+
// (while it closes, its children are still in its content, to wait for the next opening in
|
|
562
|
+
// their order: one put before one of them goes there, and waits with them)
|
|
563
|
+
if (this.state === "closing" && this.content && before !== this.island && this.content.has(before)) {
|
|
564
|
+
this.content.insertBefore(child, before);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (this.state !== "live") {
|
|
568
|
+
this.releaseInitial(child);
|
|
569
|
+
const index = this.initial.indexOf(before);
|
|
570
|
+
this.initial.splice(index < 0 ? this.initial.length : index, 0, child);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (child === this.island || this.takeIsland(child)) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
// (the island is not among the widgets of the content: nothing can go before it there)
|
|
577
|
+
if (before === this.island) {
|
|
578
|
+
this.open.append(child);
|
|
579
|
+
} else {
|
|
580
|
+
this.open.insertBefore(child, before);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
remove(child: HostNode): void {
|
|
585
|
+
// (also while it opens: what waits to go in goes from there, and does not go in after)
|
|
586
|
+
this.releaseInitial(child);
|
|
587
|
+
if (!this.content) {
|
|
588
|
+
// (a child can be live already, moved here: its handlers go before it does)
|
|
589
|
+
child.forgetTree();
|
|
590
|
+
child.dispose();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (child !== this.island) {
|
|
594
|
+
this.content.remove(child);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
child.forgetTree();
|
|
598
|
+
this.island = null;
|
|
599
|
+
(child as WidgetInstance).setHolder(null);
|
|
600
|
+
if (this.isOpen) {
|
|
601
|
+
// (this deletes it: the window owns it. What it holds of registered tags is theirs)
|
|
602
|
+
(child as WidgetInstance).releaseAdopted();
|
|
603
|
+
r.Rwindow_set_island(this.id, r.R_INVALID_ID);
|
|
604
|
+
}
|
|
605
|
+
child.dispose();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
forgetTree(): void {
|
|
609
|
+
if (this.state !== "live") {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
this.forgotten = true;
|
|
613
|
+
this.content?.forgetTree();
|
|
614
|
+
this.island?.forgetTree();
|
|
615
|
+
closeEvents.set(this.id, undefined, false);
|
|
616
|
+
closeEvents.notice(this.id, undefined, false);
|
|
617
|
+
for (const event of Object.values(WINDOW_EVENTS)) {
|
|
618
|
+
event.off(this.id, false);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
dispose(): void {
|
|
623
|
+
if (this.state === "closing") {
|
|
624
|
+
this.disposeAsked = true;
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const state = this.state;
|
|
628
|
+
const cleanups = state === "live" ? this.takeClosing() : [];
|
|
629
|
+
this.state = "gone";
|
|
630
|
+
this.opening.clear();
|
|
631
|
+
const failures: unknown[] = [];
|
|
632
|
+
// (what waited to go in: what a framework placed in it and took back, alive, goes with it. Also
|
|
633
|
+
// while it opens: a child that disposed it stopped the others going in)
|
|
634
|
+
// (one at a time: a cleanup of one may take another away first, which then is no longer here)
|
|
635
|
+
for (let child = this.initial.shift(); child; child = this.initial.shift()) {
|
|
636
|
+
const next = child;
|
|
637
|
+
attempt(failures, () => {
|
|
638
|
+
next.forgetTree();
|
|
639
|
+
next.dispose();
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
if (state !== "live") {
|
|
643
|
+
this.destroyKeptNative();
|
|
644
|
+
throwFirst(failures);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
// (what it holds is not shown from now: a window a cleanup adds in it does not open)
|
|
648
|
+
this.content?.setShown(false);
|
|
649
|
+
this.island?.setShown(false);
|
|
650
|
+
if (this.isOpen) {
|
|
651
|
+
// (the window destroys what it holds: those of registered tags are theirs)
|
|
652
|
+
this.content?.releaseAdopted();
|
|
653
|
+
this.island?.releaseAdopted();
|
|
654
|
+
}
|
|
655
|
+
attempt(failures, () => this.content?.disposeDetached());
|
|
656
|
+
attempt(failures, () => this.island?.disposeDetached());
|
|
657
|
+
// (what it asked of Raven goes: a window forgotten first has none, one disposed directly still does)
|
|
658
|
+
closeEvents.set(this.id, undefined, false);
|
|
659
|
+
closeEvents.notice(this.id, undefined, false);
|
|
660
|
+
if (this.isOpen) {
|
|
661
|
+
r.Rwindow_destroy(this.id);
|
|
662
|
+
}
|
|
663
|
+
// (what the application put in the content, kept while it was closed, and not put back: opening failed)
|
|
664
|
+
this.destroyKeptNative();
|
|
665
|
+
// (last, as for close: a cleanup that closes or disposes the window finds it gone)
|
|
666
|
+
for (const cleanup of cleanups) {
|
|
667
|
+
attempt(failures, cleanup);
|
|
668
|
+
}
|
|
669
|
+
throwFirst(failures);
|
|
670
|
+
}
|
|
671
|
+
}
|