@decky.fx/raven-tsx 0.1.0 → 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/README.md +53 -3
- package/package.json +28 -9
- package/src/core/events.ts +74 -18
- package/src/core/subscriptions.ts +4 -2
- package/src/core/widget-instance.ts +333 -24
- package/src/core/window-instance.ts +423 -8
- package/src/react/host-config.ts +1 -1
- 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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# raven-tsx
|
|
2
2
|
|
|
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
|
|
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
4
|
|
|
5
5
|
```tsx
|
|
6
6
|
import { useState } from "react";
|
|
@@ -27,11 +27,13 @@ Every C example of Raven has a TSX port in [examples](../../examples), with scre
|
|
|
27
27
|
|
|
28
28
|
bun add @decky.fx/raven-tsx react react-reconciler
|
|
29
29
|
|
|
30
|
-
|
|
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
31
|
|
|
32
32
|
| Import | What it is |
|
|
33
33
|
|---|---|
|
|
34
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` |
|
|
35
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 |
|
|
36
38
|
|
|
37
39
|
```json
|
|
@@ -63,6 +65,54 @@ For another platform, install its package first:
|
|
|
63
65
|
bun install --os=win32 --cpu=x64
|
|
64
66
|
bun build --compile --target=bun-windows-x64 src/index.tsx --outfile app.exe
|
|
65
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
|
+
|
|
66
116
|
## How props work
|
|
67
117
|
|
|
68
118
|
The components, with their props, are generated from Raven's own schema.
|
|
@@ -340,7 +390,7 @@ An `Image` shows an SVG file like any other: `<Image Source="logo.svg" />`.
|
|
|
340
390
|
|
|
341
391
|
## Working on this package
|
|
342
392
|
|
|
343
|
-
The repository is a Bun workspace, with this package in `packages/raven-tsx` and the examples in `examples`: `helloworld` tries everything, the others are the examples of Raven in TSX. It expects a Raven checkout two folders up, at `../../raven` from the root:
|
|
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:
|
|
344
394
|
|
|
345
395
|
raven/ github.com/oscar-soirey/Raven
|
|
346
396
|
bun/raven-tsx/ this repository
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decky.fx/raven-tsx",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Raven's native widgets from TSX, for Bun: a core that knows only Raven and
|
|
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
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"module": "src/core/index.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/core/index.ts",
|
|
10
|
-
"./react": "./src/react/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"
|
|
11
15
|
},
|
|
12
16
|
"engines": {
|
|
13
17
|
"bun": ">=1.4.4"
|
|
@@ -25,22 +29,37 @@
|
|
|
25
29
|
"native",
|
|
26
30
|
"widgets",
|
|
27
31
|
"react",
|
|
32
|
+
"solid",
|
|
33
|
+
"solidjs",
|
|
28
34
|
"vulkan"
|
|
29
35
|
],
|
|
30
36
|
"dependencies": {
|
|
37
|
+
"@types/babel__core": "^7.20",
|
|
31
38
|
"@types/react": "^19",
|
|
32
39
|
"@types/react-reconciler": "^0.34"
|
|
33
40
|
},
|
|
34
41
|
"peerDependencies": {
|
|
42
|
+
"@babel/core": "^7.20",
|
|
43
|
+
"babel-preset-solid": "^1.9",
|
|
35
44
|
"react": "^19.3.0",
|
|
36
|
-
"react-reconciler": "^0.34.0"
|
|
45
|
+
"react-reconciler": "^0.34.0",
|
|
46
|
+
"solid-js": "^1.9"
|
|
37
47
|
},
|
|
38
48
|
"peerDependenciesMeta": {
|
|
49
|
+
"@babel/core": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"babel-preset-solid": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
39
55
|
"react": {
|
|
40
56
|
"optional": true
|
|
41
57
|
},
|
|
42
58
|
"react-reconciler": {
|
|
43
59
|
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"solid-js": {
|
|
62
|
+
"optional": true
|
|
44
63
|
}
|
|
45
64
|
},
|
|
46
65
|
"files": [
|
|
@@ -49,10 +68,10 @@
|
|
|
49
68
|
"NOTICE"
|
|
50
69
|
],
|
|
51
70
|
"optionalDependencies": {
|
|
52
|
-
"@decky.fx/raven-tsx-linux-x64": "0.1.
|
|
53
|
-
"@decky.fx/raven-tsx-linux-arm64": "0.1.
|
|
54
|
-
"@decky.fx/raven-tsx-darwin-arm64": "0.1.
|
|
55
|
-
"@decky.fx/raven-tsx-darwin-x64": "0.1.
|
|
56
|
-
"@decky.fx/raven-tsx-win32-x64": "0.1.
|
|
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"
|
|
57
76
|
}
|
|
58
77
|
}
|
package/src/core/events.ts
CHANGED
|
@@ -692,6 +692,8 @@ export const COMMON_PROPS: readonly string[] = [
|
|
|
692
692
|
class ActionEvents {
|
|
693
693
|
private readonly handlers = new Map<string, (sender: r.Rid) => void>();
|
|
694
694
|
private readonly nameOf = new Map<r.Rid, string>();
|
|
695
|
+
/** The functions given to each name the application chose, in order: the last one has the name. */
|
|
696
|
+
private readonly named = new Map<string, ((sender: r.Rid) => void)[]>();
|
|
695
697
|
private callback: JSCallback | null = null;
|
|
696
698
|
private next = 0;
|
|
697
699
|
|
|
@@ -722,14 +724,35 @@ class ActionEvents {
|
|
|
722
724
|
this.handlers.set(name, () => (handler as () => void)());
|
|
723
725
|
}
|
|
724
726
|
|
|
725
|
-
/**
|
|
727
|
+
/**
|
|
728
|
+
* Gives a function to an action of a name chosen by the application. It receives the sender.
|
|
729
|
+
* A name given again belongs to the last one, until it stops: then to the one before.
|
|
730
|
+
*/
|
|
726
731
|
bind(name: string, handler: (sender: r.Rid) => void): void {
|
|
732
|
+
const registrations = this.named.get(name) ?? [];
|
|
733
|
+
registrations.push(handler);
|
|
734
|
+
this.named.set(name, registrations);
|
|
727
735
|
this.handlers.set(name, handler);
|
|
728
736
|
r.Raction_bind(name, this.pointer());
|
|
729
737
|
}
|
|
730
738
|
|
|
731
|
-
/** Takes
|
|
732
|
-
unbind(name: string): void {
|
|
739
|
+
/** Takes such a function away: the one given, or all of the name. */
|
|
740
|
+
unbind(name: string, handler?: (sender: r.Rid) => void): void {
|
|
741
|
+
const registrations = this.named.get(name) ?? [];
|
|
742
|
+
if (handler !== undefined) {
|
|
743
|
+
const index = registrations.indexOf(handler);
|
|
744
|
+
if (index < 0) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
registrations.splice(index, 1);
|
|
748
|
+
// (the last one still there has the name again)
|
|
749
|
+
const active = registrations.at(-1);
|
|
750
|
+
if (active) {
|
|
751
|
+
this.handlers.set(name, active);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
this.named.delete(name);
|
|
733
756
|
this.handlers.delete(name);
|
|
734
757
|
r.Raction_unbind(name);
|
|
735
758
|
}
|
|
@@ -761,38 +784,71 @@ export const actionEvents = new ActionEvents();
|
|
|
761
784
|
* the application takes the element out when it agrees. One native callback for all windows.
|
|
762
785
|
*/
|
|
763
786
|
class CloseEvents {
|
|
787
|
+
/** The `OnClose` of each window: it decides, and the window stays open. */
|
|
764
788
|
private readonly handlers = new Map<r.Rid, () => void>();
|
|
789
|
+
/** What is told that a window with no `OnClose` closes, as Raven closes it: its cleanups. */
|
|
790
|
+
private readonly notices = new Map<r.Rid, () => void>();
|
|
765
791
|
private callback: JSCallback | null = null;
|
|
766
792
|
|
|
767
793
|
/** Sets, changes or removes the function of a window. */
|
|
768
794
|
set(window: r.Rid, handler: EventHandler | undefined, alive = true): void {
|
|
795
|
+
const known = this.handlers.has(window) || this.notices.has(window);
|
|
769
796
|
if (!handler) {
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
797
|
+
this.handlers.delete(window);
|
|
798
|
+
this.release(window, known, alive);
|
|
773
799
|
return;
|
|
774
800
|
}
|
|
775
|
-
const known = this.handlers.has(window);
|
|
776
801
|
this.handlers.set(window, handler as () => void);
|
|
777
802
|
if (!known) {
|
|
778
|
-
this.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
803
|
+
this.listen(window);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Sets or removes what is told that a window closes when it has no `OnClose`: the user closed it. */
|
|
808
|
+
notice(window: r.Rid, run: (() => void) | undefined, alive = true): void {
|
|
809
|
+
const known = this.handlers.has(window) || this.notices.has(window);
|
|
810
|
+
if (!run) {
|
|
811
|
+
this.notices.delete(window);
|
|
812
|
+
this.release(window, known, alive);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
this.notices.set(window, run);
|
|
816
|
+
if (!known) {
|
|
817
|
+
this.listen(window);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** Asks Raven for the requests to close a window. */
|
|
822
|
+
private listen(window: r.Rid): void {
|
|
823
|
+
this.callback ??= new JSCallback(
|
|
824
|
+
(id: number): number => {
|
|
825
|
+
const close = this.handlers.get(id);
|
|
826
|
+
if (close) {
|
|
784
827
|
eventScope.run(close);
|
|
785
828
|
return 0;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
829
|
+
}
|
|
830
|
+
// (no OnClose: Raven closes it, once what is told of it has run)
|
|
831
|
+
const notice = this.notices.get(id);
|
|
832
|
+
if (notice) {
|
|
833
|
+
eventScope.run(notice);
|
|
834
|
+
}
|
|
835
|
+
return 1;
|
|
836
|
+
},
|
|
837
|
+
{ args: ["u32", "ptr"], returns: "i32" },
|
|
838
|
+
);
|
|
839
|
+
r.Rwindow_set_close_callback(window, this.callback.ptr);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** Stops asking Raven once a window has neither. */
|
|
843
|
+
private release(window: r.Rid, known: boolean, alive: boolean): void {
|
|
844
|
+
if (known && alive && !this.handlers.has(window) && !this.notices.has(window)) {
|
|
845
|
+
r.Rwindow_set_close_callback(window, null);
|
|
790
846
|
}
|
|
791
847
|
}
|
|
792
848
|
|
|
793
849
|
/** How many windows have a function, for the tests. */
|
|
794
850
|
get size(): number {
|
|
795
|
-
return this.handlers.size;
|
|
851
|
+
return this.handlers.size + this.notices.size;
|
|
796
852
|
}
|
|
797
853
|
}
|
|
798
854
|
|
|
@@ -45,8 +45,10 @@ export function parseKeys(keys: string): { key: r.RKey; modifiers: number } {
|
|
|
45
45
|
* action. Returns what takes the name away.
|
|
46
46
|
*/
|
|
47
47
|
export function onAction(name: string, handler: (sender: r.Rid) => void): () => void {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
// (a function of its own: the same handler given twice is two registrations)
|
|
49
|
+
const registered = (sender: r.Rid): void => handler(sender);
|
|
50
|
+
actionEvents.bind(name, registered);
|
|
51
|
+
return () => actionEvents.unbind(name, registered);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
let nextShortcut = 0;
|