Add Xsettings support, for setting scaling related settings
This allows for most GTK and Qt apps to be scaled properly. In the case of mixed DPI, it will default to using the smallest monitor scale.
This commit is contained in:
parent
ec9ff64c1e
commit
572fa4a2bf
9 changed files with 466 additions and 11 deletions
|
|
@ -196,6 +196,10 @@ pub fn main(data: impl RunData) -> Option<()> {
|
|||
if let Some(sel) = server_state.new_selection() {
|
||||
xstate.set_clipboard(sel);
|
||||
}
|
||||
|
||||
if let Some(scale) = server_state.new_global_scale() {
|
||||
xstate.update_global_scale(scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1272,6 +1272,7 @@ impl<C: XConnection> GlobalDispatch<WlOutput, Global> for ServerState<C> {
|
|||
Output::new(client, server).into()
|
||||
});
|
||||
state.output_keys.insert(key, ());
|
||||
state.output_scales_updated = true;
|
||||
}
|
||||
}
|
||||
global_dispatch_with_events!(WlDrmServer, WlDrmClient);
|
||||
|
|
|
|||
|
|
@ -822,6 +822,10 @@ impl Output {
|
|||
scale: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn scale(&self) -> i32 {
|
||||
self.scale
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -1027,6 +1031,7 @@ impl Output {
|
|||
|
||||
self.server.scale(factor);
|
||||
}
|
||||
state.output_scales_updated = true;
|
||||
}
|
||||
_ => simple_event_shunt! {
|
||||
self.server, event: Event => [
|
||||
|
|
|
|||
|
|
@ -522,6 +522,8 @@ pub struct ServerState<C: XConnection> {
|
|||
activation_state: Option<ActivationState>,
|
||||
global_output_offset: GlobalOutputOffset,
|
||||
global_offset_updated: bool,
|
||||
output_scales_updated: bool,
|
||||
new_scale: Option<i32>,
|
||||
decoration_manager: Option<ZxdgDecorationManagerV1>,
|
||||
}
|
||||
|
||||
|
|
@ -611,6 +613,8 @@ impl<C: XConnection> ServerState<C> {
|
|||
},
|
||||
},
|
||||
global_offset_updated: false,
|
||||
output_scales_updated: false,
|
||||
new_scale: None,
|
||||
decoration_manager,
|
||||
}
|
||||
}
|
||||
|
|
@ -1058,6 +1062,42 @@ impl<C: XConnection> ServerState<C> {
|
|||
self.global_offset_updated = false;
|
||||
}
|
||||
|
||||
if self.output_scales_updated {
|
||||
let mut mixed_scale = false;
|
||||
let mut scale;
|
||||
|
||||
'b: {
|
||||
let mut keys_iter = self.output_keys.iter();
|
||||
let (key, _) = keys_iter.next().unwrap();
|
||||
let Some::<&Output>(output) = &mut self.objects.get(key).map(AsRef::as_ref) else {
|
||||
// This should never happen, but you never know...
|
||||
break 'b;
|
||||
};
|
||||
|
||||
scale = output.scale();
|
||||
|
||||
for (key, _) in keys_iter {
|
||||
let Some::<&Output>(output) = self.objects.get(key).map(AsRef::as_ref) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if output.scale() != scale {
|
||||
mixed_scale = true;
|
||||
scale = scale.min(output.scale());
|
||||
}
|
||||
}
|
||||
|
||||
if mixed_scale {
|
||||
warn!("Mixed output scales detected, choosing to give apps the smallest detected scale ({scale}x)");
|
||||
}
|
||||
|
||||
debug!("Using new scale {scale}");
|
||||
self.new_scale = Some(scale);
|
||||
}
|
||||
|
||||
self.output_scales_updated = false;
|
||||
}
|
||||
|
||||
{
|
||||
if let Some(FocusData {
|
||||
window,
|
||||
|
|
@ -1083,6 +1123,10 @@ impl<C: XConnection> ServerState<C> {
|
|||
.expect("Failed flushing clientside events");
|
||||
}
|
||||
|
||||
pub fn new_global_scale(&mut self) -> Option<i32> {
|
||||
self.new_scale.take()
|
||||
}
|
||||
|
||||
pub fn new_selection(&mut self) -> Option<ForeignSelection> {
|
||||
self.clipboard_data.as_mut().and_then(|c| {
|
||||
c.source.take().and_then(|s| match s {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
mod settings;
|
||||
use settings::Settings;
|
||||
mod selection;
|
||||
use selection::{Selection, SelectionData};
|
||||
use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1;
|
||||
|
|
@ -111,6 +113,7 @@ pub struct XState {
|
|||
root: x::Window,
|
||||
wm_window: x::Window,
|
||||
selection_data: SelectionData,
|
||||
settings: Settings,
|
||||
}
|
||||
|
||||
impl XState {
|
||||
|
|
@ -185,6 +188,14 @@ impl XState {
|
|||
| SelectionEventMask::SELECTION_CLIENT_CLOSE,
|
||||
})
|
||||
.unwrap();
|
||||
connection
|
||||
.send_and_check_request(&xcb::xfixes::SelectSelectionInput {
|
||||
window: root,
|
||||
selection: atoms.xsettings,
|
||||
event_mask: SelectionEventMask::SELECTION_WINDOW_DESTROY
|
||||
| SelectionEventMask::SELECTION_CLIENT_CLOSE,
|
||||
})
|
||||
.unwrap();
|
||||
{
|
||||
// Setup default cursor theme
|
||||
let ctx = CursorContext::new(&connection, screen).unwrap();
|
||||
|
|
@ -200,6 +211,7 @@ impl XState {
|
|||
let wm_window = connection.generate_id();
|
||||
let selection_data = SelectionData::new(&connection, root);
|
||||
let window_atoms = WindowTypes::intern_all(&connection).unwrap();
|
||||
let settings = Settings::new(&connection, &atoms, root);
|
||||
|
||||
let mut r = Self {
|
||||
connection,
|
||||
|
|
@ -208,8 +220,10 @@ impl XState {
|
|||
atoms,
|
||||
window_atoms,
|
||||
selection_data,
|
||||
settings,
|
||||
};
|
||||
r.create_ewmh_window();
|
||||
r.set_xsettings_owner();
|
||||
r
|
||||
}
|
||||
|
||||
|
|
@ -876,6 +890,8 @@ xcb::atoms_struct! {
|
|||
timestamp => b"TIMESTAMP" only_if_exists = false,
|
||||
selection_reply => b"_selection_reply" only_if_exists = false,
|
||||
incr => b"INCR" only_if_exists = false,
|
||||
xsettings => b"_XSETTINGS_S0" only_if_exists = false,
|
||||
xsettings_settings => b"_XSETTINGS_SETTINGS" only_if_exists = false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -281,15 +281,16 @@ impl XState {
|
|||
debug!("Clipboard set from Wayland");
|
||||
}
|
||||
|
||||
pub(crate) fn handle_selection_event(
|
||||
pub(super) fn handle_selection_event(
|
||||
&mut self,
|
||||
event: &xcb::Event,
|
||||
server_state: &mut RealServerState,
|
||||
) -> bool {
|
||||
match event {
|
||||
// Someone else took the clipboard owner
|
||||
xcb::Event::X(x::Event::SelectionClear(e)) => {
|
||||
self.handle_new_selection_owner(e.owner(), e.time());
|
||||
if e.selection() == self.atoms.clipboard {
|
||||
self.handle_new_selection_owner(e.owner(), e.time());
|
||||
}
|
||||
}
|
||||
xcb::Event::X(x::Event::SelectionNotify(e)) => {
|
||||
if e.property() == x::ATOM_NONE {
|
||||
|
|
@ -414,9 +415,8 @@ impl XState {
|
|||
}
|
||||
}
|
||||
|
||||
xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => {
|
||||
assert_eq!(e.selection(), self.atoms.clipboard);
|
||||
match e.subtype() {
|
||||
xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => match e.selection() {
|
||||
x if x == self.atoms.clipboard => match e.subtype() {
|
||||
xcb::xfixes::SelectionEvent::SetSelectionOwner => {
|
||||
if e.owner() == self.wm_window {
|
||||
return true;
|
||||
|
|
@ -429,8 +429,17 @@ impl XState {
|
|||
debug!("Selection owner destroyed, selection will be unset");
|
||||
self.selection_data.current_selection = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
x if x == self.atoms.xsettings => match e.subtype() {
|
||||
xcb::xfixes::SelectionEvent::SelectionClientClose
|
||||
| xcb::xfixes::SelectionEvent::SelectionWindowDestroy => {
|
||||
debug!("Xsettings owner disappeared, reacquiring");
|
||||
self.set_xsettings_owner();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
},
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
|
|
|
|||
177
src/xstate/settings.rs
Normal file
177
src/xstate/settings.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
use super::XState;
|
||||
use log::warn;
|
||||
use std::collections::HashMap;
|
||||
use xcb::x;
|
||||
|
||||
impl XState {
|
||||
pub(crate) fn set_xsettings_owner(&self) {
|
||||
self.connection
|
||||
.send_and_check_request(&x::SetSelectionOwner {
|
||||
owner: self.settings.window,
|
||||
selection: self.atoms.xsettings,
|
||||
time: x::CURRENT_TIME,
|
||||
})
|
||||
.unwrap();
|
||||
let reply = self
|
||||
.connection
|
||||
.wait_for_reply(self.connection.send_request(&x::GetSelectionOwner {
|
||||
selection: self.atoms.xsettings,
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
if reply.owner() != self.settings.window {
|
||||
warn!(
|
||||
"Could not get XSETTINGS selection (owned by {:?})",
|
||||
reply.owner()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_global_scale(&mut self, scale: i32) {
|
||||
self.settings.set_scale(scale);
|
||||
self.connection
|
||||
.send_and_check_request(&x::ChangeProperty {
|
||||
window: self.settings.window,
|
||||
mode: x::PropMode::Replace,
|
||||
property: self.atoms.xsettings_settings,
|
||||
r#type: self.atoms.xsettings_settings,
|
||||
data: &self.settings.as_data(),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// The DPI consider 1x scale by X11.
|
||||
const DEFAULT_DPI: i32 = 96;
|
||||
/// I don't know why, but the DPI related xsettings seem to
|
||||
/// divide the DPI by 1024.
|
||||
const DPI_SCALE_FACTOR: i32 = 1024;
|
||||
|
||||
const XFT_DPI: &str = "Xft/DPI";
|
||||
const GDK_WINDOW_SCALE: &str = "Gdk/WindowScalingFactor";
|
||||
const GDK_UNSCALED_DPI: &str = "Gdk/UnscaledDPI";
|
||||
|
||||
pub(super) struct Settings {
|
||||
window: x::Window,
|
||||
serial: u32,
|
||||
settings: HashMap<&'static str, IntSetting>,
|
||||
}
|
||||
|
||||
struct IntSetting {
|
||||
value: i32,
|
||||
last_change_serial: u32,
|
||||
}
|
||||
|
||||
mod setting_type {
|
||||
pub const INTEGER: u8 = 0;
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub(super) fn new(connection: &xcb::Connection, atoms: &super::Atoms, root: x::Window) -> Self {
|
||||
let window = connection.generate_id();
|
||||
connection
|
||||
.send_and_check_request(&x::CreateWindow {
|
||||
wid: window,
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth: 0,
|
||||
parent: root,
|
||||
x: 0,
|
||||
y: 0,
|
||||
border_width: 0,
|
||||
class: x::WindowClass::InputOnly,
|
||||
visual: x::COPY_FROM_PARENT,
|
||||
value_list: &[],
|
||||
})
|
||||
.expect("Couldn't create window for settings");
|
||||
|
||||
let s = Settings {
|
||||
window,
|
||||
serial: 0,
|
||||
settings: HashMap::from([
|
||||
(
|
||||
XFT_DPI,
|
||||
IntSetting {
|
||||
value: DEFAULT_DPI * DPI_SCALE_FACTOR,
|
||||
last_change_serial: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
GDK_WINDOW_SCALE,
|
||||
IntSetting {
|
||||
value: 1,
|
||||
last_change_serial: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
GDK_UNSCALED_DPI,
|
||||
IntSetting {
|
||||
value: DEFAULT_DPI * DPI_SCALE_FACTOR,
|
||||
last_change_serial: 0,
|
||||
},
|
||||
),
|
||||
]),
|
||||
};
|
||||
|
||||
connection
|
||||
.send_and_check_request(&x::ChangeProperty {
|
||||
window,
|
||||
mode: x::PropMode::Replace,
|
||||
property: atoms.xsettings_settings,
|
||||
r#type: atoms.xsettings_settings,
|
||||
data: &s.as_data(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
fn as_data(&self) -> Vec<u8> {
|
||||
// https://specifications.freedesktop.org/xsettings-spec/0.5/#format
|
||||
|
||||
let mut data = vec![
|
||||
// GTK seems to use this value for byte order from the X.h header,
|
||||
// so I assume I can use it too.
|
||||
x::ImageOrder::LsbFirst as u8,
|
||||
// unused
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
];
|
||||
|
||||
data.extend_from_slice(&self.serial.to_le_bytes());
|
||||
data.extend_from_slice(&(self.settings.len() as u32).to_le_bytes());
|
||||
|
||||
fn insert_with_padding(data: &[u8], out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(data);
|
||||
// See https://x.org/releases/X11R7.7/doc/xproto/x11protocol.html#Syntactic_Conventions_b
|
||||
let num_padding_bytes = (4 - (data.len() % 4)) % 4;
|
||||
out.extend(std::iter::repeat_n(0, num_padding_bytes));
|
||||
}
|
||||
|
||||
for (name, setting) in &self.settings {
|
||||
data.extend_from_slice(&[setting_type::INTEGER, 0]);
|
||||
data.extend_from_slice(&(name.len() as u16).to_le_bytes());
|
||||
insert_with_padding(name.as_bytes(), &mut data);
|
||||
data.extend_from_slice(&setting.last_change_serial.to_le_bytes());
|
||||
data.extend_from_slice(&setting.value.to_le_bytes());
|
||||
}
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
fn set_scale(&mut self, scale: i32) {
|
||||
self.serial += 1;
|
||||
|
||||
self.settings.entry(XFT_DPI).insert_entry(IntSetting {
|
||||
value: scale * DEFAULT_DPI * DPI_SCALE_FACTOR,
|
||||
last_change_serial: self.serial,
|
||||
});
|
||||
self.settings
|
||||
.entry(GDK_WINDOW_SCALE)
|
||||
.insert_entry(IntSetting {
|
||||
value: scale,
|
||||
last_change_serial: self.serial,
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue