@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,256 @@
1
+ import * as r from "./raven";
2
+ import { RavenError } from "./errors";
3
+ import { closeEvents, WINDOW_EVENTS } from "./events";
4
+ import type { EventHandler } from "./props";
5
+ import { escapeXml, resolveProps, type RavenProps, type ResolvedProps } from "./props";
6
+ import { WidgetInstance, type HostNode } from "./widget-instance";
7
+
8
+ /** Where the widgets of a window go. Its size is the size of the window. */
9
+ export const CONTENT_NAME = "raven-tsx.content";
10
+
11
+ /** What each way of writing `Backdrop` means, as the XML loader reads it. */
12
+ const BACKDROPS: Readonly<Record<string, r.RBackdrop>> = {
13
+ None: r.R_BACKDROP_NONE,
14
+ Opaque: r.R_BACKDROP_NONE,
15
+ Transparent: r.R_BACKDROP_TRANSPARENT,
16
+ Blur: r.R_BACKDROP_BLUR,
17
+ Acrylic: r.R_BACKDROP_ACRYLIC,
18
+ Mica: r.R_BACKDROP_MICA,
19
+ };
20
+
21
+ /** What a window can take while it is open, by calling Raven. */
22
+ const SET_ON_OPEN_WINDOW: Readonly<Record<string, (window: r.Rid, value: string) => void>> = {
23
+ Title: (window, value) => r.Rwindow_set_title(window, value),
24
+ Theme: (window, value) => void r.Rwindow_set_theme(window, value),
25
+ Backdrop: (window, value) => {
26
+ const backdrop = BACKDROPS[value];
27
+ if (backdrop === undefined || !r.Rwindow_set_backdrop(window, backdrop)) {
28
+ console.warn(`raven-tsx: <Window> did not take Backdrop="${value}": it needs Transparent, and a window system that does it`);
29
+ }
30
+ },
31
+ Opacity: (window, value) => {
32
+ if (!r.Rwindow_set_opacity(window, Number(value))) {
33
+ console.warn("raven-tsx: this window system cannot make a window see-through with Opacity");
34
+ }
35
+ },
36
+ ContinuousRedraw: (window, value) => r.Rwindow_set_continuous_redraw(window, value === "true" ? r.R_TRUE : r.R_FALSE),
37
+ };
38
+
39
+ /** What the XML of a window does not take: it is set by calling Raven, once the window is there. */
40
+ const NOT_IN_XML: ReadonlySet<string> = new Set(["Opacity", "ContinuousRedraw"]);
41
+
42
+ /**
43
+ * Makes a window from attributes, with a place for widgets. That place is an `Overlay`, as for the
44
+ * content of a window in C: a child fills the window and follows its `Slot`. A `VBox` would not let
45
+ * it, and children that should stack go in one.
46
+ */
47
+ export function makeWindow(attributes: ReadonlyMap<string, string>): { window: r.Rid; content: r.Rid } {
48
+ const text = [...attributes].filter(([name]) => !NOT_IN_XML.has(name)).map(([name, value]) => `${name}="${escapeXml(value)}"`).join(" ");
49
+ const window = r.Rwindow_create_from_xml(`<Window ${text}><Overlay Name="${CONTENT_NAME}"/></Window>`);
50
+ if (window === r.R_INVALID_ID) {
51
+ RavenError.fromLibrary("Raven cannot make the window");
52
+ }
53
+ for (const [name, value] of attributes) {
54
+ if (NOT_IN_XML.has(name)) {
55
+ SET_ON_OPEN_WINDOW[name]?.(window, value);
56
+ }
57
+ }
58
+ return { window, content: r.Rwidget_find(window, CONTENT_NAME) };
59
+ }
60
+
61
+ /** Sets or removes the events of a window, from what its props say now. */
62
+ function setWindowEvents(window: r.Rid, handlers: ReadonlyMap<string, EventHandler>): void {
63
+ for (const [name, event] of Object.entries(WINDOW_EVENTS)) {
64
+ const handler = handlers.get(name);
65
+ if (handler) {
66
+ event.on(window, handler);
67
+ } else {
68
+ event.off(window, true);
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * A `<Window>` under a framework, and what a `ref` receives. It holds widgets but stands by itself:
75
+ * it can be anywhere in the tree, and goes away when its element does. It opens once it is placed:
76
+ * a render the framework throws away opens nothing.
77
+ */
78
+ export class WindowInstance implements HostNode {
79
+ readonly tag = "Window";
80
+ readonly nativeWidget = null;
81
+ private id: r.Rid = r.R_INVALID_ID;
82
+ /** Where its widgets go, once it is open. */
83
+ private content: WidgetInstance | null = null;
84
+ /** The children rendered with it, which go in when it opens. */
85
+ private readonly initial: HostNode[] = [];
86
+ /** Its `<Island>`: it floats above the content, as in XML, and the window owns it. */
87
+ private island: WidgetInstance | null = null;
88
+ private state: "pending" | "live" | "gone" = "pending";
89
+
90
+ private constructor(private resolved: ResolvedProps) {}
91
+
92
+ /** A new window, as a framework renders it: it opens at `materialize`, with what its props say. */
93
+ static create<Child>(props: RavenProps<Child>): WindowInstance {
94
+ return new WindowInstance(resolveProps(props));
95
+ }
96
+
97
+ /** Raven's handle of the window, for the functions of the `raven` export: `R_INVALID_ID` until it is placed. */
98
+ get window(): r.Rid {
99
+ return this.id;
100
+ }
101
+
102
+ /** Opens the window, with its children. If any of it fails, the window is deleted. */
103
+ materialize(): void {
104
+ if (this.state !== "pending") {
105
+ return;
106
+ }
107
+ const { window, content } = makeWindow(this.resolved.attributes);
108
+ this.id = window;
109
+ this.content = WidgetInstance.forContainer(content);
110
+ this.state = "live";
111
+ try {
112
+ closeEvents.set(window, this.resolved.handlers.get("OnClose"));
113
+ setWindowEvents(window, this.resolved.handlers);
114
+ for (const child of this.initial.splice(0)) {
115
+ this.append(child);
116
+ }
117
+ } catch (error) {
118
+ this.forgetTree();
119
+ this.dispose();
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ /** Whether the window still exists: without an `OnClose`, the user can close it at any time. */
125
+ get isOpen(): boolean {
126
+ return this.id !== r.R_INVALID_ID && r.Rwindow_is_valid(this.id) !== 0;
127
+ }
128
+
129
+ setParent(): void {}
130
+
131
+ applyParentAttributes(): void {}
132
+
133
+ /** What can change on a window that is open. The other attributes only count when it is made. */
134
+ update<Child>(props: RavenProps<Child>): void {
135
+ const next = resolveProps(props);
136
+ if (this.state === "live" && this.isOpen) {
137
+ this.applyChanges(next);
138
+ }
139
+ this.resolved = next;
140
+ }
141
+
142
+ /** Applies what changed in the props. */
143
+ private applyChanges(next: ResolvedProps): void {
144
+ const previous = this.resolved.attributes;
145
+ for (const [name, value] of next.attributes) {
146
+ if (value !== previous.get(name)) {
147
+ SET_ON_OPEN_WINDOW[name]?.(this.id, value);
148
+ }
149
+ }
150
+ const width = next.attributes.get("Width");
151
+ const height = next.attributes.get("Height");
152
+ if (width !== undefined && height !== undefined && (width !== previous.get("Width") || height !== previous.get("Height"))) {
153
+ r.Rwindow_set_size(this.id, Number(width), Number(height));
154
+ }
155
+ for (const [name, value] of next.attributes) {
156
+ if (previous.get(name) !== value && !SET_ON_OPEN_WINDOW[name] && name !== "Width" && name !== "Height") {
157
+ console.warn(`raven-tsx: <Window> ${name} only counts when the window is made`);
158
+ }
159
+ }
160
+ closeEvents.set(this.id, next.handlers.get("OnClose"));
161
+ setWindowEvents(this.id, next.handlers);
162
+ }
163
+
164
+ /** The place of its widgets: children are added and removed only once the window is placed. */
165
+ private get open(): WidgetInstance {
166
+ if (!this.content) {
167
+ return RavenError.throw("raven-tsx: a <Window> got a child before it was placed");
168
+ }
169
+ return this.content;
170
+ }
171
+
172
+ /** Makes a child the island of this window, if it is one and there is none yet. */
173
+ private takeIsland(child: HostNode): boolean {
174
+ if (child.tag !== "Island" || this.island || !(child instanceof WidgetInstance)) {
175
+ return false;
176
+ }
177
+ child.materialize();
178
+ const set = (widget: r.Rid): void => r.Rwindow_set_island(this.id, widget);
179
+ this.island = child;
180
+ child.setHolder(set);
181
+ set(child.widget);
182
+ return true;
183
+ }
184
+
185
+ append(child: HostNode): void {
186
+ if (child !== this.island && !this.takeIsland(child)) {
187
+ this.open.append(child);
188
+ }
189
+ }
190
+
191
+ appendInitial(child: HostNode): void {
192
+ if (this.state === "live") {
193
+ this.append(child);
194
+ } else {
195
+ this.initial.push(child);
196
+ }
197
+ }
198
+
199
+ insertBefore(child: HostNode, before: HostNode): void {
200
+ if (child === this.island || this.takeIsland(child)) {
201
+ return;
202
+ }
203
+ // (the island is not among the widgets of the content: nothing can go before it there)
204
+ if (before === this.island) {
205
+ this.open.append(child);
206
+ } else {
207
+ this.open.insertBefore(child, before);
208
+ }
209
+ }
210
+
211
+ remove(child: HostNode): void {
212
+ if (child !== this.island) {
213
+ this.open.remove(child);
214
+ return;
215
+ }
216
+ child.forgetTree();
217
+ this.island = null;
218
+ (child as WidgetInstance).setHolder(null);
219
+ if (this.isOpen) {
220
+ // (this deletes it: the window owns it. What it holds of registered tags is theirs)
221
+ (child as WidgetInstance).releaseAdopted();
222
+ r.Rwindow_set_island(this.id, r.R_INVALID_ID);
223
+ }
224
+ child.dispose();
225
+ }
226
+
227
+ forgetTree(): void {
228
+ if (this.state !== "live") {
229
+ return;
230
+ }
231
+ this.content?.forgetTree();
232
+ this.island?.forgetTree();
233
+ closeEvents.set(this.id, undefined, false);
234
+ for (const event of Object.values(WINDOW_EVENTS)) {
235
+ event.off(this.id, false);
236
+ }
237
+ }
238
+
239
+ dispose(): void {
240
+ const state = this.state;
241
+ this.state = "gone";
242
+ if (state !== "live") {
243
+ return;
244
+ }
245
+ if (this.isOpen) {
246
+ // (the window destroys what it holds: those of registered tags are theirs)
247
+ this.content?.releaseAdopted();
248
+ this.island?.releaseAdopted();
249
+ }
250
+ this.content?.disposeDetached();
251
+ this.island?.disposeDetached();
252
+ if (this.isOpen) {
253
+ r.Rwindow_destroy(this.id);
254
+ }
255
+ }
256
+ }
@@ -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.0",
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";