diff --git a/Cargo.lock b/Cargo.lock index 71964991..0fa55b03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3982,6 +3982,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "winsafe", ] [[package]] diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 713e5b2a..9cfe6bfe 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -51,6 +51,8 @@ objc2-foundation = { version = "0.3", default-features = false, features = ["std [target.'cfg(windows)'.dependencies] # Auto interface language: the Windows display language, without starting a process. sys-locale = "0.3" +# Region ▸ Measurement system for the default Units, read from the registry without starting reg.exe. +winsafe = { version = "0.0.29", default-features = false, features = ["advapi"] } [target.'cfg(target_arch = "wasm32")'.dependencies] rfd = { workspace = true, features = ["file-handle-inner"] } diff --git a/crates/ui-egui/src/units.rs b/crates/ui-egui/src/units.rs index d1de152c..a345456f 100644 --- a/crates/ui-egui/src/units.rs +++ b/crates/ui-egui/src/units.rs @@ -99,29 +99,37 @@ fn detect() -> Option { { return Some(unit); } + // Region ▸ Measurement system, read in-process: a child `reg.exe` is a process-creation event + // that endpoint protection watches on managed PCs, and a preference doesn't warrant one. #[cfg(target_os = "windows")] - { - use std::os::windows::process::CommandExt; - // Region ▸ Measurement system: 0 metric, 1 U.S. The absolute path keeps a `reg` earlier on - // PATH from running; CREATE_NO_WINDOW keeps a console from flashing up. - let windir = std::env::var_os("SystemRoot").unwrap_or_else(|| "C:\\Windows".into()); - let reg = std::path::Path::new(&windir).join("System32").join("reg.exe"); - if let Ok(out) = std::process::Command::new(reg) - .args(["query", "HKCU\\Control Panel\\International", "/v", "iMeasure"]) - .creation_flags(0x0800_0000) - .output() - && out.status.success() - { - match String::from_utf8_lossy(&out.stdout).split_whitespace().last() { - Some("1") => return Some(Unit::Inches), - Some("0") => return Some(Unit::Millimetres), - _ => {} - } - } + if let Some(unit) = windows_measure().and_then(|v| unit_for_imeasure(&v)) { + return Some(unit); } ["LC_ALL", "LC_MEASUREMENT", "LANG"].iter().find_map(|var| std::env::var(var).ok().filter(|v| !v.is_empty()).and_then(|v| unit_for_tag(&v))) } +/// The unit for the Windows `iMeasure` value (Region ▸ Measurement system): "0" metric, "1" U.S. +/// `None` for anything else, so a missing or odd value falls through to the locale check. +#[cfg(any(test, target_os = "windows"))] +pub(crate) fn unit_for_imeasure(value: &str) -> Option { + match value.trim() { + "1" => Some(Unit::Inches), + "0" => Some(Unit::Millimetres), + _ => None, + } +} + +/// `HKCU\Control Panel\International\iMeasure` as text; `None` when it is missing, unreadable or +/// not a string. +#[cfg(target_os = "windows")] +fn windows_measure() -> Option { + use winsafe::{HKEY, RegistryValue, co::RRF}; + match HKEY::CURRENT_USER.RegGetValue(Some("Control Panel\\International"), Some("iMeasure"), RRF::RT_REG_SZ).ok()? { + RegistryValue::Sz(v) => Some(v), + _ => None, + } +} + #[cfg(target_arch = "wasm32")] fn detect() -> Option { None @@ -129,7 +137,7 @@ fn detect() -> Option { #[cfg(test)] mod tests { - use super::{Unit, unit_for_tag}; + use super::{Unit, unit_for_imeasure, unit_for_tag}; #[test] fn region_decides_the_unit() { @@ -146,6 +154,28 @@ mod tests { assert_eq!(unit_for_tag(""), None); } + #[test] + fn windows_measurement_system_decides_the_unit() { + assert_eq!(unit_for_imeasure("1"), Some(Unit::Inches)); + assert_eq!(unit_for_imeasure("0"), Some(Unit::Millimetres)); + assert_eq!(unit_for_imeasure(" 1\r\n"), Some(Unit::Inches)); + assert_eq!(unit_for_imeasure(""), None); + assert_eq!(unit_for_imeasure("2"), None); + assert_eq!(unit_for_imeasure("metric"), None); + } + + /// The registry read works in-process (no `reg.exe`) and gives a value the mapping understands. + #[cfg(target_os = "windows")] + #[test] + fn windows_reads_imeasure_in_process() { + let value = super::windows_measure(); + // Every Windows user profile has Region settings; if a sandbox ever lacks them, the read must + // simply be None rather than failing. + if let Some(v) = value { + assert!(unit_for_imeasure(&v).is_some(), "unexpected iMeasure {v:?}"); + } + } + #[test] fn a4_in_every_unit() { assert_eq!(Unit::Millimetres.size(595.276, 841.89), "210.0 × 297.0 mm");