diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 430ce9260b..6cff977128 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -77,6 +77,8 @@ pub(crate) const XINPUT_ALL_DEVICES: xinput::DeviceId = 0; /// terminology is both archaic and unclear. pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1; +const GPUI_X11_SCALE_FACTOR_ENV: &str = "GPUI_X11_SCALE_FACTOR"; + pub(crate) struct WindowRef { window: X11WindowStatePtr, refresh_state: Option, @@ -424,12 +426,7 @@ impl X11Client { let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection) .context("Failed to create resource database")?; - let scale_factor = resource_database - .get_value("Xft.dpi", "Xft.dpi") - .ok() - .flatten() - .map(|dpi: f32| dpi / 96.0) - .unwrap_or(1.0); + let scale_factor = get_scale_factor(&xcb_connection, &resource_database, x_root_index); let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database) .context("Failed to initialize cursor theme handler")? .reply() @@ -2272,3 +2269,253 @@ fn create_invisible_cursor( xcb_flush(connection); Ok(cursor) } + +enum DpiMode { + Randr, + Scale(f32), + NotSet, +} + +fn get_scale_factor( + connection: &XCBConnection, + resource_database: &Database, + screen_index: usize, +) -> f32 { + let env_dpi = std::env::var(GPUI_X11_SCALE_FACTOR_ENV) + .ok() + .map(|var| { + if var.to_lowercase() == "randr" { + DpiMode::Randr + } else if let Ok(scale) = var.parse::() { + if valid_scale_factor(scale) { + DpiMode::Scale(scale) + } else { + panic!( + "`{}` must be a positive normal number or `randr`. Got `{}`", + GPUI_X11_SCALE_FACTOR_ENV, var + ); + } + } else if var.is_empty() { + DpiMode::NotSet + } else { + panic!( + "`{}` must be a positive number or `randr`. Got `{}`", + GPUI_X11_SCALE_FACTOR_ENV, var + ); + } + }) + .unwrap_or(DpiMode::NotSet); + + match env_dpi { + DpiMode::Scale(scale) => { + log::info!( + "Using scale factor from {}: {}", + GPUI_X11_SCALE_FACTOR_ENV, + scale + ); + return scale; + } + DpiMode::Randr => { + if let Some(scale) = get_randr_scale_factor(connection, screen_index) { + log::info!( + "Using RandR scale factor from {}=randr: {}", + GPUI_X11_SCALE_FACTOR_ENV, + scale + ); + return scale; + } + log::warn!("Failed to calculate RandR scale factor, falling back to default"); + return 1.0; + } + DpiMode::NotSet => {} + } + + // TODO: Use scale factor from XSettings here + + if let Some(dpi) = resource_database + .get_value::("Xft.dpi", "Xft.dpi") + .ok() + .flatten() + { + let scale = dpi / 96.0; // base dpi + log::info!("Using scale factor from Xft.dpi: {}", scale); + return scale; + } + + if let Some(scale) = get_randr_scale_factor(connection, screen_index) { + log::info!("Using RandR scale factor: {}", scale); + return scale; + } + + log::info!("Using default scale factor: 1.0"); + 1.0 +} + +fn get_randr_scale_factor(connection: &XCBConnection, screen_index: usize) -> Option { + let root = connection.setup().roots.get(screen_index)?.root; + + let version_cookie = connection.randr_query_version(1, 6).ok()?; + let version_reply = version_cookie.reply().ok()?; + if version_reply.major_version < 1 + || (version_reply.major_version == 1 && version_reply.minor_version < 5) + { + return legacy_get_randr_scale_factor(connection, root); // for randr <1.5 + } + + let monitors_cookie = connection.randr_get_monitors(root, true).ok()?; // true for active only + let monitors_reply = monitors_cookie.reply().ok()?; + + let mut fallback_scale: Option = None; + for monitor in monitors_reply.monitors { + if monitor.width_in_millimeters == 0 || monitor.height_in_millimeters == 0 { + continue; + } + let scale_factor = get_dpi_factor( + (monitor.width as u32, monitor.height as u32), + ( + monitor.width_in_millimeters as u64, + monitor.height_in_millimeters as u64, + ), + ); + if monitor.primary { + return Some(scale_factor); + } else if fallback_scale.is_none() { + fallback_scale = Some(scale_factor); + } + } + + fallback_scale +} + +fn legacy_get_randr_scale_factor(connection: &XCBConnection, root: u32) -> Option { + let primary_cookie = connection.randr_get_output_primary(root).ok()?; + let primary_reply = primary_cookie.reply().ok()?; + let primary_output = primary_reply.output; + + let primary_output_cookie = connection + .randr_get_output_info(primary_output, x11rb::CURRENT_TIME) + .ok()?; + let primary_output_info = primary_output_cookie.reply().ok()?; + + // try primary + if primary_output_info.connection == randr::Connection::CONNECTED + && primary_output_info.mm_width > 0 + && primary_output_info.mm_height > 0 + && primary_output_info.crtc != 0 + { + let crtc_cookie = connection + .randr_get_crtc_info(primary_output_info.crtc, x11rb::CURRENT_TIME) + .ok()?; + let crtc_info = crtc_cookie.reply().ok()?; + + if crtc_info.width > 0 && crtc_info.height > 0 { + let scale_factor = get_dpi_factor( + (crtc_info.width as u32, crtc_info.height as u32), + ( + primary_output_info.mm_width as u64, + primary_output_info.mm_height as u64, + ), + ); + return Some(scale_factor); + } + } + + // fallback: full scan + let resources_cookie = connection.randr_get_screen_resources_current(root).ok()?; + let screen_resources = resources_cookie.reply().ok()?; + + let mut crtc_cookies = Vec::with_capacity(screen_resources.crtcs.len()); + for &crtc in &screen_resources.crtcs { + if let Ok(cookie) = connection.randr_get_crtc_info(crtc, x11rb::CURRENT_TIME) { + crtc_cookies.push((crtc, cookie)); + } + } + + let mut crtc_infos: HashMap = HashMap::default(); + let mut valid_outputs: HashSet = HashSet::new(); + for (crtc, cookie) in crtc_cookies { + if let Ok(reply) = cookie.reply() { + if reply.width > 0 && reply.height > 0 && !reply.outputs.is_empty() { + crtc_infos.insert(crtc, reply.clone()); + valid_outputs.extend(&reply.outputs); + } + } + } + + if valid_outputs.is_empty() { + return None; + } + + let mut output_cookies = Vec::with_capacity(valid_outputs.len()); + for &output in &valid_outputs { + if let Ok(cookie) = connection.randr_get_output_info(output, x11rb::CURRENT_TIME) { + output_cookies.push((output, cookie)); + } + } + let mut output_infos: HashMap = HashMap::default(); + for (output, cookie) in output_cookies { + if let Ok(reply) = cookie.reply() { + output_infos.insert(output, reply); + } + } + + let mut fallback_scale: Option = None; + for crtc_info in crtc_infos.values() { + for &output in &crtc_info.outputs { + if let Some(output_info) = output_infos.get(&output) { + if output_info.connection != randr::Connection::CONNECTED { + continue; + } + + if output_info.mm_width == 0 || output_info.mm_height == 0 { + continue; + } + + let scale_factor = get_dpi_factor( + (crtc_info.width as u32, crtc_info.height as u32), + (output_info.mm_width as u64, output_info.mm_height as u64), + ); + + if output != primary_output && fallback_scale.is_none() { + fallback_scale = Some(scale_factor); + } + } + } + } + + fallback_scale +} + +fn get_dpi_factor((width_px, height_px): (u32, u32), (width_mm, height_mm): (u64, u64)) -> f32 { + let ppmm = ((width_px as f64 * height_px as f64) / (width_mm as f64 * height_mm as f64)).sqrt(); // pixels per mm + + const MM_PER_INCH: f64 = 25.4; + const BASE_DPI: f64 = 96.0; + const QUANTIZE_STEP: f64 = 12.0; // e.g. 1.25 = 15/12, 1.5 = 18/12, 1.75 = 21/12, 2.0 = 24/12 + const MIN_SCALE: f64 = 1.0; + const MAX_SCALE: f64 = 20.0; + + let dpi_factor = + ((ppmm * (QUANTIZE_STEP * MM_PER_INCH / BASE_DPI)).round() / QUANTIZE_STEP).max(MIN_SCALE); + + let validated_factor = if dpi_factor <= MAX_SCALE { + dpi_factor + } else { + MIN_SCALE + }; + + if valid_scale_factor(validated_factor as f32) { + validated_factor as f32 + } else { + log::warn!( + "Calculated DPI factor {} is invalid, using 1.0", + validated_factor + ); + 1.0 + } +} + +#[inline] +fn valid_scale_factor(scale_factor: f32) -> bool { + scale_factor.is_sign_positive() && scale_factor.is_normal() +}