mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Fix #1758: keep the custom title bar window borderless on Windows
winit 0.30 gives an undecorated window WS_CAPTION | WS_BORDER; on some Windows systems the compositor still reserves that frame, so the content is drawn offset while the pointer isn't, and the cursor no longer points at what is under its tip. Strip the two styles once and again each frame (winit restores them on maximize/restore), as PhotoCraft does (storytold/photocraft#2401).
This commit is contained in:
1 parent
bb8897a2eb
commit
cb9fa7042f
2 files changed
+86
-1
No files matched your search
@@ -45,6 +45,7 @@ mod printing;
|
||||
#[cfg(all(windows, not(target_vendor = "win7")))]
|
||||
mod system_fonts;
|
||||
mod window;
|
||||
mod window_frame;
|
||||
mod xwayland;
|
||||
|
||||
use vectorcraft_engine::Session;
|
||||
@@ -62,6 +63,8 @@ struct App {
|
||||
graphics_lost: bool,
|
||||
/// Frames the UI has run (see [`end_before_teardown`]).
|
||||
frames: u64,
|
||||
/// Keeps the custom title bar's window borderless (Windows, #1758).
|
||||
window_frame: Option<window_frame::WindowFrameFn>,
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
@@ -72,6 +75,11 @@ impl eframe::App for App {
|
||||
file_access::unconfined(|| self.logic_frame(ctx, frame));
|
||||
}
|
||||
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
// winit gives the window its caption back when it changes the window's flags (maximize,
|
||||
// restore, …): take it off again before the frame is laid out (#1758).
|
||||
if let Some(window_frame) = self.window_frame.as_mut() {
|
||||
window_frame();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
if self.app.services.native_menu.is_some() {
|
||||
mac_menu::raw_input_hook(raw);
|
||||
@@ -858,7 +866,9 @@ fn main() -> std::process::ExitCode {
|
||||
// The first frame is due from now (`gpu::watch_first_frame`).
|
||||
#[cfg(feature = "wgpu")]
|
||||
created.ready();
|
||||
Ok(Box::new(App { app, graphics_loss, graphics_lost: false, frames: 0 }))
|
||||
// The content and the pointer stay aligned on Windows (#1758).
|
||||
let window_frame = window_frame::service(cc, custom_titlebar);
|
||||
Ok(Box::new(App { app, graphics_loss, graphics_lost: false, frames: 0, window_frame }))
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//! Keeps the custom title bar's undecorated window truly borderless on Windows (#1758).
|
||||
//!
|
||||
//! winit 0.30 gives an undecorated window the `WS_CAPTION | WS_BORDER` styles and hides the
|
||||
//! non-client area by spreading the client rect over it (`WM_NCCALCSIZE`). On some systems the
|
||||
//! desktop compositor still reserves that frame for the window surface: the content is drawn
|
||||
//! offset by the caption and border while the pointer keeps the un-offset client coordinates, so
|
||||
//! it no longer points at what is drawn under it (rust-windowing/winit#2419; PhotoCraft fixed the
|
||||
//! same in storytold/photocraft#2401). [`service`] strips the two styles and has Windows
|
||||
//! recalculate the frame; winit puts them back whenever it changes the window flags (maximize,
|
||||
//! restore, …), so the returned guard runs every frame.
|
||||
|
||||
/// Run once per frame; a no-op once the window is borderless.
|
||||
pub type WindowFrameFn = Box<dyn FnMut()>;
|
||||
|
||||
pub use platform::service;
|
||||
|
||||
#[cfg(all(windows, feature = "wgpu"))]
|
||||
mod platform {
|
||||
use eframe::wgpu::rwh::{HasWindowHandle, RawWindowHandle};
|
||||
use winsafe::{HWND, HwndPlace, POINT, SIZE, co};
|
||||
|
||||
/// A guard that keeps the window borderless each frame, or `None` with the system title bar
|
||||
/// (the window is decorated) or when its handle can't be found.
|
||||
pub fn service(cc: &eframe::CreationContext<'_>, custom_titlebar: bool) -> Option<super::WindowFrameFn> {
|
||||
if !custom_titlebar {
|
||||
return None;
|
||||
}
|
||||
let target = match cc.window_handle().ok()?.as_raw() {
|
||||
RawWindowHandle::Win32(h) => h.hwnd.get(),
|
||||
_ => return None,
|
||||
};
|
||||
let hwnd = find_window(target)?;
|
||||
strip(&hwnd);
|
||||
Some(Box::new(move || strip(&hwnd)))
|
||||
}
|
||||
|
||||
/// The top-level window with the given raw handle: `EnumWindows` hands out real `HWND`s, so
|
||||
/// this needs no `unsafe` to wrap a raw one.
|
||||
fn find_window(target: isize) -> Option<HWND> {
|
||||
let mut found = None;
|
||||
let _ = winsafe::EnumWindows(|hwnd| {
|
||||
if hwnd.ptr() as isize == target {
|
||||
found = Some(hwnd);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
found
|
||||
}
|
||||
|
||||
/// Removes `WS_CAPTION` (which includes `WS_BORDER`) and has Windows recalculate the frame;
|
||||
/// the sizing border stays, so edge resizing and snapping keep working.
|
||||
fn strip(hwnd: &HWND) {
|
||||
let style = hwnd.style();
|
||||
let without = style & !(co::WS::CAPTION | co::WS::BORDER);
|
||||
if without.raw() == style.raw() {
|
||||
return;
|
||||
}
|
||||
hwnd.set_style(without);
|
||||
let _ = hwnd.SetWindowPos(
|
||||
HwndPlace::None,
|
||||
POINT::new(),
|
||||
SIZE::new(),
|
||||
co::SWP::FRAMECHANGED | co::SWP::NOMOVE | co::SWP::NOSIZE | co::SWP::NOZORDER | co::SWP::NOACTIVATE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(windows, feature = "wgpu")))]
|
||||
mod platform {
|
||||
pub fn service(_cc: &eframe::CreationContext<'_>, _custom_titlebar: bool) -> Option<super::WindowFrameFn> {
|
||||
None
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user