move pan input into an InputMapper

First slice of pulling key handling out of GameWorldView. The widget no longer
holds A/D key state; the mapper owns it and publishes the resulting direction as
PanDirectionChangedEvent, which the view consumes like any other event.

The event is level-triggered on purpose — the payload is the complete current
direction, PanDirection::None included — so a receiver never reconstructs state
from edges and cannot be left panning by a missing key-up. It is also the one
place in the UI where caching an event payload is right rather than wrong: input
has no other authority to re-read from, so the mapper is the source of truth.
Both points are written down on the event, since they look like violations of
the surrounding conventions otherwise.

Holding the state in one object is what makes releaseAll() possible; restart
uses it, and it is what a focusOutEvent will call to fix the stuck-pan bug in a
follow-up.

Bindings stay hard-coded. Only the ownership moved, so behaviour is unchanged,
including both keys held cancelling out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
2026-08-05 13:35:23 +02:00
parent b0fffdb00f
commit 1748be57fb
7 changed files with 168 additions and 28 deletions

View File

@@ -16,6 +16,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/BuilderModeExitedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintModeExitedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/PanDirectionChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructModeChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTypeSelectedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildHotkeyPressedEvent.h

View File

@@ -0,0 +1,22 @@
#pragma once
#include "Event.h"
#include "WorldCamera.h"
// The direction the player is currently panning the view has changed
// (REQ-UI-SCROLL). Deliberately level-triggered: the payload is the complete
// current direction, including PanDirection::None when panning stops, rather than
// separate started/stopped events. A receiver that only ever saw edges would have
// to reconstruct the state and would be left panning forever if one edge went
// missing — which is exactly what happens when the widget loses focus mid-pan.
//
// Unlike the state-change events elsewhere in the UI, the receiver does cache this
// payload instead of re-reading the value from somewhere authoritative. That is
// correct here: input has no other source of truth to re-read from, so the
// publisher (InputMapper) is the authority and the payload is the value.
class PanDirectionChangedEvent : public Event
{
public:
explicit PanDirectionChangedEvent(PanDirection direction) : direction(direction) {}
const PanDirection direction;
};