diff --git a/crates/gpui_windows/src/events.rs b/crates/gpui_windows/src/events.rs index a82295730eb..45e1f13d814 100644 --- a/crates/gpui_windows/src/events.rs +++ b/crates/gpui_windows/src/events.rs @@ -240,9 +240,10 @@ impl WindowsWindowInner { // A minimized window has no client area to draw, so stop requesting frames until it is // restored. The renderer keeps its size: `WM_SIZE` reports 0x0 while minimized. if wparam.0 == SIZE_MINIMIZED as usize { - self.state - .restore_from_minimized - .set(self.state.callbacks.request_frame.take()); + park_frame_callback( + &self.state.callbacks.request_frame, + &self.state.restore_from_minimized, + ); return Some(0); } @@ -1771,3 +1772,18 @@ fn notify_frame_changed(handle: HWND) { .log_err(); } } + +/// Move the frame callback into the minimized-window slot until the window is restored. +/// +/// A window that is already minimized gets `WM_SIZE(SIZE_MINIMIZED)` again whenever it is moved, +/// resized or minimized a second time while iconic, and Windows does that on its own when the +/// display layout changes (a monitor sleeping, waking or reconnecting, a resolution change, a +/// remote desktop session). The callback is then already parked and `request_frame` is empty, so +/// parking unconditionally would overwrite the parked callback with `None`. The restore would +/// find nothing to put back, and every later `WM_PAINT` would return before drawing: the window +/// keeps its last frame forever while input still reaches it. +fn park_frame_callback(request_frame: &Cell>, parked: &Cell>) { + if let Some(callback) = request_frame.take() { + parked.set(Some(callback)); + } +} diff --git a/crates/gpui_windows/src/platform.rs b/crates/gpui_windows/src/platform.rs index f8064ce15e7..f52b310d52e 100644 --- a/crates/gpui_windows/src/platform.rs +++ b/crates/gpui_windows/src/platform.rs @@ -1730,6 +1730,91 @@ mod tests { Ok(()) } + /// A minimized window can get `WM_SIZE(SIZE_MINIMIZED)` again before it is restored. It must + /// still draw after the restore: losing its frame callback leaves the last frame on screen for + /// good while input still reaches it. + #[test] + fn a_window_minimized_twice_still_draws_after_restore() -> anyhow::Result<()> { + use super::WindowsPlatform; + use crate::WindowsWindow; + use gpui::{ + AnyWindowHandle, Bounds, PlatformWindow as _, WindowHandle, WindowId, WindowKind, + WindowParams, point, px, size, + }; + use std::{cell::Cell, rc::Rc}; + use windows::Win32::Foundation::{LPARAM, WPARAM}; + use windows::Win32::Graphics::Gdi::{RDW_INVALIDATE, RDW_UPDATENOW, RedrawWindow}; + use windows::Win32::UI::WindowsAndMessaging::{ + DestroyWindow, SIZE_MINIMIZED, SW_RESTORE, SW_SHOWMINNOACTIVE, SW_SHOWNOACTIVATE, + SendMessageW, ShowWindow, WM_SIZE, + }; + + struct Root; + impl gpui::Render for Root { + fn render( + &mut self, + _: &mut gpui::Window, + _: &mut gpui::Context, + ) -> impl gpui::IntoElement { + gpui::Empty + } + } + + let platform = WindowsPlatform::new(false)?; + let handle: AnyWindowHandle = WindowHandle::::new(WindowId::from(1)).into(); + let window = WindowsWindow::new( + handle, + WindowParams { + bounds: Bounds::new(point(px(100.), px(100.)), size(px(400.), px(300.))), + titlebar: None, + kind: WindowKind::Normal, + is_movable: true, + app_owns_titlebar_drag: false, + is_resizable: true, + is_minimizable: true, + focus: false, + show: false, + icon: None, + display_id: None, + app_id: None, + window_min_size: None, + windows_renderer: Default::default(), + }, + platform.generate_creation_info(), + )?; + let hwnd = window.get_raw_handle(); + let frames = Rc::new(Cell::new(0)); + window.on_request_frame(Box::new({ + let frames = frames.clone(); + move |_| frames.set(frames.get() + 1) + })); + let paint = || unsafe { + let _ = RedrawWindow(Some(hwnd), None, None, RDW_INVALIDATE | RDW_UPDATENOW); + }; + + unsafe { + let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE); + let _ = ShowWindow(hwnd, SW_SHOWMINNOACTIVE); + SendMessageW( + hwnd, + WM_SIZE, + Some(WPARAM(SIZE_MINIMIZED as usize)), + Some(LPARAM(0)), + ); + let _ = ShowWindow(hwnd, SW_RESTORE); + } + let before = frames.get(); + paint(); + + assert!( + frames.get() > before, + "the restored window must request a frame when it paints" + ); + + unsafe { DestroyWindow(hwnd)? }; + Ok(()) + } + #[test] fn test_clipboard() { let item = ClipboardItem::new_string("你好,我是张小白".to_string());