#![cfg(all(target_os = "windows", widgets_unstripped))]
use crate::core::{MutexExt, ObjectId, Point, Rect};
use crate::event::Event;
use crate::platform::types::MousePhase;
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use winapi::shared::minwindef::{LPARAM, LRESULT, UINT, WPARAM};
use winapi::shared::windef::{HDC, HWND, RECT};
use winapi::um::wingdi::{
StretchDIBits, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, SRCCOPY,
};
use winapi::um::winuser::{
BeginPaint, CreateWindowExW, DefWindowProcW, EndPaint, GetClientRect, InvalidateRect,
LoadCursorW, RegisterClassW, SetFocus, SetWindowPos, TrackMouseEvent, UpdateWindow, CS_HREDRAW,
CS_OWNDC, CS_VREDRAW, IDC_ARROW, PAINTSTRUCT, SWP_NOACTIVATE, SWP_NOZORDER, TME_LEAVE,
TRACKMOUSEEVENT, WM_CHAR, WM_CLOSE, WM_ERASEBKGND, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS,
WM_LBUTTONDBLCLK, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSELEAVE, WM_MOUSEMOVE, WM_MOUSEWHEEL,
WM_PAINT, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETFOCUS, WM_SIZE, WM_TOUCH, WM_UNICHAR, WNDCLASSW,
WS_CHILD, WS_TABSTOP, WS_VISIBLE,
};
#[cfg(feature = "touch")]
use winapi::um::winuser::{
CloseTouchInputHandle, GetTouchInputInfo, RegisterTouchWindow, TOUCHEVENTF_DOWN,
TOUCHEVENTF_MOVE, TOUCHEVENTF_UP, TOUCHINPUT, TWF_WANTPALM,
};
const CANVAS_CLASS: &str = "RustWidgetsCanvasClass";
fn canvases() -> &'static Mutex<HashMap<usize, ObjectId>> {
static CANVASES: OnceLock<Mutex<HashMap<usize, ObjectId>>> = OnceLock::new();
CANVASES.get_or_init(|| Mutex::new(HashMap::new()))
}
fn canvas_origin(hwnd: HWND) -> Option<(i32, i32)> {
use winapi::shared::windef::POINT;
use winapi::um::errhandlingapi::{GetLastError, SetLastError};
use winapi::um::winuser::{GetParent, MapWindowPoints};
unsafe {
let parent = GetParent(hwnd);
if parent.is_null() {
log::error!("[windows] canvas_origin: hwnd {hwnd:?} has no parent window");
return None;
}
let mut origin = POINT { x: 0, y: 0 };
SetLastError(0);
let result = MapWindowPoints(hwnd, parent, &mut origin, 1);
if map_failed(result, GetLastError()) {
log::error!("[windows] canvas_origin: MapWindowPoints failed for hwnd {hwnd:?}");
return None;
}
Some((origin.x, origin.y))
}
}
fn map_failed(result: i32, last_error: u32) -> bool {
result == 0 && last_error != 0
}
fn to_wide(value: &str) -> Vec<u16> {
crate::platform::windows::types::WindowsPlatform::to_wide(value)
}
extern "system" {
fn GetModuleHandleW(lp_module_name: *const u16) -> *mut std::ffi::c_void;
}
pub(crate) fn ensure_canvas_class_registered() {
static REGISTERED: OnceLock<()> = OnceLock::new();
REGISTERED.get_or_init(|| {
unsafe {
let class_name = to_wide(CANVAS_CLASS);
let hinstance = GetModuleHandleW(std::ptr::null());
let mut wnd_class: WNDCLASSW = std::mem::zeroed();
wnd_class.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wnd_class.lpfnWndProc = Some(canvas_wnd_proc);
wnd_class.hInstance = hinstance as _;
wnd_class.hCursor = LoadCursorW(std::ptr::null_mut(), IDC_ARROW);
wnd_class.lpszClassName = class_name.as_ptr();
if RegisterClassW(&wnd_class) == 0 {
log::error!("[windows] RegisterClassW failed for {CANVAS_CLASS}");
}
}
});
}
unsafe extern "system" fn canvas_wnd_proc(
hwnd: HWND,
msg: UINT,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
match msg {
WM_PAINT => {
paint_canvas(hwnd);
0
}
WM_ERASEBKGND => 1,
WM_SIZE => {
invalidate_canvas(hwnd);
0
}
WM_LBUTTONDOWN => {
forward_mouse(hwnd, lparam, MousePhase::Press);
0
}
WM_LBUTTONUP => {
forward_mouse(hwnd, lparam, MousePhase::Release);
0
}
WM_LBUTTONDBLCLK => {
forward_mouse(hwnd, lparam, MousePhase::DoubleClick);
0
}
WM_RBUTTONDOWN => {
forward_mouse(hwnd, lparam, MousePhase::SecondaryPress);
0
}
WM_RBUTTONUP => {
forward_mouse(hwnd, lparam, MousePhase::SecondaryRelease);
0
}
WM_MOUSEMOVE => {
forward_mouse(hwnd, lparam, MousePhase::Drag);
0
}
WM_MOUSEWHEEL => {
forward_wheel(hwnd, wparam, lparam);
0
}
WM_CHAR | WM_UNICHAR => {
forward_char(hwnd, wparam);
0
}
WM_MOUSELEAVE => {
if let Some(widget_id) = widget_id_of(hwnd) {
let Some(origin) = canvas_origin(hwnd) else {
return 0;
};
let position = Point::new(origin.0, origin.1);
crate::platform::platform_facts().route_pointer_event(
widget_id,
&Event::MouseLeave { pos: position },
position,
);
invalidate_canvas(hwnd);
}
0
}
WM_TOUCH => {
#[cfg(feature = "touch")]
{
forward_touch(hwnd, wparam, lparam);
0
}
#[cfg(not(feature = "touch"))]
{
let _ = (wparam, lparam);
DefWindowProcW(hwnd, msg, wparam, lparam)
}
}
WM_KEYDOWN => {
forward_key(hwnd, wparam);
0
}
WM_KEYUP => {
forward_key_release(hwnd, wparam);
0
}
WM_SETFOCUS => {
if let Some(widget_id) = widget_id_of(hwnd) {
crate::widget::runtime::report_state(
widget_id,
crate::widget::runtime::StateFact::Focused(true),
);
invalidate_canvas(hwnd);
}
DefWindowProcW(hwnd, msg, wparam, lparam)
}
WM_KILLFOCUS => {
if let Some(widget_id) = widget_id_of(hwnd) {
crate::widget::runtime::report_state(
widget_id,
crate::widget::runtime::StateFact::Focused(false),
);
invalidate_canvas(hwnd);
}
DefWindowProcW(hwnd, msg, wparam, lparam)
}
WM_CLOSE => 0,
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
fn widget_id_of(hwnd: HWND) -> Option<ObjectId> {
canvases().lock().ok()?.get(&(hwnd as usize)).copied()
}
unsafe fn paint_canvas(hwnd: HWND) {
let mut paint: PAINTSTRUCT = std::mem::zeroed();
let hdc: HDC = BeginPaint(hwnd, &mut paint);
let Some((width, height)) = client_size(hwnd) else {
EndPaint(hwnd, &paint);
return;
};
let Some(widget_id) = widget_id_of(hwnd) else {
log::error!("[windows] canvas: WM_PAINT for an hwnd with no widget id");
EndPaint(hwnd, &paint);
return;
};
let Some(frame) = crate::widget::runtime::render_frame_cached(
widget_id,
crate::core::Size::new(width, height),
crate::core::Color::WHITE,
) else {
log::error!(
"[windows] canvas: widget id={widget_id} produced no frame \
(unmounted, or it does not implement Draw)"
);
EndPaint(hwnd, &paint);
return;
};
blit_frame(hdc, width, height, &frame);
EndPaint(hwnd, &paint);
}
pub(crate) unsafe fn client_size(hwnd: HWND) -> Option<(u32, u32)> {
let mut rect: RECT = std::mem::zeroed();
if GetClientRect(hwnd, &mut rect) == 0 {
log::error!("[windows] GetClientRect failed");
return None;
}
Some(((rect.right - rect.left).max(1) as u32, (rect.bottom - rect.top).max(1) as u32))
}
pub(crate) unsafe fn blit_frame(hdc: HDC, width: u32, height: u32, frame: &[u8]) {
if width == 0 || height == 0 {
return;
}
let (width_i32, height_i32) = match (i32::try_from(width), i32::try_from(height)) {
(Ok(w), Ok(h)) => (w, h),
_ => {
log::error!(
"[windows] blit_frame: {width}x{height} exceeds the Win32 DIB signed-extent \
limit (i32::MAX); refusing to blit"
);
return;
}
};
let expected = (width as usize) * (height as usize) * 4;
if frame.len() < expected {
log::error!(
"[windows] blit_frame: a {width}x{height} frame needs {expected} bytes, got {}",
frame.len()
);
return;
}
let mut buffer = vec![0u8; expected];
for (offset, pixel) in buffer.as_chunks_mut::<4>().0.iter_mut().zip(frame.as_chunks::<4>().0) {
offset[0] = pixel[2];
offset[1] = pixel[1];
offset[2] = pixel[0];
offset[3] = 255;
}
let mut info: BITMAPINFOHEADER = std::mem::zeroed();
info.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
info.biWidth = width_i32;
info.biHeight = height_i32.checked_neg().unwrap_or(height_i32);
info.biPlanes = 1;
info.biBitCount = 32;
info.biCompression = BI_RGB;
let scanned = StretchDIBits(
hdc,
0,
0,
width_i32,
height_i32,
0,
0,
width_i32,
height_i32,
buffer.as_ptr() as *const _,
&info as *const _ as *const BITMAPINFO,
DIB_RGB_COLORS,
SRCCOPY,
);
if scanned == 0 {
log::error!("[windows] blit_frame: StretchDIBits failed for {width}x{height}");
}
}
pub(crate) fn invalidate_canvas(hwnd: HWND) {
unsafe {
InvalidateRect(hwnd, std::ptr::null(), 0);
}
}
pub(crate) fn invalidate_for_handle(hwnd: HWND) {
unsafe {
if winapi::um::winuser::GetParent(hwnd).is_null() {
InvalidateRect(hwnd, std::ptr::null(), 0);
} else {
invalidate_canvas(hwnd);
}
}
}
pub(crate) fn invalidate_canvas_rect(hwnd: HWND, rect: Rect) -> bool {
if rect.width == 0 || rect.height == 0 {
return false;
}
let mut client: RECT = unsafe { core::mem::zeroed() };
let have_client = unsafe { GetClientRect(hwnd, &mut client) != 0 };
if !have_client {
return false;
}
let x = rect.x;
let y = rect.y;
let right = rect.x.saturating_add(rect.width as i32);
let bottom = rect.y.saturating_add(rect.height as i32);
if x >= client.right || y >= client.bottom || right <= client.left || bottom <= client.top {
return false;
}
let clip = RECT { left: x, top: y, right, bottom };
unsafe {
InvalidateRect(hwnd, &clip, 0);
}
true
}
unsafe fn forward_mouse(hwnd: HWND, lparam: LPARAM, phase: MousePhase) {
let Some(widget_id) = widget_id_of(hwnd) else {
return;
};
let x = (lparam & 0xFFFF) as u16 as i16 as i32;
let y = ((lparam >> 16) & 0xFFFF) as u16 as i16 as i32;
let Some((origin_x, origin_y)) = canvas_origin(hwnd) else {
return;
};
let position = Point::new(origin_x + x, origin_y + y);
let modifiers = current_modifiers();
let event = match phase {
MousePhase::Press => Event::mouse_press_with(
position.x,
position.y,
crate::event::mouse_button::PRIMARY,
modifiers,
),
MousePhase::Release => {
Event::MouseRelease { pos: position, button: crate::event::mouse_button::PRIMARY }
}
MousePhase::Drag => Event::MouseMove { pos: position },
MousePhase::SecondaryPress => Event::mouse_press_with(
position.x,
position.y,
crate::event::mouse_button::SECONDARY,
modifiers,
),
MousePhase::SecondaryRelease => {
Event::MouseRelease { pos: position, button: crate::event::mouse_button::SECONDARY }
}
MousePhase::DoubleClick => {
Event::mouse_double_click(position.x, position.y, crate::event::mouse_button::PRIMARY)
}
};
let delivered =
crate::platform::platform_facts().route_pointer_event(widget_id, &event, position);
if matches!(phase, MousePhase::Drag) {
let mut track: TRACKMOUSEEVENT = std::mem::zeroed();
track.cbSize = std::mem::size_of::<TRACKMOUSEEVENT>() as u32;
track.dwFlags = TME_LEAVE;
track.hwndTrack = hwnd;
TrackMouseEvent(&mut track);
}
if delivered {
if matches!(phase, MousePhase::Press | MousePhase::SecondaryPress | MousePhase::DoubleClick)
{
SetFocus(hwnd);
}
invalidate_canvas(hwnd);
}
}
unsafe fn forward_wheel(hwnd: HWND, wparam: WPARAM, lparam: LPARAM) {
use winapi::um::winuser::{GET_WHEEL_DELTA_WPARAM, WHEEL_DELTA};
let Some(widget_id) = widget_id_of(hwnd) else {
return;
};
let raw_delta = GET_WHEEL_DELTA_WPARAM(wparam);
if raw_delta == 0 {
return;
}
let mut screen = winapi::shared::windef::POINT {
x: (lparam & 0xFFFF) as u16 as i16 as i32,
y: ((lparam >> 16) & 0xFFFF) as u16 as i16 as i32,
};
if winapi::um::winuser::ScreenToClient(hwnd, &mut screen) == 0 {
return;
}
let Some((origin_x, origin_y)) = canvas_origin(hwnd) else {
return;
};
let position = Point::new(origin_x + screen.x, origin_y + screen.y);
let notches = match raw_delta / WHEEL_DELTA {
0 if raw_delta > 0 => 1,
0 => -1,
whole => whole as i32,
};
let event = Event::Wheel { delta: Point::new(0, -notches), modifiers: current_modifiers() };
if crate::platform::platform_facts().route_pointer_event(widget_id, &event, position) {
invalidate_canvas(hwnd);
}
}
#[cfg(feature = "touch")]
unsafe fn forward_touch(hwnd: HWND, wparam: WPARAM, lparam: LPARAM) {
let input_count = (wparam & 0xFFFF) as u32;
if input_count == 0 {
return;
}
let mut inputs: Vec<TOUCHINPUT> = vec![std::mem::zeroed(); input_count as usize];
let size = std::mem::size_of::<TOUCHINPUT>() as i32;
if GetTouchInputInfo(lparam as *mut _, input_count, inputs.as_mut_ptr(), size) == 0 {
CloseTouchInputHandle(lparam as *mut _);
return;
}
let Some(widget_id) = widget_id_of(hwnd) else {
CloseTouchInputHandle(lparam as *mut _);
return;
};
let Some((origin_x, origin_y)) = canvas_origin(hwnd) else {
CloseTouchInputHandle(lparam as *mut _);
return;
};
let mut delivered = false;
for input in inputs.iter().take(input_count as usize) {
let local_x = (input.x as f32 / 100.0).round() as i32;
let local_y = (input.y as f32 / 100.0).round() as i32;
let mut screen_point = winapi::shared::windef::POINT { x: local_x, y: local_y };
if winapi::um::winuser::ScreenToClient(hwnd, &mut screen_point) == 0 {
continue;
}
let position = Point::new(origin_x + screen_point.x, origin_y + screen_point.y);
let touch_id = input.dwID as u64;
let flags = input.dwFlags;
let event = if flags & TOUCHEVENTF_DOWN != 0 {
Event::TouchBegin { pos: position, touch_id }
} else if flags & TOUCHEVENTF_UP != 0 {
Event::TouchEnd { pos: position, touch_id }
} else if flags & TOUCHEVENTF_MOVE != 0 {
Event::TouchMove { pos: position, touch_id }
} else {
continue;
};
if crate::platform::platform_facts().route_pointer_event(widget_id, &event, position) {
delivered = true;
}
}
CloseTouchInputHandle(lparam as *mut _);
if delivered {
invalidate_canvas(hwnd);
}
}
unsafe fn forward_char(hwnd: HWND, wparam: WPARAM) {
let Some(widget_id) = widget_id_of(hwnd) else {
return;
};
let value = wparam as u32;
if value < 0x20 || value == 0x7F {
return;
}
let Some(character) = char::from_u32(value) else {
log::debug!("[windows] canvas: WM_CHAR delivered U+{value:04X}, not a Unicode scalar");
return;
};
let target = crate::widget::runtime::focused_widget().unwrap_or(widget_id);
if crate::widget::runtime::dispatch_event(target, &Event::text_input(character.to_string())) {
invalidate_canvas(hwnd);
}
}
unsafe fn forward_key(hwnd: HWND, wparam: WPARAM) {
let Some(widget_id) = widget_id_of(hwnd) else {
return;
};
let key = wparam as u32;
const VK_TAB: u32 = 0x09;
const WIDGET_SHIFT: u32 = 1;
if key == VK_TAB {
let forward = current_modifiers() & WIDGET_SHIFT == 0;
crate::widget::runtime::focus_next(forward);
invalidate_canvas(hwnd);
return;
}
let modifiers = current_modifiers();
let event = Event::KeyPress { key, modifiers };
let target = key_target_for_id(widget_id);
if crate::widget::runtime::dispatch_event(target, &event) {
invalidate_canvas(hwnd);
}
}
unsafe fn forward_key_release(hwnd: HWND, wparam: WPARAM) {
let Some(widget_id) = widget_id_of(hwnd) else {
return;
};
let event = Event::KeyRelease { key: wparam as u32, modifiers: current_modifiers() };
let target = key_target_for_id(widget_id);
if crate::widget::runtime::dispatch_event(target, &event) {
invalidate_canvas(hwnd);
}
}
fn key_target_for_id(widget_id: ObjectId) -> ObjectId {
crate::widget::runtime::focused_widget().unwrap_or(widget_id)
}
pub(crate) unsafe fn current_modifiers() -> u32 {
use winapi::um::winuser::{GetKeyState, VK_CONTROL, VK_MENU, VK_SHIFT};
const WIDGET_SHIFT: u32 = 1;
const WIDGET_CONTROL: u32 = 2;
const WIDGET_ALT: u32 = 4;
let mut modifiers = 0u32;
if GetKeyState(VK_SHIFT) < 0 {
modifiers |= WIDGET_SHIFT;
}
if GetKeyState(VK_CONTROL) < 0 {
modifiers |= WIDGET_CONTROL;
}
if GetKeyState(VK_MENU) < 0 {
modifiers |= WIDGET_ALT;
}
modifiers
}
pub(crate) fn mount_canvas(parent: HWND, id: ObjectId, rect: Rect) -> Option<HWND> {
ensure_canvas_class_registered();
if !crate::widget::runtime::is_mounted(id) {
log::error!(
"[windows] mount_surface: id={id} is not in widget::runtime; \
call runtime::register before mounting"
);
return None;
}
if hwnd_for_widget(id).is_some() {
log::error!(
"[windows] mount_surface: id={id} already has a canvas; a widget can be mounted \
once, so this request was refused"
);
return None;
}
unsafe {
let class_name = to_wide(CANVAS_CLASS);
let hinstance = GetModuleHandleW(std::ptr::null());
let hwnd = CreateWindowExW(
0,
class_name.as_ptr(),
to_wide("").as_ptr(),
WS_CHILD | WS_VISIBLE | WS_TABSTOP,
rect.x,
rect.y,
rect.width as i32,
rect.height as i32,
parent,
std::ptr::null_mut(),
hinstance as _,
std::ptr::null_mut(),
);
if hwnd.is_null() {
log::error!(
"[windows] mount_surface: CreateWindowExW failed (GetLastError={})",
winapi::um::errhandlingapi::GetLastError()
);
return None;
}
canvases().lock_guard().insert(hwnd as usize, id);
#[cfg(feature = "touch")]
{
if RegisterTouchWindow(hwnd, TWF_WANTPALM) == 0 {
log::debug!(
"[windows] mount_surface: RegisterTouchWindow declined for hwnd {hwnd:p}; \
touch input will not be delivered to this canvas"
);
}
}
invalidate_canvas(hwnd);
UpdateWindow(hwnd);
Some(hwnd)
}
}
pub(crate) fn resize_canvas(hwnd: HWND, rect: Rect) -> bool {
unsafe {
let moved = SetWindowPos(
hwnd,
std::ptr::null_mut(),
rect.x,
rect.y,
rect.width as i32,
rect.height as i32,
SWP_NOZORDER | SWP_NOACTIVATE,
);
if moved == 0 {
log::error!("[windows] resize_surface: SetWindowPos failed");
return false;
}
invalidate_canvas(hwnd);
true
}
}
pub(crate) fn unmount_canvas(hwnd: HWND) -> bool {
let destroyed = unsafe {
use winapi::um::winuser::DestroyWindow;
DestroyWindow(hwnd) != 0
};
if !destroyed {
log::error!(
"[windows] unmount_surface: DestroyWindow failed for hwnd {hwnd:?}; the window is \
still alive, but its registry entry was released so this backend can no longer \
route input or invalidation to it"
);
}
let removed = canvases().lock_guard().remove(&(hwnd as usize)).is_some();
if !removed {
log::error!("[windows] unmount_surface: hwnd {hwnd:?} was not a mounted canvas");
}
destroyed
}
pub(crate) fn hwnd_for_widget(id: ObjectId) -> Option<HWND> {
let map = canvases().lock().ok()?;
map.iter().find(|(_, widget)| **widget == id).map(|(hwnd, _)| *hwnd as HWND)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn class_name_is_stable() {
assert_eq!(CANVAS_CLASS, "RustWidgetsCanvasClass");
}
#[test]
fn to_wide_is_nul_terminated() {
assert_eq!(to_wide("ab"), vec![0x61, 0x62, 0x00]);
assert_eq!(to_wide(""), vec![0x00]);
}
#[test]
fn unknown_hwnd_has_no_widget() {
assert!(widget_id_of(0 as HWND).is_none());
}
#[test]
fn hwnd_for_unknown_widget_is_none() {
assert!(hwnd_for_widget(0xDEAD_BEEF).is_none());
}
#[test]
fn a_zero_displacement_is_not_a_map_failure() {
assert!(!map_failed(0, 0), "zero with no error is a successful no-move");
assert!(map_failed(0, 5), "zero with an error is a real failure");
assert!(!map_failed(1, 5), "a non-zero return succeeds regardless of a stale error");
}
}