From ec4af7d42b7e6e4808c3208e278703afb65d564d Mon Sep 17 00:00:00 2001 From: Joe Groocock Date: Sun, 16 Aug 2020 22:18:32 +0100 Subject: [PATCH] x11: reload dpi on PropertyChangeEvent This change allows X11 windows to receive ScaleFactorChanged events when using Xft.dpi via the root window PropertyChange event. Subscribe to PropertyChange events on the root window, and reload all available monitors when an XA_RESOURCE_MANAGER change is received. This is a very heavy handed approach to this problem, but it was the least intrustive way I could find to make it work with my limited skill. Load XResources directly via XGetTextProperty on every get_xft_dpi call as XResourceManagerString returns stale cached data and does not reload for the lifetime of the window (?) Signed-off-by: Joe Groocock --- .../linux/x11/event_processor.rs | 134 ++++++++++-------- src/platform_impl/linux/x11/util/randr.rs | 24 +++- src/platform_impl/linux/x11/window.rs | 8 ++ 3 files changed, 105 insertions(+), 61 deletions(-) diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index 54cdc59b039..5a77554430a 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -1184,64 +1184,20 @@ impl EventProcessor { _ => {} } } + + ffi::PropertyNotify => { + let xev: &ffi::XPropertyEvent = xev.as_ref(); + + // XA_RESOURCE_MANAGER received from the root window indicates that xresources + // have been changed. Check and update Xft.dpi if the dpi value was changed. + if xev.atom == ffi::XA_RESOURCE_MANAGER { + self.process_dpi_change(&mut callback) + } + } + _ => { if event_type == self.randr_event_offset { - // In the future, it would be quite easy to emit monitor hotplug events. - let prev_list = monitor::invalidate_cached_monitor_list(); - if let Some(prev_list) = prev_list { - let new_list = wt.xconn.available_monitors(); - for new_monitor in new_list { - // Previous list may be empty, in case of disconnecting and - // reconnecting the only one monitor. We still need to emit events in - // this case. - let maybe_prev_scale_factor = prev_list - .iter() - .find(|prev_monitor| prev_monitor.name == new_monitor.name) - .map(|prev_monitor| prev_monitor.scale_factor); - if Some(new_monitor.scale_factor) != maybe_prev_scale_factor { - for (window_id, window) in wt.windows.borrow().iter() { - if let Some(window) = window.upgrade() { - // Check if the window is on this monitor - let monitor = window.current_monitor(); - if monitor.name == new_monitor.name { - let (width, height) = window.inner_size_physical(); - let (new_width, new_height) = window.adjust_for_dpi( - // If we couldn't determine the previous scale - // factor (e.g., because all monitors were closed - // before), just pick whatever the current monitor - // has set as a baseline. - maybe_prev_scale_factor - .unwrap_or(monitor.scale_factor), - new_monitor.scale_factor, - width, - height, - &*window.shared_state.lock(), - ); - - let window_id = crate::window::WindowId(*window_id); - let old_inner_size = PhysicalSize::new(width, height); - let mut new_inner_size = - PhysicalSize::new(new_width, new_height); - - callback(Event::WindowEvent { - window_id, - event: WindowEvent::ScaleFactorChanged { - scale_factor: new_monitor.scale_factor, - new_inner_size: &mut new_inner_size, - }, - }); - - if new_inner_size != old_inner_size { - let (new_width, new_height) = new_inner_size.into(); - window - .set_inner_size_physical(new_width, new_height); - } - } - } - } - } - } - } + self.process_dpi_change(&mut callback) } } } @@ -1352,6 +1308,72 @@ impl EventProcessor { }); } } + + fn process_dpi_change( + &self, + callback: &mut F, + ) where + F: FnMut(Event<'_, T>) + { + let wt = get_xtarget(&self.target); + + // In the future, it would be quite easy to emit monitor hotplug events. + let prev_list = monitor::invalidate_cached_monitor_list(); + if let Some(prev_list) = prev_list { + let new_list = wt.xconn.available_monitors(); + for new_monitor in new_list { + // Previous list may be empty, in case of disconnecting and + // reconnecting the only one monitor. We still need to emit events in + // this case. + let maybe_prev_scale_factor = prev_list + .iter() + .find(|prev_monitor| prev_monitor.name == new_monitor.name) + .map(|prev_monitor| prev_monitor.scale_factor); + if Some(new_monitor.scale_factor) != maybe_prev_scale_factor { + for (window_id, window) in wt.windows.borrow().iter() { + if let Some(window) = window.upgrade() { + // Check if the window is on this monitor + let monitor = window.current_monitor(); + if monitor.name == new_monitor.name { + let (width, height) = window.inner_size_physical(); + let (new_width, new_height) = window.adjust_for_dpi( + // If we couldn't determine the previous scale + // factor (e.g., because all monitors were closed + // before), just pick whatever the current monitor + // has set as a baseline. + maybe_prev_scale_factor + .unwrap_or(monitor.scale_factor), + new_monitor.scale_factor, + width, + height, + &*window.shared_state.lock(), + ); + + let window_id = crate::window::WindowId(*window_id); + let old_inner_size = PhysicalSize::new(width, height); + let mut new_inner_size = + PhysicalSize::new(new_width, new_height); + + callback(Event::WindowEvent { + window_id, + event: WindowEvent::ScaleFactorChanged { + scale_factor: new_monitor.scale_factor, + new_inner_size: &mut new_inner_size, + }, + }); + + if new_inner_size != old_inner_size { + let (new_width, new_height) = new_inner_size.into(); + window + .set_inner_size_physical(new_width, new_height); + } + } + } + } + } + } + } + } } fn is_first_touch(first: &mut Option, num: &mut u32, id: u64, phase: TouchPhase) -> bool { diff --git a/src/platform_impl/linux/x11/util/randr.rs b/src/platform_impl/linux/x11/util/randr.rs index 2ce46e70e5e..287d7b55648 100644 --- a/src/platform_impl/linux/x11/util/randr.rs +++ b/src/platform_impl/linux/x11/util/randr.rs @@ -1,11 +1,13 @@ use std::{env, slice, str::FromStr}; + use super::{ ffi::{CurrentTime, RRCrtc, RRMode, Success, XRRCrtcInfo, XRRScreenResources}, *, }; use crate::platform_impl::platform::x11::monitor; use crate::{dpi::validate_scale_factor, platform_impl::platform::x11::VideoMode}; +use x11_dl::xlib::XTextProperty; /// Represents values of `WINIT_HIDPI_FACTOR`. pub enum EnvVarDPI { @@ -39,11 +41,23 @@ impl XConnection { // Retrieve DPI from Xft.dpi property pub unsafe fn get_xft_dpi(&self) -> Option { (self.xlib.XrmInitialize)(); - let resource_manager_str = (self.xlib.XResourceManagerString)(self.display); - if resource_manager_str.is_null() { - return None; - } - if let Ok(res) = ::std::ffi::CStr::from_ptr(resource_manager_str).to_str() { + + // Logic inspired by dunst + // https://github.com/dunst-project/dunst/commit/812d5a3b84c093adfbdab9939ee57545e442090c + (self.xlib.XFlush)(self.display); + + let default_screen = (self.xlib.XDefaultScreen)(self.display); + let root = (self.xlib.XRootWindow)(self.display, default_screen); + + let prop = &mut XTextProperty { value: ptr::null_mut(), encoding: 0, format: 0, nitems: 0 }; + + (self.xlib.XLockDisplay)(self.display); + (self.xlib.XGetTextProperty)(self.display, root, &mut *prop, ffi::XA_RESOURCE_MANAGER); + + (self.xlib.XFlush)(self.display); + (self.xlib.XSync)(self.display, 0); + + if let Ok(res) = ::std::ffi::CStr::from_ptr(prop.value as *const i8).to_str() { let name: &str = "Xft.dpi:\t"; for pair in res.split('\n') { if pair.starts_with(&name) { diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 39573c8867a..317841e3797 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -270,6 +270,14 @@ impl UnownedWindow { ) }; + unsafe { + (xconn.xlib.XSelectInput)( + xconn.display, + root, + ffi::PropertyChangeMask, + ); + } + let mut window = UnownedWindow { xconn: Arc::clone(xconn), xwindow,