use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, WPARAM};
use windows::Win32::Graphics::Gdi::{
ClientToScreen, GetMonitorInfoW, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromWindow,
};
use windows::Win32::UI::Input::KeyboardAndMouse::{SetFocus, VK_ESCAPE, VK_MENU};
use windows::Win32::UI::WindowsAndMessaging::*;
use crate::components::WindowMode;
use super::chrome::windowed_style;
use super::input::*;
thread_local! {
static PARKED_WINDOW: std::cell::Cell<Option<HWND>> = const { std::cell::Cell::new(None) };
}
fn park_window(hwnd: HWND) {
PARKED_WINDOW.with(|p| p.set(Some(hwnd)));
}
fn take_parked_window() -> Option<HWND> {
PARKED_WINDOW.with(|p| p.take())
}
pub(crate) struct WindowState {
pub(crate) hwnd: HWND,
pub(crate) key: KeyState,
pub(crate) mouse_dx: f32,
pub(crate) mouse_dy: f32,
pub(crate) mouse_x: f32,
pub(crate) mouse_y: f32,
pub(crate) left_click_pending: bool,
pub(crate) left_button_down: bool,
pub(crate) right_click_pending: bool,
pub(crate) scroll_delta: f32,
pub(crate) cursor_captured: bool,
pub(crate) recapture_on_click: bool,
pub(crate) ui_cursor_hidden: bool,
pub(crate) menu_mode: bool,
pub(crate) window_mode: WindowMode,
pub(crate) title_bar: bool,
pub(crate) cursor_outside_window: bool,
pub(crate) menu_clip_active: bool,
pub(crate) closed: bool,
pub(crate) width: i32,
pub(crate) height: i32,
}
impl Drop for WindowState {
fn drop(&mut self) {
unsafe { SetWindowLongPtrW(self.hwnd, GWLP_USERDATA, 0) };
park_window(self.hwnd);
}
}
pub(crate) fn do_capture_cursor(hwnd: HWND, state: &mut WindowState) {
if unsafe { GetForegroundWindow() } != hwnd {
return;
}
state.cursor_captured = true;
state.recapture_on_click = false;
unsafe { ShowCursor(false) };
let mut rect = windows::Win32::Foundation::RECT::default();
if unsafe { GetClientRect(hwnd, &mut rect) }.is_ok() {
let mut tl = POINT {
x: rect.left,
y: rect.top,
};
let mut br = POINT {
x: rect.right,
y: rect.bottom,
};
unsafe {
let _ = ClientToScreen(hwnd, &mut tl);
let _ = ClientToScreen(hwnd, &mut br);
}
let screen_rect = windows::Win32::Foundation::RECT {
left: tl.x,
top: tl.y,
right: br.x,
bottom: br.y,
};
let _ = unsafe { ClipCursor(Some(&screen_rect)) };
}
state.mouse_dx = 0.0;
state.mouse_dy = 0.0;
}
pub(crate) fn do_release_cursor(state: &mut WindowState) {
if !state.cursor_captured {
return;
}
state.cursor_captured = false;
state.recapture_on_click = true;
unsafe {
let _ = ClipCursor(None);
ShowCursor(true);
}
}
pub(crate) fn do_set_ui_cursor_hidden(state: &mut WindowState, hidden: bool) {
if hidden == state.ui_cursor_hidden {
return;
}
state.ui_cursor_hidden = hidden;
unsafe { ShowCursor(!hidden) };
}
fn client_screen_rect(hwnd: HWND) -> Option<RECT> {
let mut rect = RECT::default();
if unsafe { GetClientRect(hwnd, &mut rect) }.is_err() {
return None;
}
let mut tl = POINT {
x: rect.left,
y: rect.top,
};
let mut br = POINT {
x: rect.right,
y: rect.bottom,
};
unsafe {
if ClientToScreen(hwnd, &mut tl).as_bool() && ClientToScreen(hwnd, &mut br).as_bool() {
Some(RECT {
left: tl.x,
top: tl.y,
right: br.x,
bottom: br.y,
})
} else {
None
}
}
}
pub(crate) fn update_ui_cursor_confinement(state: &mut WindowState) {
if state.cursor_captured {
state.menu_clip_active = false;
state.cursor_outside_window = false;
return;
}
let mut cursor = POINT::default();
let (Ok(()), Some(rect)) = (
unsafe { GetCursorPos(&mut cursor) },
client_screen_rect(state.hwnd),
) else {
release_menu_clip(state);
state.cursor_outside_window = false;
return;
};
if matches!(state.window_mode, WindowMode::Fullscreen) {
if unsafe { GetForegroundWindow() } == state.hwnd {
let _ = unsafe { ClipCursor(Some(&rect)) };
state.menu_clip_active = true;
} else {
state.menu_clip_active = false;
}
state.cursor_outside_window = false;
return;
}
release_menu_clip(state);
let inside = cursor.x >= rect.left
&& cursor.x < rect.right
&& cursor.y >= rect.top
&& cursor.y < rect.bottom;
state.cursor_outside_window = !inside;
}
fn release_menu_clip(state: &mut WindowState) {
if state.menu_clip_active {
state.menu_clip_active = false;
let _ = unsafe { ClipCursor(None) };
}
}
pub(crate) fn do_set_window_mode(state: &mut WindowState, mode: WindowMode) {
let hwnd = state.hwnd;
state.window_mode = mode;
unsafe {
match mode {
WindowMode::Windowed => {
let style = windowed_style(state.title_bar);
SetWindowLongPtrW(hwnd, GWL_STYLE, style.0 as isize);
let w = state.width.max(640);
let h = state.height.max(480);
let mut rect = RECT {
left: 0,
top: 0,
right: w,
bottom: h,
};
let _ = AdjustWindowRect(&mut rect, style, false);
let _ = SetWindowPos(
hwnd,
None,
80,
80,
rect.right - rect.left,
rect.bottom - rect.top,
SWP_FRAMECHANGED | SWP_NOZORDER,
);
}
WindowMode::Borderless | WindowMode::Fullscreen => {
SetWindowLongPtrW(hwnd, GWL_STYLE, (WS_POPUP | WS_VISIBLE).0 as isize);
if let Some(rect) = monitor_rect(hwnd) {
let _ = SetWindowPos(
hwnd,
None,
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top,
SWP_FRAMECHANGED | SWP_NOZORDER,
);
}
}
}
let _ = ShowWindow(hwnd, SW_SHOW);
}
}
pub(crate) fn do_set_window_size(state: &mut WindowState, width: u32, height: u32) {
let hwnd = state.hwnd;
unsafe {
let mut rect = RECT {
left: 0,
top: 0,
right: width as i32,
bottom: height as i32,
};
let _ = AdjustWindowRect(&mut rect, windowed_style(state.title_bar), false);
let _ = SetWindowPos(
hwnd,
None,
0,
0,
rect.right - rect.left,
rect.bottom - rect.top,
SWP_NOMOVE | SWP_NOZORDER,
);
}
}
fn monitor_rect(hwnd: HWND) -> Option<RECT> {
unsafe {
let mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
let mut info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
..Default::default()
};
if GetMonitorInfoW(mon, &mut info).as_bool() {
Some(info.rcMonitor)
} else {
None
}
}
}
unsafe extern "system" fn wnd_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
unsafe {
let state_ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut WindowState;
if state_ptr.is_null() {
return DefWindowProcW(hwnd, msg, wparam, lparam);
}
let state = &mut *state_ptr;
match msg {
WM_DESTROY | WM_CLOSE => {
state.closed = true;
PostQuitMessage(0);
LRESULT(0)
}
WM_SIZE => {
state.width = (lparam.0 & 0xFFFF) as i32;
state.height = ((lparam.0 >> 16) & 0xFFFF) as i32;
LRESULT(0)
}
WM_KEYDOWN => {
let vk = vk_from_wparam(wparam.0);
if vk == VK_ESCAPE {
if state.menu_mode || !state.cursor_captured {
state.key.on_escape_uncaptured();
} else {
do_release_cursor(state);
}
}
state.key.on_key_down(vk);
LRESULT(0)
}
WM_KEYUP => {
state.key.on_key_up(vk_from_wparam(wparam.0));
LRESULT(0)
}
WM_SYSKEYDOWN | WM_SYSKEYUP => {
let vk = vk_from_wparam(wparam.0);
state.key.on_sys_key(vk, msg == WM_SYSKEYDOWN);
if vk == VK_MENU {
LRESULT(0)
} else {
DefWindowProcW(hwnd, msg, wparam, lparam)
}
}
WM_CHAR => {
if let Some(c) = char::from_u32(wparam.0 as u32) {
state.key.on_char(c);
}
LRESULT(0)
}
WM_KILLFOCUS => {
if state.cursor_captured {
state.cursor_captured = false;
state.recapture_on_click = true;
let _ = ClipCursor(None);
ShowCursor(true);
}
state.key.on_focus_lost();
LRESULT(0)
}
WM_MOUSEMOVE => {
let x = (lparam.0 & 0xFFFF) as i16 as f32;
let y = ((lparam.0 >> 16) & 0xFFFF) as i16 as f32;
if !state.cursor_captured {
state.mouse_x = x;
state.mouse_y = y;
}
LRESULT(0)
}
WM_INPUT => {
if state.cursor_captured {
let mut raw = windows::Win32::UI::Input::RAWINPUT::default();
let mut size =
std::mem::size_of::<windows::Win32::UI::Input::RAWINPUT>() as u32;
let copied = windows::Win32::UI::Input::GetRawInputData(
windows::Win32::UI::Input::HRAWINPUT(lparam.0 as _),
windows::Win32::UI::Input::RID_INPUT,
Some(&mut raw as *mut _ as *mut std::ffi::c_void),
&mut size,
std::mem::size_of::<windows::Win32::UI::Input::RAWINPUTHEADER>() as u32,
);
if copied != u32::MAX
&& raw.header.dwType == windows::Win32::UI::Input::RIM_TYPEMOUSE.0
{
state.mouse_dx += raw.data.mouse.lLastX as f32;
state.mouse_dy += raw.data.mouse.lLastY as f32;
}
}
LRESULT(0)
}
WM_LBUTTONDOWN => {
if !state.cursor_captured {
if !state.menu_mode && state.recapture_on_click {
do_capture_cursor(hwnd, state);
} else {
state.left_click_pending = true;
state.left_button_down = true;
}
}
LRESULT(0)
}
WM_RBUTTONDOWN => {
if !state.cursor_captured {
state.right_click_pending = true;
}
LRESULT(0)
}
WM_LBUTTONUP => {
state.left_button_down = false;
LRESULT(0)
}
WM_MOUSEWHEEL => {
if !state.cursor_captured {
let raw = (wparam.0 >> 16) as i16 as f32;
let notches = raw / WHEEL_DELTA as f32;
state.scroll_delta += crate::gfx::input::wheel_notches_to_scroll_delta(notches);
}
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
}
fn fresh_window_state(hwnd: HWND, width: i32, height: i32, title_bar: bool) -> Box<WindowState> {
Box::new(WindowState {
hwnd,
title_bar,
key: KeyState::default(),
mouse_dx: 0.0,
mouse_dy: 0.0,
mouse_x: 0.0,
mouse_y: 0.0,
left_click_pending: false,
left_button_down: false,
right_click_pending: false,
scroll_delta: 0.0,
cursor_captured: false,
recapture_on_click: false,
ui_cursor_hidden: false,
menu_mode: false,
window_mode: WindowMode::Windowed,
cursor_outside_window: false,
menu_clip_active: false,
closed: false,
width,
height,
})
}
fn install_window_state(hwnd: HWND, win_state: &mut WindowState) {
let rid = windows::Win32::UI::Input::RAWINPUTDEVICE {
usUsagePage: 0x01,
usUsage: 0x02, dwFlags: windows::Win32::UI::Input::RIDEV_INPUTSINK,
hwndTarget: hwnd,
};
let _ = unsafe {
windows::Win32::UI::Input::RegisterRawInputDevices(
&[rid],
std::mem::size_of::<windows::Win32::UI::Input::RAWINPUTDEVICE>() as u32,
)
};
unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, win_state as *mut WindowState as isize) };
}
fn adopt_parked_window(hwnd: HWND, title_bar: bool) -> (HWND, Box<WindowState>) {
let (width, height) = {
let mut rect = RECT::default();
if unsafe { GetClientRect(hwnd, &mut rect) }.is_ok() {
(
(rect.right - rect.left).max(1),
(rect.bottom - rect.top).max(1),
)
} else {
(1, 1)
}
};
let mut win_state = fresh_window_state(hwnd, width, height, title_bar);
install_window_state(hwnd, &mut win_state);
(hwnd, win_state)
}
pub(crate) fn create_window(
title: &str,
width: u32,
height: u32,
title_bar: bool,
) -> Result<(HWND, Box<WindowState>), String> {
if let Some(hwnd) = take_parked_window() {
let (hwnd, state) = adopt_parked_window(hwnd, title_bar);
unsafe {
SetWindowLongPtrW(hwnd, GWL_STYLE, windowed_style(title_bar).0 as isize);
let _ = SetWindowPos(
hwnd,
None,
0,
0,
0,
0,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER,
);
}
return Ok((hwnd, state));
}
let class_name: Vec<u16> = "ConcinnityWindow\0".encode_utf16().collect();
let title_wide: Vec<u16> = title.encode_utf16().chain(std::iter::once(0)).collect();
let hinstance = unsafe { windows::Win32::System::LibraryLoader::GetModuleHandleW(None) }
.map_err(|e| format!("GetModuleHandle: {e}"))?;
let wc = WNDCLASSEXW {
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wnd_proc),
hInstance: hinstance.into(),
lpszClassName: windows::core::PCWSTR(class_name.as_ptr()),
hCursor: unsafe { LoadCursorW(None, IDC_ARROW).unwrap_or_default() },
..Default::default()
};
unsafe { RegisterClassExW(&wc) };
let style = windowed_style(title_bar);
let mut rect = windows::Win32::Foundation::RECT {
left: 0,
top: 0,
right: width as i32,
bottom: height as i32,
};
unsafe { AdjustWindowRect(&mut rect, style, false) }.ok();
let hwnd = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
windows::core::PCWSTR(class_name.as_ptr()),
windows::core::PCWSTR(title_wide.as_ptr()),
style,
CW_USEDEFAULT,
CW_USEDEFAULT,
rect.right - rect.left,
rect.bottom - rect.top,
None,
None,
Some(hinstance.into()),
None,
)
}
.map_err(|e| format!("CreateWindowExW: {e}"))?;
unsafe {
let _ = ShowWindow(hwnd, SW_SHOW);
let _ = SetForegroundWindow(hwnd);
let _ = SetFocus(Some(hwnd));
};
let mut win_state = fresh_window_state(hwnd, width as i32, height as i32, title_bar);
install_window_state(hwnd, &mut win_state);
Ok((hwnd, win_state))
}
pub(crate) fn frame_tick(
state: &mut WindowState,
display: &mut super::display_mode::FullscreenDisplayMode,
) -> bool {
pump_messages();
update_ui_cursor_confinement(state);
let mode = state.window_mode;
let fullscreen = matches!(mode, WindowMode::Fullscreen);
if display.reconcile(state.hwnd, fullscreen) && !matches!(mode, WindowMode::Windowed) {
do_set_window_mode(state, mode);
}
state.closed
}
pub(crate) fn take_input_snapshot(state: &mut WindowState) -> crate::gfx::input::RenderInput {
let dx = state.mouse_dx;
let dy = state.mouse_dy;
let mx = state.mouse_x;
let my = state.mouse_y;
let lc = state.left_click_pending;
let lbd = state.left_button_down;
let rc = state.right_click_pending;
let scroll = state.scroll_delta;
state.mouse_dx = 0.0;
state.mouse_dy = 0.0;
state.left_click_pending = false;
state.right_click_pending = false;
state.scroll_delta = 0.0;
state.key.take(MouseSnapshot {
dx,
dy,
x: mx,
y: my,
left_click: lc,
left_button_down: lbd,
right_click: rc,
scroll_delta: scroll,
})
}
pub(crate) fn pump_messages() {
let mut msg = MSG::default();
while unsafe { PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE) }.as_bool() {
unsafe {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn park_then_take_round_trips_and_is_consumed_once() {
let _ = take_parked_window();
assert!(take_parked_window().is_none(), "empty when nothing parked");
let fake = HWND(0x1234 as *mut core::ffi::c_void);
park_window(fake);
let taken = take_parked_window();
assert_eq!(
taken.map(|h| h.0 as usize),
Some(0x1234),
"the parked HWND is handed back to the next create_window"
);
assert!(
take_parked_window().is_none(),
"take consumes the parked window (a second create_window builds fresh)"
);
}
#[test]
fn parking_overwrites_the_previous_slot() {
let _ = take_parked_window();
park_window(HWND(0xAAAA as *mut core::ffi::c_void));
park_window(HWND(0xBBBB as *mut core::ffi::c_void));
assert_eq!(
take_parked_window().map(|h| h.0 as usize),
Some(0xBBBB),
"only the most-recently parked window is adopted"
);
}
}