@elicdavis/node-flow 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,40 +17,90 @@ export interface InPlaceEditConfig {
17
17
  onCommit: (text: string) => void;
18
18
  }
19
19
 
20
+ export interface InPlaceEdit {
21
+ input: HTMLInputElement;
22
+
23
+ /**
24
+ * Moves the input over where the widget was just drawn, in canvas pixels.
25
+ */
26
+ place(box: Box, scale: number): void;
27
+ }
28
+
29
+ interface OpenEdit {
30
+ canvas: HTMLCanvasElement;
31
+ input: HTMLInputElement;
32
+ placed: boolean;
33
+ }
34
+
35
+ const open = new Set<OpenEdit>();
36
+
37
+ export function placeEditsDuring(canvas: HTMLCanvasElement, draw: () => void): void {
38
+ open.forEach((edit) => {
39
+ if (edit.canvas === canvas) {
40
+ edit.placed = false;
41
+ }
42
+ });
43
+ draw();
44
+ open.forEach((edit) => {
45
+ if (edit.canvas === canvas && !edit.placed) {
46
+ // Not visibility or display: hiding either way blurs the input, which ends the edit.
47
+ edit.input.style.opacity = "0";
48
+ edit.input.style.pointerEvents = "none";
49
+ }
50
+ });
51
+ }
52
+
20
53
  /**
21
54
  * Lays a text input over a widget. Enter or clicking away keeps what was
22
55
  * typed; Escape throws it away.
23
56
  */
24
- export function editInPlace(config: InPlaceEditConfig): HTMLInputElement | null {
57
+ export function editInPlace(config: InPlaceEditConfig): InPlaceEdit | null {
25
58
  const canvas = config.canvas;
26
59
  if (canvas === null) {
27
60
  return null;
28
61
  }
29
62
 
30
- const rect = canvas.getBoundingClientRect();
31
- const sx = canvas.width > 0 ? rect.width / canvas.width : 1;
32
- const sy = canvas.height > 0 ? rect.height / canvas.height : 1;
33
-
34
63
  const input = document.createElement("input");
35
64
  input.type = "text";
36
65
  input.value = config.value;
37
- input.style.cssText = [
38
- "position: fixed",
39
- `left: ${rect.left + (config.box.Position.x * sx)}px`,
40
- `top: ${rect.top + (config.box.Position.y * sy)}px`,
41
- `width: ${config.box.Size.x * sx}px`,
42
- `height: ${config.box.Size.y * sy}px`,
43
- "box-sizing: border-box",
44
- "margin: 0",
45
- `padding: 0 ${Theme.Widget.Padding * config.scale}px`,
46
- `font: ${Theme.Widget.FontSize * config.scale * sy}px ${Theme.FontFamily}`,
47
- `color: ${Theme.Widget.FontColor}`,
48
- `background: ${Theme.Widget.Hover.BackgroundColor}`,
49
- `border: ${Theme.Widget.Border.Size * config.scale}px solid ${Theme.Widget.Border.Color}`,
50
- `border-radius: ${Theme.Widget.Border.Radius * config.scale}px`,
51
- "outline: none",
52
- "z-index: 1000",
53
- ].join(";");
66
+ const edit: OpenEdit = { canvas, input, placed: true };
67
+
68
+ const place = (box: Box, scale: number) => {
69
+ const rect = canvas.getBoundingClientRect();
70
+ const sx = canvas.width > 0 ? rect.width / canvas.width : 1;
71
+ const sy = canvas.height > 0 ? rect.height / canvas.height : 1;
72
+ const left = rect.left + (box.Position.x * sx);
73
+ const top = rect.top + (box.Position.y * sy);
74
+ const width = box.Size.x * sx;
75
+ const height = box.Size.y * sy;
76
+ const clip = [
77
+ Math.max(0, rect.top - top),
78
+ Math.max(0, (left + width) - (rect.left + rect.width)),
79
+ Math.max(0, (top + height) - (rect.top + rect.height)),
80
+ Math.max(0, rect.left - left),
81
+ ];
82
+
83
+ edit.placed = true;
84
+ input.style.cssText = [
85
+ "position: fixed",
86
+ `left: ${left}px`,
87
+ `top: ${top}px`,
88
+ `width: ${width}px`,
89
+ `height: ${height}px`,
90
+ `clip-path: inset(${clip.map((c) => `${c}px`).join(" ")})`,
91
+ "box-sizing: border-box",
92
+ "margin: 0",
93
+ `padding: 0 ${Theme.Widget.Padding * scale}px`,
94
+ `font: ${Theme.Widget.FontSize * scale * sy}px ${Theme.FontFamily}`,
95
+ `color: ${Theme.Widget.FontColor}`,
96
+ `background: ${Theme.Widget.Hover.BackgroundColor}`,
97
+ `border: ${Theme.Widget.Border.Size * scale}px solid ${Theme.Widget.Border.Color}`,
98
+ `border-radius: ${Theme.Widget.Border.Radius * scale}px`,
99
+ "outline: none",
100
+ "z-index: 1000",
101
+ ].join(";");
102
+ };
103
+ place(config.box, config.scale);
54
104
 
55
105
  let done = false;
56
106
  const finish = (keep: boolean) => {
@@ -58,6 +108,7 @@ export function editInPlace(config: InPlaceEditConfig): HTMLInputElement | null
58
108
  return;
59
109
  }
60
110
  done = true;
111
+ open.delete(edit);
61
112
  input.remove();
62
113
  if (keep) {
63
114
  config.onCommit(input.value);
@@ -77,8 +128,9 @@ export function editInPlace(config: InPlaceEditConfig): HTMLInputElement | null
77
128
  });
78
129
  input.addEventListener("blur", () => finish(true));
79
130
 
131
+ open.add(edit);
80
132
  document.body.appendChild(input);
81
133
  input.focus();
82
134
  input.select();
83
- return input;
135
+ return { input, place };
84
136
  }
@@ -2,7 +2,6 @@ import { FlowNode } from "../node";
2
2
  import { CursorStyle } from "../styles/cursor";
3
3
  import { Box } from "../types/box";
4
4
  import { drawField } from "./field";
5
- import { editInPlace } from "./inplace";
6
5
  import { ValueWidget, ValueWidgetConfig } from "./value";
7
6
  import { WidgetPointer } from "./widget";
8
7
 
@@ -90,17 +89,11 @@ export class NumberWidget extends ValueWidget<number> {
90
89
  return;
91
90
  }
92
91
  const before = this.value();
93
- editInPlace({
94
- canvas: this.canvas(),
95
- box: this.box(),
96
- scale: this.scale(),
97
- value: String(before),
98
- onCommit: (text) => {
99
- const parsed = Number.parseFloat(text);
100
- if (Number.isFinite(parsed)) {
101
- this.commit(this.#clean(parsed), before);
102
- }
103
- },
92
+ this.editText(String(before), (text) => {
93
+ const parsed = Number.parseFloat(text);
94
+ if (Number.isFinite(parsed)) {
95
+ this.commit(this.#clean(parsed), before);
96
+ }
104
97
  });
105
98
  }
106
99
 
@@ -2,7 +2,6 @@ import { FlowNode } from "../node";
2
2
  import { CursorStyle } from "../styles/cursor";
3
3
  import { Box } from "../types/box";
4
4
  import { drawField } from "./field";
5
- import { editInPlace } from "./inplace";
6
5
  import { ValueWidget, ValueWidgetConfig } from "./value";
7
6
 
8
7
  export interface StringWidgetConfig extends ValueWidgetConfig<string> { }
@@ -28,12 +27,6 @@ export class StringWidget extends ValueWidget<string> {
28
27
 
29
28
  protected onClick(): void {
30
29
  const before = this.value();
31
- editInPlace({
32
- canvas: this.canvas(),
33
- box: this.box(),
34
- scale: this.scale(),
35
- value: before,
36
- onCommit: (text) => this.commit(text, before),
37
- });
30
+ this.editText(before, (text) => this.commit(text, before));
38
31
  }
39
32
  }
@@ -1,180 +1,201 @@
1
- import { Box, InBox } from '../types/box';
2
- import { Vector2 } from '../types/vector2';
3
- import { CursorStyle } from '../styles/cursor';
4
- import { requestRender } from '../render_scheduler';
5
-
6
- export const width = 150;
7
- export const height = 25;
8
-
9
- /**
10
- * Where the pointer is, in canvas pixels, and whether it is over the widget.
11
- * A press that starts on a widget keeps reporting to it until it is let go,
12
- * wherever the pointer wanders.
13
- */
14
- export interface WidgetPointer {
15
- position: Vector2;
16
- inside: boolean;
17
- }
18
-
19
- export interface Widget {
20
- /**
21
- * The smallest room the widget needs, in graph units. The node may hand
22
- * it a wider box.
23
- */
24
- Size(): Vector2;
25
-
26
- Draw(ctx: CanvasRenderingContext2D, box: Box, scale: number, mousePosition: Vector2 | undefined): void;
27
-
28
- PointerDown?(pointer: WidgetPointer): void;
29
- PointerMove?(pointer: WidgetPointer): void;
30
- PointerUp?(pointer: WidgetPointer): void;
31
-
32
- Cursor?(): CursorStyle;
33
- Hidden?(): boolean;
34
- Disabled?(): boolean;
35
- }
36
-
37
- export abstract class WidgetBase implements Widget {
38
-
39
- #hidden = false;
40
-
41
- #disabled = false;
42
-
43
- #hovered = false;
44
-
45
- #pressed = false;
46
-
47
- #box: Box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
48
-
49
- #canvas: HTMLCanvasElement | null = null;
50
-
51
- #scale = 1;
52
-
53
- abstract Size(): Vector2;
54
-
55
- protected abstract render(ctx: CanvasRenderingContext2D, box: Box, scale: number): void;
56
-
57
- Draw(ctx: CanvasRenderingContext2D, box: Box, scale: number, mousePosition: Vector2 | undefined): void {
58
- this.#box = { Position: { x: box.Position.x, y: box.Position.y }, Size: { x: box.Size.x, y: box.Size.y } };
59
- this.#canvas = ctx.canvas ?? null;
60
- this.#scale = scale;
61
- this.#hovered = !this.#disabled && mousePosition !== undefined && InBox(box, mousePosition);
62
-
63
- ctx.save();
64
- if (this.#disabled) {
65
- ctx.globalAlpha *= 0.4;
66
- }
67
- this.render(ctx, box, scale);
68
- ctx.restore();
69
- }
70
-
71
- PointerDown(pointer: WidgetPointer): void {
72
- if (this.#disabled) {
73
- return;
74
- }
75
- this.#pressed = true;
76
- this.onPointerDown(pointer);
77
- requestRender();
78
- }
79
-
80
- PointerMove(pointer: WidgetPointer): void {
81
- if (!this.#pressed) {
82
- return;
83
- }
84
- this.onPointerMove(pointer);
85
- requestRender();
86
- }
87
-
88
- PointerUp(pointer: WidgetPointer): void {
89
- if (!this.#pressed) {
90
- return;
91
- }
92
- this.#pressed = false;
93
- this.onPointerUp(pointer);
94
- if (pointer.inside) {
95
- this.onClick(pointer);
96
- }
97
- requestRender();
98
- }
99
-
100
- Cursor(): CursorStyle {
101
- return CursorStyle.Pointer;
102
- }
103
-
104
- Hidden(): boolean {
105
- return this.#hidden;
106
- }
107
-
108
- setHidden(hidden: boolean): void {
109
- this.#hidden = hidden;
110
- requestRender();
111
- }
112
-
113
- Disabled(): boolean {
114
- return this.#disabled;
115
- }
116
-
117
- setDisabled(disabled: boolean): void {
118
- this.#disabled = disabled;
119
- if (disabled) {
120
- this.#pressed = false;
121
- }
122
- requestRender();
123
- }
124
-
125
- protected hovered(): boolean {
126
- return this.#hovered;
127
- }
128
-
129
- protected pressed(): boolean {
130
- return this.#pressed;
131
- }
132
-
133
- /**
134
- * Where the widget was last drawn, in canvas pixels.
135
- */
136
- protected box(): Box {
137
- return this.#box;
138
- }
139
-
140
- protected canvas(): HTMLCanvasElement | null {
141
- return this.#canvas;
142
- }
143
-
144
- protected scale(): number {
145
- return this.#scale;
146
- }
147
-
148
- protected onPointerDown(pointer: WidgetPointer): void { }
149
-
150
- protected onPointerMove(pointer: WidgetPointer): void { }
151
-
152
- protected onPointerUp(pointer: WidgetPointer): void { }
153
-
154
- /**
155
- * A press and release that both landed on the widget.
156
- */
157
- protected onClick(pointer: WidgetPointer): void { }
158
- }
159
-
160
- interface PropertyReader {
161
- getProperty(name: string): any;
162
- }
163
-
164
- export function startingWidgetValue<T>(
165
- node: PropertyReader,
166
- property: string | undefined,
167
- configured: T | undefined,
168
- fallback: T,
169
- ): T {
170
- if (configured !== undefined) {
171
- return configured;
172
- }
173
- if (property !== undefined && property !== null) {
174
- const existing = node.getProperty(property);
175
- if (existing !== undefined && existing !== null) {
176
- return existing as T;
177
- }
178
- }
179
- return fallback;
180
- }
1
+ import { Box, InBox } from '../types/box';
2
+ import { Vector2 } from '../types/vector2';
3
+ import { CursorStyle } from '../styles/cursor';
4
+ import { requestRender } from '../render_scheduler';
5
+ import { editInPlace, InPlaceEdit } from './inplace';
6
+
7
+ export const width = 150;
8
+ export const height = 25;
9
+
10
+ /**
11
+ * Where the pointer is, in canvas pixels, and whether it is over the widget.
12
+ * A press that starts on a widget keeps reporting to it until it is let go,
13
+ * wherever the pointer wanders.
14
+ */
15
+ export interface WidgetPointer {
16
+ position: Vector2;
17
+ inside: boolean;
18
+ }
19
+
20
+ export interface Widget {
21
+ /**
22
+ * The smallest room the widget needs, in graph units. The node may hand
23
+ * it a wider box.
24
+ */
25
+ Size(): Vector2;
26
+
27
+ Draw(ctx: CanvasRenderingContext2D, box: Box, scale: number, mousePosition: Vector2 | undefined): void;
28
+
29
+ PointerDown?(pointer: WidgetPointer): void;
30
+ PointerMove?(pointer: WidgetPointer): void;
31
+ PointerUp?(pointer: WidgetPointer): void;
32
+
33
+ Cursor?(): CursorStyle;
34
+ Hidden?(): boolean;
35
+ Disabled?(): boolean;
36
+ }
37
+
38
+ export abstract class WidgetBase implements Widget {
39
+
40
+ #hidden = false;
41
+
42
+ #disabled = false;
43
+
44
+ #hovered = false;
45
+
46
+ #pressed = false;
47
+
48
+ #box: Box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
49
+
50
+ #canvas: HTMLCanvasElement | null = null;
51
+
52
+ #scale = 1;
53
+
54
+ #editing: InPlaceEdit | null = null;
55
+
56
+ abstract Size(): Vector2;
57
+
58
+ protected abstract render(ctx: CanvasRenderingContext2D, box: Box, scale: number): void;
59
+
60
+ Draw(ctx: CanvasRenderingContext2D, box: Box, scale: number, mousePosition: Vector2 | undefined): void {
61
+ this.#box = { Position: { x: box.Position.x, y: box.Position.y }, Size: { x: box.Size.x, y: box.Size.y } };
62
+ this.#canvas = ctx.canvas ?? null;
63
+ this.#scale = scale;
64
+ this.#hovered = !this.#disabled && mousePosition !== undefined && InBox(box, mousePosition);
65
+
66
+ if (this.#editing !== null) {
67
+ if (this.#editing.input.isConnected) {
68
+ this.#editing.place(this.#box, scale);
69
+ } else {
70
+ this.#editing = null;
71
+ }
72
+ }
73
+
74
+ ctx.save();
75
+ if (this.#disabled) {
76
+ ctx.globalAlpha *= 0.4;
77
+ }
78
+ this.render(ctx, box, scale);
79
+ ctx.restore();
80
+ }
81
+
82
+ PointerDown(pointer: WidgetPointer): void {
83
+ if (this.#disabled) {
84
+ return;
85
+ }
86
+ this.#pressed = true;
87
+ this.onPointerDown(pointer);
88
+ requestRender();
89
+ }
90
+
91
+ PointerMove(pointer: WidgetPointer): void {
92
+ if (!this.#pressed) {
93
+ return;
94
+ }
95
+ this.onPointerMove(pointer);
96
+ requestRender();
97
+ }
98
+
99
+ PointerUp(pointer: WidgetPointer): void {
100
+ if (!this.#pressed) {
101
+ return;
102
+ }
103
+ this.#pressed = false;
104
+ this.onPointerUp(pointer);
105
+ if (pointer.inside) {
106
+ this.onClick(pointer);
107
+ }
108
+ requestRender();
109
+ }
110
+
111
+ Cursor(): CursorStyle {
112
+ return CursorStyle.Pointer;
113
+ }
114
+
115
+ Hidden(): boolean {
116
+ return this.#hidden;
117
+ }
118
+
119
+ setHidden(hidden: boolean): void {
120
+ this.#hidden = hidden;
121
+ requestRender();
122
+ }
123
+
124
+ Disabled(): boolean {
125
+ return this.#disabled;
126
+ }
127
+
128
+ setDisabled(disabled: boolean): void {
129
+ this.#disabled = disabled;
130
+ if (disabled) {
131
+ this.#pressed = false;
132
+ }
133
+ requestRender();
134
+ }
135
+
136
+ protected hovered(): boolean {
137
+ return this.#hovered;
138
+ }
139
+
140
+ protected pressed(): boolean {
141
+ return this.#pressed;
142
+ }
143
+
144
+ /**
145
+ * Where the widget was last drawn, in canvas pixels.
146
+ */
147
+ protected box(): Box {
148
+ return this.#box;
149
+ }
150
+
151
+ protected canvas(): HTMLCanvasElement | null {
152
+ return this.#canvas;
153
+ }
154
+
155
+ protected scale(): number {
156
+ return this.#scale;
157
+ }
158
+
159
+ protected editText(value: string, onCommit: (text: string) => void): void {
160
+ this.#editing = editInPlace({
161
+ canvas: this.#canvas,
162
+ box: this.#box,
163
+ scale: this.#scale,
164
+ value,
165
+ onCommit,
166
+ });
167
+ }
168
+
169
+ protected onPointerDown(pointer: WidgetPointer): void { }
170
+
171
+ protected onPointerMove(pointer: WidgetPointer): void { }
172
+
173
+ protected onPointerUp(pointer: WidgetPointer): void { }
174
+
175
+ /**
176
+ * A press and release that both landed on the widget.
177
+ */
178
+ protected onClick(pointer: WidgetPointer): void { }
179
+ }
180
+
181
+ interface PropertyReader {
182
+ getProperty(name: string): any;
183
+ }
184
+
185
+ export function startingWidgetValue<T>(
186
+ node: PropertyReader,
187
+ property: string | undefined,
188
+ configured: T | undefined,
189
+ fallback: T,
190
+ ): T {
191
+ if (configured !== undefined) {
192
+ return configured;
193
+ }
194
+ if (property !== undefined && property !== null) {
195
+ const existing = node.getProperty(property);
196
+ if (existing !== undefined && existing !== null) {
197
+ return existing as T;
198
+ }
199
+ }
200
+ return fallback;
201
+ }
@@ -50,13 +50,14 @@ function scene(hidden: boolean) {
50
50
 
51
51
  describe('a hidden port', () => {
52
52
  it('takes no room on its node', () => {
53
- const shown = scene(false);
54
- const hidden = scene(true);
53
+ const one = scene(true);
54
+ const two = scene(true);
55
+ two.node.addInput({ name: 'Iridescence', type: 'iridescence', hidden: true });
55
56
 
56
- const shownHeight = shown.node.calculateBounds(shown.ctx, shown.camera).Size.y;
57
- const hiddenHeight = hidden.node.calculateBounds(hidden.ctx, hidden.camera).Size.y;
57
+ const oneHeight = one.node.calculateBounds(one.ctx, one.camera).Size.y;
58
+ const twoHeight = two.node.calculateBounds(two.ctx, two.camera).Size.y;
58
59
 
59
- assert.ok(hiddenHeight < shownHeight);
60
+ assert.equal(twoHeight, oneHeight);
60
61
  });
61
62
 
62
63
  it('cannot be hovered where it would have been drawn', () => {