@elicdavis/node-flow 0.2.1 → 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 +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +294 -212
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +294 -212
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/graph.ts +7 -3
- 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/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/package.json
CHANGED
package/src/graph.ts
CHANGED
|
@@ -250,8 +250,8 @@ export class NodeFlowGraph {
|
|
|
250
250
|
this.#clickEnd.bind(this),
|
|
251
251
|
this.#openContextMenu.bind(this),
|
|
252
252
|
this.#fileDrop.bind(this),
|
|
253
|
-
(
|
|
254
|
-
this
|
|
253
|
+
(factor, anchor) => {
|
|
254
|
+
this.#zoomBy(factor, anchor);
|
|
255
255
|
}
|
|
256
256
|
);
|
|
257
257
|
|
|
@@ -417,6 +417,10 @@ export class NodeFlowGraph {
|
|
|
417
417
|
|
|
418
418
|
|
|
419
419
|
zoom(amount: number, anchor?: Vector2): void {
|
|
420
|
+
this.#zoomBy(1 + amount * 0.05, anchor);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#zoomBy(factor: number, anchor?: Vector2): void {
|
|
420
424
|
const anchorPos = anchor ?? this.#mousePosition;
|
|
421
425
|
|
|
422
426
|
let oldPos: Vector2 | undefined = undefined;
|
|
@@ -424,7 +428,7 @@ export class NodeFlowGraph {
|
|
|
424
428
|
oldPos = this.#sceenPositionToGraphPosition(anchorPos);
|
|
425
429
|
}
|
|
426
430
|
|
|
427
|
-
this.#camera.zoom
|
|
431
|
+
this.#camera.zoom *= factor;
|
|
428
432
|
|
|
429
433
|
if (!oldPos || anchorPos === undefined) {
|
|
430
434
|
return;
|
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
|
+
}
|
package/test/hidden_port.test.ts
CHANGED
|
@@ -50,13 +50,14 @@ function scene(hidden: boolean) {
|
|
|
50
50
|
|
|
51
51
|
describe('a hidden port', () => {
|
|
52
52
|
it('takes no room on its node', () => {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
53
|
+
const one = scene(true);
|
|
54
|
+
const two = scene(true);
|
|
55
|
+
two.node.addInput({ name: 'Iridescence', type: 'iridescence', hidden: true });
|
|
55
56
|
|
|
56
|
-
const
|
|
57
|
-
const
|
|
57
|
+
const oneHeight = one.node.calculateBounds(one.ctx, one.camera).Size.y;
|
|
58
|
+
const twoHeight = two.node.calculateBounds(two.ctx, two.camera).Size.y;
|
|
58
59
|
|
|
59
|
-
assert.
|
|
60
|
+
assert.equal(twoHeight, oneHeight);
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
it('cannot be hovered where it would have been drawn', () => {
|