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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +12 -0
  3. package/RAVEN_VERSION +1 -0
  4. package/README.md +444 -2
  5. package/package.json +75 -4
  6. package/src/core/errors.ts +20 -0
  7. package/src/core/events.ts +925 -0
  8. package/src/core/frame.ts +76 -0
  9. package/src/core/index.ts +28 -0
  10. package/src/core/library-check.ts +89 -0
  11. package/src/core/native.ts +82 -0
  12. package/src/core/plot.ts +179 -0
  13. package/src/core/props.generated.ts +172 -0
  14. package/src/core/props.ts +158 -0
  15. package/src/core/raven.ts +10 -0
  16. package/src/core/subscriptions.ts +78 -0
  17. package/src/core/tags.ts +307 -0
  18. package/src/core/theme.ts +207 -0
  19. package/src/core/web.ts +84 -0
  20. package/src/core/widget-instance.ts +1057 -0
  21. package/src/core/window-instance.ts +671 -0
  22. package/src/react/components.generated.ts +179 -0
  23. package/src/react/hooks.ts +51 -0
  24. package/src/react/host-config.ts +138 -0
  25. package/src/react/index.ts +26 -0
  26. package/src/react/root.ts +119 -0
  27. package/src/react/tags.ts +50 -0
  28. package/src/react/web.ts +5 -0
  29. package/src/solid/babel-preset-solid.d.ts +7 -0
  30. package/src/solid/components.generated.ts +163 -0
  31. package/src/solid/index.ts +29 -0
  32. package/src/solid/node.ts +405 -0
  33. package/src/solid/plugin.ts +108 -0
  34. package/src/solid/portal.ts +27 -0
  35. package/src/solid/preload.ts +14 -0
  36. package/src/solid/primitives.ts +43 -0
  37. package/src/solid/renderer.ts +37 -0
  38. package/src/solid/root.ts +137 -0
  39. package/src/solid/tag.ts +37 -0
  40. package/src/solid/tags.ts +101 -0
  41. package/src/solid/teardown.ts +50 -0
  42. package/src/solid/web.ts +5 -0
  43. package/src/vendor/raven.ts +12652 -0
@@ -0,0 +1,179 @@
1
+ // Generated from the schema of Raven by src/tools/generate-components.ts: do not edit.
2
+ import { createElement, type Key, type ReactElement, type ReactNode, type Ref } from "react";
3
+ import type * as Props from "../core/props.generated";
4
+ import type { WidgetInstance } from "../core/widget-instance";
5
+ import type { WindowInstance } from "../core/window-instance";
6
+
7
+ /** The props every component has from React. */
8
+ interface ReactProps<R> {
9
+ readonly key?: Key | undefined;
10
+ readonly ref?: Ref<R> | undefined;
11
+ readonly children?: ReactNode | undefined;
12
+ }
13
+
14
+ /**
15
+ * A component for a tag of Raven, or one given to `Rxml_register_tag` with its props.
16
+ * JSX takes a capitalized name for a value, not for a tag.
17
+ */
18
+ export function tag<P extends object, R = WidgetInstance>(name: string): (props: P & ReactProps<R>) => ReactElement {
19
+ return (props) => createElement(name, props);
20
+ }
21
+
22
+ /**
23
+ * A window of its own, wherever it is in the tree: it opens with its element and closes with it.
24
+ * With `OnClose`, closing is a request: the window stays until the element is taken out.
25
+ */
26
+ export const Window = tag<Props.WindowProps, WindowInstance>("Window");
27
+
28
+ /** Stacks its children from top to bottom. */
29
+ export const VBox = tag<Props.VBoxProps>("VBox");
30
+
31
+ /** Lines its children up from left to right. */
32
+ export const HBox = tag<Props.HBoxProps>("HBox");
33
+
34
+ /** Places its children in rows and columns (Slot.Column, Slot.Row). */
35
+ export const Grid = tag<Props.GridProps>("Grid");
36
+
37
+ /** Cells of the same size, filled in order. */
38
+ export const UniformGrid = tag<Props.UniformGridProps>("UniformGrid");
39
+
40
+ /** Puts its children on top of each other. */
41
+ export const Overlay = tag<Props.OverlayProps>("Overlay");
42
+
43
+ /** A surface with padding around one child. */
44
+ export const Border = tag<Props.BorderProps>("Border");
45
+
46
+ /** Gives one child a size or a shape. */
47
+ export const SizeBox = tag<Props.SizeBoxProps>("SizeBox");
48
+
49
+ /** Empty space. */
50
+ export const Spacer = tag<Props.SpacerProps>("Spacer");
51
+
52
+ /** Scrolls content that is larger than its place. */
53
+ export const ScrollBox = tag<Props.ScrollBoxProps>("ScrollBox");
54
+
55
+ /** Panes separated by bars the user drags. */
56
+ export const Splitter = tag<Props.SplitterProps>("Splitter");
57
+
58
+ /** Lines its children up and continues on the next line. */
59
+ export const WrapBox = tag<Props.WrapBoxProps>("WrapBox");
60
+
61
+ /** Places its children freely (Slot.Position, Slot.Size, Slot.Anchors). */
62
+ export const Canvas = tag<Props.CanvasProps>("Canvas");
63
+
64
+ /** Shows one of its children. */
65
+ export const Switcher = tag<Props.SwitcherProps>("Switcher");
66
+
67
+ /** Its children as tabs (Slot.Title). */
68
+ export const TabView = tag<Props.TabViewProps>("TabView");
69
+
70
+ /** A header that shows and hides one child. */
71
+ export const Expander = tag<Props.ExpanderProps>("Expander");
72
+
73
+ /** Text. */
74
+ export const Label = tag<Props.LabelProps>("Label");
75
+
76
+ /** A button, with a text or with one child. */
77
+ export const Button = tag<Props.ButtonProps>("Button");
78
+
79
+ /** A box to check, a radio button or a switch. */
80
+ export const Checkbox = tag<Props.CheckboxProps>("Checkbox");
81
+
82
+ /** A value chosen by dragging. */
83
+ export const Slider = tag<Props.SliderProps>("Slider");
84
+
85
+ /** How far something is. */
86
+ export const ProgressBar = tag<Props.ProgressBarProps>("ProgressBar");
87
+
88
+ /** A picture. */
89
+ export const Image = tag<Props.ImageProps>("Image");
90
+
91
+ /** One line of text to type. */
92
+ export const TextInput = tag<Props.TextInputProps>("TextInput");
93
+
94
+ /** A number to type or to step. */
95
+ export const SpinBox = tag<Props.SpinBoxProps>("SpinBox");
96
+
97
+ /** One choice in a list that drops down. */
98
+ export const ComboBox = tag<Props.ComboBoxProps>("ComboBox");
99
+
100
+ /** A line between things. */
101
+ export const Separator = tag<Props.SeparatorProps>("Separator");
102
+
103
+ /** Rows of text, one selected; any number of them. */
104
+ export const ListView = tag<Props.ListViewProps>("ListView");
105
+
106
+ /** Rows inside rows, made of Item elements. */
107
+ export const TreeView = tag<Props.TreeViewProps>("TreeView");
108
+
109
+ /** A grid of tiles, an icon over a name, given by the application (Rtile_view_set_items). */
110
+ export const TileView = tag<Props.TileViewProps>("TileView");
111
+
112
+ /** A bar of Menu elements; on macOS, shown in the menu bar of the system. */
113
+ export const MenuBar = tag<Props.MenuBarProps>("MenuBar");
114
+
115
+ /** A menu: MenuItem, Separator and Menu elements. */
116
+ export const Menu = tag<Props.MenuProps>("Menu");
117
+
118
+ /** An item of a menu. */
119
+ export const MenuItem = tag<Props.MenuItemProps>("MenuItem");
120
+
121
+ /** A place the application draws in with its own renderer. */
122
+ export const Viewport = tag<Props.ViewportProps>("Viewport");
123
+
124
+ /** A widget painted and measured by the application. */
125
+ export const Custom = tag<Props.CustomProps>("Custom");
126
+
127
+ /** Text of several lines: notes, code. */
128
+ export const TextEditor = tag<Props.TextEditorProps>("TextEditor");
129
+
130
+ /** The title bar of a window with a custom header; holds any widgets. */
131
+ export const HeaderBar = tag<Props.HeaderBarProps>("HeaderBar");
132
+
133
+ /** Folders and files to choose from. */
134
+ export const FileBrowser = tag<Props.FileBrowserProps>("FileBrowser");
135
+
136
+ /** Panels the user arranges by dragging their tabs. */
137
+ export const DockArea = tag<Props.DockAreaProps>("DockArea");
138
+
139
+ /** A pill at the top of the window that unfolds: first child folded, second unfolded. */
140
+ export const Island = tag<Props.IslandProps>("Island");
141
+
142
+ /** A panel that shows what is behind it out of focus, around one child. */
143
+ export const BackgroundBlur = tag<Props.BackgroundBlurProps>("BackgroundBlur");
144
+
145
+ /** Chooses a color: square, hues, opacity, code, numbers, swatches. */
146
+ export const ColorPicker = tag<Props.ColorPickerProps>("ColorPicker");
147
+
148
+ /** A sequencer: Track elements holding Clip and Key elements, and Marker elements. */
149
+ export const Timeline = tag<Props.TimelineProps>("Timeline");
150
+
151
+ /** Blocks that snap into each other (Scratch): Kind elements (Name, Label, Shape, Category) and Category elements (Name, Color). */
152
+ export const BlockEditor = tag<Props.BlockEditorProps>("BlockEditor");
153
+
154
+ /** Rows of cells under columns that can be resized and sorted: Column and Row elements; any number of rows. */
155
+ export const Table = tag<Props.TableProps>("Table");
156
+
157
+ /** A date, chosen in a calendar that drops down. */
158
+ export const DatePicker = tag<Props.DatePickerProps>("DatePicker");
159
+
160
+ /** A time of the day: hours, minutes, optionally seconds. */
161
+ export const TimePicker = tag<Props.TimePickerProps>("TimePicker");
162
+
163
+ /** A short message that rises over the content for a few seconds (in an Overlay). */
164
+ export const Toast = tag<Props.ToastProps>("Toast");
165
+
166
+ /** The path to the current place: links separated by chevrons. */
167
+ export const Breadcrumb = tag<Props.BreadcrumbProps>("Breadcrumb");
168
+
169
+ /** A score shown and chosen with stars. */
170
+ export const Rating = tag<Props.RatingProps>("Rating");
171
+
172
+ /** Page numbers with previous and next. */
173
+ export const Pagination = tag<Props.PaginationProps>("Pagination");
174
+
175
+ /** Curves of X / Y data: Series elements (Name, Color, Line, LineWidth, Marker, MarkerSize, MarkerFilled, Step, Fill, XAxis, Axis, MaxPoints, X and Y lists or Points "x,y x,y"), Scatter elements, and DragLine elements (Axis, Value, Label, Color). */
176
+ export const Plot = tag<Props.PlotProps>("Plot");
177
+
178
+ /** Nodes linked by wires: Node elements (Input, Output, any widget), Link and Type elements. */
179
+ export const NodeGraph = tag<Props.NodeGraphProps>("NodeGraph");
@@ -0,0 +1,51 @@
1
+ import { useEffect, useLayoutEffect, useRef, type RefObject } from "react";
2
+ import type * as r from "../core/raven";
3
+ import type { FrameHandler } from "../core/frame";
4
+ import { onAction, onFrame, onShortcut } from "../core/subscriptions";
5
+ import type { WindowInstance } from "../core/window-instance";
6
+
7
+ /**
8
+ * The function of the last commit, for a native callback to call. Not of the last render: React
9
+ * can throw a render away, and then its function would see state that never was.
10
+ */
11
+ function useLatest<T>(value: T): RefObject<T> {
12
+ const latest = useRef(value);
13
+ useLayoutEffect(() => {
14
+ latest.current = value;
15
+ });
16
+ return latest;
17
+ }
18
+
19
+ /**
20
+ * Gives a name to a function, for what fires actions by name: `Rwidget_set_action`, the
21
+ * `Action` of a widget made from XML, `Raction_fire`. The function can change at each render.
22
+ * It receives the widget that fired the action.
23
+ */
24
+ export function useAction(name: string, handler: (sender: r.Rid) => void): void {
25
+ const latest = useLatest(handler);
26
+ useEffect(() => onAction(name, (sender) => latest.current(sender)), [name]);
27
+ }
28
+
29
+ /** A keyboard shortcut of a window: `useShortcut(window, "Ctrl+S", save)`, with `window` the ref of a `<Window>`. */
30
+ export function useShortcut(window: RefObject<WindowInstance | null>, keys: string, handler: () => void): void {
31
+ const latest = useLatest(handler);
32
+ useEffect(() => {
33
+ const owner = window.current;
34
+ if (!owner) {
35
+ return;
36
+ }
37
+ return onShortcut(owner.window, keys, () => latest.current());
38
+ }, [window, keys]);
39
+ }
40
+
41
+ /**
42
+ * A function called once per frame, before the layout, with the seconds since the last frame:
43
+ * what it changes is committed in time for that frame. It also runs when nothing is happening,
44
+ * about 60 times a second: `runApp` wakes that often so that Bun's timers run beside Raven, which
45
+ * `Rwait_frame(-1)` would stop. Any number of them can live side by side, and with
46
+ * `Rset_frame_callback`. The function can change at each render.
47
+ */
48
+ export function useFrame(handler: FrameHandler): void {
49
+ const latest = useLatest(handler);
50
+ useEffect(() => onFrame((delta) => latest.current(delta)), []);
51
+ }
@@ -0,0 +1,138 @@
1
+ import { createContext, type ReactNode } from "react";
2
+ import ReactReconciler from "react-reconciler";
3
+ import { DefaultEventPriority, DiscreteEventPriority } from "react-reconciler/constants";
4
+ import { RavenError } from "../core/errors";
5
+ import * as r from "../core/raven";
6
+ import { eventScope } from "../core/events";
7
+ import { childrenText, type RavenProps } from "../core/props";
8
+ import { ADOPTED_TAG, finishPlacing, WidgetInstance, type HostNode } from "../core/widget-instance";
9
+ import { WindowInstance } from "../core/window-instance";
10
+
11
+ /** React asks for a context per level of the tree. Raven has no use for one, but it must exist. */
12
+ type HostContext = { readonly inMenu: boolean };
13
+ const HOST_CONTEXT: HostContext = { inMenu: false };
14
+ const MENU_CONTEXT: HostContext = { inMenu: true };
15
+ const NO_TIMEOUT = -1;
16
+
17
+ /** The props of a widget under React, where children are React's. */
18
+ type ReactRavenProps = RavenProps<ReactNode>;
19
+
20
+ /** A `<Separator>` in a `<Menu>`: a line of the menu, as in XML, which is an item there and no widget of its own. */
21
+ const menuSeparator = (props: ReactRavenProps): ReactRavenProps => ({ ...(props["Name"] === undefined ? {} : { Name: props["Name"] }), Separator: true });
22
+
23
+ /** The priority React asks about: discrete while a native event runs, as a click in React DOM. */
24
+ class UpdatePriority {
25
+ /** What React set, or 0. */
26
+ current = 0;
27
+
28
+ /** Runs something at the discrete priority. */
29
+ discrete(run: () => void): void {
30
+ const before = this.current;
31
+ this.current = DiscreteEventPriority;
32
+ try {
33
+ run();
34
+ } finally {
35
+ this.current = before;
36
+ }
37
+ }
38
+ }
39
+
40
+ const updatePriority = new UpdatePriority();
41
+
42
+ type RavenHostConfig = ReactReconciler.HostConfig<
43
+ string, ReactRavenProps, HostNode, HostNode, never, never, never, never, never, HostNode,
44
+ HostContext, never, ReturnType<typeof setTimeout>, typeof NO_TIMEOUT, null, null, null, never, never, never
45
+ >;
46
+
47
+ const hostConfig: RavenHostConfig = {
48
+ supportsMutation: true,
49
+ supportsPersistence: false,
50
+ supportsHydration: false,
51
+ supportsMicrotasks: true,
52
+ isPrimaryRenderer: true,
53
+ warnsIfNotActing: false,
54
+ noTimeout: NO_TIMEOUT,
55
+ scheduleTimeout: setTimeout,
56
+ cancelTimeout: clearTimeout,
57
+ scheduleMicrotask: queueMicrotask,
58
+ NotPendingTransition: null,
59
+ HostTransitionContext: createContext<null>(null),
60
+
61
+ // (Raven makes nothing while React renders, which React may throw away: a node is made when it
62
+ // is placed, as React commits it, with what it holds. See `HostNode.materialize`)
63
+ createInstance: (tag, props, _root, context) => {
64
+ if (tag === "Window") {
65
+ return WindowInstance.create(props);
66
+ }
67
+ if (tag === ADOPTED_TAG) {
68
+ return WidgetInstance.adopt(typeof props["widget"] === "number" ? props["widget"] : r.R_INVALID_ID);
69
+ }
70
+ return tag === "Separator" && context.inMenu ? WidgetInstance.create("MenuItem", menuSeparator(props)) : WidgetInstance.create(tag, props);
71
+ },
72
+ createTextInstance: (text) => RavenError.throw(`raven-tsx: text must be alone in a widget: "${text}"`),
73
+ shouldSetTextContent: (_tag, props) => childrenText(props["children"]) !== null,
74
+ appendInitialChild: (parent, child) => parent.appendInitial(child),
75
+ finalizeInitialChildren: () => false,
76
+ commitMount: () => {},
77
+ getPublicInstance: (instance) => instance,
78
+ getRootHostContext: () => HOST_CONTEXT,
79
+ getChildHostContext: (parent, tag) => {
80
+ const inMenu = tag === "Menu";
81
+ return inMenu === parent.inMenu ? parent : inMenu ? MENU_CONTEXT : HOST_CONTEXT;
82
+ },
83
+ prepareForCommit: () => null,
84
+ resetAfterCommit: () => finishPlacing(),
85
+ preparePortalMount: () => {},
86
+ clearContainer: () => {},
87
+
88
+ appendChild: (parent, child) => parent.append(child),
89
+ appendChildToContainer: (container, child) => container.append(child),
90
+ insertBefore: (parent, child, before) => parent.insertBefore(child, before),
91
+ insertInContainerBefore: (container, child, before) => container.insertBefore(child, before),
92
+ removeChild: (parent, child) => parent.remove(child),
93
+ removeChildFromContainer: (container, child) => container.remove(child),
94
+ commitUpdate: (instance, tag, _previous, next) => instance.update(tag === "Separator" && instance.tag === "MenuItem" ? menuSeparator(next) : next),
95
+ commitTextUpdate: () => {},
96
+ hideInstance: () => {},
97
+ unhideInstance: () => {},
98
+ hideTextInstance: () => {},
99
+ unhideTextInstance: () => {},
100
+ detachDeletedInstance: () => {},
101
+ beforeActiveInstanceBlur: () => {},
102
+ afterActiveInstanceBlur: () => {},
103
+ prepareScopeUpdate: () => {},
104
+ getInstanceFromNode: () => null,
105
+ getInstanceFromScope: () => null,
106
+
107
+ setCurrentUpdatePriority: (priority) => {
108
+ updatePriority.current = priority;
109
+ },
110
+ getCurrentUpdatePriority: () => updatePriority.current,
111
+ resolveUpdatePriority: () => updatePriority.current || DefaultEventPriority,
112
+ shouldAttemptEagerTransition: () => false,
113
+ trackSchedulerEvent: () => {},
114
+ resolveEventType: () => null,
115
+ resolveEventTimeStamp: () => -1.1,
116
+ requestPostPaintCallback: () => {},
117
+ maySuspendCommit: () => false,
118
+ maySuspendCommitOnUpdate: () => false,
119
+ maySuspendCommitInSyncRender: () => false,
120
+ preloadInstance: () => true,
121
+ startSuspendingCommit: () => null,
122
+ suspendInstance: () => {},
123
+ suspendOnActiveViewTransition: () => {},
124
+ waitForCommitToBeReady: () => null,
125
+ bindToConsole: (_method, args) => console.log.bind(console, ...args),
126
+ getSuspendedCommitReason: () => null,
127
+ resetFormInstance: () => {},
128
+ rendererVersion: "0.1.1",
129
+ rendererPackageName: "raven-tsx-react",
130
+ extraDevToolsConfig: null,
131
+ };
132
+
133
+ export const reconciler = ReactReconciler(hostConfig);
134
+
135
+ eventScope.setFlush(() => {
136
+ reconciler.flushSyncWork();
137
+ });
138
+ eventScope.setWrapper((run) => updatePriority.discrete(run));
@@ -0,0 +1,26 @@
1
+ /**
2
+ * raven-tsx for React: the interface in TSX, on Raven's widgets. It exports everything of the
3
+ * core too, so an application needs this one import.
4
+ *
5
+ * import { useState } from "react";
6
+ * import { Button, Label, runApp, VBox } from "@decky.fx/raven-tsx/react";
7
+ *
8
+ * function App() {
9
+ * const [count, setCount] = useState(0);
10
+ * return (
11
+ * <VBox Spacing={12} Slot={{ Padding: 24 }}>
12
+ * <Label>Clicked {count} times</Label>
13
+ * <Button Style="Primary" OnClicked={() => setCount(count + 1)}>Click me</Button>
14
+ * </VBox>
15
+ * );
16
+ * }
17
+ *
18
+ * await runApp(<App />, { Title: "Hello", Width: 480, Height: 240 });
19
+ */
20
+
21
+ export * from "../core";
22
+ export * from "./components.generated";
23
+ export { useAction, useFrame, useShortcut } from "./hooks";
24
+ export { createPortal, createWindow, RavenRoot, runApp, type RunOptions } from "./root";
25
+ export { registerTag, type TagOptions } from "./tags";
26
+ export { WebView } from "./web";
@@ -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");
@@ -0,0 +1,7 @@
1
+ // babel-preset-solid has no types of its own: the plugin only hands it to Babel.
2
+ declare module "babel-preset-solid" {
3
+ import type { PluginItem } from "@babel/core";
4
+
5
+ const preset: PluginItem;
6
+ export default preset;
7
+ }