The wording here is intentionally vague because I don't know very well what the possibilities are and what makes sens for winit. It is safe to say that it would be good for winit to expose some way to indicate whether the application is "dpi-aware" and avoid having the window upscaled by the window manager for apps that don't want this behavior.
On Fedora 25 with wayland (now enabled by default), winit windows are all upscaled on hidpi screens.
cc @vberger
I believe this needs some discussion, as DPI is not handled the same way on all platforms.
The wayland way is:
From what I gathered, on Win32 each thread has a HiDPI-awareness level:
Notably, the third option is only available on Windows 8.1+.
I couldn't find whether moving a window from a thread to another, or changing the HiDPI awareness level during the execution, modifies the existing windows. The HiDPI awareness is supposed to be set once at initialization. The older way to deal with that was per application, and you could only set it once (all further calls would fail). Maybe this is also the case for threads.
We need to support 3 things to support high DPI:
Window::hidpi_factor() function for this.On Windows situation is as follows:
On Windows 10 Anniversary Update or later we can set DPI awareness individually for each created window:
SetThreadDpiAwarenessContext before creating the window.SetThreadDpiAwarenessContext again.Quote from some unofficial documentation:
SetThreadDpiAwarenessContext() function was introduced in windows 10 aniversary update. This allows a thread to change its DPI awareness level at will, i.e. for some time duration a thread can set itself as DPI unaware, while at other times it cadn set itself as per monitor aware, or system aware. This allows the thread to create windows which have different DPI awareness levels. Using this we can build an application in which scaling of some UI elements is handled by windows, while scaling for others is handled by the application itself.
Windows 8.1 or later. DPI awareness is set for the entire process and can be set only once with the SetProcessDpiAwareness. If DPI awareness is set in the app manifest that value will take precedence and SetProcessDpiAwareness will fail.
On Windows Vista deprecated SetProcessDPIAware can be used.
Windows 10 - use GetDpiForWindow. It seems it was introduced in the Anniversary Update.
Windows 8.1 or later - use GetDpiForMonitor.
Windows Vista. Interestingly, there is a way to set DPI awareness on Windows Vista (SetProcessDPIAware) but I don't see a way to get the DPI value. WM_DPICHANGED is only sent on Windows 8.1. I checked all functions in the High DPI Reference and all functions for getting DPI are available only on 8.1 or newer. EDIT: For older Windows you can get the DPI by calling GetDeviceCaps with LOGPIXELSX and LOGPIXELSY.
WM_DPICHANGED message is the way to go. It is sent by Windows 8.1 or later to DPI aware windows. It provides two values - X and Y DPI. MSDN says "the values of the X-axis and the Y-axis are identical for Windows apps". It is not clear why it explicitly mentions "Windows apps" as if there were some non Windows apps which could receive this message. I think for simplicity we can assume that they are always identical. I'm not sure which actions are required upon receiving this message. In MSDN they are calling SetWindowPos to change window size after receiving this message, I'm not sure why.
@kryptan For older Windows you can get the DPI by calling GetDeviceCaps with LOGPIXELSX and LOGPIXELSY.
Even on Windows 10 Anniversary Update it is still necessary to call SetProcessDpiAwareness to prevent Windows from scaling your app. If you only use SetThreadDpiAwarenessContext you will get both real values of DPI and scaling from the OS which leads to double scaling.
PR should be ready soon.
@kryptan For older Windows you can get the DPI by calling GetDeviceCaps with LOGPIXELSX and LOGPIXELSY.
It works but requires user to log out after changing scaling.
Yes, because older Windows only supports the notion of system DPI, not per monitor DPI, and changes to the system DPI require relogging. Regardless, that is the correct API to use for those older systems.
Currently Window::get_inner_size_points() invokes platform-specific code to get window size in virtual pixels while Window::get_inner_size_pixels() just multiplies this value by hidpi_factor. This means that platform backend needs to divide window size in real pixels by hidpi_factor and round that to integer while winit will then multiply it back. This logic should be switched, i.e winit should query platform backends for size in real pixels, otherwise we will get rounding errors. This requires changes to all backends.
For my Windows patch I will probably just follow the existing logic despite the rounding errors that it causes.
Aside from implementation details I'd like to raise the questions of what winit should assume from its users:
Thing is, as in wayland the user can bascially draw at any dpi they want, we need to be sure to advertize the proper used dpi factor to the server, other wise the sizes won't match.
Should we insert a mechanism for winit users to activate dpi-awareness for their program?
Should it be per-program or per-window?
My current implementation for Windows is here. Windows has application global DPI-awareness. Latest Windows 10 has mechanism for per-window DPI-awareness but I hasn't been able to make it work. Setting awareness for window different from the global one causes weird issues, so my current implementation has one global function - become_hidpi_aware(). Nevertheless, other systems probably allow per-window DPI-awareness, so this should also need to be supported.
To move forward I think we need to agree on the cross-platform interface and then implement it in backends. This is kind of an RFC for the proposed API changes to winit.
Add become_hidpi_aware() global function. It will be implemented at least for Windows but may be a no-op on other platforms.
Add MonitorId::get_hidpi_factor() -> f32 method. This method is necessary to calclulate sizes of the windows that you will create.
Add WindowAttributes::hidpi_aware: bool field. This is a hint to the OS whether this new window should be scaled for high-DPI displays by the OS or by the applications itself. Note that at least on Windows we may not have control whether created window will be scaled by the OS or not so this is just a hint.
All methods in the Window struct should operate on physical pixels for high-DPI aware windows and on virtual pixels for high-DPI unaware windows. Namely get_position, set_position, get_inner_size, get_outer_size, set_inner_size, set_cursor_position. Otherwise we'll probably need to duplicate all of them like it is done for get_inner_size, and I don't see much need for this. Users can call Window::hidpi_factor() and calculate size in virtual pixels themselves if necessary.
Remove Window::get_inner_size_points and Window::get_inner_size_pixels (replaced by Window::get_inner_size, see above).
Add WindowEvent::HiDPIFactorChanged(f32). This event will be executed when Window::hidpi_factor() changes. All backends must ensure that value passed to this event equals to the value returned from the Window::hidpi_factor().
All backends must ensure that for each window, either one of the following invariants holds:
Window::hidpi_factor() always returns 1 and all methods in Window are using virtual pixels (i.e. it is high-DPI unaware window, its content is scaled by the OS).Window::hidpi_factor() returns real value and all methods in Window are using physical pixels (i.e. it is high-DPI aware window, its content must be scaled by the application).Window::hidpi_factor() can be thought of as the scaling that is expected from the application.
Don't create become_hidpi_aware() global function, instead make application high-DPI aware when a window with WindowAttributes::hidpi_aware == true is created. The issue with this approach is that you want to know hidpi_factor before you create first window to correctly specify its size in pixels, but if we don't make process high-DPI aware on Windows then MonitorId::get_hidpi_factor() will always return 1 instead of real value.
Should we return bool from become_hidpi_aware() to indicate whether process has succesfully become high-DPI aware or already was? If we return bool should we always return true on platforms where it is a no-op?
What should be the default value for WindowAttributes::hidpi_aware?
Thoughts?
Is all the become_hidpi_aware functionality really necessary? For an application that doesn't care about it, wouldn't they just assume it's 1.0? I also wonder that, if we do need such a method, shouldn't it be a window builder method rather than a global?
The rest of this sounds good to me--both the MonitorId improvements and the additional event to the WindowEvent enum.
Is all the become_hidpi_aware functionality really necessary? For an application that doesn't care about it, wouldn't they just assume it's 1.0? I also wonder that, if we do need such a method, shouldn't it be a window builder method rather than a global?
If you don't care about it you just don't call it and assume that hidpi factor is 1.0. Whether it can be a builder method I think I addressed in the Alternatives section. By the time you create your window you already need to know hidpi factor to specify window size but to get it from MonitorId::get_hidpi_factor you need to make the process high-DPI aware first.
I may be wrong, but for me HiDPI awareness is just a thing that exists to preserve backward compatibility.
I don't think it's necessary to have a concept of not being HiDPI-aware.
@tomaka I agree with this but on Windows we can't just make process hidpi aware without consent of the programmer. Winit may be used as part of a larger application, e.g. in dll, where there are windows created by other, non-winit, code. Enabling high-dpi awareness will mess this up. Therefore, I think become_hidpi_aware() should be there. On the other hand, WindowAttributes::hidpi_aware may not be necessary and we'll just attempt to make each window hidpi aware.
I have no concerns regarding wayland implementation, as wayland allows very fine control over dpi-awareness, we should be able to implement this API without much trouble.
However, I must say, I find it weird to have both a global become_dpi_aware() and a WindowAttributes::hidpi_aware. These just seem redundant.
Winit may be used as part of a larger application, e.g. in dll, where there are windows created by other, non-winit, code.
But this reasoning would also mean that these DLLs are forbidden to enable HiDPI-awareness as well, as they would mess up the rest of the application.
What I mean, is that more generally it's not possible to isolate a code that creates a window in its own little module or its own little library. Any code that creates a window (whether it is from winit or not) should specify what it does when it comes to HiDPI-awareness. Therefore I'd be in favour of making HiDPI-awareness mandatory for winit.
But this reasoning would also mean that these DLLs are forbidden to enable HiDPI-awareness as well, as they would mess up the rest of the application.
I have the following use case in mind. There is an existing C++ Windows application which has a GUI and which can use DLL plugins. These DLL plugins may need to display their own GUI to show user some settings. This is not just a theory, see e.g. #159. If the original application is not HiDPI aware then plugin written in Rust should not attempt to change that. This issue was previously raised here.
As an alternative we can provide an option to opt-out of the DPI-awareness in which case winit will not attempt to change any process-global settings.
I think I found a reasonable solution. The use case that I mentioned above is very niche while probably 99% of winit users on Windows are writing standalone executables where changing process-global settings is not an issue.
I propose the following: don't introduce become_hidpi_aware() global function and don't add WindowAttributes::hidpi_aware option, instead always enable high-DPI awareness when necessary (when monitor DPI is requested or window is created). For the niche use case when you are writing a DLL loaded by another process we can add a cargo feature which will prevent enabling DPI awareness on Windows. The only thing this feature would do is turning our internal function which enables DPI awareness into a no-op.
We'll also need to change MonitorId::get_position to return (i32, i32) instead of (u32, u32) because on Windows position can be negative. Primary monitor always has position (0, 0), if there is a monitor to the left or to the top of it then it will have negative position.
Can we consider this closed, given #319 and #324 are now merged?
@vberger Those PRs don't include the actual backend implementation of high DPI for Windows.
@vberger This is still largely unimplemented for most platforms.
MonitorId::get_hidpi_factor()HiDPIFactorChanged eventFair point.
Overall, I feel we need a clear way to track status of issues regarding both "public api design" and "backend implementation"...
On second thought, is the HiDPIFactorChanged event necessary? Wouldn't any change in high-DPI factor be also accompanied by the resize event? E.g. if high-DPI factor is changed from 1 to 2 then window size in pixels should also be increased by 2x and resize event will be sent. Maybe we should add information about high-DPI factor changes into the Resized event instead of adding a new event?
@kryptan Depending only on Resize event means, each time the event is fired, the client is expected to check Window::hidpi_factor() to see if it has changed. This is IMO not very ergonomic and easily error prone, while having the HiDPIFactorChanged event clearly hints that this is something that can occur and should be taken into account.
Alternatively, we could change the resized event from Resized(width, height) to Resized(width, height, hidpi_factor), this way all the information is present in a single event, while hinting clearly in the API that the client is expected to take hidpi into account.
@vberger That's what I meant. Although I think it is necessary to make it explicit whether hidpi_factor has actually changed. Either by adding bool or by making it an Option and using None if it hasn't changed.
I'd personally rather have the resize and dpi changed events be separate. After all, your Windows doesn't necessarily have to get resized by the window manager when the dpi changes. Because your application is dpi aware, the window manager is perfectly justified in simply telling you the DPI changed and letting you adjust the size of your window yourself.
Although I think it is necessary to make it explicit whether hidpi_factor has actually changed
@kryptan In this case I think I prefer your original idea of having a HiDpiFactorChanged - I don't think checking a bool or Option in the Resized event would be quite as ergonomic. Perhaps we can ensure that each HiDpiFactorChanged event will always be immediately followed by a Resized event?
There's also the option of having HiDpiFactorChanged include the resize event's fields, though having two entirely separate events seems more likely to Just Work with simple client code.
your Windows doesn't necessarily have to get resized by the window manager when the dpi changes
My understanding is that it was decided that winit would alwaysreport as size of the window the actual pixel size of the drawing surface.
So if the dpi change, winit should resize the window to reflect that (preserving the pixel_dimension/hidpi_factor ratio constant). As such, a change in DPI will always trigger a resize event.
Though I'm also in favor of keeping the two events separate, mostly because I believe it's the most ergonomic.
If they are separate events, wouldn't it be a problem that application may see inconsistent state - where one of the events was already received but not the other? If it uses some UI framework it may recalculate sizes of all widgets, and when second message is received it will need to recalculate them back.
Because your application is dpi aware, the window manager is perfectly justified in simply telling you the DPI changed and letting you adjust the size of your window yourself.
Together with the DPI change notification Windows also sends the new suggested window size. In my PR I'm simply resizing window to this size. If I don't do this then this information will either be lost or need to be added to the HiDpiFactorChanged event.
Together with the DPI change notification Windows also sends the new suggested window size. In my PR I'm simply resizing window to this size. If I don't do this then this information will either be lost or need to be added to the HiDpiFactorChanged event.
In that case, I'd personally rather HiDpiFactorChanged carry along the new suggested window size.
@retep998 Then it would also be necessary to introduce new method in Window. Windows gives us the RECT that can be passed directly to SetWindowPos but there is currently no method in Window which would directly call SetWindowPos with both position and size.
What would be the use-case where you don't want your window to be automatically scaled by winit?
I've made an OpenGL app to test high-DPI support. It should correctly handle high-DPI and respond to DPI changes.
Thanks @kryptan , this is very useful!
Okay, We still have an unresolved question regarding the HiDPI API.
The window creation api requires a size. Platforms such as wayland do not give you any way to know in advance which monitor your window will be created on. And if the user tries to guess the DPI from monitor list (like @kryptan 's example), but gets it wrong, winit will end up creating a gigantic or very small window.
The ideal process for window creation with wayland would be:
HiDpiFactorChanged event followed by a Resized event with the dpi-aware actual sizeHow what do you think of this, and how would it be ok for other platforms? @tomaka @Ralith @mitchmindtree ?
Also @kryptan : I've tried using your app on linux, and it only displays a blue screen. Is that expected?
@vberger I've added screenshot to the readme. I tested it only on Windows so it is possible that I accidentally relied on some platform-specific behaviour. Rendering is very simple, no idea what might have gone wrong.
Together with the DPI change notification Windows also sends the new suggested window size. In my PR I'm simply resizing window to this size. If I don't do this then this information will either be lost or need to be added to the HiDpiFactorChanged event.
In that case, I'd personally rather HiDpiFactorChanged carry along the new suggested window size.
Letting programmer to arbitrarily resize and reposition window in response to DPI changes can have an adverse effect of causing another DPI change. After reposition and resize Windows may decide that window is now back at the monitor it was moved from and fire another DPI change. As I understand this would also be an issue for the Wayland backend as well, because after resizing, window may not have intersection with the display that caused the original DPI change and Wayland backend would initiate another DPI change to restore DPI. This may cause infinite series of DPI changes. Microsoft has introduced the WM_GETDPISCALEDSIZE message to solve this and allow applications to specify their desired sized after DPI changes. Before changing DPI, Windows asks window what size it wants to be after DPI change and then calculates window position by itself.
Hmm, I'm not sure what you describe is relevant to wayland, which models DPI quite differently.
Basically, on wayland, each monitor has a DPI factor. And each app, when drawing, tells which DPI factor it is using. If the two don't match, the wayland compositor rescales the contents of the surface appropriately.
What winit would do then, is to always use the highest DPI among the monitors the window is displayed on. But while it would generate a Resize event to its user to notify the canvas size has changed along with the DPI, the actual window size as seen by the wayland server would not change.
@vberger there is no visible change in window size when you move it between monitors? If so then what I said probably doesn't apply to wayland and it is how I would want sane implementation of high-DPI to work. But on Windows when you move window between monitors it would appear normal on one monitor and gigantic or small on the other. Once move than half was moved, window is resized and looks normal on the new monitor but the part that is still on the previous monitor would become small or gigantic. In this case there is the actual window resizing and reposition caused by DPI change.
Yes, such behavior should not appear on wayland, as the compositor is expected to resize the contents appropriately. Also, wayland windows don't have access to their location anyway, so they don't know if/how they are being moved.
My earlier question about window creation is still pending and in my opinion a blocking matter for hidpi implementation. Thoughts @tomaka, @Ralith, @mitchmindtree, @kryptan ? ( this question : https://github.com/tomaka/winit/issues/105#issuecomment-341964817 )
@vberger your proposal looks reasonable. It seems that macOS backend already works that way (#337). On Windows we'll need to create window with size in pixels, then get its DPI and then resize if necessary. At least I don't see any other way.
Also, I still think we should unify DPI change and resize events into one event to prevent application from observing inconsistent state. For example, my testing app will redraw everything twice on each DPI change - one time for the DPI change itself and one time for the associated resize, and the first redraw would actually be incorrect.
Was there a resolution on this? In #319, @kryptan added HiDPIFactorChanged(f32) event, which doesn't seem to capture the latest consensus on that event. In light of https://github.com/tomaka/winit/issues/105#issuecomment-343916767, specifically:
Also, I still think we should unify DPI change and resize events into one event to prevent application from observing inconsistent state. For example, my testing app will redraw everything twice on each DPI change - one time for the DPI change itself and one time for the associated resize, and the first redraw would actually be incorrect.
It seems like another way to handle this is to keep them as separate events, include Option(Resized) in the HiDPIFactorChanged event, and only fire HiDPIFactorChanged even if there is a resize, if it's due to the dpi changing.
Ok, I did some testing and actually that might be tricky for macos backend since a resize event is sent when the display resolution changes.
It still seems like the HiDPIFactorChanged event should include a Resized, though?
I'm gonna go ahead with a PR with the existing signature, it should be pretty easy to change later.
@vberger the way things are implemented in #332, the window is automatically resized and repositioned whenever the DPI changes, since Windows conveniently sends us the recommended geometry when that happens. Is this something you believe should be handled by the application developer instead?
@francesca64 No, I agree this is good to handle it automatically.
My only point of question is on the window creation API: when the user defines the size of their newly created window, they cannot anticipate on which monitor this window will appear (or can they? At least not on Wayland).
As such, it seems weird to specify this size in physical pixels, as this would require in advance to know the DPI factor of the monitor on which the window will appear.
This is why my suggestion was to instead always initially set this size in "logical pixels" (by this I mean assuming a DPI factor of 1), and then receive the proper HiDPI instructions from the server and forward them to the app via the appropriate events to let it handle it correctly.
@vberger well, I already have it so that it sends a HiDPIFactorChanged and Resized after window creation if the DPI factor turns out not to be 1.0, since that seems to be the idea. Outside of specifying which monitor to put the window on, I don't believe we can get this info in advance on Windows either.
As such, it seems weird to specify this size in physical pixels, as this would require in advance to know the DPI factor of the monitor on which the window will appear.
This is why my suggestion was to instead always initially set this size in "logical pixels" (by this I mean assuming a DPI factor of 1)
Okay, I'm definitely confused now. I thought we were specifically trying to use physical pixels. So, inferring from this, am I now correct in understanding that (with HiDPI) a 100x100 physical pixel area isn't 100x100 usable pixels we can render to, but instead gives us as many usable pixels as are represented by 100x100 pixels in the monitor? I was under the impression that 100x100 physical pixels would give us 100x100 usable pixels, but just not in the desired scale.
There's three kinds of pixels:
Setting the size of a window when creating it should absolutely be done in logical pixels, partially because you might not know what dpi the window will be at until created and partially because that's what you want 99% of the time anyway.
Ah, thanks for this clarification @retep998, I was not clear myself about the right terminology.
So, my understanding of the direction winit has gone for now is that winit automatically handles the DPI awareness, meaning from the point of view of the user of winit, all events are reported in application pixels.
An other possibility could be to always report everything in logical pixels, and let the user handle the DPI scaling during drawing. I tend to lean towards this direction, but I guess it's mostly because that's what the Wayland protocol does, so I'll take this as my point of view being biased unless it is the same for other platforms.
Still, I agree with @retep998 that specifying the size of window at creating all cases should always be done in logical pixels, for the reasons stated above.
vberger https://github.com/tomaka/winit/issues/105#issuecomment-388310737:
An other possibility could be to always report everything in logical pixels,
Wouldn't this lead to loss of precision?
I'd like to reference the comment by tomaka to hopefully keep this issue on track and moving towards implementation:
https://github.com/tomaka/winit/issues/105#issuecomment-335846564
I may be wrong, but for me HiDPI awareness is just a thing that exists to preserve backward compatibility.
I don't think it's necessary to have a concept of not being HiDPI-aware.
This makes sense to me and I think winit is heading in the right direction, using Physical pixels (or Application pixels) everywhere, possibly with the exception of for window creation as being discussed.
In my opinion, every application with a GUI being written today should be high-DPI aware, if not, I would almost consider that a bug.
My proposal is this:
Wouldn't this lead to loss of precision?
I don't think so, given winit reports pointer location as f64, and I don't think the possible DPI factors are large enough that this would cause a loss of precision.
The interest in processing input using logical pixels is that it allows winit users to only have to care about HiDPI when drawing, but not when processing input, as whatever the DPI your drawing for, the size and location of your UI relative to the dimensions of your window will likely be the same.
@retep998 thank you magic Windows bunny!
@anderejd
to hopefully keep this issue on track and moving towards implementation
I will say that without DPI awareness, winit is comically broken on Windows when using display scaling. That said, the implementation is basically done; we just need to ensure we're on the same page about intended behavior (...and I have to fix a bug with switching in and out of fullscreen on a different monitor in mixed DPI).
@vberger
An other possibility could be to always report everything in logical pixels, and let the user handle the DPI scaling during drawing.
Wouldn't that be a breaking change, and one that people are unlikely to catch?
Also, isn't creating a window in logical pixels with an assumed DPI factor of 1.0 the same as using physical pixels (assuming the entire window is on one monitor)? That's another thing I've been confused about.
@francesca64
Wouldn't that be a breaking change, and one that people are unlikely to catch?
It depends on what the different backends currently do I guess. Indeed if there is already an existing uniformity we should stick to it. If not, it would be a good opportunity to design it.
Wayland currently does not handle DPI scaling at all yet, meaning that everything is always in logical pixels (the server handles the rescaling for us). Apparently Window is horribly broken as you said. What do the MacOS and X11 backends do?
Also, isn't creating a window in logical pixels with an assumed DPI factor of 1.0 the same as using physical pixels (assuming the entire window is on one monitor)? That's another thing I've been confused about.
Well, my understanding is that (please someone correct me if I'm wrong):
At this point, it quite depends on what the system APIs do. The Wayland one is really oriented towards the first case, as almost everything is done in logical pixels.
@vberger MacOS and X11 both give physical pixels, and after skimming through search results on GitHub it seems to be pretty universally assumed that winit gives values in physical pixels as well. Because of that, I don't think it's feasible to switch over to logical pixels at this point.
It doesn't seem like a good idea create an API where window creation is done in logical pixels while everything else is done in logical pixels, because of the issues with API inconsistency. One option we have to handle that is to have the user explicitly request that the window size be in physical or logical pixels at creation, then have everything else assume physical pixels. The main ways I can think of to do that would be to either split the window-size functions into with_dimensions_physical/with_dimensions_logical or to have with_dimensions take an enum that specifies which kind of size the user wants.
it seems to be pretty universally assumed that winit gives values in physical pixels as well
Alright, the question is closed then. :)
Re @ creation dimensions: I still fail to see how it is possible to specify a creation size in physical pixels in a meaningful way.
For example, image a setup with two monitors, one has a dpi factor of 1.0, the other of 2.0.
A user tells winit they wand a 100x100 physical pixels window.
What should winit do? Is it a 100x100 pixels window on the 1.0 screen, that will be rescaled to 200x200 when moved to the 2.0 one, or a 100x100 pixels window on the 2.0 screen, that will be rescaled to 50x50 when moved on the 1.0 one?
@vberger I think I understand now. Up until this point, my thought process was "but isn't specifying logical pixels for 1.0 the same as specifying physical pixels?" but what you want is for the window creation size to semantically be the logical base size of the window.
Oh, right, and since you mentioned sending Resized before, the idea is for the backend to automatically resize the window after creation if the DPI isn't 1.0, right? This makes a lot of sense now. I was just sending Resized without actually resizing the window, which seems to be a symptom of working on winit without taking enough breaks.
@francesca64
but what you want is for the window creation size to semantically be the logical base size of the window.
Yes, that's it. Sorry if that wasn't clear before.
the idea is for the backend to automatically resize the window after creation if the DPI isn't 1.0, right
That is indeed my understanding. On wayland, the client can choose at which scaling factor it is drawing and the server rescales the content if necessary, that's why everything else is done in logical pixels in the protocol.
Thus, what I'm doing in #341 is that each window tracks the list of monitors it is currently being visible on, and sets its DPI factor as being the highest of them all. Thus, every time the window enters or leaves a monitor, the backend computes the new appropriate DPI factor, and if it changed, generates a Resized() event and a HiDPIFactorChanged() event with the appropriate values. I don't actually resize the window as this is not handled by winit itself due to how wayland works, but the user (glutin for example) will automatically handle it when processing the Resized() event.
@vberger I've gotten sidetracked by completely replacing the keyboard handling in the macOS backend, since things like that happen whenever I have to review a PR on another platform. However, I thought it would be a good idea to look into sorting out HiDPI on X11 before moving forward here, since I can make sure X11 is consistent with macOS and Windows, and then you can make sure X11 is consistent with Wayland.
So far it's been easier than expected. #515 contained much of the groundwork, and the only major obstacle left is figuring out why I can't get XResizeWindow to work when moving to another monitor. My hypothesis is that my WM's window snapping is interfering, but I digress... if I'm understanding your point about using logical pixels for input handling correctly, then it sounds like a good idea, and I don't anticipate this taking much longer to wrap up.
@francesca64
if I'm understanding your point about using logical pixels for input handling correctly, then it sounds like a good idea, and I don't anticipate this taking much longer to wrap up.
Hmm, didn't we conclude that as winit is basically already physical pixels everywhere, this should be kept as-is?
An other point to be careful on, when mixing physical and logical pixels, is that we can't go all the way logical, the client still needs to know the size of its actual drawing surface. So there would be a need to distribute it accordingly (modifying the Resize event to provide both physical & logical size?), or explicitly document that the drawing surface dimensions is always logical dimensions times dpi scale factor?
Since it seems likely that both logical and physical pixels will be part of the API, how about introducing a named type for each, instead of anonymous tuples? I will gladly take a broken build every time over runtime errors.
@anderejd Well, that sure is a good idea!
We could go all the way with something like:
#[derive(Copy, Clone)]
struct Coordinates { x: f64, y: f64}
impl Coordinates {
pub fn from_logical(x: f64, y: f64) -> Coordinates {
Coordinates { x, y }
}
pub fn from_physical(x: f64, y: f64, dpi_factor: f64) -> Coordinates {
Coordinates { x: x/dpi_factor, y: y/dpi_factor }
}
pub fn to_logical(&self) -> (f64, f64) {
(self.x, self.y)
}
pub fn to_physical(&self, dpi_factor: f64) -> (f64, f64) {
(self.x * dpi_factor, self.y * dpi_factor)
}
}
and use that type everywhere, letting the user use what they prefer. This would also make clear that physical coordinates are only meaningful in the context of a dpi factor.
@vberger sounds like a plan! Would it be good for that struct to contain the DPI factor as well?
Tbh I'm not quite sure. The risk of storing the DPI factor in it would be a user keeping an old Coordinates value around, and then trying to use it while the actual dpi factor changed in between, and gets invalid results due to that.
Keeping it out would make clear that the DPI factor is some context value that may change and needs to be tracked, I guess.
On the other hand, integrating the DPI factor in the struct is certainly more convenient if we assume that there are supposed to always be short-lived anyway.
That risk sounds like it's largely mitigated by the existence of HiDPIFactorChanged.
Yeah, but in that case I think we need to make it very clear that the Coordinates objects are created for a given dpi, and that a DPI change would invalidate them (and add an API to update them to a new DPI, I guess).
I think I'm still in favor of including it, since when possible users should favor responding to events over directly querying things, and I think they're more likely to follow that recommendation if the DPI is included.
(I'm assuming you agree with that recommendation. It's partially motivated by our inability to guarantee that queries will always have a low cost.)
It's partially motivated by our inability to guarantee that queries will always have a low cost.
Wouldn't that be a strong argument to not build an API that forces the backends to make such queries on every event with a coordinate in it? I'd think the queries could be made cheap just by caching the latest value for each window, though.
There's also the downside that anyone who is going to be passing around or storing lots of logical coordinates (say, for a GUI impl) will need to come up with their own type and do a lot of converting.
@Ralith true, the performance issue was a weak argument on my part. Why would including the DPI factor in the struct force application developers to make their own type and do lots of converting, though?
Because if the DPI factor is embedded, the struct is larger than necessary for performing computations purely in logical space, which is undesirable. People could just ignore the cost, but it's an incentive against using it all the same.
I'm not sure I share that sentiment, but since embedding the DPI factor would be less transparent/explicit anyway, I think I've changed my mind about it. @vberger's struct looks good as-is.
How do you handle the coordinate object having an embedded dpi factor when the window is overlapping multiple displays with different dpi factors on each?
The idea I was thinking of regarding named types instead of tuples was something more like this:
struct LogicalCoordinates { x: f64, y: f64}
struct PhysicalCoordinates { x: f64, y: f64}
With the primary purpose of not mixing them up, if we need both in the API.
I'm unsure if the DPI factor should be bundled with the coordinates or not.
@anderejd sounds good to me. It's simple, composable, clearly communicates the intentions, and provides us with more hope for getting this done before this issue hits 100 comments.
For the record: my suggestion was voluntarily going farther than yours @anderejd , I was just thinking that in APIs where there is no obvious "better choice" between them (which may be many), offering an abstraction over both could be useful.
Nevertheless, this can easily be done with your suggestion as well, by adding appropriates .to_physical(dpi) and .to_logical(dpi) methods to these types.
So, from my point of view both are pretty equivalent, I don't have any strong preference between any of the two approaches.
So, which functions should take/return logical pixels, besides with_dimensions (and presumably with_max_dimensions and with_min_dimensions)?
That seems reasonable based on what has been written earlier in this issue, @vberger ?
I agree that with_dimensions, with_max_dimensions and with_min_dimensions can hardly be made meaningful while taking something else than logical pixels.
Now, taking the rest of winit API, there are the places where coordinates or dimensions are used:
So. From this I mostly see two possibilities for a roughly general and coherent API:
with_dimensions, with_max_dimensions and with_min_dimensions), and document that they can be converted to logical pixels to make the input handling logic DPI-invariant.I would tend to favor the second case, because it matches the fact that DPI awareness really only matters for drawing, most of the time. But both options seem viable to me, so it's not an hill I'll die on.
Option 2 makes more sense to me. Making drawing special probably makes more sense to the average user than making window creation special.
Either way, besides API documentation, I think we really ought to have an example program for this. I'm not sure where it should live (since without a drawing surface there's nothing to demonstrate) but I'm confident it would improve the chances of people actually supporting HiDPI.
If we end up going with option 2, I don't think we should have winit tell the OS it's DPI-aware by default. That way, the worst-case-scenario with the program not handling DPI scaling is that the OS scales the entire window up, and everything looks blocky or a bit blurry. However, if we're telling the OS that we're handling DPI scaling, but the program isn't, and is also using pixel-based coordinates for drawing, we end up with apps that looks like this:

Which is broken to the point of being unusable.
@Osspial AFAIK "DPI awareness" the way you refer to it only exists on Windows.
I believe it was stated a few times that winit would explicitly not support non-dpi aware apps...
Furthermore, using the logical/physical pixels wrapping structs should make it rather difficult to accidentally be non-dpi aware, wouldn't it?
Alright, PR is up: #548
Most helpful comment
Since it seems likely that both logical and physical pixels will be part of the API, how about introducing a named type for each, instead of anonymous tuples? I will gladly take a broken build every time over runtime errors.