use crate::{DesktopEvent, EventResponse};
use anyhow::Result;
use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller;
use windows::core::Error as WindowsError;
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, RECT, SIZE, WPARAM};
use windows::Win32::Graphics::Gdi;
use windows::Win32::UI::HiDpi;
use windows::Win32::UI::WindowsAndMessaging::{self, MSG};
#[cfg(feature = "native-capture")]
use super::capture::CAPTURE_WAKE_MESSAGE;
use super::command::execute_window_command;
use super::event::{logical_dimension, physical_to_logical, size_event_transition};
use super::nonclient::{
defer_startup_close, initialize_frame, non_client_calc_size, non_client_hit_test, redraw_frame,
startup_background, startup_is_ready,
};
use super::state::{save_window_state, set_window_state, with_window_state, FrameState};
use super::webview::mirror_event;
#[cfg(feature = "application-ipc")]
use super::IPC_WAKE_MESSAGE;
#[cfg(feature = "local-server")]
use super::OWNER_LOST_MESSAGE;
use super::{WAKE_MESSAGE, WINDOW_ID};
pub(super) fn message_loop() -> Result<()> {
let mut msg = MSG::default();
loop {
let result = unsafe { WindowsAndMessaging::GetMessageW(&mut msg, None, 0, 0).0 };
match result {
-1 => return Err(WindowsError::from_thread().into()),
0 => return Ok(()),
_ => unsafe {
let _ = WindowsAndMessaging::TranslateMessage(&msg);
WindowsAndMessaging::DispatchMessageW(&msg);
},
}
}
}
pub(super) extern "system" fn window_proc(
hwnd: HWND,
msg: u32,
w_param: WPARAM,
l_param: LPARAM,
) -> LRESULT {
if cfg!(feature = "native-dwm-frame")
&& super::nonclient::is_dwm_frame(hwnd)
&& msg != WindowsAndMessaging::WM_NCCALCSIZE
{
let mut result = LRESULT(0);
if unsafe {
windows::Win32::Graphics::Dwm::DwmDefWindowProc(
hwnd,
msg,
w_param,
l_param,
&mut result,
)
}
.as_bool()
{
return result;
}
}
match msg {
WindowsAndMessaging::WM_NCCREATE => {
if let Err(error) = initialize_frame(hwnd, l_param) {
eprintln!(
"WebUI: failed to initialize the native window frame: {error}; help: verify the native window creation parameters"
);
return LRESULT(0);
}
unsafe { WindowsAndMessaging::DefWindowProcW(hwnd, msg, w_param, l_param) }
}
WindowsAndMessaging::WM_SETFOCUS => {
let controller =
super::state::with_window_state_result(hwnd, |state| state.controller.clone());
if let Some(controller) = controller {
if let Err(error) = super::webview::focus_controller_from_frame(hwnd, &controller) {
eprintln!(
"WebUI: failed to transfer native keyboard focus into WebView2: {error}"
);
}
}
LRESULT(0)
}
super::APP_WAKE_MESSAGE => {
let tasks = super::state::with_window_state_result(hwnd, |state| {
state.application_tasks.clone()
});
if let Some(tasks) = tasks {
tasks.drain(w_param.0);
}
LRESULT(0)
}
#[cfg(feature = "native-capture")]
CAPTURE_WAKE_MESSAGE => {
with_window_state(hwnd, |state| {
if let Some(registration) = &state.capture_registration {
registration.drain(
w_param.0,
&state.webview,
&state.controller,
(hwnd, state.content),
);
}
});
LRESULT(0)
}
#[cfg(feature = "application-ipc")]
IPC_WAKE_MESSAGE => {
let ipc = super::state::with_window_state_result(hwnd, |state| state.ipc.clone());
if let Some(Some(ipc)) = ipc {
ipc.drain(w_param.0);
}
LRESULT(0)
}
#[cfg(feature = "application-ipc")]
WindowsAndMessaging::WM_TIMER => {
let ipc = super::state::with_window_state_result(hwnd, |state| state.ipc.clone());
if let Some(Some(ipc)) = ipc {
ipc.expire_hello(w_param.0);
#[cfg(feature = "local-server")]
ipc.expire_data(w_param.0);
}
LRESULT(0)
}
WAKE_MESSAGE => {
drain_commands(hwnd);
LRESULT(0)
}
#[cfg(feature = "local-server")]
OWNER_LOST_MESSAGE => {
let retired = super::state::with_window_state_result(hwnd, |state| {
crate::local_server::should_close_for_cookie(
state.local_lifetime.as_ref(),
state.owner_close_cookie,
w_param.0,
)
})
.unwrap_or(false);
if retired {
if let Err(error) = unsafe { WindowsAndMessaging::DestroyWindow(hwnd) } {
eprintln!("WebUI: failed to close retired local-server window: {error}");
}
}
LRESULT(0)
}
WindowsAndMessaging::WM_NCCALCSIZE => non_client_calc_size(hwnd, msg, w_param, l_param),
WindowsAndMessaging::WM_NCHITTEST => non_client_hit_test(hwnd, msg, w_param, l_param),
WindowsAndMessaging::WM_DWMCOMPOSITIONCHANGED => {
refresh_frame(hwnd);
redraw_frame(hwnd);
LRESULT(0)
}
WindowsAndMessaging::WM_DPICHANGED => {
#[cfg(feature = "native-capture")]
with_window_state(hwnd, |state| {
if let Some(registration) = &state.capture_registration {
registration.owner_viewport_changed();
}
});
apply_suggested_rect(hwnd, l_param);
refresh_frame(hwnd);
dispatch_scale_changed(hwnd, w_param);
LRESULT(0)
}
WindowsAndMessaging::WM_ACTIVATE => {
refresh_frame(hwnd);
dispatch_activation(hwnd, w_param);
LRESULT(0)
}
WindowsAndMessaging::WM_SETTINGCHANGE => {
refresh_frame(hwnd);
dispatch_theme_changed(hwnd);
unsafe { WindowsAndMessaging::DefWindowProcW(hwnd, msg, w_param, l_param) }
}
WindowsAndMessaging::WM_GETMINMAXINFO => {
apply_size_limits(hwnd, l_param);
LRESULT(0)
}
WindowsAndMessaging::WM_SIZE => {
dispatch_size_changed(hwnd, w_param);
LRESULT(0)
}
WindowsAndMessaging::WM_MOVE => {
dispatch_moved(hwnd);
LRESULT(0)
}
WindowsAndMessaging::WM_CLOSE => {
close_window(hwnd);
LRESULT(0)
}
WindowsAndMessaging::WM_ERASEBKGND => erase_background(hwnd, w_param),
WindowsAndMessaging::WM_DESTROY => {
destroy_window(hwnd);
LRESULT(0)
}
_ => unsafe { WindowsAndMessaging::DefWindowProcW(hwnd, msg, w_param, l_param) },
}
}
fn drain_commands(hwnd: HWND) {
let commands =
super::state::with_window_state_result(hwnd, |state| state.window_handle.drain_commands())
.unwrap_or_default();
for command in commands {
execute_window_command(hwnd, command);
}
}
fn apply_suggested_rect(hwnd: HWND, l_param: LPARAM) {
if l_param.0 == 0 {
return;
}
let rect = unsafe { *(l_param.0 as *const RECT) };
unsafe {
let _ = WindowsAndMessaging::SetWindowPos(
hwnd,
None,
rect.left,
rect.top,
rect.right.saturating_sub(rect.left),
rect.bottom.saturating_sub(rect.top),
WindowsAndMessaging::SWP_NOZORDER | WindowsAndMessaging::SWP_NOACTIVATE,
);
}
}
fn dispatch_scale_changed(hwnd: HWND, w_param: WPARAM) {
let dpi = u16::try_from((w_param.0 >> 16) & 0xffff).unwrap_or(96);
let event = DesktopEvent::ScaleFactorChanged {
scale: f64::from(dpi) / 96.0,
};
publish(hwnd, &event);
}
fn dispatch_activation(hwnd: HWND, w_param: WPARAM) {
let event = if w_param.0 & 0xffff == 0 {
DesktopEvent::WindowBlurred {
window_id: WINDOW_ID,
}
} else {
DesktopEvent::WindowFocused {
window_id: WINDOW_ID,
}
};
publish(hwnd, &event);
}
fn dispatch_theme_changed(hwnd: HWND) {
let event = DesktopEvent::ThemeChanged {
dark: system_dark(),
};
publish(hwnd, &event);
}
pub(super) fn system_dark() -> bool {
let color = unsafe { Gdi::GetSysColor(Gdi::COLOR_WINDOW) };
let red = u32::from(u8::try_from(color & 0xff).unwrap_or(0));
let green = u32::from(u8::try_from((color >> 8) & 0xff).unwrap_or(0));
let blue = u32::from(u8::try_from((color >> 16) & 0xff).unwrap_or(0));
(red * 299 + green * 587 + blue * 114) / 1000 < 128
}
fn apply_size_limits(hwnd: HWND, l_param: LPARAM) {
if l_param.0 == 0 {
return;
}
with_window_state(hwnd, |state| {
let info = unsafe { &mut *(l_param.0 as *mut WindowsAndMessaging::MINMAXINFO) };
if let Some(value) = state.options.min_width.and_then(|v| i32::try_from(v).ok()) {
info.ptMinTrackSize.x = value;
}
if let Some(value) = state.options.min_height.and_then(|v| i32::try_from(v).ok()) {
info.ptMinTrackSize.y = value;
}
if let Some(value) = state.options.max_width.and_then(|v| i32::try_from(v).ok()) {
info.ptMaxTrackSize.x = value;
}
if let Some(value) = state.options.max_height.and_then(|v| i32::try_from(v).ok()) {
info.ptMaxTrackSize.y = value;
}
});
}
fn dispatch_size_changed(hwnd: HWND, w_param: WPARAM) {
with_window_state(hwnd, |state| {
if let Err(error) = resize_content(hwnd, state) {
eprintln!("WebUI: failed to resize native web content: {error}");
}
let size = get_window_size(state.content);
let size_code = u32::try_from(w_param.0).unwrap_or(WindowsAndMessaging::SIZE_RESTORED);
let (current, transition) = size_event_transition(state.window_state.get(), size_code);
state.window_state.set(current);
if let Some(event) = transition {
emit(state, &event);
}
let dpi = window_dpi(hwnd);
let event = DesktopEvent::WindowResized {
window_id: WINDOW_ID,
width: logical_dimension(size.cx, dpi),
height: logical_dimension(size.cy, dpi),
};
emit(state, &event);
save_window_state(hwnd, state);
});
}
pub(super) fn refresh_frame(hwnd: HWND) {
with_window_state(hwnd, |state| {
if matches!(state.options.titlebar, crate::TitlebarStyle::None) {
if let Err(error) = super::create::extend_frameless_frame(hwnd) {
eprintln!("WebUI: failed to refresh the frameless DWM border: {error}");
}
}
if let Err(error) = resize_content(hwnd, state) {
eprintln!("WebUI: failed to refresh native caption controls: {error}");
}
});
}
fn resize_content(hwnd: HWND, state: &FrameState) -> Result<()> {
if state.content != hwnd {
super::native_frame::resize_browser(hwnd, state.content)?;
}
if state.app_window.refresh(hwnd)? {
state.app_window.publish_metrics(&state.webview)?;
}
set_controller_bounds(&state.controller, state.content)
}
fn dispatch_moved(hwnd: HWND) {
with_window_state(hwnd, |state| {
let mut rect = RECT::default();
if unsafe { WindowsAndMessaging::GetWindowRect(hwnd, &mut rect) }.is_err() {
return;
}
let dpi = window_dpi(hwnd);
emit(
state,
&DesktopEvent::WindowMoved {
window_id: WINDOW_ID,
x: physical_to_logical(rect.left, dpi),
y: physical_to_logical(rect.top, dpi),
},
);
save_window_state(hwnd, state);
});
}
fn close_window(hwnd: HWND) {
if !startup_is_ready(hwnd) {
defer_startup_close(hwnd);
return;
}
let prevented = super::state::with_window_state_result(hwnd, |state| {
let event = DesktopEvent::WindowCloseRequested {
window_id: WINDOW_ID,
};
let prevented = state.events.dispatch(&event) == EventResponse::PreventDefault;
mirror_event(&state.webview, &event);
prevented
})
.unwrap_or(false);
if prevented {
return;
}
unsafe {
let _ = WindowsAndMessaging::DestroyWindow(hwnd);
}
}
fn erase_background(hwnd: HWND, w_param: WPARAM) -> LRESULT {
let color = super::state::with_window_state_result(hwnd, |state| state.options.background)
.flatten()
.or_else(|| startup_background(hwnd));
let native_glass = cfg!(feature = "native-dwm-frame")
&& super::nonclient::is_dwm_frame(hwnd)
&& super::state::window_style_bits(hwnd, WindowsAndMessaging::GWL_STYLE)
& WindowsAndMessaging::WS_CAPTION.0
!= 0;
if color.is_none() && !native_glass {
return LRESULT(0);
}
let hdc = Gdi::HDC(w_param.0 as *mut std::ffi::c_void);
let mut rect = RECT::default();
if unsafe { WindowsAndMessaging::GetClientRect(hwnd, &mut rect) }.is_err() {
return LRESULT(0);
}
unsafe {
if let Some(color) = color {
let rgb = u32::from(color.r) | (u32::from(color.g) << 8) | (u32::from(color.b) << 16);
let brush = Gdi::CreateSolidBrush(windows::Win32::Foundation::COLORREF(rgb));
let _ = Gdi::FillRect(hdc, &rect, brush);
let _ = Gdi::DeleteObject(brush.into());
}
if native_glass {
match super::native_frame::caption_rect(hwnd) {
Ok(caption) => {
let black = Gdi::GetStockObject(Gdi::BLACK_BRUSH);
let _ = Gdi::FillRect(hdc, &caption, Gdi::HBRUSH(black.0));
}
Err(error) => eprintln!("WebUI: failed to clear native caption glass: {error}"),
}
}
}
LRESULT(1)
}
fn destroy_window(hwnd: HWND) {
#[cfg(feature = "native-picker")]
with_window_state(hwnd, |state| {
if let Some(picker) = &state.picker {
picker.notify_picker_closed();
}
});
#[cfg(feature = "native-dialogs")]
with_window_state(hwnd, |state| {
if let Some(dialogs) = &state.dialogs {
dialogs.notify_closed();
}
});
#[cfg(feature = "native-clipboard")]
with_window_state(hwnd, |state| {
if let Some(clipboard) = &state.clipboard {
clipboard.notify_closed();
}
});
#[cfg(feature = "local-server")]
with_window_state(hwnd, |state| {
if let Some(controls) = &state.local_controls {
controls.close();
}
});
#[cfg(feature = "native-capture")]
with_window_state(hwnd, |state| {
if let Some(registration) = &state.capture_registration {
registration.close();
}
});
#[cfg(feature = "application-ipc")]
{
let ipc = super::state::with_window_state_result(hwnd, |state| state.ipc.clone());
if let Some(Some(ipc)) = ipc {
ipc.close();
}
}
with_window_state(hwnd, |state| {
emit(
state,
&DesktopEvent::WindowClosed {
window_id: WINDOW_ID,
},
);
});
drop(set_window_state(hwnd, None));
unsafe { WindowsAndMessaging::PostQuitMessage(0) };
}
pub(super) fn emit(state: &FrameState, event: &DesktopEvent) {
let _ = state.events.dispatch(event);
mirror_event(&state.webview, event);
}
pub(super) fn publish(hwnd: HWND, event: &DesktopEvent) {
with_window_state(hwnd, |state| emit(state, event));
}
pub(super) fn set_controller_bounds(
controller: &ICoreWebView2Controller,
hwnd: HWND,
) -> Result<()> {
let size = get_window_size(hwnd);
unsafe { controller.SetBounds(full_client_bounds(size))? };
Ok(())
}
fn full_client_bounds(size: SIZE) -> RECT {
RECT {
left: 0,
top: 0,
right: size.cx,
bottom: size.cy,
}
}
fn window_dpi(hwnd: HWND) -> u32 {
unsafe { HiDpi::GetDpiForWindow(hwnd) }
}
pub(super) fn get_window_size(hwnd: HWND) -> SIZE {
let mut client_rect = RECT::default();
let _ = unsafe { WindowsAndMessaging::GetClientRect(hwnd, &mut client_rect) };
SIZE {
cx: client_rect.right.saturating_sub(client_rect.left),
cy: client_rect.bottom.saturating_sub(client_rect.top),
}
}
#[cfg(test)]
mod overlay_bounds_tests {
use super::*;
#[test]
fn webview_fills_entire_client_even_under_native_caption() {
assert_eq!(
full_client_bounds(SIZE { cx: 1280, cy: 720 }),
RECT {
left: 0,
top: 0,
right: 1280,
bottom: 720,
}
);
}
}