Sign in Sign up
kretrod/lodestone-cpp Public
Branches
main
134 lines (122 loc) · 4.9 KB Raw
// Window-procedure hook: turns Win32 messages into framework-neutral UiInput.
//
// While the menu is open we swallow input instead of forwarding it, so the game
// never sees the clicks and keystrokes meant for the menu — no camera spin
// while you drag a slider. Everything else is passed straight through.
#pragma once

#include <windows.h>

#include "state.hpp"

namespace lodestone::input {

/// The window procedure GLFW installed, which we chain to.
inline LONG_PTR g_original = 0;

inline LRESULT chain(HWND hwnd, UINT msg, WPARAM w, LPARAM l) {
    if (!g_original) return 0;
    return CallWindowProcW(reinterpret_cast<WNDPROC>(g_original), hwnd, msg, w, l);
}

inline void push(const state::UiInput& ev) {
    state::with([&](state::Shared& s) { s.events.push_back(ev); });
}

/// Translate one message into UiInput. Returns true when the game should not
/// also see it.
inline bool feed(UINT msg, WPARAM w, LPARAM l) {
    using K = state::UiInput::Kind;
    state::UiInput ev{};
    switch (msg) {
        case WM_MOUSEMOVE:
            ev.kind = K::MouseMove;
            ev.x = static_cast<float>(static_cast<short>(LOWORD(l)));
            ev.y = static_cast<float>(static_cast<short>(HIWORD(l)));
            push(ev);
            return true;
        case WM_LBUTTONDOWN:
        case WM_LBUTTONUP:
        case WM_RBUTTONDOWN:
        case WM_RBUTTONUP:
        case WM_MBUTTONDOWN:
        case WM_MBUTTONUP:
            ev.kind = K::MouseButton;
            ev.button = (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONUP)   ? 0
                        : (msg == WM_RBUTTONDOWN || msg == WM_RBUTTONUP) ? 1
                                                                         : 2;
            ev.down = (msg == WM_LBUTTONDOWN || msg == WM_RBUTTONDOWN || msg == WM_MBUTTONDOWN);
            push(ev);
            return true;
        case WM_MOUSEWHEEL:
            ev.kind = K::Wheel;
            ev.y = static_cast<float>(GET_WHEEL_DELTA_WPARAM(w)) / static_cast<float>(WHEEL_DELTA);
            push(ev);
            return true;
        case WM_CHAR:
            if (w >= 32 && w != 127) {
                ev.kind = K::Char;
                ev.codepoint = static_cast<unsigned int>(w);
                push(ev);
            }
            return true;
        case WM_KEYDOWN:
        case WM_SYSKEYDOWN:
        case WM_KEYUP:
        case WM_SYSKEYUP:
            ev.kind = K::Key;
            ev.vk = static_cast<uint32_t>(w);
            ev.down = (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN);
            push(ev);
            return true;
        default:
            return false;
    }
}

inline LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM w, LPARAM l) {
    if (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN) {
        // A rebind in progress captures the next key first — including a new
        // menu key — so it is never mistaken for a toggle or a module bind.
        bool binding = state::with([](state::Shared& s) { return s.binding_active; });
        if (binding) {
            state::with([&](state::Shared& s) { s.handle_key(static_cast<uint32_t>(w)); });
            return 0;
        }
        // The menu key (rebindable, Insert by default) is never forwarded.
        uint32_t menu_key = state::with([](state::Shared& s) { return s.cfg.menu_key; });
        if (static_cast<uint32_t>(w) == menu_key) {
            state::with([](state::Shared& s) {
                s.menu_open = !s.menu_open;
                if (!s.menu_open) s.events.clear();
            });
            return 0;
        }
        // Module binds fire only while the menu is shut, so typing in it cannot
        // trigger a module.
        bool handled = state::with([&](state::Shared& s) {
            return s.menu_open ? false : s.handle_key(static_cast<uint32_t>(w));
        });
        if (handled) return 0;
    }

    bool open = state::with([](state::Shared& s) { return s.menu_open; });
    if (open && feed(msg, w, l)) return 0;
    return chain(hwnd, msg, w, l);
}

/// Subclass the game's window.
inline bool install(HWND hwnd) {
    // Installing twice would store our own procedure as "the original", and
    // restoring that later leaves the window pointing at a dangling address.
    if (g_original) return true;
    LONG_PTR prev = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&wnd_proc));
    if (!prev) return false;
    g_original = prev;
    return true;
}

/// Put GLFW's window procedure back.
inline bool remove(HWND hwnd) {
    if (!g_original) return true;
    // If something subclassed the window after us, restoring the old procedure
    // would cut that other hook out of the chain. Leave ours in place and say
    // so; the caller keeps the module resident rather than unmapping code the
    // window still points at.
    if (GetWindowLongPtrW(hwnd, GWLP_WNDPROC) != reinterpret_cast<LONG_PTR>(&wnd_proc)) {
        return false;
    }
    SetWindowLongPtrW(hwnd, GWLP_WNDPROC, g_original);
    g_original = 0;
    return true;
}

}  // namespace lodestone::input