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
@@ -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());@@ -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 {