@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,107 +1,52 @@
1
- import { Theme } from "../theme";
2
- import { TextBoxStyle, TextBoxStyleConfig, TextBoxStyleWithFallback } from "../styles/textBox";
3
- import { Box, InBox } from "../types/box";
4
- import { CopyVector2, Vector2 } from "../types/vector2";
5
- import { height, width } from "./widget";
6
-
7
-
8
- export interface ButtonWidgetConfig {
9
- text?: string;
10
- callback?: () => void;
11
- idleStyle?: TextBoxStyleConfig;
12
- hoverStyle?: TextBoxStyleConfig;
13
- clickStyle?: TextBoxStyleConfig;
14
- }
15
-
16
- export class ButtonWidget {
17
-
18
- #text: string;
19
-
20
- #idleStyle: TextBoxStyle;
21
-
22
- #hoverStyle: TextBoxStyle;
23
-
24
- #clickStyle: TextBoxStyle;
25
-
26
- #callback?: () => void;
27
-
28
- #gettingClicked: boolean;
29
-
30
- constructor(config?: ButtonWidgetConfig) {
31
- this.#text = config?.text === undefined ? "Button" : config?.text;
32
- this.#callback = config?.callback
33
- this.#gettingClicked = false;
34
-
35
- this.#idleStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.idleStyle, {
36
- box: {
37
- color: Theme.Widget.BackgroundColor,
38
- border: {
39
- size: Theme.Widget.Border.Size,
40
- color: Theme.Widget.Border.Color,
41
- }
42
- },
43
- text: { color: Theme.Widget.FontColor },
44
- }));
45
- this.#hoverStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.hoverStyle, {
46
- box: {
47
- color: Theme.Widget.Hover.BackgroundColor,
48
- border: {
49
- size: Theme.Widget.Border.Size,
50
- color: Theme.Widget.Border.Color,
51
- }
52
- },
53
- text: { color: Theme.Widget.FontColor },
54
- }));
55
- this.#clickStyle = new TextBoxStyle(TextBoxStyleWithFallback(config?.clickStyle, {
56
- box: {
57
- color: Theme.Widget.Button.Click.BackgroundColor,
58
- border: {
59
- size: Theme.Widget.Border.Size,
60
- color: Theme.Widget.Border.Color,
61
- }
62
- },
63
- text: { color: Theme.Widget.FontColor },
64
- }))
65
- }
66
-
67
- Size(): Vector2 {
68
- return { "x": width, "y": height }
69
- }
70
-
71
- Draw(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, mousePosition: Vector2 | undefined): Box {
72
- const box = {
73
- Position: { x: 0, y: 0 },
74
- Size: {
75
- x: width * scale,
76
- y: height * scale
77
- }
78
- };
79
- CopyVector2(box.Position, position);
80
-
81
- let style: TextBoxStyle = this.#idleStyle;
82
-
83
- if (mousePosition !== undefined && !this.#gettingClicked) {
84
- if (InBox(box, mousePosition)) {
85
- style = this.#hoverStyle;
86
- }
87
- }
88
-
89
- if (this.#gettingClicked) {
90
- style = this.#clickStyle;
91
- }
92
- style.Draw(ctx, box, scale, this.#text);
93
-
94
- return box
95
- }
96
-
97
- ClickStart(): void {
98
- this.#gettingClicked = true;
99
- }
100
-
101
- ClickEnd(): void {
102
- this.#gettingClicked = false;
103
- if (this.#callback !== undefined) {
104
- this.#callback();
105
- }
106
- }
107
- }
1
+ import { requestRender } from "../render_scheduler";
2
+ import { Box } from "../types/box";
3
+ import { Vector2 } from "../types/vector2";
4
+ import { drawField } from "./field";
5
+ import { height, width, WidgetBase } from "./widget";
6
+
7
+ export interface ButtonWidgetConfig {
8
+ text?: string;
9
+ callback?: () => void;
10
+ width?: number;
11
+ }
12
+
13
+ export class ButtonWidget extends WidgetBase {
14
+
15
+ #text: string;
16
+
17
+ #callback?: () => void;
18
+
19
+ #width: number;
20
+
21
+ constructor(config?: ButtonWidgetConfig) {
22
+ super();
23
+ this.#text = config?.text ?? "Button";
24
+ this.#callback = config?.callback;
25
+ this.#width = config?.width ?? width;
26
+ }
27
+
28
+ Size(): Vector2 {
29
+ return { x: this.#width, y: height };
30
+ }
31
+
32
+ text(): string {
33
+ return this.#text;
34
+ }
35
+
36
+ setText(text: string): void {
37
+ this.#text = text;
38
+ requestRender();
39
+ }
40
+
41
+ protected render(ctx: CanvasRenderingContext2D, box: Box, scale: number): void {
42
+ drawField(ctx, box, scale, {
43
+ hovered: this.hovered(),
44
+ pressed: this.pressed(),
45
+ value: this.#text,
46
+ });
47
+ }
48
+
49
+ protected onClick(): void {
50
+ this.#callback?.();
51
+ }
52
+ }
@@ -1,150 +1,91 @@
1
- import { HexToColor } from "../utils/color";
2
- import { TextStyleConfig } from "../styles/text";
3
- import { Box } from "../types/box";
4
- import { CopyVector2, Vector2 } from "../types/vector2";
5
- import { height, startingWidgetValue, width } from "./widget";
6
- import { Popup } from "../popup";
7
- import { TextBoxStyle } from '../styles/textBox';
8
- import { Theme } from "../theme";
9
- import { FlowNode } from "../node";
10
-
11
- export interface ColorWidgetConfig {
12
- value?: string;
13
-
14
- property?: string;
15
-
16
- textStyle?: TextStyleConfig;
17
-
18
- callback?: (newColor: string) => void;
19
- }
20
-
21
- function contrastColor(color: string): string {
22
- const c = HexToColor(color);
23
- if (c === null) {
24
- return "black";
25
- }
26
-
27
- if ((c.r + c.g + c.b) / 3 > 0.5) {
28
- return "black"
29
- }
30
-
31
- return "white";
32
- }
33
-
34
- export class ColorWidget {
35
-
36
- #node: FlowNode;
37
-
38
- #nodeProperty: string | undefined;
39
-
40
- #value: string;
41
-
42
- #contrast: string;
43
-
44
- #textBoxStyle: TextBoxStyle;
45
-
46
- #callback?: (newColor: string) => void;
47
-
48
- constructor(node: FlowNode, config?: ColorWidgetConfig) {
49
- this.#node = node;
50
- this.#nodeProperty = config?.property;
51
- this.#callback = config?.callback;
52
-
53
- this.#value = startingWidgetValue(node, this.#nodeProperty, config?.value, "#000000");
54
- this.#contrast = contrastColor(this.#value);
55
- this.#textBoxStyle = new TextBoxStyle({
56
- box: {
57
- color: this.#value,
58
- border: {
59
- color: this.#contrast,
60
- size: Theme.Widget.Border.Size
61
- }
62
- },
63
- text: config?.textStyle
64
- });
65
-
66
- if (this.#nodeProperty !== undefined) {
67
- this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
68
- this.Set(newVal);
69
- });
70
- }
71
- }
72
-
73
- Size(): Vector2 {
74
- return { "x": width, "y": height }
75
- }
76
-
77
- Set(value: string): void {
78
- if (this.#value === value) {
79
- return;
80
- }
81
-
82
- this.#value = value;
83
-
84
- if (this.#callback !== undefined) {
85
- this.#callback(this.#value);
86
- }
87
-
88
- if (this.#nodeProperty !== undefined) {
89
- this.#node.setProperty(this.#nodeProperty, this.#value);
90
- }
91
-
92
- // Update Styling
93
- this.#contrast = contrastColor(this.#value);
94
- this.#textBoxStyle.setBoxColor(this.#value);
95
- this.#textBoxStyle.setBorderColor(this.#contrast);
96
- this.#textBoxStyle.setTextColor(this.#contrast);
97
- }
98
-
99
- ClickStart(): void {
100
- }
101
-
102
- ClickEnd(): void {
103
- let input: HTMLInputElement | null = null;
104
-
105
- const popup = new Popup({
106
- title: "Set Color",
107
- options: ["Set", "Cancel"],
108
- content: () => {
109
- const container = document.createElement('div');
110
-
111
- input = document.createElement('input')
112
- input.type = "color";
113
- input.value = this.#value;
114
- input.name = "color"
115
- container.append(input);
116
-
117
- const label = document.createElement("label");
118
- label.htmlFor = "color";
119
- label.textContent = "Color";
120
- container.append(label);
121
-
122
- return container;
123
- },
124
- onClose: (button: string | null): void => {
125
- if (button !== "Set" || input === null) {
126
- return;
127
- }
128
-
129
- this.Set(input.value);
130
- },
131
- });
132
-
133
- popup.Show();
134
- }
135
-
136
- Draw(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, mousePosition: Vector2 | undefined): Box {
137
- const box = {
138
- Position: { x: 0, y: 0 },
139
- Size: {
140
- x: width * scale,
141
- y: height * scale
142
- }
143
- };
144
- CopyVector2(box.Position, position);
145
-
146
- this.#textBoxStyle.Draw(ctx, box, scale, this.#value)
147
-
148
- return box;
149
- }
150
- }
1
+ import { HexToColor } from "../utils/color";
2
+ import { Box } from "../types/box";
3
+ import { drawField } from "./field";
4
+ import { FlowNode } from "../node";
5
+ import { ValueWidget, ValueWidgetConfig } from "./value";
6
+
7
+ export interface ColorWidgetConfig extends ValueWidgetConfig<string> { }
8
+
9
+ function contrastColor(color: string): string {
10
+ const c = HexToColor(color);
11
+ if (c === null) {
12
+ return "black";
13
+ }
14
+
15
+ if ((c.r + c.g + c.b) / 3 > 0.5) {
16
+ return "black"
17
+ }
18
+
19
+ return "white";
20
+ }
21
+
22
+ let picker: HTMLInputElement | null = null;
23
+
24
+ function sharedPicker(): HTMLInputElement {
25
+ if (picker === null || !picker.isConnected) {
26
+ picker = document.createElement("input");
27
+ picker.type = "color";
28
+ document.body.appendChild(picker);
29
+ }
30
+ return picker;
31
+ }
32
+
33
+ export class ColorWidget extends ValueWidget<string> {
34
+
35
+ #contrast: string;
36
+
37
+ constructor(node: FlowNode, config?: ColorWidgetConfig) {
38
+ super(node, config, "#000000");
39
+ this.#contrast = contrastColor(this.value());
40
+ }
41
+
42
+ protected valueChanged(): void {
43
+ this.#contrast = contrastColor(this.value());
44
+ }
45
+
46
+ protected render(ctx: CanvasRenderingContext2D, box: Box, scale: number): void {
47
+ drawField(ctx, box, scale, {
48
+ input: true,
49
+ hovered: this.hovered(),
50
+ label: this.label(),
51
+ value: this.value(),
52
+ background: this.value(),
53
+ border: this.#contrast,
54
+ textColor: this.#contrast,
55
+ });
56
+ }
57
+
58
+ protected onClick(): void {
59
+ const canvas = this.canvas();
60
+ if (canvas === null) {
61
+ return;
62
+ }
63
+
64
+ const before = this.value();
65
+ // The browser's picker has no alpha, so the old alpha is carried over.
66
+ const alpha = before.slice(7);
67
+ const box = this.box();
68
+ const rect = canvas.getBoundingClientRect();
69
+ const sx = canvas.width > 0 ? rect.width / canvas.width : 1;
70
+ const sy = canvas.height > 0 ? rect.height / canvas.height : 1;
71
+
72
+ const input = sharedPicker();
73
+ input.style.cssText = [
74
+ "position: fixed",
75
+ `left: ${rect.left + (box.Position.x * sx)}px`,
76
+ `top: ${rect.top + (box.Position.y * sy)}px`,
77
+ `width: ${box.Size.x * sx}px`,
78
+ `height: ${box.Size.y * sy}px`,
79
+ "opacity: 0",
80
+ "pointer-events: none",
81
+ "border: 0",
82
+ "padding: 0",
83
+ ].join(";");
84
+ input.value = before.slice(0, 7);
85
+ input.oninput = () => this.input(input.value + alpha);
86
+ input.onchange = () => this.commit(input.value + alpha, before);
87
+ // Reading the rect lays the input out first; the picker opens at its last laid-out spot.
88
+ input.getBoundingClientRect();
89
+ input.click();
90
+ }
91
+ }
@@ -1,46 +1,89 @@
1
- import { Widget } from "./widget";
2
- import { NumberWidget, NumberWidgetConfig } from './number';
3
- import { ButtonWidget, ButtonWidgetConfig } from './button';
4
- import { ColorWidget, ColorWidgetConfig } from './color';
5
- import { SliderWidget, SliderWidgetConfig } from './slider';
6
- import { StringWidget, StringWidgetConfig } from './string';
7
- import { ToggleWidget, ToggleWidgetConfig } from './toggle';
8
- import { FlowNode } from "../node";
9
- import { ImageWidget, ImageWidgetConfig } from "./image";
10
- import { TextWidget, TextWidgetConfig } from "./text";
11
-
12
- export type WidgetBuilder = (node: FlowNode, confg?: any) => Widget;
13
-
14
- export class WidgetFactory {
15
-
16
- #registeredWidgets: Map<string, WidgetBuilder>
17
-
18
- constructor() {
19
- this.#registeredWidgets = new Map<string, WidgetBuilder>();
20
- }
21
-
22
- register(widgetType: string, builder: WidgetBuilder): void {
23
- this.#registeredWidgets.set(widgetType, builder);
24
- }
25
-
26
- create(node: FlowNode, widgetType: string, config: any): Widget {
27
- const builder = this.#registeredWidgets.get(widgetType)
28
- if (builder === undefined) {
29
- throw new Error("no builder registered for widget: " + widgetType);
30
- }
31
- return builder(node, config);
32
- }
33
- }
34
-
35
- const GlobalWidgetFactory = new WidgetFactory();
36
-
37
- GlobalWidgetFactory.register("button", (node: FlowNode, config?: ButtonWidgetConfig) => new ButtonWidget(config));
38
- GlobalWidgetFactory.register("number", (node: FlowNode, config?: NumberWidgetConfig) => new NumberWidget(node, config));
39
- GlobalWidgetFactory.register("color", (node: FlowNode, config?: ColorWidgetConfig) => new ColorWidget(node, config));
40
- GlobalWidgetFactory.register("slider", (node: FlowNode, config?: SliderWidgetConfig) => new SliderWidget(node, config));
41
- GlobalWidgetFactory.register("string", (node: FlowNode, config?: StringWidgetConfig) => new StringWidget(node, config));
42
- GlobalWidgetFactory.register("toggle", (node: FlowNode, config?: ToggleWidgetConfig) => new ToggleWidget(node, config));
43
- GlobalWidgetFactory.register("image", (node: FlowNode, config?: ImageWidgetConfig) => new ImageWidget(config));
44
- GlobalWidgetFactory.register("text", (node: FlowNode, config?: TextWidgetConfig) => new TextWidget(node, config));
45
-
46
- export { GlobalWidgetFactory };
1
+ import { Widget } from "./widget";
2
+ import { NumberWidget, NumberWidgetConfig } from './number';
3
+ import { ButtonWidget, ButtonWidgetConfig } from './button';
4
+ import { ColorWidget, ColorWidgetConfig } from './color';
5
+ import { SliderWidget, SliderWidgetConfig } from './slider';
6
+ import { StringWidget, StringWidgetConfig } from './string';
7
+ import { ToggleWidget, ToggleWidgetConfig } from './toggle';
8
+ import { FlowNode } from "../node";
9
+ import { ImageWidget, ImageWidgetConfig } from "./image";
10
+ import { TextWidget, TextWidgetConfig } from "./text";
11
+ import { RowWidget, RowWidgetConfig } from "./row";
12
+
13
+ export interface RowWidgetFactoryConfig extends RowWidgetConfig {
14
+ widgets: Array<WidgetConfig>;
15
+ }
16
+
17
+ export interface WidgetConfigs {
18
+ button: ButtonWidgetConfig;
19
+ number: NumberWidgetConfig;
20
+ color: ColorWidgetConfig;
21
+ slider: SliderWidgetConfig;
22
+ string: StringWidgetConfig;
23
+ toggle: ToggleWidgetConfig;
24
+ image: ImageWidgetConfig;
25
+ text: TextWidgetConfig;
26
+ row: RowWidgetFactoryConfig;
27
+ }
28
+
29
+ export interface WidgetTypes {
30
+ button: ButtonWidget;
31
+ number: NumberWidget;
32
+ color: ColorWidget;
33
+ slider: SliderWidget;
34
+ string: StringWidget;
35
+ toggle: ToggleWidget;
36
+ image: ImageWidget;
37
+ text: TextWidget;
38
+ row: RowWidget;
39
+ }
40
+
41
+ export type WidgetConfig = {
42
+ [K in keyof WidgetConfigs]: { type: K; config?: WidgetConfigs[K] }
43
+ }[keyof WidgetConfigs];
44
+
45
+ export type WidgetBuilder = (node: FlowNode, config?: any) => Widget;
46
+
47
+ export class WidgetFactory {
48
+
49
+ #registeredWidgets: Map<string, WidgetBuilder>
50
+
51
+ constructor() {
52
+ this.#registeredWidgets = new Map<string, WidgetBuilder>();
53
+ }
54
+
55
+ register(widgetType: string, builder: WidgetBuilder): void {
56
+ this.#registeredWidgets.set(widgetType, builder);
57
+ }
58
+
59
+ create<K extends keyof WidgetConfigs>(node: FlowNode, widgetType: K, config?: WidgetConfigs[K]): WidgetTypes[K];
60
+ create(node: FlowNode, widgetType: string, config?: unknown): Widget;
61
+ create(node: FlowNode, widgetType: string, config?: unknown): Widget {
62
+ const builder = this.#registeredWidgets.get(widgetType)
63
+ if (builder === undefined) {
64
+ throw new Error("no builder registered for widget: " + widgetType);
65
+ }
66
+ return builder(node, config);
67
+ }
68
+
69
+ createFromConfig(node: FlowNode, config: WidgetConfig): Widget {
70
+ return this.create(node, config.type, config.config);
71
+ }
72
+ }
73
+
74
+ const GlobalWidgetFactory = new WidgetFactory();
75
+
76
+ GlobalWidgetFactory.register("button", (node: FlowNode, config?: ButtonWidgetConfig) => new ButtonWidget(config));
77
+ GlobalWidgetFactory.register("number", (node: FlowNode, config?: NumberWidgetConfig) => new NumberWidget(node, config));
78
+ GlobalWidgetFactory.register("color", (node: FlowNode, config?: ColorWidgetConfig) => new ColorWidget(node, config));
79
+ GlobalWidgetFactory.register("slider", (node: FlowNode, config?: SliderWidgetConfig) => new SliderWidget(node, config));
80
+ GlobalWidgetFactory.register("string", (node: FlowNode, config?: StringWidgetConfig) => new StringWidget(node, config));
81
+ GlobalWidgetFactory.register("toggle", (node: FlowNode, config?: ToggleWidgetConfig) => new ToggleWidget(node, config));
82
+ GlobalWidgetFactory.register("image", (node: FlowNode, config?: ImageWidgetConfig) => new ImageWidget(config));
83
+ GlobalWidgetFactory.register("text", (node: FlowNode, config?: TextWidgetConfig) => new TextWidget(node, config));
84
+ GlobalWidgetFactory.register("row", (node: FlowNode, config?: RowWidgetFactoryConfig) => new RowWidget(
85
+ (config?.widgets ?? []).map((child) => GlobalWidgetFactory.createFromConfig(node, child)),
86
+ config,
87
+ ));
88
+
89
+ export { GlobalWidgetFactory };
@@ -0,0 +1,99 @@
1
+ import { Theme } from "../theme";
2
+ import { drawBevel } from "../styles/bevel";
3
+ import { Box } from "../types/box";
4
+ import { fitString } from "../utils/string";
5
+
6
+ export interface FieldLook {
7
+ hovered: boolean;
8
+ pressed?: boolean;
9
+
10
+ /**
11
+ * Something typed or dragged into, as opposed to something pressed.
12
+ */
13
+ input?: boolean;
14
+
15
+ label?: string;
16
+ value: string;
17
+ background?: string;
18
+ border?: string;
19
+ textColor?: string;
20
+ }
21
+
22
+ export function widgetFont(scale: number): string {
23
+ return `${Theme.Widget.FontSize * scale}px ${Theme.FontFamily}`;
24
+ }
25
+
26
+ function fieldBackground(look: FieldLook): string {
27
+ if (look.background !== undefined) {
28
+ return look.background;
29
+ }
30
+ if (look.input) {
31
+ return look.hovered ? Theme.Widget.Input.HoverBackgroundColor : Theme.Widget.Input.BackgroundColor;
32
+ }
33
+ if (look.pressed) {
34
+ return Theme.Widget.Button.Click.BackgroundColor;
35
+ }
36
+ return look.hovered ? Theme.Widget.Hover.BackgroundColor : Theme.Widget.BackgroundColor;
37
+ }
38
+
39
+ export function drawFieldBox(ctx: CanvasRenderingContext2D, box: Box, scale: number, look: FieldLook): void {
40
+ ctx.fillStyle = fieldBackground(look);
41
+ ctx.beginPath();
42
+ ctx.roundRect(box.Position.x, box.Position.y, box.Size.x, box.Size.y, Theme.Widget.Border.Radius * scale);
43
+ ctx.fill();
44
+
45
+ if (Theme.Widget.Border.Size > 0) {
46
+ ctx.strokeStyle = look.border ?? Theme.Widget.Border.Color;
47
+ ctx.lineWidth = Theme.Widget.Border.Size * scale;
48
+ ctx.stroke();
49
+ }
50
+
51
+ if (Theme.Widget.Bevel !== null) {
52
+ drawBevel(ctx, box, scale, Theme.Widget.Bevel, look.input === true || look.pressed === true);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * A boxed value with an optional label: the label on the left and the value
58
+ * on the right, or the value alone in the middle.
59
+ */
60
+ export function drawField(ctx: CanvasRenderingContext2D, box: Box, scale: number, look: FieldLook): void {
61
+ drawFieldBox(ctx, box, scale, look);
62
+ drawFieldText(ctx, box, scale, look);
63
+ }
64
+
65
+ export function drawFieldText(ctx: CanvasRenderingContext2D, box: Box, scale: number, look: FieldLook): void {
66
+ const padding = Theme.Widget.Padding * scale;
67
+ const middle = box.Position.y + (box.Size.y / 2);
68
+ const textColor = look.textColor ?? (look.input ? Theme.Widget.Input.FontColor : Theme.Widget.FontColor);
69
+
70
+ ctx.font = widgetFont(scale);
71
+ ctx.textBaseline = "middle";
72
+
73
+ if (look.label === undefined || look.label === "") {
74
+ ctx.textAlign = "center";
75
+ ctx.fillStyle = textColor;
76
+ ctx.fillText(
77
+ fitString(ctx, look.value, box.Size.x - (padding * 2)),
78
+ box.Position.x + (box.Size.x / 2),
79
+ middle,
80
+ );
81
+ return;
82
+ }
83
+
84
+ const room = box.Size.x - (padding * 2);
85
+ const label = fitString(ctx, look.label, room / 2);
86
+ const labelWidth = ctx.measureText(label).width;
87
+
88
+ ctx.textAlign = "left";
89
+ ctx.fillStyle = look.textColor ?? Theme.Widget.LabelColor;
90
+ ctx.fillText(label, box.Position.x + padding, middle);
91
+
92
+ ctx.textAlign = "right";
93
+ ctx.fillStyle = textColor;
94
+ ctx.fillText(
95
+ fitString(ctx, look.value, room - labelWidth - padding),
96
+ box.Position.x + box.Size.x - padding,
97
+ middle,
98
+ );
99
+ }