azul_winit/
dpi.rs

1//! UI scaling is important, so read the docs for this module if you don't want to be confused.
2//!
3//! ## Why should I care about UI scaling?
4//!
5//! Modern computer screens don't have a consistent relationship between resolution and size.
6//! 1920x1080 is a common resolution for both desktop and mobile screens, despite mobile screens
7//! normally being less than a quarter the size of their desktop counterparts. What's more, neither
8//! desktop nor mobile screens are consistent resolutions within their own size classes - common
9//! mobile screens range from below 720p to above 1440p, and desktop screens range from 720p to 5K
10//! and beyond.
11//!
12//! Given that, it's a mistake to assume that 2D content will only be displayed on screens with
13//! a consistent pixel density. If you were to render a 96-pixel-square image on a 1080p screen,
14//! then render the same image on a similarly-sized 4K screen, the 4K rendition would only take up
15//! about a quarter of the physical space as it did on the 1080p screen. That issue is especially
16//! problematic with text rendering, where quarter-sized text becomes a significant legibility
17//! problem.
18//!
19//! Failure to account for the scale factor can create a significantly degraded user experience.
20//! Most notably, it can make users feel like they have bad eyesight, which will potentially cause
21//! them to think about growing elderly, resulting in them having an existential crisis. Once users
22//! enter that state, they will no longer be focused on your application.
23//!
24//! ## How should I handle it?
25//!
26//! The solution to this problem is to account for the device's *scale factor*. The scale factor is
27//! the factor UI elements should be scaled by to be consistent with the rest of the user's system -
28//! for example, a button that's normally 50 pixels across would be 100 pixels across on a device
29//! with a scale factor of `2.0`, or 75 pixels across with a scale factor of `1.5`.
30//!
31//! Many UI systems, such as CSS, expose DPI-dependent units like [points] or [picas]. That's
32//! usually a mistake, since there's no consistent mapping between the scale factor and the screen's
33//! actual DPI. Unless you're printing to a physical medium, you should work in scaled pixels rather
34//! than any DPI-dependent units.
35//!
36//! ### Position and Size types
37//!
38//! Winit's `Physical(Position|Size)` types correspond with the actual pixels on the device, and the
39//! `Logical(Position|Size)` types correspond to the physical pixels divided by the scale factor.
40//! All of Winit's functions return physical types, but can take either logical or physical
41//! coordinates as input, allowing you to use the most convenient coordinate system for your
42//! particular application.
43//!
44//! Winit's position and size types types are generic over their exact pixel type, `P`, to allow the
45//! API to have integer precision where appropriate (e.g. most window manipulation functions) and
46//! floating precision when necessary (e.g. logical sizes for fractional scale factors and touch
47//! input). If `P` is a floating-point type, please do not cast the values with `as {int}`. Doing so
48//! will truncate the fractional part of the float, rather than properly round to the nearest
49//! integer. Use the provided `cast` function or `From`/`Into` conversions, which handle the
50//! rounding properly. Note that precision loss will still occur when rounding from a float to an
51//! int, although rounding lessens the problem.
52//!
53//! ### Events
54//!
55//! Winit will dispatch a [`ScaleFactorChanged`](crate::event::WindowEvent::ScaleFactorChanged)
56//! event whenever a window's scale factor has changed. This can happen if the user drags their
57//! window from a standard-resolution monitor to a high-DPI monitor, or if the user changes their
58//! DPI settings. This gives you a chance to rescale your application's UI elements and adjust how
59//! the platform changes the window's size to reflect the new scale factor. If a window hasn't
60//! received a [`ScaleFactorChanged`](crate::event::WindowEvent::ScaleFactorChanged) event,
61//! then its scale factor is `1.0`.
62//!
63//! ## How is the scale factor calculated?
64//!
65//! Scale factor is calculated differently on different platforms:
66//!
67//! - **Windows:** On Windows 8 and 10, per-monitor scaling is readily configured by users from the
68//!   display settings. While users are free to select any option they want, they're only given a
69//!   selection of "nice" scale factors, i.e. 1.0, 1.25, 1.5... on Windows 7, the scale factor is
70//!   global and changing it requires logging out. See [this article][windows_1] for technical
71//!   details.
72//! - **macOS:** Recent versions of macOS allow the user to change the scaling factor for certain
73//!   displays. When this is available, the user may pick a per-monitor scaling factor from a set
74//!   of pre-defined settings. All "retina displays" have a scaling factor above 1.0 by default but
75//!   the specific value varies across devices.
76//! - **X11:** Many man-hours have been spent trying to figure out how to handle DPI in X11. Winit
77//!   currently uses a three-pronged approach:
78//!   + Use the value in the `WINIT_X11_SCALE_FACTOR` environment variable, if present.
79//!   + If not present, use the value set in `Xft.dpi` in Xresources.
80//!   + Otherwise, calcuate the scale factor based on the millimeter monitor dimensions provided by XRandR.
81//!
82//!   If `WINIT_X11_SCALE_FACTOR` is set to `randr`, it'll ignore the `Xft.dpi` field and use the
83//!   XRandR scaling method. Generally speaking, you should try to configure the standard system
84//!   variables to do what you want before resorting to `WINIT_X11_SCALE_FACTOR`.
85//! - **Wayland:** On Wayland, scale factors are set per-screen by the server, and are always
86//!   integers (most often 1 or 2).
87//! - **iOS:** Scale factors are set by Apple to the value that best suits the device, and range
88//!   from `1.0` to `3.0`. See [this article][apple_1] and [this article][apple_2] for more
89//!   information.
90//! - **Android:** Scale factors are set by the manufacturer to the value that best suits the
91//!   device, and range from `1.0` to `4.0`. See [this article][android_1] for more information.
92//! - **Web:** The scale factor is the ratio between CSS pixels and the physical device pixels.
93//!   In other words, it is the value of [`window.devicePixelRatio`][web_1]. It is affected by
94//!   both the screen scaling and the browser zoom level and can go below `1.0`.
95//!
96//! [points]: https://en.wikipedia.org/wiki/Point_(typography)
97//! [picas]: https://en.wikipedia.org/wiki/Pica_(typography)
98//! [windows_1]: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows
99//! [apple_1]: https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html
100//! [apple_2]: https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/image-size-and-resolution/
101//! [android_1]: https://developer.android.com/training/multiscreen/screendensities
102//! [web_1]: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio
103
104pub trait Pixel: Copy + Into<f64> {
105    fn from_f64(f: f64) -> Self;
106    fn cast<P: Pixel>(self) -> P {
107        P::from_f64(self.into())
108    }
109}
110
111impl Pixel for u8 {
112    fn from_f64(f: f64) -> Self {
113        f.round() as u8
114    }
115}
116impl Pixel for u16 {
117    fn from_f64(f: f64) -> Self {
118        f.round() as u16
119    }
120}
121impl Pixel for u32 {
122    fn from_f64(f: f64) -> Self {
123        f.round() as u32
124    }
125}
126impl Pixel for i8 {
127    fn from_f64(f: f64) -> Self {
128        f.round() as i8
129    }
130}
131impl Pixel for i16 {
132    fn from_f64(f: f64) -> Self {
133        f.round() as i16
134    }
135}
136impl Pixel for i32 {
137    fn from_f64(f: f64) -> Self {
138        f.round() as i32
139    }
140}
141impl Pixel for f32 {
142    fn from_f64(f: f64) -> Self {
143        f as f32
144    }
145}
146impl Pixel for f64 {
147    fn from_f64(f: f64) -> Self {
148        f
149    }
150}
151
152/// Checks that the scale factor is a normal positive `f64`.
153///
154/// All functions that take a scale factor assert that this will return `true`. If you're sourcing scale factors from
155/// anywhere other than winit, it's recommended to validate them using this function before passing them to winit;
156/// otherwise, you risk panics.
157#[inline]
158pub fn validate_scale_factor(scale_factor: f64) -> bool {
159    scale_factor.is_sign_positive() && scale_factor.is_normal()
160}
161
162/// A position represented in logical pixels.
163///
164/// The position is stored as floats, so please be careful. Casting floats to integers truncates the
165/// fractional part, which can cause noticable issues. To help with that, an `Into<(i32, i32)>`
166/// implementation is provided which does the rounding for you.
167#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)]
168#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
169pub struct LogicalPosition<P> {
170    pub x: P,
171    pub y: P,
172}
173
174impl<P> LogicalPosition<P> {
175    #[inline]
176    pub const fn new(x: P, y: P) -> Self {
177        LogicalPosition { x, y }
178    }
179}
180
181impl<P: Pixel> LogicalPosition<P> {
182    #[inline]
183    pub fn from_physical<T: Into<PhysicalPosition<X>>, X: Pixel>(
184        physical: T,
185        scale_factor: f64,
186    ) -> Self {
187        physical.into().to_logical(scale_factor)
188    }
189
190    #[inline]
191    pub fn to_physical<X: Pixel>(&self, scale_factor: f64) -> PhysicalPosition<X> {
192        assert!(validate_scale_factor(scale_factor));
193        let x = self.x.into() * scale_factor;
194        let y = self.y.into() * scale_factor;
195        PhysicalPosition::new(x, y).cast()
196    }
197
198    #[inline]
199    pub fn cast<X: Pixel>(&self) -> LogicalPosition<X> {
200        LogicalPosition {
201            x: self.x.cast(),
202            y: self.y.cast(),
203        }
204    }
205}
206
207impl<P: Pixel, X: Pixel> From<(X, X)> for LogicalPosition<P> {
208    fn from((x, y): (X, X)) -> LogicalPosition<P> {
209        LogicalPosition::new(x.cast(), y.cast())
210    }
211}
212
213impl<P: Pixel, X: Pixel> Into<(X, X)> for LogicalPosition<P> {
214    fn into(self: Self) -> (X, X) {
215        (self.x.cast(), self.y.cast())
216    }
217}
218
219impl<P: Pixel, X: Pixel> From<[X; 2]> for LogicalPosition<P> {
220    fn from([x, y]: [X; 2]) -> LogicalPosition<P> {
221        LogicalPosition::new(x.cast(), y.cast())
222    }
223}
224
225impl<P: Pixel, X: Pixel> Into<[X; 2]> for LogicalPosition<P> {
226    fn into(self: Self) -> [X; 2] {
227        [self.x.cast(), self.y.cast()]
228    }
229}
230
231#[cfg(feature = "mint")]
232impl<P: Pixel> From<mint::Point2<P>> for LogicalPosition<P> {
233    fn from(mint: mint::Point2<P>) -> Self {
234        Self::new(mint.x, mint.y)
235    }
236}
237
238#[cfg(feature = "mint")]
239impl<P: Pixel> From<LogicalPosition<P>> for mint::Point2<P> {
240    fn from(winit: LogicalPosition<P>) -> Self {
241        mint::Point2 {
242            x: winit.x,
243            y: winit.y,
244        }
245    }
246}
247
248/// A position represented in physical pixels.
249#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)]
250#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
251pub struct PhysicalPosition<P> {
252    pub x: P,
253    pub y: P,
254}
255
256impl<P> PhysicalPosition<P> {
257    #[inline]
258    pub const fn new(x: P, y: P) -> Self {
259        PhysicalPosition { x, y }
260    }
261}
262
263impl<P: Pixel> PhysicalPosition<P> {
264    #[inline]
265    pub fn from_logical<T: Into<LogicalPosition<X>>, X: Pixel>(
266        logical: T,
267        scale_factor: f64,
268    ) -> Self {
269        logical.into().to_physical(scale_factor)
270    }
271
272    #[inline]
273    pub fn to_logical<X: Pixel>(&self, scale_factor: f64) -> LogicalPosition<X> {
274        assert!(validate_scale_factor(scale_factor));
275        let x = self.x.into() / scale_factor;
276        let y = self.y.into() / scale_factor;
277        LogicalPosition::new(x, y).cast()
278    }
279
280    #[inline]
281    pub fn cast<X: Pixel>(&self) -> PhysicalPosition<X> {
282        PhysicalPosition {
283            x: self.x.cast(),
284            y: self.y.cast(),
285        }
286    }
287}
288
289impl<P: Pixel, X: Pixel> From<(X, X)> for PhysicalPosition<P> {
290    fn from((x, y): (X, X)) -> PhysicalPosition<P> {
291        PhysicalPosition::new(x.cast(), y.cast())
292    }
293}
294
295impl<P: Pixel, X: Pixel> Into<(X, X)> for PhysicalPosition<P> {
296    fn into(self: Self) -> (X, X) {
297        (self.x.cast(), self.y.cast())
298    }
299}
300
301impl<P: Pixel, X: Pixel> From<[X; 2]> for PhysicalPosition<P> {
302    fn from([x, y]: [X; 2]) -> PhysicalPosition<P> {
303        PhysicalPosition::new(x.cast(), y.cast())
304    }
305}
306
307impl<P: Pixel, X: Pixel> Into<[X; 2]> for PhysicalPosition<P> {
308    fn into(self: Self) -> [X; 2] {
309        [self.x.cast(), self.y.cast()]
310    }
311}
312
313#[cfg(feature = "mint")]
314impl<P: Pixel> From<mint::Point2<P>> for PhysicalPosition<P> {
315    fn from(mint: mint::Point2<P>) -> Self {
316        Self::new(mint.x, mint.y)
317    }
318}
319
320#[cfg(feature = "mint")]
321impl<P: Pixel> From<PhysicalPosition<P>> for mint::Point2<P> {
322    fn from(winit: PhysicalPosition<P>) -> Self {
323        mint::Point2 {
324            x: winit.x,
325            y: winit.y,
326        }
327    }
328}
329
330/// A size represented in logical pixels.
331#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)]
332#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
333pub struct LogicalSize<P> {
334    pub width: P,
335    pub height: P,
336}
337
338impl<P> LogicalSize<P> {
339    #[inline]
340    pub const fn new(width: P, height: P) -> Self {
341        LogicalSize { width, height }
342    }
343}
344
345impl<P: Pixel> LogicalSize<P> {
346    #[inline]
347    pub fn from_physical<T: Into<PhysicalSize<X>>, X: Pixel>(
348        physical: T,
349        scale_factor: f64,
350    ) -> Self {
351        physical.into().to_logical(scale_factor)
352    }
353
354    #[inline]
355    pub fn to_physical<X: Pixel>(&self, scale_factor: f64) -> PhysicalSize<X> {
356        assert!(validate_scale_factor(scale_factor));
357        let width = self.width.into() * scale_factor;
358        let height = self.height.into() * scale_factor;
359        PhysicalSize::new(width, height).cast()
360    }
361
362    #[inline]
363    pub fn cast<X: Pixel>(&self) -> LogicalSize<X> {
364        LogicalSize {
365            width: self.width.cast(),
366            height: self.height.cast(),
367        }
368    }
369}
370
371impl<P: Pixel, X: Pixel> From<(X, X)> for LogicalSize<P> {
372    fn from((x, y): (X, X)) -> LogicalSize<P> {
373        LogicalSize::new(x.cast(), y.cast())
374    }
375}
376
377impl<P: Pixel, X: Pixel> Into<(X, X)> for LogicalSize<P> {
378    fn into(self: LogicalSize<P>) -> (X, X) {
379        (self.width.cast(), self.height.cast())
380    }
381}
382
383impl<P: Pixel, X: Pixel> From<[X; 2]> for LogicalSize<P> {
384    fn from([x, y]: [X; 2]) -> LogicalSize<P> {
385        LogicalSize::new(x.cast(), y.cast())
386    }
387}
388
389impl<P: Pixel, X: Pixel> Into<[X; 2]> for LogicalSize<P> {
390    fn into(self: Self) -> [X; 2] {
391        [self.width.cast(), self.height.cast()]
392    }
393}
394
395#[cfg(feature = "mint")]
396impl<P: Pixel> From<mint::Vector2<P>> for LogicalSize<P> {
397    fn from(mint: mint::Vector2<P>) -> Self {
398        Self::new(mint.x, mint.y)
399    }
400}
401
402#[cfg(feature = "mint")]
403impl<P: Pixel> From<LogicalSize<P>> for mint::Vector2<P> {
404    fn from(winit: LogicalSize<P>) -> Self {
405        mint::Vector2 {
406            x: winit.width,
407            y: winit.height,
408        }
409    }
410}
411
412/// A size represented in physical pixels.
413#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash)]
414#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
415pub struct PhysicalSize<P> {
416    pub width: P,
417    pub height: P,
418}
419
420impl<P> PhysicalSize<P> {
421    #[inline]
422    pub const fn new(width: P, height: P) -> Self {
423        PhysicalSize { width, height }
424    }
425}
426
427impl<P: Pixel> PhysicalSize<P> {
428    #[inline]
429    pub fn from_logical<T: Into<LogicalSize<X>>, X: Pixel>(logical: T, scale_factor: f64) -> Self {
430        logical.into().to_physical(scale_factor)
431    }
432
433    #[inline]
434    pub fn to_logical<X: Pixel>(&self, scale_factor: f64) -> LogicalSize<X> {
435        assert!(validate_scale_factor(scale_factor));
436        let width = self.width.into() / scale_factor;
437        let height = self.height.into() / scale_factor;
438        LogicalSize::new(width, height).cast()
439    }
440
441    #[inline]
442    pub fn cast<X: Pixel>(&self) -> PhysicalSize<X> {
443        PhysicalSize {
444            width: self.width.cast(),
445            height: self.height.cast(),
446        }
447    }
448}
449
450impl<P: Pixel, X: Pixel> From<(X, X)> for PhysicalSize<P> {
451    fn from((x, y): (X, X)) -> PhysicalSize<P> {
452        PhysicalSize::new(x.cast(), y.cast())
453    }
454}
455
456impl<P: Pixel, X: Pixel> Into<(X, X)> for PhysicalSize<P> {
457    fn into(self: Self) -> (X, X) {
458        (self.width.cast(), self.height.cast())
459    }
460}
461
462impl<P: Pixel, X: Pixel> From<[X; 2]> for PhysicalSize<P> {
463    fn from([x, y]: [X; 2]) -> PhysicalSize<P> {
464        PhysicalSize::new(x.cast(), y.cast())
465    }
466}
467
468impl<P: Pixel, X: Pixel> Into<[X; 2]> for PhysicalSize<P> {
469    fn into(self: Self) -> [X; 2] {
470        [self.width.cast(), self.height.cast()]
471    }
472}
473
474#[cfg(feature = "mint")]
475impl<P: Pixel> From<mint::Vector2<P>> for PhysicalSize<P> {
476    fn from(mint: mint::Vector2<P>) -> Self {
477        Self::new(mint.x, mint.y)
478    }
479}
480
481#[cfg(feature = "mint")]
482impl<P: Pixel> From<PhysicalSize<P>> for mint::Vector2<P> {
483    fn from(winit: PhysicalSize<P>) -> Self {
484        mint::Vector2 {
485            x: winit.width,
486            y: winit.height,
487        }
488    }
489}
490
491/// A size that's either physical or logical.
492#[derive(Debug, Copy, Clone, PartialEq)]
493#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
494pub enum Size {
495    Physical(PhysicalSize<u32>),
496    Logical(LogicalSize<f64>),
497}
498
499impl Size {
500    pub fn new<S: Into<Size>>(size: S) -> Size {
501        size.into()
502    }
503
504    pub fn to_logical<P: Pixel>(&self, scale_factor: f64) -> LogicalSize<P> {
505        match *self {
506            Size::Physical(size) => size.to_logical(scale_factor),
507            Size::Logical(size) => size.cast(),
508        }
509    }
510
511    pub fn to_physical<P: Pixel>(&self, scale_factor: f64) -> PhysicalSize<P> {
512        match *self {
513            Size::Physical(size) => size.cast(),
514            Size::Logical(size) => size.to_physical(scale_factor),
515        }
516    }
517}
518
519impl<P: Pixel> From<PhysicalSize<P>> for Size {
520    #[inline]
521    fn from(size: PhysicalSize<P>) -> Size {
522        Size::Physical(size.cast())
523    }
524}
525
526impl<P: Pixel> From<LogicalSize<P>> for Size {
527    #[inline]
528    fn from(size: LogicalSize<P>) -> Size {
529        Size::Logical(size.cast())
530    }
531}
532
533/// A position that's either physical or logical.
534#[derive(Debug, Copy, Clone, PartialEq)]
535#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
536pub enum Position {
537    Physical(PhysicalPosition<i32>),
538    Logical(LogicalPosition<f64>),
539}
540
541impl Position {
542    pub fn new<S: Into<Position>>(position: S) -> Position {
543        position.into()
544    }
545
546    pub fn to_logical<P: Pixel>(&self, scale_factor: f64) -> LogicalPosition<P> {
547        match *self {
548            Position::Physical(position) => position.to_logical(scale_factor),
549            Position::Logical(position) => position.cast(),
550        }
551    }
552
553    pub fn to_physical<P: Pixel>(&self, scale_factor: f64) -> PhysicalPosition<P> {
554        match *self {
555            Position::Physical(position) => position.cast(),
556            Position::Logical(position) => position.to_physical(scale_factor),
557        }
558    }
559}
560
561impl<P: Pixel> From<PhysicalPosition<P>> for Position {
562    #[inline]
563    fn from(position: PhysicalPosition<P>) -> Position {
564        Position::Physical(position.cast())
565    }
566}
567
568impl<P: Pixel> From<LogicalPosition<P>> for Position {
569    #[inline]
570    fn from(position: LogicalPosition<P>) -> Position {
571        Position::Logical(position.cast())
572    }
573}