@elicdavis/node-flow 0.2.0 → 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.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@elicdavis/node-flow",
3
3
  "description": "build node graphs",
4
4
  "author": "Eli C Davis",
5
- "version": "0.2.0",
5
+ "version": "0.2.1",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
8
  "nodes",
package/src/graph.ts CHANGED
@@ -19,6 +19,7 @@ export { CameraOrientation };
19
19
  import { PassSubsystem } from './pass/subsystem';
20
20
  import { QuickMenu } from './quickMenu';
21
21
  import { onRenderRequested, renderContinuously, requestRender } from './render_scheduler';
22
+ import { placeEditsDuring } from './widgets/inplace';
22
23
  import { Minimap, MinimapConfig } from './minimap';
23
24
 
24
25
  export type GraphRenderer = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, position: Vector2, scale: number) => void;
@@ -761,10 +762,12 @@ export class NodeFlowGraph {
761
762
  TimeExecution("Render_Background", this.#renderBackground.bind(this));
762
763
 
763
764
  TimeExecution("Render_View_" + this.#currentView, () => {
764
- let results = this.currentView().render(this.#ctx, this.#camera, this.#mousePosition);
765
- if (results?.cursorStyle) {
766
- this.#cursor = results?.cursorStyle;
767
- }
765
+ placeEditsDuring(this.#canvas, () => {
766
+ let results = this.currentView().render(this.#ctx, this.#camera, this.#mousePosition);
767
+ if (results?.cursorStyle) {
768
+ this.#cursor = results?.cursorStyle;
769
+ }
770
+ });
768
771
  });
769
772
 
770
773
  TimeExecution("Render_Minimap", () => {
package/src/index.ts CHANGED
@@ -66,7 +66,7 @@ import { SliderWidget, SliderWidgetConfig } from './widgets/slider';
66
66
  import { ImageWidget, ImageWidgetConfig } from './widgets/image';
67
67
  import { RowWidget, RowWidgetConfig } from './widgets/row';
68
68
  import { drawField, drawFieldBox, drawFieldText, FieldLook, widgetFont } from './widgets/field';
69
- import { editInPlace, InPlaceEditConfig } from './widgets/inplace';
69
+ import { editInPlace, InPlaceEdit, InPlaceEditConfig } from './widgets/inplace';
70
70
  export {
71
71
  WidgetBase, ValueWidget,
72
72
  NumberWidget, ColorWidget, StringWidget, TextWidget,
@@ -74,7 +74,7 @@ export {
74
74
  drawField, drawFieldBox, drawFieldText, widgetFont, editInPlace, formatNumber,
75
75
  };
76
76
  export type {
77
- Widget, WidgetPointer, ValueWidgetConfig, FieldLook, InPlaceEditConfig,
77
+ Widget, WidgetPointer, ValueWidgetConfig, FieldLook, InPlaceEdit, InPlaceEditConfig,
78
78
  NumberWidgetConfig, ColorWidgetConfig, StringWidgetConfig, TextWidgetConfig,
79
79
  ButtonWidgetConfig, ToggleWidgetConfig, SliderWidgetConfig,
80
80
  ImageWidgetConfig, RowWidgetConfig,
@@ -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
+ }
package/test/support.ts CHANGED
@@ -94,7 +94,7 @@ export interface StubElement {
94
94
  tagName: string;
95
95
  type: string;
96
96
  value: string;
97
- style: { cssText: string };
97
+ style: { cssText: string, opacity?: string, pointerEvents?: string };
98
98
  children: Array<StubElement>;
99
99
  isConnected: boolean;
100
100
  clicked: number;
@@ -115,6 +115,19 @@ export interface StubElement {
115
115
  fire(name: string, event?: any): void;
116
116
  }
117
117
 
118
+ function stubStyle(): StubElement['style'] {
119
+ const style: any = {};
120
+ let css = '';
121
+ Object.defineProperty(style, 'cssText', {
122
+ get: () => css,
123
+ set: (value: string) => {
124
+ css = value;
125
+ Object.keys(style).forEach((key) => delete style[key]);
126
+ },
127
+ });
128
+ return style;
129
+ }
130
+
118
131
  export function withDocument(run: (body: StubElement, created: Array<StubElement>) => void): void {
119
132
  const created: Array<StubElement> = [];
120
133
  const element = (tagName: string): StubElement => {
@@ -123,7 +136,7 @@ export function withDocument(run: (body: StubElement, created: Array<StubElement
123
136
  tagName,
124
137
  type: '',
125
138
  value: '',
126
- style: { cssText: '' },
139
+ style: stubStyle(),
127
140
  children: [],
128
141
  isConnected: false,
129
142
  clicked: 0,