From c91b1f3bcd9f34cf250a0b9691c8c61e82d6edf4 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:11:28 +0200 Subject: [PATCH 1/5] Filter X11 events to current window only --- src/platform/x11/event_loop.rs | 71 +++++++++++++++++-------------- src/platform/x11/window_shared.rs | 5 +++ 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index 4a2e22dc..87654a03 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -346,7 +346,7 @@ impl EventLoop { //// // window //// - XEvent::ClientMessage(event) => { + XEvent::ClientMessage(event) if event.window == self.window.raw_id() => { if event.format != 32 { return Ok(()); } @@ -384,17 +384,17 @@ impl EventLoop { warn!("Received leftover X11 error: {:?}", e); } - XEvent::ConfigureNotify(event) => { + XEvent::ConfigureNotify(event) if event.window == self.window.raw_id() => { // These are coalesced and then handled asynchronously at the end of the event loop self.new_physical_size = Some(PhysicalSize::new(event.width, event.height)); } - XEvent::Expose(_) => self.exposed = true, + XEvent::Expose(e) if e.window == self.window.raw_id() => self.exposed = true, //// // mouse //// - XEvent::MotionNotify(event) => { + XEvent::MotionNotify(event) if event.event == self.window.raw_id() => { let physical_pos = PhysicalPosition::new(event.event_x, event.event_y); self.handle_event(Event::Mouse(MouseEvent::CursorMoved { @@ -403,7 +403,7 @@ impl EventLoop { })); } - XEvent::EnterNotify(event) => { + XEvent::EnterNotify(event) if event.event == self.window.raw_id() => { self.handle_event(Event::Mouse(MouseEvent::CursorEntered)); // since no `MOTION_NOTIFY` event is generated when `ENTER_NOTIFY` is generated, // we generate a CursorMoved as well, so the mouse position from here isn't lost @@ -414,32 +414,36 @@ impl EventLoop { })); } - XEvent::LeaveNotify(_) => { + XEvent::LeaveNotify(event) if event.event == self.window.raw_id() => { self.handle_event(Event::Mouse(MouseEvent::CursorLeft)); } - XEvent::ButtonPress(event) => match event.detail { - 4..=7 => { - self.handle_event(Event::Mouse(MouseEvent::WheelScrolled { - delta: match event.detail { - 4 => ScrollDelta::Lines { x: 0.0, y: 1.0 }, - 5 => ScrollDelta::Lines { x: 0.0, y: -1.0 }, - 6 => ScrollDelta::Lines { x: -1.0, y: 0.0 }, - 7 => ScrollDelta::Lines { x: 1.0, y: 0.0 }, - _ => unreachable!(), - }, - modifiers: key_mods(event.state), - })); + XEvent::ButtonPress(event) if event.event == self.window.raw_id() => { + match event.detail { + 4..=7 => { + self.handle_event(Event::Mouse(MouseEvent::WheelScrolled { + delta: match event.detail { + 4 => ScrollDelta::Lines { x: 0.0, y: 1.0 }, + 5 => ScrollDelta::Lines { x: 0.0, y: -1.0 }, + 6 => ScrollDelta::Lines { x: -1.0, y: 0.0 }, + 7 => ScrollDelta::Lines { x: 1.0, y: 0.0 }, + _ => unreachable!(), + }, + modifiers: key_mods(event.state), + })); + } + detail => { + self.handle_event(Event::Mouse(MouseEvent::ButtonPressed { + button: mouse_id(detail), + modifiers: key_mods(event.state), + })); + } } - detail => { - self.handle_event(Event::Mouse(MouseEvent::ButtonPressed { - button: mouse_id(detail), - modifiers: key_mods(event.state), - })); - } - }, + } - XEvent::ButtonRelease(event) if !(4..=7).contains(&event.detail) => { + XEvent::ButtonRelease(event) + if event.event == self.window.raw_id() && !(4..=7).contains(&event.detail) => + { let button_id = mouse_id(event.detail); self.handle_event(Event::Mouse(MouseEvent::ButtonReleased { button: button_id, @@ -450,31 +454,34 @@ impl EventLoop { //// // keys //// - XEvent::KeyPress(event) => { + XEvent::KeyPress(event) if event.event == self.window.raw_id() => { let ev = Event::Keyboard(convert_key_press_event(&event, &mut self.xkb_state)); self.handle_event(ev); } - XEvent::KeyRelease(event) => { + XEvent::KeyRelease(event) if event.event == self.window.raw_id() => { let ev = Event::Keyboard(convert_key_release_event(&event, &mut self.xkb_state)); self.handle_event(ev); } - XEvent::FocusIn(_) => { + XEvent::FocusIn(event) if event.event == self.window.raw_id() => { self.window.is_focused.set(true); self.handle_event(Event::Window(WindowEvent::Focused)); } - XEvent::FocusOut(_) => { + XEvent::FocusOut(e) if e.event == self.window.raw_id() => { self.window.is_focused.set(false); self.handle_event(Event::Window(WindowEvent::Unfocused)); } - XEvent::MapNotify(_) => { + XEvent::MapNotify(e) if e.window == self.window.raw_id() => { self.window.is_mapped.set(true); self.exposed = true; } - XEvent::UnmapNotify(_) => self.window.is_mapped.set(false), + + XEvent::UnmapNotify(e) if e.window == self.window.raw_id() => { + self.window.is_mapped.set(false) + } _ => {} } diff --git a/src/platform/x11/window_shared.rs b/src/platform/x11/window_shared.rs index bc74b7a4..5acee142 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -12,6 +12,7 @@ use raw_window_handle::{DisplayHandle, XlibWindowHandle}; use std::cell::Cell; use std::rc::Rc; use std::sync::Arc; +use x11rb::protocol::xproto; use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt, InputFocus, Visualid}; use x11rb::CURRENT_TIME; @@ -271,4 +272,8 @@ impl WindowInner { pub fn size(&self) -> WindowSize { WindowSize::from_physical(self.window_size.get().cast(), self.scaling_factor.get()) } + + pub fn raw_id(&self) -> xproto::Window { + self.xcb_window.id().get() + } } From a6b19fdc360e09da7c11a683805572769445f09f Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:49:21 +0200 Subject: [PATCH 2/5] X11: track mapping status of all parents --- src/platform/x11/event_loop.rs | 33 +++++-- src/platform/x11/mod.rs | 1 + src/platform/x11/visibility_tree.rs | 144 ++++++++++++++++++++++++++++ src/platform/x11/window_shared.rs | 11 +++ src/platform/x11/xcb_connection.rs | 27 ++++-- 5 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 src/platform/x11/visibility_tree.rs diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index 87654a03..a101c7a8 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -148,7 +148,7 @@ impl EventLoop { } self.exposed = false; - if !self.window.is_mapped.get() { + if !self.window.visibility_state.own_window_is_viewable() { return; } @@ -474,13 +474,34 @@ impl EventLoop { self.handle_event(Event::Window(WindowEvent::Unfocused)); } - XEvent::MapNotify(e) if e.window == self.window.raw_id() => { - self.window.is_mapped.set(true); - self.exposed = true; + XEvent::MapNotify(e) => { + if e.window == self.window.raw_id() { + self.window.is_mapped.set(true); + } + + let previously_viewable = self.window.visibility_state.own_window_is_viewable(); + + if self.window.visibility_state.window_mapped(e.window) { + if !previously_viewable && self.window.visibility_state.own_window_is_viewable() + { + self.exposed = true; + } + } + } + + XEvent::UnmapNotify(e) => { + dbg!(e, e.window); + if e.window == self.window.raw_id() { + self.window.is_mapped.set(false) + } + } + + XEvent::ReparentNotify(e) => { + dbg!(e, e.window, e.parent); } - XEvent::UnmapNotify(e) if e.window == self.window.raw_id() => { - self.window.is_mapped.set(false) + XEvent::DestroyNotify(e) => { + dbg!(e, e.window); } _ => {} diff --git a/src/platform/x11/mod.rs b/src/platform/x11/mod.rs index b45c1990..3ce949d0 100644 --- a/src/platform/x11/mod.rs +++ b/src/platform/x11/mod.rs @@ -20,6 +20,7 @@ mod keyboard; mod visual_info; mod xcb_window; +mod visibility_tree; mod window_shared; mod window_thread; diff --git a/src/platform/x11/visibility_tree.rs b/src/platform/x11/visibility_tree.rs new file mode 100644 index 00000000..7ff44d70 --- /dev/null +++ b/src/platform/x11/visibility_tree.rs @@ -0,0 +1,144 @@ +use std::cell::{Cell, RefCell}; +use x11rb::errors::ReplyError; +use x11rb::protocol::xproto::{ConnectionExt, MapState, QueryTreeReply, Window}; +use x11rb::protocol::ErrorKind; +use x11rb::x11_utils::X11Error; +use x11rb::xcb_ffi::XCBConnection; + +#[cfg_attr(debug_assertions, derive(Debug))] +pub struct AncestorVisibilityState { + ancestry: RefCell>, + own_window_id: Window, + own_window_viewable: Cell, +} + +#[cfg_attr(debug_assertions, derive(Debug))] +struct Ancestor { + id: Window, + mapped: Cell, +} + +impl AncestorVisibilityState { + pub fn discover(connection: &XCBConnection, own_window_id: Window) -> Result { + let mut current_window = own_window_id; + let mut ancestry: Vec = Vec::new(); + + loop { + let (Some(mapped), Some(tree)) = ( + fetch_is_window_mapped(connection, current_window)?, + fetch_window_tree(connection, current_window)?, + ) else { + // We got a BadWindow while trying to get a window's info, it must have been destroyed. + // Try to go back a layer and fetch the window's state and parent again + + crate::warn!("Failed to get info for window {}: XBadWindow", current_window); + + let Some(previous_parent) = ancestry.pop() else { + // No previous parent, this was the first window. Stop everything and return an empty state + break; + }; + + current_window = previous_parent.id; + continue; + }; + + if tree.parent == current_window { + // Weird, but that might also mean we're at the end of the tree (or the window has no parent yet) + break; + } + + // Sanity check if the current parent is actually registered to have the child in its children list + if let Some(child_id) = ancestry.last().map(|a| a.id) { + if !tree.children.contains(&child_id) { + // The child has been orphaned, it must have been reparented between our server queries. + // Go back a step and check again. + + crate::warn!( + "Children of parent {} does not contain {}: {:?}", + current_window, + child_id, + &tree.children + ); + + let Some(_) = ancestry.pop() else { unreachable!() }; + current_window = child_id; + continue; + } + } + + // All checks succeeded, now register the current window info and fetch info from the parent + ancestry.push(Ancestor { id: current_window, mapped: mapped.into() }); + + if tree.parent == tree.root { + // No need to get info for the root, we assume it's always there. We can just stop here. + break; + } + + current_window = tree.parent; + } + + Ok(Self { + own_window_id, + own_window_viewable: ancestry.iter().all(|a| a.mapped.get()).into(), + ancestry: ancestry.into(), + }) + } + + pub fn own_window_is_viewable(&self) -> bool { + self.own_window_viewable.get() + } + + /// Returns `true` if the window is currently tracked, `false` otherwise + pub fn window_mapped(&self, mapped_window_id: Window) -> bool { + let ancestry = self.ancestry.borrow(); + let Some(ancestor) = ancestry.iter().find(|a| a.id == mapped_window_id) else { + return false; + }; + + ancestor.mapped.set(true); + + let all_mapped = ancestry.iter().all(|a| a.mapped.get()); + if all_mapped { + self.own_window_viewable.set(true); + } + + true + } + + pub fn window_unmapped(&self, mapped_window_id: Window) -> bool { + let ancestry = self.ancestry.borrow(); + let Some(ancestor) = ancestry.iter().find(|a| a.id == mapped_window_id) else { + return false; + }; + + ancestor.mapped.set(false); + + if self.own_window_viewable.get() { + self.own_window_viewable.set(false); + } + + true + } +} + +/// Returns Ok(None) on BadWindow +fn fetch_is_window_mapped( + connection: &XCBConnection, window: Window, +) -> Result, ReplyError> { + match connection.get_window_attributes(window)?.reply() { + Ok(attr) => Ok(Some(attr.map_state != MapState::UNMAPPED)), + Err(ReplyError::X11Error(X11Error { error_kind: ErrorKind::Window, .. })) => Ok(None), + Err(e) => Err(e), + } +} + +/// Returns Ok(None) on BadWindow +fn fetch_window_tree( + connection: &XCBConnection, window: Window, +) -> Result, ReplyError> { + match connection.query_tree(window)?.reply() { + Ok(tree) => Ok(Some(tree)), + Err(ReplyError::X11Error(X11Error { error_kind: ErrorKind::Window, .. })) => Ok(None), + Err(e) => Err(e), + } +} diff --git a/src/platform/x11/window_shared.rs b/src/platform/x11/window_shared.rs index 5acee142..32a9c340 100644 --- a/src/platform/x11/window_shared.rs +++ b/src/platform/x11/window_shared.rs @@ -1,4 +1,5 @@ use crate::platform::x11::event_loop::EventLoop; +use crate::platform::x11::visibility_tree::AncestorVisibilityState; use crate::platform::x11::visual_info::WindowVisualConfig; use crate::platform::x11::window_thread::WindowThreadShared; use crate::platform::x11::xcb_connection::get_size_hints; @@ -60,6 +61,8 @@ pub(crate) struct WindowInner { pub(crate) is_mapped: Cell, pub(crate) loop_signal: LoopSignal, + pub(crate) visibility_state: AncestorVisibilityState, + pub(crate) main_thread_shared: Arc, } @@ -98,6 +101,12 @@ impl WindowInner { options.parent.map(|p| p.inner.window_id), )?; + connection.register_tree_structure_events()?.check()?; + + let visibility_state = + AncestorVisibilityState::discover(&connection.conn, xcb_window.id().get())?; + dbg!(&visibility_state); + let cookies = [ xcb_window.set_title(&options.title)?, xcb_window.enable_wm_protocols()?, @@ -139,6 +148,8 @@ impl WindowInner { is_mapped: false.into(), main_thread_shared: shared, + visibility_state, + #[cfg(feature = "opengl")] gl_context, })) diff --git a/src/platform/x11/xcb_connection.rs b/src/platform/x11/xcb_connection.rs index 4dd4d44c..bbe1854f 100644 --- a/src/platform/x11/xcb_connection.rs +++ b/src/platform/x11/xcb_connection.rs @@ -1,15 +1,19 @@ +use super::cursor; +use crate::platform::*; +use crate::wrappers::xlib::XlibXcbConnection; +use crate::MouseCursor; use std::cell::RefCell; use std::collections::hash_map::{Entry, HashMap}; use std::sync::Arc; use x11rb::connection::Connection; +use x11rb::cookie::VoidCookie; use x11rb::cursor::Handle as CursorHandle; -use x11rb::protocol::xproto::{self, Cursor, Screen}; +use x11rb::errors::ConnectionError; +use x11rb::protocol::xproto::{ + self, ChangeWindowAttributesAux, ConnectionExt, Cursor, EventMask, Screen, +}; use x11rb::resource_manager; - -use super::cursor; -use crate::platform::*; -use crate::wrappers::xlib::XlibXcbConnection; -use crate::MouseCursor; +use x11rb::xcb_ffi::XCBConnection; mod get_property; pub use get_property::GetPropertyError; @@ -109,4 +113,15 @@ impl X11Connection { ) -> core::result::Result, GetPropertyError> { get_property::get_property(window, property, property_type, &self.conn) } + + pub fn register_tree_structure_events( + &self, + ) -> core::result::Result, ConnectionError> { + let root = self.screen().root; + + self.conn.change_window_attributes( + root, + &ChangeWindowAttributesAux::new().event_mask(EventMask::SUBSTRUCTURE_NOTIFY), + ) + } } From d528a64002553c52bb7cf37a6340e9fff8b96e26 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:08:33 +0200 Subject: [PATCH 3/5] cleanup --- src/platform/x11/event_loop.rs | 16 +++++++--------- src/platform/x11/visibility_tree.rs | 10 ++++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index a101c7a8..e21be01a 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -479,29 +479,27 @@ impl EventLoop { self.window.is_mapped.set(true); } - let previously_viewable = self.window.visibility_state.own_window_is_viewable(); + let became_viewable = self.window.visibility_state.window_mapped(e.window); - if self.window.visibility_state.window_mapped(e.window) { - if !previously_viewable && self.window.visibility_state.own_window_is_viewable() - { - self.exposed = true; - } + if became_viewable { + self.exposed = true; } } XEvent::UnmapNotify(e) => { - dbg!(e, e.window); if e.window == self.window.raw_id() { self.window.is_mapped.set(false) } + + self.window.visibility_state.window_unmapped(e.window); } XEvent::ReparentNotify(e) => { - dbg!(e, e.window, e.parent); + dbg!(e, e.window, e.parent); // TODO } XEvent::DestroyNotify(e) => { - dbg!(e, e.window); + dbg!(e, e.window); // TODO } _ => {} diff --git a/src/platform/x11/visibility_tree.rs b/src/platform/x11/visibility_tree.rs index 7ff44d70..40228102 100644 --- a/src/platform/x11/visibility_tree.rs +++ b/src/platform/x11/visibility_tree.rs @@ -8,7 +8,6 @@ use x11rb::xcb_ffi::XCBConnection; #[cfg_attr(debug_assertions, derive(Debug))] pub struct AncestorVisibilityState { ancestry: RefCell>, - own_window_id: Window, own_window_viewable: Cell, } @@ -78,7 +77,6 @@ impl AncestorVisibilityState { } Ok(Self { - own_window_id, own_window_viewable: ancestry.iter().all(|a| a.mapped.get()).into(), ancestry: ancestry.into(), }) @@ -88,7 +86,7 @@ impl AncestorVisibilityState { self.own_window_viewable.get() } - /// Returns `true` if the window is currently tracked, `false` otherwise + /// Returns `true` if this operation made our own window visible pub fn window_mapped(&self, mapped_window_id: Window) -> bool { let ancestry = self.ancestry.borrow(); let Some(ancestor) = ancestry.iter().find(|a| a.id == mapped_window_id) else { @@ -97,12 +95,16 @@ impl AncestorVisibilityState { ancestor.mapped.set(true); + if self.own_window_viewable.get() { + return false; + } + let all_mapped = ancestry.iter().all(|a| a.mapped.get()); if all_mapped { self.own_window_viewable.set(true); } - true + all_mapped } pub fn window_unmapped(&self, mapped_window_id: Window) -> bool { From 5d6c910a50ae2eb9b031992de63b2716bbed1c7b Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Fri, 14 Aug 2026 00:58:39 +0200 Subject: [PATCH 4/5] Make AncestorList reentrant-safe(r) --- src/platform/x11/visibility_tree.rs | 78 +++++++++++++++++++---------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/src/platform/x11/visibility_tree.rs b/src/platform/x11/visibility_tree.rs index 40228102..1e956ab8 100644 --- a/src/platform/x11/visibility_tree.rs +++ b/src/platform/x11/visibility_tree.rs @@ -7,10 +7,47 @@ use x11rb::xcb_ffi::XCBConnection; #[cfg_attr(debug_assertions, derive(Debug))] pub struct AncestorVisibilityState { - ancestry: RefCell>, + ancestry: AncestryList, own_window_viewable: Cell, } +#[cfg_attr(debug_assertions, derive(Debug))] +struct AncestryList { + inner: RefCell>, +} + +impl AncestryList { + pub fn new() -> Self { + Self { inner: RefCell::new(Vec::new()) } + } + + pub fn pop_id(&self) -> Option { + self.inner.borrow_mut().pop().map(|a| a.id) + } + + pub fn last_id(&self) -> Option { + self.inner.borrow().last().map(|a| a.id) + } + + pub fn push(&self, ancestor: Ancestor) { + self.inner.borrow_mut().push(ancestor); + } + + pub fn check_all_mapped(&self) -> bool { + self.inner.borrow().iter().all(|a| a.mapped.get()) + } + + pub fn set_mapped(&self, window: Window, mapped: bool) -> bool { + let inner = self.inner.borrow(); + let Some(ancestor) = inner.iter().find(|a| a.id == window) else { + return false; + }; + + ancestor.mapped.set(mapped); + true + } +} + #[cfg_attr(debug_assertions, derive(Debug))] struct Ancestor { id: Window, @@ -20,7 +57,7 @@ struct Ancestor { impl AncestorVisibilityState { pub fn discover(connection: &XCBConnection, own_window_id: Window) -> Result { let mut current_window = own_window_id; - let mut ancestry: Vec = Vec::new(); + let ancestry = AncestryList::new(); loop { let (Some(mapped), Some(tree)) = ( @@ -32,12 +69,12 @@ impl AncestorVisibilityState { crate::warn!("Failed to get info for window {}: XBadWindow", current_window); - let Some(previous_parent) = ancestry.pop() else { + let Some(previous_parent) = ancestry.pop_id() else { // No previous parent, this was the first window. Stop everything and return an empty state break; }; - current_window = previous_parent.id; + current_window = previous_parent; continue; }; @@ -47,7 +84,7 @@ impl AncestorVisibilityState { } // Sanity check if the current parent is actually registered to have the child in its children list - if let Some(child_id) = ancestry.last().map(|a| a.id) { + if let Some(child_id) = ancestry.last_id() { if !tree.children.contains(&child_id) { // The child has been orphaned, it must have been reparented between our server queries. // Go back a step and check again. @@ -59,7 +96,7 @@ impl AncestorVisibilityState { &tree.children ); - let Some(_) = ancestry.pop() else { unreachable!() }; + let Some(_) = ancestry.pop_id() else { unreachable!() }; current_window = child_id; continue; } @@ -76,10 +113,7 @@ impl AncestorVisibilityState { current_window = tree.parent; } - Ok(Self { - own_window_viewable: ancestry.iter().all(|a| a.mapped.get()).into(), - ancestry: ancestry.into(), - }) + Ok(Self { own_window_viewable: ancestry.check_all_mapped().into(), ancestry }) } pub fn own_window_is_viewable(&self) -> bool { @@ -88,18 +122,15 @@ impl AncestorVisibilityState { /// Returns `true` if this operation made our own window visible pub fn window_mapped(&self, mapped_window_id: Window) -> bool { - let ancestry = self.ancestry.borrow(); - let Some(ancestor) = ancestry.iter().find(|a| a.id == mapped_window_id) else { + if !self.ancestry.set_mapped(mapped_window_id, true) { return false; - }; - - ancestor.mapped.set(true); + } if self.own_window_viewable.get() { return false; } - let all_mapped = ancestry.iter().all(|a| a.mapped.get()); + let all_mapped = self.ancestry.check_all_mapped(); if all_mapped { self.own_window_viewable.set(true); } @@ -107,19 +138,12 @@ impl AncestorVisibilityState { all_mapped } - pub fn window_unmapped(&self, mapped_window_id: Window) -> bool { - let ancestry = self.ancestry.borrow(); - let Some(ancestor) = ancestry.iter().find(|a| a.id == mapped_window_id) else { - return false; - }; - - ancestor.mapped.set(false); - - if self.own_window_viewable.get() { - self.own_window_viewable.set(false); + pub fn window_unmapped(&self, mapped_window_id: Window) { + if !self.ancestry.set_mapped(mapped_window_id, false) { + return; } - true + self.own_window_viewable.set(false); } } From ebd09fc2a86f45f2282dce697bf4cef22fcbfa68 Mon Sep 17 00:00:00 2001 From: Adrien Prokopowicz <6529475+prokopyl@users.noreply.github.com> Date: Fri, 14 Aug 2026 04:11:18 +0200 Subject: [PATCH 5/5] Harden everything, make child window automatically track parent window's --- examples/open_parented/src/main.rs | 13 +- src/platform/x11/event_loop.rs | 46 +++++-- src/platform/x11/visibility_tree.rs | 191 +++++++++++++++++++--------- 3 files changed, 172 insertions(+), 78 deletions(-) diff --git a/examples/open_parented/src/main.rs b/examples/open_parented/src/main.rs index cca879de..4c9433fc 100644 --- a/examples/open_parented/src/main.rs +++ b/examples/open_parented/src/main.rs @@ -20,10 +20,8 @@ impl ParentWindowHandler { let size = window.size().physical; surface.resize(size.width.try_into()?, size.height.try_into()?)?; - let window_open_options = WindowSettings::new() - .with_size(LogicalSize::new(256, 256)) - .with_parent(&window) - .with_title("baseview child"); + let window_open_options = + WindowSettings::new().with_size(size).with_parent(&window).with_title("baseview child"); let child_window = Window::create(window_open_options, ChildWindowHandler::new)?; child_window.show()?; @@ -37,7 +35,7 @@ impl WindowHandler for ParentWindowHandler { let mut surface = self.surface.borrow_mut(); let mut buf = surface.buffer_mut()?; if self.damaged.get() { - buf.fill(0xFFAAAAAA); + buf.fill(0xFFAA0000); self.damaged.set(false); } buf.present()?; @@ -55,10 +53,7 @@ impl WindowHandler for ParentWindowHandler { self.damaged.set(true); } - let child_size = - LogicalSize::new(new_size.logical.width / 2., new_size.logical.height / 2.); self.child_window.suggest_fallback_scale_factor(new_size.scale_factor)?; - self.child_window.resize(child_size)?; Ok(()) } @@ -95,7 +90,7 @@ impl WindowHandler for ChildWindowHandler { let mut surface = self.surface.borrow_mut(); let mut buf = surface.buffer_mut()?; if self.damaged.get() { - buf.fill(0xFFAA0000); + buf.fill(0xFFAAAAAA); self.damaged.set(false); } buf.present()?; diff --git a/src/platform/x11/event_loop.rs b/src/platform/x11/event_loop.rs index e21be01a..fa966477 100644 --- a/src/platform/x11/event_loop.rs +++ b/src/platform/x11/event_loop.rs @@ -51,7 +51,8 @@ pub(crate) struct EventLoop { handler: Box, window: Rc, - new_physical_size: Option>, + new_size: Option>, + new_parent_size: Option>, exposed: bool, loop_signal: LoopSignal, @@ -90,7 +91,8 @@ impl EventLoop { Ok(Self { loop_signal: inner.get_signal(), handler, - new_physical_size: None, + new_size: None, + new_parent_size: None, exposed: false, drag_n_drop: DragNDropState::NoCurrentSession, xkb_state: XkbcommonState::new(&window.connection), @@ -162,7 +164,20 @@ impl EventLoop { } fn handle_coalesced_resize_events(&mut self) -> Result<(), FatalError> { - let Some(new_size) = self.new_physical_size.take() else { return Ok(()) }; + if let Some(new_parent_size) = self.new_parent_size.take() { + if new_parent_size != self.window.get_size() { + // The parent was resized, which means we should resize ourselves too. + if let Err(e) = self.window.xcb_window.resize(new_parent_size.cast()) { + crate::warn!("Failed to resize window: {}", e); + } else { + // Makes the rest of this function run on the new parent size immediately (without waiting for a ConfigureNotify round-trip) + // Also overrides any new sizes we may have received this event loop iteration,it would probably be invalidated anyway + self.new_size = Some(new_parent_size); + } + } + } + + let Some(new_size) = self.new_size.take() else { return Ok(()) }; let previous = self.window.store_size(new_size); if previous == new_size { @@ -384,9 +399,15 @@ impl EventLoop { warn!("Received leftover X11 error: {:?}", e); } - XEvent::ConfigureNotify(event) if event.window == self.window.raw_id() => { + XEvent::ConfigureNotify(event) => { // These are coalesced and then handled asynchronously at the end of the event loop - self.new_physical_size = Some(PhysicalSize::new(event.width, event.height)); + if event.window == self.window.raw_id() { + self.new_size = Some(PhysicalSize::new(event.width, event.height)); + } else if Some(event.window) == self.window.visibility_state.parent_id() { + // Also resize the window if the parent is resized + // This works around some hosts that might not call set_size() right away (or at all...) + self.new_parent_size = Some(PhysicalSize::new(event.width, event.height)); + } } XEvent::Expose(e) if e.window == self.window.raw_id() => self.exposed = true, @@ -494,13 +515,16 @@ impl EventLoop { self.window.visibility_state.window_unmapped(e.window); } - XEvent::ReparentNotify(e) => { - dbg!(e, e.window, e.parent); // TODO - } + XEvent::ReparentNotify(e) => self.window.visibility_state.window_reparented( + e.window, + e.parent, + &self.window.connection.conn, + ), - XEvent::DestroyNotify(e) => { - dbg!(e, e.window); // TODO - } + XEvent::DestroyNotify(e) => self + .window + .visibility_state + .window_destroyed(e.window, &self.window.connection.conn), _ => {} } diff --git a/src/platform/x11/visibility_tree.rs b/src/platform/x11/visibility_tree.rs index 1e956ab8..64eb88df 100644 --- a/src/platform/x11/visibility_tree.rs +++ b/src/platform/x11/visibility_tree.rs @@ -17,8 +17,8 @@ struct AncestryList { } impl AncestryList { - pub fn new() -> Self { - Self { inner: RefCell::new(Vec::new()) } + pub fn new(own_window: Window) -> Self { + Self { inner: RefCell::new(vec![Ancestor { id: own_window, mapped: false.into() }]) } } pub fn pop_id(&self) -> Option { @@ -33,6 +33,32 @@ impl AncestryList { self.inner.borrow_mut().push(ancestor); } + pub fn parent_id(&self) -> Option { + self.inner.borrow().get(1).map(|a| a.id) + } + + pub fn remove_window(&self, id: Window) -> bool { + let mut inner = self.inner.borrow_mut(); + let Some(index) = inner.iter().position(|a| a.id == id) else { + return false; + }; + + inner.truncate(index.saturating_add(1)); + + true + } + + pub fn remove_after_window(&self, id: Window) -> bool { + let mut inner = self.inner.borrow_mut(); + let Some(index) = inner.iter().position(|a| a.id == id) else { + return false; + }; + + inner.truncate(index.saturating_add(2)); + + true + } + pub fn check_all_mapped(&self) -> bool { self.inner.borrow().iter().all(|a| a.mapped.get()) } @@ -56,20 +82,89 @@ struct Ancestor { impl AncestorVisibilityState { pub fn discover(connection: &XCBConnection, own_window_id: Window) -> Result { - let mut current_window = own_window_id; - let ancestry = AncestryList::new(); + let this = Self { + ancestry: AncestryList::new(own_window_id), + own_window_viewable: Cell::new(false), + }; + + this.try_regenerate_from_last_window(connection)?; + + Ok(this) + } + + pub fn own_window_is_viewable(&self) -> bool { + self.own_window_viewable.get() + } + + pub fn parent_id(&self) -> Option { + self.ancestry.parent_id() + } + + /// Returns `true` if this operation made our own window visible + pub fn window_mapped(&self, window_id: Window) -> bool { + if !self.ancestry.set_mapped(window_id, true) { + return false; + } + + if self.own_window_viewable.get() { + return false; + } + + let all_mapped = self.ancestry.check_all_mapped(); + if all_mapped { + self.own_window_viewable.set(true); + } + + all_mapped + } + + pub fn window_unmapped(&self, window_id: Window) { + if !self.ancestry.set_mapped(window_id, false) { + return; + } + + self.own_window_viewable.set(false); + } + + pub fn window_destroyed(&self, window_id: Window, connection: &XCBConnection) { + if !self.ancestry.remove_window(window_id) { + return; + } + + self.regenerate_from_last_window(connection); + } + + pub fn window_reparented( + &self, window_id: Window, new_parent: Window, connection: &XCBConnection, + ) { + if !self.ancestry.remove_after_window(window_id) { + return; + } + + self.ancestry.push(Ancestor { id: new_parent, mapped: Cell::new(false) }); + + self.regenerate_from_last_window(connection); + } + + pub fn regenerate_from_last_window(&self, connection: &XCBConnection) { + if let Err(e) = self.try_regenerate_from_last_window(connection) { + crate::warn!("Failed to generate window ancestry list: {}", e) + } + } + + fn try_regenerate_from_last_window( + &self, connection: &XCBConnection, + ) -> Result<(), ReplyError> { + let Some(mut current_window) = self.ancestry.pop_id() else { return Ok(()) }; loop { - let (Some(mapped), Some(tree)) = ( - fetch_is_window_mapped(connection, current_window)?, - fetch_window_tree(connection, current_window)?, - ) else { + let Some((mapped, tree)) = fetch_window_info(connection, current_window)? else { // We got a BadWindow while trying to get a window's info, it must have been destroyed. // Try to go back a layer and fetch the window's state and parent again crate::warn!("Failed to get info for window {}: XBadWindow", current_window); - let Some(previous_parent) = ancestry.pop_id() else { + let Some(previous_parent) = self.ancestry.pop_id() else { // No previous parent, this was the first window. Stop everything and return an empty state break; }; @@ -84,7 +179,7 @@ impl AncestorVisibilityState { } // Sanity check if the current parent is actually registered to have the child in its children list - if let Some(child_id) = ancestry.last_id() { + if let Some(child_id) = self.ancestry.last_id() { if !tree.children.contains(&child_id) { // The child has been orphaned, it must have been reparented between our server queries. // Go back a step and check again. @@ -96,14 +191,14 @@ impl AncestorVisibilityState { &tree.children ); - let Some(_) = ancestry.pop_id() else { unreachable!() }; + let Some(_) = self.ancestry.pop_id() else { unreachable!() }; current_window = child_id; continue; } } // All checks succeeded, now register the current window info and fetch info from the parent - ancestry.push(Ancestor { id: current_window, mapped: mapped.into() }); + self.ancestry.push(Ancestor { id: current_window, mapped: mapped.into() }); if tree.parent == tree.root { // No need to get info for the root, we assume it's always there. We can just stop here. @@ -113,58 +208,38 @@ impl AncestorVisibilityState { current_window = tree.parent; } - Ok(Self { own_window_viewable: ancestry.check_all_mapped().into(), ancestry }) - } + self.own_window_viewable.set(self.ancestry.check_all_mapped()); - pub fn own_window_is_viewable(&self) -> bool { - self.own_window_viewable.get() + Ok(()) } +} - /// Returns `true` if this operation made our own window visible - pub fn window_mapped(&self, mapped_window_id: Window) -> bool { - if !self.ancestry.set_mapped(mapped_window_id, true) { - return false; - } - - if self.own_window_viewable.get() { - return false; +/// Returns Ok(None) on BadWindow +fn fetch_window_info( + connection: &XCBConnection, window: Window, +) -> Result, ReplyError> { + let attrs_cookie = connection.get_window_attributes(window)?; + let tree_cookie = connection.query_tree(window)?; + + let mapped = match attrs_cookie.reply() { + Ok(attr) => attr.map_state != MapState::UNMAPPED, + Err(ReplyError::X11Error(X11Error { error_kind: ErrorKind::Window, .. })) => { + tree_cookie.discard_reply_and_errors(); + return Ok(None); } - - let all_mapped = self.ancestry.check_all_mapped(); - if all_mapped { - self.own_window_viewable.set(true); + Err(e) => { + tree_cookie.discard_reply_and_errors(); + return Err(e); } + }; - all_mapped - } - - pub fn window_unmapped(&self, mapped_window_id: Window) { - if !self.ancestry.set_mapped(mapped_window_id, false) { - return; + let tree = match tree_cookie.reply() { + Ok(tree) => tree, + Err(ReplyError::X11Error(X11Error { error_kind: ErrorKind::Window, .. })) => { + return Ok(None) } + Err(e) => return Err(e), + }; - self.own_window_viewable.set(false); - } -} - -/// Returns Ok(None) on BadWindow -fn fetch_is_window_mapped( - connection: &XCBConnection, window: Window, -) -> Result, ReplyError> { - match connection.get_window_attributes(window)?.reply() { - Ok(attr) => Ok(Some(attr.map_state != MapState::UNMAPPED)), - Err(ReplyError::X11Error(X11Error { error_kind: ErrorKind::Window, .. })) => Ok(None), - Err(e) => Err(e), - } -} - -/// Returns Ok(None) on BadWindow -fn fetch_window_tree( - connection: &XCBConnection, window: Window, -) -> Result, ReplyError> { - match connection.query_tree(window)?.reply() { - Ok(tree) => Ok(Some(tree)), - Err(ReplyError::X11Error(X11Error { error_kind: ErrorKind::Window, .. })) => Ok(None), - Err(e) => Err(e), - } + Ok(Some((mapped, tree))) }