@elicdavis/node-flow 0.1.20 → 0.2.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 (53) hide show
  1. package/dist/index.d.mts +393 -226
  2. package/dist/index.d.ts +393 -226
  3. package/dist/index.js +1905 -1058
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1888 -1057
  6. package/dist/index.mjs.map +1 -1
  7. package/index.html +1134 -329
  8. package/package.json +1 -1
  9. package/src/connection.ts +30 -5
  10. package/src/contextMenu.ts +66 -34
  11. package/src/graph.ts +19 -14
  12. package/src/index.ts +26 -12
  13. package/src/markdown/entry.ts +11 -15
  14. package/src/markdown/syntaxParser.ts +2 -1
  15. package/src/node.ts +160 -101
  16. package/src/nodes/subsystem.ts +113 -29
  17. package/src/notes/note.ts +9 -1
  18. package/src/organize.ts +540 -111
  19. package/src/popup.ts +4 -2
  20. package/src/port.ts +83 -6
  21. package/src/styles/bevel.ts +31 -0
  22. package/src/styles/cursor.ts +1 -0
  23. package/src/styles/text.ts +4 -4
  24. package/src/theme.ts +179 -92
  25. package/src/types/text.ts +5 -1
  26. package/src/utils/color.ts +1 -1
  27. package/src/widgets/button.ts +52 -107
  28. package/src/widgets/color.ts +91 -150
  29. package/src/widgets/factory.ts +89 -46
  30. package/src/widgets/field.ts +99 -0
  31. package/src/widgets/image.ts +107 -165
  32. package/src/widgets/inplace.ts +136 -0
  33. package/src/widgets/number.ts +115 -154
  34. package/src/widgets/row.ts +138 -0
  35. package/src/widgets/slider.ts +101 -216
  36. package/src/widgets/string.ts +32 -130
  37. package/src/widgets/text.ts +81 -92
  38. package/src/widgets/toggle.ts +62 -207
  39. package/src/widgets/value.ts +127 -0
  40. package/src/widgets/widget.ts +173 -6
  41. package/test/context_menu.test.ts +82 -0
  42. package/test/cursor.test.ts +17 -0
  43. package/test/hidden_port.test.ts +125 -0
  44. package/test/markdown.test.ts +2 -2
  45. package/test/markdown_code_block.test.ts +58 -0
  46. package/test/multi_input.test.ts +124 -0
  47. package/test/organize.test.ts +117 -1
  48. package/test/support.ts +188 -0
  49. package/test/theme.test.ts +86 -0
  50. package/test/widget_click.test.ts +111 -97
  51. package/test/widget_construction.test.ts +5 -4
  52. package/test/widgets.test.ts +386 -0
  53. package/test/wire_follows_camera.test.ts +57 -0
package/dist/index.js CHANGED
@@ -24,11 +24,15 @@ __export(index_exports, {
24
24
  ColorWidget: () => ColorWidget,
25
25
  Connection: () => Connection,
26
26
  ContextMenuItemState: () => ContextMenuItemState,
27
+ CursorStyle: () => CursorStyle,
27
28
  DefaultConnectionRenderer: () => DefaultConnectionRenderer,
29
+ DefaultTheme: () => DefaultTheme,
28
30
  FlowNode: () => FlowNode,
29
31
  FlowNote: () => FlowNote,
30
32
  GlobalWidgetFactory: () => GlobalWidgetFactory,
33
+ GraphView: () => GraphView,
31
34
  ImageWidget: () => ImageWidget,
35
+ InBox: () => InBox,
32
36
  MessageType: () => MessageType,
33
37
  Minimap: () => Minimap,
34
38
  NodeFactory: () => NodeFactory,
@@ -38,25 +42,75 @@ __export(index_exports, {
38
42
  Port: () => Port,
39
43
  PortType: () => PortType,
40
44
  Publisher: () => Publisher,
45
+ RowWidget: () => RowWidget,
41
46
  SliderWidget: () => SliderWidget,
42
47
  StringWidget: () => StringWidget,
43
48
  TextWidget: () => TextWidget,
44
49
  Theme: () => Theme,
45
50
  ToggleWidget: () => ToggleWidget,
51
+ ValueWidget: () => ValueWidget,
52
+ WidgetBase: () => WidgetBase,
46
53
  WidgetFactory: () => WidgetFactory,
54
+ drawField: () => drawField,
55
+ drawFieldBox: () => drawFieldBox,
56
+ drawFieldText: () => drawFieldText,
57
+ editInPlace: () => editInPlace,
58
+ formatNumber: () => formatNumber,
59
+ onThemeChange: () => onThemeChange,
47
60
  portConfigTypes: () => portConfigTypes,
48
61
  portsCompatible: () => portsCompatible,
49
62
  renderContinuously: () => renderContinuously,
50
63
  requestRender: () => requestRender,
51
- typeSetsCompatible: () => typeSetsCompatible
64
+ setTheme: () => setTheme,
65
+ themeVersion: () => themeVersion,
66
+ typeSetsCompatible: () => typeSetsCompatible,
67
+ widgetFont: () => widgetFont
52
68
  });
53
69
  module.exports = __toCommonJS(index_exports);
54
70
 
71
+ // src/render_scheduler.ts
72
+ var subscribers = /* @__PURE__ */ new Set();
73
+ var frameQueued = false;
74
+ var continuous = 0;
75
+ var canSchedule = () => typeof requestAnimationFrame === "function";
76
+ function onRenderRequested(redraw) {
77
+ subscribers.add(redraw);
78
+ return () => {
79
+ subscribers.delete(redraw);
80
+ };
81
+ }
82
+ function requestRender() {
83
+ if (frameQueued || !canSchedule()) {
84
+ return;
85
+ }
86
+ frameQueued = true;
87
+ requestAnimationFrame(() => {
88
+ frameQueued = false;
89
+ subscribers.forEach((redraw) => redraw());
90
+ if (continuous > 0) {
91
+ requestRender();
92
+ }
93
+ });
94
+ }
95
+ function renderContinuously() {
96
+ continuous++;
97
+ requestRender();
98
+ let released = false;
99
+ return () => {
100
+ if (released) {
101
+ return;
102
+ }
103
+ released = true;
104
+ continuous--;
105
+ };
106
+ }
107
+
55
108
  // src/theme.ts
56
- var Theme = {
109
+ var defaults = {
57
110
  FontFamily: "Courier New",
58
111
  Graph: {
59
- BackgroundColor: "#07212a"
112
+ BackgroundColor: "#07212a",
113
+ DotColor: "rgb(41, 54, 57)"
60
114
  },
61
115
  Node: {
62
116
  FontColor: "#afb9bb",
@@ -73,6 +127,7 @@ var Theme = {
73
127
  Grabbed: "white",
74
128
  Selected: "#6e6e6e"
75
129
  },
130
+ Bevel: null,
76
131
  Port: {
77
132
  FontColor: "#afb9bb"
78
133
  },
@@ -90,15 +145,25 @@ var Theme = {
90
145
  },
91
146
  Widget: {
92
147
  FontColor: "#afb9bb",
148
+ LabelColor: "#7f9497",
149
+ FontSize: 14,
150
+ Padding: 6,
151
+ Gap: 4,
93
152
  BackgroundColor: "#0f313c",
94
153
  Border: {
95
154
  Color: "#154050",
96
155
  Size: 2,
97
156
  Radius: 2
98
157
  },
158
+ Bevel: null,
99
159
  Hover: {
100
160
  BackgroundColor: "#195366"
101
161
  },
162
+ Input: {
163
+ BackgroundColor: "#0f313c",
164
+ HoverBackgroundColor: "#195366",
165
+ FontColor: "#afb9bb"
166
+ },
102
167
  Slider: {
103
168
  FillColor: "#07212A"
104
169
  },
@@ -111,8 +176,10 @@ var Theme = {
111
176
  ContextMenu: {
112
177
  BackgroundColor: "#07212A",
113
178
  HighlightColor: "#205A6D",
179
+ HighlightFontColor: "#afb9bb",
114
180
  FontColor: "#afb9bb",
115
- DisabledFontColor: "#4a585c"
181
+ DisabledFontColor: "#4a585c",
182
+ BorderRadius: 5
116
183
  },
117
184
  Minimap: {
118
185
  BackgroundColor: "rgba(10, 16, 18, 0.85)",
@@ -122,6 +189,13 @@ var Theme = {
122
189
  NodeColor: "#6b7f85",
123
190
  ViewportColor: "rgba(175, 185, 187, 0.25)"
124
191
  },
192
+ Popup: {
193
+ BackgroundColor: "#000000",
194
+ FontColor: "#ffffff",
195
+ FontFamily: "Verdana",
196
+ BorderRadius: 8,
197
+ Backdrop: "#00000070"
198
+ },
125
199
  Note: {
126
200
  FontColor: "#afb9bb",
127
201
  FontSize: 16,
@@ -140,11 +214,47 @@ var Theme = {
140
214
  },
141
215
  CodeBlock: {
142
216
  BackgroundColor: "#0c2b35",
217
+ FontColor: "#afb9bb",
143
218
  Padding: 16,
144
219
  BorderRadius: 4
145
220
  }
146
221
  }
147
222
  };
223
+ function clone(value) {
224
+ return JSON.parse(JSON.stringify(value));
225
+ }
226
+ function merge(target, source) {
227
+ if (source === void 0) {
228
+ return target;
229
+ }
230
+ if (source === null || typeof source !== "object" || target === null || typeof target !== "object") {
231
+ return source;
232
+ }
233
+ for (const key of Object.keys(source)) {
234
+ target[key] = merge(target[key], source[key]);
235
+ }
236
+ return target;
237
+ }
238
+ var DefaultTheme = Object.freeze(clone(defaults));
239
+ var Theme = clone(defaults);
240
+ var version = 0;
241
+ var listeners = /* @__PURE__ */ new Set();
242
+ function setTheme(config) {
243
+ const next = merge(clone(defaults), config);
244
+ for (const key of Object.keys(next)) {
245
+ Theme[key] = next[key];
246
+ }
247
+ version++;
248
+ listeners.forEach((listener) => listener());
249
+ requestRender();
250
+ }
251
+ function themeVersion() {
252
+ return version;
253
+ }
254
+ function onThemeChange(listener) {
255
+ listeners.add(listener);
256
+ return () => listeners.delete(listener);
257
+ }
148
258
 
149
259
  // src/styles/text.ts
150
260
  var DefaultSize = 16;
@@ -167,19 +277,19 @@ var TextStyle = class {
167
277
  constructor(config) {
168
278
  this.#size = config?.size === void 0 ? DefaultSize : config.size;
169
279
  this.#color = config?.color === void 0 ? DefaultColor : config.color;
170
- this.#font = config?.font === void 0 ? Theme.FontFamily : config.font;
280
+ this.#font = config?.font;
171
281
  this.#weight = config?.weight === void 0 ? "" /* Normal */ : config.weight;
172
282
  this.#fontStyle = config?.style === void 0 ? "" /* Normal */ : config.style;
173
283
  }
174
284
  setupStyle(ctx, scale) {
175
285
  ctx.fillStyle = this.#color;
176
- ctx.font = this.#fontStyle + " " + this.#weight + " " + this.#size * scale + "px " + this.#font;
286
+ ctx.font = this.#fontStyle + " " + this.#weight + " " + this.#size * scale + "px " + this.getFont();
177
287
  }
178
288
  getSize() {
179
289
  return this.#size;
180
290
  }
181
291
  getFont() {
182
- return this.#font;
292
+ return this.#font ?? Theme.FontFamily;
183
293
  }
184
294
  getWeight() {
185
295
  return this.#weight;
@@ -550,7 +660,18 @@ var ContextMenu = class _ContextMenu {
550
660
  }
551
661
  #tempBox = { Position: Zero(), Size: Zero() };
552
662
  #openSubMenu;
553
- render(ctx, pppp, graphScale, mousePosition, openRight) {
663
+ #bounds = null;
664
+ #contains(point) {
665
+ if (this.#bounds !== null && InBox(this.#bounds, point)) {
666
+ return true;
667
+ }
668
+ return this.#openSubMenu !== void 0 && this.#openSubMenu.#contains(point);
669
+ }
670
+ /**
671
+ * A submenu is anchored to its parent's left edge and told the parent's
672
+ * width, so it can open on whichever side has room.
673
+ */
674
+ render(ctx, anchor, graphScale, mousePosition, openRight, parentWidth = 0) {
554
675
  const menuScale = 1.25;
555
676
  const scaledEntryHeight = menuScale * contextEntryHeight;
556
677
  const scaledEntryWidth = menuScale * 40 + this.#getMaxWidthForText(ctx, menuScale);
@@ -559,19 +680,35 @@ var ContextMenu = class _ContextMenu {
559
680
  totalScaledHeight += this.#groups.At(i).height();
560
681
  }
561
682
  totalScaledHeight *= menuScale;
562
- const position = { x: 0, y: 0 };
563
- CopyVector2(position, pppp);
564
- if (!openRight) {
565
- position.x -= scaledEntryWidth;
683
+ const canvasWidth = ctx.canvas.clientWidth;
684
+ const position = { x: anchor.x, y: anchor.y };
685
+ let opensRight = openRight;
686
+ if (parentWidth === 0) {
687
+ if (!openRight) {
688
+ position.x -= scaledEntryWidth;
689
+ }
690
+ if (openRight && position.x + scaledEntryWidth > canvasWidth) {
691
+ position.x = canvasWidth - scaledEntryWidth;
692
+ opensRight = false;
693
+ }
694
+ } else {
695
+ const rightOfParent = anchor.x + parentWidth;
696
+ const leftOfParent = anchor.x - scaledEntryWidth;
697
+ const fitsRight = rightOfParent + scaledEntryWidth <= canvasWidth;
698
+ const fitsLeft = leftOfParent >= 0;
699
+ opensRight = openRight ? fitsRight || !fitsLeft : !fitsLeft && fitsRight;
700
+ position.x = opensRight ? rightOfParent : leftOfParent;
701
+ position.x = Math.max(0, Math.min(position.x, canvasWidth - scaledEntryWidth));
566
702
  }
567
703
  if (position.y + totalScaledHeight > ctx.canvas.clientHeight) {
568
704
  position.y = ctx.canvas.clientHeight - totalScaledHeight;
569
705
  }
570
- let submenuOpenRight = openRight;
571
- if (openRight && position.x + scaledEntryWidth > ctx.canvas.clientWidth) {
572
- position.x = ctx.canvas.clientWidth - scaledEntryWidth;
573
- submenuOpenRight = !submenuOpenRight;
574
- }
706
+ position.y = Math.max(0, position.y);
707
+ this.#bounds = {
708
+ Position: { x: position.x, y: position.y },
709
+ Size: { x: scaledEntryWidth, y: totalScaledHeight }
710
+ };
711
+ const pointer = mousePosition !== void 0 && this.#openSubMenu !== void 0 && this.#openSubMenu.#contains(mousePosition) ? void 0 : mousePosition;
575
712
  this.#tempBox.Size.x = scaledEntryWidth;
576
713
  this.#tempBox.Size.y = scaledEntryHeight;
577
714
  CopyVector2(this.#tempBox.Position, position);
@@ -585,36 +722,36 @@ var ContextMenu = class _ContextMenu {
585
722
  this.#tempBox.Position.y,
586
723
  scaledEntryWidth,
587
724
  totalScaledHeight,
588
- 5 * menuScale
725
+ Theme.ContextMenu.BorderRadius * menuScale
589
726
  );
590
727
  ctx.fill();
591
728
  ctx.shadowBlur = 0;
592
729
  let mouseIsOver = null;
593
- let subOpenedThisFrame = false;
730
+ if (pointer !== void 0) {
731
+ let row = 0;
732
+ for (let groupIndex = 0; groupIndex < this.#groups.Count(); groupIndex++) {
733
+ for (const entry of this.#groups.At(groupIndex).entries) {
734
+ this.#tempBox.Position.y = position.y + scaledEntryHeight * row;
735
+ if (InBox(this.#tempBox, pointer)) {
736
+ mouseIsOver = entry;
737
+ this.#openSubMenu = entry.subMenu;
738
+ this.#submenuPosition.x = position.x;
739
+ this.#submenuPosition.y = this.#tempBox.Position.y;
740
+ }
741
+ row++;
742
+ }
743
+ }
744
+ }
594
745
  let optionsRendered = 0;
595
746
  for (let groupIndex = 0; groupIndex < this.#groups.Count(); groupIndex++) {
596
747
  const group = this.#groups.At(groupIndex);
597
748
  for (let entryIndex = 0; entryIndex < group.entries.length; entryIndex++) {
598
749
  const entry = group.entries[entryIndex];
599
750
  this.#tempBox.Position.y = position.y + scaledEntryHeight * optionsRendered;
600
- let entryMousedOver = false;
601
- if (mousePosition !== void 0 && InBox(this.#tempBox, mousePosition)) {
602
- mouseIsOver = entry;
603
- entryMousedOver = true;
604
- if (entry.subMenu !== void 0) {
605
- this.#openSubMenu = entry.subMenu;
606
- this.#submenuPosition.x = position.x;
607
- this.#submenuPosition.y = this.#tempBox.Position.y;
608
- if (submenuOpenRight) {
609
- this.#submenuPosition.x += scaledEntryWidth;
610
- }
611
- subOpenedThisFrame = true;
612
- } else {
613
- this.#openSubMenu = void 0;
614
- }
615
- }
751
+ const entryMousedOver = entry === mouseIsOver;
616
752
  const itemDisabled = entry.item !== void 0 && entry.item.state() === 1 /* Disabled */;
617
- if (!itemDisabled && (entryMousedOver || this.#openSubMenu !== void 0 && entry.subMenu === this.#openSubMenu)) {
753
+ const highlighted = !itemDisabled && (entryMousedOver || this.#openSubMenu !== void 0 && entry.subMenu === this.#openSubMenu);
754
+ if (highlighted) {
618
755
  ctx.fillStyle = Theme.ContextMenu.HighlightColor;
619
756
  ctx.beginPath();
620
757
  ctx.roundRect(
@@ -622,18 +759,20 @@ var ContextMenu = class _ContextMenu {
622
759
  this.#tempBox.Position.y + scaledEntryHeight / 10,
623
760
  scaledEntryWidth - scaledEntryHeight / 5,
624
761
  scaledEntryHeight - scaledEntryHeight / 5,
625
- 5 * menuScale
762
+ Theme.ContextMenu.BorderRadius * menuScale
626
763
  );
627
764
  ctx.fill();
628
765
  }
629
766
  this.#textStyle.setupStyle(ctx, menuScale);
630
767
  if (itemDisabled) {
631
768
  ctx.fillStyle = Theme.ContextMenu.DisabledFontColor;
769
+ } else if (highlighted) {
770
+ ctx.fillStyle = Theme.ContextMenu.HighlightFontColor;
632
771
  }
633
772
  ctx.fillText(entry.text, position.x + scaledEntryHeight / 5, this.#tempBox.Position.y + scaledEntryHeight / 2);
634
773
  if (entry.subMenu !== void 0) {
635
774
  ctx.beginPath();
636
- ctx.strokeStyle = Theme.ContextMenu.FontColor;
775
+ ctx.strokeStyle = highlighted ? Theme.ContextMenu.HighlightFontColor : Theme.ContextMenu.FontColor;
637
776
  ctx.lineWidth = 1 * menuScale;
638
777
  ctx.lineTo(position.x + scaledEntryWidth - scaledEntryHeight / 2.5, this.#tempBox.Position.y + scaledEntryHeight / 3);
639
778
  ctx.lineTo(position.x + scaledEntryWidth - scaledEntryHeight / 4, this.#tempBox.Position.y + scaledEntryHeight / 2);
@@ -654,7 +793,7 @@ var ContextMenu = class _ContextMenu {
654
793
  }
655
794
  }
656
795
  if (this.#openSubMenu !== void 0) {
657
- const mouseOverSub = this.#openSubMenu.render(ctx, this.#submenuPosition, menuScale, mousePosition, submenuOpenRight);
796
+ const mouseOverSub = this.#openSubMenu.render(ctx, this.#submenuPosition, menuScale, mousePosition, opensRight, scaledEntryWidth);
658
797
  if (mouseOverSub !== null) {
659
798
  mouseIsOver = mouseOverSub;
660
799
  }
@@ -663,43 +802,6 @@ var ContextMenu = class _ContextMenu {
663
802
  }
664
803
  };
665
804
 
666
- // src/render_scheduler.ts
667
- var subscribers = /* @__PURE__ */ new Set();
668
- var frameQueued = false;
669
- var continuous = 0;
670
- var canSchedule = () => typeof requestAnimationFrame === "function";
671
- function onRenderRequested(redraw) {
672
- subscribers.add(redraw);
673
- return () => {
674
- subscribers.delete(redraw);
675
- };
676
- }
677
- function requestRender() {
678
- if (frameQueued || !canSchedule()) {
679
- return;
680
- }
681
- frameQueued = true;
682
- requestAnimationFrame(() => {
683
- frameQueued = false;
684
- subscribers.forEach((redraw) => redraw());
685
- if (continuous > 0) {
686
- requestRender();
687
- }
688
- });
689
- }
690
- function renderContinuously() {
691
- continuous++;
692
- requestRender();
693
- let released = false;
694
- return () => {
695
- if (released) {
696
- return;
697
- }
698
- released = true;
699
- continuous--;
700
- };
701
- }
702
-
703
805
  // src/input.ts
704
806
  var LONG_PRESS_MS = 500;
705
807
  var LONG_PRESS_SLOP = 12;
@@ -1284,6 +1386,7 @@ function splitStringIntoLines(ctx, str, maxWidth) {
1284
1386
  // src/types/text.ts
1285
1387
  var Text = class _Text {
1286
1388
  #measured;
1389
+ #measuredForTheme = -1;
1287
1390
  #size;
1288
1391
  #style;
1289
1392
  #value;
@@ -1363,9 +1466,10 @@ var Text = class _Text {
1363
1466
  return results;
1364
1467
  }
1365
1468
  #measure(ctx) {
1366
- if (this.#measured) {
1469
+ if (this.#measured && this.#measuredForTheme === themeVersion()) {
1367
1470
  return;
1368
1471
  }
1472
+ this.#measuredForTheme = themeVersion();
1369
1473
  this.#style.setupStyle(ctx, 1);
1370
1474
  if (this.#maxWidth == -1) {
1371
1475
  const measurements = ctx.measureText(this.#value);
@@ -1441,7 +1545,7 @@ function RgbToHex(c) {
1441
1545
  return "#" + componentToHex(Math.round(c.r * 255)) + componentToHex(Math.round(c.g * 255)) + componentToHex(Math.round(c.b * 255));
1442
1546
  }
1443
1547
  function HexToColor(hex) {
1444
- var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
1548
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})(?:[a-f\d]{2})?$/i.exec(hex);
1445
1549
  return result ? {
1446
1550
  r: parseInt(result[1], 16) / 255,
1447
1551
  g: parseInt(result[2], 16) / 255,
@@ -1531,6 +1635,21 @@ function portConfigTypes(config) {
1531
1635
  }
1532
1636
  return [config.type === void 0 ? "" : config.type];
1533
1637
  }
1638
+ var ARRAY_SLOT_PITCH = 12;
1639
+ function arrayInsertIndex(box, count, y) {
1640
+ if (count === 0) {
1641
+ return 0;
1642
+ }
1643
+ const index = Math.round((y - box.Position.y) / (box.Size.y / count));
1644
+ return Math.min(count, Math.max(0, index));
1645
+ }
1646
+ function arraySlotCenter(box, count, index) {
1647
+ const slots = Math.max(1, count);
1648
+ return {
1649
+ x: box.Position.x + box.Size.x / 2,
1650
+ y: box.Position.y + (index + 0.5) * box.Size.y / slots
1651
+ };
1652
+ }
1534
1653
  function fallbackColor(type, s) {
1535
1654
  let value = 0;
1536
1655
  for (var i = 0; i < type.length; i++) {
@@ -1555,6 +1674,7 @@ var Port = class {
1555
1674
  #acceptedTypes;
1556
1675
  #description;
1557
1676
  #array;
1677
+ #hidden;
1558
1678
  #configuredEmptyFill;
1559
1679
  #configuredFilledFill;
1560
1680
  #dataTypePopupElement;
@@ -1570,6 +1690,7 @@ var Port = class {
1570
1690
  this.#acceptedTypes = config?.acceptedTypes === void 0 ? [] : config.acceptedTypes;
1571
1691
  this.#description = config?.description === void 0 ? "" : config.description;
1572
1692
  this.#array = config?.array === true;
1693
+ this.#hidden = config?.hidden === true;
1573
1694
  this.#configuredEmptyFill = config?.emptyStyle?.fillColor;
1574
1695
  this.#configuredFilledFill = config?.filledStyle?.fillColor;
1575
1696
  this.#emptyStyle = {
@@ -1668,9 +1789,11 @@ var Port = class {
1668
1789
  this.setDataType(type);
1669
1790
  this.#refreshDataType();
1670
1791
  }
1671
- addConnection(connection) {
1672
- const c = this.#connections.length;
1673
- this.#connections.push(connection);
1792
+ addConnection(connection, index) {
1793
+ const c = index === void 0 ? this.#connections.length : Math.min(Math.max(index, 0), this.#connections.length);
1794
+ const was = this.hidden();
1795
+ this.#connections.splice(c, 0, connection);
1796
+ this.#visibilityMayHaveChanged(was);
1674
1797
  for (let i = 0; i < this.#onConnectionAdded.length; i++) {
1675
1798
  this.#onConnectionAdded[i](connection, c, this, this.#portType, this.#node);
1676
1799
  }
@@ -1691,6 +1814,32 @@ var Port = class {
1691
1814
  connections() {
1692
1815
  return this.#connections;
1693
1816
  }
1817
+ /**
1818
+ * How many wires the port has room for: one per connection on an array
1819
+ * input, so each can be told apart and picked up on its own.
1820
+ */
1821
+ slots() {
1822
+ if (this.#portType !== "INPUTARRAY" /* InputArray */) {
1823
+ return 1;
1824
+ }
1825
+ return Math.max(1, this.#connections.length);
1826
+ }
1827
+ setHidden(hidden) {
1828
+ const was = this.hidden();
1829
+ this.#hidden = hidden;
1830
+ this.#visibilityMayHaveChanged(was);
1831
+ }
1832
+ #visibilityMayHaveChanged(was) {
1833
+ if (was !== this.hidden() || this.#portType === "INPUTARRAY" /* InputArray */) {
1834
+ this.#node?.invalidateLayout();
1835
+ }
1836
+ }
1837
+ /**
1838
+ * A port asked to hide still shows while anything is connected to it.
1839
+ */
1840
+ hidden() {
1841
+ return this.#hidden && this.#connections.length === 0;
1842
+ }
1694
1843
  addConnectionRemovedListener(callback) {
1695
1844
  if (callback === void 0) {
1696
1845
  return;
@@ -1700,7 +1849,9 @@ var Port = class {
1700
1849
  clearConnection(connection) {
1701
1850
  const index = this.#connections.indexOf(connection);
1702
1851
  if (index > -1) {
1852
+ const was = this.hidden();
1703
1853
  this.#connections.splice(index, 1);
1854
+ this.#visibilityMayHaveChanged(was);
1704
1855
  for (let i = 0; i < this.#onConnectionRemoved.length; i++) {
1705
1856
  this.#onConnectionRemoved[i](connection, index, this, this.#portType, this.#node);
1706
1857
  }
@@ -1759,20 +1910,31 @@ var Port = class {
1759
1910
  this.#dataTypePopupElement.render(ctx, { x: xPos - size.x / 2, y: yPos }, 1, size);
1760
1911
  });
1761
1912
  }
1913
+ const slots = this.slots();
1914
+ const height2 = slots === 1 ? scaledRadius * 2 : slots * ARRAY_SLOT_PITCH * camera.zoom;
1762
1915
  this.#box.Position.x = position.x - scaledRadius;
1763
- this.#box.Position.y = position.y - scaledRadius;
1916
+ this.#box.Position.y = position.y - height2 / 2;
1764
1917
  this.#box.Size.x = scaledRadius * 2;
1765
- this.#box.Size.y = scaledRadius * 2;
1918
+ this.#box.Size.y = height2;
1766
1919
  ctx.strokeStyle = style.borderColor;
1767
1920
  ctx.fillStyle = style.fillColor;
1768
1921
  ctx.beginPath();
1769
1922
  if (this.#portType === "INPUTARRAY" /* InputArray */) {
1770
- ctx.rect(position.x - scaledRadius, position.y - scaledRadius, scaledRadius * 2, scaledRadius * 2);
1923
+ ctx.roundRect(this.#box.Position.x, this.#box.Position.y, this.#box.Size.x, this.#box.Size.y, slots === 1 ? 0 : scaledRadius);
1771
1924
  } else {
1772
1925
  ctx.arc(position.x, position.y, scaledRadius, 0, 2 * Math.PI);
1773
1926
  }
1774
1927
  ctx.fill();
1775
1928
  ctx.stroke();
1929
+ if (slots > 1) {
1930
+ ctx.fillStyle = style.borderColor;
1931
+ for (let i = 0; i < slots; i++) {
1932
+ const slot = arraySlotCenter(this.#box, slots, i);
1933
+ ctx.beginPath();
1934
+ ctx.arc(slot.x, slot.y, scaledRadius * 0.4, 0, 2 * Math.PI);
1935
+ ctx.fill();
1936
+ }
1937
+ }
1776
1938
  return this.#box;
1777
1939
  }
1778
1940
  };
@@ -1888,142 +2050,330 @@ var BoxStyle = class {
1888
2050
  }
1889
2051
  };
1890
2052
 
1891
- // src/popup.ts
1892
- var Popup = class {
1893
- #title;
1894
- #options;
1895
- #content;
1896
- #onClose;
1897
- // Styling ================================================================
1898
- #buttonCSS;
1899
- // Runtime ================================================================
1900
- #popup;
1901
- #keyHandler;
1902
- constructor(config) {
1903
- this.#title = config.title;
1904
- this.#options = config.options;
1905
- this.#content = config.content;
1906
- this.#onClose = config.onClose;
1907
- this.#buttonCSS = config.buttonCSS === void 0 ? "flex-grow: 1; margin-right: 8px;" : config.buttonCSS;
1908
- this.#popup = null;
1909
- this.#keyHandler = null;
1910
- }
1911
- Hide() {
1912
- if (this.#popup === null) {
1913
- return;
1914
- }
1915
- if (this.#keyHandler !== null) {
1916
- document.removeEventListener("keydown", this.#keyHandler, true);
1917
- this.#keyHandler = null;
1918
- }
1919
- document.body.removeChild(this.#popup);
1920
- this.#popup = null;
2053
+ // src/styles/cursor.ts
2054
+ var CursorStyle = /* @__PURE__ */ ((CursorStyle2) => {
2055
+ CursorStyle2["Default"] = "default";
2056
+ CursorStyle2["Pointer"] = "pointer";
2057
+ CursorStyle2["Grab"] = "grab";
2058
+ CursorStyle2["Grabbing"] = "grabbing";
2059
+ CursorStyle2["Wait"] = "wait";
2060
+ CursorStyle2["Help"] = "help";
2061
+ CursorStyle2["Crosshair"] = "crosshair";
2062
+ CursorStyle2["Cell"] = "cell";
2063
+ CursorStyle2["Text"] = "text";
2064
+ CursorStyle2["Move"] = "move";
2065
+ CursorStyle2["NotAllowed"] = "not-allowed";
2066
+ CursorStyle2["ZoomIn"] = "zoom-in";
2067
+ CursorStyle2["ZoomOut"] = "zoom-out";
2068
+ CursorStyle2["None"] = "none";
2069
+ CursorStyle2["ContextMenu"] = "context-menu";
2070
+ CursorStyle2["EWResize"] = "ew-resize";
2071
+ return CursorStyle2;
2072
+ })(CursorStyle || {});
2073
+
2074
+ // src/styles/bevel.ts
2075
+ function drawBevel(ctx, box, scale, colors, sunken) {
2076
+ const width2 = Math.max(1, 2 * scale);
2077
+ const half = width2 / 2;
2078
+ const left = box.Position.x + half;
2079
+ const top = box.Position.y + half;
2080
+ const right = box.Position.x + box.Size.x - half;
2081
+ const bottom = box.Position.y + box.Size.y - half;
2082
+ ctx.save();
2083
+ ctx.lineWidth = width2;
2084
+ ctx.lineCap = "square";
2085
+ ctx.strokeStyle = sunken ? colors.Dark : colors.Light;
2086
+ ctx.beginPath();
2087
+ ctx.moveTo(left, bottom);
2088
+ ctx.lineTo(left, top);
2089
+ ctx.lineTo(right, top);
2090
+ ctx.stroke();
2091
+ ctx.strokeStyle = sunken ? colors.Light : colors.Dark;
2092
+ ctx.beginPath();
2093
+ ctx.moveTo(right, top);
2094
+ ctx.lineTo(right, bottom);
2095
+ ctx.lineTo(left, bottom);
2096
+ ctx.stroke();
2097
+ ctx.restore();
2098
+ }
2099
+
2100
+ // src/widgets/field.ts
2101
+ function widgetFont(scale) {
2102
+ return `${Theme.Widget.FontSize * scale}px ${Theme.FontFamily}`;
2103
+ }
2104
+ function fieldBackground(look) {
2105
+ if (look.background !== void 0) {
2106
+ return look.background;
1921
2107
  }
1922
- #close(option) {
1923
- this.Hide();
1924
- if (this.#onClose !== void 0) {
1925
- this.#onClose(option);
1926
- }
2108
+ if (look.input) {
2109
+ return look.hovered ? Theme.Widget.Input.HoverBackgroundColor : Theme.Widget.Input.BackgroundColor;
1927
2110
  }
1928
- Show() {
1929
- this.#popup = document.createElement("div");
1930
- this.#popup.style.cssText = "position:absolute;top:0;width:100%;display:flex;height:100%;justify-content:center;align-items:center;background-color:#00000070;";
1931
- document.body.appendChild(this.#popup);
1932
- const container = document.createElement("div");
1933
- container.style.cssText = "background-color:#000;color:white;font-family:Verdana;border-radius:8px;padding:16px;";
1934
- this.#popup.appendChild(container);
1935
- const title = document.createElement("h2");
1936
- title.textContent = this.#title;
1937
- title.style.cssText = "margin-top:0;margin-bottom:16px;";
1938
- container.appendChild(title);
1939
- if (this.#content !== void 0) {
1940
- container.appendChild(this.#content());
1941
- }
1942
- const buttonContainer = document.createElement("div");
1943
- buttonContainer.style.cssText = "margin-top:16px;justify-content:space-around;align-items:center;flex-direction:row;display:flex;";
1944
- container.appendChild(buttonContainer);
1945
- for (let i = 0; i < this.#options.length; i++) {
1946
- const button = document.createElement("button");
1947
- button.style.cssText = this.#buttonCSS;
1948
- button.textContent = this.#options[i];
1949
- button.onclick = () => this.#close(this.#options[i]);
1950
- buttonContainer.appendChild(button);
1951
- }
1952
- this.#keyHandler = (e) => {
1953
- if (e.key === "Enter") {
1954
- e.preventDefault();
1955
- this.#close(this.#options.length > 0 ? this.#options[0] : null);
1956
- } else if (e.key === "Escape") {
1957
- e.preventDefault();
1958
- this.#close(null);
1959
- }
1960
- e.stopPropagation();
1961
- };
1962
- document.addEventListener("keydown", this.#keyHandler, true);
2111
+ if (look.pressed) {
2112
+ return Theme.Widget.Button.Click.BackgroundColor;
1963
2113
  }
1964
- };
1965
-
1966
- // src/styles/textBox.ts
1967
- function TextBoxStyleWithFallback(input, fallback) {
1968
- return {
1969
- box: BoxStyleWithFallback(input?.box, fallback?.box),
1970
- text: TextStyleFallback(input?.text, fallback?.text),
1971
- textAlign: input?.textAlign === void 0 ? fallback?.textAlign : input?.textAlign,
1972
- textBaseline: input?.textBaseline === void 0 ? fallback?.textBaseline : input?.textBaseline
1973
- };
2114
+ return look.hovered ? Theme.Widget.Hover.BackgroundColor : Theme.Widget.BackgroundColor;
1974
2115
  }
1975
- var TextBoxStyle = class {
1976
- #box;
1977
- #text;
1978
- #textAlign;
1979
- #textBaseline;
1980
- constructor(config) {
1981
- this.#box = new BoxStyle(config?.box);
1982
- this.#text = new TextStyle(config?.text);
1983
- this.#textAlign = config?.textAlign === void 0 ? "center" /* Center */ : config.textAlign;
1984
- this.#textBaseline = config?.textBaseline === void 0 ? "middle" /* Middle */ : config.textBaseline;
1985
- }
1986
- Draw(ctx, box, scale, text) {
1987
- this.#box.Draw(ctx, box, scale);
1988
- ctx.textAlign = this.#textAlign;
1989
- ctx.textBaseline = this.#textBaseline;
1990
- this.#text.setupStyle(ctx, scale);
1991
- ctx.fillText(
1992
- text,
1993
- box.Position.x + box.Size.x / 2,
1994
- box.Position.y + box.Size.y / 2
1995
- );
2116
+ function drawFieldBox(ctx, box, scale, look) {
2117
+ ctx.fillStyle = fieldBackground(look);
2118
+ ctx.beginPath();
2119
+ ctx.roundRect(box.Position.x, box.Position.y, box.Size.x, box.Size.y, Theme.Widget.Border.Radius * scale);
2120
+ ctx.fill();
2121
+ if (Theme.Widget.Border.Size > 0) {
2122
+ ctx.strokeStyle = look.border ?? Theme.Widget.Border.Color;
2123
+ ctx.lineWidth = Theme.Widget.Border.Size * scale;
2124
+ ctx.stroke();
1996
2125
  }
1997
- DrawUnderline(ctx, box, scale, text) {
1998
- this.#box.DrawUnderline(ctx, box, scale);
1999
- ctx.textAlign = this.#textAlign;
2000
- ctx.textBaseline = this.#textBaseline;
2001
- this.#text.setupStyle(ctx, scale);
2126
+ if (Theme.Widget.Bevel !== null) {
2127
+ drawBevel(ctx, box, scale, Theme.Widget.Bevel, look.input === true || look.pressed === true);
2128
+ }
2129
+ }
2130
+ function drawField(ctx, box, scale, look) {
2131
+ drawFieldBox(ctx, box, scale, look);
2132
+ drawFieldText(ctx, box, scale, look);
2133
+ }
2134
+ function drawFieldText(ctx, box, scale, look) {
2135
+ const padding = Theme.Widget.Padding * scale;
2136
+ const middle = box.Position.y + box.Size.y / 2;
2137
+ const textColor = look.textColor ?? (look.input ? Theme.Widget.Input.FontColor : Theme.Widget.FontColor);
2138
+ ctx.font = widgetFont(scale);
2139
+ ctx.textBaseline = "middle";
2140
+ if (look.label === void 0 || look.label === "") {
2141
+ ctx.textAlign = "center";
2142
+ ctx.fillStyle = textColor;
2002
2143
  ctx.fillText(
2003
- text,
2144
+ fitString(ctx, look.value, box.Size.x - padding * 2),
2004
2145
  box.Position.x + box.Size.x / 2,
2005
- box.Position.y + box.Size.y / 2
2146
+ middle
2006
2147
  );
2148
+ return;
2007
2149
  }
2008
- setTextColor(color) {
2009
- this.#text.setColor(color);
2010
- }
2011
- setBoxColor(color) {
2012
- this.#box.setColor(color);
2013
- }
2014
- setBorderColor(color) {
2015
- this.#box.setBorderColor(color);
2150
+ const room = box.Size.x - padding * 2;
2151
+ const label = fitString(ctx, look.label, room / 2);
2152
+ const labelWidth = ctx.measureText(label).width;
2153
+ ctx.textAlign = "left";
2154
+ ctx.fillStyle = look.textColor ?? Theme.Widget.LabelColor;
2155
+ ctx.fillText(label, box.Position.x + padding, middle);
2156
+ ctx.textAlign = "right";
2157
+ ctx.fillStyle = textColor;
2158
+ ctx.fillText(
2159
+ fitString(ctx, look.value, room - labelWidth - padding),
2160
+ box.Position.x + box.Size.x - padding,
2161
+ middle
2162
+ );
2163
+ }
2164
+
2165
+ // src/widgets/inplace.ts
2166
+ var open = /* @__PURE__ */ new Set();
2167
+ function placeEditsDuring(canvas, draw) {
2168
+ open.forEach((edit) => {
2169
+ if (edit.canvas === canvas) {
2170
+ edit.placed = false;
2171
+ }
2172
+ });
2173
+ draw();
2174
+ open.forEach((edit) => {
2175
+ if (edit.canvas === canvas && !edit.placed) {
2176
+ edit.input.style.opacity = "0";
2177
+ edit.input.style.pointerEvents = "none";
2178
+ }
2179
+ });
2180
+ }
2181
+ function editInPlace(config) {
2182
+ const canvas = config.canvas;
2183
+ if (canvas === null) {
2184
+ return null;
2016
2185
  }
2017
- };
2186
+ const input = document.createElement("input");
2187
+ input.type = "text";
2188
+ input.value = config.value;
2189
+ const edit = { canvas, input, placed: true };
2190
+ const place = (box, scale) => {
2191
+ const rect = canvas.getBoundingClientRect();
2192
+ const sx = canvas.width > 0 ? rect.width / canvas.width : 1;
2193
+ const sy = canvas.height > 0 ? rect.height / canvas.height : 1;
2194
+ const left = rect.left + box.Position.x * sx;
2195
+ const top = rect.top + box.Position.y * sy;
2196
+ const width2 = box.Size.x * sx;
2197
+ const height2 = box.Size.y * sy;
2198
+ const clip = [
2199
+ Math.max(0, rect.top - top),
2200
+ Math.max(0, left + width2 - (rect.left + rect.width)),
2201
+ Math.max(0, top + height2 - (rect.top + rect.height)),
2202
+ Math.max(0, rect.left - left)
2203
+ ];
2204
+ edit.placed = true;
2205
+ input.style.cssText = [
2206
+ "position: fixed",
2207
+ `left: ${left}px`,
2208
+ `top: ${top}px`,
2209
+ `width: ${width2}px`,
2210
+ `height: ${height2}px`,
2211
+ `clip-path: inset(${clip.map((c) => `${c}px`).join(" ")})`,
2212
+ "box-sizing: border-box",
2213
+ "margin: 0",
2214
+ `padding: 0 ${Theme.Widget.Padding * scale}px`,
2215
+ `font: ${Theme.Widget.FontSize * scale * sy}px ${Theme.FontFamily}`,
2216
+ `color: ${Theme.Widget.FontColor}`,
2217
+ `background: ${Theme.Widget.Hover.BackgroundColor}`,
2218
+ `border: ${Theme.Widget.Border.Size * scale}px solid ${Theme.Widget.Border.Color}`,
2219
+ `border-radius: ${Theme.Widget.Border.Radius * scale}px`,
2220
+ "outline: none",
2221
+ "z-index: 1000"
2222
+ ].join(";");
2223
+ };
2224
+ place(config.box, config.scale);
2225
+ let done = false;
2226
+ const finish = (keep) => {
2227
+ if (done) {
2228
+ return;
2229
+ }
2230
+ done = true;
2231
+ open.delete(edit);
2232
+ input.remove();
2233
+ if (keep) {
2234
+ config.onCommit(input.value);
2235
+ }
2236
+ requestRender();
2237
+ };
2238
+ input.addEventListener("keydown", (event) => {
2239
+ event.stopPropagation();
2240
+ if (event.key === "Enter") {
2241
+ event.preventDefault();
2242
+ finish(true);
2243
+ } else if (event.key === "Escape") {
2244
+ event.preventDefault();
2245
+ finish(false);
2246
+ }
2247
+ });
2248
+ input.addEventListener("blur", () => finish(true));
2249
+ open.add(edit);
2250
+ document.body.appendChild(input);
2251
+ input.focus();
2252
+ input.select();
2253
+ return { input, place };
2254
+ }
2018
2255
 
2019
2256
  // src/widgets/widget.ts
2020
2257
  var width = 150;
2021
2258
  var height = 25;
2022
- function startingWidgetValue(node, property, configured, fallback) {
2023
- if (configured !== void 0) {
2024
- return configured;
2259
+ var WidgetBase = class {
2260
+ #hidden = false;
2261
+ #disabled = false;
2262
+ #hovered = false;
2263
+ #pressed = false;
2264
+ #box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
2265
+ #canvas = null;
2266
+ #scale = 1;
2267
+ #editing = null;
2268
+ Draw(ctx, box, scale, mousePosition) {
2269
+ this.#box = { Position: { x: box.Position.x, y: box.Position.y }, Size: { x: box.Size.x, y: box.Size.y } };
2270
+ this.#canvas = ctx.canvas ?? null;
2271
+ this.#scale = scale;
2272
+ this.#hovered = !this.#disabled && mousePosition !== void 0 && InBox(box, mousePosition);
2273
+ if (this.#editing !== null) {
2274
+ if (this.#editing.input.isConnected) {
2275
+ this.#editing.place(this.#box, scale);
2276
+ } else {
2277
+ this.#editing = null;
2278
+ }
2279
+ }
2280
+ ctx.save();
2281
+ if (this.#disabled) {
2282
+ ctx.globalAlpha *= 0.4;
2283
+ }
2284
+ this.render(ctx, box, scale);
2285
+ ctx.restore();
2025
2286
  }
2026
- if (property !== void 0 && property !== null) {
2287
+ PointerDown(pointer) {
2288
+ if (this.#disabled) {
2289
+ return;
2290
+ }
2291
+ this.#pressed = true;
2292
+ this.onPointerDown(pointer);
2293
+ requestRender();
2294
+ }
2295
+ PointerMove(pointer) {
2296
+ if (!this.#pressed) {
2297
+ return;
2298
+ }
2299
+ this.onPointerMove(pointer);
2300
+ requestRender();
2301
+ }
2302
+ PointerUp(pointer) {
2303
+ if (!this.#pressed) {
2304
+ return;
2305
+ }
2306
+ this.#pressed = false;
2307
+ this.onPointerUp(pointer);
2308
+ if (pointer.inside) {
2309
+ this.onClick(pointer);
2310
+ }
2311
+ requestRender();
2312
+ }
2313
+ Cursor() {
2314
+ return "pointer" /* Pointer */;
2315
+ }
2316
+ Hidden() {
2317
+ return this.#hidden;
2318
+ }
2319
+ setHidden(hidden) {
2320
+ this.#hidden = hidden;
2321
+ requestRender();
2322
+ }
2323
+ Disabled() {
2324
+ return this.#disabled;
2325
+ }
2326
+ setDisabled(disabled) {
2327
+ this.#disabled = disabled;
2328
+ if (disabled) {
2329
+ this.#pressed = false;
2330
+ }
2331
+ requestRender();
2332
+ }
2333
+ hovered() {
2334
+ return this.#hovered;
2335
+ }
2336
+ pressed() {
2337
+ return this.#pressed;
2338
+ }
2339
+ /**
2340
+ * Where the widget was last drawn, in canvas pixels.
2341
+ */
2342
+ box() {
2343
+ return this.#box;
2344
+ }
2345
+ canvas() {
2346
+ return this.#canvas;
2347
+ }
2348
+ scale() {
2349
+ return this.#scale;
2350
+ }
2351
+ editText(value, onCommit) {
2352
+ this.#editing = editInPlace({
2353
+ canvas: this.#canvas,
2354
+ box: this.#box,
2355
+ scale: this.#scale,
2356
+ value,
2357
+ onCommit
2358
+ });
2359
+ }
2360
+ onPointerDown(pointer) {
2361
+ }
2362
+ onPointerMove(pointer) {
2363
+ }
2364
+ onPointerUp(pointer) {
2365
+ }
2366
+ /**
2367
+ * A press and release that both landed on the widget.
2368
+ */
2369
+ onClick(pointer) {
2370
+ }
2371
+ };
2372
+ function startingWidgetValue(node, property, configured, fallback) {
2373
+ if (configured !== void 0) {
2374
+ return configured;
2375
+ }
2376
+ if (property !== void 0 && property !== null) {
2027
2377
  const existing = node.getProperty(property);
2028
2378
  if (existing !== void 0 && existing !== null) {
2029
2379
  return existing;
@@ -2032,187 +2382,192 @@ function startingWidgetValue(node, property, configured, fallback) {
2032
2382
  return fallback;
2033
2383
  }
2034
2384
 
2035
- // src/widgets/number.ts
2036
- var NumberWidget = class {
2385
+ // src/widgets/value.ts
2386
+ var ValueWidget = class extends WidgetBase {
2037
2387
  #node;
2038
- #nodeProperty;
2388
+ #property;
2039
2389
  #value;
2040
- #idleBoxStyle;
2041
- #highlightBoxStyle;
2042
- #text;
2390
+ #label;
2391
+ #width;
2043
2392
  #callback;
2044
- constructor(node, config) {
2045
- this.#value = 0;
2046
- this.#text = "0";
2393
+ #onInput;
2394
+ constructor(node, config, fallback) {
2395
+ super();
2047
2396
  this.#node = node;
2048
- this.#nodeProperty = config?.property;
2049
- this.#idleBoxStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idleBoxStyle, {
2050
- box: {
2051
- color: Theme.Widget.BackgroundColor,
2052
- border: {
2053
- size: Theme.Widget.Border.Size,
2054
- color: Theme.Widget.Border.Color
2055
- },
2056
- radius: Theme.Widget.Border.Radius
2057
- },
2058
- text: { color: Theme.Widget.FontColor }
2059
- }));
2060
- this.#highlightBoxStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.highlightBoxStyle, {
2061
- box: {
2062
- color: Theme.Widget.Hover.BackgroundColor,
2063
- border: {
2064
- size: Theme.Widget.Border.Size,
2065
- color: Theme.Widget.Border.Color
2066
- },
2067
- radius: Theme.Widget.Border.Radius
2068
- },
2069
- text: { color: Theme.Widget.FontColor }
2070
- }));
2071
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
2397
+ this.#property = config?.property;
2398
+ this.#label = config?.label ?? "";
2399
+ this.#width = config?.width ?? width;
2400
+ this.#value = startingWidgetValue(node, this.#property, config?.value, fallback);
2072
2401
  this.#callback = config?.callback;
2073
- if (this.#nodeProperty !== void 0) {
2074
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2075
- this.Set(newVal);
2076
- });
2402
+ this.#onInput = config?.onInput;
2403
+ if (this.#property !== void 0) {
2404
+ node.setProperty(this.#property, this.#value);
2405
+ node.addPropertyChangeListener(this.#property, (_, next) => this.setValue(next));
2077
2406
  }
2078
2407
  }
2079
2408
  Size() {
2080
- return { "x": width, "y": height };
2409
+ return { x: this.#width, y: height };
2081
2410
  }
2082
- Set(newNumber) {
2083
- if (Object.is(this.#value, newNumber)) {
2411
+ value() {
2412
+ return this.#value;
2413
+ }
2414
+ /**
2415
+ * Shows a new value without treating it as a person's edit: no callback
2416
+ * and no edit event.
2417
+ */
2418
+ setValue(value) {
2419
+ if (this.equal(value, this.#value)) {
2084
2420
  return;
2085
2421
  }
2086
- this.#value = newNumber;
2087
- if (this.#nodeProperty !== void 0) {
2088
- this.#node.setProperty(this.#nodeProperty, this.#value);
2422
+ this.#value = value;
2423
+ this.valueChanged();
2424
+ if (this.#property !== void 0) {
2425
+ this.#node.setProperty(this.#property, value);
2089
2426
  }
2090
- if (this.#callback !== void 0) {
2091
- this.#callback(this.#value);
2427
+ requestRender();
2428
+ }
2429
+ label() {
2430
+ return this.#label;
2431
+ }
2432
+ setLabel(label) {
2433
+ this.#label = label;
2434
+ requestRender();
2435
+ }
2436
+ node() {
2437
+ return this.#node;
2438
+ }
2439
+ input(value) {
2440
+ this.setValue(value);
2441
+ this.#onInput?.(value);
2442
+ }
2443
+ /**
2444
+ * Ends a person's edit. Nothing is reported when they ended where they
2445
+ * started.
2446
+ */
2447
+ commit(value, startedAt) {
2448
+ this.setValue(value);
2449
+ if (this.equal(value, startedAt)) {
2450
+ return;
2092
2451
  }
2093
- this.#text = "" + parseFloat(this.#value.toPrecision(6));
2452
+ if (this.#property !== void 0) {
2453
+ this.#node.editProperty(this.#property, value);
2454
+ }
2455
+ this.#callback?.(value);
2094
2456
  }
2095
- ClickStart() {
2457
+ valueChanged() {
2096
2458
  }
2097
- ClickEnd() {
2098
- let input = null;
2099
- const setOption = "Set";
2100
- const cancelOption = "Cancel";
2101
- const popup = new Popup({
2102
- title: "Set Number",
2103
- options: [setOption, cancelOption],
2104
- content: () => {
2105
- const container = document.createElement("div");
2106
- input = document.createElement("input");
2107
- input.type = "number";
2108
- input.valueAsNumber = this.#value;
2109
- container.append(input);
2110
- return container;
2111
- },
2112
- onClose: (button) => {
2113
- if (button !== setOption || input === null) {
2114
- return;
2115
- }
2116
- this.Set(input.valueAsNumber);
2117
- }
2459
+ equal(a, b) {
2460
+ return Object.is(a, b);
2461
+ }
2462
+ };
2463
+
2464
+ // src/widgets/number.ts
2465
+ var DRAG_THRESHOLD = 3;
2466
+ var NumberWidget = class extends ValueWidget {
2467
+ #step;
2468
+ #integer;
2469
+ #min;
2470
+ #max;
2471
+ #startX = 0;
2472
+ #startValue = 0;
2473
+ #dragging = false;
2474
+ #dragged = false;
2475
+ constructor(node, config) {
2476
+ super(node, config, 0);
2477
+ this.#step = config?.step;
2478
+ this.#integer = config?.integer === true;
2479
+ this.#min = config?.min ?? -Infinity;
2480
+ this.#max = config?.max ?? Infinity;
2481
+ }
2482
+ Cursor() {
2483
+ return "ew-resize" /* EWResize */;
2484
+ }
2485
+ render(ctx, box, scale) {
2486
+ drawField(ctx, box, scale, {
2487
+ input: true,
2488
+ hovered: this.hovered() || this.pressed(),
2489
+ label: this.label(),
2490
+ value: formatNumber(this.value())
2118
2491
  });
2119
- popup.Show();
2120
2492
  }
2121
- Draw(ctx, position, scale, mousePosition) {
2122
- const box = {
2123
- Position: { x: 0, y: 0 },
2124
- Size: {
2125
- x: width * scale,
2126
- y: height * scale
2127
- }
2128
- };
2129
- CopyVector2(box.Position, position);
2130
- let style = this.#idleBoxStyle;
2131
- if (mousePosition !== void 0) {
2132
- if (InBox(box, mousePosition)) {
2133
- style = this.#highlightBoxStyle;
2493
+ onPointerDown(pointer) {
2494
+ this.#startX = pointer.position.x;
2495
+ this.#startValue = this.value();
2496
+ this.#dragging = false;
2497
+ this.#dragged = false;
2498
+ }
2499
+ onPointerMove(pointer) {
2500
+ const dx = pointer.position.x - this.#startX;
2501
+ if (!this.#dragging && Math.abs(dx) < DRAG_THRESHOLD) {
2502
+ return;
2503
+ }
2504
+ this.#dragging = true;
2505
+ this.input(this.#clean(this.#startValue + dx * this.#stepFor(this.#startValue)));
2506
+ }
2507
+ onPointerUp() {
2508
+ this.#dragged = this.#dragging;
2509
+ if (this.#dragging) {
2510
+ this.#dragging = false;
2511
+ this.commit(this.value(), this.#startValue);
2512
+ }
2513
+ }
2514
+ onClick() {
2515
+ if (this.#dragged) {
2516
+ return;
2517
+ }
2518
+ const before = this.value();
2519
+ this.editText(String(before), (text) => {
2520
+ const parsed = Number.parseFloat(text);
2521
+ if (Number.isFinite(parsed)) {
2522
+ this.commit(this.#clean(parsed), before);
2134
2523
  }
2524
+ });
2525
+ }
2526
+ #stepFor(value) {
2527
+ if (this.#step !== void 0) {
2528
+ return this.#step;
2135
2529
  }
2136
- style.DrawUnderline(ctx, box, scale, this.#text);
2137
- return box;
2530
+ return Math.max(Math.abs(value) * 0.01, this.#integer ? 0.1 : 0.01);
2531
+ }
2532
+ #clean(value) {
2533
+ const rounded = this.#integer ? Math.round(value) : value;
2534
+ return Math.min(this.#max, Math.max(this.#min, rounded));
2138
2535
  }
2139
2536
  };
2537
+ function formatNumber(value) {
2538
+ return "" + parseFloat(value.toPrecision(6));
2539
+ }
2140
2540
 
2141
2541
  // src/widgets/button.ts
2142
- var ButtonWidget = class {
2542
+ var ButtonWidget = class extends WidgetBase {
2143
2543
  #text;
2144
- #idleStyle;
2145
- #hoverStyle;
2146
- #clickStyle;
2147
2544
  #callback;
2148
- #gettingClicked;
2545
+ #width;
2149
2546
  constructor(config) {
2150
- this.#text = config?.text === void 0 ? "Button" : config?.text;
2547
+ super();
2548
+ this.#text = config?.text ?? "Button";
2151
2549
  this.#callback = config?.callback;
2152
- this.#gettingClicked = false;
2153
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idleStyle, {
2154
- box: {
2155
- color: Theme.Widget.BackgroundColor,
2156
- border: {
2157
- size: Theme.Widget.Border.Size,
2158
- color: Theme.Widget.Border.Color
2159
- }
2160
- },
2161
- text: { color: Theme.Widget.FontColor }
2162
- }));
2163
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.hoverStyle, {
2164
- box: {
2165
- color: Theme.Widget.Hover.BackgroundColor,
2166
- border: {
2167
- size: Theme.Widget.Border.Size,
2168
- color: Theme.Widget.Border.Color
2169
- }
2170
- },
2171
- text: { color: Theme.Widget.FontColor }
2172
- }));
2173
- this.#clickStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.clickStyle, {
2174
- box: {
2175
- color: Theme.Widget.Button.Click.BackgroundColor,
2176
- border: {
2177
- size: Theme.Widget.Border.Size,
2178
- color: Theme.Widget.Border.Color
2179
- }
2180
- },
2181
- text: { color: Theme.Widget.FontColor }
2182
- }));
2550
+ this.#width = config?.width ?? width;
2183
2551
  }
2184
2552
  Size() {
2185
- return { "x": width, "y": height };
2553
+ return { x: this.#width, y: height };
2186
2554
  }
2187
- Draw(ctx, position, scale, mousePosition) {
2188
- const box = {
2189
- Position: { x: 0, y: 0 },
2190
- Size: {
2191
- x: width * scale,
2192
- y: height * scale
2193
- }
2194
- };
2195
- CopyVector2(box.Position, position);
2196
- let style = this.#idleStyle;
2197
- if (mousePosition !== void 0 && !this.#gettingClicked) {
2198
- if (InBox(box, mousePosition)) {
2199
- style = this.#hoverStyle;
2200
- }
2201
- }
2202
- if (this.#gettingClicked) {
2203
- style = this.#clickStyle;
2204
- }
2205
- style.Draw(ctx, box, scale, this.#text);
2206
- return box;
2555
+ text() {
2556
+ return this.#text;
2207
2557
  }
2208
- ClickStart() {
2209
- this.#gettingClicked = true;
2558
+ setText(text) {
2559
+ this.#text = text;
2560
+ requestRender();
2210
2561
  }
2211
- ClickEnd() {
2212
- this.#gettingClicked = false;
2213
- if (this.#callback !== void 0) {
2214
- this.#callback();
2215
- }
2562
+ render(ctx, box, scale) {
2563
+ drawField(ctx, box, scale, {
2564
+ hovered: this.hovered(),
2565
+ pressed: this.pressed(),
2566
+ value: this.#text
2567
+ });
2568
+ }
2569
+ onClick() {
2570
+ this.#callback?.();
2216
2571
  }
2217
2572
  };
2218
2573
 
@@ -2227,500 +2582,208 @@ function contrastColor(color) {
2227
2582
  }
2228
2583
  return "white";
2229
2584
  }
2230
- var ColorWidget = class {
2231
- #node;
2232
- #nodeProperty;
2233
- #value;
2585
+ var picker = null;
2586
+ function sharedPicker() {
2587
+ if (picker === null || !picker.isConnected) {
2588
+ picker = document.createElement("input");
2589
+ picker.type = "color";
2590
+ document.body.appendChild(picker);
2591
+ }
2592
+ return picker;
2593
+ }
2594
+ var ColorWidget = class extends ValueWidget {
2234
2595
  #contrast;
2235
- #textBoxStyle;
2236
- #callback;
2237
2596
  constructor(node, config) {
2238
- this.#node = node;
2239
- this.#nodeProperty = config?.property;
2240
- this.#callback = config?.callback;
2241
- this.#value = startingWidgetValue(node, this.#nodeProperty, config?.value, "#000000");
2242
- this.#contrast = contrastColor(this.#value);
2243
- this.#textBoxStyle = new TextBoxStyle({
2244
- box: {
2245
- color: this.#value,
2246
- border: {
2247
- color: this.#contrast,
2248
- size: Theme.Widget.Border.Size
2249
- }
2250
- },
2251
- text: config?.textStyle
2597
+ super(node, config, "#000000");
2598
+ this.#contrast = contrastColor(this.value());
2599
+ }
2600
+ valueChanged() {
2601
+ this.#contrast = contrastColor(this.value());
2602
+ }
2603
+ render(ctx, box, scale) {
2604
+ drawField(ctx, box, scale, {
2605
+ input: true,
2606
+ hovered: this.hovered(),
2607
+ label: this.label(),
2608
+ value: this.value(),
2609
+ background: this.value(),
2610
+ border: this.#contrast,
2611
+ textColor: this.#contrast
2252
2612
  });
2253
- if (this.#nodeProperty !== void 0) {
2254
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2255
- this.Set(newVal);
2256
- });
2257
- }
2258
2613
  }
2259
- Size() {
2260
- return { "x": width, "y": height };
2261
- }
2262
- Set(value) {
2263
- if (this.#value === value) {
2614
+ onClick() {
2615
+ const canvas = this.canvas();
2616
+ if (canvas === null) {
2264
2617
  return;
2265
2618
  }
2266
- this.#value = value;
2267
- if (this.#callback !== void 0) {
2268
- this.#callback(this.#value);
2269
- }
2270
- if (this.#nodeProperty !== void 0) {
2271
- this.#node.setProperty(this.#nodeProperty, this.#value);
2272
- }
2273
- this.#contrast = contrastColor(this.#value);
2274
- this.#textBoxStyle.setBoxColor(this.#value);
2275
- this.#textBoxStyle.setBorderColor(this.#contrast);
2276
- this.#textBoxStyle.setTextColor(this.#contrast);
2277
- }
2278
- ClickStart() {
2279
- }
2280
- ClickEnd() {
2281
- let input = null;
2282
- const popup = new Popup({
2283
- title: "Set Color",
2284
- options: ["Set", "Cancel"],
2285
- content: () => {
2286
- const container = document.createElement("div");
2287
- input = document.createElement("input");
2288
- input.type = "color";
2289
- input.value = this.#value;
2290
- input.name = "color";
2291
- container.append(input);
2292
- const label = document.createElement("label");
2293
- label.htmlFor = "color";
2294
- label.textContent = "Color";
2295
- container.append(label);
2296
- return container;
2297
- },
2298
- onClose: (button) => {
2299
- if (button !== "Set" || input === null) {
2300
- return;
2301
- }
2302
- this.Set(input.value);
2303
- }
2304
- });
2305
- popup.Show();
2306
- }
2307
- Draw(ctx, position, scale, mousePosition) {
2308
- const box = {
2309
- Position: { x: 0, y: 0 },
2310
- Size: {
2311
- x: width * scale,
2312
- y: height * scale
2313
- }
2314
- };
2315
- CopyVector2(box.Position, position);
2316
- this.#textBoxStyle.Draw(ctx, box, scale, this.#value);
2317
- return box;
2619
+ const before = this.value();
2620
+ const alpha = before.slice(7);
2621
+ const box = this.box();
2622
+ const rect = canvas.getBoundingClientRect();
2623
+ const sx = canvas.width > 0 ? rect.width / canvas.width : 1;
2624
+ const sy = canvas.height > 0 ? rect.height / canvas.height : 1;
2625
+ const input = sharedPicker();
2626
+ input.style.cssText = [
2627
+ "position: fixed",
2628
+ `left: ${rect.left + box.Position.x * sx}px`,
2629
+ `top: ${rect.top + box.Position.y * sy}px`,
2630
+ `width: ${box.Size.x * sx}px`,
2631
+ `height: ${box.Size.y * sy}px`,
2632
+ "opacity: 0",
2633
+ "pointer-events: none",
2634
+ "border: 0",
2635
+ "padding: 0"
2636
+ ].join(";");
2637
+ input.value = before.slice(0, 7);
2638
+ input.oninput = () => this.input(input.value + alpha);
2639
+ input.onchange = () => this.commit(input.value + alpha, before);
2640
+ input.getBoundingClientRect();
2641
+ input.click();
2318
2642
  }
2319
2643
  };
2320
2644
 
2321
2645
  // src/widgets/slider.ts
2322
- var SliderWidget = class {
2323
- // Data Variables =========================================================
2646
+ var SliderWidget = class extends ValueWidget {
2324
2647
  #min;
2325
2648
  #max;
2326
- #value;
2327
- #text;
2328
2649
  #snapIncrement;
2329
- #node;
2330
- #nodeProperty;
2331
- // Callbacks ==============================================================
2332
- #change;
2333
- #release;
2334
- // Styling Variables ======================================================
2335
- #textStyle;
2336
- #backgroundColor;
2337
- #borderColor;
2338
2650
  #fillColor;
2339
- // Runtime Variables ======================================================
2340
- #lastMousePosition;
2341
- #clickStartMousePosition;
2342
- #clicking;
2651
+ #startValue = 0;
2343
2652
  constructor(node, config) {
2344
- this.#node = node;
2345
- this.#value = 0;
2346
- this.#min = config?.min === void 0 ? 0 : config?.min;
2347
- this.#max = config?.max === void 0 ? 1 : config?.max;
2653
+ const min = config?.min ?? 0;
2654
+ const max = config?.max ?? 1;
2655
+ super(node, { ...config, value: config?.value === void 0 ? void 0 : Clamp(config.value, min, max) }, min);
2656
+ this.#min = min;
2657
+ this.#max = max;
2348
2658
  this.#snapIncrement = config?.snapIncrement;
2349
- this.#nodeProperty = config?.property;
2350
- this.#text = "";
2351
- this.#release = config?.release;
2352
- this.#backgroundColor = config?.backgroundColor === void 0 ? Theme.Widget.BackgroundColor : config?.backgroundColor;
2353
- this.#fillColor = config?.fillColor === void 0 ? Theme.Widget.Slider.FillColor : config?.fillColor;
2354
- this.#borderColor = config?.borderColor === void 0 ? Theme.Widget.Border.Color : config?.borderColor;
2355
- this.#textStyle = new TextStyle(TextStyleFallback(config?.textStyle, {
2356
- color: Theme.Widget.FontColor
2357
- }));
2358
- this.#lastMousePosition = Zero();
2359
- this.#clickStartMousePosition = Zero();
2360
- this.#clicking = false;
2361
- if (config?.property !== void 0 && config?.property !== null) {
2362
- node.addPropertyChangeListener(config.property, (oldVal, newVal) => {
2363
- this.SetValue(newVal);
2364
- });
2365
- }
2366
- this.SetValue(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
2367
- this.#change = config?.change;
2368
- }
2369
- SetValue(newValue) {
2370
- const cleanedValue = Clamp(newValue, this.#min, this.#max);
2371
- if (Object.is(this.#value, cleanedValue)) {
2372
- return;
2373
- }
2374
- this.#value = cleanedValue;
2375
- if (this.#nodeProperty) {
2376
- this.#node.setProperty(this.#nodeProperty, this.#value);
2377
- }
2378
- this.#text = this.#value.toFixed(3);
2379
- if (this.#change !== void 0) {
2380
- this.#change(this.#value);
2381
- }
2382
- }
2383
- Size() {
2384
- return { "x": width, "y": height };
2659
+ this.#fillColor = config?.fillColor;
2385
2660
  }
2386
- ClickStart() {
2387
- this.#clicking = true;
2388
- CopyVector2(this.#clickStartMousePosition, this.#lastMousePosition);
2661
+ Cursor() {
2662
+ return "ew-resize" /* EWResize */;
2389
2663
  }
2390
- ClickEnd() {
2391
- this.#clicking = false;
2392
- if (this.#release !== void 0) {
2393
- this.#release(this.#value);
2394
- }
2664
+ setValue(value) {
2665
+ super.setValue(Clamp(value, this.#min, this.#max));
2395
2666
  }
2396
- Draw(ctx, position, scale, mousePosition) {
2397
- const scaledWidth = width * scale;
2398
- const scaledHeight = height * scale;
2399
- const scaledBorderThickness = Theme.Widget.Border.Size * scale;
2400
- ctx.fillStyle = this.#backgroundColor;
2401
- ctx.strokeStyle = this.#borderColor;
2402
- ctx.lineWidth = scaledBorderThickness;
2667
+ render(ctx, box, scale) {
2668
+ drawFieldBox(ctx, box, scale, { input: true, hovered: this.hovered() || this.pressed(), value: "" });
2669
+ const border = Theme.Widget.Border.Size * scale;
2670
+ const fill = Clamp01((this.value() - this.#min) / (this.#max - this.#min));
2671
+ ctx.fillStyle = this.#fillColor ?? Theme.Widget.Slider.FillColor;
2403
2672
  ctx.beginPath();
2404
2673
  ctx.roundRect(
2405
- position.x,
2406
- position.y,
2407
- scaledWidth,
2408
- scaledHeight,
2674
+ box.Position.x + border / 2,
2675
+ box.Position.y + border / 2,
2676
+ Math.max(0, box.Size.x * fill - border),
2677
+ box.Size.y - border,
2409
2678
  Theme.Widget.Border.Radius * scale
2410
2679
  );
2411
2680
  ctx.fill();
2412
- ctx.stroke();
2413
- const fill = Clamp01((this.#value - this.#min) / (this.#max - this.#min));
2414
- ctx.fillStyle = this.#fillColor;
2415
- ctx.beginPath();
2416
- ctx.roundRect(
2417
- position.x + scaledBorderThickness / 2,
2418
- position.y + scaledBorderThickness / 2,
2419
- scaledWidth * fill - scaledBorderThickness,
2420
- scaledHeight - scaledBorderThickness,
2421
- Theme.Widget.Border.Radius * scale
2422
- );
2423
- ctx.fill();
2424
- ctx.textAlign = "center" /* Center */;
2425
- ctx.textBaseline = "middle" /* Middle */;
2426
- this.#textStyle.setupStyle(ctx, scale);
2427
- ctx.fillText(
2428
- this.#text,
2429
- position.x + scaledWidth / 2,
2430
- position.y + scaledHeight / 2
2431
- );
2432
- if (mousePosition !== void 0) {
2433
- CopyVector2(this.#lastMousePosition, mousePosition);
2434
- if (this.#clicking) {
2435
- const min = position.x + scaledBorderThickness / 2;
2436
- const max = min + scaledWidth - scaledBorderThickness;
2437
- const p = Clamp01((this.#lastMousePosition.x - min) / (max - min));
2438
- let value = p * (this.#max - this.#min) + this.#min;
2439
- if (this.#snapIncrement !== void 0) {
2440
- value = Math.round(value / this.#snapIncrement) * this.#snapIncrement;
2441
- }
2442
- this.SetValue(value);
2443
- }
2681
+ drawFieldText(ctx, box, scale, {
2682
+ input: true,
2683
+ hovered: false,
2684
+ label: this.label(),
2685
+ value: formatNumber(Number(this.value().toFixed(3)))
2686
+ });
2687
+ }
2688
+ onPointerDown(pointer) {
2689
+ this.#startValue = this.value();
2690
+ this.input(this.#valueAt(pointer.position.x));
2691
+ }
2692
+ onPointerMove(pointer) {
2693
+ this.input(this.#valueAt(pointer.position.x));
2694
+ }
2695
+ onPointerUp() {
2696
+ this.commit(this.value(), this.#startValue);
2697
+ }
2698
+ #valueAt(x) {
2699
+ const box = this.box();
2700
+ const border = Theme.Widget.Border.Size * this.scale();
2701
+ const min = box.Position.x + border / 2;
2702
+ const max = min + box.Size.x - border;
2703
+ const p = Clamp01((x - min) / (max - min));
2704
+ let value = p * (this.#max - this.#min) + this.#min;
2705
+ if (this.#snapIncrement !== void 0) {
2706
+ value = Math.round(value / this.#snapIncrement) * this.#snapIncrement;
2444
2707
  }
2445
- return {
2446
- Position: { x: position.x, y: position.y },
2447
- Size: {
2448
- x: scaledWidth,
2449
- y: scaledHeight
2450
- }
2451
- };
2708
+ return Clamp(value, this.#min, this.#max);
2452
2709
  }
2453
2710
  };
2454
2711
 
2455
- // src/popups/string.ts
2456
- function SetStringPopup(config) {
2457
- let input = null;
2458
- return new Popup({
2459
- title: config.title,
2460
- options: ["Set", "Cancel"],
2461
- content: () => {
2462
- const container = document.createElement("div");
2463
- input = document.createElement("input");
2464
- input.value = config.startingValue;
2465
- container.append(input);
2466
- return container;
2467
- },
2468
- onClose: (button) => {
2469
- if (button !== "Set" || input === null) {
2470
- if (config.onCancel) {
2471
- config.onCancel();
2472
- }
2473
- return;
2474
- }
2475
- config.onUpdate(input.value);
2476
- }
2477
- });
2478
- }
2479
-
2480
2712
  // src/widgets/string.ts
2481
- var StringWidget = class {
2482
- #value;
2483
- #idleStyle;
2484
- #hoverStyle;
2485
- #callback;
2486
- #node;
2487
- #nodeProperty;
2713
+ var StringWidget = class extends ValueWidget {
2488
2714
  constructor(node, config) {
2489
- this.#value = "";
2490
- this.#node = node;
2491
- this.#nodeProperty = config?.property;
2492
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.textBoxStyle, {
2493
- box: {
2494
- color: Theme.Widget.BackgroundColor,
2495
- border: {
2496
- size: Theme.Widget.Border.Size,
2497
- color: Theme.Widget.Border.Color
2498
- },
2499
- radius: Theme.Widget.Border.Radius
2500
- },
2501
- text: { color: Theme.Widget.FontColor }
2502
- }));
2503
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.textBoxStyle, {
2504
- box: {
2505
- color: Theme.Widget.Hover.BackgroundColor,
2506
- border: {
2507
- size: Theme.Widget.Border.Size,
2508
- color: Theme.Widget.Border.Color
2509
- },
2510
- radius: Theme.Widget.Border.Radius
2511
- },
2512
- text: { color: Theme.Widget.FontColor }
2513
- }));
2514
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, ""));
2515
- this.#callback = config?.callback;
2516
- if (this.#nodeProperty !== void 0) {
2517
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2518
- this.Set(newVal);
2519
- });
2520
- }
2715
+ super(node, config, "");
2521
2716
  }
2522
- Size() {
2523
- return { "x": width, "y": height };
2717
+ Cursor() {
2718
+ return "text" /* Text */;
2524
2719
  }
2525
- Set(value) {
2526
- if (this.#value === value) {
2527
- return;
2528
- }
2529
- this.#value = value;
2530
- if (this.#nodeProperty !== void 0) {
2531
- this.#node.setProperty(this.#nodeProperty, this.#value);
2532
- }
2533
- if (this.#callback !== void 0) {
2534
- this.#callback(this.#value);
2535
- }
2536
- }
2537
- ClickStart() {
2538
- }
2539
- ClickEnd() {
2540
- const popup = SetStringPopup({
2541
- title: "Set String",
2542
- startingValue: this.#value,
2543
- onUpdate: (value) => {
2544
- this.Set(value);
2545
- }
2720
+ render(ctx, box, scale) {
2721
+ drawField(ctx, box, scale, {
2722
+ input: true,
2723
+ hovered: this.hovered(),
2724
+ label: this.label(),
2725
+ value: this.value()
2546
2726
  });
2547
- popup.Show();
2548
2727
  }
2549
- Draw(ctx, position, scale, mousePosition) {
2550
- const box = {
2551
- Position: { x: 0, y: 0 },
2552
- Size: {
2553
- x: width * scale,
2554
- y: height * scale
2555
- }
2556
- };
2557
- CopyVector2(box.Position, position);
2558
- let style = this.#idleStyle;
2559
- if (mousePosition !== void 0) {
2560
- if (InBox(box, mousePosition)) {
2561
- style = this.#hoverStyle;
2562
- }
2563
- }
2564
- style.DrawUnderline(ctx, box, scale, fitString(ctx, this.#value, box.Size.x - 20 * scale));
2565
- return box;
2728
+ onClick() {
2729
+ const before = this.value();
2730
+ this.editText(before, (text) => this.commit(text, before));
2566
2731
  }
2567
2732
  };
2568
2733
 
2569
2734
  // src/widgets/toggle.ts
2570
- var ToggleStyle = class {
2571
- #idleStyle;
2572
- #hoverStyle;
2573
- #lightColor;
2574
- #lightBorderColor;
2575
- #lightBlur;
2576
- #box;
2577
- constructor(config) {
2578
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idle, {
2579
- box: {
2580
- color: Theme.Widget.BackgroundColor,
2581
- border: {
2582
- size: Theme.Widget.Border.Size,
2583
- color: Theme.Widget.Border.Color
2584
- }
2585
- },
2586
- text: { color: Theme.Widget.FontColor }
2587
- }));
2588
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.hover, {
2589
- box: {
2590
- color: Theme.Widget.Hover.BackgroundColor,
2591
- border: {
2592
- size: Theme.Widget.Border.Size,
2593
- color: Theme.Widget.Border.Color
2594
- }
2595
- },
2596
- text: { color: Theme.Widget.FontColor }
2597
- }));
2598
- this.#lightColor = config?.lightColor === void 0 ? "#222222" : config.lightColor;
2599
- this.#lightBorderColor = config?.lightBorderColor === void 0 ? "black" : config.lightBorderColor;
2600
- this.#lightBlur = config?.lightBlur;
2601
- this.#box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
2602
- }
2603
- Draw(ctx, pos, scale, text, mousePosition) {
2604
- const scaledWidth = width * scale;
2605
- const scaledHeight = height * scale;
2606
- this.#box.Size.x = scaledWidth;
2607
- this.#box.Size.y = scaledHeight;
2608
- CopyVector2(this.#box.Position, pos);
2609
- let style = this.#idleStyle;
2610
- if (mousePosition !== void 0) {
2611
- if (InBox(this.#box, mousePosition)) {
2612
- style = this.#hoverStyle;
2613
- }
2614
- }
2615
- style.Draw(ctx, this.#box, scale, text);
2616
- const lightScale = Math.min(scaledWidth, scaledHeight);
2617
- ctx.strokeStyle = this.#lightBorderColor;
2618
- ctx.fillStyle = this.#lightColor;
2619
- ctx.beginPath();
2620
- ctx.roundRect(
2621
- pos.x + lightScale * 0.2,
2622
- pos.y + lightScale * 0.2,
2623
- lightScale * 0.6,
2624
- lightScale * 0.6,
2625
- Theme.Widget.Border.Radius * scale
2626
- );
2627
- if (this.#lightBlur) {
2628
- ctx.shadowBlur = this.#lightBlur * scale;
2629
- ctx.shadowColor = this.#lightColor;
2630
- }
2631
- ctx.fill();
2632
- if (this.#lightBlur) {
2633
- ctx.shadowBlur = 0;
2634
- }
2635
- return this.#box;
2636
- }
2637
- };
2638
- var ToggleWidget = class {
2639
- #node;
2640
- #nodeProperty;
2641
- #value;
2642
- #text;
2643
- #enabledStyle;
2644
- #disabledStyle;
2645
- #callback;
2735
+ var ToggleWidget = class extends ValueWidget {
2736
+ #onColor;
2737
+ #offColor;
2646
2738
  constructor(node, config) {
2647
- this.#value = false;
2648
- this.#node = node;
2649
- this.#nodeProperty = config?.property;
2650
- this.#text = config?.text === void 0 ? "Toggle" : config?.text;
2651
- this.#enabledStyle = new ToggleStyle({
2652
- idle: TextBoxStyleWithFallback(config?.enabledStyle?.idle, {
2653
- box: {
2654
- color: Theme.Widget.BackgroundColor,
2655
- border: {
2656
- size: Theme.Widget.Border.Size,
2657
- color: Theme.Widget.Border.Color
2658
- }
2659
- },
2660
- text: { color: Theme.Widget.FontColor }
2661
- }),
2662
- lightBorderColor: config?.enabledStyle?.lightBorderColor,
2663
- lightColor: config?.enabledStyle?.lightColor === void 0 ? "#00FF00" : config?.enabledStyle?.lightColor,
2664
- lightBlur: config?.enabledStyle?.lightBlur === void 0 ? 15 : config?.enabledStyle?.lightBlur
2665
- });
2666
- this.#disabledStyle = new ToggleStyle({
2667
- idle: TextBoxStyleWithFallback(config?.disabledStyle?.idle, {
2668
- box: {
2669
- color: Theme.Widget.BackgroundColor,
2670
- border: {
2671
- size: Theme.Widget.Border.Size,
2672
- color: Theme.Widget.Border.Color
2673
- }
2674
- },
2675
- text: { color: Theme.Widget.FontColor }
2676
- }),
2677
- lightBorderColor: config?.disabledStyle?.lightBorderColor,
2678
- lightColor: config?.disabledStyle?.lightColor === void 0 ? "#004400" : config?.enabledStyle?.lightColor
2679
- });
2680
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, false));
2681
- this.#callback = config?.callback;
2682
- if (this.#nodeProperty !== void 0) {
2683
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2684
- this.Set(newVal);
2685
- });
2686
- }
2687
- }
2688
- Size() {
2689
- return { "x": width, "y": height };
2690
- }
2691
- Draw(ctx, position, scale, mousePosition) {
2692
- let style = this.#value ? this.#enabledStyle : this.#disabledStyle;
2693
- return style.Draw(ctx, position, scale, this.#text, mousePosition);
2739
+ super(node, { ...config, label: config?.label ?? "Toggle" }, false);
2740
+ this.#onColor = config?.onColor ?? "#00FF00";
2741
+ this.#offColor = config?.offColor ?? "#004400";
2694
2742
  }
2695
2743
  Toggle() {
2696
- this.Set(!this.#value);
2697
- }
2698
- Set(value) {
2699
- if (this.#value === value) {
2700
- return;
2701
- }
2702
- this.#value = value;
2703
- if (this.#nodeProperty !== void 0) {
2704
- this.#node.setProperty(this.#nodeProperty, this.#value);
2705
- }
2706
- if (this.#callback !== void 0) {
2707
- this.#callback(this.#value);
2744
+ const before = this.value();
2745
+ this.commit(!before, before);
2746
+ }
2747
+ render(ctx, box, scale) {
2748
+ drawFieldBox(ctx, box, scale, { hovered: this.hovered(), value: "" });
2749
+ const light = Math.min(box.Size.x, box.Size.y) * 0.6;
2750
+ const inset = (box.Size.y - light) / 2;
2751
+ const on = this.value();
2752
+ ctx.fillStyle = on ? this.#onColor : this.#offColor;
2753
+ ctx.beginPath();
2754
+ ctx.roundRect(box.Position.x + inset, box.Position.y + inset, light, light, Theme.Widget.Border.Radius * scale);
2755
+ if (on) {
2756
+ ctx.shadowBlur = 15 * scale;
2757
+ ctx.shadowColor = this.#onColor;
2708
2758
  }
2759
+ ctx.fill();
2760
+ ctx.shadowBlur = 0;
2761
+ const textLeft = box.Position.x + inset * 2 + light;
2762
+ ctx.font = widgetFont(scale);
2763
+ ctx.textAlign = "center";
2764
+ ctx.textBaseline = "middle";
2765
+ ctx.fillStyle = Theme.Widget.FontColor;
2766
+ const room = box.Position.x + box.Size.x - textLeft - inset;
2767
+ ctx.fillText(
2768
+ fitString(ctx, this.label(), room),
2769
+ textLeft + room / 2,
2770
+ box.Position.y + box.Size.y / 2
2771
+ );
2709
2772
  }
2710
- ClickStart() {
2773
+ onClick() {
2711
2774
  this.Toggle();
2712
2775
  }
2713
- ClickEnd() {
2714
- }
2715
2776
  };
2716
2777
 
2717
2778
  // src/widgets/image.ts
2718
- var ImageWidget = class {
2779
+ var ImageWidget = class extends WidgetBase {
2719
2780
  #url;
2781
+ #ownedUrl;
2720
2782
  #image;
2721
2783
  #maxWidth;
2722
2784
  #maxHeight;
2723
2785
  constructor(config) {
2786
+ super();
2724
2787
  this.#maxWidth = config?.maxWidth === void 0 ? 150 : config?.maxWidth;
2725
2788
  this.#maxHeight = config?.maxHeight === void 0 ? 150 : config?.maxHeight;
2726
2789
  if (config?.image) {
@@ -2730,16 +2793,23 @@ var ImageWidget = class {
2730
2793
  }
2731
2794
  }
2732
2795
  SetBlob(blob) {
2733
- const urlCreator = window.URL || window.webkitURL;
2734
- const imageUrl = urlCreator.createObjectURL(blob);
2735
- this.SetUrl(imageUrl);
2796
+ const url = URL.createObjectURL(blob);
2797
+ this.SetUrl(url);
2798
+ this.#ownedUrl = url;
2736
2799
  }
2737
2800
  SetUrl(url) {
2801
+ if (this.#ownedUrl !== void 0) {
2802
+ URL.revokeObjectURL(this.#ownedUrl);
2803
+ this.#ownedUrl = void 0;
2804
+ }
2738
2805
  this.#image = void 0;
2739
2806
  this.#url = url;
2740
2807
  const img = document.createElement("img");
2741
2808
  img.src = url;
2742
2809
  img.onload = () => {
2810
+ if (this.#url !== url) {
2811
+ return;
2812
+ }
2743
2813
  this.#image = img;
2744
2814
  requestRender();
2745
2815
  };
@@ -2747,6 +2817,9 @@ var ImageWidget = class {
2747
2817
  console.log("error loading image:", url, event);
2748
2818
  };
2749
2819
  }
2820
+ Cursor() {
2821
+ return "default" /* Default */;
2822
+ }
2750
2823
  Size() {
2751
2824
  if (this.#image === void 0) {
2752
2825
  return { "x": 0, "y": 0 };
@@ -2766,88 +2839,178 @@ var ImageWidget = class {
2766
2839
  "y": adjust * this.#image.height
2767
2840
  };
2768
2841
  }
2769
- ClickStart() {
2770
- }
2771
- ClickEnd() {
2772
- }
2773
- Draw(ctx, position, scale, mousePosition) {
2774
- const size = this.Size();
2775
- const box = {
2776
- Position: { x: 0, y: 0 },
2777
- Size: {
2778
- x: size.x * scale,
2779
- y: size.y * scale
2780
- }
2781
- };
2782
- CopyVector2(box.Position, position);
2842
+ render(ctx, box, scale) {
2783
2843
  if (!this.#image) {
2784
- return box;
2844
+ return;
2785
2845
  }
2846
+ const size = this.Size();
2847
+ const w = size.x * scale;
2848
+ const h = size.y * scale;
2786
2849
  ctx.drawImage(
2787
2850
  this.#image,
2788
- position.x,
2789
- position.y,
2790
- box.Size.x,
2791
- box.Size.y
2851
+ box.Position.x + (box.Size.x - w) / 2,
2852
+ box.Position.y,
2853
+ w,
2854
+ h
2792
2855
  );
2793
- return box;
2794
2856
  }
2795
2857
  };
2796
2858
 
2797
2859
  // src/widgets/text.ts
2798
- var TextWidget = class {
2860
+ var TextWidget = class extends WidgetBase {
2799
2861
  #value;
2800
- #callback;
2801
- #node;
2802
- #nodeProperty;
2803
2862
  #size;
2863
+ #styleConfig;
2864
+ #styledForTheme = -1;
2865
+ #followTheme() {
2866
+ if (this.#styledForTheme === themeVersion()) {
2867
+ return;
2868
+ }
2869
+ this.#styledForTheme = themeVersion();
2870
+ const style = this.#value.style();
2871
+ style.setColor(this.#styleConfig?.color ?? Theme.Widget.FontColor);
2872
+ style.setSize(this.#styleConfig?.size ?? Theme.Note.FontSize);
2873
+ }
2804
2874
  constructor(node, config) {
2875
+ super();
2805
2876
  this.#size = Zero();
2806
- this.#node = node;
2807
- this.#nodeProperty = config?.property;
2808
- this.#value = new Text("", TextStyleFallback(config?.textBoxStyle, {
2809
- font: Theme.FontFamily,
2810
- color: Theme.Widget.FontColor,
2811
- size: Theme.Note.FontSize
2812
- }));
2813
- this.Set(config?.value === void 0 ? "" : config?.value);
2814
- this.#callback = config?.callback;
2815
- if (this.#nodeProperty !== void 0) {
2816
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
2817
- this.Set(newVal);
2818
- });
2877
+ this.#styleConfig = config?.textBoxStyle;
2878
+ this.#value = new Text("", TextStyleFallback(config?.textBoxStyle, {}));
2879
+ this.#followTheme();
2880
+ this.setText(config?.value ?? "");
2881
+ const property = config?.property;
2882
+ if (property !== void 0) {
2883
+ const existing = node.getProperty(property);
2884
+ if (existing !== void 0 && existing !== null) {
2885
+ this.setText(String(existing));
2886
+ }
2887
+ node.addPropertyChangeListener(property, (_, next) => this.setText(String(next)));
2819
2888
  }
2820
2889
  }
2890
+ Cursor() {
2891
+ return "default" /* Default */;
2892
+ }
2821
2893
  Size() {
2822
2894
  return this.#size;
2823
2895
  }
2824
- Set(value) {
2896
+ text() {
2897
+ return this.#value.get();
2898
+ }
2899
+ setText(value) {
2825
2900
  if (this.#value.get() === value) {
2826
2901
  return;
2827
2902
  }
2828
2903
  this.#value.set(value);
2829
- if (this.#nodeProperty !== void 0) {
2830
- this.#node.setProperty(this.#nodeProperty, value);
2904
+ requestRender();
2905
+ }
2906
+ render(ctx, box, scale) {
2907
+ this.#followTheme();
2908
+ this.#value.size(ctx, 1, this.#size);
2909
+ ctx.textAlign = "left";
2910
+ this.#value.render(ctx, scale, box.Position);
2911
+ }
2912
+ };
2913
+
2914
+ // src/widgets/row.ts
2915
+ var RowWidget = class extends WidgetBase {
2916
+ #children;
2917
+ #grow;
2918
+ #gap;
2919
+ #boxes = [];
2920
+ #captured = null;
2921
+ #capturedBox = null;
2922
+ #mouse;
2923
+ constructor(children, config) {
2924
+ super();
2925
+ this.#children = children;
2926
+ this.#grow = config?.grow ?? children.map(() => 1);
2927
+ this.#gap = config?.gap ?? Theme.Widget.Gap;
2928
+ }
2929
+ children() {
2930
+ return this.#children;
2931
+ }
2932
+ Size() {
2933
+ const shown = this.#shown();
2934
+ let x = 0;
2935
+ let y = 0;
2936
+ for (const child of shown) {
2937
+ const size = child.Size();
2938
+ x += size.x;
2939
+ y = Math.max(y, size.y);
2940
+ }
2941
+ x += Math.max(0, shown.length - 1) * this.#gap;
2942
+ return { x, y };
2943
+ }
2944
+ Cursor() {
2945
+ const over = this.#captured ?? this.#childAt(this.#mouse);
2946
+ return over?.Cursor?.() ?? "default" /* Default */;
2947
+ }
2948
+ Draw(ctx, box, scale, mousePosition) {
2949
+ this.#mouse = mousePosition;
2950
+ super.Draw(ctx, box, scale, mousePosition);
2951
+ }
2952
+ render(ctx, box, scale) {
2953
+ const shown = this.#shown();
2954
+ const gap = this.#gap * scale;
2955
+ const natural = shown.reduce((sum, child) => sum + child.Size().x * scale, 0);
2956
+ const extra = Math.max(0, box.Size.x - natural - gap * Math.max(0, shown.length - 1));
2957
+ const totalGrow = shown.reduce((sum, child) => sum + this.#growOf(child), 0);
2958
+ this.#boxes = this.#children.map(() => null);
2959
+ let x = box.Position.x;
2960
+ for (const child of shown) {
2961
+ const size = child.Size();
2962
+ const share = totalGrow > 0 ? extra * (this.#growOf(child) / totalGrow) : 0;
2963
+ const childBox = {
2964
+ Position: { x, y: box.Position.y + (box.Size.y - size.y * scale) / 2 },
2965
+ Size: { x: size.x * scale + share, y: size.y * scale }
2966
+ };
2967
+ this.#boxes[this.#children.indexOf(child)] = childBox;
2968
+ child.Draw(ctx, childBox, scale, this.#captured === null || this.#captured === child ? this.#mouse : void 0);
2969
+ x += childBox.Size.x + gap;
2831
2970
  }
2832
- if (this.#callback !== void 0) {
2833
- this.#callback(value);
2971
+ }
2972
+ onPointerDown(pointer) {
2973
+ const child = this.#childAt(pointer.position);
2974
+ if (child === null || child.Disabled?.()) {
2975
+ return;
2834
2976
  }
2977
+ this.#captured = child;
2978
+ this.#capturedBox = this.#boxes[this.#children.indexOf(child)];
2979
+ child.PointerDown?.(this.#forChild(pointer));
2835
2980
  }
2836
- ClickStart() {
2981
+ onPointerMove(pointer) {
2982
+ this.#captured?.PointerMove?.(this.#forChild(pointer));
2837
2983
  }
2838
- ClickEnd() {
2984
+ onPointerUp(pointer) {
2985
+ const child = this.#captured;
2986
+ const forChild = this.#forChild(pointer);
2987
+ this.#captured = null;
2988
+ this.#capturedBox = null;
2989
+ child?.PointerUp?.(forChild);
2839
2990
  }
2840
- Draw(ctx, position, scale, mousePosition) {
2841
- this.#value.size(ctx, scale, this.#size);
2842
- const box = {
2843
- Position: { x: 0, y: 0 },
2844
- Size: this.#size
2991
+ #forChild(pointer) {
2992
+ return {
2993
+ position: pointer.position,
2994
+ inside: this.#capturedBox !== null && InBox(this.#capturedBox, pointer.position)
2845
2995
  };
2846
- CopyVector2(box.Position, position);
2847
- this.#value.size(ctx, 1, this.#size);
2848
- ctx.textAlign = "left";
2849
- this.#value.render(ctx, scale, box.Position);
2850
- return box;
2996
+ }
2997
+ #childAt(position) {
2998
+ if (position === void 0) {
2999
+ return null;
3000
+ }
3001
+ for (let i = 0; i < this.#children.length; i++) {
3002
+ const box = this.#boxes[i];
3003
+ if (box && InBox(box, position)) {
3004
+ return this.#children[i];
3005
+ }
3006
+ }
3007
+ return null;
3008
+ }
3009
+ #shown() {
3010
+ return this.#children.filter((child) => !child.Hidden?.());
3011
+ }
3012
+ #growOf(child) {
3013
+ return this.#grow[this.#children.indexOf(child)] ?? 1;
2851
3014
  }
2852
3015
  };
2853
3016
 
@@ -2867,6 +3030,9 @@ var WidgetFactory = class {
2867
3030
  }
2868
3031
  return builder(node, config);
2869
3032
  }
3033
+ createFromConfig(node, config) {
3034
+ return this.create(node, config.type, config.config);
3035
+ }
2870
3036
  };
2871
3037
  var GlobalWidgetFactory = new WidgetFactory();
2872
3038
  GlobalWidgetFactory.register("button", (node, config) => new ButtonWidget(config));
@@ -2877,6 +3043,10 @@ GlobalWidgetFactory.register("string", (node, config) => new StringWidget(node,
2877
3043
  GlobalWidgetFactory.register("toggle", (node, config) => new ToggleWidget(node, config));
2878
3044
  GlobalWidgetFactory.register("image", (node, config) => new ImageWidget(config));
2879
3045
  GlobalWidgetFactory.register("text", (node, config) => new TextWidget(node, config));
3046
+ GlobalWidgetFactory.register("row", (node, config) => new RowWidget(
3047
+ (config?.widgets ?? []).map((child) => GlobalWidgetFactory.createFromConfig(node, child)),
3048
+ config
3049
+ ));
2880
3050
 
2881
3051
  // src/connection.ts
2882
3052
  var BACKWARD_PORT_STUB = 30;
@@ -2961,7 +3131,7 @@ var Connection = class {
2961
3131
  return;
2962
3132
  }
2963
3133
  if (this.#inNode !== null) {
2964
- const inPortBox = this.#inNode.inputPortPosition(this.#inNodePortIndex);
3134
+ const inPortBox = this.#inSlotBox();
2965
3135
  if (inPortBox === void 0) {
2966
3136
  return;
2967
3137
  }
@@ -3020,14 +3190,14 @@ var Connection = class {
3020
3190
  this.clearInput();
3021
3191
  this.clearOutput();
3022
3192
  }
3023
- setInput(node, portIndex, replace) {
3193
+ setInput(node, portIndex, replace, insertAt) {
3024
3194
  this.#inNode = node;
3025
3195
  this.#inNodePortIndex = portIndex;
3026
3196
  const port = this.#inNode.inputPort(portIndex);
3027
3197
  if (!!replace && port.connections().length > 0) {
3028
3198
  port.replaceConnection(this, 0);
3029
3199
  } else {
3030
- port.addConnection(this);
3200
+ port.addConnection(this, insertAt);
3031
3201
  }
3032
3202
  }
3033
3203
  setOutput(node, portIndex) {
@@ -3045,9 +3215,33 @@ var Connection = class {
3045
3215
  this.#outNode = null;
3046
3216
  this.#outNodePortIndex = -1;
3047
3217
  }
3218
+ /**
3219
+ * The part of the input port this wire lands on: the whole port, or its
3220
+ * own slot on a port holding several wires.
3221
+ */
3222
+ #inSlotBox() {
3223
+ if (this.#inNode === null) {
3224
+ return void 0;
3225
+ }
3226
+ const box = this.#inNode.inputPortPosition(this.#inNodePortIndex);
3227
+ const port = this.#inNode.inputPort(this.#inNodePortIndex);
3228
+ const slots = port.slots();
3229
+ if (box === void 0 || slots === 1) {
3230
+ return box;
3231
+ }
3232
+ const index = port.connections().indexOf(this);
3233
+ if (index === -1) {
3234
+ return box;
3235
+ }
3236
+ const height2 = box.Size.y / slots;
3237
+ return {
3238
+ Position: { x: box.Position.x, y: box.Position.y + index * height2 },
3239
+ Size: { x: box.Size.x, y: height2 }
3240
+ };
3241
+ }
3048
3242
  mouseOverPort(mousePosition) {
3049
3243
  if (this.#inNode !== null) {
3050
- const inPortBox = this.#inNode.inputPortPosition(this.#inNodePortIndex);
3244
+ const inPortBox = this.#inSlotBox();
3051
3245
  if (inPortBox !== void 0 && InBox(inPortBox, mousePosition)) {
3052
3246
  return this.#inNode.inputPort(this.#inNodePortIndex);
3053
3247
  }
@@ -3124,7 +3318,10 @@ var Camera = class {
3124
3318
  // src/organize.ts
3125
3319
  var widthSpacing = 100;
3126
3320
  var heightSpacing = 50;
3321
+ var laneSpacing = 25;
3322
+ var pieceSpacing = 150;
3127
3323
  var orderingSweeps = 8;
3324
+ var placementSweeps = 8;
3128
3325
  function topologicalOrder(sorting, upstream) {
3129
3326
  const visited = /* @__PURE__ */ new Set();
3130
3327
  const order = new Array();
@@ -3146,11 +3343,7 @@ function topologicalOrder(sorting, upstream) {
3146
3343
  continue;
3147
3344
  }
3148
3345
  visited.add(top.node);
3149
- const feeders = upstream.get(top.node);
3150
- if (feeders === void 0) {
3151
- continue;
3152
- }
3153
- for (const feeder of feeders) {
3346
+ for (const feeder of upstream.get(top.node) ?? []) {
3154
3347
  if (!visited.has(feeder)) {
3155
3348
  stack.push({ node: feeder, expanded: false });
3156
3349
  }
@@ -3159,43 +3352,103 @@ function topologicalOrder(sorting, upstream) {
3159
3352
  }
3160
3353
  return order;
3161
3354
  }
3355
+ function consumersOf(sorting, upstream) {
3356
+ const downstream = /* @__PURE__ */ new Map();
3357
+ for (const node of sorting) {
3358
+ downstream.set(node, []);
3359
+ }
3360
+ for (const node of sorting) {
3361
+ for (const feeder of upstream.get(node) ?? []) {
3362
+ const consumers = downstream.get(feeder);
3363
+ if (consumers !== void 0 && !consumers.includes(node)) {
3364
+ consumers.push(node);
3365
+ }
3366
+ }
3367
+ }
3368
+ return downstream;
3369
+ }
3162
3370
  function assignLayers(sorting, upstream) {
3163
- const order = topologicalOrder(sorting, upstream);
3371
+ const downstream = consumersOf(sorting, upstream);
3372
+ const order = topologicalOrder(sorting, downstream);
3164
3373
  const placed = /* @__PURE__ */ new Set();
3165
- const layerOf = /* @__PURE__ */ new Map();
3374
+ const rank = /* @__PURE__ */ new Map();
3166
3375
  for (const node of order) {
3167
- let layer = 0;
3168
- for (const feeder of upstream.get(node) ?? []) {
3169
- if (placed.has(feeder)) {
3170
- layer = Math.max(layer, layerOf.get(feeder) + 1);
3376
+ let r = 0;
3377
+ for (const consumer of downstream.get(node) ?? []) {
3378
+ if (placed.has(consumer)) {
3379
+ r = Math.max(r, rank.get(consumer) + 1);
3171
3380
  }
3172
3381
  }
3173
- layerOf.set(node, layer);
3382
+ rank.set(node, r);
3174
3383
  placed.add(node);
3175
3384
  }
3176
3385
  let deepest = 0;
3177
- for (const layer of layerOf.values()) {
3178
- deepest = Math.max(deepest, layer);
3386
+ for (const r of rank.values()) {
3387
+ deepest = Math.max(deepest, r);
3179
3388
  }
3180
- const layers = new Array(deepest + 1);
3181
- for (let i = 0; i < layers.length; i++) {
3182
- layers[i] = new Array();
3389
+ const layerOf = /* @__PURE__ */ new Map();
3390
+ for (const node of sorting) {
3391
+ layerOf.set(node, deepest - rank.get(node));
3183
3392
  }
3184
- for (const node of order) {
3185
- layers[layerOf.get(node)].push(node);
3393
+ for (const node of sorting) {
3394
+ const layer = layerOf.get(node);
3395
+ if ((downstream.get(node) ?? []).some((c) => layerOf.get(c) > layer)) {
3396
+ continue;
3397
+ }
3398
+ let pulled = -1;
3399
+ for (const feeder of upstream.get(node) ?? []) {
3400
+ const feederLayer = layerOf.get(feeder);
3401
+ if (feederLayer !== void 0 && feederLayer < layer) {
3402
+ pulled = Math.max(pulled, feederLayer + 1);
3403
+ }
3404
+ }
3405
+ if (pulled >= 0) {
3406
+ layerOf.set(node, pulled);
3407
+ }
3408
+ }
3409
+ const used = [...new Set(layerOf.values())].sort((a, b) => a - b);
3410
+ const compact = /* @__PURE__ */ new Map();
3411
+ used.forEach((layer, i) => compact.set(layer, i));
3412
+ const layers = used.map(() => new Array());
3413
+ for (const node of sorting) {
3414
+ const layer = compact.get(layerOf.get(node));
3415
+ layerOf.set(node, layer);
3416
+ layers[layer].push(node);
3186
3417
  }
3187
3418
  return { layers, layerOf };
3188
3419
  }
3189
- function orderWithinLayers(layout, upstream, downstream) {
3420
+ function crossingsOf(layout, downstream) {
3190
3421
  const slotOf = /* @__PURE__ */ new Map();
3191
- const recordSlots = () => {
3192
- for (const layer of layout.layers) {
3193
- for (let i = 0; i < layer.length; i++) {
3194
- slotOf.set(layer[i], i);
3422
+ for (const layer of layout.layers) {
3423
+ layer.forEach((node, slot) => slotOf.set(node, slot));
3424
+ }
3425
+ let total = 0;
3426
+ for (let i = 0; i + 1 < layout.layers.length; i++) {
3427
+ const spans = new Array();
3428
+ for (const node of layout.layers[i]) {
3429
+ for (const consumer of downstream.get(node) ?? []) {
3430
+ if (layout.layerOf.get(consumer) === i + 1) {
3431
+ spans.push([slotOf.get(node), slotOf.get(consumer)]);
3432
+ }
3195
3433
  }
3196
3434
  }
3197
- };
3198
- recordSlots();
3435
+ for (let a = 0; a < spans.length; a++) {
3436
+ for (let b = a + 1; b < spans.length; b++) {
3437
+ if ((spans[a][0] - spans[b][0]) * (spans[a][1] - spans[b][1]) < 0) {
3438
+ total++;
3439
+ }
3440
+ }
3441
+ }
3442
+ }
3443
+ return total;
3444
+ }
3445
+ function orderWithinLayers(layout, upstream, downstream) {
3446
+ const slotOf = /* @__PURE__ */ new Map();
3447
+ for (const layer of layout.layers) {
3448
+ for (let i = 0; i < layer.length; i++) {
3449
+ slotOf.set(layer[i], i);
3450
+ }
3451
+ }
3199
3452
  const meanSlot = (node, neighbours, layer) => {
3200
3453
  let total = 0;
3201
3454
  let count = 0;
@@ -3215,22 +3468,377 @@ function orderWithinLayers(layout, upstream, downstream) {
3215
3468
  if (adjacent < 0 || adjacent >= layout.layers.length) {
3216
3469
  continue;
3217
3470
  }
3218
- const layer = layout.layers[index];
3219
- const keys = /* @__PURE__ */ new Map();
3220
- for (const node of layer) {
3221
- const mean = meanSlot(node, neighbours, adjacent);
3222
- keys.set(node, mean === -1 ? slotOf.get(node) : mean);
3471
+ const layer = layout.layers[index];
3472
+ const keys = /* @__PURE__ */ new Map();
3473
+ for (const node of layer) {
3474
+ const mean = meanSlot(node, neighbours, adjacent);
3475
+ keys.set(node, mean === -1 ? slotOf.get(node) : mean);
3476
+ }
3477
+ layer.sort((a, b) => keys.get(a) - keys.get(b));
3478
+ for (let s = 0; s < layer.length; s++) {
3479
+ slotOf.set(layer[s], s);
3480
+ }
3481
+ }
3482
+ };
3483
+ let best = layout.layers.map((layer) => layer.slice());
3484
+ let fewest = crossingsOf(layout, downstream);
3485
+ for (let i = 0; i < orderingSweeps && fewest > 0; i++) {
3486
+ sweep(upstream, -1);
3487
+ sweep(downstream, 1);
3488
+ const crossings = crossingsOf(layout, downstream);
3489
+ if (crossings < fewest) {
3490
+ fewest = crossings;
3491
+ best = layout.layers.map((layer) => layer.slice());
3492
+ }
3493
+ }
3494
+ for (let i = 0; i < best.length; i++) {
3495
+ layout.layers[i] = best[i];
3496
+ }
3497
+ }
3498
+ function findPieces(sorting, upstream, downstream) {
3499
+ const rankOf = /* @__PURE__ */ new Map();
3500
+ sorting.forEach((node, i) => rankOf.set(node, i));
3501
+ const seen = /* @__PURE__ */ new Set();
3502
+ const pieces = new Array();
3503
+ for (const start of sorting) {
3504
+ if (seen.has(start)) {
3505
+ continue;
3506
+ }
3507
+ seen.add(start);
3508
+ const piece = new Array();
3509
+ const stack = [start];
3510
+ while (stack.length > 0) {
3511
+ const node = stack.pop();
3512
+ piece.push(node);
3513
+ for (const neighbour of [...upstream.get(node) ?? [], ...downstream.get(node) ?? []]) {
3514
+ if (rankOf.has(neighbour) && !seen.has(neighbour)) {
3515
+ seen.add(neighbour);
3516
+ stack.push(neighbour);
3517
+ }
3518
+ }
3519
+ }
3520
+ piece.sort((a, b) => rankOf.get(a) - rankOf.get(b));
3521
+ pieces.push(piece);
3522
+ }
3523
+ return pieces;
3524
+ }
3525
+ function treeRoot(piece, downstream) {
3526
+ let root;
3527
+ for (const node of piece) {
3528
+ const consumers = downstream.get(node) ?? [];
3529
+ if (consumers.length > 1) {
3530
+ return void 0;
3531
+ }
3532
+ if (consumers.length === 0) {
3533
+ if (root !== void 0) {
3534
+ return void 0;
3535
+ }
3536
+ root = node;
3537
+ }
3538
+ }
3539
+ return root;
3540
+ }
3541
+ function columnXs(widths) {
3542
+ const xs = new Array();
3543
+ let x = 0;
3544
+ for (const width2 of widths) {
3545
+ xs.push(x);
3546
+ x += width2 + widthSpacing;
3547
+ }
3548
+ return xs;
3549
+ }
3550
+ function layoutTree(root, upstream, sizes) {
3551
+ const children = (node) => [...new Set(upstream.get(node) ?? [])];
3552
+ const depthOf = /* @__PURE__ */ new Map([[root, 0]]);
3553
+ const preorder = new Array();
3554
+ const stack = [root];
3555
+ let deepest = 0;
3556
+ while (stack.length > 0) {
3557
+ const node = stack.pop();
3558
+ preorder.push(node);
3559
+ const kids = children(node);
3560
+ for (let i = kids.length - 1; i >= 0; i--) {
3561
+ const depth = depthOf.get(node) + 1;
3562
+ depthOf.set(kids[i], depth);
3563
+ deepest = Math.max(deepest, depth);
3564
+ stack.push(kids[i]);
3565
+ }
3566
+ }
3567
+ const widths = new Array(deepest + 1).fill(0);
3568
+ for (const [node, depth] of depthOf) {
3569
+ widths[deepest - depth] = Math.max(widths[deepest - depth], sizes.get(node).x);
3570
+ }
3571
+ const xs = columnXs(widths);
3572
+ const band = /* @__PURE__ */ new Map();
3573
+ const topInBand = /* @__PURE__ */ new Map();
3574
+ const childOffset = /* @__PURE__ */ new Map();
3575
+ for (let i = preorder.length - 1; i >= 0; i--) {
3576
+ const node = preorder[i];
3577
+ const height2 = sizes.get(node).y;
3578
+ const kids = children(node);
3579
+ if (kids.length === 0) {
3580
+ band.set(node, height2);
3581
+ topInBand.set(node, 0);
3582
+ continue;
3583
+ }
3584
+ let offset = 0;
3585
+ for (const kid of kids) {
3586
+ childOffset.set(kid, offset);
3587
+ offset += band.get(kid) + heightSpacing;
3588
+ }
3589
+ const centre = (kid) => childOffset.get(kid) + topInBand.get(kid) + sizes.get(kid).y / 2;
3590
+ let top = (centre(kids[0]) + centre(kids[kids.length - 1])) / 2 - height2 / 2;
3591
+ let shift = 0;
3592
+ if (top < 0) {
3593
+ shift = -top;
3594
+ top = 0;
3595
+ for (const kid of kids) {
3596
+ childOffset.set(kid, childOffset.get(kid) + shift);
3597
+ }
3598
+ }
3599
+ band.set(node, Math.max(offset - heightSpacing + shift, top + height2));
3600
+ topInBand.set(node, top);
3601
+ }
3602
+ const positions = /* @__PURE__ */ new Map();
3603
+ const bandTop = /* @__PURE__ */ new Map([[root, 0]]);
3604
+ for (const node of preorder) {
3605
+ const column = deepest - depthOf.get(node);
3606
+ const size = sizes.get(node);
3607
+ positions.set(node, {
3608
+ x: xs[column] + (widths[column] - size.x) / 2,
3609
+ y: bandTop.get(node) + topInBand.get(node)
3610
+ });
3611
+ for (const kid of children(node)) {
3612
+ bandTop.set(kid, bandTop.get(node) + childOffset.get(kid));
3613
+ }
3614
+ }
3615
+ return positions;
3616
+ }
3617
+ function isotonic(targets) {
3618
+ const blocks = new Array();
3619
+ for (const target of targets) {
3620
+ blocks.push({ sum: target, count: 1 });
3621
+ while (blocks.length > 1) {
3622
+ const last = blocks[blocks.length - 1];
3623
+ const before = blocks[blocks.length - 2];
3624
+ if (before.sum / before.count <= last.sum / last.count) {
3625
+ break;
3626
+ }
3627
+ before.sum += last.sum;
3628
+ before.count += last.count;
3629
+ blocks.pop();
3630
+ }
3631
+ }
3632
+ const result = new Array();
3633
+ for (const block of blocks) {
3634
+ for (let i = 0; i < block.count; i++) {
3635
+ result.push(block.sum / block.count);
3636
+ }
3637
+ }
3638
+ return result;
3639
+ }
3640
+ function median(values) {
3641
+ const sorted = values.slice().sort((a, b) => a - b);
3642
+ const middle = Math.floor(sorted.length / 2);
3643
+ return sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
3644
+ }
3645
+ function layoutLayered(piece, upstream, downstream, sizes) {
3646
+ const layout = assignLayers(piece, upstream);
3647
+ const up = /* @__PURE__ */ new Map();
3648
+ const down = /* @__PURE__ */ new Map();
3649
+ const heightOf = /* @__PURE__ */ new Map();
3650
+ for (const node of piece) {
3651
+ up.set(node, [...new Set(upstream.get(node) ?? [])]);
3652
+ down.set(node, [...new Set(downstream.get(node) ?? [])]);
3653
+ heightOf.set(node, sizes.get(node).y);
3654
+ }
3655
+ let nextLane = -1;
3656
+ for (const from of piece) {
3657
+ const fromLayer = layout.layerOf.get(from);
3658
+ const consumers = down.get(from);
3659
+ for (let c = 0; c < consumers.length; c++) {
3660
+ const to = consumers[c];
3661
+ const toLayer = layout.layerOf.get(to);
3662
+ if (toLayer - fromLayer < 2) {
3663
+ continue;
3664
+ }
3665
+ let previous = from;
3666
+ for (let layer = fromLayer + 1; layer < toLayer; layer++) {
3667
+ const lane = nextLane--;
3668
+ layout.layerOf.set(lane, layer);
3669
+ layout.layers[layer].push(lane);
3670
+ heightOf.set(lane, 0);
3671
+ up.set(lane, [previous]);
3672
+ down.set(lane, []);
3673
+ if (previous === from) {
3674
+ consumers[c] = lane;
3675
+ } else {
3676
+ down.get(previous).push(lane);
3677
+ }
3678
+ previous = lane;
3679
+ }
3680
+ down.get(previous).push(to);
3681
+ const feeders = up.get(to);
3682
+ feeders[feeders.indexOf(from)] = previous;
3683
+ }
3684
+ }
3685
+ const sequence = /* @__PURE__ */ new Map();
3686
+ for (const sink of piece) {
3687
+ if (down.get(sink).length > 0) {
3688
+ continue;
3689
+ }
3690
+ const stack = [sink];
3691
+ while (stack.length > 0) {
3692
+ const node = stack.pop();
3693
+ if (sequence.has(node)) {
3694
+ continue;
3695
+ }
3696
+ sequence.set(node, sequence.size);
3697
+ const feeders = up.get(node) ?? [];
3698
+ for (let i = feeders.length - 1; i >= 0; i--) {
3699
+ stack.push(feeders[i]);
3223
3700
  }
3224
- layer.sort((a, b) => keys.get(a) - keys.get(b));
3225
- for (let s = 0; s < layer.length; s++) {
3226
- slotOf.set(layer[s], s);
3701
+ }
3702
+ }
3703
+ for (const layer of layout.layers) {
3704
+ layer.sort((a, b) => (sequence.get(a) ?? Infinity) - (sequence.get(b) ?? Infinity));
3705
+ }
3706
+ orderWithinLayers(layout, up, down);
3707
+ const gapAfter = (above, below) => above < 0 || below < 0 ? laneSpacing : heightSpacing;
3708
+ const topOf = /* @__PURE__ */ new Map();
3709
+ for (const layer of layout.layers) {
3710
+ let y = 0;
3711
+ for (let i = 0; i < layer.length; i++) {
3712
+ topOf.set(layer[i], y);
3713
+ y += heightOf.get(layer[i]) + (i + 1 < layer.length ? gapAfter(layer[i], layer[i + 1]) : 0);
3714
+ }
3715
+ for (const node of layer) {
3716
+ topOf.set(node, topOf.get(node) - y / 2);
3717
+ }
3718
+ }
3719
+ const centreOf = (node) => topOf.get(node) + heightOf.get(node) / 2;
3720
+ const place = (layer, neighbours, adjacent) => {
3721
+ const offsets = new Array();
3722
+ const targets = new Array();
3723
+ let offset = 0;
3724
+ for (let i = 0; i < layer.length; i++) {
3725
+ const node = layer[i];
3726
+ const centres = (neighbours.get(node) ?? []).filter((n) => layout.layerOf.get(n) === adjacent).map(centreOf);
3727
+ const desired = centres.length > 0 ? median(centres) - heightOf.get(node) / 2 : topOf.get(node);
3728
+ offsets.push(offset);
3729
+ targets.push(desired - offset);
3730
+ if (i + 1 < layer.length) {
3731
+ offset += heightOf.get(node) + gapAfter(node, layer[i + 1]);
3227
3732
  }
3228
3733
  }
3734
+ const solved = isotonic(targets);
3735
+ for (let i = 0; i < layer.length; i++) {
3736
+ topOf.set(layer[i], solved[i] + offsets[i]);
3737
+ }
3229
3738
  };
3230
- for (let i = 0; i < orderingSweeps; i++) {
3231
- sweep(upstream, -1);
3232
- sweep(downstream, 1);
3739
+ for (let s = 0; s < placementSweeps; s++) {
3740
+ if (s % 2 === 0) {
3741
+ for (let i = 1; i < layout.layers.length; i++) {
3742
+ place(layout.layers[i], up, i - 1);
3743
+ }
3744
+ } else {
3745
+ for (let i = layout.layers.length - 2; i >= 0; i--) {
3746
+ place(layout.layers[i], down, i + 1);
3747
+ }
3748
+ }
3749
+ }
3750
+ const widths = layout.layers.map((layer) => layer.reduce((widest, node) => node < 0 ? widest : Math.max(widest, sizes.get(node).x), 0));
3751
+ const xs = columnXs(widths);
3752
+ const positions = /* @__PURE__ */ new Map();
3753
+ for (const node of piece) {
3754
+ const layer = layout.layerOf.get(node);
3755
+ positions.set(node, {
3756
+ x: xs[layer] + (widths[layer] - sizes.get(node).x) / 2,
3757
+ y: topOf.get(node)
3758
+ });
3233
3759
  }
3760
+ return positions;
3761
+ }
3762
+ function layoutLoose(loose, sizes) {
3763
+ const columns = Math.ceil(Math.sqrt(loose.length));
3764
+ const widths = new Array(columns).fill(0);
3765
+ const heights = new Array(Math.ceil(loose.length / columns)).fill(0);
3766
+ loose.forEach((node, i) => {
3767
+ const size = sizes.get(node);
3768
+ widths[i % columns] = Math.max(widths[i % columns], size.x);
3769
+ heights[Math.floor(i / columns)] = Math.max(heights[Math.floor(i / columns)], size.y);
3770
+ });
3771
+ const xs = new Array();
3772
+ let x = 0;
3773
+ for (const width2 of widths) {
3774
+ xs.push(x);
3775
+ x += width2 + heightSpacing;
3776
+ }
3777
+ const ys = new Array();
3778
+ let y = 0;
3779
+ for (const height2 of heights) {
3780
+ ys.push(y);
3781
+ y += height2 + heightSpacing;
3782
+ }
3783
+ const positions = /* @__PURE__ */ new Map();
3784
+ loose.forEach((node, i) => positions.set(node, { x: xs[i % columns], y: ys[Math.floor(i / columns)] }));
3785
+ return positions;
3786
+ }
3787
+ function toBlock(positions, sizes) {
3788
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3789
+ for (const [node, position] of positions) {
3790
+ const size = sizes.get(node);
3791
+ minX = Math.min(minX, position.x);
3792
+ minY = Math.min(minY, position.y);
3793
+ maxX = Math.max(maxX, position.x + size.x);
3794
+ maxY = Math.max(maxY, position.y + size.y);
3795
+ }
3796
+ const shifted = /* @__PURE__ */ new Map();
3797
+ for (const [node, position] of positions) {
3798
+ shifted.set(node, { x: position.x - minX, y: position.y - minY });
3799
+ }
3800
+ return { positions: shifted, size: { x: maxX - minX, y: maxY - minY } };
3801
+ }
3802
+ function layoutGraph(sorting, upstream, downstream, sizes) {
3803
+ const blocks = new Array();
3804
+ const loose = new Array();
3805
+ for (const piece of findPieces(sorting, upstream, downstream)) {
3806
+ if (piece.length === 1) {
3807
+ loose.push(piece[0]);
3808
+ continue;
3809
+ }
3810
+ const root = treeRoot(piece, downstream);
3811
+ const positions2 = root !== void 0 ? layoutTree(root, upstream, sizes) : layoutLayered(piece, upstream, downstream, sizes);
3812
+ blocks.push(toBlock(positions2, sizes));
3813
+ }
3814
+ blocks.sort((a, b) => b.size.y - a.size.y || b.size.x - a.size.x);
3815
+ if (loose.length > 0) {
3816
+ blocks.push(toBlock(layoutLoose(loose, sizes), sizes));
3817
+ }
3818
+ let area = 0;
3819
+ let widest = 0;
3820
+ for (const block of blocks) {
3821
+ area += (block.size.x + pieceSpacing) * (block.size.y + pieceSpacing);
3822
+ widest = Math.max(widest, block.size.x);
3823
+ }
3824
+ const rowWidth = Math.max(widest, Math.sqrt(area));
3825
+ const positions = /* @__PURE__ */ new Map();
3826
+ let x = 0;
3827
+ let y = 0;
3828
+ let rowHeight = 0;
3829
+ for (const block of blocks) {
3830
+ if (x > 0 && x + block.size.x > rowWidth) {
3831
+ x = 0;
3832
+ y += rowHeight + pieceSpacing;
3833
+ rowHeight = 0;
3834
+ }
3835
+ for (const [node, position] of block.positions) {
3836
+ positions.set(node, { x: position.x + x, y: position.y + y });
3837
+ }
3838
+ x += block.size.x + pieceSpacing;
3839
+ rowHeight = Math.max(rowHeight, block.size.y);
3840
+ }
3841
+ return positions;
3234
3842
  }
3235
3843
  function Organize(ctx, graph, nodesToSort) {
3236
3844
  const nodes = graph.getNodes();
@@ -3239,9 +3847,6 @@ function Organize(ctx, graph, nodesToSort) {
3239
3847
  }
3240
3848
  const sorting = new Array();
3241
3849
  if (nodesToSort) {
3242
- if (nodesToSort.length < 2) {
3243
- return;
3244
- }
3245
3850
  for (const index of nodesToSort) {
3246
3851
  if (index >= 0 && index < nodes.length) {
3247
3852
  sorting.push(index);
@@ -3261,33 +3866,28 @@ function Organize(ctx, graph, nodesToSort) {
3261
3866
  nodeLUT.set(nodes[i], i);
3262
3867
  }
3263
3868
  const camera = new Camera();
3264
- const bounds = /* @__PURE__ */ new Map();
3869
+ const sizes = /* @__PURE__ */ new Map();
3265
3870
  for (const index of sorting) {
3266
- bounds.set(index, nodes[index].calculateBounds(ctx, camera));
3871
+ sizes.set(index, nodes[index].calculateBounds(ctx, camera).Size);
3267
3872
  }
3268
3873
  const upstream = /* @__PURE__ */ new Map();
3269
- const downstream = /* @__PURE__ */ new Map();
3270
3874
  for (const index of sorting) {
3875
+ const node = nodes[index];
3271
3876
  const feeders = new Array();
3272
- for (const feeder of graph.connectedInputsNodeReferencesByIndex(index)) {
3273
- const feederIndex = nodeLUT.get(feeder);
3274
- if (feederIndex !== void 0 && inLayout.has(feederIndex) && feederIndex !== index) {
3275
- feeders.push(feederIndex);
3877
+ for (let i = 0; i < node.inputs(); i++) {
3878
+ for (const connection of node.inputPort(i).connections()) {
3879
+ const feeder = connection.outNode();
3880
+ const feederIndex = feeder === null ? void 0 : nodeLUT.get(feeder);
3881
+ if (feederIndex !== void 0 && inLayout.has(feederIndex) && feederIndex !== index && !feeders.includes(feederIndex)) {
3882
+ feeders.push(feederIndex);
3883
+ }
3276
3884
  }
3277
3885
  }
3278
3886
  upstream.set(index, feeders);
3279
- const consumers = new Array();
3280
- for (const consumer of graph.connectedOutputsNodeReferences(index)) {
3281
- const consumerIndex = nodeLUT.get(consumer);
3282
- if (consumerIndex !== void 0 && inLayout.has(consumerIndex) && consumerIndex !== index) {
3283
- consumers.push(consumerIndex);
3284
- }
3285
- }
3286
- downstream.set(index, consumers);
3287
3887
  }
3288
- const layout = assignLayers(sorting, upstream);
3289
- orderWithinLayers(layout, upstream, downstream);
3290
- let anchor = { x: 0, y: 0 };
3888
+ const downstream = consumersOf(sorting, upstream);
3889
+ const positions = layoutGraph(sorting, upstream, downstream, sizes);
3890
+ const anchor = { x: 0, y: 0 };
3291
3891
  for (const index of sorting) {
3292
3892
  const position = nodes[index].getPosition();
3293
3893
  anchor.x += position.x;
@@ -3295,19 +3895,14 @@ function Organize(ctx, graph, nodesToSort) {
3295
3895
  }
3296
3896
  anchor.x /= sorting.length;
3297
3897
  anchor.y /= sorting.length;
3298
- const layerWidths = layout.layers.map((layer) => layer.reduce((widest, node) => Math.max(widest, bounds.get(node).Size.x), 0));
3299
- const layerHeights = layout.layers.map((layer) => layer.reduce((total, node) => total + bounds.get(node).Size.y + heightSpacing, -heightSpacing));
3300
- const totalWidth = layerWidths.reduce((total, width2) => total + width2 + widthSpacing, -widthSpacing);
3301
- const tallest = layerHeights.reduce((tallest2, height2) => Math.max(tallest2, height2), 0);
3302
- let x = anchor.x - totalWidth / 2;
3303
- for (let i = 0; i < layout.layers.length; i++) {
3304
- let y = anchor.y - tallest / 2 + (tallest - layerHeights[i]) / 2;
3305
- for (const node of layout.layers[i]) {
3306
- const size = bounds.get(node).Size;
3307
- nodes[node].setPosition({ x: x + (layerWidths[i] - size.x) / 2, y });
3308
- y += size.y + heightSpacing;
3309
- }
3310
- x += layerWidths[i] + widthSpacing;
3898
+ let right = 0;
3899
+ let bottom = 0;
3900
+ for (const [index, position] of positions) {
3901
+ right = Math.max(right, position.x + sizes.get(index).x);
3902
+ bottom = Math.max(bottom, position.y + sizes.get(index).y);
3903
+ }
3904
+ for (const [index, position] of positions) {
3905
+ nodes[index].setPosition({ x: anchor.x - right / 2 + position.x, y: anchor.y - bottom / 2 + position.y });
3311
3906
  }
3312
3907
  }
3313
3908
 
@@ -3499,14 +4094,17 @@ var NodeSubsystem2 = class {
3499
4094
  #portHovering;
3500
4095
  #widgetHovering;
3501
4096
  #widgetCurrentlyClicking;
4097
+ #widgetHoveringBox = null;
4098
+ #capturedWidgetBox = null;
4099
+ #capturedPointer = Zero();
3502
4100
  #cursor;
3503
4101
  #nodeFactory;
3504
4102
  #boxSelect;
3505
4103
  #boxSelectStart_graphSpace;
3506
4104
  #boxSelectEnd_graphSpace;
3507
4105
  #boxSelectionNodes;
3508
- #boxSelectStyle;
3509
4106
  #lastCtx;
4107
+ #lastMousePosition;
3510
4108
  // LISTENERS ==============================================================
3511
4109
  #nodeRemovedCallbacks;
3512
4110
  #registeredNodeAddedCallbacks;
@@ -3531,14 +4129,6 @@ var NodeSubsystem2 = class {
3531
4129
  this.#boxSelectEnd_graphSpace = Zero();
3532
4130
  this.#boxSelectionNodes = new List();
3533
4131
  this.#lastCtx = null;
3534
- this.#boxSelectStyle = new BoxStyle({
3535
- border: {
3536
- color: Theme.BoxSelect.Color,
3537
- size: Theme.BoxSelect.Size
3538
- },
3539
- color: "rgba(0,0,0,0)",
3540
- radius: Theme.BoxSelect.Radius
3541
- });
3542
4132
  }
3543
4133
  addPublisher(identifier, publisher) {
3544
4134
  this.#nodeFactory.addPublisher(identifier, publisher);
@@ -3575,6 +4165,7 @@ var NodeSubsystem2 = class {
3575
4165
  }
3576
4166
  if (intersection.Widget !== void 0) {
3577
4167
  this.#widgetHovering = intersection.Widget;
4168
+ this.#widgetHoveringBox = intersection.WidgetBox ?? null;
3578
4169
  }
3579
4170
  if (intersection.Port !== void 0 && intersection.Node !== void 0 && intersection.PortIndex !== void 0 && intersection.PortIsInput !== void 0) {
3580
4171
  this.#portHovering = {
@@ -3597,8 +4188,15 @@ var NodeSubsystem2 = class {
3597
4188
  hoveringSomething = true;
3598
4189
  }
3599
4190
  if (this.#widgetHovering !== null) {
3600
- this.#widgetHovering.ClickStart();
3601
- this.#widgetCurrentlyClicking = this.#widgetHovering;
4191
+ if (!this.#widgetHovering.Disabled?.()) {
4192
+ this.#widgetCurrentlyClicking = this.#widgetHovering;
4193
+ this.#capturedWidgetBox = this.#widgetHoveringBox === null ? null : {
4194
+ Position: { x: this.#widgetHoveringBox.Position.x, y: this.#widgetHoveringBox.Position.y },
4195
+ Size: { x: this.#widgetHoveringBox.Size.x, y: this.#widgetHoveringBox.Size.y }
4196
+ };
4197
+ CopyVector2(this.#capturedPointer, mousePosition);
4198
+ this.#widgetCurrentlyClicking.PointerDown?.(this.#widgetPointer());
4199
+ }
3602
4200
  hoveringSomething = true;
3603
4201
  }
3604
4202
  if (this.#portHovering === null) {
@@ -3613,12 +4211,12 @@ var NodeSubsystem2 = class {
3613
4211
  return hoveringSomething || ctrlKey;
3614
4212
  }
3615
4213
  if (this.#portHovering.InputPort) {
3616
- for (let i = 0; i < this.#connections.length; i++) {
3617
- if (this.#connections[i].inPort() === this.#portHovering.Port) {
3618
- this.#connections[i].clearInput();
3619
- this.#connectionSelected = this.#connections[i];
3620
- break;
3621
- }
4214
+ const port = this.#portHovering.Port;
4215
+ const wires = port.connections();
4216
+ const picked = wires.find((wire) => wire.mouseOverPort(mousePosition) === port) ?? wires[0];
4217
+ if (picked !== void 0) {
4218
+ picked.clearInput();
4219
+ this.#connectionSelected = picked;
3622
4220
  }
3623
4221
  } else {
3624
4222
  let inNode = this.#portHovering.Node;
@@ -3638,7 +4236,19 @@ var NodeSubsystem2 = class {
3638
4236
  }
3639
4237
  return true;
3640
4238
  }
4239
+ #widgetPointer() {
4240
+ return {
4241
+ position: { x: this.#capturedPointer.x, y: this.#capturedPointer.y },
4242
+ inside: this.#capturedWidgetBox !== null && InBox(this.#capturedWidgetBox, this.#capturedPointer)
4243
+ };
4244
+ }
3641
4245
  mouseDragEvent(delta, scale) {
4246
+ if (this.#widgetCurrentlyClicking !== null) {
4247
+ this.#capturedPointer.x += delta.x;
4248
+ this.#capturedPointer.y += delta.y;
4249
+ this.#widgetCurrentlyClicking.PointerMove?.(this.#widgetPointer());
4250
+ return true;
4251
+ }
3642
4252
  let nodeMoved = false;
3643
4253
  VectorPool.run(() => {
3644
4254
  const scaledDelta = VectorPool.get();
@@ -3757,13 +4367,20 @@ var NodeSubsystem2 = class {
3757
4367
  }
3758
4368
  #firstCompatiblePort(node, side, against) {
3759
4369
  const count = side === "INPUT" /* Input */ ? node.inputs() : node.outputs();
4370
+ let firstHidden = -1;
3760
4371
  for (let i = 0; i < count; i++) {
3761
4372
  const port = side === "INPUT" /* Input */ ? node.inputPort(i) : node.outputPort(i);
3762
- if (portsCompatible(port, against)) {
4373
+ if (!portsCompatible(port, against)) {
4374
+ continue;
4375
+ }
4376
+ if (!port.hidden()) {
3763
4377
  return i;
3764
4378
  }
4379
+ if (firstHidden === -1) {
4380
+ firstHidden = i;
4381
+ }
3765
4382
  }
3766
- return -1;
4383
+ return firstHidden;
3767
4384
  }
3768
4385
  clickEnd() {
3769
4386
  for (let i = 0; i < this.#nodesGrabbed.Count(); i++) {
@@ -3779,10 +4396,11 @@ var NodeSubsystem2 = class {
3779
4396
  this.#boxSelect = false;
3780
4397
  }
3781
4398
  if (this.#widgetCurrentlyClicking !== null) {
3782
- if (this.#widgetHovering === this.#widgetCurrentlyClicking) {
3783
- this.#widgetCurrentlyClicking.ClickEnd();
3784
- }
4399
+ const widget = this.#widgetCurrentlyClicking;
4400
+ const pointer = this.#widgetPointer();
3785
4401
  this.#widgetCurrentlyClicking = null;
4402
+ this.#capturedWidgetBox = null;
4403
+ widget.PointerUp?.(pointer);
3786
4404
  }
3787
4405
  if (this.#connectionSelected === null) {
3788
4406
  return;
@@ -3827,7 +4445,7 @@ var NodeSubsystem2 = class {
3827
4445
  inputPort.connections()[0].clearOutput();
3828
4446
  this.#removeConnection(inputPort.connections()[0], false);
3829
4447
  }
3830
- conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace);
4448
+ conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace, this.#insertIndex());
3831
4449
  } else {
3832
4450
  conn.setOutput(this.#portHovering.Node, this.#portHovering.Index);
3833
4451
  }
@@ -4199,7 +4817,8 @@ var NodeSubsystem2 = class {
4199
4817
  }
4200
4818
  if (intersection.Widget !== void 0) {
4201
4819
  this.#widgetHovering = intersection.Widget;
4202
- this.#cursor = "pointer" /* Pointer */;
4820
+ this.#widgetHoveringBox = intersection.WidgetBox ?? null;
4821
+ this.#cursor = intersection.Widget.Disabled?.() ? "default" /* Default */ : intersection.Widget.Cursor?.() ?? "pointer" /* Pointer */;
4203
4822
  }
4204
4823
  if (intersection.Port !== void 0 && intersection.Node !== void 0 && intersection.PortIndex !== void 0 && intersection.PortIsInput !== void 0) {
4205
4824
  this.#portHovering = {
@@ -4222,6 +4841,9 @@ var NodeSubsystem2 = class {
4222
4841
  }
4223
4842
  this.#nodes[i].render(ctx, camera, state, mousePosition, this.#postProcessPass);
4224
4843
  }
4844
+ if (this.#widgetCurrentlyClicking !== null) {
4845
+ this.#cursor = this.#widgetCurrentlyClicking.Cursor?.() ?? "pointer" /* Pointer */;
4846
+ }
4225
4847
  }
4226
4848
  #boxSelectionScreenspaceBox(camera) {
4227
4849
  const box = { Position: Zero(), Size: Zero() };
@@ -4230,29 +4852,174 @@ var NodeSubsystem2 = class {
4230
4852
  SubVector2(box.Size, box.Size, box.Position);
4231
4853
  return box;
4232
4854
  }
4855
+ /**
4856
+ * Where a wire being dropped on the hovered array input would go, or
4857
+ * undefined when it is not over one.
4858
+ */
4859
+ #insertIndex() {
4860
+ const hovering = this.#portHovering;
4861
+ if (hovering === null || !hovering.InputPort || this.#lastMousePosition === void 0 || hovering.Port.getPortType() !== "INPUTARRAY" /* InputArray */) {
4862
+ return void 0;
4863
+ }
4864
+ const box = hovering.Node.inputPortPosition(hovering.Index);
4865
+ return arrayInsertIndex(box, hovering.Port.connections().length, this.#lastMousePosition.y);
4866
+ }
4867
+ #renderInsertMarker(ctx, camera) {
4868
+ const hovering = this.#portHovering;
4869
+ if (this.#connectionSelected === null || this.#connectionSelected.inPort() !== null || hovering === null) {
4870
+ return;
4871
+ }
4872
+ const index = this.#insertIndex();
4873
+ const count = hovering.Port.connections().length;
4874
+ if (index === void 0 || count === 0) {
4875
+ return;
4876
+ }
4877
+ const box = hovering.Node.inputPortPosition(hovering.Index);
4878
+ const y = box.Position.y + index * box.Size.y / count;
4879
+ const overhang = 6 * camera.zoom;
4880
+ ctx.save();
4881
+ ctx.strokeStyle = "#ffffff";
4882
+ ctx.lineWidth = Math.max(1, 2 * camera.zoom);
4883
+ ctx.beginPath();
4884
+ ctx.moveTo(box.Position.x - overhang, y);
4885
+ ctx.lineTo(box.Position.x + box.Size.x + overhang, y);
4886
+ ctx.stroke();
4887
+ ctx.restore();
4888
+ }
4233
4889
  render(ctx, camera, mousePosition) {
4234
4890
  this.#lastCtx = ctx;
4891
+ this.#lastMousePosition = mousePosition;
4235
4892
  this.#cursor = "default" /* Default */;
4236
4893
  const awaitingLayout = this.#nodes.some((node) => !node.hasBeenLaidOut());
4894
+ for (const node of this.#nodes) {
4895
+ if (node.hasBeenLaidOut()) {
4896
+ node.placePorts(node.calculateBounds(ctx, camera), camera.zoom);
4897
+ }
4898
+ }
4237
4899
  TimeExecution("Render_Connections", () => {
4238
4900
  this.#renderConnections(ctx, camera, mousePosition);
4239
4901
  });
4240
4902
  TimeExecution("Render_Nodes", () => {
4241
4903
  this.#renderNodes(ctx, camera, mousePosition);
4242
4904
  });
4905
+ this.#renderInsertMarker(ctx, camera);
4243
4906
  if (awaitingLayout) {
4244
4907
  requestRender();
4245
4908
  }
4246
4909
  if (this.#boxSelect) {
4247
4910
  const box = this.#boxSelectionScreenspaceBox(camera);
4248
4911
  ctx.setLineDash([Theme.BoxSelect.LineDashLength]);
4249
- this.#boxSelectStyle.Draw(ctx, box, 1);
4912
+ new BoxStyle({
4913
+ border: { color: Theme.BoxSelect.Color, size: Theme.BoxSelect.Size },
4914
+ color: "rgba(0,0,0,0)",
4915
+ radius: Theme.BoxSelect.Radius
4916
+ }).Draw(ctx, box, 1);
4250
4917
  ctx.setLineDash([]);
4251
4918
  }
4252
4919
  return { cursorStyle: this.#cursor };
4253
4920
  }
4254
4921
  };
4255
4922
 
4923
+ // src/popup.ts
4924
+ var Popup = class {
4925
+ #title;
4926
+ #options;
4927
+ #content;
4928
+ #onClose;
4929
+ // Styling ================================================================
4930
+ #buttonCSS;
4931
+ // Runtime ================================================================
4932
+ #popup;
4933
+ #keyHandler;
4934
+ constructor(config) {
4935
+ this.#title = config.title;
4936
+ this.#options = config.options;
4937
+ this.#content = config.content;
4938
+ this.#onClose = config.onClose;
4939
+ this.#buttonCSS = config.buttonCSS === void 0 ? "flex-grow: 1; margin-right: 8px;" : config.buttonCSS;
4940
+ this.#popup = null;
4941
+ this.#keyHandler = null;
4942
+ }
4943
+ Hide() {
4944
+ if (this.#popup === null) {
4945
+ return;
4946
+ }
4947
+ if (this.#keyHandler !== null) {
4948
+ document.removeEventListener("keydown", this.#keyHandler, true);
4949
+ this.#keyHandler = null;
4950
+ }
4951
+ document.body.removeChild(this.#popup);
4952
+ this.#popup = null;
4953
+ }
4954
+ #close(option) {
4955
+ this.Hide();
4956
+ if (this.#onClose !== void 0) {
4957
+ this.#onClose(option);
4958
+ }
4959
+ }
4960
+ Show() {
4961
+ this.#popup = document.createElement("div");
4962
+ this.#popup.style.cssText = `position:absolute;top:0;width:100%;display:flex;height:100%;justify-content:center;align-items:center;background-color:${Theme.Popup.Backdrop};`;
4963
+ document.body.appendChild(this.#popup);
4964
+ const container = document.createElement("div");
4965
+ container.style.cssText = `background-color:${Theme.Popup.BackgroundColor};color:${Theme.Popup.FontColor};font-family:${Theme.Popup.FontFamily};border-radius:${Theme.Popup.BorderRadius}px;padding:16px;`;
4966
+ this.#popup.appendChild(container);
4967
+ const title = document.createElement("h2");
4968
+ title.textContent = this.#title;
4969
+ title.style.cssText = "margin-top:0;margin-bottom:16px;";
4970
+ container.appendChild(title);
4971
+ if (this.#content !== void 0) {
4972
+ container.appendChild(this.#content());
4973
+ }
4974
+ const buttonContainer = document.createElement("div");
4975
+ buttonContainer.style.cssText = "margin-top:16px;justify-content:space-around;align-items:center;flex-direction:row;display:flex;";
4976
+ container.appendChild(buttonContainer);
4977
+ for (let i = 0; i < this.#options.length; i++) {
4978
+ const button = document.createElement("button");
4979
+ button.style.cssText = this.#buttonCSS;
4980
+ button.textContent = this.#options[i];
4981
+ button.onclick = () => this.#close(this.#options[i]);
4982
+ buttonContainer.appendChild(button);
4983
+ }
4984
+ this.#keyHandler = (e) => {
4985
+ if (e.key === "Enter") {
4986
+ e.preventDefault();
4987
+ this.#close(this.#options.length > 0 ? this.#options[0] : null);
4988
+ } else if (e.key === "Escape") {
4989
+ e.preventDefault();
4990
+ this.#close(null);
4991
+ }
4992
+ e.stopPropagation();
4993
+ };
4994
+ document.addEventListener("keydown", this.#keyHandler, true);
4995
+ }
4996
+ };
4997
+
4998
+ // src/popups/string.ts
4999
+ function SetStringPopup(config) {
5000
+ let input = null;
5001
+ return new Popup({
5002
+ title: config.title,
5003
+ options: ["Set", "Cancel"],
5004
+ content: () => {
5005
+ const container = document.createElement("div");
5006
+ input = document.createElement("input");
5007
+ input.value = config.startingValue;
5008
+ container.append(input);
5009
+ return container;
5010
+ },
5011
+ onClose: (button) => {
5012
+ if (button !== "Set" || input === null) {
5013
+ if (config.onCancel) {
5014
+ config.onCancel();
5015
+ }
5016
+ return;
5017
+ }
5018
+ config.onUpdate(input.value);
5019
+ }
5020
+ });
5021
+ }
5022
+
4256
5023
  // src/popups/form.ts
4257
5024
  function FormPopup(config) {
4258
5025
  let input = new Array();
@@ -4369,9 +5136,11 @@ var FlowNode = class {
4369
5136
  #titleChangeCallback;
4370
5137
  #infoChangeCallback;
4371
5138
  // Styling ================================================================
5139
+ #styleConfig;
5140
+ #styledForTheme = -1;
4372
5141
  #titleColor;
4373
5142
  #selectedStyle;
4374
- #stateStyles;
5143
+ #stateStyles = /* @__PURE__ */ new Map();
4375
5144
  #padding;
4376
5145
  #portTextStyle;
4377
5146
  #elementSpacing;
@@ -4383,6 +5152,7 @@ var FlowNode = class {
4383
5152
  #data;
4384
5153
  #registeredAnyPropertyChangeCallbacks;
4385
5154
  #registeredPropertyChangeCallbacks;
5155
+ #registeredPropertyEditCallbacks = /* @__PURE__ */ new Map();
4386
5156
  constructor(config) {
4387
5157
  this.#input = new Array();
4388
5158
  this.#output = new Array();
@@ -4435,58 +5205,17 @@ var FlowNode = class {
4435
5205
  this.#position = config?.position === void 0 ? { x: 0, y: 0 } : config.position;
4436
5206
  this.#title = new Text(
4437
5207
  config?.title === void 0 ? "" : config.title,
4438
- TextStyleFallback(config?.style?.title?.textStyle, {
4439
- size: 16,
4440
- weight: "bold" /* Bold */,
4441
- color: Theme.Node.FontColor
4442
- })
5208
+ TextStyleFallback(config?.style?.title?.textStyle, { size: 16, weight: "bold" /* Bold */ })
4443
5209
  );
4444
5210
  this.#subTitle = new Text(
4445
5211
  config?.subTitle === void 0 ? "" : config.subTitle,
4446
- TextStyleFallback(config?.style?.title?.textStyle, {
4447
- size: 10,
4448
- weight: "bold" /* Bold */,
4449
- color: Theme.Node.FontColor
4450
- })
4451
- );
4452
- this.#infoSymbol = new Text(
4453
- "i",
4454
- {
4455
- size: 13,
4456
- weight: "bold" /* Bold */,
4457
- color: Theme.Node.FontColor
4458
- }
5212
+ TextStyleFallback(config?.style?.title?.textStyle, { size: 10, weight: "bold" /* Bold */ })
4459
5213
  );
5214
+ this.#infoSymbol = new Text("i", { size: 13, weight: "bold" /* Bold */ });
4460
5215
  this.#infoText = config?.info ? config?.info : "";
4461
- this.#titleColor = config?.style?.title?.color === void 0 ? "#154050" : config?.style?.title?.color;
4462
5216
  this.#padding = config?.style?.title?.padding === void 0 ? 15 : config?.style?.title?.padding;
4463
- this.#stateStyles = /* @__PURE__ */ new Map();
4464
- this.#stateStyles.set(0 /* Idle */, new BoxStyle(BoxStyleWithFallback(config?.style?.idle, {
4465
- border: { color: Theme.Node.Border.Idle, size: 1 },
4466
- radius: Theme.Node.BorderRadius,
4467
- color: Theme.Node.BackgroundColor
4468
- })));
4469
- this.#stateStyles.set(1 /* MouseOver */, new BoxStyle(BoxStyleWithFallback(config?.style?.mouseOver, {
4470
- border: { color: Theme.Node.Border.MouseOver, size: 1.1 },
4471
- radius: Theme.Node.BorderRadius,
4472
- color: Theme.Node.BackgroundColor
4473
- })));
4474
- this.#stateStyles.set(2 /* Grabbed */, new BoxStyle(BoxStyleWithFallback(config?.style?.grabbed, {
4475
- border: { color: Theme.Node.Border.Grabbed, size: 2 },
4476
- radius: Theme.Node.BorderRadius,
4477
- color: Theme.Node.BackgroundColor
4478
- })));
4479
- this.#selectedStyle = new BoxStyle(BoxStyleWithFallback(config?.style?.selected, {
4480
- border: { color: Theme.Node.Border.Selected, size: 1 },
4481
- radius: Theme.Node.BorderRadius,
4482
- color: Theme.Node.BackgroundColor
4483
- }));
4484
- this.#portTextStyle = new TextStyle({
4485
- size: config?.style?.portText?.size === void 0 ? 14 : config?.style?.portText?.size,
4486
- color: config?.style?.portText?.color === void 0 ? Theme.Node.Port.FontColor : config?.style?.portText?.color,
4487
- font: config?.style?.portText?.font,
4488
- weight: config?.style?.portText?.weight
4489
- });
5217
+ this.#styleConfig = config?.style;
5218
+ this.#applyStyle();
4490
5219
  if (config?.inputs !== void 0) {
4491
5220
  for (let i = 0; i < config.inputs.length; i++) {
4492
5221
  this.addInput(config.inputs[i]);
@@ -4498,12 +5227,8 @@ var FlowNode = class {
4498
5227
  }
4499
5228
  }
4500
5229
  if (config?.widgets !== void 0) {
4501
- for (let i = 0; i < config.widgets.length; i++) {
4502
- const widget = config.widgets[i];
4503
- if (widget.type === void 0) {
4504
- continue;
4505
- }
4506
- this.addWidget(GlobalWidgetFactory.create(this, widget.type, widget.config));
5230
+ for (const widget of config.widgets) {
5231
+ this.addWidget(GlobalWidgetFactory.createFromConfig(this, widget));
4507
5232
  }
4508
5233
  }
4509
5234
  }
@@ -4576,6 +5301,32 @@ var FlowNode = class {
4576
5301
  callbacks[i](oldValue, value);
4577
5302
  }
4578
5303
  }
5304
+ /**
5305
+ * Listens for a person changing the property through a widget, but not
5306
+ * for the property being set from code.
5307
+ */
5308
+ addPropertyEditListener(name, callback) {
5309
+ let callbacks = this.#registeredPropertyEditCallbacks.get(name);
5310
+ if (callbacks === void 0) {
5311
+ callbacks = [];
5312
+ this.#registeredPropertyEditCallbacks.set(name, callbacks);
5313
+ }
5314
+ callbacks.push(callback);
5315
+ }
5316
+ /**
5317
+ * Sets the property as a person's finished edit, so edit listeners hear
5318
+ * it even when the value was already showing.
5319
+ */
5320
+ editProperty(name, value) {
5321
+ this.setProperty(name, value);
5322
+ const callbacks = this.#registeredPropertyEditCallbacks.get(name);
5323
+ if (callbacks === void 0) {
5324
+ return;
5325
+ }
5326
+ for (const callback of [...callbacks]) {
5327
+ callback(value);
5328
+ }
5329
+ }
4579
5330
  getProperty(name) {
4580
5331
  return this.#data[name];
4581
5332
  }
@@ -4864,6 +5615,7 @@ var FlowNode = class {
4864
5615
  // return this.#titleTextStyle.measure(ctx, scale, this.#title);
4865
5616
  // }
4866
5617
  calculateBounds(ctx, camera) {
5618
+ this.#followTheme();
4867
5619
  const tempMeasurement = Zero();
4868
5620
  const doublePadding = this.#padding * 2;
4869
5621
  const screenSpacePosition = Zero();
@@ -4877,25 +5629,21 @@ var FlowNode = class {
4877
5629
  size.x += size.y * 4;
4878
5630
  }
4879
5631
  size.x += doublePadding;
4880
- size.y += doublePadding + this.#elementSpacing * this.#input.length;
4881
- for (let i = 0; i < this.#input.length; i++) {
4882
- const port = this.#input[i];
4883
- this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
4884
- size.y += tempMeasurement.y;
4885
- size.x = Math.max(size.x, tempMeasurement.x + doublePadding);
4886
- }
4887
- size.y += this.#elementSpacing * this.#output.length;
4888
- for (let i = 0; i < this.#output.length; i++) {
4889
- const port = this.#output[i];
5632
+ size.y += doublePadding;
5633
+ for (const port of [...this.#input, ...this.#output]) {
5634
+ if (port.hidden()) {
5635
+ continue;
5636
+ }
4890
5637
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
4891
- size.y += tempMeasurement.y;
5638
+ size.y += this.#portRowHeight(port, tempMeasurement.y) + this.#elementSpacing;
4892
5639
  size.x = Math.max(size.x, tempMeasurement.x + doublePadding);
4893
5640
  }
4894
- size.y += this.#elementSpacing * this.#widgets.length;
4895
- for (let i = 0; i < this.#widgets.length; i++) {
4896
- const element = this.#widgets[i];
4897
- const eleSize = element.Size();
4898
- size.y += eleSize.y;
5641
+ for (const widget of this.#widgets) {
5642
+ if (widget.Hidden?.()) {
5643
+ continue;
5644
+ }
5645
+ const eleSize = widget.Size();
5646
+ size.y += eleSize.y + this.#elementSpacing;
4899
5647
  size.x = Math.max(size.x, eleSize.x + doublePadding);
4900
5648
  }
4901
5649
  size.y += this.#elementSpacing;
@@ -4979,7 +5727,7 @@ var FlowNode = class {
4979
5727
  intersection.Node = this;
4980
5728
  }
4981
5729
  for (let i = 0; i < this.#inputPortPositions.Count(); i++) {
4982
- if (InBox(this.#inputPortPositions.At(i), position)) {
5730
+ if (!this.#input[i].hidden() && InBox(this.#inputPortPositions.At(i), position)) {
4983
5731
  intersection.Node = this;
4984
5732
  intersection.PortIndex = i;
4985
5733
  intersection.PortIsInput = true;
@@ -4987,7 +5735,7 @@ var FlowNode = class {
4987
5735
  }
4988
5736
  }
4989
5737
  for (let i = 0; i < this.#outputPortPositions.Count(); i++) {
4990
- if (InBox(this.#outputPortPositions.At(i), position)) {
5738
+ if (!this.#output[i].hidden() && InBox(this.#outputPortPositions.At(i), position)) {
4991
5739
  intersection.Node = this;
4992
5740
  intersection.PortIndex = i;
4993
5741
  intersection.PortIsInput = false;
@@ -4995,10 +5743,12 @@ var FlowNode = class {
4995
5743
  }
4996
5744
  }
4997
5745
  for (let i = 0; i < this.#widgetPositions.Count(); i++) {
4998
- if (InBox(this.#widgetPositions.At(i), position)) {
5746
+ const widget = this.#widgets[i];
5747
+ if (widget !== void 0 && !widget.Hidden?.() && InBox(this.#widgetPositions.At(i), position)) {
4999
5748
  intersection.Node = this;
5000
5749
  intersection.WidgetIndex = i;
5001
- intersection.Widget = this.#widgets[i];
5750
+ intersection.Widget = widget;
5751
+ intersection.WidgetBox = this.#widgetPositions.At(i);
5002
5752
  }
5003
5753
  }
5004
5754
  return intersection;
@@ -5047,6 +5797,12 @@ var FlowNode = class {
5047
5797
  inputPortPosition(index) {
5048
5798
  return this.#inputPortPositions.At(index);
5049
5799
  }
5800
+ widgetPosition(index) {
5801
+ if (index < 0 || index >= this.#widgetPositions.Count()) {
5802
+ return void 0;
5803
+ }
5804
+ return this.#widgetPositions.At(index);
5805
+ }
5050
5806
  inputPort(index) {
5051
5807
  return this.#input[index];
5052
5808
  }
@@ -5062,6 +5818,35 @@ var FlowNode = class {
5062
5818
  outputs() {
5063
5819
  return this.#output.length;
5064
5820
  }
5821
+ #applyStyle() {
5822
+ const style = this.#styleConfig;
5823
+ const titleText = style?.title?.textStyle?.color ?? Theme.Node.Title.FontColor;
5824
+ this.#title.style().setColor(titleText);
5825
+ this.#subTitle.style().setColor(titleText);
5826
+ this.#infoSymbol.style().setColor(titleText);
5827
+ this.#titleColor = style?.title?.color ?? Theme.Node.Title.Color;
5828
+ const box = (config, border, size) => new BoxStyle(BoxStyleWithFallback(config, {
5829
+ border: { color: border, size },
5830
+ radius: Theme.Node.BorderRadius,
5831
+ color: Theme.Node.BackgroundColor
5832
+ }));
5833
+ this.#stateStyles.set(0 /* Idle */, box(style?.idle, Theme.Node.Border.Idle, 1));
5834
+ this.#stateStyles.set(1 /* MouseOver */, box(style?.mouseOver, Theme.Node.Border.MouseOver, 1.1));
5835
+ this.#stateStyles.set(2 /* Grabbed */, box(style?.grabbed, Theme.Node.Border.Grabbed, 2));
5836
+ this.#selectedStyle = box(style?.selected, Theme.Node.Border.Selected, 1);
5837
+ this.#portTextStyle = new TextStyle({
5838
+ size: style?.portText?.size ?? 14,
5839
+ color: style?.portText?.color ?? Theme.Node.Port.FontColor,
5840
+ font: style?.portText?.font,
5841
+ weight: style?.portText?.weight
5842
+ });
5843
+ this.#styledForTheme = themeVersion();
5844
+ }
5845
+ #followTheme() {
5846
+ if (this.#styledForTheme !== themeVersion()) {
5847
+ this.#applyStyle();
5848
+ }
5849
+ }
5065
5850
  #calculateStyle(state) {
5066
5851
  if (this.#selected && state === 0 /* Idle */) {
5067
5852
  return this.#selectedStyle;
@@ -5078,9 +5863,22 @@ var FlowNode = class {
5078
5863
  clearMessages() {
5079
5864
  this.#messages = [];
5080
5865
  }
5866
+ #portRowHeight(port, textHeight) {
5867
+ const slots = port.slots();
5868
+ return slots > 1 ? Math.max(textHeight, slots * ARRAY_SLOT_PITCH) : textHeight;
5869
+ }
5081
5870
  hasBeenLaidOut() {
5082
5871
  return this.#laidOut;
5083
5872
  }
5873
+ /**
5874
+ * Throws away where the ports were last placed, so wires wait for the
5875
+ * next draw instead of pointing at the old spots.
5876
+ */
5877
+ invalidateLayout() {
5878
+ this.#laidOut = false;
5879
+ this.#portOffsets = { input: [], output: [] };
5880
+ requestRender();
5881
+ }
5084
5882
  #laidOut = false;
5085
5883
  render(ctx, camera, state, mousePosition, postProcess) {
5086
5884
  this.#laidOut = true;
@@ -5118,6 +5916,9 @@ var FlowNode = class {
5118
5916
  [nodeStyle.radius() * camera.zoom, nodeStyle.radius() * camera.zoom, 0, 0]
5119
5917
  );
5120
5918
  ctx.fill();
5919
+ if (Theme.Node.Bevel !== null) {
5920
+ drawBevel(ctx, nodeBounds, camera.zoom, Theme.Node.Bevel, false);
5921
+ }
5121
5922
  const titlePosition = VectorPool.get();
5122
5923
  titlePosition.x = nodeBounds.Position.x + nodeBounds.Size.x / 2;
5123
5924
  titlePosition.y = nodeBounds.Position.y + scaledPadding + titleSize.y / 2;
@@ -5178,8 +5979,12 @@ var FlowNode = class {
5178
5979
  for (let i = 0; i < this.#input.length; i++) {
5179
5980
  ctx.textAlign = "left" /* Left */;
5180
5981
  const port = this.#input[i];
5982
+ if (port.hidden()) {
5983
+ this.#inputPortPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
5984
+ continue;
5985
+ }
5181
5986
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
5182
- const portHeight = tempMeasurement.y * camera.zoom;
5987
+ const portHeight = this.#portRowHeight(port, tempMeasurement.y) * camera.zoom;
5183
5988
  const position = VectorPool.get();
5184
5989
  position.x = nodeBounds.Position.x;
5185
5990
  position.y = startY + portHeight / 2;
@@ -5192,6 +5997,10 @@ var FlowNode = class {
5192
5997
  ctx.textAlign = "right" /* Right */;
5193
5998
  for (let i = 0; i < this.#output.length; i++) {
5194
5999
  const port = this.#output[i];
6000
+ if (port.hidden()) {
6001
+ this.#outputPortPositions.Push({ Position: { x: rightSide, y: startY }, Size: { x: 0, y: 0 } });
6002
+ continue;
6003
+ }
5195
6004
  this.#portTextStyle.measure(ctx, 1, port.getDisplayName(), tempMeasurement);
5196
6005
  const portHeight = tempMeasurement.y * camera.zoom;
5197
6006
  const position = VectorPool.get();
@@ -5204,13 +6013,18 @@ var FlowNode = class {
5204
6013
  }
5205
6014
  for (let i = 0; i < this.#widgets.length; i++) {
5206
6015
  const widget = this.#widgets[i];
6016
+ if (widget.Hidden?.()) {
6017
+ this.#widgetPositions.Push({ Position: { x: nodeBounds.Position.x, y: startY }, Size: { x: 0, y: 0 } });
6018
+ continue;
6019
+ }
5207
6020
  const widgetSize = widget.Size();
5208
- const scaledWidgetWidth = widgetSize.x * camera.zoom;
5209
- const position = VectorPool.get();
5210
- position.x = nodeBounds.Position.x + (nodeBounds.Size.x - scaledWidgetWidth) / 2;
5211
- position.y = startY;
5212
- this.#widgetPositions.Push(widget.Draw(ctx, position, camera.zoom, mousePosition));
5213
- startY += widgetSize.y * camera.zoom + scaledElementSpacing;
6021
+ const box = {
6022
+ Position: { x: nodeBounds.Position.x + scaledPadding, y: startY },
6023
+ Size: { x: nodeBounds.Size.x - scaledPadding * 2, y: widgetSize.y * camera.zoom }
6024
+ };
6025
+ widget.Draw(ctx, box, camera.zoom, mousePosition);
6026
+ this.#widgetPositions.Push(box);
6027
+ startY += box.Size.y + scaledElementSpacing;
5214
6028
  }
5215
6029
  const messageStart = VectorPool.get();
5216
6030
  messageStart.x = nodeBounds.Position.x + nodeBounds.Size.x / 2;
@@ -5247,6 +6061,13 @@ var FlowNode = class {
5247
6061
  record(this.#outputPortPositions, this.#portOffsets.output);
5248
6062
  }
5249
6063
  layoutPortsWithoutDrawing(bounds, zoom) {
6064
+ if (!this.placePorts(bounds, zoom)) {
6065
+ return false;
6066
+ }
6067
+ this.#widgetPositions.Clear();
6068
+ return true;
6069
+ }
6070
+ placePorts(bounds, zoom) {
5250
6071
  if (this.#portOffsets.input.length === 0 && this.#portOffsets.output.length === 0) {
5251
6072
  return false;
5252
6073
  }
@@ -5268,7 +6089,6 @@ var FlowNode = class {
5268
6089
  };
5269
6090
  project(this.#portOffsets.input, this.#inputPortPositions);
5270
6091
  project(this.#portOffsets.output, this.#outputPortPositions);
5271
- this.#widgetPositions.Clear();
5272
6092
  return true;
5273
6093
  }
5274
6094
  };
@@ -5460,33 +6280,33 @@ var CodeBlockEntry = class {
5460
6280
  }
5461
6281
  render(ctx, position, scale, maxWidth) {
5462
6282
  this.#calculateLayout(ctx, maxWidth);
5463
- let padding = Theme.Note.CodeBlock.Padding * scale;
5464
- let max = 0;
5465
- for (let i = 0; i < this.#calculatedEntries.Count(); i++) {
5466
- const pos = this.#calculatedPositions.At(i);
5467
- max = Math.max(max, pos.y * scale);
5468
- }
5469
- const y = position.y + max + scale * 5;
6283
+ const padding = Theme.Note.CodeBlock.Padding * scale;
6284
+ const lineCount = this.#calculatedEntries.Count();
6285
+ const lastTop = lineCount === 0 ? 0 : this.#calculatedPositions.At(lineCount - 1).y;
6286
+ const contentHeight = lineCount === 0 ? 0 : (lastTop + this.#text.style().getSize()) * scale;
6287
+ const height2 = contentHeight + padding * 2;
5470
6288
  ctx.fillStyle = Theme.Note.CodeBlock.BackgroundColor;
5471
6289
  ctx.beginPath();
5472
6290
  ctx.roundRect(
5473
6291
  position.x,
5474
6292
  position.y,
5475
6293
  maxWidth * scale,
5476
- max + padding * 2,
6294
+ height2,
5477
6295
  Theme.Note.CodeBlock.BorderRadius * scale
5478
6296
  );
5479
6297
  ctx.fill();
5480
- for (let i = 0; i < this.#calculatedEntries.Count(); i++) {
6298
+ const baseline = ctx.textBaseline;
6299
+ ctx.textBaseline = "top";
6300
+ for (let i = 0; i < lineCount; i++) {
5481
6301
  const entry = this.#calculatedEntries.At(i);
5482
6302
  const pos = this.#calculatedPositions.At(i);
5483
6303
  entry.render(ctx, scale, {
5484
6304
  x: pos.x * scale + position.x + padding,
5485
6305
  y: pos.y * scale + position.y + padding
5486
6306
  });
5487
- max = Math.max(max, pos.y * scale);
5488
6307
  }
5489
- return max + padding * 2;
6308
+ ctx.textBaseline = baseline;
6309
+ return height2;
5490
6310
  }
5491
6311
  };
5492
6312
  var UnorderedListMarkdownEntry = class {
@@ -5864,9 +6684,10 @@ var MarkdownSyntaxParser = class {
5864
6684
  this.#inc();
5865
6685
  this.#inc();
5866
6686
  this.#inc();
5867
- const codeBlockText = this.#originalText.substring(start, end);
6687
+ const codeBlockText = this.#originalText.substring(start, end).replace(/\r?\n$/, "");
5868
6688
  const entryText = new Text(codeBlockText);
5869
6689
  this.#assignStandardStyling([entryText]);
6690
+ entryText.setColor(Theme.Note.CodeBlock.FontColor);
5870
6691
  return new CodeBlockEntry(entryText);
5871
6692
  }
5872
6693
  parse() {
@@ -5921,6 +6742,7 @@ var BOX_SIZE = 10;
5921
6742
  var FlowNote = class {
5922
6743
  #originalText;
5923
6744
  #document;
6745
+ #parsedForTheme = -1;
5924
6746
  #width;
5925
6747
  #edittingStyle;
5926
6748
  // Runtime ================================================================
@@ -5963,6 +6785,7 @@ var FlowNote = class {
5963
6785
  setText(text) {
5964
6786
  this.#originalText = text;
5965
6787
  this.#document = BuildMarkdown(this.#originalText);
6788
+ this.#parsedForTheme = themeVersion();
5966
6789
  for (let i = 0; i < this.#contentChangeCallbacks.length; i++) {
5967
6790
  this.#contentChangeCallbacks[i](this, text);
5968
6791
  }
@@ -6050,6 +6873,10 @@ var FlowNote = class {
6050
6873
  }
6051
6874
  camera.graphSpaceToScreenSpace(this.#position, this.#tempPosition);
6052
6875
  CopyVector2(this.#lastRenderedBox.Position, this.#tempPosition);
6876
+ if (this.#parsedForTheme !== themeVersion()) {
6877
+ this.#document = BuildMarkdown(this.#originalText);
6878
+ this.#parsedForTheme = themeVersion();
6879
+ }
6053
6880
  const startY = this.#tempPosition.y;
6054
6881
  const lineSpacing = Theme.Note.EntrySpacing * camera.zoom;
6055
6882
  ctx.textAlign = "left" /* Left */;
@@ -6745,13 +7572,15 @@ var Minimap = class {
6745
7572
  // src/graph.ts
6746
7573
  function BuildBackgroundRenderer(backgroundColor) {
6747
7574
  return (canvas, context, position, scale) => {
6748
- context.fillStyle = backgroundColor;
7575
+ context.fillStyle = backgroundColor ?? Theme.Graph.BackgroundColor;
6749
7576
  context.fillRect(0, 0, canvas.width, canvas.height);
6750
- const alpha = Math.round(Clamp01(scale - 0.3) * 255);
7577
+ const alpha = Clamp01(scale - 0.3);
6751
7578
  if (alpha <= 0) {
6752
7579
  return;
6753
7580
  }
6754
- context.fillStyle = `rgba(41, 54, 57, ${alpha})`;
7581
+ context.save();
7582
+ context.globalAlpha = alpha;
7583
+ context.fillStyle = Theme.Graph.DotColor;
6755
7584
  const spacing = 100;
6756
7585
  const pi2 = 2 * Math.PI;
6757
7586
  const dotScale = 2 * scale;
@@ -6764,6 +7593,7 @@ function BuildBackgroundRenderer(backgroundColor) {
6764
7593
  context.fill();
6765
7594
  }
6766
7595
  }
7596
+ context.restore();
6767
7597
  };
6768
7598
  }
6769
7599
  var contextMenuGroup = "graph-context-menu";
@@ -6814,9 +7644,9 @@ var GraphView = class {
6814
7644
  const results = {};
6815
7645
  for (let i = 0; i < this.#subsystems.length; i++) {
6816
7646
  TimeExecution("Render_Subsystem_" + i, () => {
6817
- let results2 = this.#subsystems[i].render(ctx, camera, mousePosition);
6818
- if (results2?.cursorStyle) {
6819
- results2.cursorStyle = results2?.cursorStyle;
7647
+ const layer = this.#subsystems[i].render(ctx, camera, mousePosition);
7648
+ if (layer?.cursorStyle) {
7649
+ results.cursorStyle = layer.cursorStyle;
6820
7650
  }
6821
7651
  });
6822
7652
  }
@@ -6876,8 +7706,7 @@ var NodeFlowGraph = class {
6876
7706
  if (config?.backgroundRenderer !== void 0) {
6877
7707
  this.#backgroundRenderer = config?.backgroundRenderer;
6878
7708
  } else {
6879
- const backgroundColor = config?.backgroundColor === void 0 ? Theme.Graph.BackgroundColor : config.backgroundColor;
6880
- this.#backgroundRenderer = BuildBackgroundRenderer(backgroundColor);
7709
+ this.#backgroundRenderer = BuildBackgroundRenderer(config?.backgroundColor);
6881
7710
  }
6882
7711
  this.#minimap = new Minimap(config?.minimap);
6883
7712
  this.#mainNodeSubsystem.setConnectionReleasedHandler((dangling) => {
@@ -7295,10 +8124,12 @@ var NodeFlowGraph = class {
7295
8124
  this.#cursor = "default" /* Default */;
7296
8125
  TimeExecution("Render_Background", this.#renderBackground.bind(this));
7297
8126
  TimeExecution("Render_View_" + this.#currentView, () => {
7298
- let results = this.currentView().render(this.#ctx, this.#camera, this.#mousePosition);
7299
- if (results?.cursorStyle) {
7300
- this.#cursor = results?.cursorStyle;
7301
- }
8127
+ placeEditsDuring(this.#canvas, () => {
8128
+ let results = this.currentView().render(this.#ctx, this.#camera, this.#mousePosition);
8129
+ if (results?.cursorStyle) {
8130
+ this.#cursor = results?.cursorStyle;
8131
+ }
8132
+ });
7302
8133
  });
7303
8134
  TimeExecution("Render_Minimap", () => {
7304
8135
  this.#minimap.render(this.#ctx, this.#canvas, this.#camera, this.#mainNodeSubsystem.getNodes());
@@ -7344,11 +8175,15 @@ var NodeFlowGraph = class {
7344
8175
  ColorWidget,
7345
8176
  Connection,
7346
8177
  ContextMenuItemState,
8178
+ CursorStyle,
7347
8179
  DefaultConnectionRenderer,
8180
+ DefaultTheme,
7348
8181
  FlowNode,
7349
8182
  FlowNote,
7350
8183
  GlobalWidgetFactory,
8184
+ GraphView,
7351
8185
  ImageWidget,
8186
+ InBox,
7352
8187
  MessageType,
7353
8188
  Minimap,
7354
8189
  NodeFactory,
@@ -7358,16 +8193,28 @@ var NodeFlowGraph = class {
7358
8193
  Port,
7359
8194
  PortType,
7360
8195
  Publisher,
8196
+ RowWidget,
7361
8197
  SliderWidget,
7362
8198
  StringWidget,
7363
8199
  TextWidget,
7364
8200
  Theme,
7365
8201
  ToggleWidget,
8202
+ ValueWidget,
8203
+ WidgetBase,
7366
8204
  WidgetFactory,
8205
+ drawField,
8206
+ drawFieldBox,
8207
+ drawFieldText,
8208
+ editInPlace,
8209
+ formatNumber,
8210
+ onThemeChange,
7367
8211
  portConfigTypes,
7368
8212
  portsCompatible,
7369
8213
  renderContinuously,
7370
8214
  requestRender,
7371
- typeSetsCompatible
8215
+ setTheme,
8216
+ themeVersion,
8217
+ typeSetsCompatible,
8218
+ widgetFont
7372
8219
  });
7373
8220
  //# sourceMappingURL=index.js.map