pub mod clipboard;
pub mod tray;
pub use tray::{Tray, TrayCtx, TrayMenuItem};
use std::ffi::c_void;
use std::mem::size_of;
use std::path::PathBuf;
use tiny_skia::Pixmap;
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, POINT, RECT, WPARAM};
use windows::Win32::Graphics::Gdi::{
BeginPaint, EndPaint, GetDC, GetDeviceCaps, InvalidateRect, ReleaseDC, ScreenToClient,
SetDIBitsToDevice, UpdateWindow, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS,
PAINTSTRUCT, VREFRESH,
};
use windows::Win32::Media::{timeBeginPeriod, timeEndPeriod};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::HiDpi::{
AdjustWindowRectExForDpi, GetDpiForSystem, GetDpiForWindow, GetSystemMetricsForDpi,
SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
};
use windows::Win32::UI::Input::Ime::{
ImmGetContext, ImmReleaseContext, ImmSetCandidateWindow, ImmSetCompositionWindow, CANDIDATEFORM,
CFS_CANDIDATEPOS, CFS_POINT, COMPOSITIONFORM,
};
use windows::Win32::UI::Input::Touch::{
CloseTouchInputHandle, GetTouchInputInfo, RegisterTouchWindow, HTOUCHINPUT,
REGISTER_TOUCH_WINDOW_FLAGS, TOUCHEVENTF_DOWN, TOUCHEVENTF_MOVE, TOUCHEVENTF_UP, TOUCHINPUT,
};
use windows::Win32::UI::Input::KeyboardAndMouse::{
GetDoubleClickTime, GetKeyState, ReleaseCapture, SetCapture, TrackMouseEvent, TME_LEAVE,
TRACKMOUSEEVENT, VK_BACK, VK_CONTROL, VK_DELETE,
VK_DOWN, VK_END, VK_ESCAPE, VK_HOME, VK_LEFT, VK_RETURN, VK_RIGHT, VK_SHIFT, VK_SPACE, VK_TAB,
VK_UP,
};
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetClientRect,
GetMessageExtraInfo, GetMessageTime, GetMessageW, GetSystemMetrics, GetWindowLongPtrW,
GetWindowRect, IsIconic, LoadCursorW,
MsgWaitForMultipleObjectsEx, NCCALCSIZE_PARAMS, PeekMessageW, PostQuitMessage, RegisterClassExW,
SPI_GETCLIENTAREAANIMATION, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SystemParametersInfoW,
SM_CXDOUBLECLK, SM_CXFRAME, SM_CXPADDEDBORDER, SM_CXSCREEN, SM_CYDOUBLECLK, SM_CYFRAME,
SM_CYSCREEN, SetCursor, SetWindowLongPtrW, ShowWindow,
TranslateMessage, CREATESTRUCTW, CW_USEDEFAULT, GWLP_USERDATA, HTCLIENT, MWMO_INPUTAVAILABLE,
PM_REMOVE, QS_ALLINPUT, SetWindowPos, IDC_ARROW, IDC_HAND, IDC_IBEAM, MSG, SWP_NOACTIVATE,
SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SW_SHOW, SW_SHOWNORMAL, LoadIconW, WINDOW_EX_STYLE,
WINDOW_STYLE, WM_CAPTURECHANGED, WM_CHAR, WM_DESTROY, WM_DPICHANGED, WM_IME_COMPOSITION,
WM_IME_STARTCOMPOSITION, WM_KEYDOWN, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE,
WM_MOUSEWHEEL, WM_NCMOUSEMOVE,
WM_DROPFILES, WM_NCCALCSIZE, WM_NCCREATE, WM_NCHITTEST, WM_PAINT, WM_QUIT, WM_RBUTTONDOWN,
WM_RBUTTONUP, WM_SETCURSOR, WM_SIZE, WM_TOUCH, WNDCLASSEXW, WS_MAXIMIZEBOX, WS_OVERLAPPEDWINDOW,
WS_THICKFRAME, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTCAPTION, HTLEFT, HTRIGHT,
HTTOP, HTTOPLEFT, HTTOPRIGHT, IsZoomed, SWP_FRAMECHANGED, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE,
};
use windows::Win32::UI::Shell::{
DragAcceptFiles, DragFinish, DragQueryFileW, DragQueryPoint, ShellExecuteW, HDROP,
};
use windows::Win32::Graphics::Dwm::DwmExtendFrameIntoClientArea;
use windows::Win32::UI::Controls::{MARGINS, WM_MOUSELEAVE};
use super::{AppHandler, WindowConfig};
use crate::event::{CursorShape, Key, KeyEvent, MouseButton, PointerEvent, PointerKind, WindowOp};
use crate::geometry::{Color, Point, Size};
unsafe fn os_animations_enabled() -> bool {
let mut on = windows::core::BOOL(1);
let ok = SystemParametersInfoW(
SPI_GETCLIENTAREAANIMATION,
0,
Some(&mut on as *mut _ as *mut core::ffi::c_void),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
)
.is_ok();
if ok {
on.as_bool()
} else {
true
}
}
pub fn run(cfg: WindowConfig, mut handler: Box<dyn AppHandler>) {
let os_default = if cfg.screenshot.is_some() { true } else { unsafe { os_animations_enabled() } };
crate::anim::set_enabled(cfg.animations.unwrap_or(os_default));
if let Some(path) = cfg.screenshot.clone() {
super::run_offscreen(&cfg, &mut handler, &path);
return;
}
unsafe { run_windowed(cfg, handler) };
}
struct WindowState {
handler: Box<dyn AppHandler>,
bg: Color,
capturing: bool,
pixmap: Option<Pixmap>,
buf_w: i32,
buf_h: i32,
last_click: ClickTracker,
touch: Touch,
tray: Option<tray::TrayState>,
frameless: bool,
mouse_tracked: bool,
}
#[derive(Default, Clone, Copy)]
struct Touch {
down: bool,
start: (i32, i32),
last: (i32, i32),
scrolling: bool,
last_t: u32,
vy: f32,
}
const TOUCH_THRESHOLD: i32 = 12;
const TOUCH_VEL_SMOOTH: f32 = 0.4;
#[derive(Default, Clone, Copy)]
struct ClickTracker {
time_ms: u32,
x: i32,
y: i32,
button: i32,
count: u8,
}
impl ClickTracker {
fn bump(&mut self, button: i32, x: i32, y: i32, now_ms: u32, dbl_ms: u32, dx: i32, dy: i32) -> u8 {
let continued = self.count > 0
&& self.button == button
&& now_ms.wrapping_sub(self.time_ms) <= dbl_ms
&& (x - self.x).abs() <= dx
&& (y - self.y).abs() <= dy;
let count = if continued { (self.count + 1).min(3) } else { 1 };
*self = ClickTracker { time_ms: now_ms, x, y, button, count };
count
}
}
impl WindowState {
fn new(handler: Box<dyn AppHandler>, bg: Color) -> Self {
Self {
handler,
bg,
capturing: false,
pixmap: None,
buf_w: 0,
buf_h: 0,
last_click: ClickTracker::default(),
touch: Touch::default(),
tray: None,
frameless: false,
mouse_tracked: false,
}
}
fn ensure_pixmap(&mut self, w: i32, h: i32) {
let w = w.max(1);
let h = h.max(1);
if self.buf_w == w && self.buf_h == h && self.pixmap.is_some() {
return;
}
self.pixmap = Some(Pixmap::new(w as u32, h as u32).expect("分配 pixmap 失败"));
self.buf_w = w;
self.buf_h = h;
}
unsafe fn paint(&mut self, hwnd: HWND) {
let mut rc = RECT::default();
let _ = GetClientRect(hwnd, &mut rc);
let w = rc.right - rc.left;
let h = rc.bottom - rc.top;
if w <= 0 || h <= 0 {
let mut ps = PAINTSTRUCT::default();
let _ = BeginPaint(hwnd, &mut ps);
let _ = EndPaint(hwnd, &ps);
return;
}
self.ensure_pixmap(w, h);
let size = Size::new(self.buf_w, self.buf_h);
let pixmap = self.pixmap.as_mut().unwrap();
pixmap.fill(to_skia_color(self.bg));
self.handler.render(pixmap, size);
swap_rb_inplace(pixmap.data_mut());
let bits = pixmap.data().as_ptr() as *const c_void;
let bmi = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: size_of::<BITMAPINFOHEADER>() as u32,
biWidth: self.buf_w,
biHeight: -self.buf_h, biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB.0,
..Default::default()
},
..Default::default()
};
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(hwnd, &mut ps);
let scanlines = SetDIBitsToDevice(
hdc,
0,
0,
self.buf_w as u32,
self.buf_h as u32,
0,
0,
0,
self.buf_h as u32,
bits,
&bmi,
DIB_RGB_COLORS,
);
debug_assert!(scanlines != 0, "SetDIBitsToDevice 呈现失败");
let _ = EndPaint(hwnd, &ps);
}
}
fn swap_rb_inplace(data: &mut [u8]) {
let n = data.len() / 4;
let p = data.as_mut_ptr() as *mut u32;
for i in 0..n {
unsafe {
let v = p.add(i).read_unaligned();
let s = (v & 0xFF00_FF00) | ((v & 0x0000_00FF) << 16) | ((v & 0x00FF_0000) >> 16);
p.add(i).write_unaligned(s);
}
}
}
use super::to_skia_color;
const CLASS_NAME: PCWSTR = w!("WindUiWindowClass");
unsafe fn run_windowed(mut cfg: WindowConfig, handler: Box<dyn AppHandler>) {
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
let hmodule = GetModuleHandleW(None).expect("GetModuleHandleW 失败");
let hinst = HINSTANCE(hmodule.0);
let cursor = LoadCursorW(None, IDC_ARROW).unwrap_or_default();
#[allow(clippy::manual_dangling_ptr)]
let hicon = LoadIconW(Some(hinst), PCWSTR(1usize as *const u16)).unwrap_or_default();
let wc = WNDCLASSEXW {
cbSize: size_of::<WNDCLASSEXW>() as u32,
lpfnWndProc: Some(wnd_proc),
hInstance: hinst,
lpszClassName: CLASS_NAME,
hCursor: cursor,
hIcon: hicon,
hIconSm: hicon,
..Default::default()
};
let atom = RegisterClassExW(&wc);
debug_assert!(atom != 0, "RegisterClassExW 失败");
let state = Box::new(WindowState::new(handler, cfg.bg));
let state_ptr = Box::into_raw(state);
let title: Vec<u16> = cfg.title.encode_utf16().chain(std::iter::once(0)).collect();
let sys_dpi = {
let d = GetDpiForSystem();
if d == 0 { 96 } else { d }
};
let init_scale = sys_dpi as f32 / 96.0;
let (phys_w, phys_h) = frame_size_for_client(cfg.width, cfg.height, init_scale, sys_dpi);
let win_style = if cfg.resizable {
WS_OVERLAPPEDWINDOW
} else {
WINDOW_STYLE(
WS_OVERLAPPEDWINDOW.0
& !(WS_THICKFRAME.0 | WS_MAXIMIZEBOX.0)
)
};
let hwnd = match CreateWindowExW(
WINDOW_EX_STYLE::default(),
CLASS_NAME,
PCWSTR(title.as_ptr()),
win_style,
CW_USEDEFAULT,
CW_USEDEFAULT,
phys_w,
phys_h,
None,
None,
Some(hinst),
Some(state_ptr as *const c_void),
) {
Ok(h) => h,
Err(e) => {
drop(Box::from_raw(state_ptr));
panic!("CreateWindowExW 失败: {e:?}");
}
};
let dpi = GetDpiForWindow(hwnd);
let scale = if dpi == 0 { 1.0 } else { dpi as f32 / 96.0 };
if (scale - init_scale).abs() > 0.01 {
let (w, h) = frame_size_for_client(cfg.width, cfg.height, scale, dpi);
let _ = SetWindowPos(hwnd, None, 0, 0, w, h, SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE);
}
if let Some(s) = state_from(hwnd) {
s.handler.set_scale(scale);
}
if cfg.centered {
let screen_w = GetSystemMetrics(SM_CXSCREEN);
let screen_h = GetSystemMetrics(SM_CYSCREEN);
let mut rc = RECT::default();
let _ = GetWindowRect(hwnd, &mut rc);
let win_w = rc.right - rc.left;
let win_h = rc.bottom - rc.top;
let x = (screen_w - win_w) / 2;
let y = (screen_h - win_h) / 2;
let _ = SetWindowPos(hwnd, None, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
}
let _ = RegisterTouchWindow(hwnd, REGISTER_TOUCH_WINDOW_FLAGS(0));
DragAcceptFiles(hwnd, true);
if let Some(t) = cfg.tray.take() {
if let Some(ts) = tray::install(hwnd, t) {
if let Some(s) = state_from(hwnd) {
s.tray = Some(ts);
}
}
}
if cfg.frameless {
if let Some(s) = state_from(hwnd) {
s.frameless = true;
}
let margins = MARGINS { cxLeftWidth: 0, cxRightWidth: 0, cyTopHeight: 1, cyBottomHeight: 0 };
let _ = DwmExtendFrameIntoClientArea(hwnd, &margins);
let _ = SetWindowPos(
hwnd,
None,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED,
);
}
let _ = ShowWindow(hwnd, SW_SHOW);
let _ = UpdateWindow(hwnd);
run_message_loop(hwnd);
}
struct TimerResolution;
impl TimerResolution {
fn raise() -> Self {
unsafe {
let _ = timeBeginPeriod(1);
}
TimerResolution
}
}
impl Drop for TimerResolution {
fn drop(&mut self) {
unsafe {
let _ = timeEndPeriod(1);
}
}
}
unsafe fn run_message_loop(hwnd: HWND) {
let frame_ms = frame_interval_ms(hwnd);
let mut msg = MSG::default();
let mut last_frame = std::time::Instant::now();
let mut hires: Option<TimerResolution> = None;
loop {
let animating = !IsIconic(hwnd).as_bool()
&& state_from(hwnd).map(|s| s.handler.wants_animation()).unwrap_or(false);
if animating {
if hires.is_none() {
hires = Some(TimerResolution::raise());
}
let elapsed = last_frame.elapsed().as_millis();
let wait = frame_ms.saturating_sub(elapsed) as u32;
MsgWaitForMultipleObjectsEx(None, wait, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
if msg.message == WM_QUIT {
return; }
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
if last_frame.elapsed().as_millis() >= frame_ms {
let _ = InvalidateRect(Some(hwnd), None, false);
let _ = UpdateWindow(hwnd);
last_frame = std::time::Instant::now();
}
} else {
hires = None;
let r = GetMessageW(&mut msg, None, 0, 0);
if !r.as_bool() {
return; }
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
last_frame = std::time::Instant::now(); }
}
}
unsafe fn frame_interval_ms(hwnd: HWND) -> u128 {
let hdc = GetDC(Some(hwnd));
let hz = if hdc.is_invalid() {
0
} else {
let v = GetDeviceCaps(Some(hdc), VREFRESH);
let _ = ReleaseDC(Some(hwnd), hdc);
v
};
let fps = if hz <= 1 { 60 } else { hz.min(240) };
(1000 / fps.max(1)) as u128
}
unsafe extern "system" fn wnd_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
match msg {
WM_NCCREATE => {
let cs = lparam.0 as *const CREATESTRUCTW;
if !cs.is_null() {
let state_ptr = (*cs).lpCreateParams as isize;
SetWindowLongPtrW(hwnd, GWLP_USERDATA, state_ptr);
}
DefWindowProcW(hwnd, msg, wparam, lparam)
}
WM_PAINT => {
if let Some(state) = state_from(hwnd) {
state.paint(hwnd);
}
LRESULT(0)
}
WM_SIZE => {
let _ = InvalidateRect(Some(hwnd), None, false);
LRESULT(0)
}
WM_NCCALCSIZE if wparam.0 != 0 && is_frameless(hwnd) => handle_nccalcsize(hwnd, lparam),
WM_NCHITTEST if is_frameless(hwnd) => handle_nchittest(hwnd, lparam),
WM_SETCURSOR => {
if (lparam.0 & 0xffff) as u32 == HTCLIENT {
if let Some(state) = state_from(hwnd) {
apply_cursor(state.handler.cursor());
return LRESULT(1); }
}
DefWindowProcW(hwnd, msg, wparam, lparam)
}
WM_MOUSEMOVE => {
track_mouse_leave(hwnd);
handle_pointer(hwnd, PointerKind::Move, MouseButton::Left, lparam);
LRESULT(0)
}
WM_NCMOUSEMOVE => {
handle_nc_mouse_move(hwnd, lparam);
DefWindowProcW(hwnd, msg, wparam, lparam)
}
WM_MOUSELEAVE => {
if let Some(s) = state_from(hwnd) {
s.mouse_tracked = false;
}
clear_hover(hwnd);
LRESULT(0)
}
WM_LBUTTONDOWN => {
handle_pointer(hwnd, PointerKind::Down, MouseButton::Left, lparam);
LRESULT(0)
}
WM_LBUTTONUP => {
handle_pointer(hwnd, PointerKind::Up, MouseButton::Left, lparam);
LRESULT(0)
}
WM_RBUTTONDOWN => {
handle_pointer(hwnd, PointerKind::Down, MouseButton::Right, lparam);
LRESULT(0)
}
WM_RBUTTONUP => {
handle_pointer(hwnd, PointerKind::Up, MouseButton::Right, lparam);
LRESULT(0)
}
WM_MOUSEWHEEL => {
handle_wheel(hwnd, wparam, lparam);
LRESULT(0)
}
WM_KEYDOWN => {
handle_key(hwnd, wparam);
LRESULT(0)
}
WM_CHAR => {
handle_char(hwnd, wparam);
LRESULT(0)
}
WM_CAPTURECHANGED => {
handle_capture_changed(hwnd);
LRESULT(0)
}
WM_DPICHANGED => {
handle_dpi_changed(hwnd, wparam, lparam);
LRESULT(0)
}
WM_TOUCH => {
handle_touch_input(hwnd, wparam, lparam);
LRESULT(0)
}
WM_DROPFILES => {
handle_drop_files(hwnd, wparam);
LRESULT(0)
}
tray::WM_TRAYICON => {
if let Some(state) = state_from(hwnd) {
if let Some(ts) = state.tray.as_mut() {
tray::handle_message(ts, lparam);
}
}
LRESULT(0)
}
WM_IME_STARTCOMPOSITION | WM_IME_COMPOSITION => {
handle_ime_position(hwnd);
DefWindowProcW(hwnd, msg, wparam, lparam)
}
WM_DESTROY => {
let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut WindowState;
if !ptr.is_null() {
SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
drop(Box::from_raw(ptr));
}
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
unsafe fn apply_cursor(shape: CursorShape) {
let id = match shape {
CursorShape::Hand => IDC_HAND,
CursorShape::Text => IDC_IBEAM,
CursorShape::Arrow => IDC_ARROW,
};
if let Ok(cur) = LoadCursorW(None, id) {
let _ = SetCursor(Some(cur));
}
}
unsafe fn handle_drop_files(hwnd: HWND, wparam: WPARAM) {
let hdrop = HDROP(wparam.0 as *mut c_void);
let mut pt = POINT::default();
let _ = DragQueryPoint(hdrop, &mut pt);
let count = DragQueryFileW(hdrop, 0xFFFF_FFFF, None);
let mut paths = Vec::with_capacity(count as usize);
for i in 0..count {
let len = DragQueryFileW(hdrop, i, None);
if len == 0 {
continue;
}
let mut buf = vec![0u16; len as usize + 1];
let got = DragQueryFileW(hdrop, i, Some(&mut buf));
if got > 0 {
paths.push(PathBuf::from(String::from_utf16_lossy(&buf[..got as usize])));
}
}
DragFinish(hdrop);
if paths.is_empty() {
return;
}
let repaint = {
let Some(state) = state_from(hwnd) else { return };
state.handler.on_drop_files(Point::new(pt.x, pt.y), paths)
};
if repaint {
let _ = InvalidateRect(Some(hwnd), None, false);
}
if state_from(hwnd).map(|s| s.handler.wants_close()).unwrap_or(false) {
let _ = DestroyWindow(hwnd);
}
}
unsafe fn is_frameless(hwnd: HWND) -> bool {
state_from(hwnd).map(|s| s.frameless).unwrap_or(false)
}
unsafe fn handle_nccalcsize(hwnd: HWND, lparam: LPARAM) -> LRESULT {
if IsZoomed(hwnd).as_bool() {
let params = &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS);
let dpi = GetDpiForWindow(hwnd).max(96);
let cx = GetSystemMetricsForDpi(SM_CXFRAME, dpi) + GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi);
let cy = GetSystemMetricsForDpi(SM_CYFRAME, dpi) + GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi);
params.rgrc[0].left += cx;
params.rgrc[0].right -= cx;
params.rgrc[0].top += cy;
params.rgrc[0].bottom -= cy;
}
LRESULT(0)
}
unsafe fn handle_nchittest(hwnd: HWND, lparam: LPARAM) -> LRESULT {
let sx = (lparam.0 & 0xffff) as i16 as i32;
let sy = ((lparam.0 >> 16) & 0xffff) as i16 as i32;
let mut pt = POINT { x: sx, y: sy };
let _ = ScreenToClient(hwnd, &mut pt);
let mut rc = RECT::default();
let _ = GetClientRect(hwnd, &mut rc);
let (w, h) = (rc.right, rc.bottom);
let interactive = state_from(hwnd)
.map(|s| s.handler.interactive_at(Point::new(pt.x, pt.y)))
.unwrap_or(false);
if interactive {
return LRESULT(HTCLIENT as isize);
}
let dpi = GetDpiForWindow(hwnd).max(96);
let m = ((8.0 * dpi as f32 / 96.0) as i32).max(4);
let (left, right) = (pt.x < m, pt.x >= w - m);
let (top, bottom) = (pt.y < m, pt.y >= h - m);
let ht: i32 = if top && left {
HTTOPLEFT as i32
} else if top && right {
HTTOPRIGHT as i32
} else if bottom && left {
HTBOTTOMLEFT as i32
} else if bottom && right {
HTBOTTOMRIGHT as i32
} else if left {
HTLEFT as i32
} else if right {
HTRIGHT as i32
} else if top {
HTTOP as i32
} else if bottom {
HTBOTTOM as i32
} else {
let drag = state_from(hwnd)
.map(|s| s.handler.window_drag_at(Point::new(pt.x, pt.y)))
.unwrap_or(false);
if drag {
HTCAPTION as i32
} else {
HTCLIENT as i32
}
};
LRESULT(ht as isize)
}
unsafe fn apply_window_op(hwnd: HWND) {
let op = state_from(hwnd).and_then(|s| s.handler.take_window_op());
match op {
Some(WindowOp::Minimize) => {
let _ = ShowWindow(hwnd, SW_MINIMIZE);
}
Some(WindowOp::ToggleMaximize) => {
let cmd = if IsZoomed(hwnd).as_bool() { SW_RESTORE } else { SW_MAXIMIZE };
let _ = ShowWindow(hwnd, cmd);
}
None => {}
}
}
pub fn open_url(url: &str) {
let verb = w!("open");
let target: Vec<u16> = url.encode_utf16().chain(std::iter::once(0)).collect();
unsafe {
ShellExecuteW(
None,
verb,
PCWSTR(target.as_ptr()),
PCWSTR::null(),
PCWSTR::null(),
SW_SHOWNORMAL,
);
}
}
unsafe fn frame_size_for_client(logical_w: i32, logical_h: i32, scale: f32, dpi: u32) -> (i32, i32) {
let cw = (logical_w as f32 * scale).round() as i32;
let ch = (logical_h as f32 * scale).round() as i32;
let mut rc = RECT { left: 0, top: 0, right: cw, bottom: ch };
let _ = AdjustWindowRectExForDpi(&mut rc, WS_OVERLAPPEDWINDOW, false, WINDOW_EX_STYLE::default(), dpi);
(rc.right - rc.left, rc.bottom - rc.top)
}
unsafe fn handle_pointer(hwnd: HWND, kind: PointerKind, button: MouseButton, lparam: LPARAM) {
if is_touch_event() {
return;
}
let x = (lparam.0 & 0xffff) as i16 as i32;
let y = ((lparam.0 >> 16) & 0xffff) as i16 as i32;
let click_count = if matches!(kind, PointerKind::Down) {
let btn = match button {
MouseButton::Left => 1,
MouseButton::Right => 2,
MouseButton::Middle => 3,
};
let now = GetMessageTime() as u32;
let dbl = GetDoubleClickTime();
let dx = GetSystemMetrics(SM_CXDOUBLECLK) / 2;
let dy = GetSystemMetrics(SM_CYDOUBLECLK) / 2;
state_from(hwnd).map(|s| s.last_click.bump(btn, x, y, now, dbl, dx, dy)).unwrap_or(1)
} else {
1
};
dispatch_pointer_event(hwnd, PointerEvent { kind, pos: Point::new(x, y), button, click_count });
}
unsafe fn track_mouse_leave(hwnd: HWND) {
let Some(state) = state_from(hwnd) else { return };
if state.mouse_tracked {
return;
}
state.mouse_tracked = true;
let mut tme = TRACKMOUSEEVENT {
cbSize: core::mem::size_of::<TRACKMOUSEEVENT>() as u32,
dwFlags: TME_LEAVE,
hwndTrack: hwnd,
dwHoverTime: 0,
};
let _ = TrackMouseEvent(&mut tme);
}
unsafe fn clear_hover(hwnd: HWND) {
dispatch_pointer_event(
hwnd,
PointerEvent::single(PointerKind::Move, Point::new(-1, -1), MouseButton::Left),
);
}
unsafe fn handle_nc_mouse_move(hwnd: HWND, lparam: LPARAM) {
let mut pt = POINT {
x: (lparam.0 & 0xffff) as i16 as i32,
y: ((lparam.0 >> 16) & 0xffff) as i16 as i32,
};
let _ = ScreenToClient(hwnd, &mut pt);
dispatch_pointer_event(
hwnd,
PointerEvent::single(PointerKind::Move, Point::new(pt.x, pt.y), MouseButton::Left),
);
}
unsafe fn handle_wheel(hwnd: HWND, wparam: WPARAM, lparam: LPARAM) {
let delta = ((wparam.0 >> 16) & 0xffff) as i16 as i32;
let mut pt = POINT {
x: (lparam.0 & 0xffff) as i16 as i32,
y: ((lparam.0 >> 16) & 0xffff) as i16 as i32,
};
let _ = ScreenToClient(hwnd, &mut pt);
dispatch_pointer_event(
hwnd,
PointerEvent::single(PointerKind::Wheel(delta), Point::new(pt.x, pt.y), MouseButton::Left),
);
}
unsafe fn dispatch_pointer_event(hwnd: HWND, ev: PointerEvent) {
let (repaint, active, was_capturing, close) = {
let Some(state) = state_from(hwnd) else { return };
let repaint = state.handler.on_pointer(ev);
(repaint, state.handler.capture_active(), state.capturing, state.handler.wants_close())
};
if repaint {
let _ = InvalidateRect(Some(hwnd), None, false);
}
if active && !was_capturing {
SetCapture(hwnd);
if let Some(s) = state_from(hwnd) {
s.capturing = true;
}
} else if !active && was_capturing {
let _ = ReleaseCapture();
if let Some(s) = state_from(hwnd) {
s.capturing = false;
}
}
apply_window_op(hwnd);
if close {
let _ = DestroyWindow(hwnd);
}
}
unsafe fn handle_dpi_changed(hwnd: HWND, wparam: WPARAM, lparam: LPARAM) {
let dpi = (wparam.0 & 0xffff) as u32;
let scale = if dpi == 0 { 1.0 } else { dpi as f32 / 96.0 };
let prc = lparam.0 as *const RECT;
if !prc.is_null() {
let r = &*prc;
let _ = SetWindowPos(
hwnd,
None,
r.left,
r.top,
r.right - r.left,
r.bottom - r.top,
SWP_NOZORDER | SWP_NOACTIVATE,
);
}
if let Some(s) = state_from(hwnd) {
s.handler.set_scale(scale);
}
let _ = InvalidateRect(Some(hwnd), None, false);
}
unsafe fn is_touch_event() -> bool {
const SIGNATURE: usize = 0xFF51_5700;
const MASK: usize = 0xFFFF_FF00;
(GetMessageExtraInfo().0 as usize & MASK) == SIGNATURE
}
unsafe fn handle_touch_input(hwnd: HWND, wparam: WPARAM, lparam: LPARAM) {
let count = wparam.0 & 0xffff;
if count == 0 {
return;
}
let hti = HTOUCHINPUT(lparam.0 as *mut c_void);
let mut inputs = [TOUCHINPUT::default(); 8];
let n = count.min(inputs.len());
let ok = GetTouchInputInfo(hti, &mut inputs[..n], size_of::<TOUCHINPUT>() as i32).is_ok();
let _ = CloseTouchInputHandle(hti);
if !ok {
return;
}
let ti = inputs[0];
let mut pt = POINT { x: ti.x / 100, y: ti.y / 100 };
let _ = ScreenToClient(hwnd, &mut pt);
let kind = if ti.dwFlags.0 & TOUCHEVENTF_DOWN.0 != 0 {
PointerKind::Down
} else if ti.dwFlags.0 & TOUCHEVENTF_UP.0 != 0 {
PointerKind::Up
} else if ti.dwFlags.0 & TOUCHEVENTF_MOVE.0 != 0 {
PointerKind::Move
} else {
return;
};
let t = GetMessageTime() as u32;
handle_touch(hwnd, kind, pt.x, pt.y, t);
}
unsafe fn handle_touch(hwnd: HWND, kind: PointerKind, x: i32, y: i32, t: u32) {
match kind {
PointerKind::Down => {
cancel_fling(hwnd);
if let Some(s) = state_from(hwnd) {
s.touch = Touch {
down: true,
start: (x, y),
last: (x, y),
last_t: t,
..Touch::default()
};
}
}
PointerKind::Move => {
let (down, start, last, last_t, scrolling, vy) = match state_from(hwnd) {
Some(s) => (
s.touch.down,
s.touch.start,
s.touch.last,
s.touch.last_t,
s.touch.scrolling,
s.touch.vy,
),
None => return,
};
if !down {
return;
}
let dy = y - last.1;
let dt = t.wrapping_sub(last_t) as i32;
let vy = if dt > 0 {
let inst = dy as f32 / dt as f32;
vy * (1.0 - TOUCH_VEL_SMOOTH) + inst * TOUCH_VEL_SMOOTH
} else {
vy
};
let past = scrolling
|| (x - start.0).abs() >= TOUCH_THRESHOLD
|| (y - start.1).abs() >= TOUCH_THRESHOLD;
if let Some(s) = state_from(hwnd) {
s.touch.last = (x, y);
s.touch.last_t = t;
s.touch.vy = vy;
if past {
s.touch.scrolling = true;
}
}
if past {
dispatch_pan(hwnd, Point::new(x, y), dy);
}
}
PointerKind::Up => {
let (down, start, scrolling, vy) = match state_from(hwnd) {
Some(s) => (s.touch.down, s.touch.start, s.touch.scrolling, s.touch.vy),
None => return,
};
if let Some(s) = state_from(hwnd) {
s.touch.down = false;
s.touch.scrolling = false;
}
if down && scrolling {
dispatch_fling(hwnd, Point::new(x, y), vy);
} else if down {
dispatch_pointer_event(
hwnd,
PointerEvent::single(PointerKind::Down, Point::new(start.0, start.1), MouseButton::Left),
);
dispatch_pointer_event(
hwnd,
PointerEvent::single(PointerKind::Up, Point::new(x, y), MouseButton::Left),
);
}
}
_ => {}
}
}
unsafe fn dispatch_pan(hwnd: HWND, pos: Point, dy: i32) {
let repaint = {
let Some(state) = state_from(hwnd) else { return };
state.handler.on_pan(pos, dy)
};
if repaint {
let _ = InvalidateRect(Some(hwnd), None, false);
}
}
unsafe fn dispatch_fling(hwnd: HWND, pos: Point, vy: f32) {
let started = {
let Some(state) = state_from(hwnd) else { return };
state.handler.start_fling(pos, vy)
};
if started {
let _ = InvalidateRect(Some(hwnd), None, false);
}
}
unsafe fn cancel_fling(hwnd: HWND) {
let repaint = {
let Some(state) = state_from(hwnd) else { return };
state.handler.cancel_fling()
};
if repaint {
let _ = InvalidateRect(Some(hwnd), None, false);
}
}
unsafe fn handle_ime_position(hwnd: HWND) {
let caret = match state_from(hwnd) {
Some(s) => s.handler.ime_caret(),
None => return,
};
let Some((x, y, h)) = caret else { return };
let himc = ImmGetContext(hwnd);
if himc.0.is_null() {
return; }
let cf = COMPOSITIONFORM {
dwStyle: CFS_POINT,
ptCurrentPos: POINT { x, y },
rcArea: RECT::default(),
};
let _ = ImmSetCompositionWindow(himc, &cf);
let cand = CANDIDATEFORM {
dwIndex: 0,
dwStyle: CFS_CANDIDATEPOS,
ptCurrentPos: POINT { x, y: y + h },
rcArea: RECT::default(),
};
let _ = ImmSetCandidateWindow(himc, &cand);
let _ = ImmReleaseContext(hwnd, himc);
}
unsafe fn handle_capture_changed(hwnd: HWND) {
let repaint = {
let Some(state) = state_from(hwnd) else { return };
if !state.capturing {
return;
}
state.capturing = false;
state.handler.on_capture_lost()
};
if repaint {
let _ = InvalidateRect(Some(hwnd), None, false);
}
}
unsafe fn handle_key(hwnd: HWND, wparam: WPARAM) {
let vk = wparam.0 as u16;
let shift = (GetKeyState(VK_SHIFT.0 as i32) as u16 & 0x8000) != 0;
let ctrl = (GetKeyState(VK_CONTROL.0 as i32) as u16 & 0x8000) != 0;
let key = if vk == VK_TAB.0 {
Key::Tab
} else if vk == VK_RETURN.0 {
Key::Enter
} else if vk == VK_ESCAPE.0 {
Key::Escape
} else if vk == VK_SPACE.0 {
Key::Space
} else if vk == VK_BACK.0 {
Key::Backspace
} else if vk == VK_DELETE.0 {
Key::Delete
} else if vk == VK_LEFT.0 {
Key::Left
} else if vk == VK_RIGHT.0 {
Key::Right
} else if vk == VK_UP.0 {
Key::Up
} else if vk == VK_DOWN.0 {
Key::Down
} else if vk == VK_HOME.0 {
Key::Home
} else if vk == VK_END.0 {
Key::End
} else {
Key::Other(vk as u32)
};
let ev = KeyEvent { key, pressed: true, shift, ctrl };
dispatch_key_event(hwnd, ev);
}
unsafe fn handle_char(hwnd: HWND, wparam: WPARAM) {
let Some(c) = char::from_u32(wparam.0 as u32) else { return };
if c.is_control() {
return;
}
let ev = KeyEvent { key: Key::Char(c), pressed: true, shift: false, ctrl: false };
dispatch_key_event(hwnd, ev);
}
unsafe fn dispatch_key_event(hwnd: HWND, ev: KeyEvent) {
let (repaint, close) = {
let Some(state) = state_from(hwnd) else { return };
(state.handler.on_key(ev), state.handler.wants_close())
};
if repaint {
let _ = InvalidateRect(Some(hwnd), None, false);
}
apply_window_op(hwnd);
if close {
let _ = DestroyWindow(hwnd);
}
}
unsafe fn state_from<'a>(hwnd: HWND) -> Option<&'a mut WindowState> {
let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut WindowState;
if ptr.is_null() {
None
} else {
Some(&mut *ptr)
}
}
#[cfg(test)]
mod tests {
use super::ClickTracker;
const DBL: u32 = 500;
const DX: i32 = 4;
const DY: i32 = 4;
#[test]
fn double_then_triple_then_reset() {
let mut t = ClickTracker::default();
assert_eq!(t.bump(1, 10, 10, 1000, DBL, DX, DY), 1, "首击=单击");
assert_eq!(t.bump(1, 11, 11, 1100, DBL, DX, DY), 2, "时限内同位=双击");
assert_eq!(t.bump(1, 12, 12, 1200, DBL, DX, DY), 3, "继续=三击");
assert_eq!(t.bump(1, 12, 12, 1300, DBL, DX, DY), 3, "封顶于三击");
assert_eq!(t.bump(1, 12, 12, 2000, DBL, DX, DY), 1, "超时重置为单击");
}
#[test]
fn continuation_across_u32_wraparound() {
let mut t = ClickTracker::default();
let near_max = u32::MAX - 100;
assert_eq!(t.bump(1, 10, 10, near_max, DBL, DX, DY), 1, "首击");
let wrapped = near_max.wrapping_add(150);
assert_eq!(t.bump(1, 10, 10, wrapped, DBL, DX, DY), 2, "跨回绕仍判为双击");
}
#[test]
fn reset_on_far_move_or_other_button() {
let mut t = ClickTracker::default();
assert_eq!(t.bump(1, 10, 10, 1000, DBL, DX, DY), 1);
assert_eq!(t.bump(1, 30, 10, 1100, DBL, DX, DY), 1, "漂移过大不算连击");
assert_eq!(t.bump(2, 30, 10, 1150, DBL, DX, DY), 1, "换按键不算连击");
}
}