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: f64) { 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>, } #[derive(Copy, Clone)] 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 { // 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) { 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: f64) { self.serial += 1; let scale = scale.max(1.0); let setting = IntSetting { value: (scale * DEFAULT_DPI as f64 * DPI_SCALE_FACTOR as f64).round() as i32, last_change_serial: self.serial, }; self.settings.entry(XFT_DPI).insert_entry(setting); // Gdk/WindowScalingFactor + Gdk/UnscaledDPI is identical to setting // GDK_SCALE = scale and then GDK_DPI_SCALE = 1 / scale. self.settings .entry(GDK_UNSCALED_DPI) .insert_entry(IntSetting { value: setting.value / scale as i32, last_change_serial: self.serial, }); self.settings .entry(GDK_WINDOW_SCALE) .insert_entry(IntSetting { value: scale as i32, last_change_serial: self.serial, }); } }