Skip to content

Commit 53689ae

Browse files
committed
WinUIBackend: Get window HWND, fix display scaling support
1 parent 2f4df86 commit 53689ae

File tree

5 files changed

+175
-51
lines changed

5 files changed

+175
-51
lines changed

Sources/Gtk3Backend/Gtk3Backend.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ public final class Gtk3Backend: AppBackend {
279279
rootEnvironment: EnvironmentValues
280280
) -> EnvironmentValues {
281281
let windowScaleFactor = Int(gtk_widget_get_scale_factor(window.widgetPointer))
282-
return rootEnvironment.with(\.windowScaleFactor, windowScaleFactor)
282+
return rootEnvironment.with(\.windowScaleFactor, Double(windowScaleFactor))
283283
}
284284

285285
public func setWindowEnvironmentChangeHandler(

Sources/SwiftCrossUI/Environment/EnvironmentValues.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public struct EnvironmentValues {
3636
public var onSubmit: (() -> Void)?
3737

3838
/// The scale factor of the current window.
39-
public var windowScaleFactor: Int
39+
public var windowScaleFactor: Double
4040

4141
/// Called by view graph nodes when they resize due to an internal state
4242
/// change and end up changing size. Each view graph node sets its own
@@ -69,7 +69,7 @@ public struct EnvironmentValues {
6969
/// The backend's representation of the window that the current view is
7070
/// in, if any. This is a very internal detail that should never get
7171
/// exposed to users.
72-
var window: Any?
72+
package var window: Any?
7373
/// The backend in use. Mustn't change throughout the app's lifecycle.
7474
let backend: any AppBackend
7575

Sources/SwiftCrossUI/Views/Image.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ class _ImageChildren: ViewGraphNodeChildren {
162162
var imageWidget: AnyWidget
163163
var imageChanged = false
164164
var isContainerEmpty = true
165-
var lastScaleFactor = 1
165+
var lastScaleFactor: Double = 1
166166

167167
init<Backend: AppBackend>(backend: Backend) {
168168
container = AnyWidget(backend.createContainer())
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import CWinRT
2+
import WinAppSDK
3+
import WinSDK
4+
import WinUI
5+
import WindowsFoundation
6+
7+
public func getWindowIDFromWindow(_ hWnd: HWND?) -> WinAppSDK.WindowId {
8+
HWNDInterop.shared.getWindowIDFromWindow(hWnd)
9+
}
10+
11+
public func getWindowFromWindowId(_ windowID: WinAppSDK.WindowId) -> HWND? {
12+
HWNDInterop.shared.getWindowFromWindowId(windowID)
13+
}
14+
15+
extension WinAppSDK.AppWindow {
16+
/// Returns the window handle for the app window.
17+
public func getHWND() -> HWND? {
18+
HWNDInterop.shared.getWindowFromWindowId(id)
19+
}
20+
}
21+
22+
extension WinUI.Window {
23+
/// Returns the window handle for the window.
24+
///
25+
/// - Note: This is a relatively expensive operation, particularly due to its use
26+
/// of the `appWindow` getter. If an `AppWindow` is already available, prefer to
27+
/// use `getHWND()` on that instead; better yet, if the window handle will be used
28+
/// frequently, assign it to a stored property, as it will not change during the
29+
/// lifetime of the window.
30+
public func getHWND() -> HWND? {
31+
// The appWindow can become nil when a Window is closed.
32+
guard let appWindow else { return nil }
33+
return appWindow.getHWND()
34+
}
35+
}
36+
37+
private struct HWNDInterop {
38+
private typealias pfnGetWindowIdFromWindow = @convention(c) (
39+
HWND?, UnsafeMutablePointer<__x_ABI_CMicrosoft_CUI_CWindowId>?
40+
) -> HRESULT
41+
private typealias pfnGetWindowFromWindowId = @convention(c) (
42+
__x_ABI_CMicrosoft_CUI_CWindowId, UnsafeMutablePointer<HWND?>?
43+
) -> HRESULT
44+
private var hModule: HMODULE!
45+
private var getWindowIDFromWindow_impl: pfnGetWindowIdFromWindow!
46+
private var getWindowFromWindowID_impl: pfnGetWindowFromWindowId!
47+
48+
static let shared = HWNDInterop()
49+
50+
init() {
51+
"Microsoft.Internal.FrameworkUdk.dll".withCString(encodedAs: UTF16.self) {
52+
hModule = GetModuleHandleW($0)
53+
if hModule == nil {
54+
hModule = LoadLibraryW($0)
55+
}
56+
}
57+
58+
if let pfn = GetProcAddress(hModule, "Windowing_GetWindowIdFromWindow") {
59+
getWindowIDFromWindow_impl = unsafeBitCast(pfn, to: pfnGetWindowIdFromWindow.self)
60+
}
61+
62+
if let pfn = GetProcAddress(hModule, "Windowing_GetWindowFromWindowId") {
63+
getWindowFromWindowID_impl = unsafeBitCast(pfn, to: pfnGetWindowFromWindowId.self)
64+
}
65+
}
66+
67+
fileprivate func getWindowIDFromWindow(_ hWnd: HWND?) -> WinAppSDK.WindowId {
68+
var windowID = __x_ABI_CMicrosoft_CUI_CWindowId()
69+
let hr: HRESULT = getWindowIDFromWindow_impl(hWnd, &windowID)
70+
if hr != S_OK {
71+
fatalError("Unable to get window ID")
72+
}
73+
return .init(value: windowID.Value)
74+
}
75+
76+
fileprivate func getWindowFromWindowId(_ windowID: WinAppSDK.WindowId) -> HWND? {
77+
var hWnd: HWND?
78+
let hr: HRESULT = getWindowFromWindowID_impl(.from(swift: windowID), &hWnd)
79+
if hr != S_OK {
80+
fatalError("Unable to get window from window ID")
81+
}
82+
return hWnd
83+
}
84+
}

Sources/WinUIBackend/WinUIBackend.swift

Lines changed: 87 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import UWP
55
import WinAppSDK
66
import WinUI
77
import WindowsFoundation
8+
import WinSDK
89

910
// Many force tries are required for the WinUI backend but we don't really want them
1011
// anywhere else so just disable them for this file.
@@ -85,6 +86,9 @@ public final class WinUIBackend: AppBackend {
8586
// print a warning anyway.
8687
print("Warning: Failed to attach to parent console: \(error.localizedDescription)")
8788
}
89+
90+
// Ensure that the app's windows adapt to DPI changes at runtime
91+
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
8892

8993
WinUIApplication.callback = { application in
9094
// Toggle Switch has annoying default 'internal margins' (not Control
@@ -112,7 +116,7 @@ public final class WinUIBackend: AppBackend {
112116
WinUIApplication.main()
113117
}
114118

115-
public func createWindow(withDefaultSize size: SIMD2<Int>?) -> Window {
119+
public func createWindow(withDefaultSize size: SIMD2<Int>?) -> Window {
116120
let window = CustomWindow()
117121
windows.append(window)
118122
window.closed.addHandler { _, _ in
@@ -177,9 +181,12 @@ public final class WinUIBackend: AppBackend {
177181
}
178182

179183
public func setSize(ofWindow window: Window, to newSize: SIMD2<Int>) {
184+
let scaleFactor = window.scaleFactor
185+
let width = scaleFactor * Double(newSize.x)
186+
let height = scaleFactor * Double(newSize.y + CustomWindow.menuBarHeight)
180187
let size = UWP.SizeInt32(
181-
width: Int32(newSize.x),
182-
height: Int32(newSize.y + CustomWindow.menuBarHeight)
188+
width: Int32(width.rounded(.towardZero)),
189+
height: Int32(height.rounded(.towardZero))
183190
)
184191
try! window.appWindow.resizeClient(size)
185192
}
@@ -304,8 +311,9 @@ public final class WinUIBackend: AppBackend {
304311
public func computeWindowEnvironment(
305312
window: Window,
306313
rootEnvironment: EnvironmentValues
307-
) -> EnvironmentValues {
308-
// TODO: Record window scale factor in here
314+
) -> EnvironmentValues {
315+
// TODO: Compute window scale factor (easy enough, but we would also have to keep
316+
// it up-to-date then, which is kinda annoying for now)
309317
rootEnvironment
310318
}
311319

@@ -990,49 +998,49 @@ public final class WinUIBackend: AppBackend {
990998
}
991999
}
9921000

993-
// public func showOpenDialog(
994-
// fileDialogOptions: FileDialogOptions,
995-
// openDialogOptions: OpenDialogOptions,
996-
// window: Window?,
997-
// resultHandler handleResult: @escaping (DialogResult<[URL]>) -> Void
998-
// ) {
999-
// let picker = FileOpenPicker()
1000-
// // TODO: Associate the picker with a window. Requires some janky WinUI
1001-
// // Win32 interop kinda stuff I believe.
1002-
// if openDialogOptions.allowMultipleSelections {
1003-
// let promise = try! picker.pickMultipleFilesAsync()!
1004-
// promise.completed = { operation, status in
1005-
// guard
1006-
// status == .completed,
1007-
// let operation,
1008-
// let result = try? operation.getResults()
1009-
// else {
1010-
// return
1011-
// }
1012-
// print(result)
1013-
// }
1014-
// } else {
1015-
// let promise = try! picker.pickSingleFileAsync()!
1016-
// promise.completed = { operation, status in
1017-
// guard
1018-
// status == .completed,
1019-
// let operation,
1020-
// let result = try? operation.getResults()
1021-
// else {
1022-
// return
1023-
// }
1024-
// print(result)
1025-
// }
1026-
// }
1027-
// }
1001+
public func showOpenDialog(
1002+
fileDialogOptions: FileDialogOptions,
1003+
openDialogOptions: OpenDialogOptions,
1004+
window: Window?,
1005+
resultHandler handleResult: @escaping (DialogResult<[URL]>) -> Void
1006+
) {
1007+
let picker = FileOpenPicker()
1008+
// TODO: Associate the picker with a window. Requires some janky WinUI
1009+
// Win32 interop kinda stuff I believe.
1010+
if openDialogOptions.allowMultipleSelections {
1011+
let promise = try! picker.pickMultipleFilesAsync()!
1012+
promise.completed = { operation, status in
1013+
guard
1014+
status == .completed,
1015+
let operation,
1016+
let result = try? operation.getResults()
1017+
else {
1018+
return
1019+
}
1020+
print(result)
1021+
}
1022+
} else {
1023+
let promise = try! picker.pickSingleFileAsync()!
1024+
promise.completed = { operation, status in
1025+
guard
1026+
status == .completed,
1027+
let operation,
1028+
let result = try? operation.getResults()
1029+
else {
1030+
return
1031+
}
1032+
print(result)
1033+
}
1034+
}
1035+
}
10281036

1029-
// public func showSaveDialog(
1030-
// fileDialogOptions: FileDialogOptions,
1031-
// saveDialogOptions: SaveDialogOptions,
1032-
// window: Window?,
1033-
// resultHandler handleResult: @escaping (DialogResult<URL>) -> Void
1034-
// ) {
1035-
// }
1037+
public func showSaveDialog(
1038+
fileDialogOptions: FileDialogOptions,
1039+
saveDialogOptions: SaveDialogOptions,
1040+
window: Window?,
1041+
resultHandler handleResult: @escaping (DialogResult<URL>) -> Void
1042+
) {
1043+
}
10361044

10371045
public func createTapGestureTarget(wrapping child: Widget, gesture: TapGesture) -> Widget {
10381046
if gesture != .primary {
@@ -1215,6 +1223,34 @@ public class CustomWindow: WinUI.Window {
12151223
var menuBar = WinUI.MenuBar()
12161224
var child: WinUIBackend.Widget?
12171225
var grid: WinUI.Grid
1226+
var cachedAppWindow: WinAppSDK.AppWindow!
1227+
1228+
var scaleFactor: Double {
1229+
// I'm leaving this code here for future travellers. Be warned that this always
1230+
// seems to return 100% even if the scale factor is set to 125% in settings.
1231+
// Perhaps it's only the device's built-in default scaling? But that seems pretty
1232+
// useless, and isn't what the docs seem to imply.
1233+
//
1234+
// var deviceScaleFactor = SCALE_125_PERCENT
1235+
// _ = GetScaleFactorForMonitor(monitor, &deviceScaleFactor)
1236+
1237+
let hwnd = cachedAppWindow.getHWND()!
1238+
let monitor = MonitorFromWindow(hwnd, DWORD(bitPattern: MONITOR_DEFAULTTONEAREST))!
1239+
1240+
var x: UINT = 0
1241+
var y: UINT = 0
1242+
let result = GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &x, &y);
1243+
1244+
let windowScaleFactor: Double
1245+
if result == S_OK {
1246+
windowScaleFactor = Double(x) / Double(USER_DEFAULT_SCREEN_DPI)
1247+
} else {
1248+
print("Warning: Failed to get window scale factor, defaulting to 1.0")
1249+
windowScaleFactor = 1
1250+
}
1251+
1252+
return windowScaleFactor
1253+
}
12181254

12191255
public override init() {
12201256
grid = WinUI.Grid()
@@ -1232,6 +1268,10 @@ public class CustomWindow: WinUI.Window {
12321268
grid.children.append(menuBar)
12331269
WinUI.Grid.setRow(menuBar, 0)
12341270
self.content = grid
1271+
1272+
// Caching appWindow is apparently a good idea in terms of performance:
1273+
// https://github.com/thebrowsercompany/swift-winrt/issues/199#issuecomment-2611006020
1274+
cachedAppWindow = appWindow
12351275
}
12361276

12371277
public func setChild(_ child: WinUIBackend.Widget) {

0 commit comments

Comments
 (0)