use std::cell::RefCell;
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, WPARAM};
use windows_sys::Win32::Graphics::Dwm::{
DwmDefWindowProc, DwmExtendFrameIntoClientArea, DwmGetWindowAttribute, DwmSetWindowAttribute,
DWMSBT_AUTO, DWMSBT_MAINWINDOW, DWMWA_CAPTION_BUTTON_BOUNDS, DWMWA_SYSTEMBACKDROP_TYPE,
DWMWA_USE_IMMERSIVE_DARK_MODE,
};
use windows_sys::Win32::Graphics::Gdi::{
ClientToScreen, FillRect, GetStockObject, GetSysColor, InvalidateRect, ScreenToClient,
BLACK_BRUSH, COLOR_ACTIVECAPTION, COLOR_CAPTIONTEXT, COLOR_INACTIVECAPTION,
COLOR_INACTIVECAPTIONTEXT, HDC,
};
use windows_sys::Win32::System::Registry::{RegGetValueW, HKEY_CURRENT_USER, RRF_RT_REG_DWORD};
use windows_sys::Win32::UI::Accessibility::{HCF_HIGHCONTRASTON, HIGHCONTRASTW};
use windows_sys::Win32::UI::Controls::MARGINS;
use windows_sys::Win32::UI::HiDpi::{GetDpiForWindow, GetSystemMetricsForDpi};
use windows_sys::Win32::UI::Input::KeyboardAndMouse::{
TrackMouseEvent, TME_LEAVE, TME_NONCLIENT, TRACKMOUSEEVENT,
};
use windows_sys::Win32::UI::Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass};
use windows_sys::Win32::UI::WindowsAndMessaging::{
GetClientRect, GetWindowRect, IsZoomed, PostMessageW, SetWindowPos, SystemParametersInfoW,
HTCAPTION, HTCLIENT, HTCLOSE, HTMAXBUTTON, HTMINBUTTON, HTSYSMENU, HTTOP, NCCALCSIZE_PARAMS,
SC_CLOSE, SC_MAXIMIZE, SC_MINIMIZE, SC_RESTORE, SM_CXPADDEDBORDER, SM_CYCAPTION,
SM_CYSIZEFRAME, SPI_GETHIGHCONTRAST, SWP_FRAMECHANGED, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER,
WM_DWMCOLORIZATIONCOLORCHANGED, WM_ERASEBKGND, WM_LBUTTONUP, WM_NCCALCSIZE, WM_NCHITTEST,
WM_NCLBUTTONDOWN, WM_NCLBUTTONUP, WM_NCMOUSELEAVE, WM_SETTINGCHANGE, WM_SIZE, WM_SYSCOMMAND,
WM_THEMECHANGED,
};
use frus_widgets::Color;
const SUBCLASS: usize = 640;
const RESIZE_EDGE: i32 = 6;
#[derive(Default)]
struct Line {
height: i32,
interactive: Vec<RECT>,
icon: Option<RECT>,
hovered: u32,
pressed: u32,
caption: Option<Caption>,
backdrop: Option<(bool, bool)>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct Caption {
high_contrast: Option<[Color; 4]>,
accent: Option<(Color, Option<Color>)>,
}
thread_local! {
static LINE: RefCell<Line> = RefCell::new(Line::default());
static WAKE: RefCell<Option<Box<dyn Fn()>>> = const { RefCell::new(None) };
}
pub(crate) fn on_change(wake: impl Fn() + 'static) {
set_wake(Box::new(wake));
}
#[inline(never)]
fn set_wake(wake: Box<dyn Fn()>) {
WAKE.with(|slot| *slot.borrow_mut() = Some(wake));
}
#[inline(never)]
pub(crate) fn caption_colors(dark: bool, active: bool) -> (Color, Color) {
let caption = LINE.with(|line| *line.borrow_mut().caption.get_or_insert_with(read_caption));
if let Some([surface, ink, inactive_surface, inactive_ink]) = caption.high_contrast {
return if active {
(surface, ink)
} else {
(inactive_surface, inactive_ink)
};
}
match caption.accent {
Some((accent, _)) if active => {
let words = if accent.compute_luminance() > 0.179 {
Color::BLACK
} else {
Color::WHITE
};
return (accent, words);
}
Some((_, Some(inactive))) => {
let words = if inactive.compute_luminance() > 0.179 {
Color::rgb8(0x91, 0x91, 0x91)
} else {
Color::rgb8(0x79, 0x79, 0x79)
};
return (inactive, words);
}
_ => {}
}
match (dark, active) {
(false, true) => (light_caption(), Color::BLACK),
(false, false) => (light_caption(), Color::rgb8(0x91, 0x91, 0x91)),
(true, true) => (Color::rgb8(0x20, 0x20, 0x20), Color::WHITE),
(true, false) => (Color::rgb8(0x20, 0x20, 0x20), Color::rgb8(0x79, 0x79, 0x79)),
}
}
fn light_caption() -> Color {
let build = os_build();
if build >= 22000 || build == 0 {
Color::rgb8(0xF3, 0xF3, 0xF3)
} else {
Color::WHITE
}
}
#[inline(never)]
fn os_build() -> u32 {
type RtlGetVersion = unsafe extern "system" fn(
*mut windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW,
) -> i32;
thread_local! {
static BUILD: u32 = unsafe {
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
let ntdll = GetModuleHandleA(c"ntdll.dll".as_ptr() as *const u8);
match GetProcAddress(ntdll, c"RtlGetVersion".as_ptr() as *const u8) {
Some(f) => {
let get: RtlGetVersion = std::mem::transmute(f);
let mut info: windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW =
std::mem::zeroed();
info.dwOSVersionInfoSize = std::mem::size_of_val(&info) as u32;
if get(&mut info) >= 0 {
info.dwBuildNumber
} else {
0
}
}
None => 0,
}
};
}
BUILD.with(|build| *build)
}
fn read_caption() -> Caption {
unsafe {
let mut contrast: HIGHCONTRASTW = std::mem::zeroed();
contrast.cbSize = std::mem::size_of::<HIGHCONTRASTW>() as u32;
let on = SystemParametersInfoW(
SPI_GETHIGHCONTRAST,
contrast.cbSize,
&mut contrast as *mut HIGHCONTRASTW as *mut core::ffi::c_void,
0,
) != 0
&& contrast.dwFlags & HCF_HIGHCONTRASTON != 0;
let high_contrast = on.then(|| {
[
COLOR_ACTIVECAPTION,
COLOR_CAPTIONTEXT,
COLOR_INACTIVECAPTION,
COLOR_INACTIVECAPTIONTEXT,
]
.map(|index| colorref(GetSysColor(index)))
});
let accent = (dwm_dword("ColorPrevalence") == Some(1))
.then(|| dwm_dword("AccentColor").map(abgr))
.flatten()
.map(|accent| (accent, dwm_dword("AccentColorInactive").map(abgr)));
Caption {
high_contrast,
accent,
}
}
}
fn dwm_dword(name: &str) -> Option<u32> {
let key: Vec<u16> = "Software\\Microsoft\\Windows\\DWM\0"
.encode_utf16()
.collect();
let value: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
let mut data: u32 = 0;
let mut size = std::mem::size_of::<u32>() as u32;
let status = unsafe {
RegGetValueW(
HKEY_CURRENT_USER,
key.as_ptr(),
value.as_ptr(),
RRF_RT_REG_DWORD,
std::ptr::null_mut(),
&mut data as *mut u32 as *mut core::ffi::c_void,
&mut size,
)
};
(status == 0).then_some(data)
}
fn colorref(c: u32) -> Color {
Color::rgb8(c as u8, (c >> 8) as u8, (c >> 16) as u8)
}
#[inline(never)]
fn abgr(c: u32) -> Color {
colorref(c & 0x00FF_FFFF)
}
unsafe fn caption_height(hwnd: HWND) -> i32 {
let dpi = GetDpiForWindow(hwnd);
let border = if IsZoomed(hwnd) != 0 {
0
} else {
GetSystemMetricsForDpi(SM_CYSIZEFRAME, dpi) + GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi)
};
GetSystemMetricsForDpi(SM_CYCAPTION, dpi) + border
}
unsafe fn extend_frame(hwnd: HWND) {
let height = caption_height(hwnd);
LINE.with(|line| line.borrow_mut().height = height);
let margins = MARGINS {
cxLeftWidth: 0,
cxRightWidth: 0,
cyTopHeight: height,
cyBottomHeight: 0,
};
DwmExtendFrameIntoClientArea(hwnd, &margins);
}
pub(crate) unsafe fn enable(hwnd: HWND) {
extend_frame(hwnd);
SetWindowSubclass(hwnd, Some(subclass), SUBCLASS, 0);
frame_changed(hwnd);
InvalidateRect(hwnd, std::ptr::null(), 1);
}
pub(crate) unsafe fn system_backdrop(hwnd: HWND, dark: bool) -> bool {
if let Some((was, took)) = LINE.with(|line| line.borrow().backdrop) {
if was == dark {
return took;
}
}
let set = |attribute: i32, value: i32| {
DwmSetWindowAttribute(
hwnd,
attribute as u32,
&value as *const i32 as *const core::ffi::c_void,
std::mem::size_of::<i32>() as u32,
) == 0
};
set(DWMWA_USE_IMMERSIVE_DARK_MODE, i32::from(dark));
const DWMWA_MICA_EFFECT: i32 = 1029;
let took = set(DWMWA_SYSTEMBACKDROP_TYPE, DWMSBT_MAINWINDOW) || set(DWMWA_MICA_EFFECT, 1);
LINE.with(|line| line.borrow_mut().backdrop = Some((dark, took)));
took
}
pub(crate) unsafe fn disable(hwnd: HWND) {
RemoveWindowSubclass(hwnd, Some(subclass), SUBCLASS);
if LINE.with(|line| line.borrow().backdrop.is_some_and(|(_, took)| took)) {
let auto = DWMSBT_AUTO;
DwmSetWindowAttribute(
hwnd,
DWMWA_SYSTEMBACKDROP_TYPE as u32,
&auto as *const i32 as *const core::ffi::c_void,
std::mem::size_of::<i32>() as u32,
);
}
let margins = MARGINS {
cxLeftWidth: 0,
cxRightWidth: 0,
cyTopHeight: 0,
cyBottomHeight: 0,
};
DwmExtendFrameIntoClientArea(hwnd, &margins);
LINE.with(|line| *line.borrow_mut() = Line::default());
frame_changed(hwnd);
}
unsafe fn frame_changed(hwnd: HWND) {
SetWindowPos(
hwnd,
std::ptr::null_mut(),
0,
0,
0,
0,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER,
);
}
#[inline(never)]
pub(crate) fn height() -> i32 {
LINE.with(|line| line.borrow().height)
}
pub(crate) unsafe fn buttons(hwnd: HWND) -> Option<RECT> {
let mut bounds = RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
let got = DwmGetWindowAttribute(
hwnd,
DWMWA_CAPTION_BUTTON_BOUNDS as u32,
&mut bounds as *mut RECT as *mut core::ffi::c_void,
std::mem::size_of::<RECT>() as u32,
);
if got != 0 || bounds.right <= bounds.left {
return None;
}
let mut window = RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
GetWindowRect(hwnd, &mut window);
let mut origin = POINT { x: 0, y: 0 };
ClientToScreen(hwnd, &mut origin);
let (dx, dy) = (origin.x - window.left, origin.y - window.top);
Some(RECT {
left: bounds.left - dx,
top: (bounds.top - dy).max(0),
right: bounds.right - dx,
bottom: bounds.bottom - dy,
})
}
#[inline(never)]
pub(crate) fn publish(interactive: Vec<RECT>, icon: Option<RECT>) {
LINE.with(|line| {
let mut line = line.borrow_mut();
line.interactive = interactive;
line.icon = icon;
});
}
#[inline(never)]
pub(crate) fn buttons_state() -> (u32, u32) {
LINE.with(|line| {
let line = line.borrow();
(line.hovered, line.pressed)
})
}
unsafe fn set_state(hwnd: HWND, hovered: Option<u32>, pressed: Option<u32>) {
let changed = LINE.with(|line| {
let mut line = line.borrow_mut();
let before = (line.hovered, line.pressed);
if let Some(h) = hovered {
line.hovered = h;
}
if let Some(p) = pressed {
line.pressed = p;
}
before != (line.hovered, line.pressed)
});
if changed {
InvalidateRect(hwnd, std::ptr::null(), 0);
WAKE.with(|slot| {
if let Some(wake) = slot.borrow().as_ref() {
wake();
}
});
}
}
unsafe fn button_at(hwnd: HWND, at: &POINT) -> Option<u32> {
let bounds = buttons(hwnd)?;
if !contains(&bounds, at) {
return None;
}
let third = (bounds.right - bounds.left).max(1) as f32 / 3.0;
Some(match ((at.x - bounds.left) as f32 / third) as i32 {
0 => HTMINBUTTON,
1 => HTMAXBUTTON,
_ => HTCLOSE,
})
}
unsafe fn command_for(hwnd: HWND, code: u32) -> u32 {
match code {
HTMINBUTTON => SC_MINIMIZE,
HTMAXBUTTON if IsZoomed(hwnd) != 0 => SC_RESTORE,
HTMAXBUTTON => SC_MAXIMIZE,
_ => SC_CLOSE,
}
}
fn is_button(code: u32) -> bool {
matches!(code, HTMINBUTTON | HTMAXBUTTON | HTCLOSE)
}
#[inline(never)]
fn contains(r: &RECT, at: &POINT) -> bool {
at.x >= r.left && at.x < r.right && at.y >= r.top && at.y < r.bottom
}
unsafe extern "system" fn subclass(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
_id: usize,
_data: usize,
) -> LRESULT {
let pressed_before = LINE.with(|line| line.borrow().pressed);
match msg {
WM_NCLBUTTONDOWN if is_button(wparam as u32) => {
set_state(hwnd, None, Some(wparam as u32));
}
WM_NCLBUTTONUP | WM_LBUTTONUP => set_state(hwnd, None, Some(0)),
WM_NCMOUSELEAVE => set_state(hwnd, Some(0), Some(0)),
WM_SIZE => extend_frame(hwnd),
WM_SETTINGCHANGE | WM_DWMCOLORIZATIONCOLORCHANGED | WM_THEMECHANGED => {
LINE.with(|line| line.borrow_mut().caption = None);
WAKE.with(|slot| {
if let Some(wake) = slot.borrow().as_ref() {
wake();
}
});
}
_ => {}
}
let mut answered: LRESULT = 0;
if DwmDefWindowProc(hwnd, msg, wparam, lparam, &mut answered) != 0 {
if msg == WM_NCHITTEST {
let code = answered as u32;
set_state(hwnd, Some(if is_button(code) { code } else { 0 }), None);
if is_button(code) {
let mut track = TRACKMOUSEEVENT {
cbSize: std::mem::size_of::<TRACKMOUSEEVENT>() as u32,
dwFlags: TME_LEAVE | TME_NONCLIENT,
hwndTrack: hwnd,
dwHoverTime: 0,
};
TrackMouseEvent(&mut track);
}
}
return answered;
}
match msg {
WM_NCLBUTTONDOWN if is_button(wparam as u32) => 0,
WM_NCLBUTTONUP if is_button(wparam as u32) => {
if pressed_before == wparam as u32 {
PostMessageW(
hwnd,
WM_SYSCOMMAND,
command_for(hwnd, wparam as u32) as WPARAM,
lparam,
);
}
0
}
WM_ERASEBKGND => {
let mut client = RECT {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
GetClientRect(hwnd, &mut client);
FillRect(wparam as HDC, &client, GetStockObject(BLACK_BRUSH) as _);
1
}
WM_NCCALCSIZE if wparam != 0 => {
let params = &mut *(lparam as *mut NCCALCSIZE_PARAMS);
let proposed = params.rgrc[0];
let result = DefSubclassProc(hwnd, msg, wparam, lparam);
params.rgrc[0].top = proposed.top;
if IsZoomed(hwnd) != 0 {
params.rgrc[0].top += (params.rgrc[0].left - proposed.left).max(0);
}
result
}
WM_NCHITTEST => {
set_state(hwnd, Some(0), None);
let hit = DefSubclassProc(hwnd, msg, wparam, lparam);
if hit != HTCLIENT as LRESULT {
return hit;
}
let mut at = POINT {
x: (lparam & 0xffff) as i16 as i32,
y: ((lparam >> 16) & 0xffff) as i16 as i32,
};
ScreenToClient(hwnd, &mut at);
if at.y < RESIZE_EDGE && IsZoomed(hwnd) == 0 {
return HTTOP as LRESULT;
}
if let Some(code) = button_at(hwnd, &at) {
set_state(hwnd, Some(code), None);
let mut track = TRACKMOUSEEVENT {
cbSize: std::mem::size_of::<TRACKMOUSEEVENT>() as u32,
dwFlags: TME_LEAVE | TME_NONCLIENT,
hwndTrack: hwnd,
dwHoverTime: 0,
};
TrackMouseEvent(&mut track);
return code as LRESULT;
}
LINE.with(|line| {
let line = line.borrow();
if at.y >= line.height {
hit
} else if line.icon.as_ref().is_some_and(|r| contains(r, &at)) {
HTSYSMENU as LRESULT
} else if line.interactive.iter().any(|r| contains(r, &at)) {
hit
} else {
HTCAPTION as LRESULT
}
})
}
_ => DefSubclassProc(hwnd, msg, wparam, lparam),
}
}