diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f731e5ce..0aa15a27a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - On Windows added `MonitorIdExt::hmonitor` method - Impl `Clone` for `EventsLoopProxy` - `EventsLoop::get_primary_monitor()` on X11 will fallback to any available monitor if no primary is found +- On Windows added `EventsLoopExt::new_no_dpi_aware` method # Version 0.8.3 (2017-10-11) diff --git a/Cargo.toml b/Cargo.toml index d74816cf0c..bc89c09888 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,8 @@ core-graphics = "0.10" [target.'cfg(target_os = "windows")'.dependencies] winapi = "0.2" shell32-sys = "0.1" -user32-sys = "~0.1.2" +user32-sys = "0.2.0" +gdi32-sys = "0.2.0" kernel32-sys = "0.2" dwmapi-sys = "0.1" diff --git a/src/lib.rs b/src/lib.rs index d13a7e6497..a88e77594e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,6 +95,8 @@ extern crate shell32; #[cfg(target_os = "windows")] extern crate user32; #[cfg(target_os = "windows")] +extern crate gdi32; +#[cfg(target_os = "windows")] extern crate dwmapi; #[cfg(any(target_os = "macos", target_os = "ios"))] #[macro_use] diff --git a/src/os/windows.rs b/src/os/windows.rs index a7d36260b4..593023504d 100644 --- a/src/os/windows.rs +++ b/src/os/windows.rs @@ -2,11 +2,31 @@ use std::os::raw::c_void; use libc; +use EventsLoop; use MonitorId; use Window; use WindowBuilder; +use platform::EventsLoop as WindowsEventsLoop; use winapi; +/// Additional methods on `EventsLoop` that are specific to Windows. +pub trait EventsLoopExt { + /// By default, winit on Windows will attempt to enable process-wide DPI awareness. This may be + /// undesirable in some circumstances and can be avoided by creating events loop using this function. + fn new_no_dpi_aware() -> Self + where Self: Sized; +} + +impl EventsLoopExt for EventsLoop { + #[inline] + fn new_no_dpi_aware() -> Self { + EventsLoop { + events_loop: WindowsEventsLoop::with_dpi_awareness(false), + _marker: ::std::marker::PhantomData, + } + } +} + /// Additional methods on `Window` that are specific to Windows. pub trait WindowExt { /// Returns the native handle that is used by this window. diff --git a/src/platform/windows/dpi.rs b/src/platform/windows/dpi.rs new file mode 100644 index 0000000000..7e3adcc216 --- /dev/null +++ b/src/platform/windows/dpi.rs @@ -0,0 +1,134 @@ +#![allow(non_camel_case_types, non_snake_case)] +use std::mem; +use std::os::raw::c_void; +use std::sync::{Once, ONCE_INIT}; +use winapi; +use user32; +use gdi32; +use kernel32; + +type DPI_AWARENESS_CONTEXT = isize; + +const DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE: DPI_AWARENESS_CONTEXT = -3; +const DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2: DPI_AWARENESS_CONTEXT = -4; + +type SetProcessDPIAware = unsafe extern "system" fn () -> winapi::BOOL; +type SetProcessDpiAwareness = unsafe extern "system" fn (value: winapi::PROCESS_DPI_AWARENESS) -> winapi::HRESULT; +type SetProcessDpiAwarenessContext = unsafe extern "system" fn (value: DPI_AWARENESS_CONTEXT) -> winapi::BOOL; +type GetDpiForWindow = unsafe extern "system" fn (hwnd: winapi::HWND) -> winapi::UINT; +type GetDpiForMonitor = unsafe extern "system" fn (hmonitor: winapi::HMONITOR, dpi_type: winapi::MONITOR_DPI_TYPE, dpi_x: *mut winapi::UINT, dpi_y: *mut winapi::UINT) -> winapi::HRESULT; +type EnableNonClientDpiScaling = unsafe extern "system" fn (hwnd: winapi::HWND) -> winapi::BOOL; + +// Helper function to dynamically load function pointer. +// `library` and `function` must be zero-terminated. +fn get_function_impl(library: &str, function: &str) -> Option<*const c_void> { + unsafe { + // Library names we will use are ASCII so we can use the A version to avoid string conversion. + let module = kernel32::LoadLibraryA(library.as_ptr() as winapi::LPCSTR); + if module.is_null() { + return None; + } + + let function_ptr = kernel32::GetProcAddress(module, function.as_ptr() as winapi::LPCSTR); + if function_ptr.is_null() { + return None; + } + + Some(function_ptr) + } +} + +macro_rules! get_function { + ($lib:expr, $func:ident) => { + get_function_impl(concat!($lib, '\0'), concat!(stringify!($func), '\0')).map(|f| unsafe { mem::transmute::<*const _, $func>(f) }) + } +} + +lazy_static! { + static ref GET_DPI_FOR_WINDOW: Option = get_function!("user32.dll", GetDpiForWindow); + static ref GET_DPI_FOR_MONITOR: Option = get_function!("shcore.dll", GetDpiForMonitor); + static ref ENABLE_NON_CLIENT_DPI_SCALING: Option = get_function!("user32.dll", EnableNonClientDpiScaling); +} + +pub fn become_dpi_aware(enable: bool) { + if !enable { + return; + } + + static ENABLE_DPI_AWARENESS: Once = ONCE_INIT; + ENABLE_DPI_AWARENESS.call_once(|| { + unsafe { + if let Some(SetProcessDpiAwarenessContext) = get_function!("user32.dll", SetProcessDpiAwarenessContext) { + // We are on Windows 10 Anniversary Update (1607) or later. + if SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) == winapi::FALSE { + // V2 only works with Windows 10 Creators Update (1703). Try using the older V1 if we can't set V2. + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); + } + } else if let Some(SetProcessDpiAwareness) = get_function!("shcore.dll", SetProcessDpiAwareness) { + // We are on Windows 8.1 or later. + SetProcessDpiAwareness(winapi::Process_Per_Monitor_DPI_Aware); + } else if let Some(SetProcessDPIAware) = get_function!("user32.dll", SetProcessDPIAware) { + // We are on Vista or later. + SetProcessDPIAware(); + } + } + }); +} + +pub unsafe fn enable_non_client_dpi_scaling(hwnd: winapi::HWND) { + if let Some(EnableNonClientDpiScaling) = *ENABLE_NON_CLIENT_DPI_SCALING { + EnableNonClientDpiScaling(hwnd); + } +} + +pub unsafe fn get_monitor_dpi(hmonitor: winapi::HMONITOR) -> Option { + if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { + // We are on Windows 8.1 or later. + let mut dpi_x = 0; + let mut dpi_y = 0; + if GetDpiForMonitor(hmonitor, winapi::MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == winapi::S_OK { + // MSDN says that "the values of *dpiX and *dpiY are identical. You only need to record one of the values + // to determine the DPI and respond appropriately". + // + // https://msdn.microsoft.com/en-us/library/windows/desktop/dn280510(v=vs.85).aspx + return Some(dpi_x as u32) + } + } + + None +} + +pub unsafe fn get_window_dpi(hwnd: winapi::HWND, hdc: winapi::HDC) -> u32 { + if let Some(GetDpiForWindow) = *GET_DPI_FOR_WINDOW { + // We are on Windows 10 Anniversary Update (1607) or later. + match GetDpiForWindow(hwnd) { + 0 => 96, // 0 is returned if hwnd is invalid + dpi => dpi as u32, + } + } else if let Some(GetDpiForMonitor) = *GET_DPI_FOR_MONITOR { + // We are on Windows 8.1 or later. + let monitor = user32::MonitorFromWindow(hwnd, winapi::MONITOR_DEFAULTTONEAREST); + if monitor.is_null() { + return 96; + } + + let mut dpi_x = 0; + let mut dpi_y = 0; + if GetDpiForMonitor(monitor, winapi::MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) == winapi::S_OK { + dpi_x as u32 + } else { + 96 + } + } else { + // We are on Vista or later. + if user32::IsProcessDPIAware() != winapi::FALSE { + // If the process is DPI aware then scaling is not performed by OS and must be performed by the application. + // Therefore we return real DPI value. + gdi32::GetDeviceCaps(hdc, winapi::wingdi::LOGPIXELSX) as u32 + } else { + // If the process is DPI unaware then scaling is performed by OS and we must return 96 to prevent the application + // from scaling itself which would lead to double scaling. + 96 + } + } +} \ No newline at end of file diff --git a/src/platform/windows/events_loop.rs b/src/platform/windows/events_loop.rs index 7213bcdce1..f9dcae2a01 100644 --- a/src/platform/windows/events_loop.rs +++ b/src/platform/windows/events_loop.rs @@ -32,6 +32,7 @@ use user32; use winapi; use platform::platform::event; +use platform::platform::dpi; use platform::platform::Cursor; use platform::platform::WindowId; use platform::platform::DEVICE_ID; @@ -82,11 +83,17 @@ pub struct EventsLoop { // Variable that contains the block state of the win32 event loop thread during a WM_SIZE event. // The mutex's value is `true` when it's blocked, and should be set to false when it's done // blocking. That's done by the parent thread when it receives a Resized event. - win32_block_loop: Arc<(Mutex, Condvar)> + win32_block_loop: Arc<(Mutex, Condvar)>, + // Whether to enable process-global DPI awareness. + pub(super) dpi_aware: bool, } impl EventsLoop { pub fn new() -> EventsLoop { + Self::with_dpi_awareness(true) + } + + pub fn with_dpi_awareness(dpi_aware: bool) -> EventsLoop { // The main events transfer channel. let (tx, rx) = mpsc::channel(); let win32_block_loop = Arc::new((Mutex::new(false), Condvar::new())); @@ -149,7 +156,8 @@ impl EventsLoop { EventsLoop { thread_id: unsafe { kernel32::GetThreadId(thread.as_raw_handle()) }, receiver: rx, - win32_block_loop + win32_block_loop, + dpi_aware } } @@ -315,6 +323,11 @@ pub unsafe extern "system" fn callback(window: winapi::HWND, msg: winapi::UINT, -> winapi::LRESULT { match msg { + winapi::WM_NCCREATE => { + dpi::enable_non_client_dpi_scaling(window); + user32::DefWindowProcW(window, msg, wparam, lparam) + }, + winapi::WM_CLOSE => { use events::WindowEvent::Closed; send_event(Event::WindowEvent { @@ -769,6 +782,29 @@ pub unsafe extern "system" fn callback(window: winapi::HWND, msg: winapi::UINT, 0 }, + // Only sent on Windows 8.1 or newer. On Windows 7 and older user has to log out to change DPI, + // therefore all applications are closed while DPI is changing. + winapi::WM_DPICHANGED => { + use events::WindowEvent::HiDPIFactorChanged; + + // This message actually provides two DPI values - x and y. However MSDN says that "you only need to + // use either the X-axis or the Y-axis value when scaling your application since they are the same". + // + // https://msdn.microsoft.com/en-us/library/windows/desktop/dn312083(v=vs.85).aspx + let dpi_x = winapi::LOWORD(wparam as u32); + + // Resize window to the size suggested by Windows. + let rect = *mem::transmute::(lparam); + user32::SetWindowPos(window, ptr::null_mut(), rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, winapi::SWP_NOZORDER | winapi::SWP_NOACTIVATE); + + send_event(Event::WindowEvent { + window_id: SuperWindowId(WindowId(window)), + event: HiDPIFactorChanged(dpi_x as f32/96.0) // 96 is the standard DPI value used when there is no scaling + }); + + 0 + }, + _ => { user32::DefWindowProcW(window, msg, wparam, lparam) } diff --git a/src/platform/windows/mod.rs b/src/platform/windows/mod.rs index e0b4fdb818..741ee28145 100644 --- a/src/platform/windows/mod.rs +++ b/src/platform/windows/mod.rs @@ -31,3 +31,4 @@ mod event; mod events_loop; mod monitor; mod window; +mod dpi; diff --git a/src/platform/windows/monitor.rs b/src/platform/windows/monitor.rs index fb17be8006..2e097dd023 100644 --- a/src/platform/windows/monitor.rs +++ b/src/platform/windows/monitor.rs @@ -5,6 +5,7 @@ use std::collections::VecDeque; use std::{mem, ptr}; use super::EventsLoop; +use super::dpi; /// Win32 implementation of the main `MonitorId` object. #[derive(Clone)] @@ -62,6 +63,8 @@ unsafe extern "system" fn monitor_enum_proc(hmonitor: winapi::HMONITOR, _: winap return winapi::TRUE; } + let dpi = dpi::get_monitor_dpi(hmonitor).unwrap_or(96); + (*monitors).push_back(MonitorId { adapter_name: monitor_info.szDevice, hmonitor: HMonitor(hmonitor), @@ -69,7 +72,7 @@ unsafe extern "system" fn monitor_enum_proc(hmonitor: winapi::HMONITOR, _: winap primary: monitor_info.dwFlags & winapi::MONITORINFOF_PRIMARY != 0, position, dimensions, - hidpi_factor: 1.0, + hidpi_factor: dpi as f32/96.0, }); // TRUE means continue enumeration. @@ -79,6 +82,9 @@ unsafe extern "system" fn monitor_enum_proc(hmonitor: winapi::HMONITOR, _: winap impl EventsLoop { pub fn get_available_monitors(&self) -> VecDeque { unsafe { + // We need to enable DPI awareness to get correct resolution and DPI of each monitor. + dpi::become_dpi_aware(self.dpi_aware); + let mut result: VecDeque = VecDeque::new(); user32::EnumDisplayMonitors(ptr::null_mut(), ptr::null_mut(), Some(monitor_enum_proc), &mut result as *mut _ as winapi::LPARAM); result @@ -86,6 +92,9 @@ impl EventsLoop { } pub fn get_primary_monitor(&self) -> MonitorId { + // We need to enable DPI awareness to get correct resolution and DPI of the primary monitor. + dpi::become_dpi_aware(self.dpi_aware); + // we simply get all available monitors and return the one with the `MONITORINFOF_PRIMARY` flag // TODO: it is possible to query the win32 API for the primary monitor, this should be done // instead diff --git a/src/platform/windows/window.rs b/src/platform/windows/window.rs index d3d7592124..f6766ceddb 100644 --- a/src/platform/windows/window.rs +++ b/src/platform/windows/window.rs @@ -11,6 +11,7 @@ use std::sync::Mutex; use std::sync::mpsc::channel; use platform::platform::events_loop; +use platform::platform::dpi; use platform::platform::EventsLoop; use platform::platform::PlatformSpecificWindowBuilderAttributes; use platform::platform::MonitorId; @@ -48,6 +49,8 @@ impl Window { let (tx, rx) = channel(); + dpi::become_dpi_aware(events_loop.dpi_aware); + events_loop.execute_in_thread(move |inserter| { // We dispatch an `init` function because of code style. let win = unsafe { init(w_attr.take().unwrap(), pl_attr.take().unwrap(), inserter) }; @@ -251,7 +254,9 @@ impl Window { #[inline] pub fn hidpi_factor(&self) -> f32 { - 1.0 + unsafe { + dpi::get_window_dpi(self.window.0, self.window.1) as f32/96.0 + } } pub fn set_cursor_position(&self, x: i32, y: i32) -> Result<(), ()> { @@ -320,6 +325,7 @@ impl Drop for WindowWrapper { unsafe fn init(window: WindowAttributes, pl_attribs: PlatformSpecificWindowBuilderAttributes, inserter: events_loop::Inserter) -> Result { + let title = OsStr::new(&window.title).encode_wide().chain(Some(0).into_iter()) .collect::>();