From cb9fa7042fc3334cdbe55457c923d7ce8e9d9403 Mon Sep 17 00:00:00 2001 From: "Ayberk A." <105879357+AyberkA0@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:31:03 +0300 Subject: [PATCH] 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). --- apps/vectorcraft/src/main.rs | 12 ++++- apps/vectorcraft/src/window_frame.rs | 75 ++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 apps/vectorcraft/src/window_frame.rs diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index 9bd591d6..e5a3736d 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -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, } 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 })) }), ) }); diff --git a/apps/vectorcraft/src/window_frame.rs b/apps/vectorcraft/src/window_frame.rs new file mode 100644 index 00000000..6a0f1c1b --- /dev/null +++ b/apps/vectorcraft/src/window_frame.rs @@ -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; + +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 { + 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 { + 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 { + None + } +}