winit/platform_impl/linux/x11/util/
randr.rs

1use std::num::NonZeroU16;
2use std::str::FromStr;
3use std::{env, str};
4
5use tracing::warn;
6use x11rb::protocol::randr::{self, ConnectionExt as _};
7
8use super::*;
9use crate::dpi::validate_scale_factor;
10use crate::platform_impl::platform::x11::{monitor, VideoModeHandle};
11
12/// Represents values of `WINIT_HIDPI_FACTOR`.
13pub enum EnvVarDPI {
14    Randr,
15    Scale(f64),
16    NotSet,
17}
18
19pub fn calc_dpi_factor(
20    (width_px, height_px): (u32, u32),
21    (width_mm, height_mm): (u64, u64),
22) -> f64 {
23    // See http://xpra.org/trac/ticket/728 for more information.
24    if width_mm == 0 || height_mm == 0 {
25        warn!("XRandR reported that the display's 0mm in size, which is certifiably insane");
26        return 1.0;
27    }
28
29    let ppmm = ((width_px as f64 * height_px as f64) / (width_mm as f64 * height_mm as f64)).sqrt();
30    // Quantize 1/12 step size
31    let dpi_factor = ((ppmm * (12.0 * 25.4 / 96.0)).round() / 12.0).max(1.0);
32    assert!(validate_scale_factor(dpi_factor));
33    if dpi_factor <= 20. {
34        dpi_factor
35    } else {
36        1.
37    }
38}
39
40impl XConnection {
41    // Retrieve DPI from Xft.dpi property
42    pub fn get_xft_dpi(&self) -> Option<f64> {
43        // Try to get it from XSETTINGS first.
44        if let Some(xsettings_screen) = self.xsettings_screen() {
45            match self.xsettings_dpi(xsettings_screen) {
46                Ok(Some(dpi)) => return Some(dpi),
47                Ok(None) => {},
48                Err(err) => {
49                    tracing::warn!("failed to fetch XSettings: {err}");
50                },
51            }
52        }
53
54        self.database().get_string("Xft.dpi", "").and_then(|s| f64::from_str(s).ok())
55    }
56
57    pub fn get_output_info(
58        &self,
59        resources: &monitor::ScreenResources,
60        crtc: &randr::GetCrtcInfoReply,
61    ) -> Option<(String, f64, Vec<VideoModeHandle>)> {
62        let output_info = match self
63            .xcb_connection()
64            .randr_get_output_info(crtc.outputs[0], x11rb::CURRENT_TIME)
65            .map_err(X11Error::from)
66            .and_then(|r| r.reply().map_err(X11Error::from))
67        {
68            Ok(output_info) => output_info,
69            Err(err) => {
70                warn!("Failed to get output info: {:?}", err);
71                return None;
72            },
73        };
74
75        let bit_depth = self.default_root().root_depth;
76        let output_modes = &output_info.modes;
77        let resource_modes = resources.modes();
78        let current_mode = crtc.mode;
79
80        let modes = resource_modes
81            .iter()
82            // XRROutputInfo contains an array of mode ids that correspond to
83            // modes in the array in XRRScreenResources
84            .filter(|x| output_modes.iter().any(|id| x.id == *id))
85            .map(|mode| {
86                VideoModeHandle {
87                    current: mode.id == current_mode,
88                    size: (mode.width.into(), mode.height.into()),
89                    refresh_rate_millihertz: monitor::mode_refresh_rate_millihertz(mode),
90                    bit_depth: NonZeroU16::new(bit_depth as u16),
91                    native_mode: mode.id,
92                    // This is populated in `MonitorHandle::video_modes` as the
93                    // video mode is returned to the user
94                    monitor: None,
95                }
96            })
97            .collect();
98
99        let name = match str::from_utf8(&output_info.name) {
100            Ok(name) => name.to_owned(),
101            Err(err) => {
102                warn!("Failed to get output name: {:?}", err);
103                return None;
104            },
105        };
106        // Override DPI if `WINIT_X11_SCALE_FACTOR` variable is set
107        let deprecated_dpi_override = env::var("WINIT_HIDPI_FACTOR").ok();
108        if deprecated_dpi_override.is_some() {
109            warn!(
110                "The WINIT_HIDPI_FACTOR environment variable is deprecated; use \
111                 WINIT_X11_SCALE_FACTOR"
112            )
113        }
114        let dpi_env = env::var("WINIT_X11_SCALE_FACTOR").ok().map_or_else(
115            || EnvVarDPI::NotSet,
116            |var| {
117                if var.to_lowercase() == "randr" {
118                    EnvVarDPI::Randr
119                } else if let Ok(dpi) = f64::from_str(&var) {
120                    EnvVarDPI::Scale(dpi)
121                } else if var.is_empty() {
122                    EnvVarDPI::NotSet
123                } else {
124                    panic!(
125                        "`WINIT_X11_SCALE_FACTOR` invalid; DPI factors must be either normal \
126                         floats greater than 0, or `randr`. Got `{var}`"
127                    );
128                }
129            },
130        );
131
132        let scale_factor = match dpi_env {
133            EnvVarDPI::Randr => calc_dpi_factor(
134                (crtc.width.into(), crtc.height.into()),
135                (output_info.mm_width as _, output_info.mm_height as _),
136            ),
137            EnvVarDPI::Scale(dpi_override) => {
138                if !validate_scale_factor(dpi_override) {
139                    panic!(
140                        "`WINIT_X11_SCALE_FACTOR` invalid; DPI factors must be either normal \
141                         floats greater than 0, or `randr`. Got `{dpi_override}`",
142                    );
143                }
144                dpi_override
145            },
146            EnvVarDPI::NotSet => {
147                if let Some(dpi) = self.get_xft_dpi() {
148                    dpi / 96.
149                } else {
150                    calc_dpi_factor(
151                        (crtc.width.into(), crtc.height.into()),
152                        (output_info.mm_width as _, output_info.mm_height as _),
153                    )
154                }
155            },
156        };
157
158        Some((name, scale_factor, modes))
159    }
160
161    pub fn set_crtc_config(
162        &self,
163        crtc_id: randr::Crtc,
164        mode_id: randr::Mode,
165    ) -> Result<(), X11Error> {
166        let crtc =
167            self.xcb_connection().randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)?.reply()?;
168
169        self.xcb_connection()
170            .randr_set_crtc_config(
171                crtc_id,
172                crtc.timestamp,
173                x11rb::CURRENT_TIME,
174                crtc.x,
175                crtc.y,
176                mode_id,
177                crtc.rotation,
178                &crtc.outputs,
179            )?
180            .reply()
181            .map(|_| ())
182            .map_err(Into::into)
183    }
184
185    pub fn get_crtc_mode(&self, crtc_id: randr::Crtc) -> Result<randr::Mode, X11Error> {
186        Ok(self.xcb_connection().randr_get_crtc_info(crtc_id, x11rb::CURRENT_TIME)?.reply()?.mode)
187    }
188}