@elicdavis/node-flow 0.2.0 → 0.2.2
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/dist/index.d.mts +17 -4
- package/dist/index.d.ts +17 -4
- package/dist/index.js +344 -222
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +344 -222
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/graph.ts +14 -7
- package/src/index.ts +2 -2
- package/src/input.ts +6 -8
- package/src/node.ts +53 -1
- package/src/port.ts +6 -1
- package/src/theme.ts +179 -179
- package/src/widgets/hidden_ports.ts +49 -0
- package/src/widgets/inplace.ts +75 -23
- package/src/widgets/number.ts +5 -12
- package/src/widgets/string.ts +1 -8
- package/src/widgets/widget.ts +201 -180
- package/test/hidden_port.test.ts +6 -5
- package/test/hidden_ports_toggle.test.ts +102 -0
- package/test/pinch_zoom.test.ts +78 -0
- package/test/support.ts +15 -2
- package/test/widgets.test.ts +54 -1
package/package.json
CHANGED
package/src/graph.ts
CHANGED
|
@@ -19,6 +19,7 @@ export { CameraOrientation };
|
|
|
19
19
|
import { PassSubsystem } from './pass/subsystem';
|
|
20
20
|
import { QuickMenu } from './quickMenu';
|
|
21
21
|
import { onRenderRequested, renderContinuously, requestRender } from './render_scheduler';
|
|
22
|
+
import { placeEditsDuring } from './widgets/inplace';
|
|
22
23
|
import { Minimap, MinimapConfig } from './minimap';
|
|
23
24
|
|
|
24
25
|
export type GraphRenderer = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, position: Vector2, scale: number) => void;
|
|
@@ -249,8 +250,8 @@ export class NodeFlowGraph {
|
|
|
249
250
|
this.#clickEnd.bind(this),
|
|
250
251
|
this.#openContextMenu.bind(this),
|
|
251
252
|
this.#fileDrop.bind(this),
|
|
252
|
-
(
|
|
253
|
-
this
|
|
253
|
+
(factor, anchor) => {
|
|
254
|
+
this.#zoomBy(factor, anchor);
|
|
254
255
|
}
|
|
255
256
|
);
|
|
256
257
|
|
|
@@ -416,6 +417,10 @@ export class NodeFlowGraph {
|
|
|
416
417
|
|
|
417
418
|
|
|
418
419
|
zoom(amount: number, anchor?: Vector2): void {
|
|
420
|
+
this.#zoomBy(1 + amount * 0.05, anchor);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#zoomBy(factor: number, anchor?: Vector2): void {
|
|
419
424
|
const anchorPos = anchor ?? this.#mousePosition;
|
|
420
425
|
|
|
421
426
|
let oldPos: Vector2 | undefined = undefined;
|
|
@@ -423,7 +428,7 @@ export class NodeFlowGraph {
|
|
|
423
428
|
oldPos = this.#sceenPositionToGraphPosition(anchorPos);
|
|
424
429
|
}
|
|
425
430
|
|
|
426
|
-
this.#camera.zoom
|
|
431
|
+
this.#camera.zoom *= factor;
|
|
427
432
|
|
|
428
433
|
if (!oldPos || anchorPos === undefined) {
|
|
429
434
|
return;
|
|
@@ -761,10 +766,12 @@ export class NodeFlowGraph {
|
|
|
761
766
|
TimeExecution("Render_Background", this.#renderBackground.bind(this));
|
|
762
767
|
|
|
763
768
|
TimeExecution("Render_View_" + this.#currentView, () => {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
769
|
+
placeEditsDuring(this.#canvas, () => {
|
|
770
|
+
let results = this.currentView().render(this.#ctx, this.#camera, this.#mousePosition);
|
|
771
|
+
if (results?.cursorStyle) {
|
|
772
|
+
this.#cursor = results?.cursorStyle;
|
|
773
|
+
}
|
|
774
|
+
});
|
|
768
775
|
});
|
|
769
776
|
|
|
770
777
|
TimeExecution("Render_Minimap", () => {
|
package/src/index.ts
CHANGED
|
@@ -66,7 +66,7 @@ import { SliderWidget, SliderWidgetConfig } from './widgets/slider';
|
|
|
66
66
|
import { ImageWidget, ImageWidgetConfig } from './widgets/image';
|
|
67
67
|
import { RowWidget, RowWidgetConfig } from './widgets/row';
|
|
68
68
|
import { drawField, drawFieldBox, drawFieldText, FieldLook, widgetFont } from './widgets/field';
|
|
69
|
-
import { editInPlace, InPlaceEditConfig } from './widgets/inplace';
|
|
69
|
+
import { editInPlace, InPlaceEdit, InPlaceEditConfig } from './widgets/inplace';
|
|
70
70
|
export {
|
|
71
71
|
WidgetBase, ValueWidget,
|
|
72
72
|
NumberWidget, ColorWidget, StringWidget, TextWidget,
|
|
@@ -74,7 +74,7 @@ export {
|
|
|
74
74
|
drawField, drawFieldBox, drawFieldText, widgetFont, editInPlace, formatNumber,
|
|
75
75
|
};
|
|
76
76
|
export type {
|
|
77
|
-
Widget, WidgetPointer, ValueWidgetConfig, FieldLook, InPlaceEditConfig,
|
|
77
|
+
Widget, WidgetPointer, ValueWidgetConfig, FieldLook, InPlaceEdit, InPlaceEditConfig,
|
|
78
78
|
NumberWidgetConfig, ColorWidgetConfig, StringWidgetConfig, TextWidgetConfig,
|
|
79
79
|
ButtonWidgetConfig, ToggleWidgetConfig, SliderWidgetConfig,
|
|
80
80
|
ImageWidgetConfig, RowWidgetConfig,
|
package/src/input.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { CopyVector2, Distance, SubVector2, Vector2, Zero } from "./types/vector
|
|
|
3
3
|
|
|
4
4
|
const LONG_PRESS_MS = 500;
|
|
5
5
|
const LONG_PRESS_SLOP = 12;
|
|
6
|
-
const
|
|
6
|
+
const WHEEL_ZOOM_PER_PIXEL = 0.0005;
|
|
7
7
|
const SUPPRESS_MOUSE_MS = 600;
|
|
8
8
|
|
|
9
9
|
export class MouseObserver {
|
|
@@ -18,7 +18,7 @@ export class MouseObserver {
|
|
|
18
18
|
#clickStart: (position: Vector2, shiftOrCtrl: boolean) => void;
|
|
19
19
|
#clickStop: () => void;
|
|
20
20
|
#contextMenu: (position: Vector2) => void;
|
|
21
|
-
#zoomCallback: (
|
|
21
|
+
#zoomCallback: (factor: number, anchor: Vector2) => void;
|
|
22
22
|
|
|
23
23
|
#longPressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
24
24
|
#longPressOrigin: Vector2 = Zero();
|
|
@@ -34,7 +34,7 @@ export class MouseObserver {
|
|
|
34
34
|
clickStop: () => void,
|
|
35
35
|
contextMenu: (position: Vector2) => void,
|
|
36
36
|
fileDrop: (file: File) => void,
|
|
37
|
-
zoomCallback: (
|
|
37
|
+
zoomCallback: (factor: number, anchor: Vector2) => void,
|
|
38
38
|
) {
|
|
39
39
|
this.#ele = ele;
|
|
40
40
|
this.#dragCallback = dragCallback;
|
|
@@ -132,14 +132,12 @@ export class MouseObserver {
|
|
|
132
132
|
} else if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
|
133
133
|
dy *= this.#ele.clientHeight;
|
|
134
134
|
}
|
|
135
|
-
|
|
136
|
-
const amount = -dy / 100;
|
|
137
|
-
if (amount === 0) {
|
|
135
|
+
if (dy === 0) {
|
|
138
136
|
return;
|
|
139
137
|
}
|
|
140
138
|
const anchor = this.#mousePos(event);
|
|
141
139
|
this.#moveCallback(anchor);
|
|
142
|
-
this.#zoomCallback(
|
|
140
|
+
this.#zoomCallback(Math.exp(-dy * WHEEL_ZOOM_PER_PIXEL), anchor);
|
|
143
141
|
}
|
|
144
142
|
|
|
145
143
|
#move(event: MouseEvent): void {
|
|
@@ -226,7 +224,7 @@ export class MouseObserver {
|
|
|
226
224
|
const anchor = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
227
225
|
if (this.#pinchDist > 0) {
|
|
228
226
|
this.#moveCallback(anchor);
|
|
229
|
-
this.#zoomCallback(
|
|
227
|
+
this.#zoomCallback(dist / this.#pinchDist, anchor);
|
|
230
228
|
}
|
|
231
229
|
this.#pinchDist = dist;
|
|
232
230
|
return;
|
package/src/node.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { FontWeight, TextStyle, TextStyleConfig, TextStyleFallback } from "./sty
|
|
|
4
4
|
import { Box, InBox } from "./types/box";
|
|
5
5
|
import { CopyVector2, Distance, ScaleVector, Vector2, Zero } from "./types/vector2";
|
|
6
6
|
import { Widget } from './widgets/widget';
|
|
7
|
+
import { HiddenPortsToggle } from './widgets/hidden_ports';
|
|
7
8
|
import { List } from './types/list';
|
|
8
9
|
import { BoxStyle, BoxStyleConfig, BoxStyleWithFallback } from "./styles/box";
|
|
9
10
|
import { Text } from "./types/text";
|
|
@@ -804,7 +805,8 @@ export class FlowNode {
|
|
|
804
805
|
size.x = Math.max(size.x, tempMeasurement.x + doublePadding)
|
|
805
806
|
}
|
|
806
807
|
|
|
807
|
-
|
|
808
|
+
const widgets = this.#hasHiddenPortsToggle() ? [this.#hiddenPortsToggle, ...this.#widgets] : this.#widgets;
|
|
809
|
+
for (const widget of widgets) {
|
|
808
810
|
if (widget.Hidden?.()) {
|
|
809
811
|
continue;
|
|
810
812
|
}
|
|
@@ -837,6 +839,38 @@ export class FlowNode {
|
|
|
837
839
|
return port;
|
|
838
840
|
}
|
|
839
841
|
|
|
842
|
+
#hiddenPortsShown = false;
|
|
843
|
+
|
|
844
|
+
#hiddenPortsToggle = new HiddenPortsToggle(this);
|
|
845
|
+
|
|
846
|
+
#hiddenPortsTogglePosition: Box | null = null;
|
|
847
|
+
|
|
848
|
+
hiddenPortsShown(): boolean {
|
|
849
|
+
return this.#hiddenPortsShown;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
setHiddenPortsShown(shown: boolean): void {
|
|
853
|
+
if (shown === this.#hiddenPortsShown) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
this.#hiddenPortsShown = shown;
|
|
857
|
+
this.invalidateLayout();
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
hiddenPortCount(): number {
|
|
861
|
+
let count = 0;
|
|
862
|
+
for (const port of [...this.#input, ...this.#output]) {
|
|
863
|
+
if (port.tucked()) {
|
|
864
|
+
count++;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return count;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
#hasHiddenPortsToggle(): boolean {
|
|
871
|
+
return this.hiddenPortCount() > 0;
|
|
872
|
+
}
|
|
873
|
+
|
|
840
874
|
addWidget(widget: Widget): void {
|
|
841
875
|
this.#widgets.push(widget);
|
|
842
876
|
requestRender();
|
|
@@ -930,6 +964,12 @@ export class FlowNode {
|
|
|
930
964
|
}
|
|
931
965
|
}
|
|
932
966
|
|
|
967
|
+
if (this.#hiddenPortsTogglePosition !== null && InBox(this.#hiddenPortsTogglePosition, position)) {
|
|
968
|
+
intersection.Node = this;
|
|
969
|
+
intersection.Widget = this.#hiddenPortsToggle;
|
|
970
|
+
intersection.WidgetBox = this.#hiddenPortsTogglePosition;
|
|
971
|
+
}
|
|
972
|
+
|
|
933
973
|
for (let i = 0; i < this.#widgetPositions.Count(); i++) {
|
|
934
974
|
const widget = this.#widgets[i];
|
|
935
975
|
if (widget !== undefined && !widget.Hidden?.() && InBox(this.#widgetPositions.At(i), position)) {
|
|
@@ -1297,6 +1337,17 @@ export class FlowNode {
|
|
|
1297
1337
|
startY += portHeight + scaledElementSpacing;
|
|
1298
1338
|
}
|
|
1299
1339
|
|
|
1340
|
+
this.#hiddenPortsTogglePosition = null;
|
|
1341
|
+
if (this.#hasHiddenPortsToggle()) {
|
|
1342
|
+
const box: Box = {
|
|
1343
|
+
Position: { x: nodeBounds.Position.x + scaledPadding, y: startY },
|
|
1344
|
+
Size: { x: nodeBounds.Size.x - (scaledPadding * 2), y: this.#hiddenPortsToggle.Size().y * camera.zoom },
|
|
1345
|
+
};
|
|
1346
|
+
this.#hiddenPortsToggle.Draw(ctx, box, camera.zoom, mousePosition);
|
|
1347
|
+
this.#hiddenPortsTogglePosition = box;
|
|
1348
|
+
startY += box.Size.y + scaledElementSpacing;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1300
1351
|
// Widgets
|
|
1301
1352
|
for (let i = 0; i < this.#widgets.length; i++) {
|
|
1302
1353
|
const widget = this.#widgets[i];
|
|
@@ -1360,6 +1411,7 @@ export class FlowNode {
|
|
|
1360
1411
|
return false;
|
|
1361
1412
|
}
|
|
1362
1413
|
this.#widgetPositions.Clear();
|
|
1414
|
+
this.#hiddenPortsTogglePosition = null;
|
|
1363
1415
|
return true;
|
|
1364
1416
|
}
|
|
1365
1417
|
|
package/src/port.ts
CHANGED
|
@@ -345,9 +345,14 @@ export class Port {
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
/**
|
|
348
|
-
* A port asked to hide still shows while anything is connected to it
|
|
348
|
+
* A port asked to hide still shows while anything is connected to it,
|
|
349
|
+
* and while its node is showing its hidden ports.
|
|
349
350
|
*/
|
|
350
351
|
hidden(): boolean {
|
|
352
|
+
return this.tucked() && !(this.#node?.hiddenPortsShown() ?? false);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
tucked(): boolean {
|
|
351
356
|
return this.#hidden && this.#connections.length === 0;
|
|
352
357
|
}
|
|
353
358
|
|
package/src/theme.ts
CHANGED
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
import { requestRender } from "./render_scheduler";
|
|
2
|
-
|
|
3
|
-
export interface BevelColors {
|
|
4
|
-
Light: string;
|
|
5
|
-
Dark: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const defaults = {
|
|
9
|
-
FontFamily: "Courier New",
|
|
10
|
-
Graph: {
|
|
11
|
-
BackgroundColor: "#07212a",
|
|
12
|
-
DotColor: "rgb(41, 54, 57)",
|
|
13
|
-
},
|
|
14
|
-
Node: {
|
|
15
|
-
FontColor: "#afb9bb",
|
|
16
|
-
BackgroundColor: "#0c2b35",
|
|
17
|
-
Title: {
|
|
18
|
-
Color: "#154050",
|
|
19
|
-
FontColor: "#afb9bb",
|
|
20
|
-
Padding: 5
|
|
21
|
-
},
|
|
22
|
-
BorderRadius: 15,
|
|
23
|
-
Border: {
|
|
24
|
-
Idle: "#1c1c1c",
|
|
25
|
-
MouseOver: "#8e8e8e",
|
|
26
|
-
Grabbed: "white",
|
|
27
|
-
Selected: "#6e6e6e"
|
|
28
|
-
},
|
|
29
|
-
Bevel: null as BevelColors | null,
|
|
30
|
-
Port: {
|
|
31
|
-
FontColor: "#afb9bb"
|
|
32
|
-
},
|
|
33
|
-
Message: {
|
|
34
|
-
InfoColor: "#afb9bb",
|
|
35
|
-
WarnColor: "#f1f663ff",
|
|
36
|
-
ErrorColor: "#FF0000"
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
BoxSelect: {
|
|
40
|
-
Color: "white",
|
|
41
|
-
Size: 1,
|
|
42
|
-
Radius: 2,
|
|
43
|
-
LineDashLength: 5,
|
|
44
|
-
},
|
|
45
|
-
Widget: {
|
|
46
|
-
FontColor: "#afb9bb",
|
|
47
|
-
LabelColor: "#7f9497",
|
|
48
|
-
FontSize: 14,
|
|
49
|
-
Padding: 6,
|
|
50
|
-
Gap: 4,
|
|
51
|
-
BackgroundColor: "#0f313c",
|
|
52
|
-
Border: {
|
|
53
|
-
Color: "#154050",
|
|
54
|
-
Size: 2,
|
|
55
|
-
Radius: 2
|
|
56
|
-
},
|
|
57
|
-
Bevel: null as BevelColors | null,
|
|
58
|
-
Hover: {
|
|
59
|
-
BackgroundColor: "#195366"
|
|
60
|
-
},
|
|
61
|
-
Input: {
|
|
62
|
-
BackgroundColor: "#0f313c",
|
|
63
|
-
HoverBackgroundColor: "#195366",
|
|
64
|
-
FontColor: "#afb9bb",
|
|
65
|
-
},
|
|
66
|
-
Slider: {
|
|
67
|
-
FillColor: "#07212A",
|
|
68
|
-
},
|
|
69
|
-
Button: {
|
|
70
|
-
Click: {
|
|
71
|
-
BackgroundColor: "#1f637a"
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
ContextMenu: {
|
|
76
|
-
BackgroundColor: "#07212A",
|
|
77
|
-
HighlightColor: "#205A6D",
|
|
78
|
-
HighlightFontColor: "#afb9bb",
|
|
79
|
-
FontColor: "#afb9bb",
|
|
80
|
-
DisabledFontColor: "#4a585c",
|
|
81
|
-
BorderRadius: 5,
|
|
82
|
-
},
|
|
83
|
-
Minimap: {
|
|
84
|
-
BackgroundColor: "rgba(10, 16, 18, 0.85)",
|
|
85
|
-
BorderColor: "#1c1c1c",
|
|
86
|
-
BorderRadius: 4,
|
|
87
|
-
Padding: 6,
|
|
88
|
-
NodeColor: "#6b7f85",
|
|
89
|
-
ViewportColor: "rgba(175, 185, 187, 0.25)",
|
|
90
|
-
},
|
|
91
|
-
Popup: {
|
|
92
|
-
BackgroundColor: "#000000",
|
|
93
|
-
FontColor: "#ffffff",
|
|
94
|
-
FontFamily: "Verdana",
|
|
95
|
-
BorderRadius: 8,
|
|
96
|
-
Backdrop: "#00000070",
|
|
97
|
-
},
|
|
98
|
-
Note: {
|
|
99
|
-
FontColor: "#afb9bb",
|
|
100
|
-
FontSize: 16,
|
|
101
|
-
EntrySpacing: 20,
|
|
102
|
-
LineSpacing: 5,
|
|
103
|
-
DotSize: 3,
|
|
104
|
-
HeaderLineWidth: 2,
|
|
105
|
-
H1: {
|
|
106
|
-
FontSize: 32,
|
|
107
|
-
},
|
|
108
|
-
H2: {
|
|
109
|
-
FontSize: 16,
|
|
110
|
-
},
|
|
111
|
-
H3: {
|
|
112
|
-
FontSize: 16,
|
|
113
|
-
},
|
|
114
|
-
CodeBlock: {
|
|
115
|
-
BackgroundColor: "#0c2b35",
|
|
116
|
-
FontColor: "#afb9bb",
|
|
117
|
-
Padding: 16,
|
|
118
|
-
BorderRadius: 4,
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
export type ThemeValues = typeof defaults;
|
|
124
|
-
|
|
125
|
-
type DeepPartial<T> = {
|
|
126
|
-
[K in keyof T]?: T[K] extends
|
|
127
|
-
?
|
|
128
|
-
: T[K];
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
export type ThemeConfig = DeepPartial<ThemeValues>;
|
|
132
|
-
|
|
133
|
-
function clone<T>(value: T): T {
|
|
134
|
-
return JSON.parse(JSON.stringify(value));
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function merge(target: any, source: any): any {
|
|
138
|
-
if (source === undefined) {
|
|
139
|
-
return target;
|
|
140
|
-
}
|
|
141
|
-
if (source === null || typeof source !== "object" || target === null || typeof target !== "object") {
|
|
142
|
-
return source;
|
|
143
|
-
}
|
|
144
|
-
for (const key of Object.keys(source)) {
|
|
145
|
-
target[key] = merge(target[key], source[key]);
|
|
146
|
-
}
|
|
147
|
-
return target;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export const DefaultTheme: Readonly<ThemeValues> = Object.freeze(clone(defaults));
|
|
151
|
-
|
|
152
|
-
export const Theme: ThemeValues = clone(defaults);
|
|
153
|
-
|
|
154
|
-
let version = 0;
|
|
155
|
-
|
|
156
|
-
const listeners = new Set<() => void>();
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Replaces the theme: the defaults, with anything in config laid over them.
|
|
160
|
-
* Everything already on screen picks the change up on its next draw.
|
|
161
|
-
*/
|
|
162
|
-
export function setTheme(config?: ThemeConfig): void {
|
|
163
|
-
const next = merge(clone(defaults), config);
|
|
164
|
-
for (const key of Object.keys(next) as Array<keyof ThemeValues>) {
|
|
165
|
-
(Theme as any)[key] = next[key];
|
|
166
|
-
}
|
|
167
|
-
version++;
|
|
168
|
-
listeners.forEach((listener) => listener());
|
|
169
|
-
requestRender();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function themeVersion(): number {
|
|
173
|
-
return version;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function onThemeChange(listener: () => void): () => void {
|
|
177
|
-
listeners.add(listener);
|
|
178
|
-
return () => listeners.delete(listener);
|
|
179
|
-
}
|
|
1
|
+
import { requestRender } from "./render_scheduler";
|
|
2
|
+
|
|
3
|
+
export interface BevelColors {
|
|
4
|
+
Light: string;
|
|
5
|
+
Dark: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const defaults = {
|
|
9
|
+
FontFamily: "Courier New",
|
|
10
|
+
Graph: {
|
|
11
|
+
BackgroundColor: "#07212a",
|
|
12
|
+
DotColor: "rgb(41, 54, 57)",
|
|
13
|
+
},
|
|
14
|
+
Node: {
|
|
15
|
+
FontColor: "#afb9bb",
|
|
16
|
+
BackgroundColor: "#0c2b35",
|
|
17
|
+
Title: {
|
|
18
|
+
Color: "#154050",
|
|
19
|
+
FontColor: "#afb9bb",
|
|
20
|
+
Padding: 5
|
|
21
|
+
},
|
|
22
|
+
BorderRadius: 15,
|
|
23
|
+
Border: {
|
|
24
|
+
Idle: "#1c1c1c",
|
|
25
|
+
MouseOver: "#8e8e8e",
|
|
26
|
+
Grabbed: "white",
|
|
27
|
+
Selected: "#6e6e6e"
|
|
28
|
+
},
|
|
29
|
+
Bevel: null as BevelColors | null,
|
|
30
|
+
Port: {
|
|
31
|
+
FontColor: "#afb9bb"
|
|
32
|
+
},
|
|
33
|
+
Message: {
|
|
34
|
+
InfoColor: "#afb9bb",
|
|
35
|
+
WarnColor: "#f1f663ff",
|
|
36
|
+
ErrorColor: "#FF0000"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
BoxSelect: {
|
|
40
|
+
Color: "white",
|
|
41
|
+
Size: 1,
|
|
42
|
+
Radius: 2,
|
|
43
|
+
LineDashLength: 5,
|
|
44
|
+
},
|
|
45
|
+
Widget: {
|
|
46
|
+
FontColor: "#afb9bb",
|
|
47
|
+
LabelColor: "#7f9497",
|
|
48
|
+
FontSize: 14,
|
|
49
|
+
Padding: 6,
|
|
50
|
+
Gap: 4,
|
|
51
|
+
BackgroundColor: "#0f313c",
|
|
52
|
+
Border: {
|
|
53
|
+
Color: "#154050",
|
|
54
|
+
Size: 2,
|
|
55
|
+
Radius: 2
|
|
56
|
+
},
|
|
57
|
+
Bevel: null as BevelColors | null,
|
|
58
|
+
Hover: {
|
|
59
|
+
BackgroundColor: "#195366"
|
|
60
|
+
},
|
|
61
|
+
Input: {
|
|
62
|
+
BackgroundColor: "#0f313c",
|
|
63
|
+
HoverBackgroundColor: "#195366",
|
|
64
|
+
FontColor: "#afb9bb",
|
|
65
|
+
},
|
|
66
|
+
Slider: {
|
|
67
|
+
FillColor: "#07212A",
|
|
68
|
+
},
|
|
69
|
+
Button: {
|
|
70
|
+
Click: {
|
|
71
|
+
BackgroundColor: "#1f637a"
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
ContextMenu: {
|
|
76
|
+
BackgroundColor: "#07212A",
|
|
77
|
+
HighlightColor: "#205A6D",
|
|
78
|
+
HighlightFontColor: "#afb9bb",
|
|
79
|
+
FontColor: "#afb9bb",
|
|
80
|
+
DisabledFontColor: "#4a585c",
|
|
81
|
+
BorderRadius: 5,
|
|
82
|
+
},
|
|
83
|
+
Minimap: {
|
|
84
|
+
BackgroundColor: "rgba(10, 16, 18, 0.85)",
|
|
85
|
+
BorderColor: "#1c1c1c",
|
|
86
|
+
BorderRadius: 4,
|
|
87
|
+
Padding: 6,
|
|
88
|
+
NodeColor: "#6b7f85",
|
|
89
|
+
ViewportColor: "rgba(175, 185, 187, 0.25)",
|
|
90
|
+
},
|
|
91
|
+
Popup: {
|
|
92
|
+
BackgroundColor: "#000000",
|
|
93
|
+
FontColor: "#ffffff",
|
|
94
|
+
FontFamily: "Verdana",
|
|
95
|
+
BorderRadius: 8,
|
|
96
|
+
Backdrop: "#00000070",
|
|
97
|
+
},
|
|
98
|
+
Note: {
|
|
99
|
+
FontColor: "#afb9bb",
|
|
100
|
+
FontSize: 16,
|
|
101
|
+
EntrySpacing: 20,
|
|
102
|
+
LineSpacing: 5,
|
|
103
|
+
DotSize: 3,
|
|
104
|
+
HeaderLineWidth: 2,
|
|
105
|
+
H1: {
|
|
106
|
+
FontSize: 32,
|
|
107
|
+
},
|
|
108
|
+
H2: {
|
|
109
|
+
FontSize: 16,
|
|
110
|
+
},
|
|
111
|
+
H3: {
|
|
112
|
+
FontSize: 16,
|
|
113
|
+
},
|
|
114
|
+
CodeBlock: {
|
|
115
|
+
BackgroundColor: "#0c2b35",
|
|
116
|
+
FontColor: "#afb9bb",
|
|
117
|
+
Padding: 16,
|
|
118
|
+
BorderRadius: 4,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type ThemeValues = typeof defaults;
|
|
124
|
+
|
|
125
|
+
type DeepPartial<T> = {
|
|
126
|
+
[K in keyof T]?: T[K] extends BevelColors | null
|
|
127
|
+
? T[K]
|
|
128
|
+
: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export type ThemeConfig = DeepPartial<ThemeValues>;
|
|
132
|
+
|
|
133
|
+
function clone<T>(value: T): T {
|
|
134
|
+
return JSON.parse(JSON.stringify(value));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function merge(target: any, source: any): any {
|
|
138
|
+
if (source === undefined) {
|
|
139
|
+
return target;
|
|
140
|
+
}
|
|
141
|
+
if (source === null || typeof source !== "object" || target === null || typeof target !== "object") {
|
|
142
|
+
return source;
|
|
143
|
+
}
|
|
144
|
+
for (const key of Object.keys(source)) {
|
|
145
|
+
target[key] = merge(target[key], source[key]);
|
|
146
|
+
}
|
|
147
|
+
return target;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const DefaultTheme: Readonly<ThemeValues> = Object.freeze(clone(defaults));
|
|
151
|
+
|
|
152
|
+
export const Theme: ThemeValues = clone(defaults);
|
|
153
|
+
|
|
154
|
+
let version = 0;
|
|
155
|
+
|
|
156
|
+
const listeners = new Set<() => void>();
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Replaces the theme: the defaults, with anything in config laid over them.
|
|
160
|
+
* Everything already on screen picks the change up on its next draw.
|
|
161
|
+
*/
|
|
162
|
+
export function setTheme(config?: ThemeConfig): void {
|
|
163
|
+
const next = merge(clone(defaults), config);
|
|
164
|
+
for (const key of Object.keys(next) as Array<keyof ThemeValues>) {
|
|
165
|
+
(Theme as any)[key] = next[key];
|
|
166
|
+
}
|
|
167
|
+
version++;
|
|
168
|
+
listeners.forEach((listener) => listener());
|
|
169
|
+
requestRender();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function themeVersion(): number {
|
|
173
|
+
return version;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function onThemeChange(listener: () => void): () => void {
|
|
177
|
+
listeners.add(listener);
|
|
178
|
+
return () => listeners.delete(listener);
|
|
179
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Theme } from "../theme";
|
|
2
|
+
import { Box } from "../types/box";
|
|
3
|
+
import { Vector2 } from "../types/vector2";
|
|
4
|
+
import { WidgetBase } from "./widget";
|
|
5
|
+
|
|
6
|
+
export interface HiddenPortsOwner {
|
|
7
|
+
hiddenPortCount(): number;
|
|
8
|
+
hiddenPortsShown(): boolean;
|
|
9
|
+
setHiddenPortsShown(shown: boolean): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class HiddenPortsToggle extends WidgetBase {
|
|
13
|
+
|
|
14
|
+
constructor(private readonly owner: HiddenPortsOwner) {
|
|
15
|
+
super();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Size(): Vector2 {
|
|
19
|
+
return { x: 150, y: 16 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected render(ctx: CanvasRenderingContext2D, box: Box, scale: number): void {
|
|
23
|
+
const label = this.owner.hiddenPortsShown() ? "▴ less" : `▾ ${this.owner.hiddenPortCount()} more`;
|
|
24
|
+
|
|
25
|
+
const midY = box.Position.y + box.Size.y / 2;
|
|
26
|
+
const centerX = box.Position.x + box.Size.x / 2;
|
|
27
|
+
|
|
28
|
+
ctx.font = `${11 * scale}px ${Theme.FontFamily}`;
|
|
29
|
+
const half = ctx.measureText(label).width / 2 + 6 * scale;
|
|
30
|
+
|
|
31
|
+
ctx.globalAlpha = this.hovered() ? 1 : 0.5;
|
|
32
|
+
ctx.strokeStyle = Theme.Widget.FontColor;
|
|
33
|
+
ctx.fillStyle = Theme.Widget.FontColor;
|
|
34
|
+
ctx.lineWidth = Math.max(1, scale);
|
|
35
|
+
ctx.beginPath();
|
|
36
|
+
ctx.moveTo(box.Position.x, midY);
|
|
37
|
+
ctx.lineTo(centerX - half, midY);
|
|
38
|
+
ctx.moveTo(centerX + half, midY);
|
|
39
|
+
ctx.lineTo(box.Position.x + box.Size.x, midY);
|
|
40
|
+
ctx.stroke();
|
|
41
|
+
ctx.textAlign = "center";
|
|
42
|
+
ctx.textBaseline = "middle";
|
|
43
|
+
ctx.fillText(label, centerX, midY);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected onClick(): void {
|
|
47
|
+
this.owner.setHiddenPortsShown(!this.owner.hiddenPortsShown());
|
|
48
|
+
}
|
|
49
|
+
}
|