@decky.fx/raven-tsx 0.1.0 → 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.
@@ -75,9 +75,35 @@ function setWindowEvents(window: r.Rid, handlers: ReadonlyMap<string, EventHandl
75
75
  * it can be anywhere in the tree, and goes away when its element does. It opens once it is placed:
76
76
  * a render the framework throws away opens nothing.
77
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
+
78
100
  export class WindowInstance implements HostNode {
79
101
  readonly tag = "Window";
80
102
  readonly nativeWidget = null;
103
+
104
+ get placed(): boolean {
105
+ return this.state === "live" && this.isOpen;
106
+ }
81
107
  private id: r.Rid = r.R_INVALID_ID;
82
108
  /** Where its widgets go, once it is open. */
83
109
  private content: WidgetInstance | null = null;
@@ -85,7 +111,25 @@ export class WindowInstance implements HostNode {
85
111
  private readonly initial: HostNode[] = [];
86
112
  /** Its `<Island>`: it floats above the content, as in XML, and the window owns it. */
87
113
  private island: WidgetInstance | null = null;
88
- private state: "pending" | "live" | "gone" = "pending";
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[] = [];
89
133
 
90
134
  private constructor(private resolved: ResolvedProps) {}
91
135
 
@@ -104,21 +148,249 @@ export class WindowInstance implements HostNode {
104
148
  if (this.state !== "pending") {
105
149
  return;
106
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
+ }
107
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;
108
159
  this.id = window;
109
160
  this.content = WidgetInstance.forContainer(content);
110
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;
111
166
  try {
112
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());
113
170
  setWindowEvents(window, this.resolved.handlers);
114
- for (const child of this.initial.splice(0)) {
115
- this.append(child);
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);
116
185
  }
117
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)
118
341
  this.forgetTree();
119
342
  this.dispose();
120
- throw error;
343
+ return;
121
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);
122
394
  }
123
395
 
124
396
  /** Whether the window still exists: without an `OnClose`, the user can close it at any time. */
@@ -126,7 +398,49 @@ export class WindowInstance implements HostNode {
126
398
  return this.id !== r.R_INVALID_ID && r.Rwindow_is_valid(this.id) !== 0;
127
399
  }
128
400
 
129
- setParent(): void {}
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
+ }
130
444
 
131
445
  applyParentAttributes(): void {}
132
446
 
@@ -174,15 +488,25 @@ export class WindowInstance implements HostNode {
174
488
  if (child.tag !== "Island" || this.island || !(child instanceof WidgetInstance)) {
175
489
  return false;
176
490
  }
491
+ // (shown again: closing the window said it was not)
492
+ child.setShown(true);
177
493
  child.materialize();
178
494
  const set = (widget: r.Rid): void => r.Rwindow_set_island(this.id, widget);
179
495
  this.island = child;
180
496
  child.setHolder(set);
181
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();
182
502
  return true;
183
503
  }
184
504
 
185
505
  append(child: HostNode): void {
506
+ if (this.state !== "live") {
507
+ this.appendInitial(child);
508
+ return;
509
+ }
186
510
  if (child !== this.island && !this.takeIsland(child)) {
187
511
  this.open.append(child);
188
512
  }
@@ -192,11 +516,60 @@ export class WindowInstance implements HostNode {
192
516
  if (this.state === "live") {
193
517
  this.append(child);
194
518
  } else {
519
+ this.releaseInitial(child);
195
520
  this.initial.push(child);
196
521
  }
197
522
  }
198
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
+
199
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
+ }
200
573
  if (child === this.island || this.takeIsland(child)) {
201
574
  return;
202
575
  }
@@ -209,8 +582,16 @@ export class WindowInstance implements HostNode {
209
582
  }
210
583
 
211
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
+ }
212
593
  if (child !== this.island) {
213
- this.open.remove(child);
594
+ this.content.remove(child);
214
595
  return;
215
596
  }
216
597
  child.forgetTree();
@@ -228,29 +609,63 @@ export class WindowInstance implements HostNode {
228
609
  if (this.state !== "live") {
229
610
  return;
230
611
  }
612
+ this.forgotten = true;
231
613
  this.content?.forgetTree();
232
614
  this.island?.forgetTree();
233
615
  closeEvents.set(this.id, undefined, false);
616
+ closeEvents.notice(this.id, undefined, false);
234
617
  for (const event of Object.values(WINDOW_EVENTS)) {
235
618
  event.off(this.id, false);
236
619
  }
237
620
  }
238
621
 
239
622
  dispose(): void {
623
+ if (this.state === "closing") {
624
+ this.disposeAsked = true;
625
+ return;
626
+ }
240
627
  const state = this.state;
628
+ const cleanups = state === "live" ? this.takeClosing() : [];
241
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
+ }
242
642
  if (state !== "live") {
643
+ this.destroyKeptNative();
644
+ throwFirst(failures);
243
645
  return;
244
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);
245
650
  if (this.isOpen) {
246
651
  // (the window destroys what it holds: those of registered tags are theirs)
247
652
  this.content?.releaseAdopted();
248
653
  this.island?.releaseAdopted();
249
654
  }
250
- this.content?.disposeDetached();
251
- this.island?.disposeDetached();
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);
252
660
  if (this.isOpen) {
253
661
  r.Rwindow_destroy(this.id);
254
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);
255
670
  }
256
671
  }
@@ -125,7 +125,7 @@ const hostConfig: RavenHostConfig = {
125
125
  bindToConsole: (_method, args) => console.log.bind(console, ...args),
126
126
  getSuspendedCommitReason: () => null,
127
127
  resetFormInstance: () => {},
128
- rendererVersion: "0.1.0",
128
+ rendererVersion: "0.1.1",
129
129
  rendererPackageName: "raven-tsx-react",
130
130
  extraDevToolsConfig: null,
131
131
  };
@@ -0,0 +1,7 @@
1
+ // babel-preset-solid has no types of its own: the plugin only hands it to Babel.
2
+ declare module "babel-preset-solid" {
3
+ import type { PluginItem } from "@babel/core";
4
+
5
+ const preset: PluginItem;
6
+ export default preset;
7
+ }