@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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@elicdavis/node-flow",
3
3
  "description": "build node graphs",
4
4
  "author": "Eli C Davis",
5
- "version": "0.2.0",
5
+ "version": "0.2.2",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
8
  "nodes",
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
- (amount, anchor) => {
253
- this.zoom(amount, anchor);
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 += amount * this.#camera.zoom * 0.05;
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
- let results = this.currentView().render(this.#ctx, this.#camera, this.#mousePosition);
765
- if (results?.cursorStyle) {
766
- this.#cursor = results?.cursorStyle;
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 PINCH_SENSITIVITY = 50;
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: (amount: number, anchor: Vector2) => void;
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: (amount: number, anchor: Vector2) => void,
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
- // Scroll up zooms in (matches Figma / Maps).
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(amount, anchor);
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((dist - this.#pinchDist) / PINCH_SENSITIVITY, anchor);
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
- for (const widget of this.#widgets) {
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 object | null
127
- ? (null extends T[K] ? T[K] : DeepPartial<T[K]>)
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
+ }