Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement high-DPI support on Windows #332

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
20 changes: 20 additions & 0 deletions src/os/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
134 changes: 134 additions & 0 deletions src/platform/windows/dpi.rs
Original file line number Diff line number Diff line change
@@ -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<GetDpiForWindow> = get_function!("user32.dll", GetDpiForWindow);
static ref GET_DPI_FOR_MONITOR: Option<GetDpiForMonitor> = get_function!("shcore.dll", GetDpiForMonitor);
static ref ENABLE_NON_CLIENT_DPI_SCALING: Option<EnableNonClientDpiScaling> = 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<u32> {
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
}
}
}
40 changes: 38 additions & 2 deletions src/platform/windows/events_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<bool>, Condvar)>
win32_block_loop: Arc<(Mutex<bool>, 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()));
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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::<winapi::LPARAM, *const winapi::RECT>(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)
}
Expand Down
1 change: 1 addition & 0 deletions src/platform/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ mod event;
mod events_loop;
mod monitor;
mod window;
mod dpi;
11 changes: 10 additions & 1 deletion src/platform/windows/monitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -62,14 +63,16 @@ 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),
monitor_name: wchar_as_string(&monitor_info.szDevice),
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.
Expand All @@ -79,13 +82,19 @@ unsafe extern "system" fn monitor_enum_proc(hmonitor: winapi::HMONITOR, _: winap
impl EventsLoop {
pub fn get_available_monitors(&self) -> VecDeque<MonitorId> {
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<MonitorId> = VecDeque::new();
user32::EnumDisplayMonitors(ptr::null_mut(), ptr::null_mut(), Some(monitor_enum_proc), &mut result as *mut _ as winapi::LPARAM);
result
}
}

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
Expand Down
8 changes: 7 additions & 1 deletion src/platform/windows/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) };
Expand Down Expand Up @@ -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<(), ()> {
Expand Down Expand Up @@ -320,6 +325,7 @@ impl Drop for WindowWrapper {

unsafe fn init(window: WindowAttributes, pl_attribs: PlatformSpecificWindowBuilderAttributes,
inserter: events_loop::Inserter) -> Result<Window, CreationError> {

let title = OsStr::new(&window.title).encode_wide().chain(Some(0).into_iter())
.collect::<Vec<_>>();

Expand Down