Skip to content

Commit fd4f296

Browse files
committed
Gtk3Backend: Re-scale images when window changes DPI (changing monitors)
This should complete HiDPI display support for Gtk3Backend
1 parent b4291dc commit fd4f296

File tree

10 files changed

+156
-3
lines changed

10 files changed

+156
-3
lines changed

Sources/AppKitBackend/AppKitBackend.swift

+16
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public final class AppKitBackend: AppBackend {
2020
public let defaultPaddingAmount = 10
2121
public let requiresToggleSwitchSpacer = false
2222
public let defaultToggleStyle = ToggleStyle.button
23+
public let requiresImageUpdateOnScaleFactorChange = false
2324

2425
public var scrollBarWidth: Int {
2526
// We assume that all scrollers have their controlSize set to `.regular` by default.
@@ -270,6 +271,21 @@ public final class AppKitBackend: AppBackend {
270271
}
271272
}
272273

274+
public func computeWindowEnvironment(
275+
window: Window,
276+
rootEnvironment: EnvironmentValues
277+
) -> EnvironmentValues {
278+
// TODO: Record window scale factor in here
279+
rootEnvironment
280+
}
281+
282+
public func setWindowEnvironmentChangeHandler(
283+
of window: Window,
284+
to action: @escaping () -> Void
285+
) {
286+
// TODO: Notify when window scale factor changes
287+
}
288+
273289
public func setIncomingURLHandler(to action: @escaping (URL) -> Void) {
274290
appDelegate.onOpenURLs = { urls in
275291
for url in urls {

Sources/Gtk3/Widgets/ApplicationWindow.swift

+19
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,26 @@ public class ApplicationWindow: Window {
99
self.init(
1010
gtk_application_window_new(application.applicationPointer)
1111
)
12+
13+
let handler2:
14+
@convention(c) (
15+
UnsafeMutableRawPointer,
16+
gint,
17+
UnsafeMutableRawPointer
18+
) -> Void = { _, value1, data in
19+
SignalBox1<gint>.run(data, value1)
20+
}
21+
22+
addSignal(
23+
name: "notify::scale-factor",
24+
handler: gCallback(handler2)
25+
) { [weak self] (scaleFactor: gint) in
26+
guard let self = self else { return }
27+
self.notifyScaleFactor?(Int(scaleFactor))
28+
}
1229
}
1330

31+
public var notifyScaleFactor: ((Int) -> Void)?
32+
1433
@GObjectProperty(named: "show-menubar") public var showMenuBar: Bool
1534
}

Sources/Gtk3Backend/Gtk3Backend.swift

+18
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public final class Gtk3Backend: AppBackend {
2929
public let requiresToggleSwitchSpacer = false
3030
public let scrollBarWidth = 0
3131
public let defaultToggleStyle = ToggleStyle.button
32+
public let requiresImageUpdateOnScaleFactorChange = true
3233

3334
var gtkApp: Application
3435

@@ -272,6 +273,23 @@ public final class Gtk3Backend: AppBackend {
272273
// TODO: React to theme changes
273274
}
274275

276+
public func computeWindowEnvironment(
277+
window: Window,
278+
rootEnvironment: EnvironmentValues
279+
) -> EnvironmentValues {
280+
let windowScaleFactor = Int(gtk_widget_get_scale_factor(window.widgetPointer))
281+
return rootEnvironment.with(\.windowScaleFactor, windowScaleFactor)
282+
}
283+
284+
public func setWindowEnvironmentChangeHandler(
285+
of window: Window,
286+
to action: @escaping () -> Void
287+
) {
288+
window.notifyScaleFactor = { _ in
289+
action()
290+
}
291+
}
292+
275293
public func setIncomingURLHandler(to action: @escaping (URL) -> Void) {
276294
gtkApp.onOpen = { urls in
277295
for url in urls {

Sources/GtkBackend/GtkBackend.swift

+16
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public final class GtkBackend: AppBackend {
2929
public let scrollBarWidth = 0
3030
public let requiresToggleSwitchSpacer = false
3131
public let defaultToggleStyle = ToggleStyle.button
32+
public let requiresImageUpdateOnScaleFactorChange = false
3233

3334
var gtkApp: Application
3435

@@ -262,6 +263,21 @@ public final class GtkBackend: AppBackend {
262263
// TODO: React to theme changes
263264
}
264265

266+
public func computeWindowEnvironment(
267+
window: Window,
268+
rootEnvironment: EnvironmentValues
269+
) -> EnvironmentValues {
270+
// TODO: Record window scale factor in here
271+
rootEnvironment
272+
}
273+
274+
public func setWindowEnvironmentChangeHandler(
275+
of window: Window,
276+
to action: @escaping () -> Void
277+
) {
278+
// TODO: Notify when window scale factor changes
279+
}
280+
265281
public func setIncomingURLHandler(to action: @escaping (URL) -> Void) {
266282
gtkApp.onOpen = { urls in
267283
for url in urls {

Sources/SwiftCrossUI/Backend/AppBackend.swift

+26
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ public protocol AppBackend {
7575
var requiresToggleSwitchSpacer: Bool { get }
7676
/// The default style for toggles.
7777
var defaultToggleStyle: ToggleStyle { get }
78+
/// If `true`, all images in a window will get updated when the window's
79+
/// scale factor changes (``EnvironmentValues/windowScaleFactor``).
80+
///
81+
/// Backends based on modern UI frameworks can usually get away with setting
82+
/// this to `false`, but backends such as `Gtk3Backend` have to set this to
83+
/// `true` to properly support HiDPI (aka Retina) displays because they
84+
/// manually rescale the image meaning that it must get rescaled when the
85+
/// scale factor changes.
86+
var requiresImageUpdateOnScaleFactorChange: Bool { get }
7887

7988
/// Often in UI frameworks (such as Gtk), code is run in a callback
8089
/// after starting the app, and hence this generic root window creation
@@ -164,6 +173,23 @@ public protocol AppBackend {
164173
/// may or may not override the previous handler.
165174
func setRootEnvironmentChangeHandler(to action: @escaping () -> Void)
166175

176+
/// Computes a window's environment based off the root environment. This may involve
177+
/// updating ``EnvironmentValues/windowScaleFactor`` etc.
178+
func computeWindowEnvironment(
179+
window: Window,
180+
rootEnvironment: EnvironmentValues
181+
) -> EnvironmentValues
182+
/// Sets the handler to be notified when the window's contribution to the
183+
/// environment may have to be recomputed. Use this for things such as
184+
/// updating a window's scale factor in the environment when the window
185+
/// changes displays.
186+
///
187+
/// In future this may be useful for color space handling.
188+
func setWindowEnvironmentChangeHandler(
189+
of window: Window,
190+
to action: @escaping () -> Void
191+
)
192+
167193
/// Sets the handler for URLs directed to the application (e.g. URLs
168194
/// associated with a custom URL scheme).
169195
func setIncomingURLHandler(to action: @escaping (URL) -> Void)

Sources/SwiftCrossUI/Environment/EnvironmentValues.swift

+4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public struct EnvironmentValues {
3535
/// pressing Enter/Return).
3636
public var onSubmit: (() -> Void)?
3737

38+
/// The scale factor of the current window.
39+
public var windowScaleFactor: Int
40+
3841
/// Called by view graph nodes when they resize due to an internal state
3942
/// change and end up changing size. Each view graph node sets its own
4043
/// handler when passing the environment on to its children, setting up
@@ -116,6 +119,7 @@ public struct EnvironmentValues {
116119
font = .system(size: 12)
117120
multilineTextAlignment = .leading
118121
colorScheme = .light
122+
windowScaleFactor = 1
119123
window = nil
120124
}
121125

Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift

+16-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,21 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
5252
self.scene,
5353
proposedWindowSize: newSize,
5454
backend: backend,
55-
environment: parentEnvironment,
55+
environment: self.parentEnvironment,
56+
windowSizeIsFinal:
57+
!backend.isWindowProgrammaticallyResizable(window)
58+
)
59+
}
60+
61+
backend.setWindowEnvironmentChangeHandler(of: window) { [weak self] in
62+
guard let self else {
63+
return
64+
}
65+
_ = self.update(
66+
self.scene,
67+
proposedWindowSize: backend.size(ofWindow: window),
68+
backend: backend,
69+
environment: self.parentEnvironment,
5670
windowSizeIsFinal:
5771
!backend.isWindowProgrammaticallyResizable(window)
5872
)
@@ -107,7 +121,7 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
107121
}
108122

109123
let environment =
110-
environment
124+
backend.computeWindowEnvironment(window: window, rootEnvironment: environment)
111125
.with(\.onResize) { [weak self] _ in
112126
guard let self = self else { return }
113127
// TODO: Figure out whether this would still work if we didn't recompute the

Sources/SwiftCrossUI/Views/Image.swift

+8-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,12 @@ public struct Image: TypeSafeView, View {
114114
}
115115

116116
let hasResized = children.cachedImageDisplaySize != size.size
117-
if !dryRun && (children.imageChanged || hasResized) {
117+
if !dryRun
118+
&& (children.imageChanged
119+
|| hasResized
120+
|| (backend.requiresImageUpdateOnScaleFactorChange
121+
&& children.lastScaleFactor != environment.windowScaleFactor))
122+
{
118123
if let image {
119124
backend.updateImageView(
120125
children.imageWidget.into(),
@@ -135,6 +140,7 @@ public struct Image: TypeSafeView, View {
135140
children.isContainerEmpty = true
136141
}
137142
children.imageChanged = false
143+
children.lastScaleFactor = environment.windowScaleFactor
138144
}
139145

140146
children.cachedImageDisplaySize = size.size
@@ -156,6 +162,7 @@ class _ImageChildren: ViewGraphNodeChildren {
156162
var imageWidget: AnyWidget
157163
var imageChanged = false
158164
var isContainerEmpty = true
165+
var lastScaleFactor = 1
159166

160167
init<Backend: AppBackend>(backend: Backend) {
161168
container = AnyWidget(backend.createContainer())

Sources/UIKitBackend/UIKitBackend.swift

+17
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public final class UIKitBackend: AppBackend {
1919
public let defaultTableRowContentHeight = -1
2020
public let defaultTableCellVerticalPadding = -1
2121

22+
public let requiresImageUpdateOnScaleFactorChange = false
23+
2224
var onTraitCollectionChange: (() -> Void)?
2325

2426
private let appDelegateClass: ApplicationDelegate.Type
@@ -87,6 +89,21 @@ public final class UIKitBackend: AppBackend {
8789
onTraitCollectionChange = action
8890
}
8991

92+
public func computeWindowEnvironment(
93+
window: Window,
94+
rootEnvironment: EnvironmentValues
95+
) -> EnvironmentValues {
96+
// TODO: Record window scale factor in here
97+
rootEnvironment
98+
}
99+
100+
public func setWindowEnvironmentChangeHandler(
101+
of window: Window,
102+
to action: @escaping () -> Void
103+
) {
104+
// TODO: Notify when window scale factor changes
105+
}
106+
90107
public func runInMainThread(action: @escaping () -> Void) {
91108
DispatchQueue.main.async(execute: action)
92109
}

Sources/WinUIBackend/WinUIBackend.swift

+16
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public final class WinUIBackend: AppBackend {
3737
public let defaultPaddingAmount = 10
3838
public let requiresToggleSwitchSpacer = false
3939
public let defaultToggleStyle = ToggleStyle.button
40+
public let requiresImageUpdateOnScaleFactorChange = false
4041

4142
public var scrollBarWidth: Int {
4243
12
@@ -280,6 +281,21 @@ public final class WinUIBackend: AppBackend {
280281
internalState.themeChangeAction = action
281282
}
282283

284+
public func computeWindowEnvironment(
285+
window: Window,
286+
rootEnvironment: EnvironmentValues
287+
) -> EnvironmentValues {
288+
// TODO: Record window scale factor in here
289+
rootEnvironment
290+
}
291+
292+
public func setWindowEnvironmentChangeHandler(
293+
of window: Window,
294+
to action: @escaping () -> Void
295+
) {
296+
// TODO: Notify when window scale factor changes
297+
}
298+
283299
public func setIncomingURLHandler(to action: @escaping (URL) -> Void) {
284300
print("Implement set incoming url handler")
285301
// TODO

0 commit comments

Comments
 (0)