Skip to content

Latest commit

 

History

History
334 lines (325 loc) · 30.2 KB

File metadata and controls

334 lines (325 loc) · 30.2 KB

CNA Input — NOXNA Extension Implementation Progress

Tracks the autonomous implementation of input_noxna.md. One task = one commit, never batched. Each task: implement (SDL3-only, NOXNA/EXT-tagged, no SDL leak in public headers), build, test (ctest -L input green + ASan-clean where behavior changes), record here + commit + push. If a task needs a decision only the owner can make, SKIP it and note why (owner is away).

Legend

  • [ ] not started · [~] in progress · [x] done · [!] skipped (needs owner input) / hardware-gated

Phase P1 — pure/deterministic, broadly supported, headless-testable

  • N-001 CNA::Input::Clipboard — text Get/Set/Has (SDL_GetClipboardText/SetClipboardText/HasClipboardText).
  • N-002 Keyboard scancode-name helpers EXTGetScancodeNameEXT/GetScancodeFromNameEXT (physical, layout-independent).
  • N-002b Keyboard keycode-name helpers EXTGetKeyNameEXT/GetKeyFromNameEXT (layout-dependent).
  • N-003 Keyboard::GetModStateEXT + KeyModifiersEXT — modifier flags (Shift/Ctrl/Alt/Gui + Caps/Num/Scroll/Mode) via SDL_GetModState seam.
  • [!] N-004 Mouse cursor visibility EXTSKIPPED (superseded, would conflict). CNA Game::IsMouseVisible (Game.hpp:128) already owns cursor visibility and calls SDL_ShowCursor()/SDL_HideCursor() with its own cached IsMouseVisible_. A Mouse::SetCursorVisibleEXT would be a second path to the same global SDL state and desync Game's cache. Game.IsMouseVisible is the XNA-idiomatic API — do not duplicate. (Engineering decision, not an owner question.)
  • N-005 Mouse horizontal scroll wheel EXT — surface SDL wheel.x (currently dropped, DEC-18).
  • N-006 TouchLocation::getPressureEXT — expose SDL finger pressure (XNA dropped Pressure).

Phase P2 — needs an injectable seam, desktop-strong

  • N-007 CNA::Input::Joysticks — raw joystick (axes/buttons/hats/balls); tested via injectable ISdlJoystickBackend fake.
  • N-008 GamePad touchpad fingers EXTGetTouchpadCountEXT/GetTouchpadFingerCountEXT/GetTouchpadFingerEXT (poll-based).
  • N-009 GamePad player-index EXTGet/SetPlayerIndexEXT (SDL device player-number LED).
  • N-009b GamePad battery/power EXTGetPowerInfoEXT (SDL_GetGamepadPowerInfo) + shared CNA::Input::PowerStateEXT.
  • N-010 GamePad metadata EXTGet{Name,Path,Serial,FirmwareVersion,SteamHandle}EXT.
  • N-010b GamePad connection-state EXTGetConnectionStateEXT -> {Wired,Wireless,Unknown}.
  • N-011 GamePad button labels EXTGetButtonLabelEXT (ABXY vs cross/circle/square/triangle).
  • [!] N-012 CNA::Input::PenCANCELLED (2026-07-07, owner request). Stylus support (pressure/ tilt/rotation/eraser/buttons) will not be implemented in this pass. Not an engineering conflict like N-004 — an explicit scope cut so the pass can move straight to N-007/N-013.

Phase P3 — powerful but platform-narrow / manual actuation

  • N-013 CNA::Input::Haptics — SDL_haptic force-feedback (constant/periodic/ramp/condition/custom + gain/autocenter). Real actuation manual [!].
  • N-014 TextInputEXT::TextEditingCandidatesEXT — IME candidate lists (SDL_EVENT_TEXT_EDITING_CANDIDATES). Input-type hints -> N-014b.
  • N-014b TextInputEXT input-type hintsStartTextInputWithTypeEXT(TextInputTypeEXT) (text/URL/email/number/password) via SDL_StartTextInputWithProperties (split off from N-014).
  • N-015 CNA::Input::Sensors — device-level accelerometer/gyro + enumeration (SDL_sensor) via seam.
  • N-016 Mouse capture / global-position EXTSetCaptureEXT, GetGlobalPositionEXT, WarpGlobalEXT via seam.
  • N-017 CNA::Input::InputDevices — enumeration (mice/keyboards/touch id+name) via seam. Hot-plug events -> N-017b.
  • N-017b InputDevices hot-plug eventsMouse/Keyboard Connected/DisconnectedEXT multicast events.
  • N-018 CNA::Input::Power — system battery (SDL_GetPowerInfo) via injectable seam; reuses PowerStateEXT.

Notes

  • New standalone types live in the public CNA::Input namespace (include/CNA/Input/, src/CNA/Input/), NOXNA at the class. NOXNA members on existing XNA types keep the EXT suffix + update the signature-freeze test + docs/input-public-api-frozen.md in the SAME commit.
  • No SDL type may appear in a public header (opaque uintptr_t / internal seam only).
  • Device-query capabilities get an injectable backend + fake (mirror ISdlGamepadBackend); real actuation is [!].
  • CMake auto-globs src/**/*.cpp + tests/**/*.cpp (CONFIGURE_DEPENDS) — reconfigure if a fresh file isn't picked up.
  • Not every device-scoped seam needs SdlInputBridge hot-plug tracking: N-013 Haptics is caller-managed RAII (Haptics::OpenEXT -> HapticDevice, closed on Dispose()/destruction), no bridge event-switch involvement at all (unlike N-007 Joystick's always-open registry). Pick whichever lifecycle model matches how the real device is actually used.

Pass status (2026-07-07)

All planned tasks are resolved: 21 done, N-004 skipped (engineering conflict), N-012 cancelled (owner request). Nothing remains to pick up from this backlog — see NEXT.md before starting new NOXNA work.

Log

(most recent first — filled as tasks complete)

  • N-013 done (2026-07-07): CNA::Input::Haptics + CNA::Input::HapticDevice — SDL3 force-feedback (SDL_haptic), the last task in this pass (N-004 skipped, N-012 cancelled are the only non-done rows now). New enum/descriptor headers: HapticFeatureEXT (bit flags, mirrors SDL_HapticFeatures), HapticDirectionTypeEXT/HapticDirectionEXT (Polar/Cartesian/Spherical/SteeringAxis), HapticEffectTypeEXT (13 effect families), HapticInfoEXT (enumeration id+name), HapticCapabilitiesEXT (features/axes/max-effects/rumble), and HapticEffectEXT — one flattened descriptor covering every SDL effect family (Constant/Sine/Square/Triangle/SawtoothUp/SawtoothDown/ Ramp/Spring/Damper/Inertia/Friction/LeftRight/Custom) instead of six separate tagged-union-mirroring types, since this is NOXNA-only (no FNA fidelity constraint) and a type-discriminated flat struct is far less combinatorial than 6 near-identical types + 6 test suites + 6x the Doxygen. CNA::Input::HapticDevice (whole-class NOXNA, RAII, move-only, System::IDisposable, mirrors MouseCursor's SDL-forward-declared-pointer pattern) wraps an opened SDL_Haptic*: capabilities, simple rumble (Init/Play/Stop), the full effect lifecycle (Create/Update/Run/Stop/Destroy/Status/ StopAll), and Gain/Autocenter/Pause/Resume. CNA::Input::Haptics (static factory/enumerator, like Joysticks/Sensors) opens a device standalone (OpenEXT(id)), from an already-connected raw joystick (OpenFromJoystickEXT, reusing N-007's Joysticks subsystem), or from the mouse (OpenFromMouseEXT) — a new SdlInputBridge::GetOpenedJoystickHandle(id) internal-only accessor (not part of the public CNA::Input surface) bridges N-007's opened-joystick registry to N-013's OpenFromJoystickEXT without leaking SDL_Joystick* into any public header. New, independent seam ISdlHapticBackend (include|src/CNA/Internal/Input/SdlHapticBackend.hpp/.cpp) — unlike the gamepad/joystick seams, haptic devices have no hot-plug lifecycle owned by SdlInputBridge: a HapticDevice is opened/closed explicitly by the caller (RAII), mirroring MouseCursor rather than the always-open gamepad/joystick registries, so no bridge event-switch changes were needed beyond the one accessor. The HapticEffectEXT -> SDL_HapticEffect union conversion lives in HapticDevice.cpp's anonymous namespace (explicit switch-case mapping for both the direction-type and effect-type enums — never relying on numeric ordinal coincidence with SDL's constants, even where they currently match). Whole types NOXNA/additive (no freeze pin, matches Sensors/Power/InputDevices/Joysticks precedent). Tests tests/CNA/Internal/Input/ SdlHapticBackendTests.cpp (new FakeSdlHapticBackend.hpp) — 37 tests: enumeration, open/close (standalone/from-joystick/from-mouse, using N-007's FakeSdlJoystickBackend + a synthetic SDL_EVENT_JOYSTICK_ADDED to get a real opened-joystick handle to open a haptic from), move semantics, capabilities, rumble, the full effect lifecycle, the union conversion for every effect family (including per-axis condition arrays and the Custom raw-sample-buffer path), gain/ autocenter/pause/resume, closed-device safe-defaults for every method, and descriptor-equality + bit-op tests. Caught and fixed one test-double-only bug during this task: the fake's Create/ UpdateHapticEffect initially copied the SDL_HapticEffect union by value (capturing the custom. data raw pointer) without snapshotting the pointee, so introspecting lastCreatedEffect.custom. data after the call returned read freed memory (the caller's local std::vector had already been destroyed) — real SDL copies Custom sample data synchronously during the call so production HapticDevice::CreateEffectEXT/UpdateEffectEXT were never at risk; fixed by having the fake snapshot the buffer into an owned member and repoint .custom.data at that snapshot. Added *Haptic* to CNA_INPUT_TEST_FILTER (CMakeLists.txt). ctest -L input green; ASan-clean (detect_leaks=0, documented libGLX_mesa false positive). Files: HapticFeature.hpp, HapticDirection.hpp, HapticEffectType.hpp, HapticEffect.hpp, HapticInfo.hpp, HapticCapabilities.hpp, HapticDevice.hpp/.cpp, Haptics.hpp/.cpp, SdlHapticBackend.hpp/.cpp, SdlInputBridge.hpp/.cpp (GetOpenedJoystickHandle only), FakeSdlHapticBackend.hpp (new), SdlHapticBackendTests.cpp (new), CMakeLists.txt.
  • N-012 CANCELLED (2026-07-07, owner request): stylus/pen support removed from scope for this pass. Not implemented; no code changes.
  • N-007 done (2026-07-07): CNA::Input::Joysticks — raw joystick access (axes/buttons/hats/ trackballs), independent of GamePad's mapped view of the same hardware. New descriptor/state headers include/CNA/Input/{JoystickType,JoystickHatPosition,JoystickInfo,JoystickCapabilities, JoystickState}.hpp (JoystickTypeEXT mirrors SDL_JoystickType; JoystickHatPositionEXT enumerates the 9 reachable SDL_HAT_* combinations rather than exposing them as flags; JoystickCapabilitiesEXT reuses the shared PowerStateEXT from N-009b/N-018). New, deliberately-separate seam ISdlJoystickBackend (include|src/CNA/Internal/Input/ SdlJoystickBackend.hpp/.cpp) — kept apart from ISdlGamepadBackend per NEXT.md guidance since a gamepad is a mapped joystick and this is the raw device; the same physical device is opened independently through both seams. SdlInputBridge opens every joystick on SDL_EVENT_JOYSTICK_ ADDED into a new std::unordered_map<SDL_JoystickID, SDL_Joystick*> registry (closed on _REMOVED), fires Joysticks::Connected/DisconnectedEXT (mirrors N-017b's direct-invoke style), and does NOT handle AXIS_MOTION/BUTTON_DOWN/UP/HAT_MOTION/BALL_MOTION events at all — SDL's own event pump already updates its internal per-joystick state cache independent of what CNA does with each dequeued event, so GetJoystickState polls SDL_GetJoystickAxis/Button/Hat/Ball live via the seam on every call (same poll-on-demand principle as the GamePad EXT metadata getters). Public CNA::Input::Joysticks (whole-class NOXNA, additive — no freeze pin, matches Sensors/Power/ InputDevices precedent): GetJoysticksEXT(), GetCapabilitiesEXT(id), GetStateEXT(id), Connected/DisconnectedEXT. Tests tests/CNA/Internal/Input/SdlJoystickBackendTests.cpp (new FakeSdlJoystickBackend.hpp) drive the real SdlInputBridge::ProcessEvent path with synthetic SDL_EVENT_JOYSTICK_ADDED/REMOVED: hot-plug open/close/duplicate-add/unknown-remove/open-failure, enumeration, hot-plug events, capabilities (counts/type/name/guid/power) and disconnected-default, state (axes/buttons/hats/balls) and disconnected-default, exhaustive hat-position and joystick-type mapping, plus descriptor-equality tests. Added *Joystick* to CNA_INPUT_TEST_FILTER (CMakeLists.txt) so ctest -L input picks up the new suite. ctest -L input green; ASan-clean (detect_leaks=0 for the documented libGLX_mesa false positive). Files: JoystickType.hpp, JoystickHatPosition.hpp, JoystickInfo.hpp, JoystickCapabilities.hpp, JoystickState.hpp, Joysticks.hpp/.cpp, SdlJoystickBackend.hpp/.cpp, SdlInputBridge.hpp/.cpp, FakeSdlJoystickBackend.hpp (new), SdlJoystickBackendTests.cpp (new), CMakeLists.txt.
  • N-014b done (2026-07-07): TextInputEXT::StartTextInputWithTypeEXT(CNA::Input::TextInputTypeEXT) — input-type hint (text/name/email/username/password-hidden/password-visible/number/number-password- hidden/number-password-visible) for the on-screen keyboard / IME, completing the N-014 split. New enum header include/CNA/Input/TextInputType.hpp (TextInputTypeEXT, mirrors SDL_TextInputType 1:1). No new seam: kept TextInputEXT's existing direct-SDL style (matches StartTextInput) — builds an SDL_PropertiesID via SDL_CreateProperties/SDL_SetNumberProperty(..., SDL_PROP_TEXTINPUT_TYPE_NUMBER, ...), calls SDL_StartTextInputWithProperties, then SDL_DestroyProperties; same null-window guard as StartTextInput (no window handle -> no-op). Enum-to-SDL mapping is a private switch in the .cpp anonymous namespace. Pinned the method in the freeze test + documented in docs/input-public-api-frozen.md (TextInputEXT's whole-class-NOXNA status still means member-level EXT tagging, matching N-014's precedent). Tests: no-window safe- no-op across all 9 hint values, and a real-hidden-window round-trip (Xvfb-gated, GTEST_SKIP fallback like the existing StartStopAndIsActiveRoundTripThroughRealWindow) exercising StartTextInputWithTypeEXT/IsTextInputActive/StopTextInput for all 9 values. ctest -L input green; ASan-clean. Files: TextInputType.hpp (new), TextInputEXT.hpp/.cpp, PublicApiInputSignatureFreezeTests.cpp, input-public-api-frozen.md, TextInputEXTTests.cpp.
  • N-014 done (2026-07-07): TextInputEXT::TextEditingCandidatesEXT — a MulticastAction<const vector<string>&, int, bool> (candidates, selected index, horizontal) for SDL3's IME candidate list (CJK). The bridge decodes SDL_EVENT_TEXT_EDITING_CANDIDATES (event.edit_candidates.*) into UTF-8 std::strings before the SDL event is recycled, then INTERNAL_OnTextEditingCandidates invokes the event (mirrors the existing TextInput/TextEditing dispatch); ResetForTests clears it. Kept it on the existing TextInputEXT class (not a new TextComposition type). Pinned the event in the freeze test (TextInputEXT is pinned) + documented (incl. the INTERNAL dispatcher in the internal list). Tests SdlInputBridgeCandidatesTest feed a synthetic candidates event (incl. a CJK UTF-8 string) + a null-list case. ctest -L input green; ASan-clean. Deferred the StartTextInputWithTypeEXT input-type hint to N-014b.
  • N-017b done (2026-07-06): InputDevices::{Mouse,Keyboard}{Connected,Disconnected}EXT — four System::MulticastAction<uint32_t> hot-plug events carrying the SDL device id (use +=). The bridge's ProcessEvent decodes SDL_EVENT_{MOUSE,KEYBOARD}_{ADDED,REMOVED} (event.mdevice.which/ event.kdevice.which) and Invokes the matching event; InputDevices::ResetForTests clears all four. Additive NOXNA (no freeze pin). Tests CnaInputDevicesHotplugTest feed synthetic device events through ProcessEvent and assert the right event fires with the right id + no cross-firing. ctest -L input green; ASan-clean. Completes the InputDevices feature (enumeration + hot-plug).
  • N-008 done (2026-07-06): GamePad::GetTouchpadCountEXT / GetTouchpadFingerCountEXT(touchpad) / GetTouchpadFingerEXT(touchpad, finger, out down,x,y,pressure) — PS4/PS5-style touchpad finger data, poll-based (like GetGyro/AccelEXT), NOT event-driven, so no InputManager changes. Gamepad-seam pattern #1: added GetNumGamepadTouchpadFingers + GetGamepadTouchpadFinger to ISdlGamepadBackend (real = the matching SDL fns; reused existing GetNumGamepadTouchpads); fake gained FakeTouchpadFinger + FakeGamepadConfig.touchpadFingers[[touchpad]][finger]. Bridge resolves the slot and returns 0/false (out-params reset) when disconnected/out of range. Pinned all three in the freeze test + documented. Tests: counts + finger contact/pos read, out-of-range touchpad/finger, disconnected slot. ctest -L input green; ASan-clean.
  • N-015 done (2026-07-06): CNA::Input::Sensors — host-device motion sensors (distinct from the gamepad gyro/accel EXT). GetSensorsEXT() -> vector<SensorInfoEXT{id,name,SensorTypeEXT}> + GetAccelerometerEXT(out Vector3)/GetGyroscopeEXT(out Vector3) (m/s² / rad/s), returning false when absent. New public header include/CNA/Input/Sensors.hpp (enum SensorTypeEXT + struct SensorInfoEXT w/ ==/!= + class). New injectable ISystemSensorBackend seam (real enumerates SDL_GetSensors, maps SDL_SensorType<->EXT, and ReadSensor does open->SDL_GetSensorData->close on the first matching sensor) with SetSystemSensorBackendForTests. Whole types NOXNA/additive (no freeze pin). Named Sensors (plural, matches the plan's Sensors:: shape + InputDevices style). Tests CnaInputSensorsTest + CnaInputSensorInfoEXTTest drive a fake: enumeration forwarding, accel/gyro sample reads, false-when-absent (out-param untouched), descriptor equality. ctest -L input green; ASan-clean.
  • N-016 done (2026-07-06): Mouse::SetCaptureEXT(bool), GetGlobalPositionEXT(out x,y), WarpGlobalEXT(x,y) — mouse capture + desktop-global cursor position/warp. New injectable ISystemMouseBackend seam (real = SDL_CaptureMouse/SDL_GetGlobalMouseState/SDL_WarpMouse Global) with SetSystemMouseBackendForTests — chosen over the Mouse class's usual direct-SDL + real-window tests so the global ops are deterministically testable headless (no real desktop cursor on CI). GetGlobalPositionEXT truncates SDL's float coords to int (XNA MouseState is int). Pinned all three in the freeze test + documented. Tests MouseGlobalEXTTest (fake seam): capture flag+result forwarding, global position read+truncation, warp coord forwarding+result. ctest -L input green; ASan-clean.
  • N-017 done (2026-07-06): CNA::Input::InputDevices::Get{Mice,Keyboards,TouchDevices}EXT() -> vector<InputDeviceInfoEXT{id,name}> — device enumeration (metadata only; XNA state stays merged). New descriptor include/CNA/Input/InputDeviceInfo.hpp (id+name, with ==/!=) and public class include/CNA/Input/InputDevices.hpp. New injectable ISystemDeviceBackend seam (real = SDL_GetMice/SDL_GetKeyboards/SDL_GetTouchDevices + the *NameForID getters, NULL->"", arrays SDL_free'd) with SetSystemDeviceBackendForTests. Whole types NOXNA/additive (like Clipboard/ Power — no freeze pin). Tests CnaInputDevicesTest + CnaInputDeviceInfoEXTTest drive a fake: per-category forwarding, empty lists, and descriptor equality. ctest -L input green; ASan-clean. Scoped to enumeration; hot-plug ADDED/REMOVED events deferred to N-017b.
  • N-002b done (2026-07-06): Keyboard::GetKeyNameEXT(Keys) -> std::string + GetKeyFromNameEXT(std::string) -> Keys — the layout-dependent (virtual-key) name and its inverse, completing the N-002 name helpers. Bridge resolves Keys -> SDL_Scancode -> (keymap) SDL_GetKeyFromScancode -> SDL_GetKeyName; the inverse is SDL_GetKeyFromName -> try_convert_sdl_key. Unmapped key -> "", unrecognized name -> Keys::None. Pinned in the freeze test + documented. Tests KeyboardKeyNameEXTTest gate on SDL_INIT_VIDEO (the keymap needs it; Xvfb in CI) with GTEST_SKIP, then assert US-layout names (A/Z/Space), empty for None, name<->key round-trip, unrecognized -> None. ctest -L input green; ASan-clean.
  • N-002 done (2026-07-06): Keyboard::GetScancodeNameEXT(Keys) -> std::string + GetScancodeFromNameEXT(std::string) -> Keys — the physical, layout-independent key name and its inverse (for rebind UIs). Reuses the bridge's existing try_convert_keys_to_sdl_scancode / try_convert_sdl_scancode tables around SDL_GetScancodeName / SDL_GetScancodeFromName; unmapped key -> "", unrecognized name -> Keys::None. Keyboard delegates to the bridge (same pattern as the other keyboard EXT). Fully deterministic headless (static SDL tables, no video/ layout). Pinned both in the freeze test + documented. Tests KeyboardScancodeNameEXTTest: stable names (A/Z/Space), empty for None, name<->key round-trip over a key set, unrecognized -> None. ctest -L input green; ASan-clean. Split the plan's N-002 into scancode-names (this commit, deterministic) + N-002b keycode-names (layout-dependent, deferred).
  • N-003 done (2026-07-06): Keyboard::GetModStateEXT() -> CNA::Input::KeyModifiersEXT {None,Shift,Ctrl,Alt,Gui,Caps,Num,Scroll,Mode} — active modifier + lock state. New flags enum header include/CNA/Input/KeyModifiers.hpp (constexpr |,&,~,|=,&= like Buttons/GestureType). New injectable ISystemKeyboardBackend seam (SDL_GetModState) with SetSystemKeyboardBackend ForTests, so tests inject mod state deterministically (SDL's real mod state only updates on real key events). Bridge GetModState collapses SDL's L/R variants (KMOD_SHIFT/CTRL/ALT/GUI) into one flag each + the 4 lock/mode bits; Keyboard delegates (same pattern as GetKeyFromScancodeEXT). Pinned in the freeze test + documented. Tests KeyboardModStateEXTTest (fake seam): None, per-bit mapping incl. L/R collapse, and a combined state. ctest -L input green; ASan-clean.
  • N-006 done (2026-07-06): TouchLocation::getPressureEXT() restores SDL finger pressure (0..1) that XNA 4.0 dropped. Added a NOXNA pressure_ field (default 0) + two NOXNA pressure-carrying ctors (4-arg and 6-arg); the XNA ctors leave it 0. Excluded from Equals/GetHashCode/ToString (kept FNA-frozen — has_frozen_equality still holds). Event path: InternalTouchLocationState gained a Pressure field; InputManager::SetTouchState takes a defaulted float pressure and GetTouchState builds the snapshot via the pressure ctors; the bridge's 3 FINGER_DOWN/MOTION/UP handlers forward event.tfinger.pressure. Pinned the getter + both ctors in the freeze test + documented in docs/input-public-api-frozen.md. Tests: TouchLocation ctor/default + Equals/hash/ ToString-ignore-pressure (TouchInputTests), and end-to-end pressure through GetState with a MOTION update (SdlInputBridgeTouchGestureTests, synthetic finger events). ctest -L input green; ASan-clean. Files: TouchLocation.hpp/.cpp, InputManager.hpp/.cpp, SdlInputBridge.cpp, freeze test, frozen-API doc, TouchInputTests.cpp, SdlInputBridgeTouchGestureTests.cpp.
  • N-018 done (2026-07-06): CNA::Input::Power::GetInfoEXT(out secondsLeft, out percent) -> PowerStateEXT — host system battery/charge, reusing the shared PowerStateEXT from N-009b. Established the "standalone CNA::Input type + injectable system seam" pattern (reusable for N-016/N-017): new ISystemPowerBackend seam (include/CNA/Internal/Input/SystemPowerBackend.hpp
    • .cpp, real = SDL_GetPowerInfo) with SetSystemPowerBackendForTests. Power resets the out-params to -1 then maps SDL->EXT. New public type include/CNA/Input/Power.hpp + src/CNA/ Input/Power.cpp (whole class NOXNA, like Clipboard — additive, so no freeze pin / frozen-doc entry). Tests CnaInputPowerTest drive a fake backend: exhaustive 6-state mapping with seconds/percent forwarding + the out-param-reset default. ctest -L input green; ASan-clean.
  • N-010b done (2026-07-06): GamePad::GetConnectionStateEXT -> CNA::Input:: GamePadConnectionStateEXT {Unknown,Wired,Wireless} — how the pad is attached. New enum header include/CNA/Input/GamePadConnectionState.hpp. Seam GetGamepadConnectionState (real = SDL_GetGamepadConnectionState; fake = config, returns INVALID for an unknown device). Bridge maps SDL {Invalid,Unknown}->Unknown, Wired->Wired, Wireless->Wireless; disconnected slot -> Unknown. Pinned in the freeze test + documented. Tests: 4-state SDL->EXT mapping + disconnected path. ctest -L input green; ASan-clean. Files: GamePadConnectionState.hpp (new), SdlGamepadBackend.hpp/.cpp, FakeSdlGamepadBackend.hpp, SdlInputBridge.hpp/.cpp, GamePad.hpp/.cpp, freeze test, frozen-API doc, SdlGamepadBackendTests.cpp.
  • N-010 done (2026-07-06): GamePad::Get{Name,Path,Serial}EXT -> std::string + GetFirmwareVersionEXT -> uint16 + GetSteamHandleEXT -> uint64 — device metadata via the gamepad seam. Seam gains GetGamepad{Name,Path,Serial,FirmwareVersion,SteamHandle} (real = the matching SDL getters, with NULL->"" for the string ones; fake = canned config). Bridge getters return ""/0 for a disconnected slot. Pinned all five in the freeze test + documented in docs/input-public-api-frozen.md. Tests: canned-value forwarding + disconnected empties. ctest -L input green; ASan-clean. Split the tracker's original N-010 into metadata (this commit) + N-010b connection-state (new enum). Files: SdlGamepadBackend.hpp/.cpp, FakeSdlGamepadBackend.hpp, SdlInputBridge.hpp/.cpp, GamePad.hpp/.cpp, freeze test, frozen-API doc, SdlGamepadBackendTests.cpp.
  • N-011 done (2026-07-06): GamePad::GetButtonLabelEXT(player, Buttons) -> CNA::Input:: GamePadButtonLabelEXT {Unknown,A,B,X,Y,Cross,Circle,Square,Triangle} — the printed glyph for a face button, so UI prompts show the right symbol per controller family. New enum header include/CNA/Input/GamePadButtonLabel.hpp. Seam: ISdlGamepadBackend::GetGamepadButtonLabel (real = SDL_GetGamepadButtonLabel; fake = per-button buttonLabels config map). Bridge adds try_convert_xna_button_to_sdl (public Buttons -> SDL_GamepadButton, inverse of the existing SDL->internal map) + sdl_button_label_to_ext, and GetButtonLabel returns Unknown for a disconnected pad or a non-physical Buttons value (stick dirs/triggers) without touching the device. Pinned in the freeze test + documented in docs/input-public-api-frozen.md. Tests: Xbox glyphs, PlayStation glyphs (full label mapping), and the Unknown paths (non-physical / unlabeled / disconnected). ctest -L input green; ASan-clean. Files: GamePadButtonLabel.hpp (new), SdlGamepadBackend.hpp/.cpp, FakeSdlGamepadBackend.hpp, SdlInputBridge.hpp/.cpp, GamePad.hpp/.cpp, freeze test, frozen-API doc, SdlGamepadBackendTests.cpp.
  • N-009b done (2026-07-06): GamePad::GetPowerInfoEXT(player, out percent) — reads the pad's battery/charge state via the gamepad seam. Introduced the shared public enum CNA::Input::PowerStateEXT {Error,Unknown,OnBattery,NoBattery,Charging,Charged} (include/CNA/Input/PowerState.hpp), mirroring SDL_PowerState; N-018 (system Power) will reuse it. Seam: ISdlGamepadBackend::GetGamepadPowerInfo (real = SDL_GetGamepadPowerInfo; fake = FakeGamepadConfig.powerState/powerPercent). Bridge GetPowerInfo resolves the slot, maps SDL→EXT, and returns Error+percent=-1 when disconnected. Pinned in the freeze test + documented in docs/input-public-api-frozen.md. Tests: exhaustive SDL_PowerState→PowerStateEXT mapping (6 states) with percent round-trip + disconnected→Error. ctest -L input green; ASan-clean. Files: PowerState.hpp (new), SdlGamepadBackend.hpp/.cpp, FakeSdlGamepadBackend.hpp, SdlInputBridge.hpp/.cpp, GamePad.hpp/.cpp, freeze test, frozen-API doc, SdlGamepadBackendTests.cpp.
  • N-009 done (2026-07-06): GamePad::Get/SetPlayerIndexEXT — reads/sets the SDL device player index (the 0-based player-number LED) through the injectable gamepad seam. Established the "gamepad-seam-extension" flow: added GetGamepadPlayerIndex/SetGamepadPlayerIndex to ISdlGamepadBackend (real = SDL_Get/SetGamepadPlayerIndex) + the fake (FakeGamepadConfig.playerIndex + setPlayerIndexCalls/lastSetPlayerIndex introspection); bridge Get/SetPlayerIndex resolve the slot and return -1/false when disconnected; public GamePad::Get/SetPlayerIndexEXT (NOXNA) delegate. Pinned both in PublicApiInputSignatureFreeze Tests + documented in docs/input-public-api-frozen.md. Tests: FakeGamepadTest.PlayerIndex RoundTripsThroughBackend (Get reads device index, Set forwards + Get reads back) + ...IsSafe ForDisconnectedSlot (Get→-1, Set→false, backend untouched). ctest -L input 100% green; ASan-clean. Split the tracker's old N-009 into player-index (this commit) + N-009b battery/power. Files: SdlGamepadBackend.hpp/.cpp, FakeSdlGamepadBackend.hpp, SdlInputBridge.hpp/.cpp, GamePad.hpp/.cpp, freeze test, frozen-API doc, SdlGamepadBackendTests.cpp.
  • N-005 done (2026-07-06): Mouse horizontal scroll wheel EXT (reverses DEC-18's drop of wheel.x). MouseState::getHorizontalScrollWheelValueEXTProperty + a NOXNA 9-arg ctor (8-arg XNA ctor unchanged, leaves it 0); InputManager gained a HorizontalScrollWheelValue accumulator + AddHorizontalScroll WheelDelta; the bridge MOUSE_WHEEL handler now accumulates (int)wheel.x * 120 (same cast-then-scale notch truncation as vertical). Excluded from Equals/GetHashCode so those stay FNA-frozen. Established the "NOXNA-member-on-frozen-type" flow: pinned the getter + 9-arg ctor in PublicApiInputSignatureFreeze Tests, documented both in docs/input-public-api-frozen.md. Tests: rewrote the old HorizontalWheelIs Ignored into independence + 120-notch accumulation + truncation tests, + 3 MouseState ctor/equality tests. ctest -L input 100% green; ASan-clean. Files: MouseState.hpp/.cpp, InputManager.hpp/.cpp, SdlInputBridge.cpp, freeze test, frozen-API doc, 2 test files.
  • N-001 done (2026-07-06): CNA::Input::Clipboard — new public CNA::Input namespace established (include/CNA/Input/Clipboard.hpp, src/CNA/Input/Clipboard.cpp). GetTextEXT/SetTextEXT/HasTextEXT wrap SDL3 clipboard (SDL_free'd read). Tests CnaInputClipboardTest (UTF-8 round-trip + empty) — 2 tests, Xvfb-gated with GTEST_SKIP fallback. Added *CnaInput* to CNA_INPUT_TEST_FILTER (one-time, covers all future CNA::Input suites named CnaInput*). ctest -L input 100% green; ASan-clean. Convention set: new CNA::Input types are NOXNA at the class, methods keep the EXT suffix, test suites are CnaInput<Type>Test.