diff --git a/Cargo.lock b/Cargo.lock index 092d184..0cd7fa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -668,6 +668,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "combine" version = "4.6.6" @@ -814,6 +824,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -1789,6 +1805,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k9" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "088bcebb5b68b1b14b64d7f05b0f802719250b97fdc0338ec42529ea777ed614" +dependencies = [ + "anyhow", + "colored", + "diff", + "lazy_static", + "libc", + "proc-macro2", + "regex", + "syn 2.0.55", + "terminal_size", +] + [[package]] name = "keyframe" version = "1.1.1" @@ -2174,6 +2207,7 @@ dependencies = [ "git-version", "glam", "input", + "k9", "keyframe", "libc", "log", @@ -3308,6 +3342,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "terminal_size" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +dependencies = [ + "rustix 0.37.27", + "windows-sys 0.48.0", +] + [[package]] name = "thiserror" version = "1.0.58" diff --git a/Cargo.toml b/Cargo.toml index 51c2ae0..e9f1021 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,7 @@ features = [ ] [dev-dependencies] +k9 = "0.12.0" proptest = "1.4.0" proptest-derive = "0.4.0" xshell = "0.2.5" diff --git a/src/niri.rs b/src/niri.rs index 605f5c6..10fc31a 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -116,6 +116,7 @@ use crate::ui::config_error_notification::ConfigErrorNotification; use crate::ui::exit_confirm_dialog::ExitConfirmDialog; use crate::ui::hotkey_overlay::HotkeyOverlay; use crate::ui::screenshot_ui::{ScreenshotUi, ScreenshotUiRenderElement}; +use crate::utils::scale::guess_monitor_scale; use crate::utils::spawning::CHILD_ENV; use crate::utils::{ center, center_f64, get_monotonic_time, ipc_transform_to_smithay, logical_output, @@ -934,7 +935,11 @@ impl State { let config = self.niri.config.borrow_mut(); let config = config.outputs.iter().find(|o| o.name == name); - let scale = config.map(|c| c.scale).unwrap_or(1.); + let scale = config.map(|c| c.scale).unwrap_or_else(|| { + let size_mm = output.physical_properties().size; + let resolution = output.current_mode().unwrap().size; + guess_monitor_scale(size_mm, resolution) + }); let scale = scale.clamp(1., 10.).ceil() as i32; let mut transform = config @@ -1565,7 +1570,11 @@ impl Niri { let config = self.config.borrow(); let c = config.outputs.iter().find(|o| o.name == name); - let scale = c.map(|c| c.scale).unwrap_or(1.); + let scale = c.map(|c| c.scale).unwrap_or_else(|| { + let size_mm = output.physical_properties().size; + let resolution = output.current_mode().unwrap().size; + guess_monitor_scale(size_mm, resolution) + }); let scale = scale.clamp(1., 10.).ceil() as i32; let mut transform = c .map(|c| ipc_transform_to_smithay(c.transform)) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 781eec6..db71587 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -15,6 +15,7 @@ use smithay::reexports::rustix::time::{clock_gettime, ClockId}; use smithay::utils::{Logical, Point, Rectangle, Size, Transform}; pub mod id; +pub mod scale; pub mod spawning; pub mod watcher; diff --git a/src/utils/scale.rs b/src/utils/scale.rs new file mode 100644 index 0000000..339d067 --- /dev/null +++ b/src/utils/scale.rs @@ -0,0 +1,104 @@ +//! Default monitor scale calculation. +//! +//! This module follows logic and tests from Mutter: +//! https://gitlab.gnome.org/GNOME/mutter/-/blob/gnome-46/src/backends/meta-monitor.c + +use smithay::utils::{Physical, Raw, Size}; + +const MIN_SCALE: i32 = 1; +const MAX_SCALE: i32 = 4; +const MIN_LOGICAL_AREA: i32 = 800 * 480; + +const MOBILE_TARGET_DPI: f64 = 135.; +const LARGE_TARGET_DPI: f64 = 110.; +const LARGE_MIN_SIZE_INCHES: f64 = 20.; + +/// Calculates the ideal scale for a monitor. +pub fn guess_monitor_scale(size_mm: Size, resolution: Size) -> f64 { + if size_mm.w == 0 || size_mm.h == 0 { + return 1.; + } + + let diag_inches = f64::from(size_mm.w * size_mm.w + size_mm.h * size_mm.h).sqrt() / 25.4; + + let target_dpi = if diag_inches < LARGE_MIN_SIZE_INCHES { + MOBILE_TARGET_DPI + } else { + LARGE_TARGET_DPI + }; + + let physical_dpi = + f64::from(resolution.w * resolution.w + resolution.h * resolution.h).sqrt() / diag_inches; + let perfect_scale = physical_dpi / target_dpi; + + // For integer scaling factors (we currently only do integer), bias the perfect scale down. + let perfect_scale = perfect_scale - 0.15; + + supported_scales(resolution) + .map(|scale| (scale, (scale - perfect_scale).abs())) + .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap()) + .map_or(1., |(scale, _)| scale) +} + +fn supported_scales(resolution: Size) -> impl Iterator { + (MIN_SCALE..=MAX_SCALE) + .filter(move |scale| is_valid_for_resolution(resolution, *scale)) + .map(f64::from) +} + +fn is_valid_for_resolution(resolution: Size, scale: i32) -> bool { + let logical = resolution.to_logical(scale); + logical.w * logical.h >= MIN_LOGICAL_AREA +} + +#[cfg(test)] +mod tests { + use k9::snapshot; + + use super::*; + + fn check(size_mm: (i32, i32), resolution: (i32, i32)) -> f64 { + guess_monitor_scale(Size::from(size_mm), Size::from(resolution)) + } + + #[test] + fn test_guess_monitor_scale() { + // Librem 5; not enough logical area when scaled + snapshot!(check((65, 129), (720, 1440)), "1.0"); + // OnePlus 6 + snapshot!(check((68, 144), (1080, 2280)), "2.0"); + // Google Pixel 6a + snapshot!(check((64, 142), (1080, 2400)), "2.0"); + // 13" MacBook Retina + snapshot!(check((286, 179), (2560, 1600)), "2.0"); + // Surface Laptop Studio + snapshot!(check((303, 202), (2400, 1600)), "1.0"); + // Dell XPS 9320 + snapshot!(check((290, 180), (3840, 2400)), "2.0"); + // Lenovo ThinkPad X1 Yoga Gen 6 + snapshot!(check((300, 190), (3840, 2400)), "2.0"); + // Generic 23" 1080p + snapshot!(check((509, 286), (1920, 1080)), "1.0"); + // Generic 23" 4K + snapshot!(check((509, 286), (3840, 2160)), "2.0"); + // Generic 27" 4K + snapshot!(check((598, 336), (3840, 2160)), "1.0"); + // Generic 32" 4K + snapshot!(check((708, 398), (3840, 2160)), "1.0"); + // Generic 25" 4K; ideal scale is 1.60, should round to 1.5 and 1.0 + snapshot!(check((554, 312), (3840, 2160)), "1.0"); + // Generic 23.5" 4K; ideal scale is 1.70, should round to 1.75 and 2.0 + snapshot!(check((522, 294), (3840, 2160)), "2.0"); + // Lenovo Legion 7 Gen 7 AMD 16" + snapshot!(check((340, 210), (2560, 1600)), "1.0"); + // Acer Nitro XV320QU LV 31.5" + snapshot!(check((700, 390), (2560, 1440)), "1.0"); + // Surface Pro 6 + snapshot!(check((260, 170), (2736, 1824)), "2.0"); + } + + #[test] + fn guess_monitor_scale_unknown_size() { + assert_eq!(check((0, 0), (1920, 1080)), 1.); + } +} diff --git a/wiki/Configuration:-Outputs.md b/wiki/Configuration:-Outputs.md index a175fd6..53872ff 100644 --- a/wiki/Configuration:-Outputs.md +++ b/wiki/Configuration:-Outputs.md @@ -68,6 +68,8 @@ Set the scale of the monitor. This is a floating-point number to enable fractional scaling in the future, but at the moment only integer scale values will work. +Since: 0.1.6 If scale is unset, niri will guess an appropriate scale based on the physical dimensions and the resolution of the monitor. + ``` output "eDP-1" { scale 2.0