@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,869 @@
1
+ import { CString, JSCallback, ptr, toArrayBuffer, type FFITypeOrString, type Pointer } from "bun:ffi";
2
+ import * as r from "./raven";
3
+ import type { EventHandler } from "./props";
4
+
5
+ /** The second argument of a native callback, before it is decoded. */
6
+ type RawValue = number | Pointer | null;
7
+
8
+ /** Connects a native callback to a widget. Each kind of widget has its own function in the C API. */
9
+ type Connect = (widget: r.Rid, callback: Pointer | null) => void;
10
+
11
+ /** What `OnMeasure` of a `Custom` gives back. */
12
+ export interface MeasuredSize {
13
+ readonly Width: number;
14
+ readonly Height: number;
15
+ }
16
+
17
+ /** The painter `OnPaint` of a `Custom` receives, for the `Rdraw_` functions. */
18
+ export type Painter = Pointer | null;
19
+
20
+ /** What `OnPaint` of a `Custom` receives: the painter, and the size. */
21
+ type PaintHandler = (painter: Painter, width: number, height: number) => void;
22
+ type MeasureHandler = () => MeasuredSize;
23
+ type ListTextHandler = (index: number) => string;
24
+ type TileFillHandler = (index: number, item: r.RTileItem) => void;
25
+ type CellTextHandler = (row: number, column: number) => string;
26
+
27
+ /** What a `Plot` shows, by axis: the lowest and the highest value of each. */
28
+ export interface PlotView {
29
+ readonly X: readonly [number, number];
30
+ readonly Y: readonly [number, number];
31
+ readonly X2: readonly [number, number];
32
+ readonly Y2: readonly [number, number];
33
+ }
34
+
35
+ /** What a function of two values from C receives: an index, and a flag. */
36
+ type IndexAndFlagHandler = (index: number, flag: boolean) => void;
37
+
38
+ /**
39
+ * Runs what a native event starts: the handler, then the commit of what it changed.
40
+ * The commit happens before the next event, as in a browser: a discrete event must not
41
+ * leave state behind for the next click to read.
42
+ */
43
+ class EventScope {
44
+ private flush: () => void = () => {};
45
+ private wrap: (run: () => void) => void = (run) => run();
46
+ private ignoring = 0;
47
+
48
+ /** Gives the function that commits pending updates: an adapter says how, for its framework. */
49
+ setFlush(flush: () => void): void {
50
+ this.flush = flush;
51
+ }
52
+
53
+ /** Gives what surrounds the run of a native event, for a framework that wants to know of one. */
54
+ setWrapper(wrap: (run: () => void) => void): void {
55
+ this.wrap = wrap;
56
+ }
57
+
58
+ /** Runs a change made by the renderer: an event it causes is not from the user. */
59
+ ignoringEvents(change: () => void): void {
60
+ this.ignoring++;
61
+ try {
62
+ change();
63
+ } finally {
64
+ this.ignoring--;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Calls application code from a native callback. The one catch of this package: an
70
+ * exception must not cross the frames of C, which cannot unwind.
71
+ */
72
+ guard(run: () => void): void {
73
+ try {
74
+ run();
75
+ } catch (error) {
76
+ console.error("raven-tsx: exception in an event handler:", error);
77
+ }
78
+ }
79
+
80
+ /** Runs a handler of the user, and commits what it changed. */
81
+ run(handler: () => void, afterwards?: () => void): void {
82
+ if (this.ignoring > 0) {
83
+ return;
84
+ }
85
+ this.wrap(() => {
86
+ this.guard(() => {
87
+ handler();
88
+ this.flush();
89
+ afterwards?.();
90
+ });
91
+ });
92
+ }
93
+ }
94
+
95
+ export const eventScope = new EventScope();
96
+
97
+ /**
98
+ * Makes a function for a callback of the C API behave as a prop would: an exception stays on this
99
+ * side, and what it changes in the interface is committed before the next event, not some time later.
100
+ * What it returns goes back to C, or `otherwise` if it threw.
101
+ *
102
+ * raven.Rwidget_set_gesture_callback(widget, inEventScope((_widget, event) => { setZoom(event.scale); return 1; }, 0));
103
+ */
104
+ export function inEventScope<A extends readonly (number | string | object | null)[], R>(callback: (...values: A) => R, otherwise?: R): (...values: A) => R {
105
+ return (...values) => {
106
+ let result = otherwise as R;
107
+ eventScope.run(() => {
108
+ result = callback(...values);
109
+ });
110
+ return result;
111
+ };
112
+ }
113
+
114
+ /**
115
+ * A widget that holds its own value, which the user changes before the framework knows: what the
116
+ * props say comes back when a handler refused the change, as a controlled input in a browser.
117
+ */
118
+ class ControlledSync {
119
+ private readonly restore = new Map<r.Rid, () => void>();
120
+
121
+ /** Gives what to do after an event of this widget. */
122
+ set(widget: r.Rid, restore: () => void): void {
123
+ this.restore.set(widget, restore);
124
+ }
125
+
126
+ /** Forgets a widget. */
127
+ forget(widget: r.Rid): void {
128
+ this.restore.delete(widget);
129
+ }
130
+
131
+ /** Runs after the handler of a widget and the commit it caused. */
132
+ run(widget: r.Rid): void {
133
+ this.restore.get(widget)?.();
134
+ }
135
+ }
136
+
137
+ export const controlledSync = new ControlledSync();
138
+
139
+ /**
140
+ * One event of one kind of widget, such as the change of a TextInput. It owns one native
141
+ * callback, shared by all the widgets: a JSCallback per handler would stay alive for
142
+ * ever, and an interface that changes would make many.
143
+ */
144
+ class NativeEvent {
145
+ private readonly handlers = new Map<r.Rid, EventHandler>();
146
+ private callback: JSCallback | null = null;
147
+
148
+ constructor(
149
+ private readonly connect: Connect,
150
+ private readonly make: (handlerOf: (widget: r.Rid) => EventHandler | undefined) => JSCallback,
151
+ ) {}
152
+
153
+ /** Sets the function of a widget. */
154
+ on(widget: r.Rid, handler: EventHandler): void {
155
+ const known = this.handlers.has(widget);
156
+ this.handlers.set(widget, handler);
157
+ if (!known) {
158
+ this.callback ??= this.make((id) => this.handlers.get(id));
159
+ this.connect(widget, this.callback.ptr);
160
+ }
161
+ }
162
+
163
+ /** Removes the function of a widget. */
164
+ off(widget: r.Rid, alive: boolean): void {
165
+ if (this.handlers.delete(widget) && alive) {
166
+ this.connect(widget, null);
167
+ }
168
+ }
169
+
170
+ /** The C function that connects it, for the tests. */
171
+ get connectName(): string {
172
+ return this.connect.name;
173
+ }
174
+
175
+ /** How many widgets have a function, for the tests. */
176
+ get size(): number {
177
+ return this.handlers.size;
178
+ }
179
+
180
+ /** Whether a native callback was made, for the tests. */
181
+ get hasCallback(): boolean {
182
+ return this.callback !== null;
183
+ }
184
+ }
185
+
186
+ /** An event that gives one value to its function. */
187
+ function valueEvent<V>(connect: Connect, rawType: FFITypeOrString, decode: (raw: RawValue) => V): NativeEvent {
188
+ return new NativeEvent(connect, (handlerOf) => new JSCallback(
189
+ (widget: number, raw: RawValue): void => {
190
+ const handler = handlerOf(widget) as ((value: V) => void) | undefined;
191
+ if (handler) {
192
+ eventScope.run(() => handler(decode(raw)), () => controlledSync.run(widget));
193
+ }
194
+ },
195
+ { args: ["u32", rawType, "ptr"], returns: "void" },
196
+ ));
197
+ }
198
+
199
+ const textOf = (raw: RawValue): string => (raw ? new CString(raw as Pointer).toString() : "");
200
+ const textEvent = (connect: Connect): NativeEvent => valueEvent<string>(connect, "ptr", textOf);
201
+ const flagEvent = (connect: Connect): NativeEvent => valueEvent<boolean>(connect, "i32", (raw) => raw !== 0);
202
+ const indexEvent = (connect: Connect): NativeEvent => valueEvent<number>(connect, "i32", Number);
203
+ const idEvent = (connect: Connect): NativeEvent => valueEvent<r.Rid>(connect, "u32", Number);
204
+ const floatEvent = (connect: Connect): NativeEvent => valueEvent<number>(connect, "f32", Number);
205
+ const doubleEvent = (connect: Connect): NativeEvent => valueEvent<number>(connect, "f64", Number);
206
+
207
+ /** An event that gives what C gives as it is. `args` are the types after the widget. */
208
+ function directEvent(connect: Connect, args: readonly FFITypeOrString[]): NativeEvent {
209
+ return new NativeEvent(connect, (handlerOf) => new JSCallback(
210
+ (widget: number, ...values: number[]): void => {
211
+ const handler = handlerOf(widget) as ((...values: number[]) => void) | undefined;
212
+ if (handler) {
213
+ eventScope.run(() => handler(...values.slice(0, args.length)), () => controlledSync.run(widget));
214
+ }
215
+ },
216
+ { args: ["u32", ...args, "ptr"], returns: "void" },
217
+ ));
218
+ }
219
+
220
+ /** An event whose value is read from the widget, as C gives none of it, or not as its attribute is written. */
221
+ function readEvent<V>(connect: Connect, args: readonly FFITypeOrString[], read: (widget: r.Rid) => V): NativeEvent {
222
+ return new NativeEvent(connect, (handlerOf) => new JSCallback(
223
+ (widget: number): void => {
224
+ const handler = handlerOf(widget) as ((value: V) => void) | undefined;
225
+ if (handler) {
226
+ eventScope.run(() => handler(read(widget)), () => controlledSync.run(widget));
227
+ }
228
+ },
229
+ { args: ["u32", ...args, "ptr"], returns: "void" },
230
+ ));
231
+ }
232
+
233
+ /**
234
+ * An event that asks: the function says yes by returning true, and anything else is no, an
235
+ * exception included. A change made by the renderer is not asked about.
236
+ */
237
+ function requestEvent(connect: Connect, args: readonly FFITypeOrString[]): NativeEvent {
238
+ return new NativeEvent(connect, (handlerOf) => new JSCallback(
239
+ (widget: number, ...values: number[]): number => {
240
+ const handler = handlerOf(widget) as ((...values: number[]) => boolean | void) | undefined;
241
+ let answer = 1;
242
+ if (handler) {
243
+ eventScope.run(() => {
244
+ answer = 0;
245
+ answer = handler(...values.slice(0, args.length)) === true ? 1 : 0;
246
+ });
247
+ }
248
+ return answer;
249
+ },
250
+ { args: ["u32", ...args, "ptr"], returns: "i32" },
251
+ ));
252
+ }
253
+
254
+ /** A right click: what is under the pointer, and where. */
255
+ const contextEvent = (connect: Connect, item: FFITypeOrString): NativeEvent => directEvent(connect, [item, "f32", "f32"]);
256
+
257
+ /**
258
+ * An event that gives a structure, valid during the call, and consumes what it receives when
259
+ * the function returns true.
260
+ */
261
+ function structEvent<S>(connect: Connect, read: (raw: Pointer | null) => S | null): NativeEvent {
262
+ return new NativeEvent(connect, (handlerOf) => new JSCallback(
263
+ (widget: number, raw: Pointer | null): number => {
264
+ const handler = handlerOf(widget) as ((event: S) => boolean | void) | undefined;
265
+ const event = read(raw);
266
+ let consumed = 0;
267
+ if (handler && event) {
268
+ eventScope.run(() => {
269
+ consumed = handler(event) === true ? 1 : 0;
270
+ });
271
+ }
272
+ return consumed;
273
+ },
274
+ { args: ["u32", "ptr", "ptr"], returns: "i32" },
275
+ ));
276
+ }
277
+
278
+ /** The events of a window, which are not widgets. `OnClose` is apart: without it, Raven closes. */
279
+ export const WINDOW_EVENTS: Readonly<Record<string, NativeEvent>> = {
280
+ OnResize: directEvent(r.Rwindow_set_resize_callback, ["i32", "i32"]),
281
+ OnDrop: structEvent(r.Rwindow_set_drop_callback, (raw) => r.RDropEvent.from(raw)),
282
+ };
283
+
284
+ /**
285
+ * The events every widget has. `OnEvent` is its input before the widget sees it, for what has no
286
+ * prop of its own, as the pointer over a `Custom`. `OnDrop` is what is dropped on it.
287
+ */
288
+ const COMMON_EVENTS: Readonly<Record<string, NativeEvent>> = {
289
+ OnEvent: structEvent(r.Rwidget_set_event_callback, (raw) => r.REvent.from(raw)),
290
+ OnDrop: structEvent(r.Rwidget_set_drop_callback, (raw) => r.RDropEvent.from(raw)),
291
+ };
292
+
293
+ /** Two digits, for a date or a time. */
294
+ const two = (value: number): string => String(value).padStart(2, "0");
295
+
296
+ /** An event that gives three numbers from C as one text: a date or a time, as its attribute is written. */
297
+ function threeNumbersEvent(connect: Connect, write: (a: number, b: number, c: number) => string): NativeEvent {
298
+ return new NativeEvent(connect, (handlerOf) => new JSCallback(
299
+ (widget: number, a: number, b: number, c: number): void => {
300
+ const handler = handlerOf(widget) as ((value: string) => void) | undefined;
301
+ if (handler) {
302
+ eventScope.run(() => handler(write(a, b, c)), () => controlledSync.run(widget));
303
+ }
304
+ },
305
+ { args: ["u32", "i32", "i32", "i32", "ptr"], returns: "void" },
306
+ ));
307
+ }
308
+
309
+ /** An event that gives an index and a flag: the column of a Table and its direction, a series of a Plot and whether it shows. */
310
+ const indexAndFlagEvent = (connect: Connect): NativeEvent => new NativeEvent(connect, (handlerOf) => new JSCallback(
311
+ (widget: number, index: number, flag: number): void => {
312
+ const handler = handlerOf(widget) as IndexAndFlagHandler | undefined;
313
+ if (handler) {
314
+ eventScope.run(() => handler(index, flag !== 0), () => controlledSync.run(widget));
315
+ }
316
+ },
317
+ { args: ["u32", "i32", "i32", "ptr"], returns: "void" },
318
+ ));
319
+
320
+ /** `OnClicked` of a Toast: the button of its message. */
321
+ const toastActionEvent = new NativeEvent(r.Rtoast_set_action_callback, (handlerOf) => new JSCallback(
322
+ (widget: number): void => {
323
+ const handler = handlerOf(widget) as (() => void) | undefined;
324
+ if (handler) {
325
+ eventScope.run(handler);
326
+ }
327
+ },
328
+ { args: ["u32", "ptr"], returns: "void" },
329
+ ));
330
+
331
+ /** `OnPaint`: runs while a frame is drawn, so nothing is committed after it. */
332
+ const paintEvent = new NativeEvent(r.Rcustom_set_paint_callback, (handlerOf) => new JSCallback(
333
+ (widget: number, painter: Pointer | null, width: number, height: number): void => {
334
+ const handler = handlerOf(widget) as PaintHandler | undefined;
335
+ if (handler) {
336
+ eventScope.guard(() => handler(painter, width, height));
337
+ }
338
+ },
339
+ { args: ["u32", "ptr", "f32", "f32", "ptr"], returns: "void" },
340
+ ));
341
+
342
+ /** `OnMeasure`: says the size a `Custom` wants, through the two floats C gives. */
343
+ const measureEvent = new NativeEvent(r.Rcustom_set_measure_callback, (handlerOf) => new JSCallback(
344
+ (widget: number, width: Pointer | null, height: Pointer | null): void => {
345
+ const handler = handlerOf(widget) as MeasureHandler | undefined;
346
+ if (handler && width && height) {
347
+ eventScope.guard(() => {
348
+ const size = handler();
349
+ new Float32Array(toArrayBuffer(width, 0, 4))[0] = size.Width;
350
+ new Float32Array(toArrayBuffer(height, 0, 4))[0] = size.Height;
351
+ });
352
+ }
353
+ },
354
+ { args: ["u32", "ptr", "ptr", "ptr"], returns: "void" },
355
+ ));
356
+
357
+ /**
358
+ * The events with a value, by tag. `OnClicked`, `OnChanged`, `OnCommitted` and `OnActivated`
359
+ * as XML attributes all set the one action of a widget, without a value, and for a TextInput
360
+ * it only fires on Enter. These callbacks fire at each change, with the value.
361
+ */
362
+ const WIDGET_EVENTS: Readonly<Record<string, Readonly<Record<string, NativeEvent>>>> = {
363
+ TextInput: {
364
+ OnChanged: textEvent(r.Rtext_input_set_changed_callback),
365
+ OnCommitted: textEvent(r.Rtext_input_set_committed_callback),
366
+ },
367
+ Checkbox: { OnChanged: flagEvent(r.Rcheckbox_set_changed_callback) },
368
+ Slider: { OnChanged: floatEvent(r.Rslider_set_changed_callback) },
369
+ SpinBox: { OnChanged: doubleEvent(r.Rspin_box_set_changed_callback) },
370
+ ComboBox: { OnChanged: indexEvent(r.Rcombo_box_set_changed_callback) },
371
+ TabView: {
372
+ OnChanged: indexEvent(r.Rtab_view_set_changed_callback),
373
+ OnClose: requestEvent(r.Rtab_view_set_close_callback, ["i32", "u32"]),
374
+ },
375
+ Expander: { OnChanged: flagEvent(r.Rexpander_set_changed_callback) },
376
+ ListView: {
377
+ OnChanged: indexEvent(r.Rlist_view_set_selection_changed_callback),
378
+ OnActivated: indexEvent(r.Rlist_view_set_activated_callback),
379
+ },
380
+ TreeView: {
381
+ OnChanged: idEvent(r.Rtree_view_set_selection_changed_callback),
382
+ OnActivated: idEvent(r.Rtree_view_set_activated_callback),
383
+ OnContext: contextEvent(r.Rtree_view_set_context_callback, "u32"),
384
+ },
385
+ TileView: {
386
+ OnChanged: indexEvent(r.Rtile_view_set_selection_changed_callback),
387
+ OnActivated: indexEvent(r.Rtile_view_set_activated_callback),
388
+ OnContext: contextEvent(r.Rtile_view_set_context_callback, "i32"),
389
+ },
390
+ Custom: { OnPaint: paintEvent, OnMeasure: measureEvent },
391
+ Table: {
392
+ OnChanged: indexEvent(r.Rtable_set_selection_changed_callback),
393
+ OnActivated: indexEvent(r.Rtable_set_activated_callback),
394
+ OnSort: indexAndFlagEvent(r.Rtable_set_sort_callback),
395
+ },
396
+ DatePicker: { OnChanged: threeNumbersEvent(r.Rdate_picker_set_changed_callback, (year, month, day) => `${year}-${two(month)}-${two(day)}`) },
397
+ TimePicker: { OnChanged: threeNumbersEvent(r.Rtime_picker_set_changed_callback, (hour, minute, second) => `${two(hour)}:${two(minute)}:${two(second)}`) },
398
+ Rating: { OnChanged: floatEvent(r.Rrating_set_changed_callback) },
399
+ Pagination: { OnChanged: indexEvent(r.Rpagination_set_changed_callback) },
400
+ Plot: {
401
+ OnChanged: directEvent(r.Rplot_set_drag_line_callback, ["i32", "f64"]),
402
+ OnViewChanged: readEvent(r.Rplot_set_view_changed_callback, [], (plot): PlotView => ({
403
+ X: r.Rplot_get_axis_range(plot, r.R_PLOT_X),
404
+ Y: r.Rplot_get_axis_range(plot, r.R_PLOT_Y),
405
+ X2: r.Rplot_get_axis_range(plot, r.R_PLOT_X2),
406
+ Y2: r.Rplot_get_axis_range(plot, r.R_PLOT_Y2),
407
+ })),
408
+ OnSeriesToggled: indexAndFlagEvent(r.Rplot_set_series_toggled_callback),
409
+ },
410
+ Breadcrumb: { OnClicked: indexEvent(r.Rbreadcrumb_set_clicked_callback) },
411
+ Toast: { OnClicked: toastActionEvent },
412
+ TextEditor: {
413
+ OnChanged: readEvent(r.Rtext_editor_set_changed_callback, [], (widget) => r.Rtext_editor_get_text(widget) ?? ""),
414
+ OnZoom: floatEvent(r.Rtext_editor_set_zoom_callback),
415
+ OnCompletion: textEvent(r.Rtext_editor_set_completion_callback),
416
+ },
417
+ ColorPicker: { OnChanged: readEvent(r.Rcolor_picker_set_changed_callback, ["f32", "f32", "f32", "f32"], (widget) => r.Rcolor_picker_get_hex(widget) ?? "") },
418
+ Label: { OnLink: textEvent(r.Rlabel_set_link_callback) },
419
+ MenuItem: { OnClicked: readEvent(r.Rmenu_item_set_clicked_callback, [], (widget) => r.Rmenu_item_is_checked(widget) !== 0) },
420
+ Island: { OnChanged: flagEvent(r.Risland_set_changed_callback) },
421
+ Viewport: { OnResize: directEvent(r.Rviewport_set_resize_callback, ["i32", "i32"]) },
422
+ FileBrowser: {
423
+ OnChanged: textEvent(r.Rfile_browser_set_selection_changed_callback),
424
+ OnAccepted: textEvent(r.Rfile_browser_set_accepted_callback),
425
+ OnCancelled: textEvent(r.Rfile_browser_set_cancelled_callback),
426
+ },
427
+ DockArea: {
428
+ OnChanged: readEvent(r.Rdock_area_set_changed_callback, [], (widget) => r.Rdock_area_save_layout(widget) ?? ""),
429
+ OnClose: requestEvent(r.Rdock_area_set_close_callback, ["u32"]),
430
+ },
431
+ Timeline: {
432
+ OnTime: doubleEvent(r.Rtimeline_set_time_callback),
433
+ OnItemChanged: indexEvent(r.Rtimeline_set_changed_callback),
434
+ OnSelection: indexEvent(r.Rtimeline_set_selection_callback),
435
+ OnActivated: indexEvent(r.Rtimeline_set_activated_callback),
436
+ OnRemoved: indexEvent(r.Rtimeline_set_removed_callback),
437
+ },
438
+ NodeGraph: {
439
+ OnLinked: indexEvent(r.Rnode_graph_set_linked_callback),
440
+ OnUnlinked: indexEvent(r.Rnode_graph_set_unlinked_callback),
441
+ OnMoved: indexEvent(r.Rnode_graph_set_moved_callback),
442
+ OnSelection: indexEvent(r.Rnode_graph_set_selection_callback),
443
+ OnRemoved: indexEvent(r.Rnode_graph_set_removed_callback),
444
+ OnActivated: indexEvent(r.Rnode_graph_set_activated_callback),
445
+ OnContext: contextEvent(r.Rnode_graph_set_context_callback, "i32"),
446
+ OnConnect: requestEvent(r.Rnode_graph_set_connect_callback, ["i32", "i32"]),
447
+ },
448
+ BlockEditor: {
449
+ OnChanged: indexEvent(r.Rblock_editor_set_changed_callback),
450
+ OnClicked: indexEvent(r.Rblock_editor_set_clicked_callback),
451
+ OnContext: contextEvent(r.Rblock_editor_set_context_callback, "i32"),
452
+ },
453
+ };
454
+
455
+ /** The items of a list asked for as they are shown: `ItemCount`, and a function for one item. */
456
+ class VirtualItems {
457
+ private readonly handlers = new Map<r.Rid, EventHandler>();
458
+ private callback: JSCallback | null = null;
459
+
460
+ constructor(
461
+ private readonly connect: (widget: r.Rid, count: number, callback: Pointer | null) => void,
462
+ private readonly make: (handlerOf: (widget: r.Rid) => EventHandler | undefined) => JSCallback,
463
+ ) {}
464
+
465
+ /** Sets the items of a widget. */
466
+ set(widget: r.Rid, count: number, handler: EventHandler): void {
467
+ this.handlers.set(widget, handler);
468
+ this.callback ??= this.make((id) => this.handlers.get(id));
469
+ this.connect(widget, count, this.callback.ptr);
470
+ }
471
+
472
+ /** Takes the function of a widget away. */
473
+ clear(widget: r.Rid, alive: boolean): void {
474
+ if (this.handlers.delete(widget) && alive) {
475
+ this.connect(widget, 0, null);
476
+ }
477
+ }
478
+
479
+ /** The C function that connects it, for the tests. */
480
+ get connectName(): string {
481
+ return this.connect.name;
482
+ }
483
+
484
+ /** How many widgets have a function, for the tests. */
485
+ get size(): number {
486
+ return this.handlers.size;
487
+ }
488
+ }
489
+
490
+ /** The text C reads when the callback returns: it lives until the next call. */
491
+ let listText: Buffer = Buffer.from([0]);
492
+ /** The structure C reads when the callback returns, with the texts it points to. */
493
+ let tileItem: r.RTileItem | null = null;
494
+
495
+ /** What makes a list virtual: the prop with its number of items, and the prop with its function. */
496
+ export interface VirtualItemsProps {
497
+ readonly count: string;
498
+ readonly prop: string;
499
+ }
500
+
501
+ const VIRTUAL_ITEMS: Readonly<Record<string, VirtualItemsProps & { readonly items: VirtualItems }>> = {
502
+ ListView: {
503
+ count: "ItemCount",
504
+ prop: "ItemText",
505
+ items: new VirtualItems(r.Rlist_view_set_virtual_items, (handlerOf) => new JSCallback(
506
+ (widget: number, index: number): Pointer | null => {
507
+ const handler = handlerOf(widget) as ListTextHandler | undefined;
508
+ eventScope.guard(() => {
509
+ listText = Buffer.from(`${handler?.(index) ?? ""}\0`);
510
+ });
511
+ return ptr(listText);
512
+ },
513
+ { args: ["u32", "i32", "ptr"], returns: "ptr" },
514
+ )),
515
+ },
516
+ TileView: {
517
+ count: "ItemCount",
518
+ prop: "ItemFill",
519
+ items: new VirtualItems(r.Rtile_view_set_items, (handlerOf) => new JSCallback(
520
+ (widget: number, index: number, item: Pointer | null): void => {
521
+ const handler = handlerOf(widget) as TileFillHandler | undefined;
522
+ tileItem = r.RTileItem.from(item);
523
+ if (handler && tileItem) {
524
+ const filled = tileItem;
525
+ eventScope.guard(() => handler(index, filled));
526
+ }
527
+ },
528
+ { args: ["u32", "i32", "ptr", "ptr"], returns: "void" },
529
+ )),
530
+ },
531
+ Table: {
532
+ count: "RowCount",
533
+ prop: "CellText",
534
+ items: new VirtualItems(r.Rtable_set_virtual_rows, (handlerOf) => new JSCallback(
535
+ (widget: number, row: number, column: number): Pointer | null => {
536
+ const handler = handlerOf(widget) as CellTextHandler | undefined;
537
+ eventScope.guard(() => {
538
+ listText = Buffer.from(`${handler?.(row, column) ?? ""}\0`);
539
+ });
540
+ return ptr(listText);
541
+ },
542
+ { args: ["u32", "i32", "i32", "ptr"], returns: "ptr" },
543
+ )),
544
+ },
545
+ };
546
+
547
+ /** The props that give the items of a tag, if it has them. */
548
+ export function virtualItemsOf(tag: string): VirtualItemsProps | undefined {
549
+ return VIRTUAL_ITEMS[tag];
550
+ }
551
+
552
+ /** Gives a list its items. */
553
+ export function setVirtualItems(tag: string, widget: r.Rid, count: number, handler: EventHandler): void {
554
+ VIRTUAL_ITEMS[tag]?.items.set(widget, count, handler);
555
+ }
556
+
557
+ /** What an `On...` prop receives, as TypeScript, for the generator. Other events receive nothing. */
558
+ export const EVENT_VALUE_TYPES: Readonly<Record<string, Readonly<Record<string, string>>>> = {
559
+ TextInput: { OnChanged: "string", OnCommitted: "string" },
560
+ Checkbox: { OnChanged: "boolean" },
561
+ Slider: { OnChanged: "number" },
562
+ SpinBox: { OnChanged: "number" },
563
+ ComboBox: { OnChanged: "number" },
564
+ TabView: { OnChanged: "number" },
565
+ Expander: { OnChanged: "boolean" },
566
+ ListView: { OnChanged: "number", OnActivated: "number" },
567
+ TreeView: { OnChanged: "number", OnActivated: "number" },
568
+ TileView: { OnChanged: "number", OnActivated: "number" },
569
+ Table: { OnActivated: "number" },
570
+ DatePicker: { OnChanged: "string" },
571
+ TimePicker: { OnChanged: "string" },
572
+ Rating: { OnChanged: "number" },
573
+ Pagination: { OnChanged: "number" },
574
+ Plot: { OnChanged: "line: number, value: number" },
575
+ Breadcrumb: { OnClicked: "number" },
576
+ TextEditor: { OnChanged: "string" },
577
+ ColorPicker: { OnChanged: "string" },
578
+ MenuItem: { OnClicked: "boolean" },
579
+ BlockEditor: { OnChanged: "number" },
580
+ };
581
+
582
+ /** What fills a widget from the elements XML takes inside it: `<Track>`, `<Node>`... */
583
+ const MARKUP = "readonly Markup?: string";
584
+
585
+ /**
586
+ * What every axis of a Plot takes: `XMin`, `Y2Format`... The schema lists most of these for X only,
587
+ * and says the others exist in a description. (those it does list keep the type it gives)
588
+ */
589
+ const PLOT_AXIS_PROPS: readonly string[] = ["X", "Y", "X2", "Y2"].flatMap((axis) => [
590
+ `readonly ${axis}Label?: string`,
591
+ `readonly ${axis}Scale?: "Linear" | "Log" | "SymLog"`,
592
+ `readonly ${axis}Min?: number`,
593
+ `readonly ${axis}Max?: number`,
594
+ `readonly ${axis}Range?: readonly [number, number]`,
595
+ `readonly ${axis}AutoFit?: boolean`,
596
+ `readonly ${axis}Format?: string`,
597
+ `readonly ${axis}LinearThreshold?: number`,
598
+ `readonly ${axis}Grid?: boolean`,
599
+ `readonly ${axis}Inverted?: boolean`,
600
+ `readonly ${axis}Locked?: boolean`,
601
+ `readonly ${axis}Visible?: boolean`,
602
+ ]);
603
+
604
+ /** The props this package adds to those in Raven's schema, as TypeScript, for the generator. */
605
+ export const EXTRA_PROPS: Readonly<Record<string, readonly string[]>> = {
606
+ Window: ["readonly Opacity?: number", "readonly ContinuousRedraw?: boolean"],
607
+ Custom: [
608
+ "readonly OnPaint?: (painter: Painter, width: number, height: number) => void",
609
+ "readonly OnMeasure?: () => MeasuredSize",
610
+ ],
611
+ ListView: ["readonly ItemCount?: number", "readonly ItemText?: (index: number) => string"],
612
+ // (the schema of Raven does not list this event, which exists)
613
+ TileView: [
614
+ "readonly ItemCount?: number",
615
+ "readonly ItemFill?: (index: number, item: RTileItem) => void",
616
+ "readonly OnChanged?: (value: number) => void",
617
+ "readonly OnContext?: (index: number, x: number, y: number) => void",
618
+ ],
619
+ Table: [
620
+ "readonly RowCount?: number",
621
+ "readonly CellText?: (row: number, column: number) => string",
622
+ "readonly OnChanged?: (row: number) => void",
623
+ "readonly OnSort?: (column: number, descending: boolean) => void",
624
+ MARKUP,
625
+ ],
626
+ Image: ["readonly Texture?: Rid"],
627
+ Plot: [
628
+ "readonly Series?: readonly PlotSeries[]",
629
+ "readonly DragLines?: readonly PlotDragLine[]",
630
+ "readonly OnViewChanged?: (view: PlotView) => void",
631
+ "readonly OnSeriesToggled?: (series: number, visible: boolean) => void",
632
+ ...PLOT_AXIS_PROPS,
633
+ ],
634
+ TabView: [
635
+ "readonly Documents?: boolean",
636
+ "readonly OnClose?: (index: number, content: Rid) => boolean | void",
637
+ ],
638
+ TreeView: [
639
+ "readonly Items?: readonly TreeItem[]",
640
+ "readonly OnContext?: (item: Rid, x: number, y: number) => void",
641
+ MARKUP,
642
+ ],
643
+ TextEditor: ["readonly OnZoom?: (zoom: number) => void", "readonly OnCompletion?: (prefix: string) => void"],
644
+ Label: ['readonly FontStyle?: "Regular" | "Bold" | "Italic" | "BoldItalic"', "readonly OnLink?: (url: string) => void"],
645
+ Island: ["readonly OnChanged?: (expanded: boolean) => void"],
646
+ Viewport: ["readonly OnResize?: (width: number, height: number) => void"],
647
+ FileBrowser: [
648
+ "readonly OnChanged?: (path: string) => void",
649
+ "readonly OnAccepted?: (path: string) => void",
650
+ "readonly OnCancelled?: () => void",
651
+ ],
652
+ DockArea: [
653
+ "readonly OnChanged?: (layout: string) => void",
654
+ "readonly OnClose?: (panel: Rid) => boolean | void",
655
+ ],
656
+ Timeline: [
657
+ "readonly OnTime?: (seconds: number) => void",
658
+ "readonly OnItemChanged?: (item: number) => void",
659
+ "readonly OnSelection?: (item: number) => void",
660
+ "readonly OnActivated?: (item: number) => void",
661
+ "readonly OnRemoved?: (item: number) => void",
662
+ MARKUP,
663
+ ],
664
+ NodeGraph: [
665
+ "readonly OnLinked?: (link: number) => void",
666
+ "readonly OnUnlinked?: (link: number) => void",
667
+ "readonly OnMoved?: (node: number) => void",
668
+ "readonly OnSelection?: (node: number) => void",
669
+ "readonly OnRemoved?: (node: number) => void",
670
+ "readonly OnActivated?: (node: number) => void",
671
+ "readonly OnContext?: (node: number, x: number, y: number) => void",
672
+ "readonly OnConnect?: (outputPin: number, inputPin: number) => boolean",
673
+ MARKUP,
674
+ ],
675
+ BlockEditor: [
676
+ "readonly OnClicked?: (block: number) => void",
677
+ "readonly OnContext?: (block: number, x: number, y: number) => void",
678
+ MARKUP,
679
+ ],
680
+ };
681
+
682
+ /** The props every widget has from this package, as TypeScript, for the generator. */
683
+ export const COMMON_PROPS: readonly string[] = [
684
+ "readonly OnEvent?: (event: REvent) => boolean | void",
685
+ "readonly OnDrop?: (event: RDropEvent) => boolean | void",
686
+ ];
687
+
688
+ /**
689
+ * Events without a value, through the action of the widget: `OnClicked` of a Button, for
690
+ * one. One action name per widget, and one native callback for them all.
691
+ */
692
+ class ActionEvents {
693
+ private readonly handlers = new Map<string, (sender: r.Rid) => void>();
694
+ private readonly nameOf = new Map<r.Rid, string>();
695
+ private callback: JSCallback | null = null;
696
+ private next = 0;
697
+
698
+ /** The callback of every action, made the first time it is needed. */
699
+ private pointer(): Pointer | null {
700
+ this.callback ??= new JSCallback(
701
+ (sender: number, action: Pointer | null): void => {
702
+ const handler = action ? this.handlers.get(new CString(action).toString()) : undefined;
703
+ if (handler) {
704
+ eventScope.run(() => handler(sender));
705
+ }
706
+ },
707
+ { args: ["u32", "ptr", "ptr"], returns: "void" },
708
+ );
709
+ return this.callback.ptr;
710
+ }
711
+
712
+ /** Sets the function of a widget. */
713
+ on(widget: r.Rid, handler: EventHandler): void {
714
+ let name = this.nameOf.get(widget);
715
+ if (!name) {
716
+ name = `raven-tsx.${++this.next}`;
717
+ this.nameOf.set(widget, name);
718
+ r.Raction_bind(name, this.pointer());
719
+ r.Rwidget_set_action(widget, name);
720
+ }
721
+ // (a prop receives nothing, as its type says: `OnClicked={toggle}` must not get a handle)
722
+ this.handlers.set(name, () => (handler as () => void)());
723
+ }
724
+
725
+ /** Gives a function to an action of a name chosen by the application. It receives the sender. */
726
+ bind(name: string, handler: (sender: r.Rid) => void): void {
727
+ this.handlers.set(name, handler);
728
+ r.Raction_bind(name, this.pointer());
729
+ }
730
+
731
+ /** Takes the function of such an action away. */
732
+ unbind(name: string): void {
733
+ this.handlers.delete(name);
734
+ r.Raction_unbind(name);
735
+ }
736
+
737
+ /** Removes the function of a widget. */
738
+ off(widget: r.Rid, alive: boolean): void {
739
+ const name = this.nameOf.get(widget);
740
+ if (name) {
741
+ this.handlers.delete(name);
742
+ this.nameOf.delete(widget);
743
+ r.Raction_unbind(name);
744
+ // (a prop that went from a function to a name has set that name by now: it stays)
745
+ if (alive && r.Rwidget_get_action(widget) === name) {
746
+ r.Rwidget_set_action(widget, null);
747
+ }
748
+ }
749
+ }
750
+
751
+ /** How many widgets have a function, for the tests. */
752
+ get size(): number {
753
+ return this.handlers.size;
754
+ }
755
+ }
756
+
757
+ export const actionEvents = new ActionEvents();
758
+
759
+ /**
760
+ * `OnClose` of a window. With a function, closing is a request: Raven keeps the window, and
761
+ * the application takes the element out when it agrees. One native callback for all windows.
762
+ */
763
+ class CloseEvents {
764
+ private readonly handlers = new Map<r.Rid, () => void>();
765
+ private callback: JSCallback | null = null;
766
+
767
+ /** Sets, changes or removes the function of a window. */
768
+ set(window: r.Rid, handler: EventHandler | undefined, alive = true): void {
769
+ if (!handler) {
770
+ if (this.handlers.delete(window) && alive) {
771
+ r.Rwindow_set_close_callback(window, null);
772
+ }
773
+ return;
774
+ }
775
+ const known = this.handlers.has(window);
776
+ this.handlers.set(window, handler as () => void);
777
+ if (!known) {
778
+ this.callback ??= new JSCallback(
779
+ (id: number): number => {
780
+ const close = this.handlers.get(id);
781
+ if (!close) {
782
+ return 1;
783
+ }
784
+ eventScope.run(close);
785
+ return 0;
786
+ },
787
+ { args: ["u32", "ptr"], returns: "i32" },
788
+ );
789
+ r.Rwindow_set_close_callback(window, this.callback.ptr);
790
+ }
791
+ }
792
+
793
+ /** How many windows have a function, for the tests. */
794
+ get size(): number {
795
+ return this.handlers.size;
796
+ }
797
+ }
798
+
799
+ export const closeEvents = new CloseEvents();
800
+
801
+ /** The native event of a prop of a tag, if it has one: the rest goes through the action of the widget. */
802
+ const eventOf = (tag: string, name: string): NativeEvent | undefined => WIDGET_EVENTS[tag]?.[name] ?? COMMON_EVENTS[name];
803
+
804
+ /** Sets, changes or removes the handlers of a widget, from one state of its props to the next. */
805
+ export function applyHandlers(tag: string, widget: r.Rid, previous: ReadonlyMap<string, EventHandler>, next: ReadonlyMap<string, EventHandler>): void {
806
+ const itemsProp = VIRTUAL_ITEMS[tag]?.prop;
807
+ for (const [name, handler] of next) {
808
+ if (name === itemsProp) {
809
+ continue;
810
+ }
811
+ const event = eventOf(tag, name);
812
+ if (event) {
813
+ event.on(widget, handler);
814
+ } else {
815
+ actionEvents.on(widget, handler);
816
+ }
817
+ }
818
+ for (const name of previous.keys()) {
819
+ if (next.has(name) || name === itemsProp) {
820
+ continue;
821
+ }
822
+ const event = eventOf(tag, name);
823
+ if (event) {
824
+ event.off(widget, true);
825
+ } else if (![...next.keys()].some((other) => other !== itemsProp && !eventOf(tag, other))) {
826
+ actionEvents.off(widget, true);
827
+ }
828
+ }
829
+ }
830
+
831
+ /** Removes every handler of a widget. `alive` is false once the widget is gone. */
832
+ export function forgetHandlers(tag: string, widget: r.Rid, alive: boolean): void {
833
+ for (const event of Object.values(WIDGET_EVENTS[tag] ?? {})) {
834
+ event.off(widget, alive);
835
+ }
836
+ for (const event of Object.values(COMMON_EVENTS)) {
837
+ event.off(widget, alive);
838
+ }
839
+ VIRTUAL_ITEMS[tag]?.items.clear(widget, alive);
840
+ actionEvents.off(widget, alive);
841
+ controlledSync.forget(widget);
842
+ }
843
+
844
+ /** Every native event. */
845
+ const allEvents = (): NativeEvent[] => [...Object.values(WIDGET_EVENTS).flatMap((group) => Object.values(group)), ...Object.values(COMMON_EVENTS), ...Object.values(WINDOW_EVENTS)];
846
+
847
+ /** What the tests read to see that nothing grows. */
848
+ export function eventStats(): { handlers: number; nativeCallbacks: number } {
849
+ const events = allEvents();
850
+ const virtual = Object.values(VIRTUAL_ITEMS).reduce((sum, entry) => sum + entry.items.size, 0);
851
+ return {
852
+ handlers: actionEvents.size + closeEvents.size + virtual + events.reduce((sum, event) => sum + event.size, 0),
853
+ nativeCallbacks: 1 + events.filter((event) => event.hasCallback).length,
854
+ };
855
+ }
856
+
857
+ /** The C functions the tables above connect, for the test of what is covered. */
858
+ export function wiredCallbacks(): string[] {
859
+ return [
860
+ ...allEvents().map((event) => event.connectName),
861
+ ...Object.values(VIRTUAL_ITEMS).map((entry) => entry.items.connectName),
862
+ r.Rwindow_set_close_callback.name,
863
+ ];
864
+ }
865
+
866
+ /** Which props of which tags have a native event, for the test of what is covered. */
867
+ export function wiredProps(): Array<readonly [string, string]> {
868
+ return Object.entries(WIDGET_EVENTS).flatMap(([tag, group]) => Object.keys(group).map((name) => [tag, name] as const));
869
+ }