Sign in Sign up
kretrod/lodestone Public
Branches
master
159 lines (146 loc) · 5.4 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.

use std::sync::atomic::{AtomicIsize, Ordering};

use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
use windows_sys::Win32::UI::WindowsAndMessaging::{
    CallWindowProcW, GetWindowLongPtrW, SetWindowLongPtrW, GWLP_WNDPROC, WHEEL_DELTA, WM_CHAR, WM_KEYDOWN, WM_KEYUP,
    WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL,
    WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SYSKEYDOWN, WM_SYSKEYUP,
};

use crate::state;

/// The window procedure GLFW installed, which we chain to.
static ORIGINAL: AtomicIsize = AtomicIsize::new(0);

pub const VK_INSERT: usize = 0x2D;

/// Subclass the game's window.
///
/// # Safety
/// `hwnd` must be the game's top-level window.
pub unsafe fn install(hwnd: HWND) -> bool {
    // Installing twice would store our own procedure as "the original", and
    // restoring that later leaves a dangling pointer on the window.
    if ORIGINAL.load(Ordering::SeqCst) != 0 {
        return true;
    }
    let prev = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, wnd_proc as isize);
    if prev == 0 {
        return false;
    }
    ORIGINAL.store(prev, Ordering::SeqCst);
    true
}

/// Put GLFW's window procedure back.
///
/// # Safety
/// `hwnd` must be the window `install` was called with.
pub unsafe fn remove(hwnd: HWND) -> bool {
    let prev = ORIGINAL.swap(0, Ordering::SeqCst);
    if prev == 0 {
        return true;
    }
    // If something subclassed the window after us, putting the old procedure
    // back 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) != wnd_proc as isize {
        ORIGINAL.store(prev, Ordering::SeqCst);
        return false;
    }
    SetWindowLongPtrW(hwnd, GWLP_WNDPROC, prev);
    true
}

fn chain(hwnd: HWND, msg: u32, w: WPARAM, l: LPARAM) -> LRESULT {
    let prev = ORIGINAL.load(Ordering::SeqCst);
    if prev == 0 {
        return 0;
    }
    // SAFETY: `prev` is the procedure GLFW registered for this window.
    unsafe {
        CallWindowProcW(
            Some(std::mem::transmute::<
                isize,
                unsafe extern "system" fn(HWND, u32, WPARAM, LPARAM) -> LRESULT,
            >(prev)),
            hwnd,
            msg,
            w,
            l,
        )
    }
}

unsafe extern "system" fn wnd_proc(hwnd: HWND, msg: u32, w: WPARAM, l: LPARAM) -> LRESULT {
    if msg == WM_KEYDOWN {
        // 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.
        if state::with(|s| s.binding.is_some()).unwrap_or(false) {
            state::with(|s| s.handle_key(w as u32));
            return 0;
        }
        // The menu key (rebindable, default Insert) toggles the menu and is
        // never forwarded to the game.
        let menu_key = state::with(|s| s.cfg.menu_key).unwrap_or(VK_INSERT as u32);
        if w as u32 == menu_key {
            state::with(|s| s.toggle_menu());
            return 0;
        }
        // Module binds fire only while the menu is shut, so typing in it cannot
        // trigger a module.
        let handled = state::with(|s| if !s.menu_open { s.handle_key(w as u32) } else { false })
            .unwrap_or(false);
        if handled {
            return 0;
        }
    }

    let open = state::with(|s| s.menu_open).unwrap_or(false);
    if !open {
        return chain(hwnd, msg, w, l);
    }

    let consumed = feed(msg, w, l);
    if consumed {
        return 0;
    }
    chain(hwnd, msg, w, l)
}

/// Translate one Win32 message into framework-neutral UiInput. Returns true if
/// the game should not also see it.
fn feed(msg: u32, w: WPARAM, l: LPARAM) -> bool {
    use crate::state::UiInput;
    match msg {
        WM_MOUSEMOVE => {
            let x = (l & 0xFFFF) as i16 as f32;
            let y = ((l >> 16) & 0xFFFF) as i16 as f32;
            state::with(|s| {
                s.pointer = (x, y);
                s.events.push(UiInput::MouseMove(x, y));
            });
            true
        }
        WM_LBUTTONDOWN => push(UiInput::MouseButton(0, true)),
        WM_LBUTTONUP => push(UiInput::MouseButton(0, false)),
        WM_RBUTTONDOWN => push(UiInput::MouseButton(1, true)),
        WM_RBUTTONUP => push(UiInput::MouseButton(1, false)),
        WM_MBUTTONDOWN => push(UiInput::MouseButton(2, true)),
        WM_MBUTTONUP => push(UiInput::MouseButton(2, false)),
        WM_MOUSEWHEEL => {
            let delta = ((w >> 16) & 0xFFFF) as i16 as f32 / WHEEL_DELTA as f32;
            push(UiInput::Wheel(delta))
        }
        WM_CHAR => {
            if let Some(c) = char::from_u32(w as u32) {
                if !c.is_control() {
                    let _ = push(UiInput::Char(c));
                }
            }
            true
        }
        WM_KEYDOWN | WM_SYSKEYDOWN => push(UiInput::Key(w as u32, true)),
        WM_KEYUP | WM_SYSKEYUP => push(UiInput::Key(w as u32, false)),
        _ => false,
    }
}

fn push(ev: crate::state::UiInput) -> bool {
    state::with(|s| s.events.push(ev));
    true
}