@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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Generated from the schema of Raven by src/tools/generate-components.ts: do not edit.
|
|
2
|
+
import type * as Props from "../core/props.generated";
|
|
3
|
+
import type { WindowInstance } from "../core/window-instance";
|
|
4
|
+
import { tag } from "./tag";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A window of its own, wherever it is in the tree: it opens with its element and closes with it.
|
|
8
|
+
* With `OnClose`, closing is a request: the window stays until the element is taken out.
|
|
9
|
+
*/
|
|
10
|
+
export const Window = tag<Props.WindowProps, WindowInstance>("Window");
|
|
11
|
+
|
|
12
|
+
/** Stacks its children from top to bottom. */
|
|
13
|
+
export const VBox = tag<Props.VBoxProps>("VBox");
|
|
14
|
+
|
|
15
|
+
/** Lines its children up from left to right. */
|
|
16
|
+
export const HBox = tag<Props.HBoxProps>("HBox");
|
|
17
|
+
|
|
18
|
+
/** Places its children in rows and columns (Slot.Column, Slot.Row). */
|
|
19
|
+
export const Grid = tag<Props.GridProps>("Grid");
|
|
20
|
+
|
|
21
|
+
/** Cells of the same size, filled in order. */
|
|
22
|
+
export const UniformGrid = tag<Props.UniformGridProps>("UniformGrid");
|
|
23
|
+
|
|
24
|
+
/** Puts its children on top of each other. */
|
|
25
|
+
export const Overlay = tag<Props.OverlayProps>("Overlay");
|
|
26
|
+
|
|
27
|
+
/** A surface with padding around one child. */
|
|
28
|
+
export const Border = tag<Props.BorderProps>("Border");
|
|
29
|
+
|
|
30
|
+
/** Gives one child a size or a shape. */
|
|
31
|
+
export const SizeBox = tag<Props.SizeBoxProps>("SizeBox");
|
|
32
|
+
|
|
33
|
+
/** Empty space. */
|
|
34
|
+
export const Spacer = tag<Props.SpacerProps>("Spacer");
|
|
35
|
+
|
|
36
|
+
/** Scrolls content that is larger than its place. */
|
|
37
|
+
export const ScrollBox = tag<Props.ScrollBoxProps>("ScrollBox");
|
|
38
|
+
|
|
39
|
+
/** Panes separated by bars the user drags. */
|
|
40
|
+
export const Splitter = tag<Props.SplitterProps>("Splitter");
|
|
41
|
+
|
|
42
|
+
/** Lines its children up and continues on the next line. */
|
|
43
|
+
export const WrapBox = tag<Props.WrapBoxProps>("WrapBox");
|
|
44
|
+
|
|
45
|
+
/** Places its children freely (Slot.Position, Slot.Size, Slot.Anchors). */
|
|
46
|
+
export const Canvas = tag<Props.CanvasProps>("Canvas");
|
|
47
|
+
|
|
48
|
+
/** Shows one of its children. */
|
|
49
|
+
export const Switcher = tag<Props.SwitcherProps>("Switcher");
|
|
50
|
+
|
|
51
|
+
/** Its children as tabs (Slot.Title). */
|
|
52
|
+
export const TabView = tag<Props.TabViewProps>("TabView");
|
|
53
|
+
|
|
54
|
+
/** A header that shows and hides one child. */
|
|
55
|
+
export const Expander = tag<Props.ExpanderProps>("Expander");
|
|
56
|
+
|
|
57
|
+
/** Text. */
|
|
58
|
+
export const Label = tag<Props.LabelProps>("Label");
|
|
59
|
+
|
|
60
|
+
/** A button, with a text or with one child. */
|
|
61
|
+
export const Button = tag<Props.ButtonProps>("Button");
|
|
62
|
+
|
|
63
|
+
/** A box to check, a radio button or a switch. */
|
|
64
|
+
export const Checkbox = tag<Props.CheckboxProps>("Checkbox");
|
|
65
|
+
|
|
66
|
+
/** A value chosen by dragging. */
|
|
67
|
+
export const Slider = tag<Props.SliderProps>("Slider");
|
|
68
|
+
|
|
69
|
+
/** How far something is. */
|
|
70
|
+
export const ProgressBar = tag<Props.ProgressBarProps>("ProgressBar");
|
|
71
|
+
|
|
72
|
+
/** A picture. */
|
|
73
|
+
export const Image = tag<Props.ImageProps>("Image");
|
|
74
|
+
|
|
75
|
+
/** One line of text to type. */
|
|
76
|
+
export const TextInput = tag<Props.TextInputProps>("TextInput");
|
|
77
|
+
|
|
78
|
+
/** A number to type or to step. */
|
|
79
|
+
export const SpinBox = tag<Props.SpinBoxProps>("SpinBox");
|
|
80
|
+
|
|
81
|
+
/** One choice in a list that drops down. */
|
|
82
|
+
export const ComboBox = tag<Props.ComboBoxProps>("ComboBox");
|
|
83
|
+
|
|
84
|
+
/** A line between things. */
|
|
85
|
+
export const Separator = tag<Props.SeparatorProps>("Separator");
|
|
86
|
+
|
|
87
|
+
/** Rows of text, one selected; any number of them. */
|
|
88
|
+
export const ListView = tag<Props.ListViewProps>("ListView");
|
|
89
|
+
|
|
90
|
+
/** Rows inside rows, made of Item elements. */
|
|
91
|
+
export const TreeView = tag<Props.TreeViewProps>("TreeView");
|
|
92
|
+
|
|
93
|
+
/** A grid of tiles, an icon over a name, given by the application (Rtile_view_set_items). */
|
|
94
|
+
export const TileView = tag<Props.TileViewProps>("TileView");
|
|
95
|
+
|
|
96
|
+
/** A bar of Menu elements; on macOS, shown in the menu bar of the system. */
|
|
97
|
+
export const MenuBar = tag<Props.MenuBarProps>("MenuBar");
|
|
98
|
+
|
|
99
|
+
/** A menu: MenuItem, Separator and Menu elements. */
|
|
100
|
+
export const Menu = tag<Props.MenuProps>("Menu");
|
|
101
|
+
|
|
102
|
+
/** An item of a menu. */
|
|
103
|
+
export const MenuItem = tag<Props.MenuItemProps>("MenuItem");
|
|
104
|
+
|
|
105
|
+
/** A place the application draws in with its own renderer. */
|
|
106
|
+
export const Viewport = tag<Props.ViewportProps>("Viewport");
|
|
107
|
+
|
|
108
|
+
/** A widget painted and measured by the application. */
|
|
109
|
+
export const Custom = tag<Props.CustomProps>("Custom");
|
|
110
|
+
|
|
111
|
+
/** Text of several lines: notes, code. */
|
|
112
|
+
export const TextEditor = tag<Props.TextEditorProps>("TextEditor");
|
|
113
|
+
|
|
114
|
+
/** The title bar of a window with a custom header; holds any widgets. */
|
|
115
|
+
export const HeaderBar = tag<Props.HeaderBarProps>("HeaderBar");
|
|
116
|
+
|
|
117
|
+
/** Folders and files to choose from. */
|
|
118
|
+
export const FileBrowser = tag<Props.FileBrowserProps>("FileBrowser");
|
|
119
|
+
|
|
120
|
+
/** Panels the user arranges by dragging their tabs. */
|
|
121
|
+
export const DockArea = tag<Props.DockAreaProps>("DockArea");
|
|
122
|
+
|
|
123
|
+
/** A pill at the top of the window that unfolds: first child folded, second unfolded. */
|
|
124
|
+
export const Island = tag<Props.IslandProps>("Island");
|
|
125
|
+
|
|
126
|
+
/** A panel that shows what is behind it out of focus, around one child. */
|
|
127
|
+
export const BackgroundBlur = tag<Props.BackgroundBlurProps>("BackgroundBlur");
|
|
128
|
+
|
|
129
|
+
/** Chooses a color: square, hues, opacity, code, numbers, swatches. */
|
|
130
|
+
export const ColorPicker = tag<Props.ColorPickerProps>("ColorPicker");
|
|
131
|
+
|
|
132
|
+
/** A sequencer: Track elements holding Clip and Key elements, and Marker elements. */
|
|
133
|
+
export const Timeline = tag<Props.TimelineProps>("Timeline");
|
|
134
|
+
|
|
135
|
+
/** Blocks that snap into each other (Scratch): Kind elements (Name, Label, Shape, Category) and Category elements (Name, Color). */
|
|
136
|
+
export const BlockEditor = tag<Props.BlockEditorProps>("BlockEditor");
|
|
137
|
+
|
|
138
|
+
/** Rows of cells under columns that can be resized and sorted: Column and Row elements; any number of rows. */
|
|
139
|
+
export const Table = tag<Props.TableProps>("Table");
|
|
140
|
+
|
|
141
|
+
/** A date, chosen in a calendar that drops down. */
|
|
142
|
+
export const DatePicker = tag<Props.DatePickerProps>("DatePicker");
|
|
143
|
+
|
|
144
|
+
/** A time of the day: hours, minutes, optionally seconds. */
|
|
145
|
+
export const TimePicker = tag<Props.TimePickerProps>("TimePicker");
|
|
146
|
+
|
|
147
|
+
/** A short message that rises over the content for a few seconds (in an Overlay). */
|
|
148
|
+
export const Toast = tag<Props.ToastProps>("Toast");
|
|
149
|
+
|
|
150
|
+
/** The path to the current place: links separated by chevrons. */
|
|
151
|
+
export const Breadcrumb = tag<Props.BreadcrumbProps>("Breadcrumb");
|
|
152
|
+
|
|
153
|
+
/** A score shown and chosen with stars. */
|
|
154
|
+
export const Rating = tag<Props.RatingProps>("Rating");
|
|
155
|
+
|
|
156
|
+
/** Page numbers with previous and next. */
|
|
157
|
+
export const Pagination = tag<Props.PaginationProps>("Pagination");
|
|
158
|
+
|
|
159
|
+
/** 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). */
|
|
160
|
+
export const Plot = tag<Props.PlotProps>("Plot");
|
|
161
|
+
|
|
162
|
+
/** Nodes linked by wires: Node elements (Input, Output, any widget), Link and Type elements. */
|
|
163
|
+
export const NodeGraph = tag<Props.NodeGraphProps>("NodeGraph");
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* raven-tsx for Solid: the interface in TSX, on Raven's widgets. It exports everything of the
|
|
3
|
+
* core too, so an application needs this one import. Its TSX is compiled for Solid by the plugin
|
|
4
|
+
* of `@decky.fx/raven-tsx/solid/plugin`: `preload = ["@decky.fx/raven-tsx/solid/preload"]` in bunfig.toml.
|
|
5
|
+
*
|
|
6
|
+
* import { createSignal } from "solid-js";
|
|
7
|
+
* import { Button, Label, runApp, VBox } from "@decky.fx/raven-tsx/solid";
|
|
8
|
+
*
|
|
9
|
+
* function App() {
|
|
10
|
+
* const [count, setCount] = createSignal(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
|
+
export * from "../core";
|
|
23
|
+
export * from "./components.generated";
|
|
24
|
+
export { Portal } from "./portal";
|
|
25
|
+
export { createAction, createFrame, createShortcut } from "./primitives";
|
|
26
|
+
export { createWindow, RavenRoot, runApp, type RunOptions } from "./root";
|
|
27
|
+
export { tag, type SolidProps } from "./tag";
|
|
28
|
+
export { registerTag, type TagOptions } from "./tags";
|
|
29
|
+
export { WebView } from "./web";
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Solid's renderer works on: an element over a node of the core (a widget, a window), or a
|
|
3
|
+
* text its parent shows as `Text`. Solid makes a node first and gives it props one by one, before
|
|
4
|
+
* and after it is placed; the core gets them all each time, as it does from React.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getOwner, onCleanup } from "solid-js";
|
|
8
|
+
import { RavenError } from "../core/errors";
|
|
9
|
+
import type * as r from "../core/raven";
|
|
10
|
+
import type { RavenProps } from "../core/props";
|
|
11
|
+
import { finishPlacing, WidgetInstance, type HostNode } from "../core/widget-instance";
|
|
12
|
+
import { WindowInstance } from "../core/window-instance";
|
|
13
|
+
|
|
14
|
+
/** The props of a widget under Solid. Its children are the nodes, not a prop. */
|
|
15
|
+
type SolidProps = Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
/** A `<Separator>` in a `<Menu>`: a line of the menu, as in XML, which is an item there and no widget of its own. */
|
|
18
|
+
const menuSeparator = (props: SolidProps): SolidProps => ({ ...(props["Name"] === undefined ? {} : { Name: props["Name"] }), Separator: true });
|
|
19
|
+
|
|
20
|
+
export class SolidNode {
|
|
21
|
+
parent: SolidNode | null = null;
|
|
22
|
+
readonly children: SolidNode[] = [];
|
|
23
|
+
private readonly props: SolidProps = {};
|
|
24
|
+
/**
|
|
25
|
+
* Whether what made it in Solid is gone. Until then, a node taken out of the tree stays alive:
|
|
26
|
+
* Solid may place it again (`<Show>` of an element kept in a variable). After, it goes when out.
|
|
27
|
+
*/
|
|
28
|
+
private ownerGone: boolean;
|
|
29
|
+
private destroyed = false;
|
|
30
|
+
/** The top of a tree: its widget is the application's, which a tree never changes. */
|
|
31
|
+
private isContainer = false;
|
|
32
|
+
/** A child that waits for the node it replaces to go: see `insert`. */
|
|
33
|
+
private held: { readonly child: SolidNode; readonly before: SolidNode } | null = null;
|
|
34
|
+
/** What waits for the node of the core: the `ref` of a `<Separator>` not placed yet. */
|
|
35
|
+
private readonly waiting: ((instance: HostNode) => void)[] = [];
|
|
36
|
+
|
|
37
|
+
private constructor(
|
|
38
|
+
readonly tag: string,
|
|
39
|
+
private host: HostNode | null,
|
|
40
|
+
/** The text of a text node; null for an element. */
|
|
41
|
+
public text: string | null,
|
|
42
|
+
/** Whether Solid owns it: the top of a tree is the application's, and stays when Solid's owner goes. */
|
|
43
|
+
owned = true,
|
|
44
|
+
) {
|
|
45
|
+
const owner = owned ? getOwner() : null;
|
|
46
|
+
this.ownerGone = owned && owner === null;
|
|
47
|
+
if (owner) {
|
|
48
|
+
onCleanup(() => this.leaveOwner());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** An element of a tag. A `<Separator>` waits for its parent: in a `<Menu>`, it is a line of the menu. */
|
|
53
|
+
static element(tag: string): SolidNode {
|
|
54
|
+
const host = tag === "Separator" ? null : tag === "Window" ? WindowInstance.create({}) : WidgetInstance.create(tag, {});
|
|
55
|
+
return new SolidNode(tag, host, null);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static text(value: string): SolidNode {
|
|
59
|
+
return new SolidNode("", null, value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The top of a tree: a widget the application owns, or nothing, for a tree of windows. */
|
|
63
|
+
static container(widget: r.Rid): SolidNode {
|
|
64
|
+
const node = new SolidNode("", WidgetInstance.forContainer(widget), null, false);
|
|
65
|
+
node.isContainer = true;
|
|
66
|
+
return node;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A widget Raven made, a child of a registered tag in XML, which is only placed: see `WidgetInstance.adopt`. */
|
|
70
|
+
static adopted(widget: r.Rid): SolidNode {
|
|
71
|
+
return new SolidNode("", WidgetInstance.adopt(widget), null);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get isText(): boolean {
|
|
75
|
+
return this.text !== null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The node of the core. A `<Separator>` gets it when it is placed: in a `<Menu>`, a line of the menu. */
|
|
79
|
+
private get instance(): HostNode {
|
|
80
|
+
if (!this.host) {
|
|
81
|
+
const inMenu = this.parent?.tag === "Menu";
|
|
82
|
+
const host = inMenu ? WidgetInstance.create("MenuItem", menuSeparator(this.props)) : WidgetInstance.create(this.tag, this.props);
|
|
83
|
+
this.host = host;
|
|
84
|
+
for (const run of this.waiting.splice(0)) {
|
|
85
|
+
run(host);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return this.host;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Gives the node of the core to a function: now, or once it exists (a `<Separator>`, when it is placed). */
|
|
92
|
+
withInstance(run: (instance: HostNode) => void): void {
|
|
93
|
+
if (this.host) {
|
|
94
|
+
run(this.host);
|
|
95
|
+
} else {
|
|
96
|
+
this.waiting.push(run);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Sets a prop, or takes it away with `undefined`. */
|
|
101
|
+
setProp(name: string, value: unknown): void {
|
|
102
|
+
if (value === undefined) {
|
|
103
|
+
delete this.props[name];
|
|
104
|
+
} else {
|
|
105
|
+
this.props[name] = value;
|
|
106
|
+
}
|
|
107
|
+
this.refresh();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Gives the node of the core all the props, with the text of the children. */
|
|
111
|
+
refresh(): void {
|
|
112
|
+
// (the widget of a container is not the tree's: nothing of its props is set)
|
|
113
|
+
if (!this.host || this.destroyed || this.isContainer) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const props = this.host.tag === "MenuItem" && this.tag === "Separator" ? menuSeparator(this.props) : this.props;
|
|
117
|
+
this.host.update({ ...props, ...this.textChildren() } as RavenProps<string>);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The text of the children, as React gives it: text alone in a widget is its `Text`. Solid
|
|
122
|
+
* places an empty text where a condition shows nothing: that is no text beside widgets.
|
|
123
|
+
*/
|
|
124
|
+
private textChildren(): { readonly children?: readonly string[] } {
|
|
125
|
+
const texts = this.children.filter((child) => child.isText);
|
|
126
|
+
if (texts.length === 0) {
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
if (texts.length < this.children.length) {
|
|
130
|
+
const shown = texts.find((child) => child.text !== "");
|
|
131
|
+
if (shown) {
|
|
132
|
+
RavenError.throw(`raven-tsx: text must be alone in a widget: "${shown.text}"`);
|
|
133
|
+
}
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
return { children: texts.map((child) => child.text ?? "") };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Changes the text of a text node, which its parent shows as `Text`. Refused text changes nothing. */
|
|
140
|
+
setText(value: string): void {
|
|
141
|
+
const siblings = this.parent?.children ?? [];
|
|
142
|
+
if (value !== "" && this.parent?.isContainer) {
|
|
143
|
+
RavenError.throw(`raven-tsx: text must be in a widget: "${value}"`);
|
|
144
|
+
}
|
|
145
|
+
if (value !== "" && siblings.some((node) => !node.isText)) {
|
|
146
|
+
RavenError.throw(`raven-tsx: text must be alone in a widget: "${value}"`);
|
|
147
|
+
}
|
|
148
|
+
this.text = value;
|
|
149
|
+
this.parent?.refresh();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Gives the Text again when widgets came or went beside an empty text: see `textChildren`. */
|
|
153
|
+
private refreshTextOf(): void {
|
|
154
|
+
if (this.children.some((node) => node.isText)) {
|
|
155
|
+
this.refresh();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** What conflicts with a child here: the widgets beside a text, or the texts beside a widget. */
|
|
160
|
+
private conflictsOf(child: SolidNode): SolidNode[] {
|
|
161
|
+
const others = this.children.filter((node) => node !== child);
|
|
162
|
+
if (child.isText) {
|
|
163
|
+
return child.text !== "" ? others.filter((node) => !node.isText) : [];
|
|
164
|
+
}
|
|
165
|
+
return others.filter((node) => node.isText && node.text !== "");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Text beside widgets is an error, as with React: said before anything changes, whichever comes first. */
|
|
169
|
+
private checkMixed(child: SolidNode): void {
|
|
170
|
+
// (as with React: text at the top of a tree, in a root or a portal, has no widget to be the Text of)
|
|
171
|
+
if (this.isContainer && child.isText && child.text !== "") {
|
|
172
|
+
RavenError.throw(`raven-tsx: text must be in a widget: "${child.text}"`);
|
|
173
|
+
}
|
|
174
|
+
const others = this.children.filter((node) => node !== child);
|
|
175
|
+
const text = child.isText ? (child.text !== "" && others.some((node) => !node.isText) ? child.text : null) : (others.find((node) => node.isText && node.text !== "")?.text ?? null);
|
|
176
|
+
if (text !== null) {
|
|
177
|
+
RavenError.throw(`raven-tsx: text must be alone in a widget: "${text}"`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Places a child before another one, or at the end. A child already in a tree moves. */
|
|
182
|
+
insert(child: SolidNode, before: SolidNode | null): void {
|
|
183
|
+
if (child === before) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.placeHeld();
|
|
187
|
+
// (Solid replaces a node by putting the new one before it, then taking it away: a widget in
|
|
188
|
+
// place of a text, `{busy() ? <Spinner /> : "Save"}`. Side by side they would be refused: the
|
|
189
|
+
// new one waits for that, see `release`. Anything else first places it as it is, refused or not)
|
|
190
|
+
if (before && !this.isContainer && this.children.includes(before)) {
|
|
191
|
+
const conflicts = this.conflictsOf(child);
|
|
192
|
+
if (conflicts.length === 1 && conflicts[0] === before) {
|
|
193
|
+
this.held = { child, before };
|
|
194
|
+
queueMicrotask(() => this.placeHeldLater());
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
this.place(child, before);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Places a child that waited for the node it replaces to go, as it is: one beside text is refused. */
|
|
202
|
+
private placeHeld(): void {
|
|
203
|
+
const held = this.held;
|
|
204
|
+
if (!held) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.held = null;
|
|
208
|
+
this.place(held.child, this.children.includes(held.before) ? held.before : null);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Places a child still waiting at the end of the task: Solid replaces in one go, so it was no replacement. */
|
|
212
|
+
private placeHeldLater(): void {
|
|
213
|
+
if (this.destroyed) {
|
|
214
|
+
this.held = null;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
this.placeHeld();
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error("raven-tsx:", error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Places a child, as `insert` does, with no waiting. */
|
|
225
|
+
private place(child: SolidNode, before: SolidNode | null): void {
|
|
226
|
+
this.checkMixed(child);
|
|
227
|
+
const moved = child.parent !== null && child.parent !== this;
|
|
228
|
+
if (child.parent && moved) {
|
|
229
|
+
// (a move: the child stays alive on the way, whatever its owner, and its windows open)
|
|
230
|
+
child.parent.detach(child, false);
|
|
231
|
+
}
|
|
232
|
+
const current = this.children.indexOf(child);
|
|
233
|
+
if (current >= 0) {
|
|
234
|
+
this.children.splice(current, 1);
|
|
235
|
+
}
|
|
236
|
+
const index = before ? this.children.indexOf(before) : -1;
|
|
237
|
+
this.children.splice(index < 0 ? this.children.length : index, 0, child);
|
|
238
|
+
child.parent = this;
|
|
239
|
+
if (child.isText) {
|
|
240
|
+
this.refresh();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const host = this.instance;
|
|
244
|
+
try {
|
|
245
|
+
// (making a `<Separator>` runs its ref, which may throw, or take the child away, or delete
|
|
246
|
+
// it or this, or put others beside it: what comes after it is read after)
|
|
247
|
+
const childHost = child.instance;
|
|
248
|
+
if (child.parent !== this || child.destroyed || this.destroyed) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const next = this.children.slice(this.children.indexOf(child) + 1).find((node) => !node.isText);
|
|
252
|
+
const beforeHost = next?.instance;
|
|
253
|
+
if (beforeHost) {
|
|
254
|
+
host.insertBefore(childHost, beforeHost);
|
|
255
|
+
} else {
|
|
256
|
+
host.append(childHost);
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
// (user code that ran meanwhile placed it elsewhere: it is that parent's, as it is)
|
|
260
|
+
if (child.parent !== this) {
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
// (it is not in this tree: nor in the core, which keeps one placed with a failure, a window
|
|
264
|
+
// open whose function threw. It closes there, and goes if nothing in Solid owns it)
|
|
265
|
+
const index = this.children.indexOf(child);
|
|
266
|
+
if (index >= 0) {
|
|
267
|
+
this.children.splice(index, 1);
|
|
268
|
+
}
|
|
269
|
+
child.parent = null;
|
|
270
|
+
try {
|
|
271
|
+
if (child.host) {
|
|
272
|
+
host.release(child.host);
|
|
273
|
+
child.host.closeWindows();
|
|
274
|
+
}
|
|
275
|
+
} catch (cleanup) {
|
|
276
|
+
console.error("raven-tsx: cleaning up a child that failed:", cleanup);
|
|
277
|
+
} finally {
|
|
278
|
+
// (also when closing it threw: nothing else would delete one that no owner keeps)
|
|
279
|
+
if (child.ownerGone && child.parent === null) {
|
|
280
|
+
try {
|
|
281
|
+
child.destroy();
|
|
282
|
+
} catch (cleanup) {
|
|
283
|
+
console.error("raven-tsx: cleaning up a child that failed:", cleanup);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
// (moved to a parent not placed: its windows close, to open with it)
|
|
290
|
+
if (moved && !host.placed) {
|
|
291
|
+
child.host?.closeWindows();
|
|
292
|
+
}
|
|
293
|
+
// (an empty text beside widgets is no Text, and alone it is: the Text follows what is here)
|
|
294
|
+
this.refreshTextOf();
|
|
295
|
+
// (what was made now has its parent: the attributes of its place go to it)
|
|
296
|
+
finishPlacing();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Takes a child out, alive, wherever it goes next. Its windows close, unless it moves to another place now. */
|
|
300
|
+
private detach(child: SolidNode, closing = true): void {
|
|
301
|
+
const index = this.children.indexOf(child);
|
|
302
|
+
if (index < 0) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.children.splice(index, 1);
|
|
306
|
+
child.parent = null;
|
|
307
|
+
if (child.isText) {
|
|
308
|
+
this.refresh();
|
|
309
|
+
} else if (child.host) {
|
|
310
|
+
this.instance.release(child.host);
|
|
311
|
+
this.refreshTextOf();
|
|
312
|
+
// (a window is no widget of its parent: out of the tree, it closes, and those it holds, to open when placed again)
|
|
313
|
+
if (closing) {
|
|
314
|
+
child.host.closeWindows();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Takes a child out of the tree. It stays alive while what made it in Solid is there, and goes after. */
|
|
320
|
+
release(child: SolidNode): void {
|
|
321
|
+
// (the node a waiting child replaces goes: the child takes its place)
|
|
322
|
+
if (this.held?.before === child) {
|
|
323
|
+
const replacement = this.held.child;
|
|
324
|
+
this.held = null;
|
|
325
|
+
const after = this.children[this.children.indexOf(child) + 1] ?? null;
|
|
326
|
+
this.releaseNow(child);
|
|
327
|
+
this.place(replacement, after && this.children.includes(after) ? after : null);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.placeHeld();
|
|
331
|
+
this.releaseNow(child);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Takes a child out of the tree, as `release` does. */
|
|
335
|
+
private releaseNow(child: SolidNode): void {
|
|
336
|
+
if (!this.children.includes(child)) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
this.detach(child);
|
|
341
|
+
} finally {
|
|
342
|
+
// (also when closing its windows threw: it is out of the tree. Unless a cleanup of its
|
|
343
|
+
// windows placed it elsewhere: it is that parent's now)
|
|
344
|
+
if (child.ownerGone && child.parent === null) {
|
|
345
|
+
child.destroy();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Takes every child out, each going or staying as its owner says: a portal that goes. */
|
|
351
|
+
releaseAll(): void {
|
|
352
|
+
this.releaseEach(false);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Takes every child out and deletes them: the end of a tree, after its owners are gone. */
|
|
356
|
+
clear(): void {
|
|
357
|
+
this.releaseEach(true);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Takes every child out, all of them, and then says the first failure. */
|
|
361
|
+
private releaseEach(deleting: boolean): void {
|
|
362
|
+
// (a child waiting to replace another is not placed: its owner deletes it)
|
|
363
|
+
this.held = null;
|
|
364
|
+
const failures: unknown[] = [];
|
|
365
|
+
for (const child of [...this.children]) {
|
|
366
|
+
try {
|
|
367
|
+
// (one that a cleanup moved to another parent meanwhile is that one's, owner and all)
|
|
368
|
+
if (deleting && child.parent === this) {
|
|
369
|
+
child.ownerGone = true;
|
|
370
|
+
}
|
|
371
|
+
this.release(child);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
failures.push(error);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (failures.length > 0) {
|
|
377
|
+
throw failures[0];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** What made it in Solid is gone: it goes now, unless it is still in a tree, which deletes it with itself. */
|
|
382
|
+
private leaveOwner(): void {
|
|
383
|
+
this.ownerGone = true;
|
|
384
|
+
if (!this.parent) {
|
|
385
|
+
this.destroy();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Deletes it in Raven, with what it holds. */
|
|
390
|
+
private destroy(): void {
|
|
391
|
+
if (this.destroyed || !this.host) {
|
|
392
|
+
this.destroyed = true;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
this.destroyed = true;
|
|
396
|
+
const host = this.host;
|
|
397
|
+
try {
|
|
398
|
+
// (a child whose owner is still there leaves alive: it may be shown again; the others go)
|
|
399
|
+
this.releaseAll();
|
|
400
|
+
} finally {
|
|
401
|
+
host.forgetTree();
|
|
402
|
+
host.dispose();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|