@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,119 @@
1
+ import { createElement, type ReactNode, type ReactPortal } from "react";
2
+ import { ConcurrentRoot } from "react-reconciler/constants";
3
+ import * as r from "../core/raven";
4
+ import { RavenError } from "../core/errors";
5
+ import { resolveProps, type RavenProps } from "../core/props";
6
+ import type { WindowOptions } from "../core/props.generated";
7
+ import { WidgetInstance } from "../core/widget-instance";
8
+ import { makeWindow } from "../core/window-instance";
9
+ import { Window } from "./components.generated";
10
+ import { reconciler } from "./host-config";
11
+
12
+ /** Says what a component threw during a render or an effect. */
13
+ function report(error: unknown): void {
14
+ console.error("raven-tsx: error in a component:", error);
15
+ }
16
+
17
+ /**
18
+ * A tree of React, as `createRoot` of ReactDOM. Given a widget, it renders into it.
19
+ * Without one, it renders `<Window>` elements, each its own window.
20
+ */
21
+ export class RavenRoot {
22
+ private readonly root: ReturnType<typeof reconciler.createContainer>;
23
+
24
+ constructor(parent: r.Rid = r.R_INVALID_ID) {
25
+ this.root = reconciler.createContainer(
26
+ WidgetInstance.forContainer(parent), ConcurrentRoot, null, false, null, "raven-tsx", report, report, report, () => {}, null,
27
+ );
28
+ }
29
+
30
+ /** Renders or updates the tree. */
31
+ render(element: ReactNode): void {
32
+ reconciler.updateContainer(element, this.root, null, null);
33
+ }
34
+
35
+ /** Removes the tree and runs the cleanups of its effects, now. */
36
+ unmount(): void {
37
+ reconciler.updateContainerSync(null, this.root, null, null);
38
+ reconciler.flushSyncWork();
39
+ reconciler.flushPassiveEffects();
40
+ }
41
+ }
42
+
43
+ /** What portals render into, by handle: React takes a container that changes as another place. */
44
+ const portalContainers = new Map<r.Rid, WidgetInstance>();
45
+
46
+ /**
47
+ * Renders children into a widget outside this part of the tree, as `createPortal` of ReactDOM:
48
+ * context goes on as if they were here, which a second `RavenRoot` cannot do. For a widget given
49
+ * to Raven by hand, the content of a graph node for one:
50
+ *
51
+ * const [box] = useState(() => raven.Rwidget_create_from_xml("<VBox/>"));
52
+ * useLayoutEffect(() => raven.Rnode_graph_set_node_content(graph, node, box), [graph, node, box]);
53
+ * return createPortal(<Button>Run</Button>, box);
54
+ *
55
+ * What holds the widget owns it: the portal only adds and removes the children.
56
+ */
57
+ export function createPortal(children: ReactNode, widget: r.Rid, key: string | null = null): ReactPortal {
58
+ for (const id of portalContainers.keys()) {
59
+ if (!r.Rwidget_is_valid(id)) {
60
+ portalContainers.delete(id);
61
+ }
62
+ }
63
+ let container = portalContainers.get(widget);
64
+ if (!container) {
65
+ container = WidgetInstance.forContainer(widget);
66
+ portalContainers.set(widget, container);
67
+ }
68
+ return reconciler.createPortal(children, container, null, key);
69
+ }
70
+
71
+ /** Makes a window with a place for a tree, and returns the handle of the window with its root. */
72
+ export function createWindow(options: WindowOptions = {}): { window: r.Rid; root: RavenRoot } {
73
+ const { window, content } = makeWindow(resolveProps(options as RavenProps).attributes);
74
+ return { window, root: new RavenRoot(content) };
75
+ }
76
+
77
+ /** How `runApp` starts Raven. */
78
+ export interface RunOptions {
79
+ /** The name of the application: `RInitDesc::app_name`, which system notifications show. */
80
+ readonly appName?: string | undefined;
81
+ /** Runs after Raven started and before the first render: for a look, a theme, a registered tag. */
82
+ readonly setup?: (() => void) | undefined;
83
+ /** Raven's allocator: functions of C only, as it allocates on several threads. See `Rset_allocator`. */
84
+ readonly allocator?: Parameters<typeof r.Rset_allocator>[0] | undefined;
85
+ }
86
+
87
+ /**
88
+ * Starts Raven, shows a tree and runs until the last window closes, the event loop of Bun
89
+ * running beside it. Then the effects are cleaned up and Raven stops, also when the run ends
90
+ * with an error, so that it can start again.
91
+ *
92
+ * With options, the tree goes in one window made from them. Without, the tree says what
93
+ * windows there are, with `<Window>` elements.
94
+ */
95
+ export async function runApp(element: ReactNode, options?: WindowOptions, start?: RunOptions): Promise<void> {
96
+ if (start?.allocator) {
97
+ r.Rset_allocator(start.allocator);
98
+ }
99
+ const desc = new r.RInitDesc();
100
+ desc.backend = r.R_BACKEND_VULKAN;
101
+ desc.app_name = start?.appName ?? null;
102
+ if (!r.Rinit_ex(desc)) {
103
+ RavenError.fromLibrary("Raven cannot start");
104
+ }
105
+ let root: RavenRoot | null = null;
106
+ try {
107
+ start?.setup?.();
108
+ root = new RavenRoot();
109
+ root.render(options ? createElement(Window, options, element) : element);
110
+ // (React commits in the loop, and makes the windows then. Rwait_frame ends once the last one closed: until there was one, it goes on)
111
+ await r.runLoop();
112
+ } finally {
113
+ try {
114
+ root?.unmount();
115
+ } finally {
116
+ r.Rshutdown();
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * React components as tags of Raven: what `<Widget Name="...">` does in XML, with a component.
3
+ * The core does the part that speaks to Raven; this renders the component, a root of its own per
4
+ * element, so context does not reach it from the tree around it.
5
+ */
6
+
7
+ import { createElement, type ComponentType, type ReactNode } from "react";
8
+ import * as r from "../core/raven";
9
+ import { registerTagWith, type TagAdapter, type TagContent, type TagOptions as CoreTagOptions } from "../core/tags";
10
+ import { ADOPTED_TAG } from "../core/widget-instance";
11
+ import { RavenRoot } from "./root";
12
+
13
+ /** What a tag takes from its element. */
14
+ export type TagOptions<P> = CoreTagOptions<P, ReactNode>;
15
+
16
+ /** What a registered component is given: its attributes, and its places with their widgets. */
17
+ type TagProps = Readonly<Record<string, string | number | boolean | ReactNode>>;
18
+
19
+ /** The elements of adopted widgets, in their order. */
20
+ function adopted(widgets: readonly r.Rid[]): ReactNode {
21
+ return widgets.length === 0 ? undefined : widgets.map((widget) => createElement(ADOPTED_TAG, { key: widget, widget }));
22
+ }
23
+
24
+ /** The element of a component, with what its element in XML gives it. */
25
+ function elementOf(component: ComponentType<TagProps>, content: TagContent): ReactNode {
26
+ const slots = Object.fromEntries(Object.entries(content.slots).map(([name, widgets]) => [name, adopted(widgets)]));
27
+ return createElement(component, { ...content.attributes, ...slots, children: adopted(content.children) });
28
+ }
29
+
30
+ /** Renders a registered component in a root of its own. */
31
+ const reactAdapter: TagAdapter<ComponentType<TagProps>> = {
32
+ mount(component, props, container) {
33
+ const root = new RavenRoot(container);
34
+ root.render(elementOf(component, props));
35
+ return {
36
+ update: (next) => root.render(elementOf(component, next)),
37
+ unmount: () => root.unmount(),
38
+ };
39
+ },
40
+ };
41
+
42
+ /**
43
+ * Makes a component a tag Raven knows, built from XML as its own tags are: in documents, in
44
+ * `Markup`, in files. Its attributes become props of the kinds given; the widgets of its children
45
+ * are placed where it renders `children`, and those of `<Into Name="...">` where it renders the
46
+ * prop of that name. Registering a name again replaces it. Returns what unregisters it.
47
+ */
48
+ export function registerTag<P extends object>(name: string, component: ComponentType<P>, options: TagOptions<P> = {}): () => void {
49
+ return registerTagWith<ComponentType<TagProps>, P, ReactNode>(reactAdapter, name, component as ComponentType<TagProps>, options);
50
+ }
@@ -0,0 +1,5 @@
1
+ import type { WebViewProps } from "../core/web";
2
+ import { tag } from "./components.generated";
3
+
4
+ /** A page of the web, after `registerWebView`. */
5
+ export const WebView = tag<WebViewProps>("WebView");