From 809bcc55a5205c05bc5f2a660845506ac28a9675 Mon Sep 17 00:00:00 2001 From: Jeffrey Knockel Date: Sun, 18 Feb 2024 13:27:09 -0500 Subject: [PATCH] x11: fix window contents jumping on async resize We can't hope to keep the renderer's idea of the window size consistent with the actual window size when the user can asynchronously change its size at any moment. Therefore, we create a child window which we render to and which we synchronously resize in response to async resizes. Since by default the contents of X11 windows are undefined on resize, we take the additional steps of setting our windows' X11 background color and bit gravity as appropriate. --- window/src/os/x11/window.rs | 64 ++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/window/src/os/x11/window.rs b/window/src/os/x11/window.rs index da0de8227..38da24876 100644 --- a/window/src/os/x11/window.rs +++ b/window/src/os/x11/window.rs @@ -83,6 +83,7 @@ impl Default for DragAndDrop { pub(crate) struct XWindowInner { pub window_id: xcb::x::Window, + pub child_id: xcb::x::Window, conn: Weak, pub events: WindowEventSender, width: u16, @@ -122,6 +123,9 @@ impl Drop for XWindowInner { .flush() .context("flush pending requests prior to issuing DestroyWindow") .ok(); + conn.send_request_no_reply_log(&xcb::x::DestroyWindow { + window: self.child_id, + }); conn.send_request_no_reply_log(&xcb::x::DestroyWindow { window: self.window_id, }); @@ -145,7 +149,7 @@ unsafe impl HasRawDisplayHandle for XWindowInner { unsafe impl HasRawWindowHandle for XWindowInner { fn raw_window_handle(&self) -> RawWindowHandle { let mut handle = XcbWindowHandle::empty(); - handle.window = self.window_id.resource_id(); + handle.window = self.child_id.resource_id(); handle.visual_id = self.conn.upgrade().unwrap().visual.visual_id(); RawWindowHandle::Xcb(handle) } @@ -158,11 +162,11 @@ impl XWindowInner { let gl_state = match conn.gl_connection.borrow().as_ref() { None => crate::egl::GlState::create( Some(conn.conn.get_raw_dpy() as *const _), - self.window_id.resource_id() as *mut _, + self.child_id.resource_id() as *mut _, ), Some(glconn) => crate::egl::GlState::create_with_existing_connection( glconn, - self.window_id.resource_id() as *mut _, + self.child_id.resource_id() as *mut _, ), }; @@ -261,6 +265,18 @@ impl XWindowInner { self.pending.push(event); } + fn resize_child(&self, width: u32, height: u32) { + self.conn() + .send_request_no_reply_log(&xcb::x::ConfigureWindow { + window: self.child_id, + value_list: &[ + xcb::x::ConfigWindow::Width(width as u32), + xcb::x::ConfigWindow::Height(height as u32), + ], + }); + // send_request_no_reply_log() is synchronous, so no further synchronization required + } + pub fn dispatch_pending_events(&mut self) -> anyhow::Result<()> { if self.pending.is_empty() { return Ok(()); @@ -355,6 +371,8 @@ impl XWindowInner { || self.height != geom.height() || self.last_wm_state != window_state { + self.resize_child(geom.width() as u32, geom.height() as u32); + self.width = geom.width(); self.height = geom.height(); self.last_wm_state = window_state; @@ -460,6 +478,7 @@ impl XWindowInner { fn configure_notify(&mut self, source: &str, width: u16, height: u16) -> anyhow::Result<()> { let conn = self.conn(); + self.update_ime_position(); let mut dpi = conn.default_dpi(); @@ -510,6 +529,8 @@ impl XWindowInner { return Ok(()); } + self.resize_child(width as u32, height as u32); + log::trace!( "{source}: width {} -> {}, height {} -> {}, dpi {} -> {}", self.width, @@ -1355,6 +1376,7 @@ impl XWindow { let mut events = WindowEventSender::new(event_handler); let window_id; + let child_id; let window = { let setup = conn.conn().get_setup(); let screen = setup @@ -1363,6 +1385,7 @@ impl XWindow { .ok_or_else(|| anyhow!("no screen?"))?; window_id = conn.conn().generate_id(); + child_id = conn.conn().generate_id(); let color_map_id = conn.conn().generate_id(); conn.send_request_no_reply(&xcb::x::CreateColormap { @@ -1388,7 +1411,8 @@ impl XWindow { // We have to specify both a border pixel color and a colormap // when specifying a depth that doesn't match the root window in // order to avoid a BadMatch - xcb::x::Cw::BorderPixel(0), + xcb::x::Cw::BackPixel(0), // transparent background + xcb::x::Cw::BorderPixel(screen.black_pixel()), xcb::x::Cw::EventMask( xcb::x::EventMask::EXPOSURE | xcb::x::EventMask::FOCUS_CHANGE @@ -1407,6 +1431,32 @@ impl XWindow { }) .context("xcb::create_window_checked")?; + conn.send_request_no_reply(&xcb::x::CreateWindow { + depth: conn.depth, + wid: child_id, + parent: window_id, + x: x.unwrap_or(0).try_into()?, + y: y.unwrap_or(0).try_into()?, + width: width.try_into()?, + height: height.try_into()?, + border_width: 0, + class: xcb::x::WindowClass::InputOutput, + visual: conn.visual.visual_id(), + value_list: &[ + // We have to specify both a border pixel color and a colormap + // when specifying a depth that doesn't match the root window in + // order to avoid a BadMatch + xcb::x::Cw::BackPixel(0), // transparent background + xcb::x::Cw::BorderPixel(screen.black_pixel()), + xcb::x::Cw::BitGravity(xcb::x::Gravity::NorthWest), + xcb::x::Cw::Colormap(color_map_id), + ], + }) + .context("xcb::create_window_checked")?; + + conn.send_request_no_reply(&xcb::x::MapWindow { window: child_id }) + .context("xcb::map_window")?; + events.assign_window(Window::X11(XWindow::from_id(window_id))); let appearance = conn.get_appearance(); @@ -1415,6 +1465,7 @@ impl XWindow { title: String::new(), appearance, window_id, + child_id, conn: Rc::downgrade(&conn), events, width: width.try_into()?, @@ -1539,6 +1590,10 @@ impl XWindowInner { window: self.window_id, }); + conn.send_request_no_reply_log(&xcb::x::DestroyWindow { + window: self.child_id, + }); + // Arrange to destroy the window after a couple of seconds; that // should give whatever stuff is still referencing the window // to finish and avoid triggering a protocol error. @@ -1989,6 +2044,7 @@ impl WindowOps for XWindow { xcb::x::ConfigWindow::Height(height as u32), ], }); + inner.resize_child(width as u32, height as u32); inner.outstanding_configure_requests += 1; Ok(()) });