use crate::{DesktopEvent, DesktopHostMessage, WindowCommand};
use webview2_com::CoTaskMemPWSTR;
use windows::Win32::Foundation::{HWND, LPARAM, POINT, RECT, WPARAM};
use windows::Win32::Graphics::Gdi;
use windows::Win32::UI::Input::KeyboardAndMouse;
use windows::Win32::UI::WindowsAndMessaging;
use super::state::{with_window_state, FrameState};
use super::{message, WINDOW_ID};
pub(super) fn execute_window_command(hwnd: HWND, command: WindowCommand) {
match command {
WindowCommand::SetTitle(title) => set_title(hwnd, &title),
WindowCommand::SetBackground(color) => {
with_window_state(hwnd, |state| {
if let Err(error) =
super::webview::update_background(&state.controller, &state.webview, color)
{
eprintln!("WebUI: failed to update window background: {error}");
}
});
}
WindowCommand::SetSize { width, height } => set_size(hwnd, width, height),
WindowCommand::Minimize => show_window(hwnd, WindowsAndMessaging::SW_MINIMIZE),
WindowCommand::Maximize => show_window(hwnd, WindowsAndMessaging::SW_MAXIMIZE),
WindowCommand::Unmaximize => show_window(hwnd, WindowsAndMessaging::SW_RESTORE),
WindowCommand::Close => post_close(hwnd),
WindowCommand::Focus => focus_window(hwnd),
WindowCommand::Center => center_window(hwnd),
WindowCommand::StartDrag => start_drag(hwnd),
WindowCommand::SetAlwaysOnTop(value) => set_always_on_top(hwnd, value),
WindowCommand::SetFullscreen(value) => {
with_window_state(hwnd, |state| set_fullscreen(hwnd, state, value));
}
}
}
pub(super) fn execute_host_message(hwnd: HWND, message: DesktopHostMessage) {
match message {
DesktopHostMessage::StartDrag => start_drag(hwnd),
DesktopHostMessage::Minimize => show_window(hwnd, WindowsAndMessaging::SW_MINIMIZE),
DesktopHostMessage::ToggleMaximize => toggle_maximize(hwnd),
DesktopHostMessage::Close => post_close(hwnd),
}
}
pub(super) fn toggle_maximize(hwnd: HWND) {
let zoomed = unsafe { WindowsAndMessaging::IsZoomed(hwnd) }.as_bool();
let command = if zoomed {
WindowsAndMessaging::SC_RESTORE
} else {
WindowsAndMessaging::SC_MAXIMIZE
};
let Ok(command) = usize::try_from(command) else {
return;
};
unsafe {
WindowsAndMessaging::SendMessageW(
hwnd,
WindowsAndMessaging::WM_SYSCOMMAND,
Some(WPARAM(command)),
Some(LPARAM(0)),
);
}
}
pub(super) fn set_fullscreen(hwnd: HWND, state: &FrameState, enable: bool) {
if state.fullscreen.get() == enable {
return;
}
let previous = state.fullscreen.replace(enable);
if let Err(error) = state.app_window.set_fullscreen(enable) {
state.fullscreen.set(previous);
eprintln!("WebUI: failed to change the native fullscreen presenter: {error}");
return;
}
message::refresh_frame(hwnd);
let event = if enable {
DesktopEvent::WindowEnteredFullscreen {
window_id: WINDOW_ID,
}
} else {
DesktopEvent::WindowLeftFullscreen {
window_id: WINDOW_ID,
}
};
message::emit(state, &event);
}
pub(super) fn monitor_rect(hwnd: HWND, work_area: bool) -> Option<RECT> {
let monitor = unsafe { Gdi::MonitorFromWindow(hwnd, Gdi::MONITOR_DEFAULTTONEAREST) };
let mut info = Gdi::MONITORINFO {
cbSize: u32::try_from(std::mem::size_of::<Gdi::MONITORINFO>()).unwrap_or(40),
..Default::default()
};
let ok = unsafe { Gdi::GetMonitorInfoW(monitor, &mut info) }.as_bool();
ok.then_some(if work_area {
info.rcWork
} else {
info.rcMonitor
})
}
pub(super) fn center_window(hwnd: HWND) {
let mut rect = RECT::default();
if unsafe { WindowsAndMessaging::GetWindowRect(hwnd, &mut rect) }.is_err() {
return;
}
let Some(work) = monitor_rect(hwnd, true) else {
return;
};
let width = rect.right.saturating_sub(rect.left);
let height = rect.bottom.saturating_sub(rect.top);
let x = work.left + (work.right.saturating_sub(work.left).saturating_sub(width)) / 2;
let y = work.top + (work.bottom.saturating_sub(work.top).saturating_sub(height)) / 2;
unsafe {
let _ = WindowsAndMessaging::SetWindowPos(
hwnd,
None,
x,
y,
0,
0,
WindowsAndMessaging::SWP_NOSIZE | WindowsAndMessaging::SWP_NOZORDER,
);
}
}
fn set_title(hwnd: HWND, title: &str) {
let title = CoTaskMemPWSTR::from(title);
unsafe {
let _ = WindowsAndMessaging::SetWindowTextW(hwnd, *title.as_ref().as_pcwstr());
}
}
fn set_size(hwnd: HWND, width: u32, height: u32) {
if cfg!(feature = "native-dwm-frame") && super::nonclient::is_dwm_frame(hwnd) {
if let Err(error) = super::native_frame::resize_client(hwnd, width, height) {
eprintln!("WebUI: failed to resize the native client viewport: {error}");
}
return;
}
let (Ok(width), Ok(height)) = (i32::try_from(width), i32::try_from(height)) else {
return;
};
unsafe {
let _ = WindowsAndMessaging::SetWindowPos(
hwnd,
None,
0,
0,
width,
height,
WindowsAndMessaging::SWP_NOMOVE | WindowsAndMessaging::SWP_NOZORDER,
);
}
}
fn show_window(hwnd: HWND, command: WindowsAndMessaging::SHOW_WINDOW_CMD) {
unsafe {
let _ = WindowsAndMessaging::ShowWindow(hwnd, command);
}
}
fn post_close(hwnd: HWND) {
unsafe {
let _ = WindowsAndMessaging::PostMessageW(
Some(hwnd),
WindowsAndMessaging::WM_CLOSE,
WPARAM(0),
LPARAM(0),
);
}
}
fn focus_window(hwnd: HWND) {
unsafe {
let _ = KeyboardAndMouse::SetFocus(Some(hwnd));
}
}
fn start_drag(hwnd: HWND) {
let Ok(caption) = usize::try_from(WindowsAndMessaging::HTCAPTION) else {
return;
};
let mut cursor = POINT::default();
if unsafe { WindowsAndMessaging::GetCursorPos(&mut cursor) }.is_err() {
return;
}
let anchor = pack_point(cursor);
unsafe {
let _ = KeyboardAndMouse::ReleaseCapture();
WindowsAndMessaging::SendMessageW(
hwnd,
WindowsAndMessaging::WM_NCLBUTTONDOWN,
Some(WPARAM(caption)),
Some(LPARAM(anchor)),
);
}
}
fn pack_point(point: POINT) -> isize {
let x = point.x.cast_unsigned() & 0xffff;
let y = point.y.cast_unsigned() & 0xffff;
isize::try_from((y << 16) | x).unwrap_or_default()
}
fn set_always_on_top(hwnd: HWND, value: bool) {
let insert_after = if value {
WindowsAndMessaging::HWND_TOPMOST
} else {
WindowsAndMessaging::HWND_NOTOPMOST
};
unsafe {
let _ = WindowsAndMessaging::SetWindowPos(
hwnd,
Some(insert_after),
0,
0,
0,
0,
WindowsAndMessaging::SWP_NOMOVE | WindowsAndMessaging::SWP_NOSIZE,
);
}
}
pub(super) fn screen_point(l_param: LPARAM) -> POINT {
let bits = l_param.0;
let low = u16::try_from(bits & 0xffff).unwrap_or_default();
let high = u16::try_from((bits >> 16) & 0xffff).unwrap_or_default();
POINT {
x: i32::from(low.cast_signed()),
y: i32::from(high.cast_signed()),
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
#[test]
fn packed_points_round_trip_through_lparam() {
for point in [
POINT { x: 0, y: 0 },
POINT { x: 1280, y: 720 },
POINT { x: -1920, y: -40 },
] {
let unpacked = screen_point(LPARAM(pack_point(point)));
assert_eq!((unpacked.x, unpacked.y), (point.x, point.y));
}
}
}