druid_shell/backend/windows/
util.rs

1// Copyright 2017 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Various utilities for working with windows. Includes utilities for converting between Windows
16//! and Rust types, including strings.
17//! Also includes some code to dynamically load functions at runtime. This is needed for functions
18//! which are only supported on certain versions of windows.
19
20use std::ffi::{CString, OsStr, OsString};
21use std::mem;
22use std::os::windows::ffi::{OsStrExt, OsStringExt};
23use std::ptr;
24use std::slice;
25
26use once_cell::sync::Lazy;
27use winapi::ctypes::c_void;
28use winapi::shared::dxgi::IDXGIDevice;
29use winapi::shared::guiddef::REFIID;
30use winapi::shared::minwindef::{BOOL, HMODULE, UINT};
31use winapi::shared::ntdef::{HRESULT, LPWSTR};
32use winapi::shared::windef::{HMONITOR, HWND, RECT};
33use winapi::shared::winerror::SUCCEEDED;
34use winapi::um::fileapi::{CreateFileA, GetFileType, OPEN_EXISTING};
35use winapi::um::handleapi::INVALID_HANDLE_VALUE;
36use winapi::um::libloaderapi::{GetModuleHandleW, GetProcAddress, LoadLibraryW};
37use winapi::um::processenv::{GetStdHandle, SetStdHandle};
38use winapi::um::shellscalingapi::{MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS};
39use winapi::um::winbase::{FILE_TYPE_UNKNOWN, STD_ERROR_HANDLE, STD_OUTPUT_HANDLE};
40use winapi::um::wincon::{AttachConsole, ATTACH_PARENT_PROCESS};
41use winapi::um::winnt::{FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE};
42
43use crate::kurbo::Rect;
44use crate::region::Region;
45use crate::scale::{Scalable, Scale};
46
47use super::error::Error;
48
49pub fn as_result(hr: HRESULT) -> Result<(), Error> {
50    if SUCCEEDED(hr) {
51        Ok(())
52    } else {
53        Err(Error::Hr(hr))
54    }
55}
56
57impl From<HRESULT> for Error {
58    fn from(hr: HRESULT) -> Error {
59        Error::Hr(hr)
60    }
61}
62
63pub trait ToWide {
64    fn to_wide_sized(&self) -> Vec<u16>;
65    fn to_wide(&self) -> Vec<u16>;
66}
67
68impl<T> ToWide for T
69where
70    T: AsRef<OsStr>,
71{
72    fn to_wide_sized(&self) -> Vec<u16> {
73        self.as_ref().encode_wide().collect()
74    }
75    fn to_wide(&self) -> Vec<u16> {
76        self.as_ref().encode_wide().chain(Some(0)).collect()
77    }
78}
79
80pub trait FromWide {
81    fn to_u16_slice(&self) -> &[u16];
82
83    fn to_os_string(&self) -> OsString {
84        OsStringExt::from_wide(self.to_u16_slice())
85    }
86
87    fn to_string(&self) -> Option<String> {
88        String::from_utf16(self.to_u16_slice()).ok()
89    }
90}
91
92impl FromWide for LPWSTR {
93    fn to_u16_slice(&self) -> &[u16] {
94        unsafe {
95            let mut len = 0;
96            while *self.offset(len) != 0 {
97                len += 1;
98            }
99            slice::from_raw_parts(*self, len as usize)
100        }
101    }
102}
103
104impl FromWide for [u16] {
105    fn to_u16_slice(&self) -> &[u16] {
106        self
107    }
108}
109
110/// Converts a `Rect` to a winapi `RECT`.
111#[inline]
112pub(crate) fn rect_to_recti(rect: Rect) -> RECT {
113    RECT {
114        left: rect.x0 as i32,
115        top: rect.y0 as i32,
116        right: rect.x1 as i32,
117        bottom: rect.y1 as i32,
118    }
119}
120
121/// Converts a winapi `RECT` to a `Rect`.
122#[inline]
123pub(crate) fn recti_to_rect(rect: RECT) -> Rect {
124    Rect::new(
125        rect.left as f64,
126        rect.top as f64,
127        rect.right as f64,
128        rect.bottom as f64,
129    )
130}
131
132/// Converts a `Region` into a vec of winapi `RECT`, with a scaling applied.
133/// If necessary, the rectangles are rounded to the nearest pixel border.
134pub(crate) fn region_to_rectis(region: &Region, scale: Scale) -> Vec<RECT> {
135    region
136        .rects()
137        .iter()
138        .map(|r| rect_to_recti(r.to_px(scale).round()))
139        .collect()
140}
141
142// Types for functions we want to load, which are only supported on newer windows versions
143// from user32.dll
144type GetDpiForSystem = unsafe extern "system" fn() -> UINT;
145type GetDpiForWindow = unsafe extern "system" fn(HWND) -> UINT;
146type SetProcessDpiAwarenessContext =
147    unsafe extern "system" fn(winapi::shared::windef::DPI_AWARENESS_CONTEXT) -> BOOL;
148type GetSystemMetricsForDpi =
149    unsafe extern "system" fn(winapi::ctypes::c_int, UINT) -> winapi::ctypes::c_int;
150// from shcore.dll
151type GetDpiForMonitor = unsafe extern "system" fn(HMONITOR, MONITOR_DPI_TYPE, *mut UINT, *mut UINT);
152type SetProcessDpiAwareness = unsafe extern "system" fn(PROCESS_DPI_AWARENESS) -> HRESULT;
153type DCompositionCreateDevice = unsafe extern "system" fn(
154    dxgiDevice: *const IDXGIDevice,
155    iid: REFIID,
156    dcompositionDevice: *mut *mut c_void,
157) -> HRESULT;
158
159#[allow(non_snake_case)] // For member fields
160pub struct OptionalFunctions {
161    pub GetDpiForSystem: Option<GetDpiForSystem>,
162    pub GetDpiForWindow: Option<GetDpiForWindow>,
163    pub SetProcessDpiAwarenessContext: Option<SetProcessDpiAwarenessContext>,
164    pub GetDpiForMonitor: Option<GetDpiForMonitor>,
165    pub SetProcessDpiAwareness: Option<SetProcessDpiAwareness>,
166    pub GetSystemMetricsForDpi: Option<GetSystemMetricsForDpi>,
167    pub DCompositionCreateDevice: Option<DCompositionCreateDevice>,
168}
169
170#[allow(non_snake_case)] // For local variables
171fn load_optional_functions() -> OptionalFunctions {
172    // Tries to load $function from $lib. $function should be one of the types defined just before
173    // `load_optional_functions`. This sets the corresponding local field to `Some(function pointer)`
174    // if it manages to load the function.
175    macro_rules! load_function {
176        ($lib: expr, $function: ident, $min_windows_version: expr) => {{
177            let name = stringify!($function);
178
179            // The `unwrap` is fine, because we only call this macro for winapi functions, which
180            // have simple ascii-only names
181            let cstr = CString::new(name).unwrap();
182
183            let function_ptr = unsafe { GetProcAddress($lib, cstr.as_ptr()) };
184
185            if function_ptr.is_null() {
186                tracing::info!(
187                    "Could not load `{}`. Windows {} or later is needed",
188                    name,
189                    $min_windows_version
190                );
191            } else {
192                let function = unsafe { mem::transmute::<_, $function>(function_ptr) };
193                $function = Some(function);
194            }
195        }};
196    }
197
198    fn load_library(name: &str) -> HMODULE {
199        let encoded_name = name.to_wide();
200
201        // If we already have loaded the library (somewhere else in the process) we don't need to
202        // call LoadLibrary again
203        let library = unsafe { GetModuleHandleW(encoded_name.as_ptr()) };
204        if !library.is_null() {
205            return library;
206        }
207
208        unsafe { LoadLibraryW(encoded_name.as_ptr()) }
209    }
210
211    let shcore = load_library("shcore.dll");
212    let user32 = load_library("user32.dll");
213    let dcomp = load_library("dcomp.dll");
214
215    let mut GetDpiForSystem = None;
216    let mut GetDpiForMonitor = None;
217    let mut GetDpiForWindow = None;
218    let mut SetProcessDpiAwarenessContext = None;
219    let mut SetProcessDpiAwareness = None;
220    let mut GetSystemMetricsForDpi = None;
221    let mut DCompositionCreateDevice = None;
222
223    if shcore.is_null() {
224        tracing::info!("No shcore.dll");
225    } else {
226        load_function!(shcore, SetProcessDpiAwareness, "8.1");
227        load_function!(shcore, GetDpiForMonitor, "8.1");
228    }
229
230    if user32.is_null() {
231        tracing::info!("No user32.dll");
232    } else {
233        load_function!(user32, GetDpiForSystem, "10");
234        load_function!(user32, GetDpiForWindow, "10");
235        load_function!(user32, SetProcessDpiAwarenessContext, "10");
236        load_function!(user32, GetSystemMetricsForDpi, "10");
237    }
238
239    if dcomp.is_null() {
240        tracing::info!("No dcomp.dll");
241    } else {
242        load_function!(dcomp, DCompositionCreateDevice, "8.1");
243    }
244
245    OptionalFunctions {
246        GetDpiForSystem,
247        GetDpiForWindow,
248        SetProcessDpiAwarenessContext,
249        GetDpiForMonitor,
250        SetProcessDpiAwareness,
251        GetSystemMetricsForDpi,
252        DCompositionCreateDevice,
253    }
254}
255
256pub static OPTIONAL_FUNCTIONS: Lazy<OptionalFunctions> = Lazy::new(load_optional_functions);
257
258pub(crate) const CLASS_NAME: &str = "druid";
259
260/// Convenience macro for defining accelerator tables.
261#[macro_export]
262macro_rules! accel {
263    ( $( $fVirt:expr, $key:expr, $cmd:expr, )* ) => {
264        [
265            $(
266                ACCEL { fVirt: $fVirt | FVIRTKEY, key: $key as WORD, cmd: $cmd as WORD },
267            )*
268        ]
269    }
270}
271
272/// Attach the process to the console of the parent process. This allows xi-win to
273/// correctly print to a console when run from powershell or cmd.
274/// If no console is available, allocate a new console.
275pub(crate) fn attach_console() {
276    unsafe {
277        let stdout = GetStdHandle(STD_OUTPUT_HANDLE);
278        if stdout != INVALID_HANDLE_VALUE && GetFileType(stdout) != FILE_TYPE_UNKNOWN {
279            // We already have a perfectly valid stdout and must not attach.
280            // This happens, for example in MingW consoles like git bash.
281            return;
282        }
283
284        if AttachConsole(ATTACH_PARENT_PROCESS) > 0 {
285            let name = CString::new("CONOUT$").unwrap();
286            let chnd = CreateFileA(
287                name.as_ptr(),
288                GENERIC_READ | GENERIC_WRITE,
289                FILE_SHARE_WRITE,
290                ptr::null_mut(),
291                OPEN_EXISTING,
292                0,
293                ptr::null_mut(),
294            );
295
296            if chnd == INVALID_HANDLE_VALUE {
297                // CreateFileA failed.
298                return;
299            }
300
301            SetStdHandle(STD_OUTPUT_HANDLE, chnd);
302            SetStdHandle(STD_ERROR_HANDLE, chnd);
303        }
304    }
305}