/* Crafter®.Graphics Copyright (C) 2026 Catcrafts® Catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ export module Crafter.Graphics:Input; import std; import Crafter.Math; import Crafter.Event; import :Keys; import :Gamepad; import :Window; // Named-action input mapping with rebindable bindings. Game code declares // actions ("Jump", "Move", "Fire"), attaches binding sources (keys, mouse // buttons, gamepad sticks, WASD-as-Vector2, etc.), and subscribes to // `onPerformed` / `onCanceled` / `onValueChanged` events. Bindings can be // rebound at runtime via `StartRebind` — the next qualifying input // becomes the new binding. // // Polling-based evaluation: `Map::Tick()` (called once per frame from // game code, AFTER `Gamepad::Tick()`) reads the polled state of the // attached window + connected gamepads and diffs against the action's // previous state. No per-device event listeners — works regardless of // gamepad connect/disconnect timing. // // Bindings store raw platform key codes (the same `KeyCode` domain as // `Window::onRawKeyDown`). Source code uses `Key(CrafterKeys::Space)` // from :Keys to obtain cross-platform default codes at compile time. // Binding files serialized with `BindingToString` are platform-specific // (a Win32 save won't load on Wayland) — document this for callers. export namespace Crafter::Input { enum class ActionType : std::uint8_t { Button, // digital — pressed/released Axis1D, // -1..1 (or 0..1 for triggers / scroll) Vector2, // 2D — sticks, WASD, mouse delta }; // ─── Binding alternatives ─────────────────────────────────────── struct KeyBind { KeyCode code; }; struct MouseButtonBind { std::uint8_t button; }; // 0 = left, 1 = right struct MouseScrollBind { }; // Axis1D, accumulator drained per tick struct MouseDeltaBind { float scale = 1.0f; }; // Vector2 struct GamepadButtonBind { std::uint32_t gamepadId; Gamepad::Button button; }; struct GamepadAxisBind { std::uint32_t gamepadId; Gamepad::Axis axis; bool invert = false; }; struct GamepadStickBind { std::uint32_t gamepadId; Gamepad::Stick stick; }; // Vector2 struct WASDBind { KeyCode up; KeyCode down; KeyCode left; KeyCode right; }; using Binding = std::variant< KeyBind, MouseButtonBind, MouseScrollBind, MouseDeltaBind, GamepadButtonBind, GamepadAxisBind, GamepadStickBind, WASDBind >; struct Action { std::string name; ActionType type; std::vector bindings; // Analog deadzone — bindings below this magnitude read as zero. // Applied per-axis to GamepadAxisBind, radially to // GamepadStickBind. Doesn't apply to Button (digital). float deadzone = 0.15f; // Polled state. Read freely from the listener callback or // anywhere else; updated by `Map::Tick()`. bool pressed = false; float value = 0.0f; Vector vector2{}; // Events fired during `Map::Tick()` when the polled state crosses // an edge or changes value. Event onPerformed; // pressed (Button) / non-zero edge (axis) Event onCanceled; // released (Button) / back to zero Event onValueChanged; // Axis1D — value Event> onVector2Changed; // Vector2 — vector2 }; enum class CaptureMask : std::uint8_t { Keyboard = 1, Mouse = 2, Gamepad = 4, Any = 0xFF, }; constexpr CaptureMask operator|(CaptureMask a, CaptureMask b) { return (CaptureMask)((std::uint8_t)a | (std::uint8_t)b); } constexpr bool HasFlag(CaptureMask m, CaptureMask f) { return ((std::uint8_t)m & (std::uint8_t)f) != 0; } struct Map { std::vector> actions; Action& AddAction(std::string name, ActionType type); Action* Find(std::string_view name); // Subscribe internally to the window's mouse-scroll event so a // single scroll between ticks isn't lost. All other state is // polled from `Window` / `Gamepad`. Detaching releases that one // listener; the polled-state reads automatically stop. void Attach(Window& window); void Detach(); // Apply current input state to all actions. Fire edge / value // events. Call once per frame AFTER `Gamepad::Tick()`. Safe to // call when not attached (no-op). void Tick(); // Capture the next qualifying input as a binding. While capture // is active, that input does NOT dispatch to actions — it only // routes to `onCaptured`. Capturing replaces nothing in the // action's bindings; the callback decides whether to assign, // append, or discard. Cancel via `StopRebind`. void StartRebind(Action& action, CaptureMask mask, std::function onCaptured); void StopRebind(); bool IsRebinding() const; // ─── Internals (exposed because we're a POD-ish struct) ────── Window* window = nullptr; // Mouse scroll is the one event-driven source; everything else // is polled. The listener feeds into `scrollAccumulator`, // drained on the next Tick. std::unique_ptr> scrollListener; std::int32_t scrollAccumulator = 0; // Mouse delta is computed from window.currentMousePos vs the // position observed last tick. nullopt before first tick. std::optional> lastMousePos; // Rebind state. struct RebindState { Action* action; CaptureMask mask; std::function onCaptured; // Snapshot of all input at rebind-start, so we only fire on // a fresh down-edge (not on a button held since before). std::unordered_set keysHeldAtStart; bool mouseLeftHeldAtStart = false; bool mouseRightHeldAtStart = false; std::unordered_map> gamepadButtonsAtStart; }; std::optional rebind; }; // ─── Serialization ────────────────────────────────────────────── // Single-binding text round-trip. Caller composes binding files // however they like (JSON / INI / line-per-binding). Formats: // "key:" — KeyBind // "mb:<0|1>" — MouseButtonBind // "mscroll" — MouseScrollBind // "mdelta:" — MouseDeltaBind // "gpb::