Sign in Sign up
kretrod/lodestone Public
Branches
master
238 lines (222 loc) · 7.7 KB Raw
//! Window-procedure hook: turns Win32 messages into egui events.
//!
//! 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 {
    // Insert toggles the menu, and is never forwarded to the game.
    if msg == WM_KEYDOWN && w == VK_INSERT {
        state::with(|s| s.toggle_menu());
        return 0;
    }

    // Keybinds: always while waiting for one to be set, and otherwise only
    // when the menu is shut, so typing in the menu cannot fire modules.
    if msg == WM_KEYDOWN {
        let handled = state::with(|s| {
            if s.binding.is_some() || !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 message into egui events. Returns true if the game should not
/// see it.
fn feed(msg: u32, w: WPARAM, l: LPARAM) -> bool {
    use egui::{Event, PointerButton, Pos2, Vec2};

    let mods = current_modifiers();
    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 = Pos2::new(x / s.scale, y / s.scale);
                let p = s.pointer;
                s.events.push(Event::PointerMoved(p));
            });
            true
        }
        WM_LBUTTONDOWN | WM_LBUTTONUP | WM_RBUTTONDOWN | WM_RBUTTONUP | WM_MBUTTONDOWN
        | WM_MBUTTONUP => {
            let (button, pressed) = match msg {
                WM_LBUTTONDOWN => (PointerButton::Primary, true),
                WM_LBUTTONUP => (PointerButton::Primary, false),
                WM_RBUTTONDOWN => (PointerButton::Secondary, true),
                WM_RBUTTONUP => (PointerButton::Secondary, false),
                WM_MBUTTONDOWN => (PointerButton::Middle, true),
                _ => (PointerButton::Middle, false),
            };
            state::with(|s| {
                let pos = s.pointer;
                s.events.push(Event::PointerButton { pos, button, pressed, modifiers: mods });
            });
            true
        }
        WM_MOUSEWHEEL => {
            let delta = ((w >> 16) & 0xFFFF) as i16 as f32 / WHEEL_DELTA as f32;
            state::with(|s| {
                s.events.push(Event::MouseWheel {
                    unit: egui::MouseWheelUnit::Line,
                    delta: Vec2::new(0.0, delta),
                    modifiers: mods,
                });
            });
            true
        }
        WM_CHAR => {
            if let Some(c) = char::from_u32(w as u32) {
                // Control characters are handled as key events, not text.
                if !c.is_control() {
                    state::with(|s| s.events.push(Event::Text(c.to_string())));
                }
            }
            true
        }
        WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP => {
            let pressed = msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN;
            if let Some(key) = vk_to_key(w) {
                state::with(|s| {
                    s.events.push(Event::Key {
                        key,
                        physical_key: None,
                        pressed,
                        repeat: false,
                        modifiers: mods,
                    });
                });
            }
            true
        }
        _ => false,
    }
}

fn current_modifiers() -> egui::Modifiers {
    use windows_sys::Win32::UI::Input::KeyboardAndMouse::{
        GetAsyncKeyState, VK_CONTROL, VK_MENU, VK_SHIFT,
    };
    // SAFETY: GetAsyncKeyState has no preconditions.
    let down = |vk: i32| unsafe { (GetAsyncKeyState(vk) as u16 & 0x8000) != 0 };
    let ctrl = down(VK_CONTROL as i32);
    egui::Modifiers {
        alt: down(VK_MENU as i32),
        ctrl,
        shift: down(VK_SHIFT as i32),
        mac_cmd: false,
        command: ctrl,
    }
}

fn vk_to_key(vk: WPARAM) -> Option<egui::Key> {
    use egui::Key;
    Some(match vk as u32 {
        0x08 => Key::Backspace,
        0x09 => Key::Tab,
        0x0D => Key::Enter,
        0x1B => Key::Escape,
        0x20 => Key::Space,
        0x21 => Key::PageUp,
        0x22 => Key::PageDown,
        0x23 => Key::End,
        0x24 => Key::Home,
        0x25 => Key::ArrowLeft,
        0x26 => Key::ArrowUp,
        0x27 => Key::ArrowRight,
        0x28 => Key::ArrowDown,
        0x2E => Key::Delete,
        0x30..=0x39 => match vk as u32 - 0x30 {
            0 => Key::Num0,
            1 => Key::Num1,
            2 => Key::Num2,
            3 => Key::Num3,
            4 => Key::Num4,
            5 => Key::Num5,
            6 => Key::Num6,
            7 => Key::Num7,
            8 => Key::Num8,
            _ => Key::Num9,
        },
        0x41..=0x5A => Key::from_name(&((b'A' + (vk as u8 - 0x41)) as char).to_string())?,
        0x70..=0x7B => Key::from_name(&format!("F{}", vk as u32 - 0x70 + 1))?,
        _ => return None,
    })
}