use std::sync::{Arc, Mutex};
use kui_core::{Rect, Vec2, WindowButton, WindowRole};
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, WPARAM};
use windows_sys::Win32::Graphics::Gdi::ScreenToClient;
use windows_sys::Win32::UI::Input::KeyboardAndMouse::{
TME_LEAVE, TME_NONCLIENT, TRACKMOUSEEVENT, TrackMouseEvent,
};
use windows_sys::Win32::UI::Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass};
use windows_sys::Win32::UI::WindowsAndMessaging::{
GetClientRect, GetCursorPos, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTCAPTION, HTCLIENT,
HTCLOSE, HTLEFT, HTMAXBUTTON, HTMINBUTTON, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, PostMessageW,
WM_DESTROY, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_NCHITTEST, WM_NCLBUTTONDBLCLK,
WM_NCLBUTTONDOWN, WM_NCLBUTTONUP, WM_NCMOUSELEAVE, WM_NCMOUSEMOVE,
};
const SUBCLASS_ID: usize = 0x6b7569; const MK_LBUTTON: WPARAM = 0x0001;
const WM_MOUSELEAVE: u32 = 0x02a3;
const BAND: f32 = 6.0;
#[derive(Default)]
struct NcState {
scale: f32,
maximized: bool,
resize_border: bool,
regions: Vec<(Option<WindowRole>, Rect)>,
}
pub struct NcHitTest {
state: Arc<Mutex<NcState>>,
}
impl NcHitTest {
pub fn install(window: &winit::window::Window, resize_border: bool) -> Option<Self> {
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
let RawWindowHandle::Win32(h) = window.window_handle().ok()?.as_raw() else {
return None;
};
let state = Arc::new(Mutex::new(NcState {
scale: 1.0,
resize_border,
..Default::default()
}));
let data = Arc::into_raw(state.clone()) as usize;
let ok = unsafe {
SetWindowSubclass(h.hwnd.get() as HWND, Some(subclass_proc), SUBCLASS_ID, data)
};
if ok == 0 {
unsafe { drop(Arc::from_raw(data as *const Mutex<NcState>)) };
return None;
}
Some(Self { state })
}
pub fn update(
&self,
scale: f32,
maximized: bool,
regions: impl Iterator<Item = (Option<WindowRole>, Rect)>,
) {
let mut s = self.state.lock().unwrap();
s.scale = scale;
s.maximized = maximized;
s.regions.clear();
s.regions.extend(regions);
}
}
fn point_of(lparam: LPARAM) -> POINT {
POINT {
x: (lparam & 0xffff) as i16 as i32,
y: ((lparam >> 16) & 0xffff) as i16 as i32,
}
}
fn forward_as_client(hwnd: HWND, msg: u32, wparam: WPARAM, screen_lparam: LPARAM) {
let p = to_client(hwnd, point_of(screen_lparam));
let lparam = (((p.y as u32) << 16) | (p.x as u32 & 0xffff)) as i32 as LPARAM;
unsafe { PostMessageW(hwnd, msg, wparam, lparam) };
}
fn to_client(hwnd: HWND, mut p: POINT) -> POINT {
unsafe { ScreenToClient(hwnd, &mut p) };
p
}
fn client_rect(hwnd: HWND) -> RECT {
let mut rc = RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
unsafe { GetClientRect(hwnd, &mut rc) };
rc
}
fn track_leave(hwnd: HWND, whole_window: bool) {
let mut t = TRACKMOUSEEVENT {
cbSize: size_of::<TRACKMOUSEEVENT>() as u32,
dwFlags: if whole_window {
TME_LEAVE | TME_NONCLIENT
} else {
TME_LEAVE
},
hwndTrack: hwnd,
dwHoverTime: 0,
};
unsafe { TrackMouseEvent(&mut t) };
}
fn cursor_in_window(hwnd: HWND) -> Option<POINT> {
let mut p = POINT { x: 0, y: 0 };
if unsafe { GetCursorPos(&mut p) } == 0 {
return None;
}
let p = to_client(hwnd, p);
let rc = client_rect(hwnd);
(p.x >= rc.left && p.x < rc.right && p.y >= rc.top && p.y < rc.bottom).then_some(p)
}
fn on_own_chrome(state: &Mutex<NcState>, hwnd: HWND) -> bool {
let Some(p) = cursor_in_window(hwnd) else {
return false;
};
matches!(hit_code(state, hwnd, p), Some(code) if code != HTCLIENT)
}
fn hit_code(state: &Mutex<NcState>, hwnd: HWND, client_pt: POINT) -> Option<u32> {
let s = state.lock().ok()?;
let scale = if s.scale > 0.0 { s.scale } else { 1.0 };
let pt = Vec2::new(client_pt.x as f32 / scale, client_pt.y as f32 / scale);
if s.resize_border && !s.maximized {
let rc = client_rect(hwnd);
let w = (rc.right - rc.left) as f32 / scale;
let h = (rc.bottom - rc.top) as f32 / scale;
let (l, r) = (pt.x < BAND, pt.x > w - BAND);
let (t, b) = (pt.y < BAND, pt.y > h - BAND);
let code = match (l, r, t, b) {
(true, _, true, _) => HTTOPLEFT,
(_, true, true, _) => HTTOPRIGHT,
(true, _, _, true) => HTBOTTOMLEFT,
(_, true, _, true) => HTBOTTOMRIGHT,
(true, ..) => HTLEFT,
(_, true, ..) => HTRIGHT,
(_, _, true, _) => HTTOP,
(_, _, _, true) => HTBOTTOM,
_ => 0,
};
if code != 0 {
return Some(code);
}
}
for (role, rect) in s.regions.iter().rev() {
if rect.contains(pt) {
return Some(match role {
Some(WindowRole::Drag) => HTCAPTION,
Some(WindowRole::Button(WindowButton::Close)) => HTCLOSE,
Some(WindowRole::Button(WindowButton::Minimize)) => HTMINBUTTON,
Some(WindowRole::Button(WindowButton::Maximize)) => HTMAXBUTTON,
None => HTCLIENT,
});
}
}
None
}
unsafe extern "system" fn subclass_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
_id: usize,
data: usize,
) -> LRESULT {
let state = unsafe { &*(data as *const Mutex<NcState>) };
match msg {
WM_NCHITTEST => {
let def = unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) };
if def != HTCLIENT as LRESULT {
return def;
}
match hit_code(state, hwnd, to_client(hwnd, point_of(lparam))) {
Some(code) => code as LRESULT,
None => def,
}
}
WM_NCMOUSEMOVE => {
track_leave(hwnd, true);
forward_as_client(hwnd, WM_MOUSEMOVE, 0, lparam);
unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) }
}
WM_MOUSELEAVE if on_own_chrome(state, hwnd) => {
track_leave(hwnd, true);
0
}
WM_NCMOUSELEAVE => {
if cursor_in_window(hwnd).is_some() {
track_leave(hwnd, false);
} else {
unsafe { PostMessageW(hwnd, WM_MOUSELEAVE, 0, 0) };
}
unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) }
}
WM_NCLBUTTONDOWN | WM_NCLBUTTONDBLCLK
if matches!(wparam as u32, HTCLOSE | HTMINBUTTON | HTMAXBUTTON) =>
{
forward_as_client(hwnd, WM_MOUSEMOVE, MK_LBUTTON, lparam);
forward_as_client(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, lparam);
0
}
WM_NCLBUTTONUP if matches!(wparam as u32, HTCLOSE | HTMINBUTTON | HTMAXBUTTON) => {
forward_as_client(hwnd, WM_LBUTTONUP, 0, lparam);
0
}
WM_DESTROY => {
let res = unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) };
unsafe {
RemoveWindowSubclass(hwnd, Some(subclass_proc), SUBCLASS_ID);
drop(Arc::from_raw(data as *const Mutex<NcState>));
}
res
}
_ => unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) },
}
}