@elicdavis/node-flow 0.1.19 → 0.2.0

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 (55) hide show
  1. package/dist/index.d.mts +386 -227
  2. package/dist/index.d.ts +386 -227
  3. package/dist/index.js +1884 -1076
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1867 -1075
  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 +12 -10
  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 +162 -111
  16. package/src/nodes/subsystem.ts +123 -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 +84 -0
  33. package/src/widgets/number.ts +122 -154
  34. package/src/widgets/row.ts +138 -0
  35. package/src/widgets/slider.ts +101 -216
  36. package/src/widgets/string.ts +39 -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 +180 -34
  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/hover_after_removal.test.ts +72 -0
  45. package/test/markdown.test.ts +2 -2
  46. package/test/markdown_code_block.test.ts +58 -0
  47. package/test/multi_input.test.ts +124 -0
  48. package/test/organize.test.ts +117 -1
  49. package/test/property_binding.test.ts +49 -0
  50. package/test/support.ts +175 -0
  51. package/test/theme.test.ts +86 -0
  52. package/test/widget_click.test.ts +111 -97
  53. package/test/widget_construction.test.ts +5 -4
  54. package/test/widgets.test.ts +333 -0
  55. package/test/wire_follows_camera.test.ts +57 -0
@@ -1,92 +1,81 @@
1
- import { Theme } from "../theme";
2
- import { Box, InBox } from "../types/box";
3
- import { CopyVector2, Vector2, Zero } from "../types/vector2";
4
- import { FlowNode } from '../node';
5
- import { Text } from "../types/text";
6
- import { TextStyle, TextStyleConfig, TextStyleFallback } from "../styles/text";
7
-
8
- export interface TextWidgetConfig {
9
- property?: string;
10
-
11
- value?: string;
12
-
13
- textBoxStyle?: TextStyleConfig;
14
-
15
- callback?: (newString: string) => void;
16
- }
17
-
18
- export class TextWidget {
19
-
20
- #value: Text;
21
-
22
- #callback?: (newString: string) => void;
23
-
24
- #node: FlowNode
25
-
26
- #nodeProperty: string | undefined;
27
-
28
- #size: Vector2;
29
-
30
- constructor(node: FlowNode, config?: TextWidgetConfig) {
31
- this.#size = Zero();
32
- this.#node = node;
33
- this.#nodeProperty = config?.property;
34
- this.#value = new Text("", TextStyleFallback(config?.textBoxStyle, {
35
- font: Theme.FontFamily,
36
- color: Theme.Widget.FontColor,
37
- size: Theme.Note.FontSize,
38
- }))
39
-
40
- this.Set(config?.value === undefined ? "" : config?.value);
41
- this.#callback = config?.callback;
42
- if (this.#nodeProperty !== undefined) {
43
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
44
- this.Set(newVal);
45
- });
46
- }
47
- }
48
-
49
- Size(): Vector2 {
50
- return this.#size;
51
- }
52
-
53
- Set(value: string): void {
54
- if (this.#value.get() === value) {
55
- return;
56
- }
57
- this.#value.set(value);
58
-
59
- if (this.#nodeProperty !== undefined) {
60
- this.#node.setProperty(this.#nodeProperty, value)
61
- }
62
-
63
- if (this.#callback !== undefined) {
64
- this.#callback(value);
65
- }
66
- }
67
-
68
- ClickStart(): void {
69
- // Left blank for implementing Widget interface
70
- }
71
-
72
- ClickEnd(): void {
73
- // Left blank for implementing Widget interface
74
- }
75
-
76
-
77
- Draw(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, mousePosition: Vector2 | undefined): Box {
78
- this.#value.size(ctx, scale, this.#size);
79
- const box = {
80
- Position: { x: 0, y: 0 },
81
- Size: this.#size
82
- };
83
- CopyVector2(box.Position, position);
84
- this.#value.size(ctx, 1, this.#size);
85
-
86
- ctx.textAlign = "left";
87
- this.#value.render(ctx, scale, box.Position);
88
- // this.#idleStyle.DrawUnderline(ctx, box, scale, fitString(ctx, this.#value, box.Size.x - (20 * scale)))
89
-
90
- return box;
91
- }
92
- }
1
+ import { Theme, themeVersion } from "../theme";
2
+ import { Box } from "../types/box";
3
+ import { Vector2, Zero } from "../types/vector2";
4
+ import { FlowNode } from '../node';
5
+ import { Text } from "../types/text";
6
+ import { TextStyleConfig, TextStyleFallback } from "../styles/text";
7
+ import { CursorStyle } from "../styles/cursor";
8
+ import { requestRender } from "../render_scheduler";
9
+ import { WidgetBase } from "./widget";
10
+
11
+ export interface TextWidgetConfig {
12
+ property?: string;
13
+ value?: string;
14
+ textBoxStyle?: TextStyleConfig;
15
+ }
16
+
17
+ export class TextWidget extends WidgetBase {
18
+
19
+ #value: Text;
20
+
21
+ #size: Vector2;
22
+
23
+ #styleConfig: TextStyleConfig | undefined;
24
+
25
+ #styledForTheme = -1;
26
+
27
+ #followTheme(): void {
28
+ if (this.#styledForTheme === themeVersion()) {
29
+ return;
30
+ }
31
+ this.#styledForTheme = themeVersion();
32
+ const style = this.#value.style();
33
+ style.setColor(this.#styleConfig?.color ?? Theme.Widget.FontColor);
34
+ style.setSize(this.#styleConfig?.size ?? Theme.Note.FontSize);
35
+ }
36
+
37
+ constructor(node: FlowNode, config?: TextWidgetConfig) {
38
+ super();
39
+ this.#size = Zero();
40
+ this.#styleConfig = config?.textBoxStyle;
41
+ this.#value = new Text("", TextStyleFallback(config?.textBoxStyle, {}));
42
+ this.#followTheme();
43
+ this.setText(config?.value ?? "");
44
+
45
+ const property = config?.property;
46
+ if (property !== undefined) {
47
+ const existing = node.getProperty(property);
48
+ if (existing !== undefined && existing !== null) {
49
+ this.setText(String(existing));
50
+ }
51
+ node.addPropertyChangeListener(property, (_, next) => this.setText(String(next)));
52
+ }
53
+ }
54
+
55
+ Cursor(): CursorStyle {
56
+ return CursorStyle.Default;
57
+ }
58
+
59
+ Size(): Vector2 {
60
+ return this.#size;
61
+ }
62
+
63
+ text(): string {
64
+ return this.#value.get();
65
+ }
66
+
67
+ setText(value: string): void {
68
+ if (this.#value.get() === value) {
69
+ return;
70
+ }
71
+ this.#value.set(value);
72
+ requestRender();
73
+ }
74
+
75
+ protected render(ctx: CanvasRenderingContext2D, box: Box, scale: number): void {
76
+ this.#followTheme();
77
+ this.#value.size(ctx, 1, this.#size);
78
+ ctx.textAlign = "left";
79
+ this.#value.render(ctx, scale, box.Position);
80
+ }
81
+ }
@@ -1,207 +1,62 @@
1
- import { Theme } from "../theme";
2
- import { Box, InBox } from "../types/box";
3
- import { CopyVector2, Vector2 } from "../types/vector2";
4
- import { height, startingWidgetValue, width } from "./widget";
5
- import { TextBoxStyle, TextBoxStyleConfig, TextBoxStyleWithFallback } from "../styles/textBox";
6
- import { FlowNode } from "../node";
7
-
8
- export interface ToggleStyleConfig {
9
- idle?: TextBoxStyleConfig,
10
- hover?: TextBoxStyleConfig,
11
- lightColor?: string;
12
- lightBorderColor?: string;
13
- lightBlur?: number;
14
- }
15
-
16
- export interface ToggleWidgetConfig {
17
- property?: string;
18
- value?: boolean;
19
- text?: string;
20
- callback?: () => void;
21
- enabledStyle?: ToggleStyleConfig;
22
- disabledStyle?: ToggleStyleConfig;
23
- }
24
-
25
- class ToggleStyle {
26
-
27
- #idleStyle: TextBoxStyle;
28
-
29
- #hoverStyle: TextBoxStyle;
30
-
31
- #lightColor: string;
32
-
33
- #lightBorderColor: string;
34
-
35
- #lightBlur?: number;
36
-
37
- #box: Box;
38
-
39
- constructor(config?: ToggleStyleConfig) {
40
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idle, {
41
- box: {
42
- color: Theme.Widget.BackgroundColor,
43
- border: {
44
- size: Theme.Widget.Border.Size,
45
- color: Theme.Widget.Border.Color,
46
- }
47
- },
48
- text: { color: Theme.Widget.FontColor },
49
- }));
50
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.hover, {
51
- box: {
52
- color: Theme.Widget.Hover.BackgroundColor,
53
- border: {
54
- size: Theme.Widget.Border.Size,
55
- color: Theme.Widget.Border.Color,
56
- }
57
- },
58
- text: { color: Theme.Widget.FontColor },
59
- }));
60
- this.#lightColor = config?.lightColor === undefined ? "#222222" : config.lightColor;
61
- this.#lightBorderColor = config?.lightBorderColor === undefined ? "black" : config.lightBorderColor;
62
- this.#lightBlur = config?.lightBlur;
63
-
64
- this.#box = {Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
65
- }
66
-
67
- Draw(ctx: CanvasRenderingContext2D, pos: Vector2, scale: number, text: string, mousePosition: Vector2 | undefined): Box {
68
- const scaledWidth = width * scale;
69
- const scaledHeight = height * scale;
70
- this.#box.Size.x = scaledWidth;
71
- this.#box.Size.y = scaledHeight;
72
- CopyVector2(this.#box.Position, pos);
73
-
74
- // Background
75
- let style: TextBoxStyle = this.#idleStyle;
76
- if (mousePosition !== undefined) {
77
- if (InBox(this.#box, mousePosition)) {
78
- style = this.#hoverStyle;
79
- }
80
- }
81
- style.Draw(ctx, this.#box, scale, text);
82
-
83
- // Light
84
- const lightScale = Math.min(scaledWidth, scaledHeight);
85
- ctx.strokeStyle = this.#lightBorderColor;
86
- ctx.fillStyle = this.#lightColor
87
- ctx.beginPath();
88
- ctx.roundRect(
89
- pos.x + (lightScale * .2),
90
- pos.y + (lightScale * .2),
91
- lightScale * .6,
92
- lightScale * .6,
93
- Theme.Widget.Border.Radius * scale
94
- );
95
- if (this.#lightBlur) {
96
- ctx.shadowBlur = this.#lightBlur * scale;
97
- ctx.shadowColor = this.#lightColor;
98
- }
99
- ctx.fill();
100
- if (this.#lightBlur) {
101
- ctx.shadowBlur = 0;
102
- }
103
- // ctx.stroke();
104
-
105
- return this.#box
106
- }
107
- }
108
-
109
- export class ToggleWidget {
110
-
111
- #node: FlowNode;
112
-
113
- #nodeProperty: string | undefined;
114
-
115
- #value: boolean;
116
-
117
- #text: string;
118
-
119
- #enabledStyle: ToggleStyle;
120
-
121
- #disabledStyle: ToggleStyle;
122
-
123
- #callback?: (value: boolean) => void;
124
-
125
- constructor(node: FlowNode, config?: ToggleWidgetConfig) {
126
- this.#value = false;
127
-
128
- this.#node = node;
129
- this.#nodeProperty = config?.property;
130
- this.#text = config?.text === undefined ? "Toggle" : config?.text;
131
-
132
- this.#enabledStyle = new ToggleStyle({
133
- idle: TextBoxStyleWithFallback(config?.enabledStyle?.idle, {
134
- box: {
135
- color: Theme.Widget.BackgroundColor,
136
- border: {
137
- size: Theme.Widget.Border.Size,
138
- color: Theme.Widget.Border.Color,
139
- }
140
- },
141
- text: { color: Theme.Widget.FontColor },
142
- }),
143
- lightBorderColor: config?.enabledStyle?.lightBorderColor,
144
- lightColor: config?.enabledStyle?.lightColor === undefined ? "#00FF00" : config?.enabledStyle?.lightColor,
145
- lightBlur: config?.enabledStyle?.lightBlur === undefined ? 15 : config?.enabledStyle?.lightBlur
146
- });
147
- this.#disabledStyle = new ToggleStyle({
148
- idle: TextBoxStyleWithFallback(config?.disabledStyle?.idle, {
149
- box: {
150
- color: Theme.Widget.BackgroundColor,
151
- border: {
152
- size: Theme.Widget.Border.Size,
153
- color: Theme.Widget.Border.Color,
154
- }
155
- },
156
- text: { color: Theme.Widget.FontColor },
157
- }),
158
- lightBorderColor: config?.disabledStyle?.lightBorderColor,
159
- lightColor: config?.disabledStyle?.lightColor === undefined ? "#004400" : config?.enabledStyle?.lightColor,
160
- });
161
-
162
- this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, false));
163
-
164
- this.#callback = config?.callback;
165
-
166
- if (this.#nodeProperty !== undefined) {
167
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
168
- this.Set(newVal);
169
- });
170
- }
171
- }
172
-
173
- Size(): Vector2 {
174
- return { "x": width, "y": height }
175
- }
176
-
177
- Draw(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, mousePosition: Vector2 | undefined): Box {
178
- let style = this.#value ? this.#enabledStyle : this.#disabledStyle;
179
- return style.Draw(ctx, position, scale, this.#text, mousePosition);
180
- }
181
-
182
- Toggle(): void {
183
- this.Set(!this.#value);
184
- }
185
-
186
- Set(value: boolean): void {
187
- if (this.#value === value) {
188
- return;
189
- }
190
- this.#value = value;
191
-
192
- if (this.#nodeProperty !== undefined) {
193
- this.#node.setProperty(this.#nodeProperty, this.#value);
194
- }
195
-
196
- if (this.#callback !== undefined) {
197
- this.#callback(this.#value);
198
- }
199
- }
200
-
201
- ClickStart(): void {
202
- this.Toggle();
203
- }
204
-
205
- ClickEnd(): void {
206
- }
207
- }
1
+ import { Theme } from "../theme";
2
+ import { Box } from "../types/box";
3
+ import { FlowNode } from "../node";
4
+ import { drawFieldBox, widgetFont } from "./field";
5
+ import { ValueWidget, ValueWidgetConfig } from "./value";
6
+ import { fitString } from "../utils/string";
7
+
8
+ export interface ToggleWidgetConfig extends ValueWidgetConfig<boolean> {
9
+ onColor?: string;
10
+ offColor?: string;
11
+ }
12
+
13
+ export class ToggleWidget extends ValueWidget<boolean> {
14
+
15
+ #onColor: string;
16
+
17
+ #offColor: string;
18
+
19
+ constructor(node: FlowNode, config?: ToggleWidgetConfig) {
20
+ super(node, { ...config, label: config?.label ?? "Toggle" }, false);
21
+ this.#onColor = config?.onColor ?? "#00FF00";
22
+ this.#offColor = config?.offColor ?? "#004400";
23
+ }
24
+
25
+ Toggle(): void {
26
+ const before = this.value();
27
+ this.commit(!before, before);
28
+ }
29
+
30
+ protected render(ctx: CanvasRenderingContext2D, box: Box, scale: number): void {
31
+ drawFieldBox(ctx, box, scale, { hovered: this.hovered(), value: "" });
32
+
33
+ const light = Math.min(box.Size.x, box.Size.y) * 0.6;
34
+ const inset = (box.Size.y - light) / 2;
35
+ const on = this.value();
36
+ ctx.fillStyle = on ? this.#onColor : this.#offColor;
37
+ ctx.beginPath();
38
+ ctx.roundRect(box.Position.x + inset, box.Position.y + inset, light, light, Theme.Widget.Border.Radius * scale);
39
+ if (on) {
40
+ ctx.shadowBlur = 15 * scale;
41
+ ctx.shadowColor = this.#onColor;
42
+ }
43
+ ctx.fill();
44
+ ctx.shadowBlur = 0;
45
+
46
+ const textLeft = box.Position.x + (inset * 2) + light;
47
+ ctx.font = widgetFont(scale);
48
+ ctx.textAlign = "center";
49
+ ctx.textBaseline = "middle";
50
+ ctx.fillStyle = Theme.Widget.FontColor;
51
+ const room = box.Position.x + box.Size.x - textLeft - inset;
52
+ ctx.fillText(
53
+ fitString(ctx, this.label(), room),
54
+ textLeft + (room / 2),
55
+ box.Position.y + (box.Size.y / 2),
56
+ );
57
+ }
58
+
59
+ protected onClick(): void {
60
+ this.Toggle();
61
+ }
62
+ }
@@ -0,0 +1,127 @@
1
+ import { FlowNode } from "../node";
2
+ import { requestRender } from "../render_scheduler";
3
+ import { Vector2 } from "../types/vector2";
4
+ import { height, startingWidgetValue, width, WidgetBase } from "./widget";
5
+
6
+ export interface ValueWidgetConfig<T> {
7
+ /**
8
+ * A node property the widget shows and edits. Setting the property from
9
+ * code updates the widget without counting as an edit.
10
+ */
11
+ property?: string;
12
+
13
+ value?: T;
14
+
15
+ label?: string;
16
+
17
+ /**
18
+ * The narrowest the widget gets, in graph units.
19
+ */
20
+ width?: number;
21
+
22
+ /**
23
+ * A person finished changing the value.
24
+ */
25
+ callback?: (value: T) => void;
26
+
27
+ /**
28
+ * The value moved while a person is still changing it.
29
+ */
30
+ onInput?: (value: T) => void;
31
+ }
32
+
33
+ export abstract class ValueWidget<T> extends WidgetBase {
34
+
35
+ #node: FlowNode;
36
+
37
+ #property: string | undefined;
38
+
39
+ #value: T;
40
+
41
+ #label: string;
42
+
43
+ #width: number;
44
+
45
+ #callback?: (value: T) => void;
46
+
47
+ #onInput?: (value: T) => void;
48
+
49
+ constructor(node: FlowNode, config: ValueWidgetConfig<T> | undefined, fallback: T) {
50
+ super();
51
+ this.#node = node;
52
+ this.#property = config?.property;
53
+ this.#label = config?.label ?? "";
54
+ this.#width = config?.width ?? width;
55
+ this.#value = startingWidgetValue(node, this.#property, config?.value, fallback);
56
+ this.#callback = config?.callback;
57
+ this.#onInput = config?.onInput;
58
+
59
+ if (this.#property !== undefined) {
60
+ node.setProperty(this.#property, this.#value);
61
+ node.addPropertyChangeListener(this.#property, (_, next) => this.setValue(next));
62
+ }
63
+ }
64
+
65
+ Size(): Vector2 {
66
+ return { x: this.#width, y: height };
67
+ }
68
+
69
+ value(): T {
70
+ return this.#value;
71
+ }
72
+
73
+ /**
74
+ * Shows a new value without treating it as a person's edit: no callback
75
+ * and no edit event.
76
+ */
77
+ setValue(value: T): void {
78
+ if (this.equal(value, this.#value)) {
79
+ return;
80
+ }
81
+ this.#value = value;
82
+ this.valueChanged();
83
+ if (this.#property !== undefined) {
84
+ this.#node.setProperty(this.#property, value);
85
+ }
86
+ requestRender();
87
+ }
88
+
89
+ label(): string {
90
+ return this.#label;
91
+ }
92
+
93
+ setLabel(label: string): void {
94
+ this.#label = label;
95
+ requestRender();
96
+ }
97
+
98
+ protected node(): FlowNode {
99
+ return this.#node;
100
+ }
101
+
102
+ protected input(value: T): void {
103
+ this.setValue(value);
104
+ this.#onInput?.(value);
105
+ }
106
+
107
+ /**
108
+ * Ends a person's edit. Nothing is reported when they ended where they
109
+ * started.
110
+ */
111
+ protected commit(value: T, startedAt: T): void {
112
+ this.setValue(value);
113
+ if (this.equal(value, startedAt)) {
114
+ return;
115
+ }
116
+ if (this.#property !== undefined) {
117
+ this.#node.editProperty(this.#property, value);
118
+ }
119
+ this.#callback?.(value);
120
+ }
121
+
122
+ protected valueChanged(): void { }
123
+
124
+ protected equal(a: T, b: T): boolean {
125
+ return Object.is(a, b);
126
+ }
127
+ }