Bug 1521450 - Enable per-monitor DPI scaling V2 in the Windows GUI r=gsvelto,glandium

This is quite an improvement on the quirks of the previous GDI scaling.

It also mostly supports the windows 10+ "Make text bigger" setting: it
reads the value from the registry (albeit at an unofficial location),
but doesn't register a key change listener to update the value if it
changes while the application is open. I think this is very, very likely
to be good enough; I will be surprised if someone notices this
deficiency! The official API is part of UWP and is accessible through
C++ libraries, but not conveniently through win32 APIs, which is why I
use the registry.

Differential Revision: https://phabricator.services.mozilla.com/D221544
This commit is contained in:
Alex Franchuk 2024-09-27 17:18:58 +00:00
Родитель d9fa881390
Коммит 0ad3c6a678
9 изменённых файлов: 289 добавлений и 54 удалений

Просмотреть файл

@ -216,11 +216,13 @@ features = [
"Win32_System_Memory",
"Win32_System_Pipes",
"Win32_System_ProcessStatus",
"Win32_System_Registry",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_System_WindowsProgramming",
"Win32_UI_Controls",
"Win32_UI_HiDpi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",

Просмотреть файл

@ -47,9 +47,11 @@ features = [
"Win32_Graphics_Gdi",
"Win32_System_Com",
"Win32_System_LibraryLoader",
"Win32_System_Registry",
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_UI_Controls",
"Win32_UI_HiDpi",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging"

Просмотреть файл

@ -25,9 +25,9 @@ fn windows_manifest() {
// Use legacy active code page because GDI doesn't support per-process UTF8 (and older
// win10 may not support this setting anyway).
.active_code_page(manifest::ActiveCodePage::Legacy)
// GDI scaling is not enabled by default but we need it to make the GDI-drawn text look
// nice on high-DPI displays.
.gdi_scaling(manifest::Setting::Enabled);
// We support WM_DPICHANGED for scaling, but need to set our DPI awareness to receive the
// messages.
.dpi_awareness(manifest::DpiAwareness::PerMonitorV2);
embed_manifest(manifest).expect("unable to embed windows manifest file");

Просмотреть файл

@ -135,6 +135,15 @@ impl<T> Synchronized<T> {
});
}
/// Immediately call the closure on the current value and call it whenever the value changes.
pub fn map_with<F: Fn(&T) + 'static>(&self, f: F) {
// Hold the borrow to guarantee the value doesn't change until we are subscribed (just as a
// measure of extra sanity; this should never occur).
let borrow = self.borrow();
f(&*borrow);
self.on_change(f);
}
/// Create a new synchronized value which will update when this one changes.
pub fn mapped<U: 'static, F: Fn(&T) -> U + 'static>(&self, f: F) -> Synchronized<U> {
let s = Synchronized::new(f(&*self.borrow()));

Просмотреть файл

@ -0,0 +1,63 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
//! DPI management utilities.
use windows_sys::Win32::{
Foundation::HWND,
UI::{HiDpi::GetDpiForWindow, WindowsAndMessaging::USER_DEFAULT_SCREEN_DPI as DEFAULT_DPI},
};
// To simplify layout code (avoiding passing values through many functions), we provide an API
// which can set a contextual Dpi.
thread_local! {
static CONTEXT_DPI: std::cell::Cell<Dpi> = Dpi::default().into();
}
/// A DPI value.
#[derive(Clone, Copy, Debug)]
pub struct Dpi(u32);
impl Default for Dpi {
fn default() -> Self {
Dpi(DEFAULT_DPI)
}
}
impl Dpi {
/// Create a new Dpi.
pub fn new(dpi: u32) -> Self {
Dpi(dpi)
}
/// Get the Dpi for the given window.
pub fn for_window(hwnd: HWND) -> Self {
Dpi(unsafe { GetDpiForWindow(hwnd) })
}
/// Scale a pixel value according to this Dpi.
pub fn scale(&self, value: u32) -> u32 {
if self.0 == DEFAULT_DPI {
value
} else {
(value as u64 * self.0 as u64 / DEFAULT_DPI as u64) as u32
}
}
/// Call the given capture with this Dpi set as the contextual Dpi.
pub fn with_context<F, R>(&self, f: F) -> R
where
F: FnOnce() -> R,
{
let old = CONTEXT_DPI.replace(*self);
let ret = f();
CONTEXT_DPI.set(old);
ret
}
/// Scale a pixel value according to the contextual Dpi.
pub fn context_scale(value: u32) -> u32 {
CONTEXT_DPI.get().scale(value)
}
}

Просмотреть файл

@ -2,21 +2,101 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use windows_sys::Win32::{Foundation::S_OK, Graphics::Gdi, UI::Controls};
use super::Dpi;
use super::WideString;
use windows_sys::Win32::{
Foundation::{ERROR_SUCCESS, S_OK},
Graphics::Gdi,
System::Registry,
UI::Controls,
};
/// Windows font handle (`HFONT`).
pub struct Font(Gdi::HFONT);
/// The default font size to use.
///
/// `GetThemeSysFont` scales the font based on DPI and accessibility settings, however it only takes
/// those active at application startup into account (it won't scale correctly across monitors, nor
/// if the DPI of the current monitor changes). So we use a fixed size instead and scale that.
const DEFAULT_FONT_SIZE: i32 = -12;
impl Font {
/// The set of fonts to use.
pub struct Fonts {
pub normal: Font,
pub bold: Font,
}
/// The scale factor set by the windows 10 "make text bigger" accessibility setting.
#[derive(Clone, Copy, Debug)]
pub struct ScaleFactor(f32);
impl ScaleFactor {
/// Create a new scale factor.
///
/// The factor will be clamped to [1,2.25], to match the
/// [documentation](https://learn.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.uisettings.textscalefactor)
/// and avoid any surprises.
pub fn new(factor: f32) -> Self {
ScaleFactor(factor.clamp(1., 2.25))
}
/// Get the current scale factor setting from the registry.
pub fn from_registry() -> Self {
let key = WideString::new("SOFTWARE\\Microsoft\\Accessibility");
let value = WideString::new("TextScaleFactor");
let mut scale_factor: [u8; 4] = Default::default();
let mut size: u32 = std::mem::size_of_val(&scale_factor) as u32;
let mut reg_type: u32 = 0;
let result = unsafe {
Registry::RegGetValueW(
Registry::HKEY_CURRENT_USER,
key.pcwstr(),
value.pcwstr(),
Registry::RRF_RT_REG_DWORD,
&mut reg_type,
&mut scale_factor as *mut u8 as _,
&mut size,
)
};
let percent = if result == ERROR_SUCCESS {
if reg_type == Registry::REG_DWORD_BIG_ENDIAN {
u32::from_be_bytes(scale_factor)
} else {
u32::from_le_bytes(scale_factor)
}
} else {
100
};
Self::new(percent as f32 / 100.)
}
}
impl Fonts {
pub fn new(dpi: Dpi, scale_factor: ScaleFactor) -> Self {
let builder = FontBuilder { dpi, scale_factor };
Fonts {
normal: builder.caption(),
bold: builder.caption_bold().unwrap_or_else(|| builder.caption()),
}
}
}
pub struct FontBuilder {
dpi: Dpi,
scale_factor: ScaleFactor,
}
impl FontBuilder {
/// Get the system theme caption font.
///
/// Panics if the font cannot be retrieved.
pub fn caption() -> Self {
pub fn caption(&self) -> Font {
unsafe {
let mut font = std::mem::zeroed::<Gdi::LOGFONTW>();
success!(hresult
Controls::GetThemeSysFont(0, Controls::TMT_CAPTIONFONT as i32, &mut font)
);
font.lfHeight = DEFAULT_FONT_SIZE;
self.scale_font_height(&mut font.lfHeight);
font.lfWidth = 0;
Font(success!(pointer Gdi::CreateFontIndirectW(&font)))
}
}
@ -24,12 +104,15 @@ impl Font {
/// Get the system theme bold caption font.
///
/// Returns `None` if the font cannot be retrieved.
pub fn caption_bold() -> Option<Self> {
pub fn caption_bold(&self) -> Option<Font> {
unsafe {
let mut font = std::mem::zeroed::<Gdi::LOGFONTW>();
if Controls::GetThemeSysFont(0, Controls::TMT_CAPTIONFONT as i32, &mut font) != S_OK {
return None;
}
font.lfHeight = DEFAULT_FONT_SIZE;
self.scale_font_height(&mut font.lfHeight);
font.lfWidth = 0;
font.lfWeight = Gdi::FW_BOLD as i32;
let ptr = Gdi::CreateFontIndirectW(&font);
@ -39,8 +122,16 @@ impl Font {
Some(Font(ptr))
}
}
fn scale_font_height(&self, height: &mut i32) {
*height = (self.dpi.scale(height.abs() as u32) as f32 * self.scale_factor.0) as i32
* if height.is_negative() { -1 } else { 1 };
}
}
/// Windows font handle (`HFONT`).
pub struct Font(Gdi::HFONT);
impl std::ops::Deref for Font {
type Target = Gdi::HFONT;

Просмотреть файл

@ -6,7 +6,7 @@
use super::{
model::{self, Alignment, Element, ElementStyle, Margin},
ElementRef, WideString,
Dpi, ElementRef, WideString,
};
use crate::data::Property;
use std::collections::HashMap;
@ -29,6 +29,7 @@ pub(super) type ElementMapping = HashMap<ElementRef, HWND>;
/// disparate locations.
pub struct Layout<'a> {
elements: &'a ElementMapping,
dpi: Dpi,
sizes: HashMap<ElementRef, Size>,
last_positioned: Option<HWND>,
}
@ -49,9 +50,10 @@ const CHECKBOX_MARGIN: Margin = Margin {
};
impl<'a> Layout<'a> {
pub(super) fn new(elements: &'a ElementMapping) -> Self {
pub(super) fn new(elements: &'a ElementMapping, dpi: Dpi) -> Self {
Layout {
elements,
dpi,
sizes: Default::default(),
last_positioned: None,
}
@ -59,12 +61,14 @@ impl<'a> Layout<'a> {
/// Perform a layout of the element and all child elements.
pub fn layout(mut self, element: &Element, max_width: u32, max_height: u32) {
let max_size = Size {
width: max_width,
height: max_height,
};
self.resize(element, &max_size);
self.reposition(element, &Position::default(), &max_size);
self.dpi.clone().with_context(|| {
let max_size = Size {
width: max_width,
height: max_height,
};
self.resize(element, &max_size);
self.reposition(element, &Position::default(), &max_size);
})
}
fn resize(&mut self, element: &Element, max_size: &Size) -> Size {
@ -126,6 +130,7 @@ impl<'a> Layout<'a> {
content_size = Some(size);
}
VBox(model::VBox { items, spacing }) => {
let spacing = Dpi::context_scale(*spacing);
let mut height = 0;
let mut max_width = 0;
let mut remaining_size = inner_size.clone();
@ -160,6 +165,7 @@ impl<'a> Layout<'a> {
spacing,
affirmative_order: _,
}) => {
let spacing = Dpi::context_scale(*spacing);
let mut width = 0;
let mut max_height = 0;
let mut remaining_size = inner_size.clone();
@ -197,8 +203,8 @@ impl<'a> Layout<'a> {
Progress(model::Progress { .. }) => {
// Min size recommended by windows uxguide
content_size = Some(Size {
width: 160,
height: 15,
width: Dpi::context_scale(160),
height: Dpi::context_scale(15),
});
}
// We don't support sizing by textbox content yet (need to read from the HWND due to
@ -273,7 +279,7 @@ impl<'a> Layout<'a> {
let mut size = inner_size;
for item in items {
self.reposition(item, &position, &size);
let consumed = self.get_size(item).height + spacing;
let consumed = self.get_size(item).height + Dpi::context_scale(*spacing);
if item.style.vertical_alignment != Alignment::End {
position.top += consumed;
}
@ -290,7 +296,7 @@ impl<'a> Layout<'a> {
let mut size = inner_size;
for item in items {
self.reposition(item, &position, &inner_size);
let consumed = self.get_size(item).width + spacing;
let consumed = self.get_size(item).width + Dpi::context_scale(*spacing);
if item.style.horizontal_alignment != Alignment::End {
position.start += consumed;
}
@ -377,21 +383,21 @@ impl Size {
pub fn inner_size(&self, style: &ElementStyle) -> Self {
let mut ret = self.less_margin(&style.margin);
if let Some(width) = style.horizontal_size_request {
ret.width = width;
ret.width = Dpi::context_scale(width);
}
if let Some(height) = style.vertical_size_request {
ret.height = height;
ret.height = Dpi::context_scale(height);
}
ret
}
pub fn from_content_size(&mut self, style: &ElementStyle, content_size: &Self) {
if style.horizontal_size_request < Some(content_size.width)
if style.horizontal_size_request.map(Dpi::context_scale) < Some(content_size.width)
&& style.horizontal_alignment != Alignment::Fill
{
self.width = content_size.width;
}
if style.vertical_size_request < Some(content_size.height)
if style.vertical_size_request.map(Dpi::context_scale) < Some(content_size.height)
&& style.vertical_alignment != Alignment::Fill
{
self.height = content_size.height;
@ -400,15 +406,19 @@ impl Size {
pub fn plus_margin(&self, margin: &Margin) -> Self {
let mut ret = self.clone();
ret.width += margin.start + margin.end;
ret.height += margin.top + margin.bottom;
ret.width += Dpi::context_scale(margin.start + margin.end);
ret.height += Dpi::context_scale(margin.top + margin.bottom);
ret
}
pub fn less_margin(&self, margin: &Margin) -> Self {
let mut ret = self.clone();
ret.width = ret.width.saturating_sub(margin.start + margin.end);
ret.height = ret.height.saturating_sub(margin.top + margin.bottom);
ret.width = ret
.width
.saturating_sub(Dpi::context_scale(margin.start + margin.end));
ret.height = ret
.height
.saturating_sub(Dpi::context_scale(margin.top + margin.bottom));
ret
}
}
@ -423,15 +433,15 @@ impl Position {
#[allow(dead_code)]
pub fn plus_margin(&self, margin: &Margin) -> Self {
let mut ret = self.clone();
ret.start = ret.start.saturating_sub(margin.start);
ret.top = ret.top.saturating_sub(margin.top);
ret.start = ret.start.saturating_sub(Dpi::context_scale(margin.start));
ret.top = ret.top.saturating_sub(Dpi::context_scale(margin.top));
ret
}
pub fn less_margin(&self, margin: &Margin) -> Self {
let mut ret = self.clone();
ret.start += margin.start;
ret.top += margin.top;
ret.start += Dpi::context_scale(margin.start);
ret.top += Dpi::context_scale(margin.top);
ret
}
}

Просмотреть файл

@ -22,8 +22,9 @@
extern "C" {}
use super::model::{self, Application, Element, ElementStyle, TypedElement};
use crate::data::Property;
use font::Font;
use crate::data::{Property, Synchronized};
use dpi::Dpi;
use font::Fonts;
use once_cell::sync::Lazy;
use quit_token::QuitToken;
use std::cell::RefCell;
@ -61,6 +62,7 @@ macro_rules! success {
}};
}
mod dpi;
mod font;
mod gdi;
mod layout;
@ -318,6 +320,8 @@ impl CustomWindowClass for AppWindow {
let model = me.renderer.model();
match umsg {
win::WM_CREATE => {
me.renderer.set_dpi(Dpi::for_window(hwnd));
if let Some(close) = &model.close {
close.subscribe(move |&()| unsafe {
win::SendMessageW(hwnd, win::WM_CLOSE, 0, 0);
@ -358,8 +362,8 @@ impl CustomWindowClass for AppWindow {
}
win::WM_GETMINMAXINFO => {
let minmaxinfo = unsafe { (lparam as *mut win::MINMAXINFO).as_mut().unwrap() };
minmaxinfo.ptMinTrackSize.x = me.renderer.min_size.0.try_into().unwrap();
minmaxinfo.ptMinTrackSize.y = me.renderer.min_size.1.try_into().unwrap();
minmaxinfo.ptMinTrackSize.x = me.renderer.min_width().try_into().unwrap();
minmaxinfo.ptMinTrackSize.y = me.renderer.min_height().try_into().unwrap();
return Some(0);
}
win::WM_SIZE => {
@ -373,7 +377,31 @@ impl CustomWindowClass for AppWindow {
}
return Some(0);
}
win::WM_GETFONT => return Some(**me.renderer.font() as _),
win::WM_DPICHANGED => {
// When DPI changes, recompute the layout and move the window.
let rect: &RECT = unsafe { (lparam as *const RECT).as_ref() }
.expect("null RECT pointer in WM_DPICHANGED");
let dpi = loword(wparam as _) as u32;
let width = rect.right - rect.left;
let height = rect.bottom - rect.top;
me.renderer.set_dpi(Dpi::new(dpi));
// This wil send WM_SIZE, which will take care of the new layout.
unsafe {
win::SetWindowPos(
hwnd,
win::HWND_TOP,
rect.left,
rect.top,
width,
height,
win::SWP_NOZORDER | win::SWP_NOACTIVATE,
)
};
return Some(0);
}
win::WM_GETFONT => return Some(me.renderer.font()),
win::WM_COMMAND => {
let child = lparam as HWND;
let windows = me.renderer.windows.borrow();
@ -435,17 +463,20 @@ struct WindowRendererInner {
/// changes. Unfortunately the win32 API doesn't have any nice ways to automatically perform
/// layout.
pub model: RefCell<Pin<Box<model::Window>>>,
pub min_size: (u32, u32),
min_size: (u32, u32),
/// Mapping between model elements and windows.
///
/// Element references pertain to elements in `model`.
pub windows: RefCell<twoway::TwoWay<ElementRef, HWND>>,
pub font: Font,
pub bold_font: Font,
pub dpi: Synchronized<Dpi>,
pub fonts: Synchronized<Fonts>,
}
impl WindowRenderer {
pub fn new(module: HINSTANCE, model: model::Window, style: &model::ElementStyle) -> Self {
let dpi: Synchronized<Dpi> = Default::default();
let scale_factor = font::ScaleFactor::from_registry();
let fonts = dpi.mapped(move |dpi| Fonts::new(*dpi, scale_factor));
WindowRenderer {
inner: Rc::new(WindowRendererInner {
module,
@ -455,8 +486,8 @@ impl WindowRenderer {
style.vertical_size_request.unwrap_or(0),
),
windows: Default::default(),
font: Font::caption(),
bold_font: Font::caption_bold().unwrap_or_else(Font::caption),
dpi,
fonts,
}),
}
}
@ -470,9 +501,16 @@ impl WindowRenderer {
}
}
pub fn set_dpi(&self, dpi: Dpi) {
*self.dpi.borrow_mut() = dpi;
}
pub fn layout(&self, element: &Element, max_width: u32, max_height: u32) {
layout::Layout::new(self.inner.windows.borrow().forward())
.layout(element, max_width, max_height);
layout::Layout::new(
self.inner.windows.borrow().forward(),
*self.inner.dpi.borrow(),
)
.layout(element, max_width, max_height);
}
pub fn model(&self) -> std::cell::Ref<'_, model::Window> {
@ -483,8 +521,16 @@ impl WindowRenderer {
std::cell::RefMut::map(self.inner.model.borrow_mut(), |b| &mut **b)
}
pub fn font(&self) -> &Font {
&self.inner.font
pub fn font(&self) -> Gdi::HFONT {
*self.inner.fonts.borrow().normal
}
pub fn min_width(&self) -> u32 {
self.inner.dpi.borrow().scale(self.min_size.0)
}
pub fn min_height(&self) -> u32 {
self.inner.dpi.borrow().scale(self.min_size.1)
}
}
@ -558,7 +604,7 @@ impl<'a> WindowChildRenderer<'a> {
fn render_child(&mut self, element: &Element) {
if let Some(mut window) = self.render_element_type(&element.element_type) {
window.set_default_font(&self.renderer.font);
window.set_default_font(&self.renderer.fonts, |fonts| &fonts.normal);
// Store the element to handle mapping.
self.renderer
@ -641,7 +687,7 @@ impl<'a> WindowChildRenderer<'a> {
}
};
if *bold {
window.set_font(&self.renderer.bold_font);
window.set_font(&self.renderer.fonts, |fonts| &fonts.bold);
}
Some(window.generic())
}

Просмотреть файл

@ -4,8 +4,9 @@
//! Types and helpers relating to windows and window classes.
use super::Font;
use super::font::{Font, Fonts};
use super::WideString;
use crate::data::Synchronized;
use std::cell::RefCell;
use windows_sys::Win32::{
Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM},
@ -297,15 +298,26 @@ impl<W> Window<W> {
}
/// Set a window's font.
pub fn set_font(&mut self, font: &Font) {
unsafe { win::SendMessageW(self.handle, win::WM_SETFONT, **font as _, 1 as _) };
pub fn set_font(
&mut self,
fonts: &Synchronized<Fonts>,
font: impl Fn(&Fonts) -> &Font + 'static,
) {
let handle = self.handle;
fonts.map_with(move |fonts| unsafe {
win::SendMessageW(handle, win::WM_SETFONT, **font(fonts) as _, 1 as _);
});
self.font_set = true;
}
/// Set a window's font if not already set.
pub fn set_default_font(&mut self, font: &Font) {
pub fn set_default_font(
&mut self,
fonts: &Synchronized<Fonts>,
font: impl Fn(&Fonts) -> &Font + 'static,
) {
if !self.font_set {
self.set_font(font);
self.set_font(fonts, font);
}
}
}