Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/wm-platform/src/native_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,17 @@ pub trait NativeWindowWindowsExt {
/// This method is only available on Windows.
fn has_window_style_ex(&self, style: WINDOW_EX_STYLE) -> bool;

/// DPI of the window's current per-monitor DPI-awareness context.
///
/// This lags a cross-monitor move until Windows delivers
/// `WM_DPICHANGED`, so comparing it against the target monitor's DPI
/// reveals whether the window still needs a scale correction.
///
/// # Platform-specific
///
/// This method is only available on Windows.
fn dpi(&self) -> crate::Result<u32>;

/// Thin wrapper around [`SetWindowPos`](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos).
///
/// # Platform-specific
Expand Down Expand Up @@ -359,6 +370,10 @@ impl NativeWindowWindowsExt for NativeWindow {
self.inner.has_window_style_ex(style)
}

fn dpi(&self) -> crate::Result<u32> {
self.inner.dpi()
}

fn set_window_pos(
&self,
z_order: &WindowZOrder,
Expand Down
15 changes: 15 additions & 0 deletions packages/wm-platform/src/platform_impl/windows/native_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use windows::{
PROCESS_QUERY_LIMITED_INFORMATION,
},
UI::{
HiDpi::GetDpiForWindow,
Input::KeyboardAndMouse::{
SendInput, INPUT, INPUT_0, INPUT_MOUSE, MOUSEINPUT,
},
Expand Down Expand Up @@ -161,6 +162,20 @@ impl NativeWindow {
Ok((f64::from(frame.width()), f64::from(frame.height())))
}

/// Implements [`NativeWindow::dpi`].
pub(crate) fn dpi(&self) -> crate::Result<u32> {
let dpi = unsafe { GetDpiForWindow(self.hwnd()) };

// `GetDpiForWindow` returns 0 for an invalid window handle.
if dpi == 0 {
return Err(crate::Error::Platform(
"`GetDpiForWindow` returned an invalid DPI.".to_string(),
));
}

Ok(dpi)
}

/// Implements [`NativeWindow::is_valid`].
pub(crate) fn is_valid(&self) -> bool {
unsafe { IsWindow(self.hwnd()) }.as_bool()
Expand Down
40 changes: 35 additions & 5 deletions packages/wm/src/commands/general/platform_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,13 +445,17 @@ fn reposition_window(
_ => {
swp_flags |= SWP_FRAMECHANGED;

// Capture whether a scale correction is needed *before* the
// move, while the window's DPI context still reflects its old
// monitor.
let needs_dpi_correction = needs_dpi_scale_correction(window);

window.native().set_window_pos(z_order, &rect, swp_flags)?;

// When there's a mismatch between the DPI of the monitor and the
// window, the window might be sized incorrectly after the first
// move. If we set the position twice, inconsistencies after the
// first move are resolved.
if window.has_pending_dpi_adjustment() {
// A second `SetWindowPos`, issued after Windows has updated the
// window's DPI in response to the first, re-sizes it under the
// now-correct scale.
if needs_dpi_correction {
window.native().set_window_pos(z_order, &rect, swp_flags)?;
}
}
Expand All @@ -471,6 +475,32 @@ fn reposition_window(
Ok(())
}

/// Whether a window needs a corrective second `SetWindowPos` to be sized
/// at the right scale for the monitor it's being placed on.
///
/// A window whose DPI-awareness context still reflects a monitor other
/// than its target is sized under the wrong scale by the first
/// `SetWindowPos` (e.g. leaking onto the neighbouring monitor); a second
/// call, once Windows has updated the window's DPI in response to the
/// first, corrects it.
///
/// The mismatch is detected live by comparing the window's current DPI
/// against its target monitor, so freshly-opened windows are caught too:
/// the OS may spawn a window straight onto its target monitor, where the
/// `has_pending_dpi_adjustment` hint (set only on a cross-monitor spawn)
/// misses that the window's initial context carries the wrong DPI. That
/// hint is still honoured as a fallback. A window already at the correct
/// scale needs no second call.
#[cfg(target_os = "windows")]
fn needs_dpi_scale_correction(window: &WindowContainer) -> bool {
let window_dpi = window.native().dpi().ok();
let monitor_dpi = window.monitor().map(|m| m.native_properties().dpi);
let has_dpi_mismatch =
matches!((window_dpi, monitor_dpi), (Some(w), Some(m)) if w != m);

has_dpi_mismatch || window.has_pending_dpi_adjustment()
}

fn jump_cursor(
focused_container: Container,
state: &WmState,
Expand Down
75 changes: 72 additions & 3 deletions packages/wm/src/events/handle_window_moved_or_resized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
},
events::handle_window_moved_or_resized_end,
models::{Monitor, NonTilingWindow, WindowContainer},
traits::{CommonGetters, WindowGetters},
traits::{CommonGetters, PositionGetters, WindowGetters},
user_config::UserConfig,
wm_state::WmState,
};
Expand Down Expand Up @@ -350,7 +350,35 @@ pub fn handle_window_moved_or_resized(
)?;
}
}
_ => {}
_ => {
// A tiling window changed size without us commanding it. The
// usual cause is an app resizing itself in response to
// `WM_DPICHANGED` after we moved it onto a monitor with a
// different DPI: it snaps to its old size scaled by the DPI
// ratio, overriding the frame we set and overflowing its tile
// onto the neighbouring monitor. This arrives after our
// `SetWindowPos`, so the size can only be reasserted here, once
// the DPI change has settled.
//
// Act only while a DPI adjustment is pending and only when the
// frame's *size* diverges from the window's computed rect: a
// cross-workspace move first emits position-only echoes (from
// re-tiling) whose size already matches, and consuming the flag
// on those would leave nothing to correct the later self-resize.
// Reasserting via a redraw and clearing the flag on the real
// divergence fixes the leak while firing at most once per DPI
// transition, so it can't feed back into a redraw loop.
if window.has_pending_dpi_adjustment() {
let target = window
.to_rect()?
.apply_delta(&window.total_border_delta()?, None);

if size_diverged(&frame_position, &target) {
window.set_has_pending_dpi_adjustment(false);
state.pending_sync.queue_container_to_redraw(window.clone());
}
}
}
}
}

Expand Down Expand Up @@ -539,11 +567,26 @@ fn is_in_corner(window_frame: &Rect, monitor_rect: &Rect) -> bool {
(is_left_corner || is_right_corner) && is_bottom_of_monitor
}

/// Gets whether a window's actual frame has diverged in *size* from the
/// size we last commanded for it.
///
/// Used to distinguish an app resizing itself (e.g. after `WM_DPICHANGED`)
/// from position-only echoes emitted while re-tiling, which keep the same
/// size. Sub-tile differences from borders and shadows are ignored via a
/// threshold; a DPI-scaled self-resize is off by the scale ratio, i.e.
/// hundreds of pixels, so it clears the threshold comfortably.
fn size_diverged(frame: &Rect, target: &Rect) -> bool {
const DIVERGENCE_THRESHOLD_PX: i32 = 50;

(frame.width() - target.width()).abs() > DIVERGENCE_THRESHOLD_PX
|| (frame.height() - target.height()).abs() > DIVERGENCE_THRESHOLD_PX
}

#[cfg(test)]
mod tests {
use wm_platform::Rect;

use super::is_in_corner;
use super::{is_in_corner, size_diverged};

#[test]
fn matches_corner_positions() {
Expand All @@ -563,4 +606,30 @@ mod tests {

assert!(!is_in_corner(&frame, &monitor));
}

#[test]
fn size_divergence_ignores_sub_tile_differences() {
let target = Rect::from_xy(0, 0, 1000, 800);

// Identical size does not diverge.
assert!(!size_diverged(&target, &target));

// Border/shadow-scale differences stay under the threshold, even at a
// shifted position.
let shifted = Rect::from_xy(500, 500, 1010, 790);
assert!(!size_diverged(&shifted, &target));
}

#[test]
fn size_divergence_detects_dpi_rescale() {
let target = Rect::from_xy(0, 0, 1000, 800);

// A 1.5x DPI self-resize is off by hundreds of pixels.
let rescaled = Rect::from_xy(0, 0, 1500, 1200);
assert!(size_diverged(&rescaled, &target));

// Divergence in a single dimension is enough.
let taller = Rect::from_xy(0, 0, 1000, 1200);
assert!(size_diverged(&taller, &target));
}
}