Skip to content

Commit 27f5057

Browse files
committed
Fix weird Gtk3Backend rendering glitch (tracking down the cause was such a pain)
Long story short, Gtk3Backend.naturalSize(of:) is basically impossible to implement properly due to Gtk 3's design so I've had to just add a safety net to WindowGroupNode as a bit of a bandaid. The safety net prints a warning before attempting to recover so it at least shouldn't surprise anyone. Gtk3Backend is only for legacy support so I think this solution is fine. The issue this commit fixes is that if the window grows due to a view containing buttons appearing, all buttons would lose their labels until you trigger a full redraw of the window by clicking off it. The fact that buttons all lost their labels doesn't seem related to buttons causing the issue, since all buttons lost their labels, even the ones that had correct natural sizes and weren't causing the issue. I still have no clue why this size underestimation (and subsequent mis-sizing of the window) had the symptom of all buttons losing their labels temporarily; Gtk 3 is a temperamental beast.
1 parent a25cca5 commit 27f5057

File tree

6 files changed

+70
-22
lines changed

6 files changed

+70
-22
lines changed

Sources/Gtk3Backend/Gtk3Backend.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,11 @@ public final class Gtk3Backend: AppBackend {
8686

8787
public func setSize(ofWindow window: Window, to newSize: SIMD2<Int>) {
8888
let child = window.child! as! CustomRootWidget
89+
child.preemptAllocatedSize(
90+
allocatedWidth: newSize.x,
91+
allocatedHeight: newSize.y
92+
)
8993
window.size = Size(width: newSize.x, height: newSize.y)
90-
child.preemptAllocatedSize(allocatedWidth: newSize.x, allocatedHeight: newSize.y)
9194
}
9295

9396
public func setMinimumSize(ofWindow window: Window, to minimumSize: SIMD2<Int>) {

Sources/Gtk3CustomWidgets/gtk_custom_root_widget.c

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ static void gtk_custom_root_widget_class_init(GtkCustomRootWidgetClass *klass) {
1010
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1111
widget_class->get_preferred_width = gtk_custom_root_widget_get_preferred_width;
1212
widget_class->get_preferred_height = gtk_custom_root_widget_get_preferred_height;
13-
widget_class->size_allocate = gtk_custom_root_widget_allocate;
13+
widget_class->size_allocate = gtk_custom_root_widget_size_allocate;
1414
widget_class->get_request_mode = gtk_custom_root_widget_size_request_mode;
1515
widget_class->realize = gtk_custom_root_widget_realize;
1616
}
@@ -57,21 +57,15 @@ void gtk_custom_root_widget_get_preferred_height(
5757
*natural = max(root_widget->natural_height, root_widget->minimum_height);
5858
}
5959

60-
void gtk_custom_root_widget_allocate(
60+
void gtk_custom_root_widget_size_allocate(
6161
GtkWidget *widget,
6262
GtkAllocation *allocation
6363
) {
6464
GtkCustomRootWidget *root_widget = GTK_CUSTOM_ROOT_WIDGET(widget);
6565
gtk_widget_set_allocation(widget, allocation);
6666
gtk_widget_size_allocate(root_widget->child, allocation);
6767

68-
if (!root_widget->has_been_allocated) {
69-
if (allocation->width == root_widget->natural_width && allocation->height == root_widget->natural_height) {
70-
root_widget->allocated_width = allocation->width;
71-
root_widget->allocated_height = allocation->height;
72-
return;
73-
}
74-
} else if (allocation->width == root_widget->allocated_width && allocation->height == root_widget->allocated_height) {
68+
if (allocation->width == root_widget->allocated_width && allocation->height == root_widget->allocated_height) {
7569
return;
7670
}
7771

Sources/Gtk3CustomWidgets/include/gtk_custom_root_widget.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ void gtk_custom_root_widget_get_preferred_height(
5151
int *natural
5252
);
5353

54-
void gtk_custom_root_widget_allocate(
54+
void gtk_custom_root_widget_size_allocate(
5555
GtkWidget *widget,
5656
GtkAllocation *allocation
5757
);

Sources/SwiftCrossUI/LayoutSystem.swift

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,7 @@ public enum LayoutSystem {
109109
if size.size != .zero || size.participateInStackLayoutsWhenEmpty {
110110
print("warning: Hidden view became visible on second update. Layout may break.")
111111
}
112-
renderedChildren[index] = ViewSize(
113-
size: .zero,
114-
idealSize: .zero,
115-
minimumWidth: 0,
116-
minimumHeight: 0,
117-
maximumWidth: 0,
118-
maximumHeight: 0,
119-
participateInStackLayoutsWhenEmpty: false
120-
)
112+
renderedChildren[index] = .hidden
121113
continue
122114
}
123115

Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,64 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
130130
)
131131
}
132132

133-
_ = viewGraph.update(
133+
let finalContentSize = viewGraph.update(
134134
with: newScene?.body,
135135
proposedSize: proposedWindowSize,
136136
environment: environment,
137137
dryRun: false
138138
)
139139

140+
// The Gtk 3 backend has some broken sizing code that can't really be
141+
// fixed due to the design of Gtk 3. Our layout system underestimates
142+
// the size of the new view due to the button not being in the Gtk 3
143+
// widget hierarchy yet (which prevents Gtk 3 from computing the
144+
// natural sizes of the new buttons). One fix seems to be removing
145+
// view size reuse (currently the second check in ViewGraphNode.update)
146+
// and I'm not exactly sure why, but that makes things awfully slow.
147+
// The other fix is to add an alternative path to
148+
// Gtk3Backend.naturalSize(of:) for buttons that moves non-realized
149+
// buttons to a secondary window before measuring their natural size,
150+
// but that's super janky, easy to break if the button in the real
151+
// window is inheriting styles from its ancestors, and I'm not sure
152+
// how to hide the window (it's probably terrible for performance too).
153+
//
154+
// I still have no clue why this size underestimation (and subsequent
155+
// mis-sizing of the window) had the symptom of all buttons losing
156+
// their labels temporarily; Gtk 3 is a temperamental beast.
157+
//
158+
// Anyway, Gtk3Backend isn't really intended to be a recommended
159+
// backend so I think this is a fine solution for now (people should
160+
// only use Gtk3Backend if they can't use GtkBackend).
161+
if finalContentSize != contentSize {
162+
print(
163+
"""
164+
warning: Final window content size didn't match dry-run size. This is a sign that
165+
either view size caching is broken or that backend.naturalSize(of:) is
166+
broken (or both).
167+
-> contentSize: \(contentSize)
168+
-> finalContentSize: \(finalContentSize)
169+
"""
170+
)
171+
172+
// Give the view graph one more chance to sort itself out to fail
173+
// as gracefully as possible.
174+
let newWindowSize = computeNewWindowSize(
175+
currentProposedSize: proposedWindowSize,
176+
backend: backend,
177+
contentSize: finalContentSize,
178+
environment: environment
179+
)
180+
181+
if let newWindowSize {
182+
return update(
183+
scene,
184+
proposedWindowSize: newWindowSize,
185+
backend: backend,
186+
environment: environment
187+
)
188+
}
189+
}
190+
140191
if scene.resizability.isResizable {
141192
backend.setMinimumSize(
142193
ofWindow: window,

Sources/SwiftCrossUI/Views/Button.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,15 @@ public struct Button: ElementaryView, View {
2222
backend: Backend,
2323
dryRun: Bool
2424
) -> ViewSize {
25-
// TODO: Implement button sizing within SwiftCrossUI so that we can properly implement `dryRun`.
25+
// TODO: Implement button sizing within SwiftCrossUI so that we can properly implement
26+
// `dryRun`. Relying on the backend for button sizing also makes the Gtk 3 backend
27+
// basically impossible to implement correctly, hence the
28+
// `finalContentSize != contentSize` check in WindowGroupNode to catch any weird
29+
// behaviour. Without that extra safety net logic, buttons all end up label-less
30+
// whenever the window grows due to a view containing buttons appearing. Not sure
31+
// why all buttons lose their labels (until you click off the window, forcing it to
32+
// refresh), but the reason Gtk 3 doesn't like it is that the window gets set smaller
33+
// than its content I think.
2634
backend.updateButton(widget, label: label, action: action, environment: environment)
2735
return ViewSize(fixedSize: backend.naturalSize(of: widget))
2836
}

0 commit comments

Comments
 (0)