@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
package/README.md
CHANGED
|
@@ -1,3 +1,445 @@
|
|
|
1
|
-
#
|
|
1
|
+
# raven-tsx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Native desktop interfaces in TSX, for [Bun](https://bun.com), on the real widgets of [Raven](https://github.com/oscar-soirey/Raven). A core knows only Raven, and an adapter for a framework is built on it: there is one for React and one for [Solid](#solid). No XML, and with React no build step, as Bun compiles TSX itself.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { Button, Label, runApp, VBox } from "@decky.fx/raven-tsx/react";
|
|
8
|
+
|
|
9
|
+
function App() {
|
|
10
|
+
const [count, setCount] = useState(0);
|
|
11
|
+
return (
|
|
12
|
+
<VBox Spacing={12} Slot={{ Padding: 24 }}>
|
|
13
|
+
<Label>Clicked {count} times</Label>
|
|
14
|
+
<Button Style="Primary" OnClicked={() => setCount(count + 1)}>Click me</Button>
|
|
15
|
+
</VBox>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await runApp(<App />, { Title: "Hello", Width: 480, Height: 240 });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
Every C example of Raven has a TSX port in [examples](../../examples), with screenshots in the [README of the repository](../../README.md).
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
bun add @decky.fx/raven-tsx react react-reconciler
|
|
29
|
+
|
|
30
|
+
For Solid, see [Solid](#solid). The frameworks are optional peer dependencies: an application installs those of the adapter it uses, and one that uses only the core installs none.
|
|
31
|
+
|
|
32
|
+
| Import | What it is |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `@decky.fx/raven-tsx/react` | The components, `runApp`, the hooks and `registerTag`, for React, and everything of the core |
|
|
35
|
+
| `@decky.fx/raven-tsx/solid` | The same for Solid, with `create...` functions in place of the hooks |
|
|
36
|
+
| `@decky.fx/raven-tsx/solid/preload`, `/solid/plugin` | The Bun plugin that compiles TSX for Solid: to preload, or to give `Bun.build` |
|
|
37
|
+
| `@decky.fx/raven-tsx` | The core, for code that is not a component: `defineTheme`, `loadTheme`, `raven`, and `onAction`, `onShortcut`, `onFrame`, which each return what stops them |
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{ "compilerOptions": { "jsx": "react-jsx", "types": ["bun", "react"] } }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The package is TypeScript, so your compiler checks it with your settings (`skipLibCheck` skips only `.d.ts` files). It compiles with `strict`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes` (optional props take `undefined`, as React's do), `noPropertyAccessFromIndexSignature`, `noUnusedLocals`, `noUnusedParameters`, `noImplicitOverride`, `noImplicitReturns`, `noFallthroughCasesInSwitch`, `isolatedModules` and `verbatimModuleSyntax`. Not yet with `erasableSyntaxOnly`: Raven's own binding has `enum`s.
|
|
44
|
+
|
|
45
|
+
Raven's native library comes with it: there is a package per platform, and the package manager installs the one that fits. No install script runs, and nothing is compiled on your machine.
|
|
46
|
+
|
|
47
|
+
| Platform | Package |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Linux x64, arm64 | `@decky.fx/raven-tsx-linux-x64`, `-linux-arm64` |
|
|
50
|
+
| macOS Apple Silicon, Intel | `@decky.fx/raven-tsx-darwin-arm64`, `-darwin-x64` |
|
|
51
|
+
| Windows x64 | `@decky.fx/raven-tsx-win32-x64` |
|
|
52
|
+
|
|
53
|
+
What the system must have: a Vulkan driver, and on Linux libX11. On macOS, version 14 or newer, Vulkan comes from MoltenVK: `brew install molten-vk`.
|
|
54
|
+
|
|
55
|
+
`RAVEN_LIBRARY` set to the path of a library has priority, to try a build of your own.
|
|
56
|
+
|
|
57
|
+
## One executable
|
|
58
|
+
|
|
59
|
+
`bun build --compile` holds Bun, your code and the native library in one file:
|
|
60
|
+
|
|
61
|
+
bun build --compile src/index.tsx --outfile app
|
|
62
|
+
|
|
63
|
+
For another platform, install its package first:
|
|
64
|
+
|
|
65
|
+
bun install --os=win32 --cpu=x64
|
|
66
|
+
bun build --compile --target=bun-windows-x64 src/index.tsx --outfile app.exe
|
|
67
|
+
|
|
68
|
+
An application of Solid builds with `Bun.build` and the plugin, as [examples/solid-helloworld/build.ts](../../examples/solid-helloworld/build.ts) does.
|
|
69
|
+
|
|
70
|
+
## Solid
|
|
71
|
+
|
|
72
|
+
bun add @decky.fx/raven-tsx solid-js
|
|
73
|
+
bun add -d babel-preset-solid @babel/core
|
|
74
|
+
|
|
75
|
+
Solid compiles its JSX itself, with Babel, to calls of a renderer: Bun alone would compile it for React. The plugin of the package does that, and gives Solid's build for clients, as on Bun `solid-js` would otherwise be its build for servers, which does not react. Preload it, for `bun run` and `bun test`:
|
|
76
|
+
|
|
77
|
+
```toml
|
|
78
|
+
# bunfig.toml
|
|
79
|
+
preload = ["@decky.fx/raven-tsx/solid/preload"]
|
|
80
|
+
|
|
81
|
+
[test]
|
|
82
|
+
preload = ["@decky.fx/raven-tsx/solid/preload"]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{ "compilerOptions": { "jsx": "preserve", "jsxImportSource": "solid-js", "types": ["bun"] } }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import { createSignal } from "solid-js";
|
|
91
|
+
import { Button, Label, runApp, VBox } from "@decky.fx/raven-tsx/solid";
|
|
92
|
+
|
|
93
|
+
function App() {
|
|
94
|
+
const [count, setCount] = createSignal(0);
|
|
95
|
+
return (
|
|
96
|
+
<VBox Spacing={12} Slot={{ Padding: 24 }}>
|
|
97
|
+
<Label>Clicked {count()} times</Label>
|
|
98
|
+
<Button Style="Primary" OnClicked={() => setCount(count() + 1)}>Click me</Button>
|
|
99
|
+
</VBox>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await runApp(() => <App />, { Title: "Hello", Width: 480, Height: 240 });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The components, their props, events, windows, themes and tags are the same as with React, on the same core; what differs is Solid's:
|
|
107
|
+
|
|
108
|
+
- `runApp`, `RavenRoot.render` and `createWindow().root.render` take a function that returns the tree, as Solid's `render` does.
|
|
109
|
+
- A component runs once, and a signal changes only the widgets that read it. A handler's change is on the widgets when the handler returns, so a controlled `TextInput` or `Checkbox` works as with React.
|
|
110
|
+
- `ref` receives the `WidgetInstance` once Raven has the widget, placed, as React's does: `ref={(input) => input.focus()}` works. An element never placed gives no ref. The `WindowInstance` of a `<Window>` comes when the element is made; the window opens once it is placed.
|
|
111
|
+
- `createAction(name, handler)`, `createShortcut(() => window, keys, handler)` and `createFrame(handler)` are the hooks of React, and last as long as the component that calls them.
|
|
112
|
+
- `<Portal widget={handle}>` renders into a widget made by hand, as `createPortal` does with React.
|
|
113
|
+
- `registerTag` takes a Solid component, whose props follow the attributes of its element.
|
|
114
|
+
- An element kept in a variable and shown again (`<Show>`) keeps its widget; one whose component is gone is deleted.
|
|
115
|
+
|
|
116
|
+
## How props work
|
|
117
|
+
|
|
118
|
+
The components, with their props, are generated from Raven's own schema.
|
|
119
|
+
|
|
120
|
+
| XML | TSX |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `Text="Hi"` | `Text="Hi"`, or `<Label>Hi</Label>` |
|
|
123
|
+
| `Width="120"` | `Width={120}` |
|
|
124
|
+
| `Slot.Padding="24"` | `Slot={{ Padding: 24 }}`, since JSX has no dots in names |
|
|
125
|
+
| `Items="a\|b"` | `Items={["a", "b"]}` |
|
|
126
|
+
| `OnClicked="name"` | `OnClicked={() => ...}` |
|
|
127
|
+
|
|
128
|
+
## Events
|
|
129
|
+
|
|
130
|
+
Events with a value give it to the function, at each change. A `TextInput` gives its text in `OnChanged` and again in `OnCommitted` on Enter.
|
|
131
|
+
|
|
132
|
+
| Component | Event | Value |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `TextInput` | `OnChanged`, `OnCommitted` | `string` |
|
|
135
|
+
| `Checkbox`, `Expander` | `OnChanged` | `boolean` |
|
|
136
|
+
| `Slider`, `SpinBox` | `OnChanged` | `number` |
|
|
137
|
+
| `ComboBox`, `TabView` | `OnChanged` | index |
|
|
138
|
+
| `ListView`, `TileView` | `OnChanged`, `OnActivated` | index |
|
|
139
|
+
| `TreeView` | `OnChanged`, `OnActivated` | handle of the item |
|
|
140
|
+
| `Table` | `OnChanged`, `OnActivated` | row |
|
|
141
|
+
| `Table` | `OnSort` | column, and whether it is descending |
|
|
142
|
+
| `DatePicker` | `OnChanged` | `"YYYY-MM-DD"` |
|
|
143
|
+
| `TimePicker` | `OnChanged` | `"HH:MM:SS"` |
|
|
144
|
+
| `Rating` | `OnChanged` | `number` |
|
|
145
|
+
| `Pagination` | `OnChanged` | page, from 0 |
|
|
146
|
+
| `Breadcrumb` | `OnClicked` | index |
|
|
147
|
+
| `TextEditor` | `OnChanged`, `OnCompletion` | its text, the start of the word |
|
|
148
|
+
| `TextEditor` | `OnZoom` | `number` |
|
|
149
|
+
| `ColorPicker` | `OnChanged` | `"#RRGGBB"`, or `"#RRGGBBAA"` when it is not opaque |
|
|
150
|
+
| `Label` | `OnLink` | the address of the link |
|
|
151
|
+
| `MenuItem` | `OnClicked` | whether it is checked now |
|
|
152
|
+
| `Island` | `OnChanged` | `boolean` |
|
|
153
|
+
| `Viewport`, `<Window>` | `OnResize` | width, height |
|
|
154
|
+
| `FileBrowser` | `OnChanged`, `OnAccepted` | path |
|
|
155
|
+
| `DockArea` | `OnChanged` | its layout, for `Layout` or `Rdock_area_load_layout` |
|
|
156
|
+
| `TreeView`, `TileView`, `NodeGraph`, `BlockEditor` | `OnContext` | what was right clicked, x, y |
|
|
157
|
+
| `Timeline` | `OnTime` | seconds |
|
|
158
|
+
| `Timeline` | `OnItemChanged`, `OnSelection`, `OnActivated`, `OnRemoved` | item |
|
|
159
|
+
| `NodeGraph` | `OnLinked`, `OnUnlinked`, `OnMoved`, `OnSelection`, `OnRemoved`, `OnActivated` | link or node |
|
|
160
|
+
| `BlockEditor` | `OnChanged`, `OnClicked` | block |
|
|
161
|
+
| any widget, `<Window>` | `OnDrop` | an `RDropEvent` |
|
|
162
|
+
| any widget | `OnEvent` | an `REvent`, before the widget sees it: the pointer over a `Custom`, for one |
|
|
163
|
+
|
|
164
|
+
Some events ask: `OnClose` of a `TabView` (index, page) and of a `DockArea` (panel), `OnConnect` of a `NodeGraph` (two pins), and `OnEvent` and `OnDrop`, where true means taken. Return true to let the thing happen. A tab or a panel that stays can then go by taking its element out, which is the way to do it: when Raven closes a panel by itself, it deletes a widget React still has, which does no harm but leaves the element with a `.widget` that is no longer valid. `Documents` on a `TabView` gives its tabs a close button.
|
|
165
|
+
|
|
166
|
+
Other events give nothing, and share the one action a widget has in Raven: give a widget one of them. `Timeline` and `NodeGraph` have an `OnChanged` of this kind, once per gesture.
|
|
167
|
+
|
|
168
|
+
Inputs are controlled, as in React DOM. With `Text={value}`, what the state says stays on screen if a handler refuses a change. An update made in an event is committed before the next event, as a click in React DOM. `ListView` and `ComboBox` keep their `Selected` when their `Items` change. A `|` in an item would split it, as `Items` is written that way in XML: for such items use `ItemText`. `FileBrowser`'s `Directory` and `Timeline`'s `Playing` are not held this way: the user changes them with no event that says so.
|
|
169
|
+
|
|
170
|
+
## Windows
|
|
171
|
+
|
|
172
|
+
`<Window>` is an element like any other, anywhere in the tree: it opens with its element and closes with it. Without options, `runApp` lets the tree say what windows there are, and ends when the last one closes.
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
function App() {
|
|
176
|
+
const [help, setHelp] = useState(false);
|
|
177
|
+
return (
|
|
178
|
+
<>
|
|
179
|
+
<Window Title="Main" Width={640} Height={480}>
|
|
180
|
+
<Button OnClicked={() => setHelp(true)}>Help</Button>
|
|
181
|
+
</Window>
|
|
182
|
+
{help && <Window Title="Help" OnClose={() => setHelp(false)}><Label>...</Label></Window>}
|
|
183
|
+
</>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await runApp(<App />);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
A tree that never opens a window never ends: Raven runs until a window has existed, so that one can open later. An application that renders nothing at all waits for ever, and wakes about 60 times a second.
|
|
191
|
+
|
|
192
|
+
A splash screen is a window like the others, with `IsSplashScreen`: no frame, centered, above the other windows and out of the taskbar. Render the main window beside it and take the splash out when the main window is ready.
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
{loading && <Window IsSplashScreen Width={480} Height={280}><Label>Starting...</Label></Window>}
|
|
196
|
+
<Window Title="Main" Width={1024} Height={720}>...</Window>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
With `OnClose`, closing is a request: the window stays until the element is taken out. Without it, the user can close the window at any time. `Title`, `Theme`, `Width`, `Height`, `Backdrop`, `Opacity` and `ContinuousRedraw` can change on an open window. Other attributes only count when it is made.
|
|
200
|
+
|
|
201
|
+
What a `<Window>` holds directly fills it and follows its `Slot`, as the content of a window in C. Several such children lie on top of each other: put them in a `<VBox>` to stack them.
|
|
202
|
+
|
|
203
|
+
On macOS a `MenuBar` goes to the menu bar of the system; `Native={false}` keeps it in the window. An `<Island>` directly in a `<Window>` floats above the content at the top, as in XML. A `<Menu>` that no `MenuBar` or `Menu` holds is not shown until it is asked: its ref has `open(x, y)`, in the coordinates `OnContext` gives. A `<Separator>` in a `<Menu>` is a line of the menu.
|
|
204
|
+
|
|
205
|
+
## Themes
|
|
206
|
+
|
|
207
|
+
A theme is a typed object, not a theme file: `defineTheme` checks it, and `loadTheme` gives it to Raven, which keeps it under its name. The colors and metrics take the names of Raven's schema, so a typo does not compile.
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
import { defineTheme, loadTheme, raven } from "@decky.fx/raven-tsx/react";
|
|
211
|
+
|
|
212
|
+
export const ocean = defineTheme({
|
|
213
|
+
Name: "Ocean",
|
|
214
|
+
Base: "Dark",
|
|
215
|
+
Accent: "#22D3EE",
|
|
216
|
+
Colors: { Window: "#0B1220", Panel: "#111A2C", Text: "#DCE7F5" },
|
|
217
|
+
Metrics: { ControlHeight: 32, Spacing: 8 },
|
|
218
|
+
Syntax: { Keyword: "#7DD3FC", String: "#86EFAC" },
|
|
219
|
+
Styles: [
|
|
220
|
+
{ Widget: "Button", CornerRadius: 16 },
|
|
221
|
+
{ Class: "danger", Accent: "#FB7185" },
|
|
222
|
+
{ Widget: "Slider", Part: "Knob", State: "Hovered", Size: 16 },
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await runApp(<App />, options, { setup: () => raven.Rtheme_use(loadTheme(ocean)) });
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`Theme={ocean.Name}` gives it to a part of the window only. Everything a theme file has is there: `Look` or `Base`, `Motion`, `Material`, `Font`, `Wallpaper`, `Colors`, `Metrics`, `Glass`, `Syntax`, and `Styles` for a type, a class, a part and its states. A mistake Raven finds (a `Base` that names no theme) is reported by Raven, and the rest is used, as in a file. `themeXml(theme)` gives the text of the theme file, to save one.
|
|
230
|
+
|
|
231
|
+
## Tags of your own
|
|
232
|
+
|
|
233
|
+
A component can be a tag Raven knows, built by Raven from XML as `<Button>` is: in a document made by code, in `Markup`, in a file of someone else. There is no XML declaration of it: the component is the only one.
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
await runApp(<App />, options, {
|
|
237
|
+
// (once Raven started: Raven refuses a tag before)
|
|
238
|
+
setup: () => {
|
|
239
|
+
registerTag("Card", Card, {
|
|
240
|
+
attributes: { Title: "string", Wide: "boolean", Count: "number" },
|
|
241
|
+
slots: ["footer"],
|
|
242
|
+
});
|
|
243
|
+
registerTag("Stat", Stat, { attributes: { Label: "string", Value: "string" } });
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```xml
|
|
249
|
+
<Card Title="Sales" Wide="true">
|
|
250
|
+
<Stat Label="This week" Value="1 284"/>
|
|
251
|
+
<Into Name="footer"><Button Text="Details"/></Into>
|
|
252
|
+
</Card>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`attributes` lists the attributes it takes, with the kind of their prop, checked against the component's props; the others stay with the widget (`Name`, `Width`, `Slot`...), and a value of the wrong kind is reported by Raven. The widgets of the children go where the component renders `children`, and those of `<Into Name="...">` where it renders the prop of that name, one of `slots`, with no box around them. Those it does not render are not shown, and stay alive until the element is destroyed. `Rwidget_set_attribute` renders it again, and destroying the widget unmounts it. Each element is a root of its own, so context does not reach it from around it. `registerTag` returns what unregisters the tag.
|
|
256
|
+
|
|
257
|
+
## Going beyond props
|
|
258
|
+
|
|
259
|
+
`useShortcut(window, "Ctrl+S", save)` adds a shortcut to a window, given the ref of its `<Window>`. `useAction("scene.play", play)` names a function, for whatever fires actions by name; it receives the widget that fired it. An `On...` prop also takes such a name, or a built-in action: `OnClicked="app.quit"`.
|
|
260
|
+
|
|
261
|
+
`useFrame((seconds) => ...)` runs a function once per frame, before the layout, in any number of components. It also runs when nothing is happening, about 60 times a second: `runApp` wakes that often so that Bun's timers run beside Raven. `raven.Rset_frame_callback` goes in the same slot as one more function, so neither takes the place of the other, and the last one it was given is the only one kept alive.
|
|
262
|
+
|
|
263
|
+
`runApp(<App />, options, { appName, setup, allocator })`: `setup` runs after Raven starts and before the first render, for a look or a theme. `allocator` is what `Rset_allocator` takes, functions of C only.
|
|
264
|
+
|
|
265
|
+
`Theme` takes the name of a theme loaded with `loadTheme` (or `Rtheme_load`), and `Language` a language that was registered, beside those the types propose. A color or a metric goes back to the theme with `null`, without making the widget again: `Color={{ Text: null }}`. `FontStyle` on a `Label` takes a style of the font of the theme, and does not go with `Font`.
|
|
266
|
+
|
|
267
|
+
A function given directly to the C API runs outside what props get. `inEventScope(fn)` makes it the same: an exception stays on this side, and what it changes in React is committed before the next event.
|
|
268
|
+
|
|
269
|
+
`createPortal(children, widget)`, as in ReactDOM, renders into a widget given to Raven by hand, the content of a graph node for one, and context goes on through it. `tag<Props>("Name")` makes a component for a tag given to `Rxml_register_tag`.
|
|
270
|
+
|
|
271
|
+
Children added with `Rwidget_add_child` to a parent React also fills stay in their place, through inserts and when the parent is made again. They are yours to delete.
|
|
272
|
+
|
|
273
|
+
Web pages need `registerWebView` once, as the `setup` of `runApp`, which runs before the first render. They are apart from the binding, as a library can be built without them, and `webFunctions()` gives the functions of `raven_web.h`.
|
|
274
|
+
|
|
275
|
+
A `WebView` drives a browser of the Chromium family installed on the machine (Chrome, Chromium, Edge...), hidden, and shows its picture: there is none in the package. So show something else when there is no browser (`Rweb_is_available()` is 0), and when the library has no web pages: built with `-DRAVEN_BUILD_WEB=OFF`, `registerWebView` and `webFunctions()` throw a `RavenError`.
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
/** Whether web pages can be shown, known once `setup` ran. */
|
|
279
|
+
let pages = false;
|
|
280
|
+
|
|
281
|
+
function App(): ReactNode {
|
|
282
|
+
return pages ? <WebView Url="https://example.com" Slot={{ Fill: 1 }} /> : <Label>No browser to show the page in</Label>;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await runApp(<App />, { Width: 800, Height: 600 }, {
|
|
286
|
+
setup: () => {
|
|
287
|
+
try {
|
|
288
|
+
registerWebView();
|
|
289
|
+
pages = webFunctions().Rweb_is_available() !== 0;
|
|
290
|
+
} catch {
|
|
291
|
+
// (a library without web pages)
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The data of a few widgets is given as props. Rows of a `TreeView` have new handles each time `Items` changes. `Markup` holds the elements XML takes inside a widget, and a change to it makes the widget again:
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
<TreeView Items={[{ Text: "World", Expanded: true, Children: [{ Text: "Sky" }] }]} />
|
|
301
|
+
<Timeline Markup={`<Track Name="Camera"><Clip Start="0" Length="3"/></Track>`} />
|
|
302
|
+
<Image Texture={raven.Rtexture_create_from_pixels(pixels, width, height)} />
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
A `Plot` takes its curves in `Series` and the lines the user drags in `DragLines`. The numbers are lists or `Float64Array`s, handed to Raven as they are: a new render sends only what changed, and a new array is what says that values changed. Streaming a million points with `Rplot_add_point` works too: leave `Y` out of that series.
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
<Plot
|
|
309
|
+
Title="Signal" XLabel="t (s)" Legend="TopLeft" YScale="SymLog"
|
|
310
|
+
Series={[{ Name: "wave", X: times, Y: values, Marker: "Circle" }, { Name: "mean", Y: means, Color: "#E5484D", Line: "Dashed" }]}
|
|
311
|
+
DragLines={[{ Axis: "Y", Value: limit, Label: "limit" }]}
|
|
312
|
+
OnChanged={(_line, value) => setLimit(value)}
|
|
313
|
+
OnViewChanged={(view) => console.log(view.X)}
|
|
314
|
+
/>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
A `ref` gives a `WidgetInstance`, and `raven` is all of Raven's C API. Read `.widget` when you need it: the handle changes if the widget is made again.
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
const input = useRef<WidgetInstance>(null);
|
|
321
|
+
useEffect(() => input.current?.focus(), []);
|
|
322
|
+
<TextInput ref={input} />
|
|
323
|
+
|
|
324
|
+
// a TreeView is filled this way
|
|
325
|
+
raven.Rtree_view_add_item(tree.current.widget, raven.R_INVALID_ID, "World");
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
A `Custom` draws with the `Rdraw_` functions. `OnPaint` runs while a frame is drawn, so it must not change state.
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
<Custom
|
|
332
|
+
OnMeasure={() => ({ Width: 400, Height: 200 })}
|
|
333
|
+
OnPaint={(painter, width, height) => raven.Rdraw_rect(painter, 0, 0, width, height, 8, 0.2, 0.3, 0.4, 1)}
|
|
334
|
+
/>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Long lists ask for their rows as they are shown, and a million rows cost nothing:
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
<ListView ItemCount={1_000_000} ItemText={(index) => `Row ${index}`} />
|
|
341
|
+
<TileView ItemCount={200} ItemFill={(index, item) => { item.text = names[index]; }} />
|
|
342
|
+
<Table Columns={["Name", "Age"]} RowCount={people.length} CellText={(row, column) => cell(people[row], column)} />
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
A `NodeGraph` and a `Timeline` take one of Raven's looks, `Preset` "Auto" (the theme), "Modern", "Blueprint", "Blender" or "Neon", and their parts can be changed one by one: `LinkShape`, `PinShape`, `HeaderStyle`, `ClipStyle`, `KeyShape`, `PlayheadColor`... One type of pin, one node or one track takes its own with `Rnode_graph_set_type_style`, `Rnode_graph_set_node_style` and `Rtimeline_set_track_style`.
|
|
346
|
+
|
|
347
|
+
```tsx
|
|
348
|
+
<NodeGraph Preset="Blueprint" LinkShape="Step" />
|
|
349
|
+
<Timeline Preset={look} ClipStyle="Outline" />
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Some things are done through a `ref`, as they are actions and not state. A `Toast` shows one message at a time; with `Queue="Sequence"` the next one waits its turn, with `Queue="Stack"` up to `MaxVisible` show at once.
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
raven.Rtoast_show(toast.current.widget, "Saved", 3);
|
|
356
|
+
raven.Rnotification_show("Backup", "It is finished", raven.R_NOTIFICATION_NORMAL);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
What belongs to the system and not to a window has no component: an icon in the tray (`Rtray_create`, `Rtray_set_menu`, `Rtray_set_clicked_callback`), the badge, progress and menu of the taskbar or the Dock (`Rtaskbar_set_badge`, `Rwindow_set_taskbar_progress`), picture in picture (`Rwindow_set_picture_in_picture`), and the inspector of a window, which lists its widgets and picks one with a click (`Rwindow_set_inspector`). Call them from an effect, and undo them in its cleanup.
|
|
360
|
+
|
|
361
|
+
An application with a tray icon lives on when its last window closes, as a tray application should: `runApp` returns only after `raven.Rquit(0)`, or the action `app.quit`. Give the icon a way to end it, a "Quit" item of its menu for one; the cleanup that destroys the icon still runs then. Make it only where the system shows it, `Rtray_is_supported()`: on Linux an icon is made even when no panel runs to show it, and the application would live on with nothing to click. Without one, closing the last window ends it, as usual.
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
// (on Linux, a panel that shows tray icons must be running)
|
|
366
|
+
const tray = raven.Rtray_is_supported() ? raven.Rtray_create() : raven.R_INVALID_ID;
|
|
367
|
+
if (tray === raven.R_INVALID_ID) {
|
|
368
|
+
return; // (no tray: the application ends with its last window)
|
|
369
|
+
}
|
|
370
|
+
// Its picture, as RGBA pixels: without one, some panels show nothing to click.
|
|
371
|
+
// An orange square here; in practice, the icon of the application.
|
|
372
|
+
const pixels = new Uint8Array(32 * 32 * 4);
|
|
373
|
+
for (let index = 0; index < pixels.length; index += 4) {
|
|
374
|
+
pixels.set([0xe8, 0x59, 0x0c, 0xff], index);
|
|
375
|
+
}
|
|
376
|
+
raven.Rtray_set_icon(tray, pixels, 32, 32);
|
|
377
|
+
raven.Rtray_set_tooltip(tray, "Backup");
|
|
378
|
+
raven.Rtray_set_clicked_callback(tray, () => raven.Rquit(0));
|
|
379
|
+
return () => raven.Rtray_destroy(tray);
|
|
380
|
+
}, []);
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
An `Image` shows an SVG file like any other: `<Image Source="logo.svg" />`.
|
|
384
|
+
|
|
385
|
+
## What to expect
|
|
386
|
+
|
|
387
|
+
- Removing a prop makes the widget again, in its place, with the same children: Raven has no way to give an attribute its default back. What the widget held by itself (focus, typed text, scrolling) is lost. To avoid that, keep the prop and change its value.
|
|
388
|
+
- The designer of Raven does not edit TSX.
|
|
389
|
+
- Style parts and states (`Part`, `State`) only exist in themes: `Styles` of `defineTheme`, not props.
|
|
390
|
+
|
|
391
|
+
## Working on this package
|
|
392
|
+
|
|
393
|
+
The repository is a Bun workspace, with this package in `packages/raven-tsx` and the examples in `examples`: `helloworld` tries everything, `solid-helloworld` is the same with Solid, the others are the examples of Raven in TSX. It expects a Raven checkout two folders up, at `../../raven` from the root:
|
|
394
|
+
|
|
395
|
+
raven/ github.com/oscar-soirey/Raven
|
|
396
|
+
bun/raven-tsx/ this repository
|
|
397
|
+
|
|
398
|
+
Check out the commit of Raven this package is pinned to, build it, then make the native package of your platform from it (`native:local` refuses a library built from any other commit):
|
|
399
|
+
|
|
400
|
+
git -C raven checkout "$(cat bun/raven-tsx/packages/raven-tsx/RAVEN_VERSION)"
|
|
401
|
+
cmake -S raven -B raven/build-shared -DCMAKE_BUILD_TYPE=Release -DRAVEN_BUILD_SHARED=ON
|
|
402
|
+
cmake --build raven/build-shared --parallel
|
|
403
|
+
cd bun/raven-tsx/packages/raven-tsx
|
|
404
|
+
bun run native:local
|
|
405
|
+
bun install
|
|
406
|
+
bun test
|
|
407
|
+
|
|
408
|
+
Then, in `examples/helloworld`: `bun run dev`, or `bun run build`. Every other example runs with `bun run dev` in its folder.
|
|
409
|
+
|
|
410
|
+
| Task | Command |
|
|
411
|
+
|---|---|
|
|
412
|
+
| Regenerate `src/core/props.generated.ts` and `src/react/components.generated.ts` | `bun run generate` |
|
|
413
|
+
| Cross-build `raven.dll` on Linux with MinGW-w64, then `bun install` | `bun run native:windows` |
|
|
414
|
+
| Compile a Windows `.exe` of the example, after that | `bun run build:windows` in `examples/helloworld` |
|
|
415
|
+
| Where things stand, and what changed in Raven's API | `bun run raven:status [ref]` |
|
|
416
|
+
| Check that everything agrees with the pin | `bun run raven:check` |
|
|
417
|
+
| Move to another Raven, or to the latest | `bun run raven:update [ref]` |
|
|
418
|
+
|
|
419
|
+
`RAVEN_VERSION` says which Raven this is, a full commit or a tag, and nothing else does. `raven:update` is the way to change it: it moves the checkout, builds the libraries, packages one, and only then takes the binding, writes the pin and generates the components, since a newer library works with an older binding and the opposite stops every application at startup. It stops at the first step that fails, and can be run again.
|
|
420
|
+
|
|
421
|
+
Nothing in a library says which commit it is: `Rget_version` is the same for every commit between two releases. So `native:local` asks CMake which checkout the build folder belongs to, refuses it unless that checkout is at the pin with no changes, brings the build up to date, and writes `raven.json` beside the library. `--allow-mismatch` packages a library to try a change, and `raven:check` then says so. An application started with a library that lacks what the binding needs says so too, rather than naming one symbol.
|
|
422
|
+
|
|
423
|
+
`src/test/core/coverage.test.ts` fails when Raven adds a tag, an attribute or a callback nobody has decided about: wire it, or say there why not.
|
|
424
|
+
|
|
425
|
+
### Drift
|
|
426
|
+
|
|
427
|
+
`drift.txt` records the git blob of each Raven file that raven-tsx copies or mirrors: the examples, `src/widgets`, `src/widget` and `src/xml`. It catches what an API diff cannot, such as a widget that behaves differently or an example that gained a feature.
|
|
428
|
+
|
|
429
|
+
| Task | Command |
|
|
430
|
+
|---|---|
|
|
431
|
+
| What differs from `drift.txt` at the pin | `bun run raven:drift` |
|
|
432
|
+
| Preview another commit before moving to it | `bun run raven:drift upstream/main` |
|
|
433
|
+
| The same, failing if anything differs | `bun run raven:drift --check` |
|
|
434
|
+
| Record a file or folder as checked, at the pin | `bun run raven:drift --accept examples/music` |
|
|
435
|
+
| Record everything as checked, at the pin | `bun run raven:drift --accept-all` |
|
|
436
|
+
|
|
437
|
+
`raven:status` and `raven:check` include it. A changed file is listed as `old..new`, and `git diff old new` in the Raven checkout shows the change. Accept a file only after you have looked at it: the diff of `drift.txt` in a commit is the record of what was reviewed.
|
|
438
|
+
|
|
439
|
+
Every push builds Raven at `RAVEN_VERSION` on each platform, and runs `raven:check` and the tests there.
|
|
440
|
+
|
|
441
|
+
Publishing is off until the repository variable `NPM_PUBLISH` is `true`; until then the workflow rehearses it with `npm pack --dry-run`. A new version is: set `version` in `package.json`, and push the tag `v<version>`. The packages of the platforms are published first, then this one.
|
|
442
|
+
|
|
443
|
+
npm can only trust a workflow for a package that exists, so the first version needs the secret `NPM_TOKEN`: a granular token that can write to the scope, with bypass 2FA on, as nobody is there to answer a prompt. Delete the secret after that release.
|
|
444
|
+
|
|
445
|
+
Later versions publish through OIDC, with provenance. Just before pushing the next tag, give each of the 6 packages a trusted publisher on npm (repository `deckyfx/raven-tsx`, workflow `release.yml`, environment `npm`, with `npm publish` allowed: by default it may only stage). Limit the GitHub environment `npm` to `v*` tags, so that a run of a branch cannot publish. A new publisher expires unless it publishes within 2 days; if that happens, delete it and make another.
|
package/package.json
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decky.fx/raven-tsx",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Raven's native widgets from TSX, for Bun: a core that knows only Raven and adapters for React and Solid, with typed components made from Raven's schema",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"module": "src/core/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/core/index.ts",
|
|
10
|
+
"./react": "./src/react/index.ts",
|
|
11
|
+
"./solid": "./src/solid/index.ts",
|
|
12
|
+
"./solid/renderer": "./src/solid/renderer.ts",
|
|
13
|
+
"./solid/plugin": "./src/solid/plugin.ts",
|
|
14
|
+
"./solid/preload": "./src/solid/preload.ts"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"bun": ">=1.4.4"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/deckyfx/raven-tsx.git"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"raven",
|
|
25
|
+
"tsx",
|
|
26
|
+
"bun",
|
|
27
|
+
"gui",
|
|
28
|
+
"desktop",
|
|
29
|
+
"native",
|
|
30
|
+
"widgets",
|
|
31
|
+
"react",
|
|
32
|
+
"solid",
|
|
33
|
+
"solidjs",
|
|
34
|
+
"vulkan"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@types/babel__core": "^7.20",
|
|
38
|
+
"@types/react": "^19",
|
|
39
|
+
"@types/react-reconciler": "^0.34"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@babel/core": "^7.20",
|
|
43
|
+
"babel-preset-solid": "^1.9",
|
|
44
|
+
"react": "^19.3.0",
|
|
45
|
+
"react-reconciler": "^0.34.0",
|
|
46
|
+
"solid-js": "^1.9"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"@babel/core": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"babel-preset-solid": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"react": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"react-reconciler": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"solid-js": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"files": [
|
|
66
|
+
"src",
|
|
67
|
+
"RAVEN_VERSION",
|
|
68
|
+
"NOTICE"
|
|
69
|
+
],
|
|
70
|
+
"optionalDependencies": {
|
|
71
|
+
"@decky.fx/raven-tsx-linux-x64": "0.1.1",
|
|
72
|
+
"@decky.fx/raven-tsx-linux-arm64": "0.1.1",
|
|
73
|
+
"@decky.fx/raven-tsx-darwin-arm64": "0.1.1",
|
|
74
|
+
"@decky.fx/raven-tsx-darwin-x64": "0.1.1",
|
|
75
|
+
"@decky.fx/raven-tsx-win32-x64": "0.1.1"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as r from "./raven";
|
|
2
|
+
|
|
3
|
+
/** An error of the renderer or of Raven under it. */
|
|
4
|
+
export class RavenError extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "RavenError";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Throws an error with this message. */
|
|
11
|
+
static throw(message: string): never {
|
|
12
|
+
throw new RavenError(message);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Throws with the message of the last error of the library added. */
|
|
16
|
+
static fromLibrary(context: string): never {
|
|
17
|
+
const [code, detail] = r.Rerror_get_last();
|
|
18
|
+
throw new RavenError(`${context}: ${r.Rerror_to_string(code)}: ${detail}`);
|
|
19
|
+
}
|
|
20
|
+
}
|