Crafter.Graphics/interfaces/Crafter.Graphics-Input.cppm

180 lines
8 KiB
C++

/*
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<Binding> 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<float, 2> vector2{};
// Events fired during `Map::Tick()` when the polled state crosses
// an edge or changes value.
Event<void> onPerformed; // pressed (Button) / non-zero edge (axis)
Event<void> onCanceled; // released (Button) / back to zero
Event<float> onValueChanged; // Axis1D — value
Event<Vector<float,2>> 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<std::unique_ptr<Action>> 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<void(Binding)> 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<EventListener<std::uint32_t>> 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<Vector<float, 2>> lastMousePos;
// Rebind state.
struct RebindState {
Action* action;
CaptureMask mask;
std::function<void(Binding)> 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<KeyCode> keysHeldAtStart;
bool mouseLeftHeldAtStart = false;
bool mouseRightHeldAtStart = false;
std::unordered_map<std::uint32_t, std::array<bool, (std::size_t)Gamepad::Button::Max>> gamepadButtonsAtStart;
};
std::optional<RebindState> rebind;
};
// ─── Serialization ──────────────────────────────────────────────
// Single-binding text round-trip. Caller composes binding files
// however they like (JSON / INI / line-per-binding). Formats:
// "key:<hex>" — KeyBind
// "mb:<0|1>" — MouseButtonBind
// "mscroll" — MouseScrollBind
// "mdelta:<scale>" — MouseDeltaBind
// "gpb:<id>:<button>" — GamepadButtonBind
// "gpa:<id>:<axis>:<inv>" — GamepadAxisBind (inv = 0/1)
// "gps:<id>:<stick>" — GamepadStickBind
// "wasd:<u>:<d>:<l>:<r>" — WASDBind, four hex KeyCodes
// KeyCode hex values are platform-specific; see :Keys.
std::string BindingToString(const Binding&);
std::optional<Binding> BindingFromString(std::string_view);
}