@decky.fx/raven-tsx 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,748 @@
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
+ /** The host type of a widget Raven made and an adapter only places: `adopt`. */
8
+ export const ADOPTED_TAG = "raven-tsx:adopted";
9
+
10
+ /**
11
+ * Where a node is. `pending`: the framework rendered it, and it is only props and children here,
12
+ * with nothing in Raven, as the framework may throw that render away. `live`: it is placed, and
13
+ * Raven made it. `gone`: it was deleted, or could not be made.
14
+ */
15
+ type NodeState = "pending" | "live" | "gone";
16
+
17
+ /** The nodes Raven made in this batch of changes, whose attributes that need a parent wait for its end. */
18
+ const placedInThisBatch: WidgetInstance[] = [];
19
+
20
+ /**
21
+ * Ends a batch of changes, what an adapter calls once all of it is placed: React at the end of a
22
+ * commit. The nodes made during the batch get their attributes that need a parent, as they have one now.
23
+ */
24
+ export function finishPlacing(): void {
25
+ for (const instance of placedInThisBatch.splice(0)) {
26
+ instance.applyParentAttributes();
27
+ }
28
+ }
29
+
30
+ /** What a framework's tree is made of: a widget, or a window, which is no widget of its parent. */
31
+ export interface HostNode {
32
+ readonly tag: string;
33
+ /** The widget to put in a parent, or null for a window, which stands by itself. */
34
+ readonly nativeWidget: r.Rid | null;
35
+ /** Says who holds it. */
36
+ setParent(parent: WidgetInstance | null): void;
37
+ /**
38
+ * Makes it in Raven, with what it holds, when it is placed: what a framework renders and then
39
+ * throws away (an error, a render done again) never reaches Raven. Throws, leaving nothing in
40
+ * Raven, when Raven cannot make it.
41
+ */
42
+ materialize(): void;
43
+ update<Child>(props: RavenProps<Child>): void;
44
+ applyParentAttributes(): void;
45
+ append(child: HostNode): void;
46
+ /** Adds a child while this new node is rendered, in its props only: Raven gets it at `materialize`. */
47
+ appendInitial(child: HostNode): void;
48
+ insertBefore(child: HostNode, before: HostNode): void;
49
+ remove(child: HostNode): void;
50
+ /** Forgets the handlers of this node and of everything under it. */
51
+ forgetTree(): void;
52
+ /** Deletes it, after its parent let it go. */
53
+ dispose(): void;
54
+ }
55
+
56
+ /** How to read the value a widget holds by itself, for the attribute that sets it. */
57
+ interface ControlledValue {
58
+ readonly attribute: string;
59
+ /** The value of the widget, as the attribute would be written. */
60
+ readonly read: (widget: r.Rid) => string;
61
+ /** Whether the widget already holds a value, when `sameValue` is not the way to tell. */
62
+ readonly same?: ((held: string, wanted: string) => boolean) | undefined;
63
+ /**
64
+ * The user changes it without an event that says so: it is not set to what it holds, and not
65
+ * put back either, or the view would jump back at the next event of something else.
66
+ */
67
+ readonly unreported?: true | undefined;
68
+ }
69
+
70
+ const numberText = (value: number): string => String(value);
71
+ const twoDigits = (value: number): string => String(value).padStart(2, "0");
72
+
73
+ const flagText = (value: number): string => (value ? "true" : "false");
74
+ /** A text is that text: "1.0" is not "1", which is what makes a number the same as another. */
75
+ const sameText = (held: string, wanted: string): boolean => held === wanted;
76
+ /** A code of a color, in either case. */
77
+ const sameColor = (held: string, wanted: string): boolean => held.toLowerCase() === wanted.toLowerCase();
78
+
79
+ /**
80
+ * The attributes of what the user changes directly, by tag: the value is held by the widget, so
81
+ * an attribute must not be set to what it already holds. `src/test/core/coverage.test.ts` says why
82
+ * the other attributes of these tags are not here.
83
+ */
84
+ const CONTROLLED: Readonly<Record<string, readonly ControlledValue[]>> = {
85
+ TextInput: [{ attribute: "Text", read: (widget) => r.Rtext_input_get_text(widget) ?? "", same: sameText }],
86
+ TextEditor: [
87
+ { attribute: "Text", read: (widget) => r.Rtext_editor_get_text(widget) ?? "", same: sameText },
88
+ { attribute: "Zoom", read: (widget) => numberText(r.Rtext_editor_get_zoom(widget)), unreported: true },
89
+ ],
90
+ Checkbox: [{ attribute: "Checked", read: (widget) => flagText(r.Rcheckbox_is_checked(widget)) }],
91
+ Slider: [{ attribute: "Value", read: (widget) => numberText(r.Rslider_get_value(widget)) }],
92
+ SpinBox: [{ attribute: "Value", read: (widget) => numberText(r.Rspin_box_get_value(widget)) }],
93
+ ComboBox: [{ attribute: "Selected", read: (widget) => numberText(r.Rcombo_box_get_selected(widget)) }],
94
+ ListView: [{ attribute: "Selected", read: (widget) => numberText(r.Rlist_view_get_selected(widget)) }],
95
+ TabView: [{ attribute: "ActiveTab", read: (widget) => numberText(r.Rtab_view_get_active(widget)) }],
96
+ Expander: [{ attribute: "Expanded", read: (widget) => flagText(r.Rexpander_is_expanded(widget)) }],
97
+ Island: [{ attribute: "Expanded", read: (widget) => flagText(r.Risland_is_expanded(widget)) }],
98
+ Table: [
99
+ { attribute: "Selected", read: (widget) => numberText(r.Rtable_get_selected(widget)) },
100
+ { attribute: "SortColumn", read: (widget) => numberText(r.Rtable_get_sort_column(widget)) },
101
+ ],
102
+ Rating: [{ attribute: "Value", read: (widget) => numberText(r.Rrating_get_value(widget)) }],
103
+ Pagination: [{ attribute: "Page", read: (widget) => numberText(r.Rpagination_get_page(widget)) }],
104
+ DatePicker: [{
105
+ attribute: "Date",
106
+ read: (widget) => {
107
+ const [year, month, day] = r.Rdate_picker_get_date(widget);
108
+ return `${year}-${twoDigits(month)}-${twoDigits(day)}`;
109
+ },
110
+ }],
111
+ TimePicker: [{
112
+ attribute: "Time",
113
+ read: (widget) => r.Rtime_picker_get_time(widget).map(twoDigits).join(":"),
114
+ same: (held, wanted) => held === wanted || held === `${wanted}:00`,
115
+ }],
116
+ ColorPicker: [{ attribute: "Color", read: (widget) => r.Rcolor_picker_get_hex(widget) ?? "", same: sameColor }],
117
+ MenuItem: [{ attribute: "Checked", read: (widget) => flagText(r.Rmenu_item_is_checked(widget)) }],
118
+ DockArea: [{ attribute: "Layout", read: (widget) => r.Rdock_area_save_layout(widget) ?? "", same: sameText }],
119
+ Timeline: [
120
+ { attribute: "Time", read: (widget) => numberText(r.Rtimeline_get_time(widget)) },
121
+ { attribute: "ViewStart", read: (widget) => numberText(r.Rtimeline_get_view(widget)[0]), unreported: true },
122
+ { attribute: "ViewEnd", read: (widget) => numberText(r.Rtimeline_get_view(widget)[1]), unreported: true },
123
+ ],
124
+ NodeGraph: [
125
+ { attribute: "CenterX", read: (widget) => numberText(r.Rnode_graph_get_view(widget)[0]), unreported: true },
126
+ { attribute: "CenterY", read: (widget) => numberText(r.Rnode_graph_get_view(widget)[1]), unreported: true },
127
+ { attribute: "Zoom", read: (widget) => numberText(r.Rnode_graph_get_view(widget)[2]), unreported: true },
128
+ ],
129
+ BlockEditor: [{ attribute: "Zoom", read: (widget) => numberText(r.Rblock_editor_get_view(widget)[2]), unreported: true }],
130
+ };
131
+
132
+ /** The attributes held by the widgets of a tag, for the test of what is covered. */
133
+ export const controlledAttributes = (tag: string): readonly string[] => (CONTROLLED[tag] ?? []).map(({ attribute }) => attribute);
134
+
135
+ /** Whether what `Items` holds, once parsed, are rows a TreeView can show. */
136
+ function isRows(value: unknown): value is readonly TreeItem[] {
137
+ return Array.isArray(value) && value.every((row: TreeItem | null) => typeof row?.Text === "string" && isRows(row.Children ?? []));
138
+ }
139
+
140
+ /** The rows of the `Items` of a TreeView, from its text. Throws when they are not rows. */
141
+ function treeRows(text: string): readonly TreeItem[] {
142
+ const items: unknown = text ? JSON.parse(text) : [];
143
+ if (!isRows(items)) {
144
+ RavenError.throw("raven-tsx: the Items of a TreeView should be a list of rows, each with a Text");
145
+ }
146
+ return items;
147
+ }
148
+
149
+ /** Fills a TreeView from its `Items`, as text. Its rows are made again: their handles change. Text that is not rows leaves them. */
150
+ function fillTree(tree: r.Rid, text: string): void {
151
+ const items = treeRows(text);
152
+ const add = (parent: r.Rid, items: readonly TreeItem[]): void => {
153
+ for (const item of items) {
154
+ const row = r.Rtree_view_add_item(tree, parent, item.Text);
155
+ if (item.Expanded) {
156
+ r.Rtree_item_set_expanded(row, r.R_TRUE);
157
+ }
158
+ add(row, item.Children ?? []);
159
+ }
160
+ };
161
+ r.Rtree_view_clear_items(tree);
162
+ add(r.R_INVALID_ID, items);
163
+ }
164
+
165
+ /** The styles of `FontStyle`, of the font of the theme. */
166
+ 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 };
167
+
168
+ /** `TextDisabled` as the C API writes it: `TEXT_DISABLED`. */
169
+ const constantName = (name: string): string => name.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase();
170
+
171
+ /** The roles and metrics an override of a group is for, or undefined: only those can be taken back. */
172
+ export function overrideOf(name: string): { readonly role: r.RColorRole } | { readonly metric: r.RMetric } | undefined {
173
+ const [group, key = ""] = name.split(".");
174
+ if (group === "Color") {
175
+ const role = r.RColorRole[`R_COLOR_${constantName(key)}` as keyof typeof r.RColorRole];
176
+ return role === undefined ? undefined : { role };
177
+ }
178
+ if (group === "Metric") {
179
+ const metric = r.RMetric[`R_METRIC_${constantName(key)}` as keyof typeof r.RMetric];
180
+ return metric === undefined ? undefined : { metric };
181
+ }
182
+ return undefined;
183
+ }
184
+
185
+ /** Gives a color or a metric back to what the theme says. */
186
+ function clearOverride(widget: r.Rid, name: string): void {
187
+ const override = overrideOf(name);
188
+ if (override && "role" in override) {
189
+ r.Rwidget_clear_color(widget, override.role);
190
+ } else if (override) {
191
+ r.Rwidget_clear_metric(widget, override.metric);
192
+ } else {
193
+ console.warn(`raven-tsx: ${name} cannot be taken back: leave the prop out instead`);
194
+ }
195
+ }
196
+
197
+ /** The props XML does not take, by tag: they are set by calling Raven. */
198
+ const SET_BY_CALL: Readonly<Record<string, Readonly<Record<string, (widget: r.Rid, value: string) => void>>>> = {
199
+ Image: { Texture: (widget, value) => r.Rimage_set_texture(widget, Number(value)) },
200
+ Label: {
201
+ FontStyle: (widget, value) => {
202
+ const style = FONT_STYLES[value];
203
+ if (style !== undefined) {
204
+ r.Rlabel_set_font(widget, r.Rfont_get_variant(r.Rfont_get_default(), style));
205
+ }
206
+ },
207
+ },
208
+ TreeView: { Items: fillTree },
209
+ };
210
+
211
+ /**
212
+ * What can be found wrong with props without Raven, by tag: it is checked while a framework
213
+ * renders, so that its nearest error boundary takes it, as for any error of a render. (React gives
214
+ * an error of its commit to a boundary above what it is placing: not to one placed with it)
215
+ */
216
+ const CHECKED_AT_RENDER: Readonly<Record<string, Readonly<Record<string, (value: string) => void>>>> = {
217
+ TreeView: { Items: (value) => void treeRows(value) },
218
+ };
219
+
220
+ /** What a tag does with its data props, which reach Raven by calling it, as they are. */
221
+ interface DataProps {
222
+ readonly names: readonly string[];
223
+ /** Throws when the data cannot be shown. Comes before anything changes. */
224
+ readonly check: (data: ResolvedProps["data"]) => void;
225
+ /** Gives the widget what changed. */
226
+ readonly apply: (widget: r.Rid, previous: ResolvedProps["data"], next: ResolvedProps["data"]) => void;
227
+ /** After an event, puts back what the props say if a handler refused the change. */
228
+ readonly restore: (widget: r.Rid, data: ResolvedProps["data"]) => void;
229
+ }
230
+
231
+ const DATA_PROPS: Readonly<Record<string, DataProps>> = {
232
+ Plot: { names: ["Series", "DragLines"], check: checkPlotData, apply: applyPlotData, restore: restorePlotData },
233
+ };
234
+
235
+ /** The prop that holds the elements XML takes inside a widget. It is part of how the widget is made. */
236
+ const MARKUP = "Markup";
237
+
238
+ /** Whether a tag stands outside what holds it: a `Menu` paints as a panel unless a `MenuBar` or a `Menu` holds it. */
239
+ const floatsIn = (parent: string, child: string): boolean => child === "Menu" && parent !== "MenuBar" && parent !== "Menu";
240
+
241
+ /** Whether two values of an attribute are the same. A Slider holds a float: its value comes back rounded. */
242
+ function sameValue(held: string, wanted: string): boolean {
243
+ if (held === wanted) {
244
+ return true;
245
+ }
246
+ const a = Number(held);
247
+ const b = Number(wanted);
248
+ return held !== "" && wanted !== "" && !Number.isNaN(a) && !Number.isNaN(b) && Math.abs(a - b) < 1e-5;
249
+ }
250
+
251
+ /**
252
+ * A widget under a framework, and what a `ref` receives. Raven's handle is here with what the
253
+ * renderer must remember: the tree, because Raven has no way to ask where a child is, and
254
+ * the last props.
255
+ */
256
+ export class WidgetInstance implements HostNode {
257
+ private readonly children: HostNode[] = [];
258
+ private parent: WidgetInstance | null = null;
259
+ /** Told of a new handle when what holds it is not a parent: the island of a window. */
260
+ private holder: ((widget: r.Rid) => void) | null = null;
261
+ private resolved: ResolvedProps = { attributes: new Map(), handlers: new Map(), data: new Map() };
262
+ /** How many children the widget had when it was made: those of its markup come first. */
263
+ private madeChildren = 0;
264
+ private state: NodeState;
265
+
266
+ private constructor(
267
+ private id: r.Rid,
268
+ readonly tag: string,
269
+ /** The props of a node a framework rendered, until Raven makes it. */
270
+ private pending: ResolvedProps | null = null,
271
+ ) {
272
+ this.state = pending ? "pending" : "live";
273
+ }
274
+
275
+ /** A new widget of a tag, as a framework renders it: Raven makes it once it is placed, at `materialize`. */
276
+ static create<Child>(tag: string, props: RavenProps<Child>): WidgetInstance {
277
+ const resolved = resolveProps(props, DATA_PROPS[tag]?.names);
278
+ DATA_PROPS[tag]?.check(resolved.data);
279
+ for (const [name, check] of Object.entries(CHECKED_AT_RENDER[tag] ?? {})) {
280
+ const value = resolved.attributes.get(name);
281
+ if (value !== undefined) {
282
+ check(value);
283
+ }
284
+ }
285
+ return new WidgetInstance(r.R_INVALID_ID, tag, resolved);
286
+ }
287
+
288
+ /**
289
+ * Makes the widget, with its props but those that need a parent, then its children, in their
290
+ * order: those of its markup come first. If any of it fails, what was made is deleted.
291
+ */
292
+ materialize(): void {
293
+ const resolved = this.pending;
294
+ if (this.state !== "pending" || !resolved) {
295
+ return;
296
+ }
297
+ this.id = WidgetInstance.make(this.tag, resolved.attributes.get(MARKUP));
298
+ this.madeChildren = r.Rwidget_get_child_count(this.id);
299
+ this.state = "live";
300
+ this.pending = null;
301
+ try {
302
+ this.applyAll(resolved);
303
+ for (const child of this.children) {
304
+ child.materialize();
305
+ // (an adopted widget Raven moves here from where it is shown; one its element destroyed is left out)
306
+ const widget = child.nativeWidget;
307
+ if (this.holds(child) && widget !== null && r.Rwidget_is_valid(widget)) {
308
+ r.Rwidget_add_child(this.id, widget);
309
+ }
310
+ }
311
+ } catch (error) {
312
+ // (the framework keeps it, and takes it away with the rest of the tree that failed)
313
+ this.forgetTree();
314
+ this.dispose();
315
+ throw error;
316
+ }
317
+ placedInThisBatch.push(this);
318
+ }
319
+
320
+ /** An instance for a widget the framework does not own: the top of a root. */
321
+ static forContainer(id: r.Rid): WidgetInstance {
322
+ return new WidgetInstance(id, "");
323
+ }
324
+
325
+ /**
326
+ * A widget Raven made, a child of a registered tag in XML: an adapter places it where it is
327
+ * rendered, and sets nothing on it. Taken out of the tree, it is only let go, alive: the
328
+ * component may show it again, or move it into another box. Its element destroys it.
329
+ */
330
+ static adopt(id: r.Rid): WidgetInstance {
331
+ // (it stays where it is shown: the framework may drop the render that made this, and Raven moves a
332
+ // widget it adds from the parent it had)
333
+ return new WidgetInstance(id, ADOPTED_TAG);
334
+ }
335
+
336
+ /** Makes the widget of a tag in Raven. */
337
+ private static make(tag: string, markup = ""): r.Rid {
338
+ const id = r.Rwidget_create_from_xml(`<${tag}>${markup}</${tag}>`);
339
+ if (id === r.R_INVALID_ID) {
340
+ RavenError.fromLibrary(`Raven cannot make <${tag}>`);
341
+ }
342
+ return id;
343
+ }
344
+
345
+ /**
346
+ * Raven's handle of the widget, for the functions of the `raven` export: `R_INVALID_ID` until
347
+ * it is placed (a ref and an effect come after). Read it when you need it and do not keep
348
+ * it: it changes if the widget has to be made again.
349
+ */
350
+ get widget(): r.Rid {
351
+ return this.id;
352
+ }
353
+
354
+ /** Gives the widget the keyboard. */
355
+ focus(): void {
356
+ r.Rwidget_focus(this.id);
357
+ }
358
+
359
+ get nativeWidget(): r.Rid {
360
+ return this.id;
361
+ }
362
+
363
+ /**
364
+ * Opens a `<Menu>` that stands by itself as a popup, at a position of its window. A right click
365
+ * gives that position: `OnContext` of a TreeView or a TileView, in the window.
366
+ */
367
+ open(x: number, y: number): void {
368
+ let holder = this.parent;
369
+ while (holder && holder.id === r.R_INVALID_ID) {
370
+ holder = holder.parent;
371
+ }
372
+ const window = holder ? r.Rwidget_get_window(holder.id) : r.R_INVALID_ID;
373
+ if (this.tag !== "Menu" || window === r.R_INVALID_ID) {
374
+ RavenError.throw(`raven-tsx: only a <Menu> in a window opens, and this is a <${this.tag}>`);
375
+ }
376
+ r.Rmenu_open(this.id, window, x, y);
377
+ }
378
+
379
+ setParent(parent: WidgetInstance | null): void {
380
+ this.parent = parent;
381
+ }
382
+
383
+ /** Says who to tell of a new handle, when what holds this widget is not a parent. */
384
+ setHolder(holder: ((widget: r.Rid) => void) | null): void {
385
+ this.holder = holder;
386
+ }
387
+
388
+ /** Whether a child is one of the widgets of this one: a window or a menu in the tree is not. */
389
+ private holds(child: HostNode): boolean {
390
+ return child.nativeWidget !== null && !floatsIn(this.tag, child.tag);
391
+ }
392
+
393
+ /** Whether an attribute is about the place of the widget in its parent: `Slot.Padding`. */
394
+ private static belongsToParent(name: string): boolean {
395
+ return name.startsWith("Slot.");
396
+ }
397
+
398
+ /**
399
+ * A list of items is emptied when it gets new ones, with the row it had selected: `Selected`,
400
+ * which did not change, comes back. This is also for a `Selected` that comes before `Items`.
401
+ */
402
+ private restoreSelected(): void {
403
+ const selected = this.resolved.attributes.get("Selected");
404
+ if (selected !== undefined && this.resolved.attributes.has("Items") && CONTROLLED[this.tag]) {
405
+ this.setAttribute("Selected", selected);
406
+ }
407
+ }
408
+
409
+ /** Sets one attribute. What the types let through and Raven refuses is said, not lost. */
410
+ private setAttribute(name: string, value: string): void {
411
+ if (name === virtualItemsOf(this.tag)?.count || name === MARKUP) {
412
+ return;
413
+ }
414
+ if (value === CLEARED) {
415
+ clearOverride(this.id, name);
416
+ return;
417
+ }
418
+ const controlled = CONTROLLED[this.tag]?.find(({ attribute }) => attribute === name);
419
+ if (controlled && (controlled.same ?? sameValue)(controlled.read(this.id), value)) {
420
+ return;
421
+ }
422
+ eventScope.ignoringEvents(() => {
423
+ const call = SET_BY_CALL[this.tag]?.[name];
424
+ if (call) {
425
+ call(this.id, value);
426
+ } else if (!r.Rwidget_set_attribute(this.id, name, value)) {
427
+ console.warn(`raven-tsx: <${this.tag}> did not take ${name}="${value}"`);
428
+ }
429
+ });
430
+ }
431
+
432
+ /**
433
+ * Gives a new widget all its props. That includes those of its place in a parent, which Raven
434
+ * only keeps until there is one: a DockArea places a child when it receives it, from `Slot.Dock`
435
+ * and the others, so they cannot wait until it has been added. `applyParentAttributes` then
436
+ * gives them again, which tells the parent, that already took its child, to look at them.
437
+ */
438
+ private applyAll(next: ResolvedProps): void {
439
+ for (const [name, value] of next.attributes) {
440
+ this.setAttribute(name, value);
441
+ }
442
+ this.applyData(new Map(), next);
443
+ applyHandlers(this.tag, this.id, new Map(), next.handlers);
444
+ this.applyVirtualItems(null, next);
445
+ this.resolved = next;
446
+ this.restoreSelected();
447
+ this.watchControlled();
448
+ }
449
+
450
+ /** Gives the widget what changed in its data props. */
451
+ private applyData(previous: ResolvedProps["data"], next: ResolvedProps): void {
452
+ const data = DATA_PROPS[this.tag];
453
+ if (data) {
454
+ eventScope.ignoringEvents(() => data.apply(this.id, previous, next.data));
455
+ }
456
+ }
457
+
458
+ /** Gives a list its items when their number or their function changed, and shows them. */
459
+ private applyVirtualItems(previous: ResolvedProps | null, next: ResolvedProps): void {
460
+ const virtual = virtualItemsOf(this.tag);
461
+ const handler = virtual ? next.handlers.get(virtual.prop) : undefined;
462
+ if (!virtual || !handler) {
463
+ return;
464
+ }
465
+ const count = next.attributes.get(virtual.count);
466
+ const before = previous?.handlers.get(virtual.prop);
467
+ if (before === handler && previous?.attributes.get(virtual.count) === count) {
468
+ return;
469
+ }
470
+ setVirtualItems(this.tag, this.id, Number(count ?? 0), handler);
471
+ if (before) {
472
+ r.Rwidget_invalidate(this.id);
473
+ }
474
+ }
475
+
476
+ /** After an event, puts back what the props say if a handler refused the change. */
477
+ private watchControlled(): void {
478
+ const controlled = CONTROLLED[this.tag];
479
+ const data = DATA_PROPS[this.tag];
480
+ if (!controlled && !data) {
481
+ return;
482
+ }
483
+ controlledSync.set(this.id, () => {
484
+ // (with nothing to refuse the change, the user's value stays, as for `unreported`: other events of the widget come here too)
485
+ if (data && this.resolved.handlers.has("OnChanged")) {
486
+ eventScope.ignoringEvents(() => data.restore(this.id, this.resolved.data));
487
+ }
488
+ for (const { attribute, unreported } of controlled ?? []) {
489
+ const wanted = this.resolved.attributes.get(attribute);
490
+ if (wanted !== undefined && !unreported) {
491
+ this.setAttribute(attribute, wanted);
492
+ }
493
+ }
494
+ });
495
+ }
496
+
497
+ /** Sets the attributes that need a parent, once there is one. */
498
+ applyParentAttributes(): void {
499
+ // (a part that failed later in the commit deleted it: its children it held are still live, with no widget)
500
+ if (this.state !== "live" || !r.Rwidget_is_valid(this.id)) {
501
+ return;
502
+ }
503
+ for (const [name, value] of this.resolved.attributes) {
504
+ if (WidgetInstance.belongsToParent(name)) {
505
+ this.setAttribute(name, value);
506
+ }
507
+ }
508
+ }
509
+
510
+ /** Goes from the last props to new ones, setting only what changed. */
511
+ update<Child>(props: RavenProps<Child>): void {
512
+ // (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)
513
+ if (this.state !== "live" || !r.Rwidget_is_valid(this.id) || this.tag === ADOPTED_TAG) {
514
+ return;
515
+ }
516
+ const next = resolveProps(props, DATA_PROPS[this.tag]?.names);
517
+ DATA_PROPS[this.tag]?.check(next.data);
518
+ const previous = this.resolved;
519
+ // Raven has no way to give an attribute its default back, and no way to read it:
520
+ // a widget that loses an attribute is made again, from the attributes that are left.
521
+ // (so is one whose markup changed, which only counts when it is made)
522
+ if ([...previous.attributes.keys()].some((name) => !next.attributes.has(name)) || previous.attributes.get(MARKUP) !== next.attributes.get(MARKUP)) {
523
+ this.remake(next);
524
+ return;
525
+ }
526
+ for (const [name, value] of next.attributes) {
527
+ if (previous.attributes.get(name) !== value) {
528
+ this.setAttribute(name, value);
529
+ }
530
+ }
531
+ this.applyData(previous.data, next);
532
+ applyHandlers(this.tag, this.id, previous.handlers, next.handlers);
533
+ this.applyVirtualItems(previous, next);
534
+ if (next.handlers.get("OnPaint") !== previous.handlers.get("OnPaint")) {
535
+ r.Rwidget_invalidate(this.id);
536
+ }
537
+ this.resolved = next;
538
+ if (previous.attributes.get("Items") !== next.attributes.get("Items")) {
539
+ this.restoreSelected();
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Replaces the widget by a new one in the same place, with the same children: those the framework
545
+ * does not know of too, added by the application. What a new widget holds by itself stays with it.
546
+ * If the new one cannot take the props, the old one stays as it was, and the framework hears of it.
547
+ */
548
+ private remake(next: ResolvedProps): void {
549
+ const old = this.id;
550
+ const previous = this.resolved;
551
+ const parent = this.parent;
552
+ const placed = parent?.holds(this) === true;
553
+ const index = placed ? parent.nativeIndexOf(this, true) : -1;
554
+ this.id = WidgetInstance.make(this.tag, next.attributes.get(MARKUP));
555
+ const madeChildren = r.Rwidget_get_child_count(this.id);
556
+ try {
557
+ this.applyAll(next);
558
+ } catch (error) {
559
+ forgetHandlers(this.tag, this.id, true);
560
+ r.Rwidget_destroy(this.id);
561
+ this.id = old;
562
+ this.resolved = previous;
563
+ throw error;
564
+ }
565
+ forgetHandlers(this.tag, old, true);
566
+ // What Raven made in the old widget: as many as in the new one, but for a markup that changed.
567
+ const own = r.Rwidget_get_child_count(this.id) + this.madeChildren - madeChildren;
568
+ this.madeChildren = madeChildren;
569
+ const moving = Array.from({ length: Math.max(r.Rwidget_get_child_count(old) - own, 0) }, (_, position) => r.Rwidget_get_child(old, own + position));
570
+ for (const child of moving) {
571
+ r.Rwidget_remove_child(old, child);
572
+ r.Rwidget_add_child(this.id, child);
573
+ }
574
+ if (placed) {
575
+ r.Rwidget_remove_child(parent.id, old);
576
+ r.Rwidget_insert_child(parent.id, this.id, index);
577
+ }
578
+ this.holder?.(this.id);
579
+ if (r.Rwidget_is_valid(old)) {
580
+ r.Rwidget_destroy(old);
581
+ }
582
+ this.applyParentAttributes();
583
+ }
584
+
585
+ /**
586
+ * Where a child goes among the widgets of this one. Those the framework knows are enough, unless
587
+ * the application added others: then it is just before the next child the framework knows.
588
+ * `attached` says whether Raven already has the child, so that it counts among what Raven holds.
589
+ */
590
+ private nativeIndexOf(child: HostNode, attached: boolean): number {
591
+ const held = this.children.filter((other) => this.holds(other));
592
+ const index = held.indexOf(child);
593
+ const count = r.Rwidget_get_child_count(this.id);
594
+ if (count <= (attached ? held.length : held.length - 1)) {
595
+ return index;
596
+ }
597
+ const positionOf = (widget: r.Rid | null): number => {
598
+ for (let position = 0; position < count; position++) {
599
+ if (r.Rwidget_get_child(this.id, position) === widget) {
600
+ return position;
601
+ }
602
+ }
603
+ return -1;
604
+ };
605
+ const here = positionOf(child.nativeWidget);
606
+ if (here >= 0) {
607
+ return here;
608
+ }
609
+ const next = positionOf(held[index + 1]?.nativeWidget ?? null);
610
+ return next >= 0 ? next : count;
611
+ }
612
+
613
+ /** Makes sure a widget has a widget to go in: the top of a root of windows has none. */
614
+ private checkHolds(child: HostNode): void {
615
+ if (this.holds(child) && this.id === r.R_INVALID_ID) {
616
+ RavenError.throw(`raven-tsx: <${child.tag}> must be inside a <Window>`);
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Adds a child while this new widget is rendered: to the tree here only. Raven gets it with
622
+ * this widget, at `materialize`, if they are placed. (An adopted child stays where it is
623
+ * shown until then)
624
+ */
625
+ appendInitial(child: HostNode): void {
626
+ if (this.state === "live") {
627
+ this.append(child);
628
+ return;
629
+ }
630
+ this.children.push(child);
631
+ child.setParent(this);
632
+ }
633
+
634
+ /** Adds a child at the end, making it in Raven if it was only pending. A child already here moves to the end. */
635
+ append(child: HostNode): void {
636
+ this.checkHolds(child);
637
+ child.materialize();
638
+ this.detach(child);
639
+ this.children.push(child);
640
+ child.setParent(this);
641
+ if (this.holds(child)) {
642
+ r.Rwidget_add_child(this.id, child.nativeWidget as r.Rid);
643
+ }
644
+ }
645
+
646
+ /** Puts a child before another one. A child already here moves. */
647
+ insertBefore(child: HostNode, before: HostNode): void {
648
+ this.checkHolds(child);
649
+ child.materialize();
650
+ this.detach(child);
651
+ const index = this.children.indexOf(before);
652
+ if (index < 0) {
653
+ RavenError.throw(`raven-tsx: <${before.tag}> is not a child of <${this.tag}>`);
654
+ }
655
+ this.children.splice(index, 0, child);
656
+ child.setParent(this);
657
+ if (this.holds(child)) {
658
+ r.Rwidget_insert_child(this.id, child.nativeWidget as r.Rid, this.nativeIndexOf(child, false));
659
+ }
660
+ }
661
+
662
+ /** Takes a child out without deleting it. */
663
+ private detach(child: HostNode): void {
664
+ const index = this.children.indexOf(child);
665
+ if (index >= 0) {
666
+ this.children.splice(index, 1);
667
+ if (this.holds(child)) {
668
+ r.Rwidget_remove_child(this.id, child.nativeWidget as r.Rid);
669
+ }
670
+ }
671
+ }
672
+
673
+ /** Removes a child and deletes it with everything under it. */
674
+ remove(child: HostNode): void {
675
+ child.forgetTree();
676
+ // (the window may be gone already, closed by the user, with its widgets. An adopted widget
677
+ // may be in another place already, where the component moved it)
678
+ const widget = child.nativeWidget;
679
+ if (this.holds(child) && widget !== null && r.Rwidget_is_valid(widget) && r.Rwidget_get_parent(widget) === this.id) {
680
+ r.Rwidget_remove_child(this.id, widget);
681
+ }
682
+ const index = this.children.indexOf(child);
683
+ if (index >= 0) {
684
+ this.children.splice(index, 1);
685
+ }
686
+ child.setParent(null);
687
+ child.dispose();
688
+ }
689
+
690
+ forgetTree(): void {
691
+ // (a node Raven has not made has no handlers: it gets them at `materialize`)
692
+ if (this.state !== "live") {
693
+ return;
694
+ }
695
+ for (const child of this.children) {
696
+ child.forgetTree();
697
+ }
698
+ forgetHandlers(this.tag, this.id, false);
699
+ }
700
+
701
+ /** Deletes what is in this tree without being a widget of it: deleting a widget does not reach those. */
702
+ disposeDetached(): void {
703
+ for (const child of this.children) {
704
+ if (!this.holds(child)) {
705
+ child.dispose();
706
+ } else if (child instanceof WidgetInstance) {
707
+ child.disposeDetached();
708
+ }
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Takes the adopted widgets of this tree out of the widgets that hold them, before those are
714
+ * destroyed: they belong to the element of their registered tag, which may show them again.
715
+ * Those the component already moved elsewhere are left where they are.
716
+ */
717
+ releaseAdopted(): void {
718
+ for (const child of this.children) {
719
+ if (child.tag === ADOPTED_TAG) {
720
+ const widget = child.nativeWidget;
721
+ if (widget !== null && r.Rwidget_is_valid(widget) && r.Rwidget_get_parent(widget) === this.id) {
722
+ r.Rwidget_remove_child(this.id, widget);
723
+ }
724
+ } else if (child instanceof WidgetInstance) {
725
+ child.releaseAdopted();
726
+ }
727
+ }
728
+ }
729
+
730
+ dispose(): void {
731
+ if (this.tag === ADOPTED_TAG) {
732
+ // (its parent let it go, unless it was already placed elsewhere: it belongs to the element of its registered tag)
733
+ return;
734
+ }
735
+ const state = this.state;
736
+ this.state = "gone";
737
+ if (state !== "live") {
738
+ return;
739
+ }
740
+ if (r.Rwidget_is_valid(this.id)) {
741
+ this.releaseAdopted();
742
+ }
743
+ this.disposeDetached();
744
+ if (r.Rwidget_is_valid(this.id)) {
745
+ r.Rwidget_destroy(this.id);
746
+ }
747
+ }
748
+ }