@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.
Files changed (43) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +12 -0
  3. package/RAVEN_VERSION +1 -0
  4. package/README.md +444 -2
  5. package/package.json +75 -4
  6. package/src/core/errors.ts +20 -0
  7. package/src/core/events.ts +925 -0
  8. package/src/core/frame.ts +76 -0
  9. package/src/core/index.ts +28 -0
  10. package/src/core/library-check.ts +89 -0
  11. package/src/core/native.ts +82 -0
  12. package/src/core/plot.ts +179 -0
  13. package/src/core/props.generated.ts +172 -0
  14. package/src/core/props.ts +158 -0
  15. package/src/core/raven.ts +10 -0
  16. package/src/core/subscriptions.ts +78 -0
  17. package/src/core/tags.ts +307 -0
  18. package/src/core/theme.ts +207 -0
  19. package/src/core/web.ts +84 -0
  20. package/src/core/widget-instance.ts +1057 -0
  21. package/src/core/window-instance.ts +671 -0
  22. package/src/react/components.generated.ts +179 -0
  23. package/src/react/hooks.ts +51 -0
  24. package/src/react/host-config.ts +138 -0
  25. package/src/react/index.ts +26 -0
  26. package/src/react/root.ts +119 -0
  27. package/src/react/tags.ts +50 -0
  28. package/src/react/web.ts +5 -0
  29. package/src/solid/babel-preset-solid.d.ts +7 -0
  30. package/src/solid/components.generated.ts +163 -0
  31. package/src/solid/index.ts +29 -0
  32. package/src/solid/node.ts +405 -0
  33. package/src/solid/plugin.ts +108 -0
  34. package/src/solid/portal.ts +27 -0
  35. package/src/solid/preload.ts +14 -0
  36. package/src/solid/primitives.ts +43 -0
  37. package/src/solid/renderer.ts +37 -0
  38. package/src/solid/root.ts +137 -0
  39. package/src/solid/tag.ts +37 -0
  40. package/src/solid/tags.ts +101 -0
  41. package/src/solid/teardown.ts +50 -0
  42. package/src/solid/web.ts +5 -0
  43. package/src/vendor/raven.ts +12652 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The one slot Raven has for a function called at each frame, shared. `onFrame` and
3
+ * `Rset_frame_callback` of the application both end up in one native callback, which goes away
4
+ * when nothing needs it. Without this, each would take the place of the others, and the binding
5
+ * would keep every function it was given alive for ever.
6
+ */
7
+
8
+ import "./native";
9
+ import { CFunction, JSCallback, ptr, type Pointer } from "bun:ffi";
10
+ import { Rset_frame_callback as connectNative, type RFrameCallback, type RPointer } from "../vendor/raven";
11
+
12
+ /** What is called at each frame, with the seconds since the last one. */
13
+ export type FrameHandler = (deltaSeconds: number) => void;
14
+
15
+ class FrameSlot {
16
+ private readonly subscribers = new Set<FrameHandler>();
17
+ private application: FrameHandler | null = null;
18
+ private callback: JSCallback | null = null;
19
+ private connected = false;
20
+
21
+ /** Connects the native callback when something needs it, and takes it away when nothing does. */
22
+ private sync(): void {
23
+ const wanted = this.application !== null || this.subscribers.size > 0;
24
+ if (wanted !== this.connected) {
25
+ this.callback ??= new JSCallback((delta: number): void => this.dispatch(delta), { args: ["f64", "ptr"], returns: "void" });
26
+ connectNative(wanted ? this.callback.ptr : null);
27
+ this.connected = wanted;
28
+ }
29
+ }
30
+
31
+ /** An exception must not cross the frames of C, nor keep the others from being called. */
32
+ private dispatch(delta: number): void {
33
+ for (const handler of [...(this.application ? [this.application] : []), ...this.subscribers]) {
34
+ try {
35
+ handler(delta);
36
+ } catch (error) {
37
+ console.error("raven-tsx: exception in a frame callback:", error);
38
+ }
39
+ }
40
+ }
41
+
42
+ /** What `Rset_frame_callback` does: one function, replaced by the next, beside the subscribers. */
43
+ setApplication(callback: RFrameCallback | Pointer | null, userData: RPointer): void {
44
+ if (typeof callback === "number") {
45
+ // a function of C: called from here, as it no longer has the slot to itself
46
+ const native = CFunction({ ptr: callback, args: ["f64", "ptr"], returns: "void" });
47
+ this.application = (delta) => void native(delta, userData);
48
+ } else if (callback) {
49
+ // an address, as C would give it. An array stays alive for as long as this function does.
50
+ this.application = (delta) => void callback(delta, userData === null || typeof userData === "number" ? userData : ptr(userData));
51
+ } else {
52
+ this.application = null;
53
+ }
54
+ this.sync();
55
+ }
56
+
57
+ /** Adds a function, and returns what takes it away. */
58
+ subscribe(handler: FrameHandler): () => void {
59
+ this.subscribers.add(handler);
60
+ this.sync();
61
+ return () => {
62
+ this.subscribers.delete(handler);
63
+ this.sync();
64
+ };
65
+ }
66
+ }
67
+
68
+ export const frameSlot = new FrameSlot();
69
+
70
+ /**
71
+ * Registers a function called once per frame, before the layout, beside what `onFrame` added.
72
+ * Takes the place of the one of the C API, under the same name.
73
+ */
74
+ export function Rset_frame_callback(callback: RFrameCallback | Pointer | null, user_data: RPointer = null): void {
75
+ frameSlot.setApplication(callback, user_data);
76
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * raven-tsx: Raven's widgets from TSX. This is the core, which knows only Raven and is what the
3
+ * adapters are made on: an application writes its components with one (`@decky.fx/raven-tsx/react`,
4
+ * which also exports everything here), and imports this for code that is not a component:
5
+ *
6
+ * import { defineTheme, loadTheme, raven } from "@decky.fx/raven-tsx";
7
+ *
8
+ * What an adapter needs beyond this (`finishPlacing`, `registerTagWith`, `WidgetInstance`...) is in
9
+ * the modules of this folder: it is for this package, not for applications.
10
+ */
11
+
12
+ export type * from "./props.generated";
13
+ export { RavenError } from "./errors";
14
+ export { inEventScope, type MeasuredSize, type Painter, type PlotView } from "./events";
15
+ export type { FrameHandler } from "./frame";
16
+ export type { AttributeGroup, AttributeValue, EventHandler, PlotDragLine, PlotNumbers, PlotSeries, TreeItem } from "./props";
17
+ export { onAction, onFrame, onShortcut, parseKeys } from "./subscriptions";
18
+ export type { AttributeKind } from "./tags";
19
+ export type { WidgetInstance } from "./widget-instance";
20
+ export type { WindowInstance } from "./window-instance";
21
+ /** All of Raven's C API, for what props do not cover: painting, for one. */
22
+ export * as raven from "./raven";
23
+ export { registerWebView, webFunctions, type WebViewProps } from "./web";
24
+ export {
25
+ defineTheme, loadTheme, themeXml,
26
+ type GlassProperty, type LookName, type PartProperties, type PartShape, type SyntaxToken, type ThemeColor, type ThemeColors,
27
+ type ThemeMetrics, type ThemePart, type ThemeSpec, type ThemeState, type ThemeStyle,
28
+ } from "./theme";
@@ -0,0 +1,89 @@
1
+ /**
2
+ * What a native library is, and whether it fits the binding. Raven cannot say by itself:
3
+ * `Rget_version` is the version in its CMakeLists, the same for every commit between two
4
+ * releases, and no commit is compiled in. So `raven.json` goes beside a library when it is
5
+ * packaged, and the functions the binding opens can be looked for in the file.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { dirname, resolve } from "node:path";
10
+
11
+ /** The file beside a packaged library that says what it is. */
12
+ export const STAMP_FILE = "raven.json";
13
+
14
+ /** What `raven.json` holds. */
15
+ export interface LibraryStamp {
16
+ /** The commit of Raven it was built from, or an empty text when that is not known. */
17
+ readonly commit: string;
18
+ /** What `RAVEN_VERSION` said when it was packaged. */
19
+ readonly pin: string;
20
+ /** What `Rget_version` says. */
21
+ readonly version: string;
22
+ /** The SHA-256 of the library, to see that it is still that one. */
23
+ readonly sha256: string;
24
+ /** False when it was packaged with `--allow-mismatch`. */
25
+ readonly verified: boolean;
26
+ }
27
+
28
+ /** The stamp in a folder, if it has one that can be read. */
29
+ export function readStamp(directory: string): LibraryStamp | null {
30
+ try {
31
+ return JSON.parse(readFileSync(`${directory}/${STAMP_FILE}`, "utf8")) as LibraryStamp;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /** The names of the C functions a binding opens. */
38
+ export function functionsOfBinding(binding: string): string[] {
39
+ return Array.from(binding.matchAll(/^\tR[a-z][a-z0-9_]*(?=: \{ args: )/gm), (match) => match[0].slice(1));
40
+ }
41
+
42
+ /**
43
+ * The functions a library does not have, from the bytes of the file: its table of exports
44
+ * holds them by name on Linux, macOS and Windows. A name in the file can be something else,
45
+ * so an empty answer proves nothing and the system still has the last word, but a name that is
46
+ * nowhere in the file is really missing. With most of them missing this is no such file.
47
+ */
48
+ export function missingFunctions(functions: readonly string[], library: Buffer): string[] {
49
+ const found = new Set(library.toString("latin1").match(/R[a-z][a-z0-9_]*/g));
50
+ const missing = functions.filter((name) => !found.has(name));
51
+ return missing.length > functions.length / 2 ? [] : missing;
52
+ }
53
+
54
+ /** A commit as it is shown. */
55
+ export function shortCommit(commit: string): string {
56
+ return /^[0-9a-f]{40}$/.test(commit) ? commit.slice(0, 7) : commit;
57
+ }
58
+
59
+ /**
60
+ * Says what is wrong with a library before the binding opens it, as all the system would say is
61
+ * that one symbol was not found. Nothing happens in an executable, which holds what it was
62
+ * built with, or when the stamp says the library was packaged for this pin and checked:
63
+ * `--allow-mismatch` writes the pin too, without having checked anything.
64
+ */
65
+ export function checkLibrary(library: string): void {
66
+ const binding = `${import.meta.dir}/../vendor/raven.ts`;
67
+ if (!existsSync(binding) || !existsSync(library)) {
68
+ return;
69
+ }
70
+ const pinFile = resolve(import.meta.dir, "../../RAVEN_VERSION");
71
+ const pin = existsSync(pinFile) ? readFileSync(pinFile, "utf8").trim() : "";
72
+ const stamp = readStamp(dirname(library));
73
+ if (stamp?.verified && stamp.pin === pin) {
74
+ return;
75
+ }
76
+ const built = stamp?.commit ? `built from Raven ${shortCommit(stamp.commit)}` : "built from a commit it cannot say";
77
+ const missing = missingFunctions(functionsOfBinding(readFileSync(binding, "utf8")), readFileSync(library));
78
+ if (missing.length > 0) {
79
+ const named = `${missing.slice(0, 3).join(", ")}${missing.length > 3 ? ` and ${missing.length - 3} more` : ""}`;
80
+ throw new Error(
81
+ `raven-tsx: the library ${library} is older than the binding: it has no ${named}.\n` +
82
+ `The binding is pinned to Raven ${shortCommit(pin)}, and the library was ${built}.\n` +
83
+ `In packages/raven-tsx, bun run raven:status says what differs, and bun run native:local packages a library of the pin.`,
84
+ );
85
+ }
86
+ if (stamp) {
87
+ console.warn(`raven-tsx: the library was ${built}, and RAVEN_VERSION says ${shortCommit(pin)}: bun run raven:status says what differs`);
88
+ }
89
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Finds Raven's native library and says where it is, before the binding opens it.
3
+ *
4
+ * It comes in a package per platform, listed in `optionalDependencies`: the package manager
5
+ * installs the one that fits, without an install script (Bun does not run those by default).
6
+ * `RAVEN_LIBRARY` set by hand has priority, to try another build.
7
+ */
8
+
9
+ import { readFileSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { checkLibrary } from "./library-check";
12
+
13
+ /** What a package of a platform exports: the path of its library. */
14
+ interface PlatformPackage {
15
+ readonly default: string;
16
+ }
17
+
18
+ /**
19
+ * The library of this platform, or nothing when its package is not installed.
20
+ *
21
+ * These `require` calls are literal, and inside `try`, on purpose: `bun build --compile` embeds
22
+ * what it can resolve, and takes an unresolved `require` in a `try` as optional. An `import()`
23
+ * of a package of another platform would stop the build.
24
+ */
25
+ function findPackagedLibrary(): string | undefined {
26
+ let found: PlatformPackage | undefined;
27
+ try {
28
+ switch (`${process.platform}-${process.arch}`) {
29
+ case "linux-x64":
30
+ found = require("@decky.fx/raven-tsx-linux-x64");
31
+ break;
32
+ case "linux-arm64":
33
+ found = require("@decky.fx/raven-tsx-linux-arm64");
34
+ break;
35
+ case "darwin-arm64":
36
+ found = require("@decky.fx/raven-tsx-darwin-arm64");
37
+ break;
38
+ case "darwin-x64":
39
+ found = require("@decky.fx/raven-tsx-darwin-x64");
40
+ break;
41
+ case "win32-x64":
42
+ found = require("@decky.fx/raven-tsx-win32-x64");
43
+ break;
44
+ }
45
+ } catch {
46
+ // not installed: said below, with what to do about it
47
+ }
48
+ return found?.default;
49
+ }
50
+
51
+ /**
52
+ * A path the system can load. In a Windows executable the library is inside the file, at
53
+ * `B:/~BUN/...`, which `dlopen` cannot open: it is written to the temporary folder first,
54
+ * under a name made from its content. Bun does this by itself on Linux.
55
+ *
56
+ * Synchronous on purpose: a top-level `await` here would not hold back the binding, which
57
+ * is imported beside this module and does not depend on it.
58
+ */
59
+ function loadablePath(path: string): string {
60
+ if (!path.startsWith("B:/~BUN/")) {
61
+ return path;
62
+ }
63
+ const bytes = readFileSync(path);
64
+ const extracted = `${tmpdir()}\\raven-tsx-${Bun.hash(bytes).toString(16)}.dll`;
65
+ if (Bun.file(extracted).size !== bytes.length) {
66
+ writeFileSync(extracted, bytes);
67
+ }
68
+ return extracted;
69
+ }
70
+
71
+ if (!process.env["RAVEN_LIBRARY"]) {
72
+ const packaged = findPackagedLibrary();
73
+ if (!packaged) {
74
+ throw new Error(
75
+ `raven-tsx: no native library for ${process.platform}-${process.arch}. ` +
76
+ `Install @decky.fx/raven-tsx-${process.platform}-${process.arch}, or set RAVEN_LIBRARY to the path of a library you built.`,
77
+ );
78
+ }
79
+ process.env["RAVEN_LIBRARY"] = loadablePath(packaged);
80
+ }
81
+
82
+ checkLibrary(process.env["RAVEN_LIBRARY"] ?? "");
@@ -0,0 +1,179 @@
1
+ /**
2
+ * The curves and drag lines of a `Plot`, given as props. XML would have them as elements inside the
3
+ * widget, with their numbers as text: here they go to Raven by calling it, so that a million points
4
+ * stay one typed array, and a new render only sends what changed.
5
+ */
6
+
7
+ import { RavenError } from "./errors";
8
+ import type { DataValue, PlotDragLine, PlotNumbers, PlotSeries } from "./props";
9
+ import * as r from "./raven";
10
+
11
+ /** The data props of a widget, by name. */
12
+ type PlotData = ReadonlyMap<string, DataValue>;
13
+
14
+ const LINES: Readonly<Record<NonNullable<PlotSeries["Line"]>, r.RPlotLine>> = {
15
+ Solid: r.R_PLOT_LINE_SOLID,
16
+ Dashed: r.R_PLOT_LINE_DASHED,
17
+ Dotted: r.R_PLOT_LINE_DOTTED,
18
+ DashDot: r.R_PLOT_LINE_DASH_DOT,
19
+ None: r.R_PLOT_LINE_NONE,
20
+ };
21
+
22
+ const MARKERS: Readonly<Record<NonNullable<PlotSeries["Marker"]>, r.RPlotMarker>> = {
23
+ None: r.R_PLOT_MARKER_NONE,
24
+ Circle: r.R_PLOT_MARKER_CIRCLE,
25
+ Square: r.R_PLOT_MARKER_SQUARE,
26
+ Diamond: r.R_PLOT_MARKER_DIAMOND,
27
+ TriangleUp: r.R_PLOT_MARKER_TRIANGLE_UP,
28
+ TriangleDown: r.R_PLOT_MARKER_TRIANGLE_DOWN,
29
+ Cross: r.R_PLOT_MARKER_CROSS,
30
+ Plus: r.R_PLOT_MARKER_PLUS,
31
+ Asterisk: r.R_PLOT_MARKER_ASTERISK,
32
+ };
33
+
34
+ const AXES: Readonly<Record<NonNullable<PlotDragLine["Axis"]>, r.RPlotAxis>> = { X: r.R_PLOT_X, Y: r.R_PLOT_Y, X2: r.R_PLOT_X2, Y2: r.R_PLOT_Y2 };
35
+
36
+ const NEW_SERIES: PlotSeries = {};
37
+ const COLOR = /^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
38
+
39
+ const seriesOf = (data: PlotData): readonly PlotSeries[] => (data.get("Series") ?? []) as readonly PlotSeries[];
40
+ const linesOf = (data: PlotData): readonly PlotDragLine[] => (data.get("DragLines") ?? []) as readonly PlotDragLine[];
41
+
42
+ /** A color as the four numbers Raven takes for a series or a line. */
43
+ function colorOf(color: string): [red: number, green: number, blue: number, alpha: number] {
44
+ const channel = (from: number): number => parseInt(color.slice(from, from + 2), 16) / 255;
45
+ return [channel(1), channel(3), channel(5), color.length > 7 ? channel(7) : 1];
46
+ }
47
+
48
+ /** Whether two lists of numbers hold the same values. NaN, a gap in a curve, is the same as NaN. */
49
+ function sameNumbers(a: PlotNumbers | undefined, b: PlotNumbers | undefined): boolean {
50
+ if (a === b) {
51
+ return true;
52
+ }
53
+ if (!a || !b || a.length !== b.length) {
54
+ return false;
55
+ }
56
+ for (let index = 0; index < a.length; index++) {
57
+ if (!Object.is(a[index], b[index])) {
58
+ return false;
59
+ }
60
+ }
61
+ return true;
62
+ }
63
+
64
+ /** Whether something an entry had is gone from it: Raven has no way to give it its default back. */
65
+ function lostSomething<T extends object>(before: T, after: T): boolean {
66
+ return Object.entries(before).some(([name, value]) => value !== undefined && after[name as keyof T] === undefined);
67
+ }
68
+
69
+ /** How many entries, from the first, can be changed in place. The others are made again. */
70
+ function keptCount<T>(before: readonly T[], after: readonly T[], remake: (before: T, after: T) => boolean): number {
71
+ const shared = Math.min(before.length, after.length);
72
+ const first = before.slice(0, shared).findIndex((entry, index) => remake(entry, after[index] as T));
73
+ return first < 0 ? shared : first;
74
+ }
75
+
76
+ /** Gives a series what changed in its props. */
77
+ function setSeries(plot: r.Rid, index: number, before: PlotSeries, after: PlotSeries): void {
78
+ if (before.Name !== after.Name) {
79
+ r.Rplot_set_series_name(plot, index, after.Name ?? null);
80
+ }
81
+ if (after.Color !== undefined && before.Color !== after.Color) {
82
+ r.Rplot_set_series_color(plot, index, ...colorOf(after.Color));
83
+ }
84
+ // (a width of 0 and a size of 0 keep what the series has)
85
+ if (before.Line !== after.Line || before.LineWidth !== after.LineWidth) {
86
+ r.Rplot_set_series_line(plot, index, LINES[after.Line ?? "Solid"], after.LineWidth ?? 0);
87
+ }
88
+ if (before.Marker !== after.Marker || before.MarkerSize !== after.MarkerSize) {
89
+ r.Rplot_set_series_marker(plot, index, MARKERS[after.Marker ?? "None"], after.MarkerSize ?? 0);
90
+ }
91
+ if (after.MarkerFilled !== undefined && before.MarkerFilled !== after.MarkerFilled) {
92
+ r.Rplot_set_series_marker_filled(plot, index, Number(after.MarkerFilled));
93
+ }
94
+ if (after.Step !== undefined && before.Step !== after.Step) {
95
+ r.Rplot_set_series_step(plot, index, Number(after.Step));
96
+ }
97
+ if (after.Fill !== undefined && before.Fill !== after.Fill) {
98
+ r.Rplot_set_series_fill(plot, index, after.Fill);
99
+ }
100
+ if (before.XAxis !== after.XAxis || before.YAxis !== after.YAxis) {
101
+ r.Rplot_set_series_axes(plot, index, AXES[after.XAxis ?? "X"], AXES[after.YAxis ?? "Y"]);
102
+ }
103
+ if (after.Visible !== undefined && before.Visible !== after.Visible) {
104
+ r.Rplot_set_series_visible(plot, index, Number(after.Visible));
105
+ }
106
+ // (before the points, as in XML, so that they are cut to it once)
107
+ if (after.MaxPoints !== undefined && before.MaxPoints !== after.MaxPoints) {
108
+ r.Rplot_set_series_max_points(plot, index, after.MaxPoints);
109
+ }
110
+ if (!sameNumbers(before.X, after.X) || !sameNumbers(before.Y, after.Y)) {
111
+ r.Rplot_set_series_data(plot, index, after.X ?? null, after.Y ?? null, after.Y?.length ?? 0);
112
+ }
113
+ }
114
+
115
+ /** Goes from one list of series to another, by their place in it. */
116
+ function applySeries(plot: r.Rid, before: readonly PlotSeries[], after: readonly PlotSeries[]): void {
117
+ const kept = keptCount(before, after, lostSomething);
118
+ for (let index = before.length - 1; index >= kept; index--) {
119
+ r.Rplot_remove_series(plot, index);
120
+ }
121
+ after.forEach((series, index) => {
122
+ if (index >= kept) {
123
+ r.Rplot_add_series(plot, null);
124
+ }
125
+ setSeries(plot, index, (index < kept ? before[index] : undefined) ?? NEW_SERIES, series);
126
+ });
127
+ }
128
+
129
+ /** Goes from one list of drag lines to another. Raven cannot change the axis or the label of a line. */
130
+ function applyDragLines(plot: r.Rid, before: readonly PlotDragLine[], after: readonly PlotDragLine[]): void {
131
+ const kept = keptCount(before, after, (old, now) => lostSomething(old, now) || old.Axis !== now.Axis || old.Label !== now.Label);
132
+ for (let index = before.length - 1; index >= kept; index--) {
133
+ r.Rplot_remove_drag_line(plot, index);
134
+ }
135
+ after.forEach((line, index) => {
136
+ const old = index < kept ? before[index] : undefined;
137
+ if (!old) {
138
+ r.Rplot_add_drag_line(plot, AXES[line.Axis ?? "X"], line.Value, line.Label ?? null);
139
+ } else if (old.Value !== line.Value) {
140
+ r.Rplot_set_drag_line_value(plot, index, line.Value);
141
+ }
142
+ if (line.Color !== undefined && line.Color !== old?.Color) {
143
+ r.Rplot_set_drag_line_color(plot, index, ...colorOf(line.Color));
144
+ }
145
+ });
146
+ }
147
+
148
+ /** Throws when the data of a `Plot` cannot be shown. Called before anything changes. */
149
+ export function checkPlotData(data: PlotData): void {
150
+ for (const { Name, X, Y, Color } of seriesOf(data)) {
151
+ if (X && X.length !== (Y?.length ?? 0)) {
152
+ RavenError.throw(`raven-tsx: the series ${Name ?? "without a name"} of a Plot has ${X.length} X for ${Y?.length ?? 0} Y`);
153
+ }
154
+ if (Color !== undefined && !COLOR.test(Color)) {
155
+ RavenError.throw(`raven-tsx: the color of a series of a Plot should be #RRGGBB or #RRGGBBAA, and is ${Color}`);
156
+ }
157
+ }
158
+ for (const { Color } of linesOf(data)) {
159
+ if (Color !== undefined && !COLOR.test(Color)) {
160
+ RavenError.throw(`raven-tsx: the color of a drag line of a Plot should be #RRGGBB or #RRGGBBAA, and is ${Color}`);
161
+ }
162
+ }
163
+ }
164
+
165
+ /** Gives a `Plot` what changed in its data. */
166
+ export function applyPlotData(plot: r.Rid, previous: PlotData, next: PlotData): void {
167
+ applySeries(plot, seriesOf(previous), seriesOf(next));
168
+ applyDragLines(plot, linesOf(previous), linesOf(next));
169
+ }
170
+
171
+ /** Puts the drag lines back where the props say, after a handler that refused the move. */
172
+ export function restorePlotData(plot: r.Rid, data: PlotData): void {
173
+ const held = r.Rplot_get_drag_line_count(plot);
174
+ linesOf(data).slice(0, held).forEach(({ Value }, index) => {
175
+ if (r.Rplot_get_drag_line_value(plot, index) !== Value) {
176
+ r.Rplot_set_drag_line_value(plot, index, Value);
177
+ }
178
+ });
179
+ }