@flighthq/gui 0.4.1-edge.28.7f8e221
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/LICENSE.md +9 -0
- package/README.md +17 -0
- package/dist/buttonController.d.ts +7 -0
- package/dist/buttonController.d.ts.map +1 -0
- package/dist/buttonController.js +83 -0
- package/dist/buttonController.js.map +1 -0
- package/dist/comboBoxController.d.ts +7 -0
- package/dist/comboBoxController.d.ts.map +1 -0
- package/dist/comboBoxController.js +46 -0
- package/dist/comboBoxController.js.map +1 -0
- package/dist/contract.d.ts +16 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +16 -0
- package/dist/contract.js.map +1 -0
- package/dist/guiController.d.ts +24 -0
- package/dist/guiController.d.ts.map +1 -0
- package/dist/guiController.js +101 -0
- package/dist/guiController.js.map +1 -0
- package/dist/guiTestHelper.d.ts +5 -0
- package/dist/guiTestHelper.d.ts.map +1 -0
- package/dist/guiTestHelper.js +57 -0
- package/dist/guiTestHelper.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/listController.d.ts +8 -0
- package/dist/listController.d.ts.map +1 -0
- package/dist/listController.js +82 -0
- package/dist/listController.js.map +1 -0
- package/dist/progressBarController.d.ts +6 -0
- package/dist/progressBarController.d.ts.map +1 -0
- package/dist/progressBarController.js +42 -0
- package/dist/progressBarController.js.map +1 -0
- package/dist/radioGroupController.d.ts +7 -0
- package/dist/radioGroupController.d.ts.map +1 -0
- package/dist/radioGroupController.js +50 -0
- package/dist/radioGroupController.js.map +1 -0
- package/dist/scrollBarController.d.ts +7 -0
- package/dist/scrollBarController.d.ts.map +1 -0
- package/dist/scrollBarController.js +124 -0
- package/dist/scrollBarController.js.map +1 -0
- package/dist/scrollViewController.d.ts +8 -0
- package/dist/scrollViewController.d.ts.map +1 -0
- package/dist/scrollViewController.js +99 -0
- package/dist/scrollViewController.js.map +1 -0
- package/dist/sliderController.d.ts +7 -0
- package/dist/sliderController.d.ts.map +1 -0
- package/dist/sliderController.js +105 -0
- package/dist/sliderController.js.map +1 -0
- package/dist/splitPaneController.d.ts +7 -0
- package/dist/splitPaneController.d.ts.map +1 -0
- package/dist/splitPaneController.js +87 -0
- package/dist/splitPaneController.js.map +1 -0
- package/dist/tabBarController.d.ts +7 -0
- package/dist/tabBarController.d.ts.map +1 -0
- package/dist/tabBarController.js +43 -0
- package/dist/tabBarController.js.map +1 -0
- package/dist/textInputController.d.ts +9 -0
- package/dist/textInputController.d.ts.map +1 -0
- package/dist/textInputController.js +132 -0
- package/dist/textInputController.js.map +1 -0
- package/dist/toggleController.d.ts +7 -0
- package/dist/toggleController.d.ts.map +1 -0
- package/dist/toggleController.js +57 -0
- package/dist/toggleController.js.map +1 -0
- package/dist/tooltipController.d.ts +6 -0
- package/dist/tooltipController.d.ts.map +1 -0
- package/dist/tooltipController.js +62 -0
- package/dist/tooltipController.js.map +1 -0
- package/dist/treeViewController.d.ts +10 -0
- package/dist/treeViewController.d.ts.map +1 -0
- package/dist/treeViewController.js +128 -0
- package/dist/treeViewController.js.map +1 -0
- package/dist/windowController.d.ts +7 -0
- package/dist/windowController.d.ts.map +1 -0
- package/dist/windowController.js +119 -0
- package/dist/windowController.js.map +1 -0
- package/package.json +53 -0
- package/src/buttonController.test.ts +90 -0
- package/src/comboBoxController.test.ts +71 -0
- package/src/guiController.test.ts +162 -0
- package/src/guiTreeShaking.test.ts +35 -0
- package/src/listController.test.ts +96 -0
- package/src/progressBarController.test.ts +49 -0
- package/src/radioGroupController.test.ts +61 -0
- package/src/scrollBarController.test.ts +86 -0
- package/src/scrollViewController.test.ts +80 -0
- package/src/sliderController.test.ts +74 -0
- package/src/splitPaneController.test.ts +85 -0
- package/src/tabBarController.test.ts +50 -0
- package/src/textInputController.test.ts +93 -0
- package/src/toggleController.test.ts +72 -0
- package/src/tooltipController.test.ts +56 -0
- package/src/treeViewController.test.ts +105 -0
- package/src/windowController.test.ts +75 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { getNodeHeight, getNodeWidth } from '@flighthq/node/contract';
|
|
2
|
+
import { createSignal, emitSignal } from '@flighthq/signals/contract';
|
|
3
|
+
import { getButtonControllerSignals } from './buttonController';
|
|
4
|
+
import { connectGuiInteraction, connectGuiSignal, createGuiController, createGuiControllerRuntime, disposeGuiController, getGuiControllerRuntime, setGuiVisualProperty, } from './guiController';
|
|
5
|
+
export function createWindowController(options) {
|
|
6
|
+
const width = getNodeWidth(options.frame);
|
|
7
|
+
const height = getNodeHeight(options.frame);
|
|
8
|
+
const runtime = createGuiControllerRuntime({
|
|
9
|
+
closeButton: options.closeButton ?? null,
|
|
10
|
+
content: options.content ?? null,
|
|
11
|
+
dragPointer: -1,
|
|
12
|
+
dragStartFrameX: 0,
|
|
13
|
+
dragStartFrameY: 0,
|
|
14
|
+
dragStartWorldX: 0,
|
|
15
|
+
dragStartWorldY: 0,
|
|
16
|
+
draggable: options.draggable ?? true,
|
|
17
|
+
frame: options.frame,
|
|
18
|
+
frameBaseHeight: height,
|
|
19
|
+
frameBaseScaleX: options.frame.scaleX,
|
|
20
|
+
frameBaseScaleY: options.frame.scaleY,
|
|
21
|
+
frameBaseWidth: width,
|
|
22
|
+
minimumHeight: Math.max(0, options.minimumHeight ?? 0),
|
|
23
|
+
minimumWidth: Math.max(0, options.minimumWidth ?? 0),
|
|
24
|
+
resizeHandle: options.resizeHandle ?? null,
|
|
25
|
+
resizePointer: -1,
|
|
26
|
+
resizeStartHeight: 0,
|
|
27
|
+
resizeStartWidth: 0,
|
|
28
|
+
resizeStartWorldX: 0,
|
|
29
|
+
resizeStartWorldY: 0,
|
|
30
|
+
resizable: options.resizable ?? true,
|
|
31
|
+
signals: { onClose: createSignal(), onMove: createSignal(), onResize: createSignal() },
|
|
32
|
+
titleBar: options.titleBar ?? null,
|
|
33
|
+
}, options.transition);
|
|
34
|
+
const controller = createGuiController(runtime);
|
|
35
|
+
if (runtime.closeButton !== null) {
|
|
36
|
+
connectGuiSignal(runtime, getButtonControllerSignals(runtime.closeButton).onClick, () => emitSignal(runtime.signals.onClose));
|
|
37
|
+
}
|
|
38
|
+
if (runtime.titleBar !== null) {
|
|
39
|
+
connectGuiInteraction(runtime, runtime.titleBar, 'onPointerDown', (data) => {
|
|
40
|
+
if (!runtime.draggable || runtime.frame === null)
|
|
41
|
+
return;
|
|
42
|
+
runtime.dragPointer = data.pointerId;
|
|
43
|
+
runtime.dragStartWorldX = data.worldX;
|
|
44
|
+
runtime.dragStartWorldY = data.worldY;
|
|
45
|
+
runtime.dragStartFrameX = runtime.frame.x;
|
|
46
|
+
runtime.dragStartFrameY = runtime.frame.y;
|
|
47
|
+
});
|
|
48
|
+
connectGuiInteraction(runtime, runtime.titleBar, 'onPointerMove', (data) => {
|
|
49
|
+
if (data.pointerId !== runtime.dragPointer)
|
|
50
|
+
return;
|
|
51
|
+
setWindowControllerPosition(controller, runtime.dragStartFrameX + data.worldX - runtime.dragStartWorldX, runtime.dragStartFrameY + data.worldY - runtime.dragStartWorldY);
|
|
52
|
+
});
|
|
53
|
+
const endDrag = (data) => {
|
|
54
|
+
if (data.pointerId === runtime.dragPointer)
|
|
55
|
+
runtime.dragPointer = -1;
|
|
56
|
+
};
|
|
57
|
+
connectGuiInteraction(runtime, runtime.titleBar, 'onPointerUp', endDrag);
|
|
58
|
+
connectGuiInteraction(runtime, runtime.titleBar, 'onPointerCancel', endDrag);
|
|
59
|
+
connectGuiInteraction(runtime, runtime.titleBar, 'onReleaseOutside', endDrag);
|
|
60
|
+
}
|
|
61
|
+
if (runtime.resizeHandle !== null) {
|
|
62
|
+
connectGuiInteraction(runtime, runtime.resizeHandle, 'onPointerDown', (data) => {
|
|
63
|
+
if (!runtime.resizable || runtime.frame === null)
|
|
64
|
+
return;
|
|
65
|
+
runtime.resizePointer = data.pointerId;
|
|
66
|
+
runtime.resizeStartWorldX = data.worldX;
|
|
67
|
+
runtime.resizeStartWorldY = data.worldY;
|
|
68
|
+
runtime.resizeStartWidth = getNodeWidth(runtime.frame);
|
|
69
|
+
runtime.resizeStartHeight = getNodeHeight(runtime.frame);
|
|
70
|
+
});
|
|
71
|
+
connectGuiInteraction(runtime, runtime.resizeHandle, 'onPointerMove', (data) => {
|
|
72
|
+
if (data.pointerId !== runtime.resizePointer)
|
|
73
|
+
return;
|
|
74
|
+
setWindowControllerSize(controller, runtime.resizeStartWidth + data.worldX - runtime.resizeStartWorldX, runtime.resizeStartHeight + data.worldY - runtime.resizeStartWorldY);
|
|
75
|
+
});
|
|
76
|
+
const endResize = (data) => {
|
|
77
|
+
if (data.pointerId === runtime.resizePointer)
|
|
78
|
+
runtime.resizePointer = -1;
|
|
79
|
+
};
|
|
80
|
+
connectGuiInteraction(runtime, runtime.resizeHandle, 'onPointerUp', endResize);
|
|
81
|
+
connectGuiInteraction(runtime, runtime.resizeHandle, 'onPointerCancel', endResize);
|
|
82
|
+
connectGuiInteraction(runtime, runtime.resizeHandle, 'onReleaseOutside', endResize);
|
|
83
|
+
}
|
|
84
|
+
return controller;
|
|
85
|
+
}
|
|
86
|
+
export function disposeWindowController(controller) {
|
|
87
|
+
const runtime = getGuiControllerRuntime(controller);
|
|
88
|
+
disposeGuiController(controller, () => {
|
|
89
|
+
runtime.closeButton = null;
|
|
90
|
+
runtime.content = null;
|
|
91
|
+
runtime.frame = null;
|
|
92
|
+
runtime.resizeHandle = null;
|
|
93
|
+
runtime.titleBar = null;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export function getWindowControllerSignals(controller) {
|
|
97
|
+
return getGuiControllerRuntime(controller).signals;
|
|
98
|
+
}
|
|
99
|
+
export function setWindowControllerPosition(controller, x, y) {
|
|
100
|
+
const runtime = getGuiControllerRuntime(controller);
|
|
101
|
+
if (runtime.frame === null || runtime.disposed || (runtime.frame.x === x && runtime.frame.y === y))
|
|
102
|
+
return;
|
|
103
|
+
setGuiVisualProperty(runtime, runtime.frame, 'x', x);
|
|
104
|
+
setGuiVisualProperty(runtime, runtime.frame, 'y', y);
|
|
105
|
+
emitSignal(runtime.signals.onMove, x, y);
|
|
106
|
+
}
|
|
107
|
+
export function setWindowControllerSize(controller, width, height) {
|
|
108
|
+
const runtime = getGuiControllerRuntime(controller);
|
|
109
|
+
if (runtime.frame === null || runtime.disposed)
|
|
110
|
+
return;
|
|
111
|
+
const nextWidth = Math.max(runtime.minimumWidth, width);
|
|
112
|
+
const nextHeight = Math.max(runtime.minimumHeight, height);
|
|
113
|
+
if (runtime.frameBaseWidth > 0)
|
|
114
|
+
setGuiVisualProperty(runtime, runtime.frame, 'scaleX', (runtime.frameBaseScaleX * nextWidth) / runtime.frameBaseWidth);
|
|
115
|
+
if (runtime.frameBaseHeight > 0)
|
|
116
|
+
setGuiVisualProperty(runtime, runtime.frame, 'scaleY', (runtime.frameBaseScaleY * nextHeight) / runtime.frameBaseHeight);
|
|
117
|
+
emitSignal(runtime.signals.onResize, nextWidth, nextHeight);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=windowController.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windowController.js","sourceRoot":"","sources":["../src/windowController.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/gui",
|
|
3
|
+
"version": "0.4.1-edge.28.7f8e221",
|
|
4
|
+
"author": "Joshua Granick and other contributors",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/flighthq/flight.git",
|
|
9
|
+
"directory": "packages/gui"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./contract": {
|
|
20
|
+
"types": "./dist/contract.d.ts",
|
|
21
|
+
"default": "./dist/contract.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src/**/*.test.ts",
|
|
27
|
+
"!dist/**/*.test.js",
|
|
28
|
+
"!dist/**/*.test.d.ts",
|
|
29
|
+
"!dist/**/*.test.js.map",
|
|
30
|
+
"!dist/**/*.test.d.ts.map"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -b",
|
|
34
|
+
"clean": "tsc -b --clean",
|
|
35
|
+
"test": "vitest run --config vitest.config.ts",
|
|
36
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
37
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
38
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@flighthq/interaction": "0.4.1-edge.28.7f8e221",
|
|
42
|
+
"@flighthq/node": "0.4.1-edge.28.7f8e221",
|
|
43
|
+
"@flighthq/signals": "0.4.1-edge.28.7f8e221",
|
|
44
|
+
"@flighthq/text": "0.4.1-edge.28.7f8e221",
|
|
45
|
+
"@flighthq/textinput": "0.4.1-edge.28.7f8e221",
|
|
46
|
+
"@flighthq/types": "0.4.1-edge.28.7f8e221"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"typescript": "^5.3.0"
|
|
50
|
+
},
|
|
51
|
+
"description": "Controller-only behavior for caller-authored 2D interface visuals",
|
|
52
|
+
"sideEffects": false
|
|
53
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { connectSignal } from '@flighthq/signals/contract';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createButtonController,
|
|
5
|
+
disposeButtonController,
|
|
6
|
+
getButtonControllerSignals,
|
|
7
|
+
isButtonControllerDisabled,
|
|
8
|
+
setButtonControllerDisabled,
|
|
9
|
+
} from './buttonController';
|
|
10
|
+
import { createGuiTestNode, emitGuiPointer } from './guiTestHelper';
|
|
11
|
+
|
|
12
|
+
describe('createButtonController', () => {
|
|
13
|
+
it('drives optional states and emits the complete pointer lifecycle', () => {
|
|
14
|
+
const up = createGuiTestNode();
|
|
15
|
+
const over = createGuiTestNode();
|
|
16
|
+
const down = createGuiTestNode();
|
|
17
|
+
const controller = createButtonController({ downState: down, overState: over, upState: up });
|
|
18
|
+
const events: string[] = [];
|
|
19
|
+
const signals = getButtonControllerSignals(controller);
|
|
20
|
+
connectSignal(signals.onPress, () => events.push('press'));
|
|
21
|
+
connectSignal(signals.onRelease, () => events.push('release'));
|
|
22
|
+
connectSignal(signals.onClick, () => events.push('click'));
|
|
23
|
+
|
|
24
|
+
emitGuiPointer(up, 'onPointerOver');
|
|
25
|
+
expect([up.visible, over.visible, down.visible]).toEqual([false, true, false]);
|
|
26
|
+
emitGuiPointer(up, 'onPointerDown');
|
|
27
|
+
expect([up.visible, over.visible, down.visible]).toEqual([false, false, true]);
|
|
28
|
+
emitGuiPointer(up, 'onPointerUp');
|
|
29
|
+
emitGuiPointer(up, 'onClick');
|
|
30
|
+
expect(events).toEqual(['press', 'release', 'click']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('delegates property assignment only when a transition is supplied', () => {
|
|
34
|
+
const up = createGuiTestNode();
|
|
35
|
+
const over = createGuiTestNode();
|
|
36
|
+
const applies: Array<() => void> = [];
|
|
37
|
+
createButtonController({
|
|
38
|
+
overState: over,
|
|
39
|
+
transition: { run: (request) => applies.push(() => request.apply()) },
|
|
40
|
+
upState: up,
|
|
41
|
+
});
|
|
42
|
+
for (const apply of applies.splice(0)) apply();
|
|
43
|
+
expect([up.visible, over.visible]).toEqual([true, false]);
|
|
44
|
+
|
|
45
|
+
emitGuiPointer(up, 'onPointerOver');
|
|
46
|
+
expect([up.visible, over.visible]).toEqual([true, false]);
|
|
47
|
+
for (const apply of applies.splice(0)) apply();
|
|
48
|
+
expect([up.visible, over.visible]).toEqual([false, true]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('disposeButtonController', () => {
|
|
53
|
+
it('detaches listeners idempotently', () => {
|
|
54
|
+
const up = createGuiTestNode();
|
|
55
|
+
const controller = createButtonController({ upState: up });
|
|
56
|
+
let clicks = 0;
|
|
57
|
+
connectSignal(getButtonControllerSignals(controller).onClick, () => clicks++);
|
|
58
|
+
disposeButtonController(controller);
|
|
59
|
+
disposeButtonController(controller);
|
|
60
|
+
emitGuiPointer(up, 'onClick');
|
|
61
|
+
expect(clicks).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('getButtonControllerSignals', () => {
|
|
66
|
+
it('returns a stable signal group', () => {
|
|
67
|
+
const controller = createButtonController({ upState: createGuiTestNode() });
|
|
68
|
+
expect(getButtonControllerSignals(controller)).toBe(getButtonControllerSignals(controller));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('isButtonControllerDisabled', () => {
|
|
73
|
+
it('reads the configured state', () => {
|
|
74
|
+
expect(isButtonControllerDisabled(createButtonController({ disabled: true, upState: createGuiTestNode() }))).toBe(
|
|
75
|
+
true,
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('setButtonControllerDisabled', () => {
|
|
81
|
+
it('blocks click emission', () => {
|
|
82
|
+
const up = createGuiTestNode();
|
|
83
|
+
const controller = createButtonController({ upState: up });
|
|
84
|
+
let clicks = 0;
|
|
85
|
+
connectSignal(getButtonControllerSignals(controller).onClick, () => clicks++);
|
|
86
|
+
setButtonControllerDisabled(controller, true);
|
|
87
|
+
emitGuiPointer(up, 'onClick');
|
|
88
|
+
expect(clicks).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { connectSignal } from '@flighthq/signals/contract';
|
|
2
|
+
|
|
3
|
+
import { createButtonController } from './buttonController';
|
|
4
|
+
import {
|
|
5
|
+
createComboBoxController,
|
|
6
|
+
disposeComboBoxController,
|
|
7
|
+
getComboBoxControllerSignals,
|
|
8
|
+
isComboBoxControllerOpen,
|
|
9
|
+
setComboBoxControllerOpen,
|
|
10
|
+
} from './comboBoxController';
|
|
11
|
+
import { createGuiTestNode, emitGuiPointer } from './guiTestHelper';
|
|
12
|
+
import { createListController } from './listController';
|
|
13
|
+
|
|
14
|
+
function parts() {
|
|
15
|
+
const buttonNode = createGuiTestNode();
|
|
16
|
+
const viewport = createGuiTestNode();
|
|
17
|
+
const item = createGuiTestNode();
|
|
18
|
+
return {
|
|
19
|
+
button: createButtonController({ upState: buttonNode }),
|
|
20
|
+
buttonNode,
|
|
21
|
+
item,
|
|
22
|
+
list: createListController({ content: createGuiTestNode(), items: [item], viewport }),
|
|
23
|
+
viewport,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('createComboBoxController', () => {
|
|
28
|
+
it('opens from its button and closes on list selection', () => {
|
|
29
|
+
const p = parts();
|
|
30
|
+
const controller = createComboBoxController(p);
|
|
31
|
+
emitGuiPointer(p.buttonNode, 'onClick');
|
|
32
|
+
expect(isComboBoxControllerOpen(controller)).toBe(true);
|
|
33
|
+
emitGuiPointer(p.item, 'onClick');
|
|
34
|
+
expect(isComboBoxControllerOpen(controller)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('disposeComboBoxController', () => {
|
|
39
|
+
it('detaches composed controller signals', () => {
|
|
40
|
+
const p = parts();
|
|
41
|
+
const controller = createComboBoxController(p);
|
|
42
|
+
disposeComboBoxController(controller);
|
|
43
|
+
emitGuiPointer(p.buttonNode, 'onClick');
|
|
44
|
+
expect(isComboBoxControllerOpen(controller)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('getComboBoxControllerSignals', () => {
|
|
49
|
+
it('emits open-state transitions', () => {
|
|
50
|
+
const controller = createComboBoxController(parts());
|
|
51
|
+
const values: boolean[] = [];
|
|
52
|
+
connectSignal(getComboBoxControllerSignals(controller).onOpenChange, (open) => values.push(open));
|
|
53
|
+
setComboBoxControllerOpen(controller, true);
|
|
54
|
+
expect(values).toEqual([true]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('isComboBoxControllerOpen', () => {
|
|
59
|
+
it('reads initial open state', () => {
|
|
60
|
+
expect(isComboBoxControllerOpen(createComboBoxController({ ...parts(), open: true }))).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('setComboBoxControllerOpen', () => {
|
|
65
|
+
it('updates list visibility', () => {
|
|
66
|
+
const p = parts();
|
|
67
|
+
const controller = createComboBoxController(p);
|
|
68
|
+
setComboBoxControllerOpen(controller, true);
|
|
69
|
+
expect(p.viewport.visible).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { getNodeHitArea, isNodeHitTestEnabled } from '@flighthq/interaction/contract';
|
|
2
|
+
import { createSignal, emitSignal } from '@flighthq/signals/contract';
|
|
3
|
+
import type { Entity } from '@flighthq/types/contract';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
clampGuiValue,
|
|
7
|
+
configureGuiHitArea,
|
|
8
|
+
connectGuiInteraction,
|
|
9
|
+
connectGuiSignal,
|
|
10
|
+
createGuiController,
|
|
11
|
+
createGuiControllerRuntime,
|
|
12
|
+
disposeGuiController,
|
|
13
|
+
getGuiControllerRuntime,
|
|
14
|
+
getGuiLength,
|
|
15
|
+
getGuiPosition,
|
|
16
|
+
setGuiPosition,
|
|
17
|
+
setGuiScale,
|
|
18
|
+
setGuiVisible,
|
|
19
|
+
setGuiVisualProperty,
|
|
20
|
+
snapGuiValue,
|
|
21
|
+
} from './guiController';
|
|
22
|
+
import { createGuiTestNode, emitGuiPointer } from './guiTestHelper';
|
|
23
|
+
|
|
24
|
+
describe('clampGuiValue', () => {
|
|
25
|
+
it('clamps finite values and normalizes reversed bounds', () => {
|
|
26
|
+
expect(clampGuiValue(8, 0, 5)).toBe(5);
|
|
27
|
+
expect(clampGuiValue(2, 5, 0)).toBe(2);
|
|
28
|
+
expect(clampGuiValue(NaN, 2, 5)).toBe(2);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('configureGuiHitArea', () => {
|
|
33
|
+
it('restores a caller hit area on disposal', () => {
|
|
34
|
+
const target = createGuiTestNode();
|
|
35
|
+
const area = createGuiTestNode();
|
|
36
|
+
const runtime = createGuiControllerRuntime({});
|
|
37
|
+
const controller = createGuiController<Entity, typeof runtime>(runtime);
|
|
38
|
+
configureGuiHitArea(runtime, target, area);
|
|
39
|
+
expect(getNodeHitArea(target)).toBe(area);
|
|
40
|
+
disposeGuiController(controller, () => {});
|
|
41
|
+
expect(getNodeHitArea(target)).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('connectGuiInteraction', () => {
|
|
46
|
+
it('connects input and restores hit-test state', () => {
|
|
47
|
+
const target = createGuiTestNode();
|
|
48
|
+
const runtime = createGuiControllerRuntime({});
|
|
49
|
+
const controller = createGuiController<Entity, typeof runtime>(runtime);
|
|
50
|
+
let calls = 0;
|
|
51
|
+
connectGuiInteraction(runtime, target, 'onClick', () => calls++);
|
|
52
|
+
emitGuiPointer(target, 'onClick');
|
|
53
|
+
expect(calls).toBe(1);
|
|
54
|
+
disposeGuiController(controller, () => {});
|
|
55
|
+
expect(isNodeHitTestEnabled(target)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('connectGuiSignal', () => {
|
|
60
|
+
it('disconnects a generic signal on disposal', () => {
|
|
61
|
+
const signal = createSignal<() => void>();
|
|
62
|
+
const runtime = createGuiControllerRuntime({});
|
|
63
|
+
const controller = createGuiController<Entity, typeof runtime>(runtime);
|
|
64
|
+
let calls = 0;
|
|
65
|
+
connectGuiSignal(runtime, signal, () => calls++);
|
|
66
|
+
disposeGuiController(controller, () => {});
|
|
67
|
+
emitSignal(signal);
|
|
68
|
+
expect(calls).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('createGuiController', () => {
|
|
73
|
+
it('stores the supplied runtime', () => {
|
|
74
|
+
const runtime = createGuiControllerRuntime({ value: 4 });
|
|
75
|
+
const controller = createGuiController<Entity, typeof runtime>(runtime);
|
|
76
|
+
expect(getGuiControllerRuntime<{ value: number }>(controller).value).toBe(4);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('createGuiControllerRuntime', () => {
|
|
81
|
+
it('starts undisposed with no transition', () => {
|
|
82
|
+
const runtime = createGuiControllerRuntime({});
|
|
83
|
+
expect([runtime.disposed, runtime.transition]).toEqual([false, null]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('disposeGuiController', () => {
|
|
88
|
+
it('runs clear once', () => {
|
|
89
|
+
const runtime = createGuiControllerRuntime({});
|
|
90
|
+
const controller = createGuiController<Entity, typeof runtime>(runtime);
|
|
91
|
+
let clears = 0;
|
|
92
|
+
disposeGuiController(controller, () => clears++);
|
|
93
|
+
disposeGuiController(controller, () => clears++);
|
|
94
|
+
expect(clears).toBe(1);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('getGuiControllerRuntime', () => {
|
|
99
|
+
it('returns the exact runtime identity', () => {
|
|
100
|
+
const runtime = createGuiControllerRuntime({});
|
|
101
|
+
expect(getGuiControllerRuntime(createGuiController<Entity, typeof runtime>(runtime))).toBe(runtime);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('getGuiLength', () => {
|
|
106
|
+
it('reads the requested bounded axis', () => {
|
|
107
|
+
const node = createGuiTestNode(30, 40);
|
|
108
|
+
expect([getGuiLength(node, 'horizontal'), getGuiLength(node, 'vertical')]).toEqual([30, 40]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('getGuiPosition', () => {
|
|
113
|
+
it('reads the requested transform axis', () => {
|
|
114
|
+
const node = createGuiTestNode();
|
|
115
|
+
node.x = 3;
|
|
116
|
+
node.y = 4;
|
|
117
|
+
expect([getGuiPosition(node, 'horizontal'), getGuiPosition(node, 'vertical')]).toEqual([3, 4]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('setGuiPosition', () => {
|
|
122
|
+
it('sets one axis', () => {
|
|
123
|
+
const runtime = createGuiControllerRuntime({});
|
|
124
|
+
const node = createGuiTestNode();
|
|
125
|
+
setGuiPosition(runtime, node, 'vertical', 5);
|
|
126
|
+
expect(node.y).toBe(5);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('setGuiScale', () => {
|
|
131
|
+
it('sets one scale axis', () => {
|
|
132
|
+
const runtime = createGuiControllerRuntime({});
|
|
133
|
+
const node = createGuiTestNode();
|
|
134
|
+
setGuiScale(runtime, node, 'horizontal', 0.5);
|
|
135
|
+
expect(node.scaleX).toBe(0.5);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('setGuiVisible', () => {
|
|
140
|
+
it('sets visibility directly by default', () => {
|
|
141
|
+
const runtime = createGuiControllerRuntime({});
|
|
142
|
+
const node = createGuiTestNode();
|
|
143
|
+
setGuiVisible(runtime, node, false);
|
|
144
|
+
expect(node.visible).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('setGuiVisualProperty', () => {
|
|
149
|
+
it('offers intermediate writes to an opt-in transition', () => {
|
|
150
|
+
const node = createGuiTestNode();
|
|
151
|
+
const runtime = createGuiControllerRuntime({}, { run: (request) => request.apply(0.5) });
|
|
152
|
+
setGuiVisualProperty(runtime, node, 'alpha', 0);
|
|
153
|
+
expect(node.alpha).toBe(0.5);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('snapGuiValue', () => {
|
|
158
|
+
it('snaps around the minimum', () => {
|
|
159
|
+
expect(snapGuiValue(3.1, 1, 2)).toBe(3);
|
|
160
|
+
expect(snapGuiValue(3.1, 1, null)).toBe(3.1);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { build } from 'esbuild';
|
|
4
|
+
|
|
5
|
+
describe('GUI controller tree shaking', () => {
|
|
6
|
+
it('does not pull the catalog or tween runtime into a Button-only bundle', async () => {
|
|
7
|
+
const result = await build({
|
|
8
|
+
bundle: true,
|
|
9
|
+
format: 'esm',
|
|
10
|
+
logLevel: 'silent',
|
|
11
|
+
minify: true,
|
|
12
|
+
packages: 'external',
|
|
13
|
+
stdin: {
|
|
14
|
+
contents: `export { createButtonController } from './contract.ts';`,
|
|
15
|
+
resolveDir: fileUrlDirectory(import.meta.url),
|
|
16
|
+
sourcefile: 'tree-shake-button.ts',
|
|
17
|
+
},
|
|
18
|
+
treeShaking: true,
|
|
19
|
+
write: false,
|
|
20
|
+
});
|
|
21
|
+
const output = result.outputFiles[0].text;
|
|
22
|
+
expect(output).toContain('createButtonController');
|
|
23
|
+
expect(output).not.toContain('setInterval');
|
|
24
|
+
expect(output).not.toContain('onExpandChange');
|
|
25
|
+
expect(output).not.toContain('createTextInputManager');
|
|
26
|
+
expect(output).not.toContain('@flighthq/tween');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function fileUrlDirectory(url: string): string {
|
|
31
|
+
const directory = new URL('.', url);
|
|
32
|
+
const pathname = decodeURIComponent(directory.pathname);
|
|
33
|
+
if (directory.hostname !== '') return `//${directory.hostname}${pathname}`;
|
|
34
|
+
return /^\/[A-Za-z]:\//.test(pathname) ? pathname.slice(1) : pathname;
|
|
35
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { connectSignal } from '@flighthq/signals/contract';
|
|
2
|
+
|
|
3
|
+
import { createGuiTestNode, emitGuiKeyboard, emitGuiPointer } from './guiTestHelper';
|
|
4
|
+
import {
|
|
5
|
+
createListController,
|
|
6
|
+
disposeListController,
|
|
7
|
+
getListControllerSelectedIndex,
|
|
8
|
+
getListControllerSignals,
|
|
9
|
+
setListControllerSelectedIndex,
|
|
10
|
+
setListControllerVisible,
|
|
11
|
+
} from './listController';
|
|
12
|
+
|
|
13
|
+
describe('createListController', () => {
|
|
14
|
+
it('selects and activates item visuals', () => {
|
|
15
|
+
const items = [createGuiTestNode(), createGuiTestNode()];
|
|
16
|
+
const controller = createListController({ content: createGuiTestNode(), items, viewport: createGuiTestNode() });
|
|
17
|
+
const activations: number[] = [];
|
|
18
|
+
connectSignal(getListControllerSignals(controller).onActivate, (index) => activations.push(index));
|
|
19
|
+
emitGuiPointer(items[1], 'onClick');
|
|
20
|
+
emitGuiPointer(items[1], 'onDoubleClick');
|
|
21
|
+
expect(getListControllerSelectedIndex(controller)).toBe(1);
|
|
22
|
+
expect(activations).toEqual([1]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('supports hierarchical-independent flat keyboard navigation', () => {
|
|
26
|
+
const viewport = createGuiTestNode();
|
|
27
|
+
const controller = createListController({
|
|
28
|
+
content: createGuiTestNode(),
|
|
29
|
+
items: [createGuiTestNode(), createGuiTestNode()],
|
|
30
|
+
viewport,
|
|
31
|
+
});
|
|
32
|
+
emitGuiKeyboard(viewport, 'onKeyDown', 'ArrowDown');
|
|
33
|
+
expect(getListControllerSelectedIndex(controller)).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('disposeListController', () => {
|
|
38
|
+
it('detaches item input', () => {
|
|
39
|
+
const item = createGuiTestNode();
|
|
40
|
+
const controller = createListController({
|
|
41
|
+
content: createGuiTestNode(),
|
|
42
|
+
items: [item],
|
|
43
|
+
viewport: createGuiTestNode(),
|
|
44
|
+
});
|
|
45
|
+
disposeListController(controller);
|
|
46
|
+
emitGuiPointer(item, 'onClick');
|
|
47
|
+
expect(getListControllerSelectedIndex(controller)).toBe(-1);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('getListControllerSelectedIndex', () => {
|
|
52
|
+
it('normalizes invalid initial values', () => {
|
|
53
|
+
expect(
|
|
54
|
+
getListControllerSelectedIndex(
|
|
55
|
+
createListController({
|
|
56
|
+
content: createGuiTestNode(),
|
|
57
|
+
items: [],
|
|
58
|
+
selectedIndex: 2,
|
|
59
|
+
viewport: createGuiTestNode(),
|
|
60
|
+
}),
|
|
61
|
+
),
|
|
62
|
+
).toBe(-1);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('getListControllerSignals', () => {
|
|
67
|
+
it('returns stable signals', () => {
|
|
68
|
+
const controller = createListController({ content: createGuiTestNode(), items: [], viewport: createGuiTestNode() });
|
|
69
|
+
expect(getListControllerSignals(controller)).toBe(getListControllerSignals(controller));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('setListControllerSelectedIndex', () => {
|
|
74
|
+
it('emits only real changes', () => {
|
|
75
|
+
const controller = createListController({
|
|
76
|
+
content: createGuiTestNode(),
|
|
77
|
+
items: [createGuiTestNode()],
|
|
78
|
+
viewport: createGuiTestNode(),
|
|
79
|
+
});
|
|
80
|
+
const values: number[] = [];
|
|
81
|
+
connectSignal(getListControllerSignals(controller).onSelect, (index) => values.push(index));
|
|
82
|
+
setListControllerSelectedIndex(controller, 0);
|
|
83
|
+
setListControllerSelectedIndex(controller, 0);
|
|
84
|
+
expect(values).toEqual([0]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('setListControllerVisible', () => {
|
|
89
|
+
it('changes both list-owned references without creating visuals', () => {
|
|
90
|
+
const content = createGuiTestNode();
|
|
91
|
+
const viewport = createGuiTestNode();
|
|
92
|
+
const controller = createListController({ content, items: [], viewport });
|
|
93
|
+
setListControllerVisible(controller, false);
|
|
94
|
+
expect([content.visible, viewport.visible]).toEqual([false, false]);
|
|
95
|
+
});
|
|
96
|
+
});
|