use std::cell::Cell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use teksilo_canvas::{Point, Rect, Size};
use teksilo_core::signal::Signal;
use teksilo_core::widget_id::WidgetId;
use teksilo_core::{
ControlTarget, HitRegions, PlatformError, PlatformTitleBarHost, ResizeEdge,
TitleBarHostCallbacks, TitleBarHoverEvent, TitleBarSyntheticEvent,
};
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
use winit::window::Window;
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, WPARAM};
use windows::Win32::Graphics::Dwm::DwmExtendFrameIntoClientArea;
use windows::Win32::Graphics::Gdi::{
GetMonitorInfoW, HMONITOR, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromWindow,
ScreenToClient,
};
use windows::Win32::UI::Controls::{HOVER_DEFAULT, MARGINS};
use windows::Win32::UI::HiDpi::{GetDpiForWindow, GetSystemMetricsForDpi};
use windows::Win32::UI::Input::KeyboardAndMouse::{
TME_LEAVE, TME_NONCLIENT, TRACKMOUSEEVENT, TrackMouseEvent,
};
use windows::Win32::UI::Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass};
use windows::Win32::UI::WindowsAndMessaging::{
GetClientRect, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTCAPTION, HTCLIENT, HTCLOSE, HTLEFT,
HTMAXBUTTON, HTMINBUTTON, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IsZoomed, NCCALCSIZE_PARAMS,
SC_KEYMENU, SM_CXFRAME, SM_CXPADDEDBORDER, SM_CYFRAME, SWP_FRAMECHANGED, SWP_NOACTIVATE,
SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SendMessageW, SetWindowPos, WM_DPICHANGED, WM_NCACTIVATE,
WM_NCCALCSIZE, WM_NCHITTEST, WM_NCLBUTTONDOWN, WM_NCLBUTTONUP, WM_NCMOUSELEAVE, WM_NCMOUSEMOVE,
WM_NCPAINT, WM_SYSCOMMAND,
};
use super::edge_to_direction;
static NEXT_SUBCLASS_ID: AtomicUsize = AtomicUsize::new(0xFE_111_000);
struct SubclassData {
hit_regions: Mutex<HitRegions>,
callbacks: TitleBarHostCallbacks,
last_hover: Cell<Option<ControlTarget>>,
hover_signals: Mutex<HashMap<ControlTarget, Signal<bool>>>,
}
unsafe impl Send for SubclassData {}
unsafe impl Sync for SubclassData {}
pub struct WindowsHost {
window: Arc<Window>,
hwnd: HWND,
subclass_id: usize,
data: Rc<SubclassData>,
}
impl WindowsHost {
pub fn new(
window: Arc<Window>,
callbacks: TitleBarHostCallbacks,
) -> Result<Self, PlatformError> {
let hwnd = extract_hwnd(&window)?;
let margins = MARGINS {
cxLeftWidth: 0,
cxRightWidth: 0,
cyTopHeight: 1,
cyBottomHeight: 0,
};
unsafe {
DwmExtendFrameIntoClientArea(hwnd, &margins)
.map_err(|e| PlatformError::Os(format!("DwmExtendFrameIntoClientArea: {e}")))?;
}
let subclass_id = NEXT_SUBCLASS_ID.fetch_add(1, Ordering::Relaxed);
let data = Rc::new(SubclassData {
hit_regions: Mutex::new(HitRegions::default()),
callbacks,
last_hover: Cell::new(None),
hover_signals: Mutex::new(HashMap::new()),
});
let raw_ptr = Rc::as_ptr(&data) as usize;
let installed =
unsafe { SetWindowSubclass(hwnd, Some(teksilo_titlebar_proc), subclass_id, raw_ptr) };
if !installed.as_bool() {
return Err(PlatformError::Os("SetWindowSubclass returned FALSE".into()));
}
unsafe {
let _ = SetWindowPos(
hwnd,
None,
0,
0,
0,
0,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE,
);
}
Ok(Self {
window,
hwnd,
subclass_id,
data,
})
}
}
impl Drop for WindowsHost {
fn drop(&mut self) {
unsafe {
let _ = RemoveWindowSubclass(self.hwnd, Some(teksilo_titlebar_proc), self.subclass_id);
}
}
}
impl PlatformTitleBarHost for WindowsHost {
fn reserved_leading_inset(&self) -> Size {
Size::ZERO
}
fn reserved_trailing_inset(&self) -> Size {
Size::ZERO
}
fn renders_custom_controls(&self) -> bool {
true
}
fn needs_custom_resize_handles(&self) -> bool {
false
}
fn begin_drag(&self) -> Result<(), PlatformError> {
self.window
.drag_window()
.map_err(|e| PlatformError::Os(e.to_string()))
}
fn begin_resize(&self, edge: ResizeEdge) -> Result<(), PlatformError> {
self.window
.drag_resize_window(edge_to_direction(edge))
.map_err(|e| PlatformError::Os(e.to_string()))
}
fn show_window_menu(&self, _at: Point) -> Result<(), PlatformError> {
unsafe {
let _ = SendMessageW(
self.hwnd,
WM_SYSCOMMAND,
Some(WPARAM(SC_KEYMENU as usize)),
Some(LPARAM(0)),
);
}
Ok(())
}
fn update_hit_regions(&self, regions: &HitRegions) {
let dpi = unsafe { GetDpiForWindow(self.hwnd) } as f32;
let scale = if dpi > 0.0 { dpi / 96.0 } else { 1.0 };
let scaled = scale_hit_regions(regions, scale);
if let Ok(mut shared) = self.data.hit_regions.lock() {
*shared = scaled;
}
}
fn title_bar_widget_id(&self, target: ControlTarget) -> Option<WidgetId> {
let regions = self.data.hit_regions.lock().ok()?;
match target {
ControlTarget::Minimize => regions.minimize_id,
ControlTarget::Maximize => regions.maximize_id,
ControlTarget::Close => regions.close_id,
}
}
fn set_button_hover(&self, target: ControlTarget, entered: bool) {
if let Ok(map) = self.data.hover_signals.lock() {
if let Some(sig) = map.get(&target) {
sig.set(entered);
}
}
}
fn register_hover_signal(&self, target: ControlTarget, signal: Signal<bool>) {
if let Ok(mut map) = self.data.hover_signals.lock() {
map.insert(target, signal);
}
}
}
fn extract_hwnd(window: &Arc<Window>) -> Result<HWND, PlatformError> {
let handle = window
.window_handle()
.map_err(|e| PlatformError::Os(format!("window_handle: {e}")))?;
let RawWindowHandle::Win32(raw) = handle.as_raw() else {
return Err(PlatformError::Os("expected Win32 window handle".into()));
};
Ok(HWND(raw.hwnd.get() as *mut _))
}
fn scale_hit_regions(src: &HitRegions, scale: f32) -> HitRegions {
let scale_rect = |r: Rect| -> Rect {
Rect::new(r.x * scale, r.y * scale, r.width * scale, r.height * scale)
};
HitRegions {
minimize: src.minimize.map(scale_rect),
maximize: src.maximize.map(scale_rect),
close: src.close.map(scale_rect),
minimize_id: src.minimize_id,
maximize_id: src.maximize_id,
close_id: src.close_id,
drag: src.drag.iter().copied().map(scale_rect).collect(),
no_drag: src.no_drag.iter().copied().map(scale_rect).collect(),
resize_borders: src.resize_borders,
}
}
unsafe extern "system" fn teksilo_titlebar_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
_uid: usize,
dw_ref_data: usize,
) -> LRESULT {
if dw_ref_data == 0 {
return unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) };
}
let data: &SubclassData = unsafe { &*(dw_ref_data as *const SubclassData) };
match msg {
WM_NCCALCSIZE if wparam.0 != 0 => handle_nccalcsize(hwnd, lparam),
WM_NCHITTEST => handle_nchittest(hwnd, lparam, data),
WM_NCLBUTTONDOWN => {
let is_button = matches!(
wparam.0 as u32,
v if v == HTMINBUTTON || v == HTMAXBUTTON || v == HTCLOSE
);
if is_button {
return LRESULT(0);
}
unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) }
}
WM_NCLBUTTONUP => {
let target = match wparam.0 as u32 {
v if v == HTMINBUTTON => Some(ControlTarget::Minimize),
v if v == HTMAXBUTTON => Some(ControlTarget::Maximize),
v if v == HTCLOSE => Some(ControlTarget::Close),
_ => None,
};
if let Some(target) = target {
let payload = TitleBarSyntheticEvent {
teksilo_id: data.callbacks.teksilo_id,
target,
};
(data.callbacks.post_external)(Box::new(payload));
return LRESULT(0);
}
unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) }
}
WM_NCMOUSEMOVE => {
let target = match wparam.0 as u32 {
v if v == HTMINBUTTON => Some(ControlTarget::Minimize),
v if v == HTMAXBUTTON => Some(ControlTarget::Maximize),
v if v == HTCLOSE => Some(ControlTarget::Close),
_ => None,
};
let prev = data.last_hover.get();
if prev != target {
if let Some(p) = prev {
let payload = TitleBarHoverEvent {
teksilo_id: data.callbacks.teksilo_id,
target: p,
entered: false,
};
(data.callbacks.post_external)(Box::new(payload));
}
if let Some(t) = target {
let payload = TitleBarHoverEvent {
teksilo_id: data.callbacks.teksilo_id,
target: t,
entered: true,
};
(data.callbacks.post_external)(Box::new(payload));
let mut tme = TRACKMOUSEEVENT {
cbSize: std::mem::size_of::<TRACKMOUSEEVENT>() as u32,
dwFlags: TME_NONCLIENT | TME_LEAVE,
hwndTrack: hwnd,
dwHoverTime: HOVER_DEFAULT,
};
unsafe {
let _ = TrackMouseEvent(&mut tme);
}
}
data.last_hover.set(target);
}
unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) }
}
WM_NCMOUSELEAVE => {
if let Some(prev) = data.last_hover.replace(None) {
let payload = TitleBarHoverEvent {
teksilo_id: data.callbacks.teksilo_id,
target: prev,
entered: false,
};
(data.callbacks.post_external)(Box::new(payload));
}
unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) }
}
WM_DPICHANGED => {
let margins = MARGINS {
cxLeftWidth: 0,
cxRightWidth: 0,
cyTopHeight: 1,
cyBottomHeight: 0,
};
unsafe {
let _ = DwmExtendFrameIntoClientArea(hwnd, &margins);
}
unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) }
}
WM_NCPAINT => {
LRESULT(0)
}
WM_NCACTIVATE => {
LRESULT(1)
}
_ => unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) },
}
}
fn handle_nccalcsize(hwnd: HWND, lparam: LPARAM) -> LRESULT {
unsafe {
let p = &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS);
let dpi = GetDpiForWindow(hwnd);
if IsZoomed(hwnd).as_bool() {
let padded = GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi);
let frame = GetSystemMetricsForDpi(SM_CYFRAME, dpi);
p.rgrc[0].top += padded + frame;
p.rgrc[0].left += padded + frame;
p.rgrc[0].right -= padded + frame;
p.rgrc[0].bottom -= padded + frame;
let mon: HMONITOR = 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() {
let work = info.rcWork;
if p.rgrc[0].top < work.top {
p.rgrc[0].top = work.top;
}
if p.rgrc[0].left < work.left {
p.rgrc[0].left = work.left;
}
if p.rgrc[0].right > work.right {
p.rgrc[0].right = work.right;
}
if p.rgrc[0].bottom > work.bottom {
p.rgrc[0].bottom = work.bottom;
}
}
}
}
LRESULT(0)
}
fn handle_nchittest(hwnd: HWND, lparam: LPARAM, data: &SubclassData) -> LRESULT {
let raw = lparam.0 as i32;
let screen_x = (raw & 0xFFFF) as i16 as i32;
let screen_y = ((raw >> 16) & 0xFFFF) as i16 as i32;
let mut pt = POINT {
x: screen_x,
y: screen_y,
};
let _ = unsafe { ScreenToClient(hwnd, &mut pt) };
let dpi = unsafe { GetDpiForWindow(hwnd) };
let resize = unsafe {
GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) + GetSystemMetricsForDpi(SM_CXFRAME, dpi)
};
let mut rect = RECT::default();
let _ = unsafe { GetClientRect(hwnd, &mut rect) };
let zoomed = unsafe { IsZoomed(hwnd) }.as_bool();
if !zoomed {
let on_top = pt.y < resize;
let on_bottom = pt.y >= rect.bottom - resize;
let on_left = pt.x < resize;
let on_right = pt.x >= rect.right - resize;
let edge: Option<u32> = match (on_top, on_bottom, on_left, on_right) {
(true, false, true, false) => Some(HTTOPLEFT),
(true, false, false, true) => Some(HTTOPRIGHT),
(false, true, true, false) => Some(HTBOTTOMLEFT),
(false, true, false, true) => Some(HTBOTTOMRIGHT),
(true, false, _, _) => Some(HTTOP),
(false, true, _, _) => Some(HTBOTTOM),
(_, _, true, false) => Some(HTLEFT),
(_, _, false, true) => Some(HTRIGHT),
_ => None,
};
if let Some(e) = edge {
return LRESULT(e as isize);
}
}
if let Ok(regions) = data.hit_regions.try_lock() {
let pt_canvas = Point::new(pt.x as f32, pt.y as f32);
for hole in ®ions.no_drag {
if hole.contains(pt_canvas) {
return LRESULT(HTCLIENT as isize);
}
}
if let Some(r) = regions.minimize {
if r.contains(pt_canvas) {
return LRESULT(HTMINBUTTON as isize);
}
}
if let Some(r) = regions.maximize {
if r.contains(pt_canvas) {
return LRESULT(HTMAXBUTTON as isize);
}
}
if let Some(r) = regions.close {
if r.contains(pt_canvas) {
return LRESULT(HTCLOSE as isize);
}
}
for drag in ®ions.drag {
if drag.contains(pt_canvas) {
return LRESULT(HTCAPTION as isize);
}
}
}
LRESULT(HTCLIENT as isize)
}