@decky.fx/raven-tsx 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +12 -0
- package/RAVEN_VERSION +1 -0
- package/README.md +444 -2
- package/package.json +75 -4
- package/src/core/errors.ts +20 -0
- package/src/core/events.ts +925 -0
- package/src/core/frame.ts +76 -0
- package/src/core/index.ts +28 -0
- package/src/core/library-check.ts +89 -0
- package/src/core/native.ts +82 -0
- package/src/core/plot.ts +179 -0
- package/src/core/props.generated.ts +172 -0
- package/src/core/props.ts +158 -0
- package/src/core/raven.ts +10 -0
- package/src/core/subscriptions.ts +78 -0
- package/src/core/tags.ts +307 -0
- package/src/core/theme.ts +207 -0
- package/src/core/web.ts +84 -0
- package/src/core/widget-instance.ts +1057 -0
- package/src/core/window-instance.ts +671 -0
- package/src/react/components.generated.ts +179 -0
- package/src/react/hooks.ts +51 -0
- package/src/react/host-config.ts +138 -0
- package/src/react/index.ts +26 -0
- package/src/react/root.ts +119 -0
- package/src/react/tags.ts +50 -0
- package/src/react/web.ts +5 -0
- package/src/solid/babel-preset-solid.d.ts +7 -0
- package/src/solid/components.generated.ts +163 -0
- package/src/solid/index.ts +29 -0
- package/src/solid/node.ts +405 -0
- package/src/solid/plugin.ts +108 -0
- package/src/solid/portal.ts +27 -0
- package/src/solid/preload.ts +14 -0
- package/src/solid/primitives.ts +43 -0
- package/src/solid/renderer.ts +37 -0
- package/src/solid/root.ts +137 -0
- package/src/solid/tag.ts +37 -0
- package/src/solid/tags.ts +101 -0
- package/src/solid/teardown.ts +50 -0
- package/src/solid/web.ts +5 -0
- package/src/vendor/raven.ts +12652 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { ThemeColor } from "./theme";
|
|
2
|
+
|
|
3
|
+
/** A function given to an `On...` prop. What it receives depends on the widget and the event. */
|
|
4
|
+
export type EventHandler = (...values: never[]) => void;
|
|
5
|
+
|
|
6
|
+
/** A row of a `TreeView` given by its `Items` prop, with the rows inside it. */
|
|
7
|
+
export interface TreeItem {
|
|
8
|
+
readonly Text: string;
|
|
9
|
+
readonly Expanded?: boolean | undefined;
|
|
10
|
+
readonly Children?: readonly TreeItem[] | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Numbers given to Raven as they are: a list, or a typed array when there are many. */
|
|
14
|
+
export type PlotNumbers = Float64Array | readonly number[];
|
|
15
|
+
|
|
16
|
+
/** A curve of a `Plot`: what `<Series>` is in XML. Curves are known by their place in the list. */
|
|
17
|
+
export interface PlotSeries {
|
|
18
|
+
/** Its name in the legend. Without one it is not in the legend. */
|
|
19
|
+
readonly Name?: string | undefined;
|
|
20
|
+
/** The Y of its points; NaN leaves a gap. New values are a new array: the same array is taken for the same values. */
|
|
21
|
+
readonly Y?: PlotNumbers | undefined;
|
|
22
|
+
/** The X of its points, as many as Y. Without it they are numbered 0, 1, 2... */
|
|
23
|
+
readonly X?: PlotNumbers | undefined;
|
|
24
|
+
readonly Color?: ThemeColor | undefined;
|
|
25
|
+
/** `None` leaves only the markers: a scatter plot. */
|
|
26
|
+
readonly Line?: "Solid" | "Dashed" | "Dotted" | "DashDot" | "None" | undefined;
|
|
27
|
+
readonly LineWidth?: number | undefined;
|
|
28
|
+
readonly Marker?: "None" | "Circle" | "Square" | "Diamond" | "TriangleUp" | "TriangleDown" | "Cross" | "Plus" | "Asterisk" | undefined;
|
|
29
|
+
readonly MarkerSize?: number | undefined;
|
|
30
|
+
readonly MarkerFilled?: boolean | undefined;
|
|
31
|
+
/** Draws the line in steps. */
|
|
32
|
+
readonly Step?: boolean | undefined;
|
|
33
|
+
/** The opacity of the area between the line and zero, from 0 to 1. */
|
|
34
|
+
readonly Fill?: number | undefined;
|
|
35
|
+
readonly XAxis?: "X" | "X2" | undefined;
|
|
36
|
+
readonly YAxis?: "Y" | "Y2" | undefined;
|
|
37
|
+
/** The user also hides a series, from the legend. */
|
|
38
|
+
readonly Visible?: boolean | undefined;
|
|
39
|
+
/** Keeps only the last points, for data that streams in with `Rplot_add_point`. */
|
|
40
|
+
readonly MaxPoints?: number | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A line across the data of a `Plot` that the user drags: what `<DragLine>` is in XML. */
|
|
44
|
+
export interface PlotDragLine {
|
|
45
|
+
readonly Value: number;
|
|
46
|
+
/** `X` or `X2` makes it vertical, `Y` or `Y2` horizontal. `X` by default. */
|
|
47
|
+
readonly Axis?: "X" | "Y" | "X2" | "Y2" | undefined;
|
|
48
|
+
/** Written on the line, before its value. */
|
|
49
|
+
readonly Label?: string | undefined;
|
|
50
|
+
readonly Color?: ThemeColor | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A prop given to Raven by calling it, as it is, and never as text. */
|
|
54
|
+
export type DataValue = readonly PlotSeries[] | readonly PlotDragLine[];
|
|
55
|
+
|
|
56
|
+
/** One value of an attribute, in the forms a component accepts. */
|
|
57
|
+
export type AttributeValue = string | number | boolean | readonly string[] | readonly number[] | readonly TreeItem[];
|
|
58
|
+
|
|
59
|
+
/** `Slot={{ Padding: 24 }}`: JSX has no dots in names, so `Slot.Padding` is written as a group. */
|
|
60
|
+
export type AttributeGroup = { readonly [key: string]: AttributeValue | null | undefined };
|
|
61
|
+
|
|
62
|
+
/** What an attribute holds for `null` in a group: `Color={{ Text: null }}` takes the override away. */
|
|
63
|
+
export const CLEARED = "\0cleared";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A prop of a component. `Child` is what an adapter's framework gives as children: the core only
|
|
67
|
+
* looks in it for text (`childrenText`), so it has no need to know that type.
|
|
68
|
+
*/
|
|
69
|
+
export type PropValue<Child = never> = AttributeValue | AttributeGroup | EventHandler | DataValue | Child | null | undefined;
|
|
70
|
+
|
|
71
|
+
/** The props of a widget, as an adapter gives them. */
|
|
72
|
+
export type RavenProps<Child = never> = { readonly [name: string]: PropValue<Child> };
|
|
73
|
+
|
|
74
|
+
/** What props mean for a widget once they are read. */
|
|
75
|
+
export interface ResolvedProps {
|
|
76
|
+
/** Attribute name to its text, as XML would hold it. */
|
|
77
|
+
readonly attributes: ReadonlyMap<string, string>;
|
|
78
|
+
/** `On...` name to its function. */
|
|
79
|
+
readonly handlers: ReadonlyMap<string, EventHandler>;
|
|
80
|
+
/** The props of `dataProps`, by name, as they were given. */
|
|
81
|
+
readonly data: ReadonlyMap<string, DataValue>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Tells whether a prop is a function. */
|
|
85
|
+
function isHandler<Child>(value: PropValue<Child>): value is EventHandler {
|
|
86
|
+
return typeof value === "function";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Tells whether a prop is a group of attributes, as opposed to a list or an element of a framework, which marks itself with a symbol. */
|
|
90
|
+
function isGroup<Child>(value: PropValue<Child>): value is AttributeGroup {
|
|
91
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !Object.values(value).some((item) => typeof item === "symbol");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** An attribute value as text: lists of numbers are comma separated, and lists of texts `|` separated. */
|
|
95
|
+
function toText(value: AttributeValue): string {
|
|
96
|
+
if (typeof value === "string") {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
100
|
+
return String(value);
|
|
101
|
+
}
|
|
102
|
+
if (value.some((item) => typeof item === "object")) {
|
|
103
|
+
return JSON.stringify(value);
|
|
104
|
+
}
|
|
105
|
+
return value.every((item) => typeof item === "number") ? value.join(",") : value.join("|");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The text held by the children of a widget, or null when they are not only text:
|
|
110
|
+
* `<Label>Clicked {count} times</Label>` has the children `["Clicked ", 3, " times"]`.
|
|
111
|
+
*/
|
|
112
|
+
export function childrenText<Child>(children: PropValue<Child>): string | null {
|
|
113
|
+
if (typeof children === "string" || typeof children === "number") {
|
|
114
|
+
return String(children);
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(children) && children.length > 0 && children.every((item) => typeof item === "string" || typeof item === "number")) {
|
|
117
|
+
return children.join("");
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Reads props into attributes and handlers. Text in the children means the attribute `Text`.
|
|
124
|
+
* The props named in `dataProps` are kept as they are: they do not go through XML.
|
|
125
|
+
*/
|
|
126
|
+
export function resolveProps<Child>(props: RavenProps<Child>, dataProps: readonly string[] = []): ResolvedProps {
|
|
127
|
+
const attributes = new Map<string, string>();
|
|
128
|
+
const handlers = new Map<string, EventHandler>();
|
|
129
|
+
const data = new Map<string, DataValue>();
|
|
130
|
+
for (const [name, value] of Object.entries(props)) {
|
|
131
|
+
if (name === "children" || name === "key" || name === "ref" || value === undefined || value === null) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (dataProps.includes(name)) {
|
|
135
|
+
data.set(name, value as DataValue);
|
|
136
|
+
} else if (isHandler(value)) {
|
|
137
|
+
handlers.set(name, value);
|
|
138
|
+
} else if (isGroup(value)) {
|
|
139
|
+
for (const [key, item] of Object.entries(value)) {
|
|
140
|
+
if (item !== undefined) {
|
|
141
|
+
attributes.set(`${name}.${key}`, item === null ? CLEARED : toText(item));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || Array.isArray(value)) {
|
|
145
|
+
attributes.set(name, toText(value));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const text = childrenText(props["children"]);
|
|
149
|
+
if (text !== null) {
|
|
150
|
+
attributes.set("Text", text);
|
|
151
|
+
}
|
|
152
|
+
return { attributes, handlers, data };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Text with what XML would take for markup escaped, for an attribute value. */
|
|
156
|
+
export function escapeXml(text: string): string {
|
|
157
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
158
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The binding of Raven. It opens the library as soon as it is imported, so `./native`
|
|
3
|
+
* comes first. Everything in this package imports Raven from here.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import "./native";
|
|
7
|
+
|
|
8
|
+
export * from "../vendor/raven";
|
|
9
|
+
// (shared with onFrame: a name given here has priority over the one of `export *`)
|
|
10
|
+
export { Rset_frame_callback } from "./frame";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Raven tells the application, as plain subscriptions: each returns the function that stops
|
|
3
|
+
* it. An adapter wraps these in what its framework offers, the hooks of React for one.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as r from "./raven";
|
|
7
|
+
import { RavenError } from "./errors";
|
|
8
|
+
import { actionEvents, eventScope } from "./events";
|
|
9
|
+
import { frameSlot, type FrameHandler } from "./frame";
|
|
10
|
+
|
|
11
|
+
const MODIFIERS: Readonly<Record<string, number>> = {
|
|
12
|
+
ctrl: r.R_MOD_CTRL,
|
|
13
|
+
control: r.R_MOD_CTRL,
|
|
14
|
+
shift: r.R_MOD_SHIFT,
|
|
15
|
+
alt: r.R_MOD_ALT,
|
|
16
|
+
super: r.R_MOD_SUPER,
|
|
17
|
+
cmd: r.R_MOD_SUPER,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Keys that are not spelled as in `R_KEY_...`. */
|
|
21
|
+
const KEY_NAMES: Readonly<Record<string, string>> = { esc: "ESCAPE", del: "DELETE", return: "ENTER", pgup: "PAGE_UP", pgdn: "PAGE_DOWN" };
|
|
22
|
+
|
|
23
|
+
/** Reads `"Ctrl+Shift+S"` or `"F5"` into a key and its modifiers. */
|
|
24
|
+
export function parseKeys(keys: string): { key: r.RKey; modifiers: number } {
|
|
25
|
+
let modifiers = 0;
|
|
26
|
+
let key: r.RKey | undefined;
|
|
27
|
+
for (const part of keys.split("+")) {
|
|
28
|
+
const name = part.trim().toLowerCase();
|
|
29
|
+
const modifier = MODIFIERS[name];
|
|
30
|
+
if (modifier !== undefined) {
|
|
31
|
+
modifiers |= modifier;
|
|
32
|
+
} else {
|
|
33
|
+
key = r.RKey[`R_KEY_${KEY_NAMES[name] ?? name.toUpperCase()}` as keyof typeof r.RKey];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (key === undefined) {
|
|
37
|
+
RavenError.throw(`raven-tsx: no key in the shortcut "${keys}"`);
|
|
38
|
+
}
|
|
39
|
+
return { key, modifiers };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Gives a name to a function, for what fires actions by name: `Rwidget_set_action`, the
|
|
44
|
+
* `Action` of a widget made from XML, `Raction_fire`. It receives the widget that fired the
|
|
45
|
+
* action. Returns what takes the name away.
|
|
46
|
+
*/
|
|
47
|
+
export function onAction(name: string, handler: (sender: r.Rid) => void): () => void {
|
|
48
|
+
// (a function of its own: the same handler given twice is two registrations)
|
|
49
|
+
const registered = (sender: r.Rid): void => handler(sender);
|
|
50
|
+
actionEvents.bind(name, registered);
|
|
51
|
+
return () => actionEvents.unbind(name, registered);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let nextShortcut = 0;
|
|
55
|
+
|
|
56
|
+
/** A keyboard shortcut of a window: `onShortcut(window, "Ctrl+S", save)`. Returns what takes it away. */
|
|
57
|
+
export function onShortcut(window: r.Rid, keys: string, handler: () => void): () => void {
|
|
58
|
+
const { key, modifiers } = parseKeys(keys);
|
|
59
|
+
const name = `raven-tsx.shortcut.${++nextShortcut}`;
|
|
60
|
+
actionEvents.bind(name, () => handler());
|
|
61
|
+
r.Rwindow_register_shortcut(window, key, modifiers, name);
|
|
62
|
+
return () => {
|
|
63
|
+
// (the window can be gone already: without an `OnClose`, the user closes it at any time)
|
|
64
|
+
if (r.Rwindow_is_valid(window) !== 0) {
|
|
65
|
+
r.Rwindow_unregister_shortcut(window, key, modifiers);
|
|
66
|
+
}
|
|
67
|
+
actionEvents.unbind(name);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A function called once per frame, before the layout, with the seconds since the last frame:
|
|
73
|
+
* what it changes is committed in time for that frame. Any number of them can live side by side,
|
|
74
|
+
* and with `Rset_frame_callback`. Returns what takes it away.
|
|
75
|
+
*/
|
|
76
|
+
export function onFrame(handler: FrameHandler): () => void {
|
|
77
|
+
return frameSlot.subscribe((delta) => eventScope.run(() => handler(delta)));
|
|
78
|
+
}
|
package/src/core/tags.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Components as tags of Raven: what a `<Widget Name="...">` declaration of XML does, with a
|
|
3
|
+
* component instead. Raven then builds the component from XML (a document, `Markup`, a file of
|
|
4
|
+
* someone else) as it builds `<Button>`:
|
|
5
|
+
*
|
|
6
|
+
* registerTag("Card", Card, { attributes: { Title: "string", Wide: "boolean" }, slots: ["footer"] });
|
|
7
|
+
* // <Card Title="Sales"><Stat/><Into Name="footer"><Button/></Into></Card>
|
|
8
|
+
*
|
|
9
|
+
* This is the half that speaks to Raven: the callbacks, the attributes, the widgets of the children.
|
|
10
|
+
* A framework comes in through a `TagAdapter`, which only knows how to render a component into a widget.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { CString, JSCallback, type Pointer } from "bun:ffi";
|
|
14
|
+
import { RavenError } from "./errors";
|
|
15
|
+
import * as r from "./raven";
|
|
16
|
+
|
|
17
|
+
/** How the text of an attribute becomes a prop. */
|
|
18
|
+
export type AttributeKind = "string" | "number" | "boolean";
|
|
19
|
+
|
|
20
|
+
/** The kind of attribute a prop can be given from, or never for a prop text cannot give. */
|
|
21
|
+
type KindOf<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : never;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The props of a component that take widgets, by the `Node` of its framework: its places for the
|
|
25
|
+
* children of `<Into>`. (in brackets, so that a prop taking only a part of `Node`, a `string`, is not one)
|
|
26
|
+
*/
|
|
27
|
+
type SlotOf<P, Node> = { [K in keyof P]-?: K extends "children" ? never : [Node] extends [P[K]] ? K : never }[keyof P];
|
|
28
|
+
|
|
29
|
+
/** What a tag takes from its element. `Node` is what its framework renders. */
|
|
30
|
+
export interface TagOptions<P, Node> {
|
|
31
|
+
/** The attributes it takes, each with the kind of its prop. The others stay the widget's: `Name`, `Width`, `Slot`... */
|
|
32
|
+
readonly attributes?: { readonly [K in keyof P]?: KindOf<NonNullable<P[K]>> | undefined } | undefined;
|
|
33
|
+
/** Its props that take widgets, by name: the children of `<Into Name="...">` go there; the other children go to `children`. */
|
|
34
|
+
readonly slots?: readonly SlotOf<P, Node>[] | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What an element gives its component: its attributes, and the widgets Raven made for its children. */
|
|
38
|
+
export interface TagContent {
|
|
39
|
+
/** The props taken from attributes. */
|
|
40
|
+
readonly attributes: Readonly<Record<string, string | number | boolean>>;
|
|
41
|
+
/** The widgets of its children, for an adapter to show as adopted (`WidgetInstance.adopt`). */
|
|
42
|
+
readonly children: readonly r.Rid[];
|
|
43
|
+
/** The widgets of its named places, by name. */
|
|
44
|
+
readonly slots: Readonly<Record<string, readonly r.Rid[]>>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A component rendered in a widget of a tag, as an adapter gives it back. */
|
|
48
|
+
export interface TagMount {
|
|
49
|
+
/** Renders the component again, with what it has now. */
|
|
50
|
+
update(props: TagContent): void;
|
|
51
|
+
/** Takes the component away. Its widget stays: Raven destroys that. */
|
|
52
|
+
unmount(): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** How a framework shows a registered component in a widget. `Component` is its kind of component. */
|
|
56
|
+
export interface TagAdapter<Component> {
|
|
57
|
+
/** Renders a component into a widget, which it fills, and returns how to change it and take it away. */
|
|
58
|
+
mount(component: Component, props: TagContent, container: r.Rid): TagMount;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A registered tag. */
|
|
62
|
+
interface Registration {
|
|
63
|
+
readonly mount: (props: TagContent, container: r.Rid) => TagMount;
|
|
64
|
+
readonly attributes: Readonly<Record<string, AttributeKind>>;
|
|
65
|
+
readonly slots: ReadonlySet<string>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** An element of a registered tag, as Raven built it. */
|
|
69
|
+
interface Element {
|
|
70
|
+
readonly tag: string;
|
|
71
|
+
readonly widget: r.Rid;
|
|
72
|
+
props: Record<string, string | number | boolean>;
|
|
73
|
+
/** The widgets of the children of the element, once Raven added them all. */
|
|
74
|
+
children: readonly r.Rid[];
|
|
75
|
+
slots: Record<string, readonly r.Rid[]>;
|
|
76
|
+
/** Rendered once its children are known: until then, attributes are only kept. */
|
|
77
|
+
ready: boolean;
|
|
78
|
+
/** The widgets of its children, which the adapter only places: those it does not show are destroyed with the element. */
|
|
79
|
+
adopted: readonly r.Rid[];
|
|
80
|
+
/** Its component, with the registration it came from: a tag registered again renders the new one. */
|
|
81
|
+
mounted: { readonly registration: Registration; readonly mount: TagMount } | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The tag that sends its children to a named place of the component around it. */
|
|
85
|
+
const INTO = "Into";
|
|
86
|
+
|
|
87
|
+
const registrations = new Map<string, Registration>();
|
|
88
|
+
const elements = new Map<r.Rid, Element>();
|
|
89
|
+
/** The widgets of `<Into>` elements, with the name of their place. */
|
|
90
|
+
const intos = new Set<r.Rid>();
|
|
91
|
+
|
|
92
|
+
/** The children of a widget, in order. */
|
|
93
|
+
function childrenOf(widget: r.Rid): r.Rid[] {
|
|
94
|
+
return Array.from({ length: r.Rwidget_get_child_count(widget) }, (_, index) => r.Rwidget_get_child(widget, index));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Renders an element with what it has now, through the adapter of its tag. */
|
|
98
|
+
function render(element: Element): void {
|
|
99
|
+
const registration = registrations.get(element.tag);
|
|
100
|
+
if (!registration || !element.ready) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const content: TagContent = { attributes: element.props, children: element.children, slots: element.slots };
|
|
104
|
+
if (element.mounted?.registration === registration) {
|
|
105
|
+
element.mounted.mount.update(content);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// (forgotten first: taken away once, even if this or the new mount throws)
|
|
109
|
+
const previous = element.mounted;
|
|
110
|
+
element.mounted = null;
|
|
111
|
+
previous?.mount.unmount();
|
|
112
|
+
element.mounted = { registration, mount: registration.mount(content, element.widget) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Raven adds the children of an element after making it, and before its attributes: once the
|
|
117
|
+
* call that built it returns, they are all there, and go to their places.
|
|
118
|
+
*/
|
|
119
|
+
function place(widget: r.Rid, element: Element): void {
|
|
120
|
+
const registration = registrations.get(element.tag);
|
|
121
|
+
const children: r.Rid[] = [];
|
|
122
|
+
const slots: Record<string, r.Rid[]> = {};
|
|
123
|
+
// (each one taken out is the element's at once: destroyed with it, even if this stops half way)
|
|
124
|
+
const taken: r.Rid[] = [];
|
|
125
|
+
element.adopted = taken;
|
|
126
|
+
for (const child of childrenOf(widget)) {
|
|
127
|
+
if (!intos.has(child)) {
|
|
128
|
+
// (taken out, as those of <Into>: only what the component renders is shown)
|
|
129
|
+
r.Rwidget_remove_child(widget, child);
|
|
130
|
+
taken.push(child);
|
|
131
|
+
children.push(child);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const name = r.Rwidget_get_name(child) ?? "";
|
|
135
|
+
const inside = childrenOf(child);
|
|
136
|
+
if (registration?.slots.has(name)) {
|
|
137
|
+
slots[name] = [...(slots[name] ?? []), ...inside];
|
|
138
|
+
} else {
|
|
139
|
+
console.error(`raven-tsx: <${element.tag}>: <Into Name="${name}">: no place of this name; its widgets go with the others`);
|
|
140
|
+
children.push(...inside);
|
|
141
|
+
}
|
|
142
|
+
// (the widgets it held are taken out of it first: the adapter places them)
|
|
143
|
+
for (const held of inside) {
|
|
144
|
+
r.Rwidget_remove_child(child, held);
|
|
145
|
+
taken.push(held);
|
|
146
|
+
}
|
|
147
|
+
intos.delete(child);
|
|
148
|
+
r.Rwidget_destroy(child);
|
|
149
|
+
}
|
|
150
|
+
element.adopted = [...children, ...Object.values(slots).flat()];
|
|
151
|
+
element.children = children;
|
|
152
|
+
element.slots = slots;
|
|
153
|
+
element.ready = true;
|
|
154
|
+
render(element);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Makes the widget of an element: a box the adapter renders the component into. */
|
|
158
|
+
function create(tag: string): r.Rid {
|
|
159
|
+
const widget = r.Rwidget_create_from_xml("<VBox/>");
|
|
160
|
+
if (tag === INTO) {
|
|
161
|
+
intos.add(widget);
|
|
162
|
+
r.Rwidget_set_destroy_callback(widget, nativeCallbacks().destroy);
|
|
163
|
+
return widget;
|
|
164
|
+
}
|
|
165
|
+
const element: Element = { tag, widget, props: {}, children: [], slots: {}, ready: false, adopted: [], mounted: null };
|
|
166
|
+
elements.set(widget, element);
|
|
167
|
+
r.Rwidget_set_destroy_callback(widget, nativeCallbacks().destroy);
|
|
168
|
+
queueMicrotask(() => {
|
|
169
|
+
try {
|
|
170
|
+
if (elements.get(widget) === element) {
|
|
171
|
+
place(widget, element);
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error(`raven-tsx: error placing the children of <${tag}>:`, error);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return widget;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The prop of an attribute, or null when the text is not of its kind. */
|
|
181
|
+
function parse(kind: AttributeKind, value: string): string | number | boolean | null {
|
|
182
|
+
if (kind === "string") {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
if (kind === "number") {
|
|
186
|
+
const number = Number(value);
|
|
187
|
+
return value.trim() !== "" && Number.isFinite(number) ? number : null;
|
|
188
|
+
}
|
|
189
|
+
return value === "true" ? true : value === "false" ? false : null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Takes an attribute of an element, if its component has a prop for it. */
|
|
193
|
+
function setAttribute(widget: r.Rid, name: string, value: string): boolean {
|
|
194
|
+
const element = elements.get(widget);
|
|
195
|
+
const kind = element ? registrations.get(element.tag)?.attributes[name] : undefined;
|
|
196
|
+
if (!element || !kind) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
const prop = parse(kind, value);
|
|
200
|
+
if (prop === null) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
element.props = { ...element.props, [name]: prop };
|
|
204
|
+
render(element);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Unmounts the component of an element whose widget Raven destroys. */
|
|
209
|
+
function destroyed(widget: r.Rid): void {
|
|
210
|
+
const element = elements.get(widget);
|
|
211
|
+
elements.delete(widget);
|
|
212
|
+
intos.delete(widget);
|
|
213
|
+
try {
|
|
214
|
+
element?.mounted?.mount.unmount();
|
|
215
|
+
} finally {
|
|
216
|
+
// (those in its tree went with it; those the component did not show are held by nothing)
|
|
217
|
+
for (const child of element?.adopted ?? []) {
|
|
218
|
+
if (r.Rwidget_is_valid(child) && r.Rwidget_get_parent(child) === r.R_INVALID_ID) {
|
|
219
|
+
r.Rwidget_destroy(child);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The pointers of the functions Raven calls for every registered tag and element. */
|
|
226
|
+
interface NativeCallbacks {
|
|
227
|
+
readonly create: Pointer;
|
|
228
|
+
readonly attribute: Pointer;
|
|
229
|
+
readonly destroy: Pointer;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Made once and shared by every tag and element: a callback given to Raven stays alive for good. */
|
|
233
|
+
let callbacks: NativeCallbacks | null = null;
|
|
234
|
+
|
|
235
|
+
/** The pointers of the callbacks, made on first use. */
|
|
236
|
+
function nativeCallbacks(): NativeCallbacks {
|
|
237
|
+
if (!callbacks) {
|
|
238
|
+
const make = new JSCallback((tag: Pointer | null): number => {
|
|
239
|
+
try {
|
|
240
|
+
return create(tag ? new CString(tag).toString() : "");
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error("raven-tsx: error making a registered tag:", error);
|
|
243
|
+
return r.R_INVALID_ID;
|
|
244
|
+
}
|
|
245
|
+
}, { args: ["ptr", "ptr"], returns: "u32" });
|
|
246
|
+
const attribute = new JSCallback((widget: number, name: Pointer | null, value: Pointer | null): number => {
|
|
247
|
+
try {
|
|
248
|
+
return name && value && setAttribute(widget, new CString(name).toString(), new CString(value).toString()) ? 1 : 0;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error("raven-tsx: error giving an attribute to a registered tag:", error);
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
}, { args: ["u32", "ptr", "ptr", "ptr"], returns: "i32" });
|
|
254
|
+
const destroy = new JSCallback((widget: number): void => {
|
|
255
|
+
try {
|
|
256
|
+
destroyed(widget);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error("raven-tsx: error unmounting a registered tag:", error);
|
|
259
|
+
}
|
|
260
|
+
}, { args: ["u32", "ptr"], returns: "void" });
|
|
261
|
+
if (!make.ptr || !attribute.ptr || !destroy.ptr) {
|
|
262
|
+
RavenError.throw("raven-tsx: cannot make the callbacks of registered tags");
|
|
263
|
+
}
|
|
264
|
+
callbacks = { create: make.ptr, attribute: attribute.ptr, destroy: destroy.ptr };
|
|
265
|
+
}
|
|
266
|
+
return callbacks;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Registers `<Into>`, which every registered tag uses for its named places. */
|
|
270
|
+
function registerInto(): void {
|
|
271
|
+
if (!r.Rxml_is_tag_registered(INTO)) {
|
|
272
|
+
const { create: make, attribute } = nativeCallbacks();
|
|
273
|
+
if (!r.Rxml_register_tag(INTO, make, attribute)) {
|
|
274
|
+
RavenError.fromLibrary("registerTag: <Into>");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Makes a component a tag Raven knows, built from XML as its own tags are: in documents, in
|
|
281
|
+
* `Markup`, in files. Its attributes become props of the kinds given; the widgets of its children
|
|
282
|
+
* are placed where it renders `children`, and those of `<Into Name="...">` where it renders the
|
|
283
|
+
* prop of that name. Registering a name again replaces it. Returns what unregisters it. An
|
|
284
|
+
* adapter calls this with what renders its components, and gives the application its own `registerTag`.
|
|
285
|
+
*/
|
|
286
|
+
export function registerTagWith<Component, P extends object, Node>(adapter: TagAdapter<Component>, name: string, component: Component, options: TagOptions<P, Node> = {}): () => void {
|
|
287
|
+
if (name === INTO) {
|
|
288
|
+
RavenError.throw(`registerTag: <${INTO}> is the tag of the named places of registered tags`);
|
|
289
|
+
}
|
|
290
|
+
registerInto();
|
|
291
|
+
const { create: make, attribute } = nativeCallbacks();
|
|
292
|
+
if (!r.Rxml_register_tag(name, make, attribute)) {
|
|
293
|
+
RavenError.fromLibrary(`registerTag(${JSON.stringify(name)})`);
|
|
294
|
+
}
|
|
295
|
+
const registration: Registration = {
|
|
296
|
+
mount: (props, container) => adapter.mount(component, props, container),
|
|
297
|
+
attributes: Object.fromEntries(Object.entries(options.attributes ?? {}).filter((entry): entry is [string, AttributeKind] => typeof entry[1] === "string")),
|
|
298
|
+
slots: new Set(options.slots?.map(String)),
|
|
299
|
+
};
|
|
300
|
+
registrations.set(name, registration);
|
|
301
|
+
return () => {
|
|
302
|
+
if (registrations.get(name) === registration) {
|
|
303
|
+
registrations.delete(name);
|
|
304
|
+
r.Rxml_unregister_tag(name);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|