@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 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; React is the one there is today. No XML and no build step, as Bun compiles TSX itself.
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
- `react` and `react-reconciler` are optional peer dependencies: `@decky.fx/raven-tsx/react` needs both, and an application that uses only the core installs neither.
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.0",
4
- "description": "Raven's native widgets from TSX, for Bun: a core that knows only Raven and an adapter for React, with typed components made from Raven's schema",
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.0",
53
- "@decky.fx/raven-tsx-linux-arm64": "0.1.0",
54
- "@decky.fx/raven-tsx-darwin-arm64": "0.1.0",
55
- "@decky.fx/raven-tsx-darwin-x64": "0.1.0",
56
- "@decky.fx/raven-tsx-win32-x64": "0.1.0"
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
  }
@@ -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
- /** Gives a function to an action of a name chosen by the application. It receives the sender. */
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 the function of such an action away. */
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
- if (this.handlers.delete(window) && alive) {
771
- r.Rwindow_set_close_callback(window, null);
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.callback ??= new JSCallback(
779
- (id: number): number => {
780
- const close = this.handlers.get(id);
781
- if (!close) {
782
- return 1;
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
- { args: ["u32", "ptr"], returns: "i32" },
788
- );
789
- r.Rwindow_set_close_callback(window, this.callback.ptr);
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
- actionEvents.bind(name, handler);
49
- return () => actionEvents.unbind(name);
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;