@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.
Files changed (43) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +12 -0
  3. package/RAVEN_VERSION +1 -0
  4. package/README.md +444 -2
  5. package/package.json +75 -4
  6. package/src/core/errors.ts +20 -0
  7. package/src/core/events.ts +925 -0
  8. package/src/core/frame.ts +76 -0
  9. package/src/core/index.ts +28 -0
  10. package/src/core/library-check.ts +89 -0
  11. package/src/core/native.ts +82 -0
  12. package/src/core/plot.ts +179 -0
  13. package/src/core/props.generated.ts +172 -0
  14. package/src/core/props.ts +158 -0
  15. package/src/core/raven.ts +10 -0
  16. package/src/core/subscriptions.ts +78 -0
  17. package/src/core/tags.ts +307 -0
  18. package/src/core/theme.ts +207 -0
  19. package/src/core/web.ts +84 -0
  20. package/src/core/widget-instance.ts +1057 -0
  21. package/src/core/window-instance.ts +671 -0
  22. package/src/react/components.generated.ts +179 -0
  23. package/src/react/hooks.ts +51 -0
  24. package/src/react/host-config.ts +138 -0
  25. package/src/react/index.ts +26 -0
  26. package/src/react/root.ts +119 -0
  27. package/src/react/tags.ts +50 -0
  28. package/src/react/web.ts +5 -0
  29. package/src/solid/babel-preset-solid.d.ts +7 -0
  30. package/src/solid/components.generated.ts +163 -0
  31. package/src/solid/index.ts +29 -0
  32. package/src/solid/node.ts +405 -0
  33. package/src/solid/plugin.ts +108 -0
  34. package/src/solid/portal.ts +27 -0
  35. package/src/solid/preload.ts +14 -0
  36. package/src/solid/primitives.ts +43 -0
  37. package/src/solid/renderer.ts +37 -0
  38. package/src/solid/root.ts +137 -0
  39. package/src/solid/tag.ts +37 -0
  40. package/src/solid/tags.ts +101 -0
  41. package/src/solid/teardown.ts +50 -0
  42. package/src/solid/web.ts +5 -0
  43. package/src/vendor/raven.ts +12652 -0
@@ -0,0 +1,1057 @@
1
+ import * as r from "./raven";
2
+ import { RavenError } from "./errors";
3
+ import { applyHandlers, controlledSync, eventScope, forgetHandlers, setVirtualItems, virtualItemsOf } from "./events";
4
+ import { applyPlotData, checkPlotData, restorePlotData } from "./plot";
5
+ import { CLEARED, resolveProps, type RavenProps, type ResolvedProps, type TreeItem } from "./props";
6
+
7
+ /** Runs a function, and keeps its failure with the others, to go on with what must be done. */
8
+ function attempt(failures: unknown[], run: () => void): void {
9
+ try {
10
+ run();
11
+ } catch (error) {
12
+ failures.push(error);
13
+ }
14
+ }
15
+
16
+ /** Throws the first of some failures, once all that could run has run. */
17
+ function throwFirst(failures: readonly unknown[]): void {
18
+ if (failures.length > 0) {
19
+ throw failures[0];
20
+ }
21
+ }
22
+
23
+ /** The host type of a widget Raven made and an adapter only places: `adopt`. */
24
+ export const ADOPTED_TAG = "raven-tsx:adopted";
25
+
26
+ /**
27
+ * Where a node is. `pending`: the framework rendered it, and it is only props and children here,
28
+ * with nothing in Raven, as the framework may throw that render away. `live`: it is placed, and
29
+ * Raven made it. `gone`: it was deleted, or could not be made.
30
+ */
31
+ type NodeState = "pending" | "live" | "gone";
32
+
33
+ /** The nodes Raven made in this batch of changes, whose attributes that need a parent wait for its end. */
34
+ const placedInThisBatch: WidgetInstance[] = [];
35
+
36
+ /**
37
+ * Ends a batch of changes, what an adapter calls once all of it is placed: React at the end of a
38
+ * commit. The nodes made during the batch get their attributes that need a parent, as they have one now.
39
+ */
40
+ export function finishPlacing(): void {
41
+ const placed = placedInThisBatch.splice(0);
42
+ for (const instance of placed) {
43
+ instance.applyParentAttributes();
44
+ }
45
+ // (then what waited for them to be placed, user code: a ref. All of it, then the first failure)
46
+ const failures: unknown[] = [];
47
+ for (const instance of placed) {
48
+ attempt(failures, () => instance.runPlaced());
49
+ }
50
+ throwFirst(failures);
51
+ }
52
+
53
+ /** What a framework's tree is made of: a widget, or a window, which is no widget of its parent. */
54
+ export interface HostNode {
55
+ readonly tag: string;
56
+ /** The widget to put in a parent, or null for a window, which stands by itself. */
57
+ readonly nativeWidget: r.Rid | null;
58
+ /** Whether Raven has it: made, and not deleted. A window open whose function threw still is. */
59
+ readonly placed: boolean;
60
+ /** Says who holds it. */
61
+ setParent(parent: WidgetInstance | null): void;
62
+ /**
63
+ * Makes it in Raven, with what it holds, when it is placed: what a framework renders and then
64
+ * throws away (an error, a render done again) never reaches Raven. Throws, leaving nothing in
65
+ * Raven, when Raven cannot make it.
66
+ */
67
+ materialize(): void;
68
+ update<Child>(props: RavenProps<Child>): void;
69
+ applyParentAttributes(): void;
70
+ append(child: HostNode): void;
71
+ /** Adds a child while this new node is rendered, in its props only: Raven gets it at `materialize`. */
72
+ appendInitial(child: HostNode): void;
73
+ insertBefore(child: HostNode, before: HostNode): void;
74
+ /** Takes a child out without deleting it: a framework may place it again, here or elsewhere. */
75
+ release(child: HostNode): void;
76
+ remove(child: HostNode): void;
77
+ /** Forgets the handlers of this node and of everything under it. */
78
+ forgetTree(): void;
79
+ /**
80
+ * Gives back the handlers `forgetTree` took, to what is still alive: a cleanup run while its tree
81
+ * was deleted placed it elsewhere. Nothing to do for a node never forgotten.
82
+ */
83
+ rememberTree(): void;
84
+ /** Deletes it, after its parent let it go. */
85
+ dispose(): void;
86
+ /** Closes the windows in this tree, keeping them to open again: what holds them left the tree, alive. */
87
+ closeWindows(): void;
88
+ /** Opens the windows in this tree that are closed: what holds them is placed again. */
89
+ openWindows(): void;
90
+ }
91
+
92
+ /** How to read the value a widget holds by itself, for the attribute that sets it. */
93
+ interface ControlledValue {
94
+ readonly attribute: string;
95
+ /** The value of the widget, as the attribute would be written. */
96
+ readonly read: (widget: r.Rid) => string;
97
+ /** Whether the widget already holds a value, when `sameValue` is not the way to tell. */
98
+ readonly same?: ((held: string, wanted: string) => boolean) | undefined;
99
+ /**
100
+ * The user changes it without an event that says so: it is not set to what it holds, and not
101
+ * put back either, or the view would jump back at the next event of something else.
102
+ */
103
+ readonly unreported?: true | undefined;
104
+ }
105
+
106
+ const numberText = (value: number): string => String(value);
107
+ const twoDigits = (value: number): string => String(value).padStart(2, "0");
108
+
109
+ const flagText = (value: number): string => (value ? "true" : "false");
110
+ /** A text is that text: "1.0" is not "1", which is what makes a number the same as another. */
111
+ const sameText = (held: string, wanted: string): boolean => held === wanted;
112
+ /** A code of a color, in either case. */
113
+ const sameColor = (held: string, wanted: string): boolean => held.toLowerCase() === wanted.toLowerCase();
114
+
115
+ /**
116
+ * The attributes of what the user changes directly, by tag: the value is held by the widget, so
117
+ * an attribute must not be set to what it already holds. `src/test/core/coverage.test.ts` says why
118
+ * the other attributes of these tags are not here.
119
+ */
120
+ const CONTROLLED: Readonly<Record<string, readonly ControlledValue[]>> = {
121
+ TextInput: [{ attribute: "Text", read: (widget) => r.Rtext_input_get_text(widget) ?? "", same: sameText }],
122
+ TextEditor: [
123
+ { attribute: "Text", read: (widget) => r.Rtext_editor_get_text(widget) ?? "", same: sameText },
124
+ { attribute: "Zoom", read: (widget) => numberText(r.Rtext_editor_get_zoom(widget)), unreported: true },
125
+ ],
126
+ Checkbox: [{ attribute: "Checked", read: (widget) => flagText(r.Rcheckbox_is_checked(widget)) }],
127
+ Slider: [{ attribute: "Value", read: (widget) => numberText(r.Rslider_get_value(widget)) }],
128
+ SpinBox: [{ attribute: "Value", read: (widget) => numberText(r.Rspin_box_get_value(widget)) }],
129
+ ComboBox: [{ attribute: "Selected", read: (widget) => numberText(r.Rcombo_box_get_selected(widget)) }],
130
+ ListView: [{ attribute: "Selected", read: (widget) => numberText(r.Rlist_view_get_selected(widget)) }],
131
+ TabView: [{ attribute: "ActiveTab", read: (widget) => numberText(r.Rtab_view_get_active(widget)) }],
132
+ Expander: [{ attribute: "Expanded", read: (widget) => flagText(r.Rexpander_is_expanded(widget)) }],
133
+ Island: [{ attribute: "Expanded", read: (widget) => flagText(r.Risland_is_expanded(widget)) }],
134
+ Table: [
135
+ { attribute: "Selected", read: (widget) => numberText(r.Rtable_get_selected(widget)) },
136
+ { attribute: "SortColumn", read: (widget) => numberText(r.Rtable_get_sort_column(widget)) },
137
+ ],
138
+ Rating: [{ attribute: "Value", read: (widget) => numberText(r.Rrating_get_value(widget)) }],
139
+ Pagination: [{ attribute: "Page", read: (widget) => numberText(r.Rpagination_get_page(widget)) }],
140
+ DatePicker: [{
141
+ attribute: "Date",
142
+ read: (widget) => {
143
+ const [year, month, day] = r.Rdate_picker_get_date(widget);
144
+ return `${year}-${twoDigits(month)}-${twoDigits(day)}`;
145
+ },
146
+ }],
147
+ TimePicker: [{
148
+ attribute: "Time",
149
+ read: (widget) => r.Rtime_picker_get_time(widget).map(twoDigits).join(":"),
150
+ same: (held, wanted) => held === wanted || held === `${wanted}:00`,
151
+ }],
152
+ ColorPicker: [{ attribute: "Color", read: (widget) => r.Rcolor_picker_get_hex(widget) ?? "", same: sameColor }],
153
+ MenuItem: [{ attribute: "Checked", read: (widget) => flagText(r.Rmenu_item_is_checked(widget)) }],
154
+ DockArea: [{ attribute: "Layout", read: (widget) => r.Rdock_area_save_layout(widget) ?? "", same: sameText }],
155
+ Timeline: [
156
+ { attribute: "Time", read: (widget) => numberText(r.Rtimeline_get_time(widget)) },
157
+ { attribute: "ViewStart", read: (widget) => numberText(r.Rtimeline_get_view(widget)[0]), unreported: true },
158
+ { attribute: "ViewEnd", read: (widget) => numberText(r.Rtimeline_get_view(widget)[1]), unreported: true },
159
+ ],
160
+ NodeGraph: [
161
+ { attribute: "CenterX", read: (widget) => numberText(r.Rnode_graph_get_view(widget)[0]), unreported: true },
162
+ { attribute: "CenterY", read: (widget) => numberText(r.Rnode_graph_get_view(widget)[1]), unreported: true },
163
+ { attribute: "Zoom", read: (widget) => numberText(r.Rnode_graph_get_view(widget)[2]), unreported: true },
164
+ ],
165
+ BlockEditor: [{ attribute: "Zoom", read: (widget) => numberText(r.Rblock_editor_get_view(widget)[2]), unreported: true }],
166
+ };
167
+
168
+ /** The attributes held by the widgets of a tag, for the test of what is covered. */
169
+ export const controlledAttributes = (tag: string): readonly string[] => (CONTROLLED[tag] ?? []).map(({ attribute }) => attribute);
170
+
171
+ /** Whether what `Items` holds, once parsed, are rows a TreeView can show. */
172
+ function isRows(value: unknown): value is readonly TreeItem[] {
173
+ return Array.isArray(value) && value.every((row: TreeItem | null) => typeof row?.Text === "string" && isRows(row.Children ?? []));
174
+ }
175
+
176
+ /** The rows of the `Items` of a TreeView, from its text. Throws when they are not rows. */
177
+ function treeRows(text: string): readonly TreeItem[] {
178
+ const items: unknown = text ? JSON.parse(text) : [];
179
+ if (!isRows(items)) {
180
+ RavenError.throw("raven-tsx: the Items of a TreeView should be a list of rows, each with a Text");
181
+ }
182
+ return items;
183
+ }
184
+
185
+ /** Fills a TreeView from its `Items`, as text. Its rows are made again: their handles change. Text that is not rows leaves them. */
186
+ function fillTree(tree: r.Rid, text: string): void {
187
+ const items = treeRows(text);
188
+ const add = (parent: r.Rid, items: readonly TreeItem[]): void => {
189
+ for (const item of items) {
190
+ const row = r.Rtree_view_add_item(tree, parent, item.Text);
191
+ if (item.Expanded) {
192
+ r.Rtree_item_set_expanded(row, r.R_TRUE);
193
+ }
194
+ add(row, item.Children ?? []);
195
+ }
196
+ };
197
+ r.Rtree_view_clear_items(tree);
198
+ add(r.R_INVALID_ID, items);
199
+ }
200
+
201
+ /** The styles of `FontStyle`, of the font of the theme. */
202
+ const FONT_STYLES: Readonly<Record<string, r.RFontStyle>> = { Regular: r.R_FONT_REGULAR, Bold: r.R_FONT_BOLD, Italic: r.R_FONT_ITALIC, BoldItalic: r.R_FONT_BOLD_ITALIC };
203
+
204
+ /** `TextDisabled` as the C API writes it: `TEXT_DISABLED`. */
205
+ const constantName = (name: string): string => name.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
206
+
207
+ /** The roles and metrics an override of a group is for, or undefined: only those can be taken back. */
208
+ export function overrideOf(name: string): { readonly role: r.RColorRole } | { readonly metric: r.RMetric } | undefined {
209
+ const [group, key = ""] = name.split(".");
210
+ if (group === "Color") {
211
+ const role = r.RColorRole[`R_COLOR_${constantName(key)}` as keyof typeof r.RColorRole];
212
+ return role === undefined ? undefined : { role };
213
+ }
214
+ if (group === "Metric") {
215
+ const metric = r.RMetric[`R_METRIC_${constantName(key)}` as keyof typeof r.RMetric];
216
+ return metric === undefined ? undefined : { metric };
217
+ }
218
+ return undefined;
219
+ }
220
+
221
+ /** Gives a color or a metric back to what the theme says. */
222
+ function clearOverride(widget: r.Rid, name: string): void {
223
+ const override = overrideOf(name);
224
+ if (override && "role" in override) {
225
+ r.Rwidget_clear_color(widget, override.role);
226
+ } else if (override) {
227
+ r.Rwidget_clear_metric(widget, override.metric);
228
+ } else {
229
+ console.warn(`raven-tsx: ${name} cannot be taken back: leave the prop out instead`);
230
+ }
231
+ }
232
+
233
+ /** The props XML does not take, by tag: they are set by calling Raven. */
234
+ const SET_BY_CALL: Readonly<Record<string, Readonly<Record<string, (widget: r.Rid, value: string) => void>>>> = {
235
+ Image: { Texture: (widget, value) => r.Rimage_set_texture(widget, Number(value)) },
236
+ Label: {
237
+ FontStyle: (widget, value) => {
238
+ const style = FONT_STYLES[value];
239
+ if (style !== undefined) {
240
+ r.Rlabel_set_font(widget, r.Rfont_get_variant(r.Rfont_get_default(), style));
241
+ }
242
+ },
243
+ },
244
+ TreeView: { Items: fillTree },
245
+ };
246
+
247
+ /**
248
+ * What can be found wrong with props without Raven, by tag: it is checked while a framework
249
+ * renders, so that its nearest error boundary takes it, as for any error of a render. (React gives
250
+ * an error of its commit to a boundary above what it is placing: not to one placed with it)
251
+ */
252
+ const CHECKED_AT_RENDER: Readonly<Record<string, Readonly<Record<string, (value: string) => void>>>> = {
253
+ TreeView: { Items: (value) => void treeRows(value) },
254
+ };
255
+
256
+ /** What a tag does with its data props, which reach Raven by calling it, as they are. */
257
+ interface DataProps {
258
+ readonly names: readonly string[];
259
+ /** Throws when the data cannot be shown. Comes before anything changes. */
260
+ readonly check: (data: ResolvedProps["data"]) => void;
261
+ /** Gives the widget what changed. */
262
+ readonly apply: (widget: r.Rid, previous: ResolvedProps["data"], next: ResolvedProps["data"]) => void;
263
+ /** After an event, puts back what the props say if a handler refused the change. */
264
+ readonly restore: (widget: r.Rid, data: ResolvedProps["data"]) => void;
265
+ }
266
+
267
+ const DATA_PROPS: Readonly<Record<string, DataProps>> = {
268
+ Plot: { names: ["Series", "DragLines"], check: checkPlotData, apply: applyPlotData, restore: restorePlotData },
269
+ };
270
+
271
+ /** The prop that holds the elements XML takes inside a widget. It is part of how the widget is made. */
272
+ const MARKUP = "Markup";
273
+
274
+ /** Whether a tag stands outside what holds it: a `Menu` paints as a panel unless a `MenuBar` or a `Menu` holds it. */
275
+ const floatsIn = (parent: string, child: string): boolean => child === "Menu" && parent !== "MenuBar" && parent !== "Menu";
276
+
277
+ /** Whether two values of an attribute are the same. A Slider holds a float: its value comes back rounded. */
278
+ function sameValue(held: string, wanted: string): boolean {
279
+ if (held === wanted) {
280
+ return true;
281
+ }
282
+ const a = Number(held);
283
+ const b = Number(wanted);
284
+ return held !== "" && wanted !== "" && !Number.isNaN(a) && !Number.isNaN(b) && Math.abs(a - b) < 1e-5;
285
+ }
286
+
287
+ /**
288
+ * A widget under a framework, and what a `ref` receives. Raven's handle is here with what the
289
+ * renderer must remember: the tree, because Raven has no way to ask where a child is, and
290
+ * the last props.
291
+ */
292
+ export class WidgetInstance implements HostNode {
293
+ private readonly children: HostNode[] = [];
294
+ private parent: WidgetInstance | null = null;
295
+ /** Told of a new handle when what holds it is not a parent: the island of a window. */
296
+ private holder: ((widget: r.Rid) => void) | null = null;
297
+ private resolved: ResolvedProps = { attributes: new Map(), handlers: new Map(), data: new Map() };
298
+ /** How many children the widget had when it was made: those of its markup come first. */
299
+ private madeChildren = 0;
300
+ private state: NodeState;
301
+ /** Whether the tree it is the top of is shown: a window that closes says no before its windows close. */
302
+ private shown = true;
303
+ /** What waits for it to be placed, at the end of the batch that placed it: a ref of a framework. */
304
+ private readonly placedWaiting: ((instance: WidgetInstance) => void)[] = [];
305
+ /** Made, and not placed yet: what waits for it to be placed waits for the end of the batch. */
306
+ private placing = false;
307
+ /** Its handlers were forgotten (`forgetTree`): placed again alive, it gets them back. */
308
+ private forgotten = false;
309
+ /** Counts the passes that close or open the windows in it: a newer one ends the one under way. */
310
+ private windowsPass = 0;
311
+
312
+ private constructor(
313
+ private id: r.Rid,
314
+ readonly tag: string,
315
+ /** The props of a node a framework rendered, until Raven makes it. */
316
+ private pending: ResolvedProps | null = null,
317
+ ) {
318
+ this.state = pending ? "pending" : "live";
319
+ }
320
+
321
+ /** A new widget of a tag, as a framework renders it: Raven makes it once it is placed, at `materialize`. */
322
+ static create<Child>(tag: string, props: RavenProps<Child>): WidgetInstance {
323
+ return new WidgetInstance(r.R_INVALID_ID, tag, WidgetInstance.resolveChecked(tag, props));
324
+ }
325
+
326
+ /** The props of a tag, checked for what can be found wrong with them without Raven. */
327
+ private static resolveChecked<Child>(tag: string, props: RavenProps<Child>): ResolvedProps {
328
+ const resolved = resolveProps(props, DATA_PROPS[tag]?.names);
329
+ DATA_PROPS[tag]?.check(resolved.data);
330
+ for (const [name, check] of Object.entries(CHECKED_AT_RENDER[tag] ?? {})) {
331
+ const value = resolved.attributes.get(name);
332
+ if (value !== undefined) {
333
+ check(value);
334
+ }
335
+ }
336
+ return resolved;
337
+ }
338
+
339
+ /**
340
+ * Makes the widget, with its props but those that need a parent, then its children, in their
341
+ * order: those of its markup come first. If any of it fails, what was made is deleted.
342
+ */
343
+ materialize(): void {
344
+ const resolved = this.pending;
345
+ if (this.state !== "pending" || !resolved) {
346
+ return;
347
+ }
348
+ this.id = WidgetInstance.make(this.tag, resolved.attributes.get(MARKUP));
349
+ this.madeChildren = r.Rwidget_get_child_count(this.id);
350
+ this.state = "live";
351
+ this.pending = null;
352
+ this.placing = true;
353
+ this.forgotten = false;
354
+ try {
355
+ this.applyAll(resolved);
356
+ for (const child of [...this.children]) {
357
+ // (user code run for an earlier one may have deleted this one: the others are not made)
358
+ if (this.state !== "live") {
359
+ break;
360
+ }
361
+ // (or taken it away)
362
+ if (!this.children.includes(child)) {
363
+ continue;
364
+ }
365
+ child.materialize();
366
+ // (making it ran user code, which may have taken it away from here, or deleted this: its
367
+ // windows are not this one's to open then)
368
+ if (this.state !== "live" || !this.children.includes(child)) {
369
+ continue;
370
+ }
371
+ child.rememberTree();
372
+ child.openWindows();
373
+ // (opening its windows ran user code too)
374
+ if (this.state !== "live" || !this.children.includes(child)) {
375
+ continue;
376
+ }
377
+ // (an adopted widget Raven moves here from where it is shown; one its element destroyed is left out)
378
+ const widget = child.nativeWidget;
379
+ if (this.holds(child) && widget !== null && r.Rwidget_is_valid(widget)) {
380
+ r.Rwidget_insert_child(this.id, widget, this.placeAmongPlaced(child));
381
+ }
382
+ }
383
+ } catch (error) {
384
+ // (the framework keeps it, and takes it away with the rest of the tree that failed)
385
+ this.forgetTree();
386
+ this.dispose();
387
+ throw error;
388
+ }
389
+ if (this.state === "live") {
390
+ placedInThisBatch.push(this);
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Where a child goes among the widgets Raven holds here: just before the first one that comes
396
+ * after it, or at the end. Only those placed count: user code run while a sibling is made may
397
+ * place a child while that sibling waits. (and those of its markup come first)
398
+ */
399
+ private placeAmongPlaced(child: HostNode): number {
400
+ const count = r.Rwidget_get_child_count(this.id);
401
+ const after = new Set(
402
+ this.children
403
+ .slice(this.children.indexOf(child) + 1)
404
+ .filter((other) => this.holds(other))
405
+ .map((other) => other.nativeWidget),
406
+ );
407
+ for (let index = 0; index < count; index++) {
408
+ if (after.has(r.Rwidget_get_child(this.id, index))) {
409
+ return index;
410
+ }
411
+ }
412
+ return count;
413
+ }
414
+
415
+ /** An instance for a widget the framework does not own: the top of a root. */
416
+ static forContainer(id: r.Rid): WidgetInstance {
417
+ return new WidgetInstance(id, "");
418
+ }
419
+
420
+ /**
421
+ * A widget Raven made, a child of a registered tag in XML: an adapter places it where it is
422
+ * rendered, and sets nothing on it. Taken out of the tree, it is only let go, alive: the
423
+ * component may show it again, or move it into another box. Its element destroys it.
424
+ */
425
+ static adopt(id: r.Rid): WidgetInstance {
426
+ // (it stays where it is shown: the framework may drop the render that made this, and Raven moves a
427
+ // widget it adds from the parent it had)
428
+ return new WidgetInstance(id, ADOPTED_TAG);
429
+ }
430
+
431
+ /** Makes the widget of a tag in Raven. */
432
+ private static make(tag: string, markup = ""): r.Rid {
433
+ const id = r.Rwidget_create_from_xml(`<${tag}>${markup}</${tag}>`);
434
+ if (id === r.R_INVALID_ID) {
435
+ RavenError.fromLibrary(`Raven cannot make <${tag}>`);
436
+ }
437
+ return id;
438
+ }
439
+
440
+ /**
441
+ * Raven's handle of the widget, for the functions of the `raven` export: `R_INVALID_ID` until
442
+ * it is placed (a ref and an effect come after). Read it when you need it and do not keep
443
+ * it: it changes if the widget has to be made again.
444
+ */
445
+ get widget(): r.Rid {
446
+ return this.id;
447
+ }
448
+
449
+ /** Gives the widget the keyboard. */
450
+ focus(): void {
451
+ r.Rwidget_focus(this.id);
452
+ }
453
+
454
+ get nativeWidget(): r.Rid {
455
+ return this.id;
456
+ }
457
+
458
+ get placed(): boolean {
459
+ return this.state === "live";
460
+ }
461
+
462
+ /**
463
+ * Opens a `<Menu>` that stands by itself as a popup, at a position of its window. A right click
464
+ * gives that position: `OnContext` of a TreeView or a TileView, in the window.
465
+ */
466
+ open(x: number, y: number): void {
467
+ let holder = this.parent;
468
+ while (holder && holder.id === r.R_INVALID_ID) {
469
+ holder = holder.parent;
470
+ }
471
+ const window = holder ? r.Rwidget_get_window(holder.id) : r.R_INVALID_ID;
472
+ if (this.tag !== "Menu" || window === r.R_INVALID_ID) {
473
+ RavenError.throw(`raven-tsx: only a <Menu> in a window opens, and this is a <${this.tag}>`);
474
+ }
475
+ r.Rmenu_open(this.id, window, x, y);
476
+ }
477
+
478
+ setParent(parent: WidgetInstance | null): void {
479
+ this.parent = parent;
480
+ }
481
+
482
+ /**
483
+ * Whether it is in a tree that is shown: its parents lead to the top of a root (or to the
484
+ * content of a window), or to an island a window holds. One a framework took out, alive, is not:
485
+ * the windows in it stay closed until it is placed again.
486
+ */
487
+ get attached(): boolean {
488
+ let top: WidgetInstance = this;
489
+ while (top.parent) {
490
+ top = top.parent;
491
+ }
492
+ return top.shown && (top.tag === "" || top.holder !== null);
493
+ }
494
+
495
+ /** Whether a node is one of its children. */
496
+ has(child: HostNode): boolean {
497
+ return this.children.includes(child);
498
+ }
499
+
500
+ /** Says whether the tree it is the top of is shown: the content of a window that closes is not. */
501
+ setShown(shown: boolean): void {
502
+ this.shown = shown;
503
+ }
504
+
505
+ /**
506
+ * Gives it to a function once Raven has it, placed: now if it is, or at the end of the batch
507
+ * that places it (`finishPlacing`), as React gives a ref after its commit.
508
+ */
509
+ whenPlaced(run: (instance: WidgetInstance) => void): void {
510
+ if (this.state === "gone") {
511
+ return;
512
+ }
513
+ // (one being made, or made in a batch not over, is not placed yet: it waits for `finishPlacing`)
514
+ if (this.state === "live" && !this.placing) {
515
+ // (a parent deleted with it leaves it live here, with no widget: it gets nothing)
516
+ if (r.Rwidget_is_valid(this.id)) {
517
+ run(this);
518
+ }
519
+ return;
520
+ }
521
+ this.placedWaiting.push(run);
522
+ }
523
+
524
+ /** Runs what waited for it to be placed: see `whenPlaced`. */
525
+ runPlaced(): void {
526
+ this.placing = false;
527
+ const waiting = this.placedWaiting.splice(0);
528
+ if (this.state !== "live") {
529
+ return;
530
+ }
531
+ const failures: unknown[] = [];
532
+ for (const run of waiting) {
533
+ // (one of them may have deleted it, or a parent with it: the others get nothing)
534
+ if (this.state !== "live" || !r.Rwidget_is_valid(this.id)) {
535
+ break;
536
+ }
537
+ attempt(failures, () => run(this));
538
+ }
539
+ throwFirst(failures);
540
+ }
541
+
542
+ /** Says who to tell of a new handle, when what holds this widget is not a parent. */
543
+ setHolder(holder: ((widget: r.Rid) => void) | null): void {
544
+ this.holder = holder;
545
+ }
546
+
547
+ /** Whether a child is one of the widgets of this one: a window or a menu in the tree is not. */
548
+ private holds(child: HostNode): boolean {
549
+ return child.nativeWidget !== null && !floatsIn(this.tag, child.tag);
550
+ }
551
+
552
+ /** Whether an attribute is about the place of the widget in its parent: `Slot.Padding`. */
553
+ private static belongsToParent(name: string): boolean {
554
+ return name.startsWith("Slot.");
555
+ }
556
+
557
+ /**
558
+ * A list of items is emptied when it gets new ones, with the row it had selected: `Selected`,
559
+ * which did not change, comes back. This is also for a `Selected` that comes before `Items`.
560
+ */
561
+ private restoreSelected(): void {
562
+ const selected = this.resolved.attributes.get("Selected");
563
+ if (selected !== undefined && this.resolved.attributes.has("Items") && CONTROLLED[this.tag]) {
564
+ this.setAttribute("Selected", selected);
565
+ }
566
+ }
567
+
568
+ /** Sets one attribute. What the types let through and Raven refuses is said, not lost. */
569
+ private setAttribute(name: string, value: string): void {
570
+ if (name === virtualItemsOf(this.tag)?.count || name === MARKUP) {
571
+ return;
572
+ }
573
+ if (value === CLEARED) {
574
+ clearOverride(this.id, name);
575
+ return;
576
+ }
577
+ const controlled = CONTROLLED[this.tag]?.find(({ attribute }) => attribute === name);
578
+ if (controlled && (controlled.same ?? sameValue)(controlled.read(this.id), value)) {
579
+ return;
580
+ }
581
+ eventScope.ignoringEvents(() => {
582
+ const call = SET_BY_CALL[this.tag]?.[name];
583
+ if (call) {
584
+ call(this.id, value);
585
+ } else if (!r.Rwidget_set_attribute(this.id, name, value)) {
586
+ console.warn(`raven-tsx: <${this.tag}> did not take ${name}="${value}"`);
587
+ }
588
+ });
589
+ }
590
+
591
+ /**
592
+ * Gives a new widget all its props. That includes those of its place in a parent, which Raven
593
+ * only keeps until there is one: a DockArea places a child when it receives it, from `Slot.Dock`
594
+ * and the others, so they cannot wait until it has been added. `applyParentAttributes` then
595
+ * gives them again, which tells the parent, that already took its child, to look at them.
596
+ */
597
+ private applyAll(next: ResolvedProps): void {
598
+ for (const [name, value] of next.attributes) {
599
+ this.setAttribute(name, value);
600
+ }
601
+ this.applyData(new Map(), next);
602
+ applyHandlers(this.tag, this.id, new Map(), next.handlers);
603
+ this.applyVirtualItems(null, next);
604
+ this.resolved = next;
605
+ this.restoreSelected();
606
+ this.watchControlled();
607
+ }
608
+
609
+ /** Gives the widget what changed in its data props. */
610
+ private applyData(previous: ResolvedProps["data"], next: ResolvedProps): void {
611
+ const data = DATA_PROPS[this.tag];
612
+ if (data) {
613
+ eventScope.ignoringEvents(() => data.apply(this.id, previous, next.data));
614
+ }
615
+ }
616
+
617
+ /** Gives a list its items when their number or their function changed, and shows them. */
618
+ private applyVirtualItems(previous: ResolvedProps | null, next: ResolvedProps): void {
619
+ const virtual = virtualItemsOf(this.tag);
620
+ const handler = virtual ? next.handlers.get(virtual.prop) : undefined;
621
+ if (!virtual || !handler) {
622
+ return;
623
+ }
624
+ const count = next.attributes.get(virtual.count);
625
+ const before = previous?.handlers.get(virtual.prop);
626
+ if (before === handler && previous?.attributes.get(virtual.count) === count) {
627
+ return;
628
+ }
629
+ setVirtualItems(this.tag, this.id, Number(count ?? 0), handler);
630
+ if (before) {
631
+ r.Rwidget_invalidate(this.id);
632
+ }
633
+ }
634
+
635
+ /** After an event, puts back what the props say if a handler refused the change. */
636
+ private watchControlled(): void {
637
+ const controlled = CONTROLLED[this.tag];
638
+ const data = DATA_PROPS[this.tag];
639
+ if (!controlled && !data) {
640
+ return;
641
+ }
642
+ controlledSync.set(this.id, () => {
643
+ // (with nothing to refuse the change, the user's value stays, as for `unreported`: other events of the widget come here too)
644
+ if (data && this.resolved.handlers.has("OnChanged")) {
645
+ eventScope.ignoringEvents(() => data.restore(this.id, this.resolved.data));
646
+ }
647
+ for (const { attribute, unreported } of controlled ?? []) {
648
+ const wanted = this.resolved.attributes.get(attribute);
649
+ if (wanted !== undefined && !unreported) {
650
+ this.setAttribute(attribute, wanted);
651
+ }
652
+ }
653
+ });
654
+ }
655
+
656
+ /** Sets the attributes that need a parent, once there is one. */
657
+ applyParentAttributes(): void {
658
+ // (a part that failed later in the commit deleted it: its children it held are still live, with no widget)
659
+ if (this.state !== "live" || !r.Rwidget_is_valid(this.id)) {
660
+ return;
661
+ }
662
+ for (const [name, value] of this.resolved.attributes) {
663
+ if (WidgetInstance.belongsToParent(name)) {
664
+ this.setAttribute(name, value);
665
+ }
666
+ }
667
+ }
668
+
669
+ /** Goes from the last props to new ones, setting only what changed. */
670
+ update<Child>(props: RavenProps<Child>): void {
671
+ // (a framework that gives props one by one, Solid, gives some before the node is placed: Raven gets them at `materialize`)
672
+ if (this.state === "pending") {
673
+ this.pending = WidgetInstance.resolveChecked(this.tag, props);
674
+ return;
675
+ }
676
+ // (its window can be gone already: closed by the user, with no OnClose to ask first. An adopted widget has no props of the framework)
677
+ if (this.state !== "live" || !r.Rwidget_is_valid(this.id) || this.tag === ADOPTED_TAG) {
678
+ return;
679
+ }
680
+ const next = resolveProps(props, DATA_PROPS[this.tag]?.names);
681
+ DATA_PROPS[this.tag]?.check(next.data);
682
+ const previous = this.resolved;
683
+ // Raven has no way to give an attribute its default back, and no way to read it:
684
+ // a widget that loses an attribute is made again, from the attributes that are left.
685
+ // (so is one whose markup changed, which only counts when it is made)
686
+ if ([...previous.attributes.keys()].some((name) => !next.attributes.has(name)) || previous.attributes.get(MARKUP) !== next.attributes.get(MARKUP)) {
687
+ this.remake(next);
688
+ return;
689
+ }
690
+ for (const [name, value] of next.attributes) {
691
+ if (previous.attributes.get(name) !== value) {
692
+ this.setAttribute(name, value);
693
+ }
694
+ }
695
+ this.applyData(previous.data, next);
696
+ applyHandlers(this.tag, this.id, previous.handlers, next.handlers);
697
+ this.applyVirtualItems(previous, next);
698
+ if (next.handlers.get("OnPaint") !== previous.handlers.get("OnPaint")) {
699
+ r.Rwidget_invalidate(this.id);
700
+ }
701
+ this.resolved = next;
702
+ if (previous.attributes.get("Items") !== next.attributes.get("Items")) {
703
+ this.restoreSelected();
704
+ }
705
+ }
706
+
707
+ /**
708
+ * Replaces the widget by a new one in the same place, with the same children: those the framework
709
+ * does not know of too, added by the application. What a new widget holds by itself stays with it.
710
+ * If the new one cannot take the props, the old one stays as it was, and the framework hears of it.
711
+ */
712
+ private remake(next: ResolvedProps): void {
713
+ const old = this.id;
714
+ const previous = this.resolved;
715
+ const parent = this.parent;
716
+ const placed = parent?.holds(this) === true;
717
+ const index = placed ? parent.nativeIndexOf(this, true) : -1;
718
+ this.id = WidgetInstance.make(this.tag, next.attributes.get(MARKUP));
719
+ const madeChildren = r.Rwidget_get_child_count(this.id);
720
+ try {
721
+ this.applyAll(next);
722
+ } catch (error) {
723
+ forgetHandlers(this.tag, this.id, true);
724
+ r.Rwidget_destroy(this.id);
725
+ this.id = old;
726
+ this.resolved = previous;
727
+ throw error;
728
+ }
729
+ forgetHandlers(this.tag, old, true);
730
+ // What Raven made in the old widget: as many as in the new one, but for a markup that changed.
731
+ const own = r.Rwidget_get_child_count(this.id) + this.madeChildren - madeChildren;
732
+ this.madeChildren = madeChildren;
733
+ const moving = Array.from({ length: Math.max(r.Rwidget_get_child_count(old) - own, 0) }, (_, position) => r.Rwidget_get_child(old, own + position));
734
+ for (const child of moving) {
735
+ r.Rwidget_remove_child(old, child);
736
+ r.Rwidget_add_child(this.id, child);
737
+ }
738
+ if (placed) {
739
+ r.Rwidget_remove_child(parent.id, old);
740
+ r.Rwidget_insert_child(parent.id, this.id, index);
741
+ }
742
+ this.holder?.(this.id);
743
+ if (r.Rwidget_is_valid(old)) {
744
+ r.Rwidget_destroy(old);
745
+ }
746
+ this.applyParentAttributes();
747
+ }
748
+
749
+ /**
750
+ * Where a child goes among the widgets of this one. Those the framework knows are enough, unless
751
+ * the application added others: then it is just before the next child the framework knows.
752
+ * `attached` says whether Raven already has the child, so that it counts among what Raven holds.
753
+ */
754
+ private nativeIndexOf(child: HostNode, attached: boolean): number {
755
+ const held = this.children.filter((other) => this.holds(other));
756
+ const index = held.indexOf(child);
757
+ const count = r.Rwidget_get_child_count(this.id);
758
+ if (count <= (attached ? held.length : held.length - 1)) {
759
+ return index;
760
+ }
761
+ const positionOf = (widget: r.Rid | null): number => {
762
+ for (let position = 0; position < count; position++) {
763
+ if (r.Rwidget_get_child(this.id, position) === widget) {
764
+ return position;
765
+ }
766
+ }
767
+ return -1;
768
+ };
769
+ const here = positionOf(child.nativeWidget);
770
+ if (here >= 0) {
771
+ return here;
772
+ }
773
+ const next = positionOf(held[index + 1]?.nativeWidget ?? null);
774
+ return next >= 0 ? next : count;
775
+ }
776
+
777
+ /** Makes sure a widget has a widget to go in: the top of a root of windows has none. */
778
+ private checkHolds(child: HostNode): void {
779
+ if (this.holds(child) && this.id === r.R_INVALID_ID) {
780
+ RavenError.throw(`raven-tsx: <${child.tag}> must be inside a <Window>`);
781
+ }
782
+ }
783
+
784
+ /**
785
+ * Adds a child while this new widget is rendered: to the tree here only. Raven gets it with
786
+ * this widget, at `materialize`, if they are placed. (An adopted child stays where it is
787
+ * shown until then)
788
+ */
789
+ appendInitial(child: HostNode): void {
790
+ if (this.state === "live") {
791
+ this.append(child);
792
+ return;
793
+ }
794
+ this.children.push(child);
795
+ child.setParent(this);
796
+ }
797
+
798
+ /** Adds a child at the end, making it in Raven if it was only pending. A child already here moves to the end. */
799
+ append(child: HostNode): void {
800
+ if (this.state === "pending") {
801
+ this.detach(child);
802
+ this.appendInitial(child);
803
+ return;
804
+ }
805
+ this.checkHolds(child);
806
+ this.detach(child);
807
+ this.children.push(child);
808
+ child.setParent(this);
809
+ this.make(child, () => {
810
+ // (at its place, not at the end: making it ran user code, which may have added others after it)
811
+ if (this.holds(child)) {
812
+ r.Rwidget_insert_child(this.id, child.nativeWidget as r.Rid, this.placeAmongPlaced(child));
813
+ }
814
+ });
815
+ }
816
+
817
+ /**
818
+ * Makes a child that is here in Raven, then places it there. The child is recorded first: if
819
+ * making it fails with something of it left in Raven (a window open whose function threw), what
820
+ * deletes this deletes it. One that Raven does not have after a failure is not kept.
821
+ */
822
+ private make(child: HostNode, place: () => void): void {
823
+ const failures: unknown[] = [];
824
+ attempt(failures, () => child.materialize());
825
+ // (one that failed and that Raven does not have is not kept. Nor one whose making ran user code
826
+ // that deleted this: deleting this widget did not reach it, not placed yet)
827
+ // (making it ran user code, which may have placed it elsewhere: that parent has it, as it is)
828
+ if (!this.children.includes(child)) {
829
+ throwFirst(failures);
830
+ return;
831
+ }
832
+ if ((failures.length > 0 && !child.placed) || this.state !== "live") {
833
+ const index = this.children.indexOf(child);
834
+ if (index >= 0) {
835
+ this.children.splice(index, 1);
836
+ }
837
+ child.setParent(null);
838
+ // (what is left of it in JavaScript goes: its handlers, a window Raven destroyed)
839
+ attempt(failures, () => {
840
+ child.forgetTree();
841
+ child.dispose();
842
+ });
843
+ throwFirst(failures);
844
+ return;
845
+ }
846
+ // (making it ran user code, which may have taken it away from here)
847
+ if (this.children.includes(child)) {
848
+ child.rememberTree();
849
+ place();
850
+ // (in a tree that is not shown, its windows stay closed: they open when it is placed again)
851
+ attempt(failures, () => (this.attached ? child.openWindows() : child.closeWindows()));
852
+ }
853
+ throwFirst(failures);
854
+ }
855
+
856
+ /** Puts a child before another one. A child already here moves. */
857
+ insertBefore(child: HostNode, before: HostNode): void {
858
+ const pending = this.state === "pending";
859
+ if (!pending) {
860
+ this.checkHolds(child);
861
+ }
862
+ this.detach(child);
863
+ const index = this.children.indexOf(before);
864
+ if (index < 0) {
865
+ RavenError.throw(`raven-tsx: <${before.tag}> is not a child of <${this.tag}>`);
866
+ }
867
+ this.children.splice(index, 0, child);
868
+ child.setParent(this);
869
+ if (!pending) {
870
+ this.make(child, () => {
871
+ if (this.holds(child)) {
872
+ r.Rwidget_insert_child(this.id, child.nativeWidget as r.Rid, this.placeAmongPlaced(child));
873
+ }
874
+ });
875
+ }
876
+ }
877
+
878
+ /** Takes a child out without deleting it. */
879
+ private detach(child: HostNode): void {
880
+ const index = this.children.indexOf(child);
881
+ if (index >= 0) {
882
+ this.children.splice(index, 1);
883
+ const widget = child.nativeWidget;
884
+ // (a node not placed yet has no widget, and neither has a child that was not placed with it)
885
+ // (also while it is disposed: a cleanup run then may take one away, which must not go with it)
886
+ if (r.Rwidget_is_valid(this.id) && this.holds(child) && widget !== null && r.Rwidget_is_valid(widget) && r.Rwidget_get_parent(widget) === this.id) {
887
+ r.Rwidget_remove_child(this.id, widget);
888
+ }
889
+ }
890
+ }
891
+
892
+ release(child: HostNode): void {
893
+ if (this.children.includes(child)) {
894
+ this.detach(child);
895
+ child.setParent(null);
896
+ }
897
+ }
898
+
899
+ closeWindows(): void {
900
+ // (all of them, also past one that throws)
901
+ const failures: unknown[] = [];
902
+ const pass = ++this.windowsPass;
903
+ for (const child of [...this.children]) {
904
+ // (a cleanup of one may have shown this tree again, opening its windows: this pass is over)
905
+ if (this.windowsPass !== pass) {
906
+ break;
907
+ }
908
+ // (or moved another away: it is no longer this one's to close)
909
+ if (this.children.includes(child)) {
910
+ attempt(failures, () => child.closeWindows());
911
+ }
912
+ }
913
+ throwFirst(failures);
914
+ }
915
+
916
+ openWindows(): void {
917
+ const failures: unknown[] = [];
918
+ const pass = ++this.windowsPass;
919
+ for (const child of [...this.children]) {
920
+ // (and one that closes them again ends this one)
921
+ if (this.windowsPass !== pass) {
922
+ break;
923
+ }
924
+ if (this.children.includes(child)) {
925
+ attempt(failures, () => child.openWindows());
926
+ }
927
+ }
928
+ throwFirst(failures);
929
+ }
930
+
931
+ /** Takes every child out, alive, and gives them: a window that closes keeps what it holds. */
932
+ releaseChildren(): HostNode[] {
933
+ const children = [...this.children];
934
+ for (const child of children) {
935
+ this.release(child);
936
+ }
937
+ return children;
938
+ }
939
+
940
+ /** Removes a child and deletes it with everything under it. */
941
+ remove(child: HostNode): void {
942
+ child.forgetTree();
943
+ // (the window may be gone already, closed by the user, with its widgets. An adopted widget
944
+ // may be in another place already, where the component moved it)
945
+ const widget = child.nativeWidget;
946
+ if (this.holds(child) && widget !== null && r.Rwidget_is_valid(widget) && r.Rwidget_get_parent(widget) === this.id) {
947
+ r.Rwidget_remove_child(this.id, widget);
948
+ }
949
+ const index = this.children.indexOf(child);
950
+ if (index >= 0) {
951
+ this.children.splice(index, 1);
952
+ }
953
+ child.setParent(null);
954
+ child.dispose();
955
+ }
956
+
957
+ forgetTree(): void {
958
+ this.forgotten = true;
959
+ // (what it holds may be made, added to it while it waited)
960
+ for (const child of this.children) {
961
+ child.forgetTree();
962
+ }
963
+ // (a node Raven has not made has no handlers: it gets them at `materialize`)
964
+ if (this.state === "live") {
965
+ forgetHandlers(this.tag, this.id, false);
966
+ }
967
+ }
968
+
969
+ rememberTree(): void {
970
+ if (!this.forgotten) {
971
+ return;
972
+ }
973
+ this.forgotten = false;
974
+ for (const child of [...this.children]) {
975
+ child.rememberTree();
976
+ }
977
+ // (an adopted widget has no handlers of the framework: those of its element are its own)
978
+ if (this.state === "live" && this.tag !== ADOPTED_TAG && r.Rwidget_is_valid(this.id)) {
979
+ applyHandlers(this.tag, this.id, new Map(), this.resolved.handlers);
980
+ this.applyVirtualItems(null, this.resolved);
981
+ this.watchControlled();
982
+ }
983
+ }
984
+
985
+ /** Deletes what is in this tree without being a widget of it: deleting a widget does not reach those. */
986
+ disposeDetached(): void {
987
+ // (all of them, also past one whose cleanup throws)
988
+ const failures: unknown[] = [];
989
+ const parent = this.parent;
990
+ for (const child of [...this.children]) {
991
+ // (a cleanup of one may have placed this tree elsewhere: what is left is that place's now)
992
+ if (this.parent !== parent) {
993
+ break;
994
+ }
995
+ // (or taken another away: it is no longer this one's)
996
+ if (!this.children.includes(child)) {
997
+ continue;
998
+ }
999
+ if (!this.holds(child)) {
1000
+ attempt(failures, () => child.dispose());
1001
+ } else if (child instanceof WidgetInstance) {
1002
+ attempt(failures, () => child.disposeDetached());
1003
+ }
1004
+ }
1005
+ throwFirst(failures);
1006
+ }
1007
+
1008
+ /**
1009
+ * Takes the adopted widgets of this tree out of the widgets that hold them, before those are
1010
+ * destroyed: they belong to the element of their registered tag, which may show them again.
1011
+ * Those the component already moved elsewhere are left where they are.
1012
+ */
1013
+ releaseAdopted(): void {
1014
+ for (const child of this.children) {
1015
+ if (child.tag === ADOPTED_TAG) {
1016
+ const widget = child.nativeWidget;
1017
+ if (widget !== null && r.Rwidget_is_valid(widget) && r.Rwidget_get_parent(widget) === this.id) {
1018
+ r.Rwidget_remove_child(this.id, widget);
1019
+ }
1020
+ } else if (child instanceof WidgetInstance) {
1021
+ child.releaseAdopted();
1022
+ }
1023
+ }
1024
+ }
1025
+
1026
+ dispose(): void {
1027
+ if (this.tag === ADOPTED_TAG) {
1028
+ // (its parent let it go, unless it was already placed elsewhere: it belongs to the element of its registered tag)
1029
+ return;
1030
+ }
1031
+ const state = this.state;
1032
+ this.state = "gone";
1033
+ if (state !== "live") {
1034
+ // (what it holds may be made, added to it while it waited: deleting no widget deletes those)
1035
+ const failures: unknown[] = [];
1036
+ for (const child of [...this.children]) {
1037
+ // (a cleanup of one may have taken another away: it is no longer this one's)
1038
+ if (this.children.includes(child)) {
1039
+ attempt(failures, () => child.dispose());
1040
+ }
1041
+ }
1042
+ throwFirst(failures);
1043
+ return;
1044
+ }
1045
+ if (r.Rwidget_is_valid(this.id)) {
1046
+ this.releaseAdopted();
1047
+ }
1048
+ try {
1049
+ this.disposeDetached();
1050
+ } finally {
1051
+ // (also when a cleanup of what it held threw: it is gone, and nothing deletes it after)
1052
+ if (r.Rwidget_is_valid(this.id)) {
1053
+ r.Rwidget_destroy(this.id);
1054
+ }
1055
+ }
1056
+ }
1057
+ }