collective

Browse / github.com/storytold/craft-launcher / patch

fix(linux): honour desktop X11 scale factor and propagate to launched…

fix(linux): honour desktop X11 scale factor and propagate to launched… ## Summary On Linux, `winit`'s X11 backend ignores desktop DPI settings (`Xft.dpi`, `GDK_SCALE`, `QT_SCALE_FACTOR`) when `WINIT_X11_SCALE_FACTOR` is unset and instead computes the scale factor from the monitor's EDID physical millimeter dimensions reported by XRandR. Many HDMI monitors and TVs (such as Samsung 16:9 displays) encode their aspect ratio (`160 mm × 90 mm`, i.e., `16 cm × 9 cm`) in the EDID physical size fields rather than actual physical millimeters. On a `1920×1080` screen reporting `160 mm × 90 mm`, `winit` computes `304.8 DPI` (`scale_factor = 3.1667`), causing ArtCraft Launc

Status
open
Score
0.00
Adopters
0 other operators
Author
ccsimenson (imported from pull request #660, open)
Base
7c7271e5f62975326d0add2dcf31f03096be4189
Size
+165 −0 in 2 files
Review
pending
Shared
2026-10-28
Id
f3b6466529b4c2576e45dfb5

How it went

No other operator has reported using it yet.

Diff

raw · apps/artcraft-launcher/src/main.rs crates/native/src/process.rs

diff --git a/apps/artcraft-launcher/src/main.rs b/apps/artcraft-launcher/src/main.rs--- a/apps/artcraft-launcher/src/main.rs+++ b/apps/artcraft-launcher/src/main.rs@@ -33,7 +33,30 @@ const SETTINGS_KEY: &str = "settings";  type Wake = Arc<OnceLock<Box<dyn Fn() + Send + Sync>>>; +/// Resolves the X11 scale factor from `xrdb -query`, desktop environment variables, or XSETTINGS.+#[cfg(any(all(unix, not(target_os = "macos")), test))]+fn resolve_x11_scale_factor(xrdb_stdout: Option<&str>, xsettings: Option<&str>, env: impl Fn(&str) -> Option<String>) -> String {+    artcraft_launcher_native::process::resolve_x11_scale_factor(xrdb_stdout, xsettings, env)+}++/// Ensures `WINIT_X11_SCALE_FACTOR` is populated with a validated desktop scale factor on X11.+#[cfg(all(unix, not(target_os = "macos")))]+fn ensure_x11_scale_factor() {+    if std::env::var_os("DISPLAY").is_some()+        && window_can_hide_here(|name| std::env::var_os(name))+        && std::env::var_os("WINIT_X11_SCALE_FACTOR").is_none()+    {+        let scale = artcraft_launcher_native::process::cached_x11_scale_factor();+        if let Ok(exe) = std::env::current_exe() {+            use std::os::unix::process::CommandExt;+            let _ = std::process::Command::new(exe).args(std::env::args_os().skip(1)).env("WINIT_X11_SCALE_FACTOR", scale).exec();+        }+    }+}+ fn main() -> ExitCode {+    #[cfg(all(unix, not(target_os = "macos")))]+    ensure_x11_scale_factor();     // First, so the panic report and every start-up warning are recorded (`logging`).     let logs = logging::install();     logging::install_panic_hook();@@ -832,6 +855,37 @@ mod tests {         assert!(window_can_hide(None));     } +    #[test]+    fn x11_scale_factor_validates_inputs_and_preserves_xsettings_before_default() {+        let none = |_: &str| None;+        assert_eq!(resolve_x11_scale_factor(Some("Xft.dpi:\t96\n"), None, none), "1");+        assert_eq!(resolve_x11_scale_factor(Some("Xft.dpi: 144\n"), None, none), "1.5");+        // Malformed or non-normal Xft.dpi falls through to GDK_SCALE / QT_SCALE_FACTOR.+        assert_eq!(resolve_x11_scale_factor(Some("Xft.dpi: inf\n"), None, |k| (k == "GDK_SCALE").then(|| "2".to_string())), "2");+        // Empty or invalid GDK_SCALE falls through to valid QT_SCALE_FACTOR.+        assert_eq!(+            resolve_x11_scale_factor(None, None, |k| match k {+                "GDK_SCALE" => Some(String::new()),+                "QT_SCALE_FACTOR" => Some("1.25".to_string()),+                _ => None,+            }),+            "1.25"+        );+        assert_eq!(+            resolve_x11_scale_factor(None, None, |k| match k {+                "GDK_SCALE" => Some("auto".to_string()),+                "QT_SCALE_FACTOR" => Some("1.5".to_string()),+                _ => None,+            }),+            "1.5"+        );+        // XSETTINGS Xft/DPI (1024-scaled) is preserved when xrdb and env vars are unset.+        assert_eq!(resolve_x11_scale_factor(None, Some("Xft/DPI 196608\n"), none), "2");+        assert_eq!(resolve_x11_scale_factor(None, Some("Gdk/WindowScalingFactor 2\nGdk/UnscaledDPI 98304\n"), none), "2");+        // Malformed env and unset XSETTINGS fall back to "1".+        assert_eq!(resolve_x11_scale_factor(None, None, |k| (k == "GDK_SCALE").then(|| "invalid".to_string())), "1");+    }+     #[test]     fn a_wayland_session_is_known_before_the_window_opens() {         let env = |set: &'static [(&'static str, &'static str)]| move |name: &str| set.iter().find(|(n, _)| *n == name).map(|(_, v)| v.into());diff --git a/crates/native/src/process.rs b/crates/native/src/process.rs--- a/crates/native/src/process.rs+++ b/crates/native/src/process.rs@@ -18,6 +18,10 @@ pub fn command(program: impl AsRef<std::ffi::OsStr>) -> Command {     }     #[cfg(unix)]     clean_appimage_env(&mut cmd);+    #[cfg(all(unix, not(target_os = "macos")))]+    if std::env::var_os("DISPLAY").is_some() && std::env::var_os("WINIT_X11_SCALE_FACTOR").is_none() {+        cmd.env("WINIT_X11_SCALE_FACTOR", cached_x11_scale_factor());+    }     cmd } @@ -446,6 +450,113 @@ fn pgrep_pattern(path: &str) -> String {     pattern } +/// Validates a candidate X11 scale factor so invalid, non-positive, or non-normal values never+/// reach `WINIT_X11_SCALE_FACTOR` (which would panic inside `winit`).+pub fn valid_x11_scale(scale: f64) -> Option<String> {+    (scale.is_finite() && scale.is_normal() && scale > 0.0).then(|| scale.to_string())+}++/// Parses an XSETTINGS dump (`dump_xsettings` or `xsettingsd.conf`) for `Xft/DPI` or+/// `Gdk/WindowScalingFactor` / `Gdk/UnscaledDPI`.+pub fn parse_xsettings_scale(xsettings: &str) -> Option<String> {+    let mut xft_dpi: Option<f64> = None;+    let mut gdk_window_scale: Option<f64> = None;+    let mut gdk_unscaled_dpi: Option<f64> = None;++    for line in xsettings.lines() {+        let line = line.trim();+        if line.is_empty() || line.starts_with('#') {+            continue;+        }+        let mut parts = line.split_whitespace();+        let Some(key) = parts.next() else { continue };+        let Some(val_str) = parts.next() else { continue };+        let key = key.trim_end_matches(':');+        let Ok(raw) = val_str.parse::<f64>() else { continue };+        if !raw.is_finite() || raw <= 0.0 {+            continue;+        }+        match key {+            "Xft/DPI" => {+                let dpi = if raw >= 1024.0 { raw / 1024.0 } else { raw };+                xft_dpi = Some(dpi);+            }+            "Gdk/WindowScalingFactor" => {+                gdk_window_scale = Some(raw);+            }+            "Gdk/UnscaledDPI" => {+                let dpi = if raw >= 1024.0 { raw / 1024.0 } else { raw };+                gdk_unscaled_dpi = Some(dpi);+            }+            _ => {}+        }+    }++    if let Some(dpi) = xft_dpi+        && let Some(scale) = valid_x11_scale(dpi / 96.0)+    {+        return Some(scale);+    }+    if let Some(win_scale) = gdk_window_scale {+        let unscaled_factor = gdk_unscaled_dpi.map_or(1.0, |dpi| dpi / 96.0);+        if let Some(scale) = valid_x11_scale(win_scale * unscaled_factor) {+            return Some(scale);+        }+    }+    if let Some(dpi) = gdk_unscaled_dpi {+        return valid_x11_scale(dpi / 96.0);+    }+    None+}++/// Resolves the X11 scale factor from `xrdb -query` (`Xft.dpi`), `GDK_SCALE`, `QT_SCALE_FACTOR`,+/// or XSETTINGS (`Xft/DPI`, `Gdk/WindowScalingFactor`), defaulting to `"1"` so `winit` does not+/// compute an inflated DPI from aspect-ratio EDID millimeter placeholders (such as `160mm x 90mm`+/// on 16:9 HDMI monitors).+pub fn resolve_x11_scale_factor(xrdb_stdout: Option<&str>, xsettings: Option<&str>, env: impl Fn(&str) -> Option<String>) -> String {+    let valid_env = |name: &str| env(name).and_then(|value| value.trim().parse::<f64>().ok().and_then(valid_x11_scale));+    if let Some(text) = xrdb_stdout {+        for line in text.lines() {+            if let Some(rest) = line.strip_prefix("Xft.dpi:")+                && let Ok(dpi) = rest.trim().parse::<f64>()+                && let Some(scale) = valid_x11_scale(dpi / 96.0)+            {+                return scale;+            }+        }+    }+    if let Some(scale) = valid_env("GDK_SCALE").or_else(|| valid_env("QT_SCALE_FACTOR")) {+        return scale;+    }+    if let Some(scale) = xsettings.and_then(parse_xsettings_scale) {+        return scale;+    }+    "1".to_string()+}++/// Queries the host's X11 scale factor once with bounded timeouts and caches the resolved value.+#[cfg(all(unix, not(target_os = "macos")))]+pub fn cached_x11_scale_factor() -> &'static str {+    static SCALE: std::sync::OnceLock<String> = std::sync::OnceLock::new();+    SCALE.get_or_init(|| {+        if let Some(existing) = std::env::var("WINIT_X11_SCALE_FACTOR").ok().and_then(|v| v.trim().parse::<f64>().ok().and_then(valid_x11_scale)) {+            return existing;+        }+        let probe = |prog: &str, args: &[&str]| -> Option<String> {+            let mut cmd = Command::new(prog);+            clean_appimage_env(&mut cmd);+            cmd.args(args);+            run_command(cmd, prog, Duration::from_millis(500)).ok()+        };+        let xrdb_out = probe("xrdb", &["-query"]);+        let xsettings_out = probe("dump_xsettings", &[]).or_else(|| {+            let config = dirs::config_dir().or_else(|| dirs::home_dir().map(|h| h.join(".config")))?;+            std::fs::read_to_string(config.join("xsettingsd/xsettingsd.conf")).ok()+        });+        resolve_x11_scale_factor(xrdb_out.as_deref(), xsettings_out.as_deref(), |k| std::env::var(k).ok())+    })+}+ #[cfg(test)] #[cfg(unix)] mod tests {