#![cfg(all(target_os = "windows", not(any(feature = "mini", feature = "embedded"))))]
use crate::core::{ObjectId, Point, Rect};
use crate::event::Event;
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, SetWindowPos, UpdateWindow, CS_HREDRAW, CS_OWNDC, CS_VREDRAW,
IDC_ARROW, PAINTSTRUCT, SWP_NOACTIVATE, SWP_NOZORDER, WM_ERASEBKGND, WM_KEYDOWN,
WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_PAINT, WM_SIZE, WNDCLASSW, WS_CHILD, WS_TABSTOP,
WS_VISIBLE,
};
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 to_wide(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
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_MOUSEMOVE => {
forward_mouse(hwnd, lparam, MousePhase::Drag);
0
}
WM_KEYDOWN => {
forward_key(hwnd, wparam);
0
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
#[derive(Clone, Copy)]
enum MousePhase {
Press,
Release,
Drag,
}
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 mut rect: RECT = std::mem::zeroed();
if GetClientRect(hwnd, &mut rect) == 0 {
log::error!("[windows] canvas: GetClientRect failed");
EndPaint(hwnd, &paint);
return;
}
let width = (rect.right - rect.left).max(1) as u32;
let height = (rect.bottom - rect.top).max(1) as u32;
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(
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;
};
let mut buffer = vec![0u8; width as usize * height as usize * 4];
for (index, pixel) in frame.chunks_exact(4).enumerate() {
let offset = index * 4;
buffer[offset] = pixel[2];
buffer[offset + 1] = pixel[1];
buffer[offset + 2] = pixel[0];
buffer[offset + 3] = 255;
}
let mut info: BITMAPINFOHEADER = std::mem::zeroed();
info.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
info.biWidth = width as i32;
info.biHeight = -(height as i32);
info.biPlanes = 1;
info.biBitCount = 32;
info.biCompression = BI_RGB;
let scanned = StretchDIBits(
hdc,
0,
0,
width as i32,
height as i32,
0,
0,
width as i32,
height as i32,
buffer.as_ptr() as *const _,
&info as *const _ as *const BITMAPINFO,
DIB_RGB_COLORS,
SRCCOPY,
);
if scanned == 0 {
log::error!("[windows] canvas: StretchDIBits failed for {width}x{height}");
}
EndPaint(hwnd, &paint);
}
pub(crate) fn invalidate_canvas(hwnd: HWND) {
unsafe {
InvalidateRect(hwnd, std::ptr::null(), 0);
}
}
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 position = Point::new(x, y);
let event = match phase {
MousePhase::Press => Event::MousePress { pos: position, button: 1 },
MousePhase::Release => Event::MouseRelease { pos: position, button: 1 },
MousePhase::Drag => Event::MouseMove { pos: position },
};
if crate::widget::runtime::dispatch_event(widget_id, &event) {
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;
let modifiers = current_modifiers();
let event = Event::KeyPress { key, modifiers };
if crate::widget::runtime::dispatch_event(widget_id, &event) {
invalidate_canvas(hwnd);
}
}
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_self_drawn: id={id} is not in widget::runtime; \
call runtime::register before mounting"
);
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_self_drawn: CreateWindowExW failed (GetLastError={})",
winapi::um::errhandlingapi::GetLastError()
);
return None;
}
canvases().lock().expect("windows canvas lock poisoned").insert(hwnd as usize, id);
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_self_drawn: SetWindowPos failed");
return false;
}
invalidate_canvas(hwnd);
true
}
}
pub(crate) fn unmount_canvas(hwnd: HWND) -> bool {
unsafe {
use winapi::um::winuser::DestroyWindow;
if DestroyWindow(hwnd) == 0 {
log::error!("[windows] unmount_self_drawn: DestroyWindow failed");
return false;
}
}
canvases().lock().expect("windows canvas lock poisoned").remove(&(hwnd as usize)).is_some()
}
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());
}
}