Skip to content

Commit 40bccbe

Browse files
author
DavidQ
committed
Fix Input Mapping V2 gesture selection and used-input highlights - PR_26140_105-fix-input-mapping-v2-gesture-selection-and-highlights
1 parent 673c9e3 commit 40bccbe

7 files changed

Lines changed: 306 additions & 58 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Input Mapping V2 Gesture Selection + Highlights Report
2+
3+
PR: PR_26140_105-fix-input-mapping-v2-gesture-selection-and-highlights
4+
5+
## Source of Truth
6+
7+
- Read `docs/dev/PROJECT_INSTRUCTIONS.md` before implementation.
8+
- Active `docs/pr/BUILD_PR.md` is an unrelated Level 18 rebase note, so this report follows the explicit PR105 user request as the current BUILD scope.
9+
- One PR purpose: fix Input Mapping V2 gesture selection, selected-input highlighting, and remove the visible Cross-device gesture group while preserving cross-device combo capture internally.
10+
11+
## Changes
12+
13+
- Added selected gesture state to Input Mapping V2.
14+
- Gesture buttons now stay visibly highlighted with `is-selected` and `aria-pressed="true"` after selection and after capture completion.
15+
- Selected descriptor gestures are applied to the next matching live capture:
16+
- Keyboard Release/Hold/Press update captured keyboard mapping text and binding.
17+
- Mouse Click/Double Click update captured mouse mapping text and binding.
18+
- Game Controller Button/Trigger/Stick/DPad update captured gamepad mapping text and binding when selected.
19+
- Keyboard Release now captures as visible tile text such as `Keyboard KeyP Release` instead of falling back to `Keyboard KeyP Press`.
20+
- Non-default gesture bindings include the gesture id, for example `KeyP:KeyboardRelease`, so Press and Release for the same physical key remain distinct without schema changes.
21+
- Preserved existing immediate behavior for pointer drag and wheel gesture buttons.
22+
- Preserved combo capture behavior, including keyboard, mouse, wheel, game controller, and cross-device combinations.
23+
- Removed `Cross-device` as a visible gesture group and removed the `CrossDeviceCombo` visible descriptor.
24+
- Kept cross-device combo support available through the existing Combo gesture flow, including Game Controller Combo capturing a controller button plus keyboard Alt.
25+
- Preserved haptics/rumble UI behavior, shortcut suppression, context suppression, selected tile indication, and gamepad auto-poll behavior.
26+
27+
## ToolState / Schema
28+
29+
- No schemas changed.
30+
- No sample JSON changed.
31+
- Input Mapping V2 continues to use the existing tool-local mapping/toolState contract.
32+
33+
## Playwright Impact
34+
35+
Playwright impacted: Yes.
36+
37+
Focused coverage now validates:
38+
39+
- selected gesture remains highlighted,
40+
- Keyboard Release capture produces `Keyboard KeyP Release`,
41+
- selected tile highlights corresponding keyboard input,
42+
- selected tile highlights corresponding mouse input,
43+
- selected tile highlights corresponding game controller input,
44+
- Cross-device group and `CrossDeviceCombo` button are not visible,
45+
- cross-device combo representation still works through Game Controller Combo without a Cross-device UI group.
46+
47+
Expected pass behavior: gesture selection is visible and affects the next matching capture; used inputs are highlighted for the selected tile; Cross-device is absent from visible gestures; cross-device combo still maps internally.
48+
49+
Expected fail behavior: selected gestures lose highlighting, Keyboard Release maps as Press, used controls do not highlight for the selected tile, Cross-device appears as a visible group, or cross-device combo cannot be represented without that group.
50+
51+
## Validation
52+
53+
Passed:
54+
55+
- `node --check src/engine/input/InputCapabilityDescriptors.js`
56+
- `node --check tools/input-mapping-v2/js/ToolStarterApp.js`
57+
- `node --check tools/input-mapping-v2/js/controls/GestureListControl.js`
58+
- `node --check tools/input-mapping-v2/js/services/EngineInputSourceService.js`
59+
- `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
60+
- `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs -g "Input Mapping V2" --project=playwright --workers=1 --reporter=list` - 3 passed.
61+
- `npm run test:workspace-v2` - 62 passed.
62+
- `git diff --check` - passed; Git reported LF-to-CRLF working-copy warnings only.
63+
- `git status --short -- samples games` - no sample/game JSON changes.
64+
- `rg "imageDataUrl" tools/input-mapping-v2 src/engine/input tests/playwright/tools/WorkspaceManagerV2.spec.mjs` - no matches.
65+
66+
Coverage:
67+
68+
- Playwright V8 coverage was produced at `docs/dev/reports/playwright_v8_coverage_report.txt`.
69+
- `docs/dev/reports/coverage_changed_js_guardrail.txt` reports no changed runtime JS coverage warnings.
70+
71+
Skipped:
72+
73+
- Full samples smoke test was skipped by explicit PR instruction.
74+
75+
## Manual Validation
76+
77+
1. Open Input Mapping V2.
78+
2. Add or select a `Pause` mapping tile.
79+
3. Click `Keyboard > Release` and confirm the Release gesture remains highlighted.
80+
4. Click `Capture Keyboard`, press `P`, and confirm the Pause tile shows `Keyboard KeyP Release`.
81+
5. Select the Pause tile and confirm the Keyboard used-input display highlights `Keyboard KeyP Release`.
82+
6. Add mouse and game controller mappings, select each tile, and confirm the matching Mouse/Game Controller used-input displays are highlighted.
83+
7. Confirm no visible `Cross-device` gesture group appears.
84+
8. Start a combo from an existing device Combo gesture and confirm a controller button + keyboard Alt can still map as a combo.
85+
9. Confirm haptics/rumble controls and shortcut/context suppression still behave as before.
86+
87+
## Files Changed
88+
89+
- `src/engine/input/InputCapabilityDescriptors.js`
90+
- `tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
91+
- `tools/input-mapping-v2/js/ToolStarterApp.js`
92+
- `tools/input-mapping-v2/js/controls/GestureListControl.js`
93+
- `tools/input-mapping-v2/js/services/EngineInputSourceService.js`
94+
- `tools/input-mapping-v2/styles/inputMappingV2.css`

src/engine/input/InputCapabilityDescriptors.js

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,7 @@ const GESTURE_DEFINITIONS = Object.freeze([
8282
gameControllerGesture('GameControllerTrigger', 'Trigger', 'Game controller trigger. Use for analog trigger actions such as accelerate, brake, or charge.', 'gameController'),
8383
gameControllerGesture('GameControllerStick', 'Stick', 'Game controller stick. Use for analog movement, aiming, or steering.', 'gameController'),
8484
gameControllerGesture('GameControllerDPad', 'DPad', 'Game controller DPad. Use for directional menu or movement actions.', 'gameController'),
85-
comboGesture('GameControllerCombo', 'Game Controller', ['gameController'], 'Game Controller Combo. Use for controller combinations such as Button 1 + Button 2.'),
86-
crossDeviceComboGesture()
85+
comboGesture('GameControllerCombo', 'Game Controller', ['gameController'], 'Game Controller Combo. Use for controller combinations such as Button 1 + Button 2. Cross-device combinations such as Joystick Button 1 + Keyboard Alt are supported during combo capture.')
8786
]);
8887

8988
export function inputDeviceCapabilities({
@@ -259,22 +258,6 @@ function comboGesture(binding, deviceLabel, requiredDeviceIds, title) {
259258
};
260259
}
261260

262-
function crossDeviceComboGesture() {
263-
return {
264-
binding: 'CrossDeviceCombo',
265-
captureKind: 'combo',
266-
deviceLabel: 'Cross-device',
267-
displayLabelLines: ['Cross-device', 'Combo'],
268-
engine: 'InputService Combo',
269-
label: 'Combo',
270-
anyOfDeviceIds: ['keyboard', 'mouse', 'gameController'],
271-
minimumEnabledDevices: 2,
272-
requiredDeviceIds: [],
273-
source: 'keyboard',
274-
title: 'Cross-device Combo. Use for combinations such as Joystick Button 1 + Keyboard Alt. Capture any two keyboard, mouse, wheel, or game controller inputs for one selected action.'
275-
};
276-
}
277-
278261
function patternGesture({ binding, deviceLabel, label, title, deviceId }) {
279262
return {
280263
binding,

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1459,6 +1459,111 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14591459
}
14601460
});
14611461

1462+
test("selects Input Mapping V2 gestures for capture and highlights used controls", async ({ page }) => {
1463+
await page.setViewportSize({ width: 1600, height: 900 });
1464+
const pageErrors = [];
1465+
page.on("pageerror", (error) => {
1466+
pageErrors.push(error.message);
1467+
});
1468+
await page.addInitScript(() => {
1469+
window.__inputMappingV2MockGamepads = [
1470+
{
1471+
axes: [0, 0, 0, 0],
1472+
buttons: [{ pressed: false }, { pressed: false }],
1473+
connected: true,
1474+
id: "Arcade Joystick",
1475+
index: 0,
1476+
mapping: "",
1477+
timestamp: 10
1478+
}
1479+
];
1480+
Object.defineProperty(navigator, "getGamepads", {
1481+
configurable: true,
1482+
value: () => window.__inputMappingV2MockGamepads
1483+
});
1484+
});
1485+
const server = await openInputMappingV2(page);
1486+
try {
1487+
await expect(page.locator("body[data-tool-id='input-mapping-v2']")).toBeVisible();
1488+
await expect(page.locator(".input-mapping-v2__gesture-group", { hasText: "Cross-device" })).toHaveCount(0);
1489+
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='CrossDeviceCombo']")).toHaveCount(0);
1490+
1491+
await page.locator("#inputMappingV2ActionSelect").selectOption("pause");
1492+
await page.locator("#inputMappingV2AddActionButton").click();
1493+
const keyboardReleaseButton = page.locator(".input-mapping-v2__gesture-group", { hasText: "Keyboard" })
1494+
.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='KeyboardRelease']");
1495+
await keyboardReleaseButton.click();
1496+
await expect(keyboardReleaseButton).toHaveClass(/is-selected/);
1497+
await expect(keyboardReleaseButton).toHaveAttribute("aria-pressed", "true");
1498+
await page.locator("#inputMappingV2CaptureKeyboardButton").click();
1499+
await page.keyboard.press("KeyP");
1500+
await expect(keyboardReleaseButton).toHaveClass(/is-selected/);
1501+
await expect(page.locator(".input-mapping-v2__mapping-card[data-input-mapping-tile-action-id='pause']")).toContainText("Keyboard KeyP Release");
1502+
await expect(page.locator(".input-mapping-v2__mapping-card[data-input-mapping-tile-action-id='pause']")).not.toContainText("Keyboard KeyP Press");
1503+
await expect(page.locator("#inspectorOutput")).toContainText('"binding": "KeyP:KeyboardRelease"');
1504+
await expect(page.locator("#inputMappingV2CaptureKeyboardButton")).toHaveClass(/has-used-input/);
1505+
await expect(page.locator(".input-mapping-v2__used-source[data-input-mapping-used-source-id='keyboard'] .input-mapping-v2__used-control", { hasText: "Keyboard KeyP Release" })).toHaveCount(1);
1506+
1507+
await page.locator("#inputMappingV2ActionSelect").selectOption("fire");
1508+
await page.locator("#inputMappingV2AddActionButton").click();
1509+
await page.locator(".input-mapping-v2__gesture-group", { hasText: "Mouse" })
1510+
.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='MouseClick']")
1511+
.click();
1512+
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='MouseClick']")).toHaveClass(/is-selected/);
1513+
await page.locator("#inputMappingV2CaptureMouseButton").click();
1514+
await page.evaluate(() => {
1515+
window.dispatchEvent(new MouseEvent("mousedown", {
1516+
bubbles: true,
1517+
button: 2,
1518+
cancelable: true
1519+
}));
1520+
});
1521+
await expect(page.locator(".input-mapping-v2__mapping-card[data-input-mapping-tile-action-id='fire']")).toContainText("Mouse Right Button Click");
1522+
await expect(page.locator("#inputMappingV2CaptureMouseButton")).toHaveClass(/has-used-input/);
1523+
await expect(page.locator(".input-mapping-v2__used-source[data-input-mapping-used-source-id='mouse'] .input-mapping-v2__used-control", { hasText: "Mouse Right Button Click" })).toHaveCount(1);
1524+
1525+
await page.locator("#inputMappingV2ActionSelect").selectOption("start");
1526+
await page.locator("#inputMappingV2AddActionButton").click();
1527+
await page.locator(".input-mapping-v2__gesture-group", { hasText: "Game Controller" })
1528+
.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='GameControllerButton']")
1529+
.click();
1530+
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='GameControllerButton']")).toHaveClass(/is-selected/);
1531+
await page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='0']").click();
1532+
await page.evaluate(() => {
1533+
window.__inputMappingV2MockGamepads[0] = {
1534+
...window.__inputMappingV2MockGamepads[0],
1535+
buttons: [{ pressed: false }, { pressed: true }],
1536+
timestamp: 11
1537+
};
1538+
});
1539+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='0']")).not.toHaveClass(/is-capturing/, { timeout: 2000 });
1540+
await expect(page.locator(".input-mapping-v2__mapping-card[data-input-mapping-tile-action-id='start']")).toContainText("Game Controller Button 1 Button");
1541+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='0']")).toHaveClass(/has-used-input/);
1542+
await expect(page.locator(".input-mapping-v2__used-source[data-input-mapping-used-source-id='gameController'] .input-mapping-v2__used-control", { hasText: "Game Controller Button 1 Button" })).toHaveCount(1);
1543+
1544+
await page.locator(".input-mapping-v2__gesture-group", { hasText: "Game Controller" })
1545+
.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='GameControllerCombo']")
1546+
.click();
1547+
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='GameControllerCombo']")).toHaveClass(/is-selected/);
1548+
await expect(page.locator("#inputMappingV2CaptureMessage")).toContainText("Combo capture recorded Game Controller Button 1");
1549+
await page.evaluate(() => {
1550+
window.dispatchEvent(new KeyboardEvent("keydown", {
1551+
altKey: true,
1552+
bubbles: true,
1553+
cancelable: true,
1554+
code: "AltLeft",
1555+
key: "Alt"
1556+
}));
1557+
});
1558+
await expect(page.locator(".input-mapping-v2__mapping-card[data-input-mapping-tile-action-id='start']")).toContainText("Combo Game Controller Button 1 + Alt");
1559+
await expect(page.locator("#inspectorOutput")).toContainText('"binding": "Combo:Pad0:Button1+AltLeft"');
1560+
expect(pageErrors).toEqual([]);
1561+
} finally {
1562+
await workspaceV2CoverageReporter.stop(page);
1563+
await server.close();
1564+
}
1565+
});
1566+
14621567
test("launches Input Mapping V2 and captures keyboard mappings", async ({ page }) => {
14631568
await page.setViewportSize({ width: 1920, height: 900 });
14641569
const server = await openInputMappingV2(page);
@@ -1526,14 +1631,15 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15261631
await expect(page.locator(".input-mapping-v2__gesture-group", { hasText: "Mouse" })).toContainText("Wheel Up");
15271632
const gestureGroupTitles = await page.locator(".input-mapping-v2__gesture-group strong").allTextContents();
15281633
expect(gestureGroupTitles).not.toContain("Wheel");
1529-
expect(gestureGroupTitles).toContain("Cross-device");
1634+
expect(gestureGroupTitles).not.toContain("Cross-device");
1635+
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='CrossDeviceCombo']")).toHaveCount(0);
15301636
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='MousePrimaryDrag']")).toHaveAttribute("title", /continuous movement while held/);
15311637
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='MousePrimaryDragRelease']")).toHaveAttribute("title", /box-selecting objects/);
15321638
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='MouseWheelUp']")).toHaveAttribute("title", /zoom, scrolling, or cycling selections/);
15331639
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='KeyboardCombo']")).toHaveAttribute("title", /Ctrl \+ R/);
15341640
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='MouseCombo']")).toHaveAttribute("title", /Shift \+ Mouse Right Button/);
15351641
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='GameControllerCombo']")).toHaveAttribute("title", /Button 1 \+ Button 2/);
1536-
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='CrossDeviceCombo']")).toHaveAttribute("title", /Joystick Button 1 \+ Keyboard Alt/);
1642+
await expect(page.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='GameControllerCombo']")).toHaveAttribute("title", /Joystick Button 1 \+ Keyboard Alt/);
15371643
const compactAccordionSpacing = await page.locator("#gestureSetupContent, #captureInputContent").evaluateAll((contents) => (
15381644
contents.map((content) => ({
15391645
columnGap: getComputedStyle(content).columnGap,
@@ -2013,7 +2119,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
20132119
timestamp: 43.5
20142120
};
20152121
});
2016-
await page.locator(".input-mapping-v2__gesture-group", { hasText: "Cross-device" }).locator(".input-mapping-v2__gesture-button", { hasText: "Combo" }).click();
2122+
await page.locator(".input-mapping-v2__gesture-group", { hasText: "Game Controller" }).locator(".input-mapping-v2__gesture-button", { hasText: "Combo" }).click();
20172123
await expect(page.locator("#inputMappingV2CaptureMessage")).toContainText("Combo capture recorded Game Controller Button 1");
20182124
await page.evaluate(() => {
20192125
window.dispatchEvent(new KeyboardEvent("keydown", {

0 commit comments

Comments
 (0)