use crate::{TitlebarStyle, WindowOptions, WindowState};
use anyhow::{Context, Result};
use webview2_com::CoTaskMemPWSTR;
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{HINSTANCE, HWND};
use windows::Win32::System::LibraryLoader;
use windows::Win32::UI::WindowsAndMessaging::{
self, WNDCLASSW, WS_MAXIMIZEBOX, WS_OVERLAPPEDWINDOW, WS_THICKFRAME,
};
use super::command::center_window;
use super::message::window_proc;
use super::nonclient::CUSTOM_FRAME_BYTES;
pub(super) struct FrameWindow {
pub(super) hwnd: HWND,
}
impl FrameWindow {
pub(super) fn new(window: &WindowOptions, saved: Option<&WindowState>) -> Result<Self> {
let title = CoTaskMemPWSTR::from(window.title.as_str());
let (x, y, width, height) = initial_geometry(window, saved)?;
let mut style = window_style(window);
if window.maximized || saved.is_some_and(|state| state.maximized) {
style |= WindowsAndMessaging::WS_MAXIMIZE;
}
let instance = unsafe { HINSTANCE(LibraryLoader::GetModuleHandleW(None)?.0) };
let icon = unsafe {
WindowsAndMessaging::LoadIconW(
Some(instance),
PCWSTR::from_raw(std::ptr::without_provenance(1)),
)
.unwrap_or_default()
};
let hwnd = unsafe {
let class = WNDCLASSW {
lpfnWndProc: Some(window_proc),
lpszClassName: w!("WebUIDesktopWindow"),
cbWndExtra: CUSTOM_FRAME_BYTES,
hInstance: instance,
hIcon: icon,
hCursor: WindowsAndMessaging::LoadCursorW(None, WindowsAndMessaging::IDC_ARROW)
.unwrap_or_default(),
..Default::default()
};
WindowsAndMessaging::RegisterClassW(&class);
WindowsAndMessaging::CreateWindowExW(
Default::default(),
w!("WebUIDesktopWindow"),
*title.as_ref().as_pcwstr(),
style,
x,
y,
width,
height,
None,
None,
Some(instance),
Some(std::ptr::from_ref(window).cast()),
)?
};
let frame = Self { hwnd };
frame.apply_custom_frame(window);
frame.apply_initial_placement(window, saved);
Ok(frame)
}
pub(super) fn show(&self) -> Result<()> {
if super::nonclient::startup_close_requested(self.hwnd) {
return Ok(());
}
if unsafe { WindowsAndMessaging::IsWindowVisible(self.hwnd) }.as_bool() {
return Ok(());
}
unsafe {
WindowsAndMessaging::SetWindowPos(
self.hwnd,
None,
0,
0,
0,
0,
WindowsAndMessaging::SWP_NOMOVE
| WindowsAndMessaging::SWP_NOSIZE
| WindowsAndMessaging::SWP_NOZORDER
| WindowsAndMessaging::SWP_SHOWWINDOW
| WindowsAndMessaging::SWP_FRAMECHANGED,
)?;
}
Ok(())
}
pub(super) fn focus_after_startup(&self) {
use windows::Win32::UI::Input::KeyboardAndMouse;
unsafe {
let focused = KeyboardAndMouse::GetFocus();
if focused != self.hwnd && !WindowsAndMessaging::IsChild(self.hwnd, focused).as_bool() {
let _ = KeyboardAndMouse::SetFocus(Some(self.hwnd));
}
}
}
fn apply_custom_frame(&self, window: &WindowOptions) {
if !matches!(window.titlebar, TitlebarStyle::None) {
return;
}
if let Err(error) = extend_frameless_frame(self.hwnd) {
eprintln!("WebUI: failed to extend the desktop frame: {error}");
}
unsafe {
if let Err(error) = WindowsAndMessaging::SetWindowPos(
self.hwnd,
None,
0,
0,
0,
0,
WindowsAndMessaging::SWP_NOMOVE
| WindowsAndMessaging::SWP_NOSIZE
| WindowsAndMessaging::SWP_NOZORDER
| WindowsAndMessaging::SWP_FRAMECHANGED,
) {
eprintln!("WebUI: failed to refresh the desktop frame: {error}");
}
}
}
fn apply_initial_placement(&self, window: &WindowOptions, saved: Option<&WindowState>) {
if saved.is_none() && window.center && !window.maximized {
center_window(self.hwnd);
}
if window.always_on_top {
unsafe {
let _ = WindowsAndMessaging::SetWindowPos(
self.hwnd,
Some(WindowsAndMessaging::HWND_TOPMOST),
0,
0,
0,
0,
WindowsAndMessaging::SWP_NOMOVE | WindowsAndMessaging::SWP_NOSIZE,
);
}
}
}
}
impl Drop for FrameWindow {
fn drop(&mut self) {
unsafe {
if WindowsAndMessaging::IsWindow(Some(self.hwnd)).as_bool() {
if let Err(error) = WindowsAndMessaging::DestroyWindow(self.hwnd) {
eprintln!("WebUI: failed to destroy the desktop window: {error}");
}
}
}
}
}
pub(super) fn extend_frameless_frame(hwnd: HWND) -> windows::core::Result<()> {
unsafe {
windows::Win32::Graphics::Dwm::DwmExtendFrameIntoClientArea(
hwnd,
&windows::Win32::UI::Controls::MARGINS {
cxLeftWidth: 1,
cxRightWidth: 1,
cyTopHeight: 1,
cyBottomHeight: 1,
},
)
}
}
fn initial_geometry(
window: &WindowOptions,
saved: Option<&WindowState>,
) -> Result<(i32, i32, i32, i32)> {
if let Some(saved) = saved {
let width = i32::try_from(saved.width).context("saved window width exceeds Win32 range")?;
let height =
i32::try_from(saved.height).context("saved window height exceeds Win32 range")?;
return Ok((saved.x, saved.y, width, height));
}
let width = i32::try_from(window.width).context("window width exceeds Win32 range")?;
let height = i32::try_from(window.height).context("window height exceeds Win32 range")?;
Ok((
WindowsAndMessaging::CW_USEDEFAULT,
WindowsAndMessaging::CW_USEDEFAULT,
width,
height,
))
}
pub(super) fn window_style(window: &WindowOptions) -> WindowsAndMessaging::WINDOW_STYLE {
let mut style = WS_OVERLAPPEDWINDOW;
if !window.resizable {
style &= !WS_THICKFRAME;
style &= !WS_MAXIMIZEBOX;
}
style
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use windows::Win32::Foundation::{LPARAM, RECT, WPARAM};
use windows::Win32::UI::WindowsAndMessaging::WS_CAPTION;
std::thread_local! {
static FRAME_CALCULATIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
extern "system" fn count_frame_calculations(
hwnd: HWND,
message: u32,
w_param: WPARAM,
l_param: LPARAM,
) -> windows::Win32::Foundation::LRESULT {
if message == WindowsAndMessaging::WM_NCCALCSIZE {
FRAME_CALCULATIONS.set(FRAME_CALCULATIONS.get() + 1);
}
super::super::message::window_proc(hwnd, message, w_param, l_param)
}
#[test]
fn showing_a_visible_startup_frame_does_not_recalculate_its_native_caption() {
let frame = FrameWindow::new(
&WindowOptions {
center: false,
..Default::default()
},
None,
)
.unwrap();
unsafe {
assert_ne!(
WindowsAndMessaging::SetWindowLongPtrW(
frame.hwnd,
WindowsAndMessaging::GWLP_WNDPROC,
count_frame_calculations as *const () as isize,
),
0
);
}
FRAME_CALCULATIONS.set(0);
frame.show().unwrap();
let first = FRAME_CALCULATIONS.get();
assert!(first > 0);
frame.show().unwrap();
assert_eq!(FRAME_CALCULATIONS.get(), first);
}
#[test]
fn a_deferred_startup_close_is_not_undone_by_later_show_calls() {
let frame = FrameWindow::new(
&WindowOptions {
center: false,
..Default::default()
},
None,
)
.unwrap();
frame.show().unwrap();
super::super::nonclient::defer_startup_close(frame.hwnd);
frame.show().unwrap();
assert!(!unsafe { WindowsAndMessaging::IsWindowVisible(frame.hwnd) }.as_bool());
assert!(super::super::nonclient::mark_startup_ready(frame.hwnd));
}
struct TestFrame(FrameWindow);
impl Drop for TestFrame {
fn drop(&mut self) {
if let Err(error) = unsafe { WindowsAndMessaging::DestroyWindow(self.0.hwnd) } {
eprintln!("Failed to destroy test window: {error}");
}
}
}
fn frame_rectangles(frame: &TestFrame) -> (RECT, RECT) {
let mut outer = RECT::default();
let mut client = RECT::default();
unsafe {
WindowsAndMessaging::GetWindowRect(frame.0.hwnd, &mut outer).unwrap();
WindowsAndMessaging::GetClientRect(frame.0.hwnd, &mut client).unwrap();
}
(outer, client)
}
#[test]
fn startup_custom_frame_reserves_only_native_resize_borders() {
{
let titlebar = TitlebarStyle::None;
let options = WindowOptions {
titlebar,
center: false,
width: 600,
height: 400,
..WindowOptions::default()
};
let frame = TestFrame(FrameWindow::new(&options, None).unwrap());
let (outer, client) = frame_rectangles(&frame);
let (border_x, border_y) = if matches!(options.titlebar, TitlebarStyle::None) {
unsafe {
let dpi = windows::Win32::UI::HiDpi::GetDpiForWindow(frame.0.hwnd);
let metric =
|index| windows::Win32::UI::HiDpi::GetSystemMetricsForDpi(index, dpi);
let padded = metric(WindowsAndMessaging::SM_CXPADDEDBORDER);
(
metric(WindowsAndMessaging::SM_CXFRAME) + padded,
metric(WindowsAndMessaging::SM_CYFRAME) + padded,
)
}
} else {
(0, 0)
};
assert_eq!(
(client.right - client.left, client.bottom - client.top),
(
outer.right - outer.left - 2 * border_x,
outer.bottom - outer.top - 2 * border_y
),
"hide the caption without letting WebView2 cover resize borders: {:?}",
options.titlebar
);
assert!(!unsafe { WindowsAndMessaging::IsWindowVisible(frame.0.hwnd) }.as_bool());
}
}
#[test]
fn startup_native_frame_retains_system_chrome() {
let frame = TestFrame(
FrameWindow::new(
&WindowOptions {
center: false,
..WindowOptions::default()
},
None,
)
.unwrap(),
);
let (outer, client) = frame_rectangles(&frame);
assert!(client.bottom - client.top < outer.bottom - outer.top);
}
#[test]
fn finishing_startup_does_not_replace_focus_already_inside_the_window() {
use windows::Win32::UI::Input::KeyboardAndMouse;
let frame = TestFrame(
FrameWindow::new(
&WindowOptions {
center: false,
..WindowOptions::default()
},
None,
)
.unwrap(),
);
frame.0.show().unwrap();
let child = unsafe {
WindowsAndMessaging::CreateWindowExW(
Default::default(),
w!("BUTTON"),
None,
WindowsAndMessaging::WS_CHILD | WindowsAndMessaging::WS_VISIBLE,
0,
0,
40,
20,
Some(frame.0.hwnd),
None,
None,
None,
)
.unwrap()
};
unsafe {
let _ = KeyboardAndMouse::SetFocus(Some(child));
assert_eq!(KeyboardAndMouse::GetFocus(), child);
frame.0.focus_after_startup();
assert_eq!(KeyboardAndMouse::GetFocus(), child);
let _ = KeyboardAndMouse::SetFocus(None);
frame.0.focus_after_startup();
assert_eq!(KeyboardAndMouse::GetFocus(), frame.0.hwnd);
}
}
#[test]
fn startup_custom_frame_handles_both_non_client_geometry_messages() {
let frame = TestFrame(
FrameWindow::new(
&WindowOptions {
titlebar: TitlebarStyle::None,
resizable: false,
center: false,
..WindowOptions::default()
},
None,
)
.unwrap(),
);
let expected = RECT {
left: 10,
top: 20,
right: 610,
bottom: 420,
};
let mut rect = expected;
let mut params = WindowsAndMessaging::NCCALCSIZE_PARAMS {
rgrc: [expected; 3],
..Default::default()
};
unsafe {
WindowsAndMessaging::SendMessageW(
frame.0.hwnd,
WindowsAndMessaging::WM_NCCALCSIZE,
Some(WPARAM(0)),
Some(LPARAM(std::ptr::from_mut(&mut rect) as isize)),
);
WindowsAndMessaging::SendMessageW(
frame.0.hwnd,
WindowsAndMessaging::WM_NCCALCSIZE,
Some(WPARAM(1)),
Some(LPARAM(std::ptr::from_mut(&mut params) as isize)),
);
}
assert_eq!(rect, expected);
assert_eq!(params.rgrc[0], expected);
}
#[test]
fn startup_maximized_frame_stays_hidden_until_ready() {
let saved = WindowState {
x: 30,
y: 40,
width: 900,
height: 700,
maximized: true,
};
for (maximized, saved) in [(true, None), (false, Some(&saved))] {
let frame = TestFrame(
FrameWindow::new(
&WindowOptions {
titlebar: TitlebarStyle::None,
maximized,
center: false,
..WindowOptions::default()
},
saved,
)
.unwrap(),
);
unsafe {
assert!(WindowsAndMessaging::IsZoomed(frame.0.hwnd).as_bool());
assert!(
!WindowsAndMessaging::IsWindowVisible(frame.0.hwnd).as_bool(),
"restoring maximized geometry must not show an uninitialized window"
);
}
let (_, client) = frame_rectangles(&frame);
let work_area = super::super::command::monitor_rect(frame.0.hwnd, true).unwrap();
assert_eq!(
(client.right - client.left, client.bottom - client.top),
(
work_area.right - work_area.left,
work_area.bottom - work_area.top
),
"maximized custom content must fill, but not exceed, the monitor work area"
);
frame.0.show().unwrap();
unsafe {
assert!(WindowsAndMessaging::IsWindowVisible(frame.0.hwnd).as_bool());
assert!(WindowsAndMessaging::IsZoomed(frame.0.hwnd).as_bool());
}
assert_eq!(frame_rectangles(&frame).1, client);
}
}
#[test]
fn frameless_style_preserves_native_frame_capabilities() {
let style = window_style(&WindowOptions {
titlebar: TitlebarStyle::None,
..WindowOptions::default()
});
assert_eq!(style & WS_CAPTION, WS_CAPTION);
assert_eq!(style & WS_THICKFRAME, WS_THICKFRAME);
}
#[test]
fn non_resizable_style_drops_resize_affordances() {
let style = window_style(&WindowOptions {
resizable: false,
..WindowOptions::default()
});
assert_eq!(style & WS_THICKFRAME, WindowsAndMessaging::WINDOW_STYLE(0));
assert_eq!(style & WS_MAXIMIZEBOX, WindowsAndMessaging::WINDOW_STYLE(0));
assert_eq!(style & WS_CAPTION, WS_CAPTION);
}
#[test]
fn saved_geometry_overrides_manifest_size() {
let saved = WindowState {
x: 30,
y: 40,
width: 900,
height: 700,
maximized: false,
};
let geometry = initial_geometry(&WindowOptions::default(), Some(&saved)).unwrap();
assert_eq!(geometry, (30, 40, 900, 700));
}
#[test]
fn default_geometry_uses_system_placement() {
let geometry = initial_geometry(&WindowOptions::default(), None).unwrap();
assert_eq!(
geometry,
(
WindowsAndMessaging::CW_USEDEFAULT,
WindowsAndMessaging::CW_USEDEFAULT,
1200,
800
)
);
}
}