Debugging a GNOME Shell layout collapse: how a 4,294,967,296 px preferred height collapsed St.ScrollView to 1 px
Tested Environment:
- OS: Ubuntu 22.04.5 LTS (Linux x86_64)
- Runtime: GNOME Shell 42.9 (GJS 1.72.4), Mutter 42.0+
- Session: Wayland & X11
Imagine this scenario: you are developing an extension or desktop UI component for GNOME Shell using JavaScript (GJS). The extension loads cleanly with zero errors in journalctl. All GObjects are instantiated, icons are built, but on the screen... absolute void.
When you log the actor geometry to the console, you see an unusual output:
appsScroll=1
appsBox=1
status=4294967040
The parent St.ScrollView container has collapsed to a height of 1px, turning your interface into an invisible line across the screen. Meanwhile, its child grid actor claims its natural height is... 4,294,967,296 pixels (over 4 million kilometers — more than ten times the distance from Earth to the Moon!).
In this article, we will walk through the debugging investigation: from recursively traversing the Clutter Scene Graph to dissecting geometry allocation clamping and floating-point (gfloat) step boundaries near $2^{32}$.
1. Diagnostics: Locating the "Astronomical" Actor
When an element vanishes in Clutter/St, the initial hypothesis is usually an opacity issue (opacity = 0), visibility toggle (visible = false), or a z-index clipping problem. However, checking actor state confirms that elements exist in the Scene Graph.
To identify which container breaks layout calculations, we write a recursive helper function to inspect the Clutter Actor Tree:
function inspectActorTree(actor, depth = 0) {
// In get_preferred_height(-1), -1 represents an unconstrained width query (for_width = -1)
let [minH, natH] = actor.get_preferred_height(-1);
let alloc = actor.get_allocation_box();
let name = actor.get_name() || actor.constructor.name;
console.log(`${' '.repeat(depth * 2)}${name}: minH=${minH}, natH=${natH}, allocH=${alloc.get_height()}`);
actor.get_children().forEach(child => inspectActorTree(child, depth + 1));
}
Running this inspection tool outputs the critical clue:
StBoxLayout (parent): minH=0, natH=4294967296, allocH=600
StScrollView: minH=1, natH=4294967296, allocH=1 <-- Root cause container!
ChildGridActor: minH=0, natH=4294967296, allocH=0
The child grid ChildGridActor reports a natural height natH = 4294967296. Seeing this astronomical request, the parent St.ScrollView collapses to 1px.
2. Observed Facts & Evidence Breakdown
Let's separate confirmed observations from logical deductions.
1. The Input Parameter -1 (for_width)
In Clutter API, clutter_actor_get_preferred_height(actor, -1, &min_h, &nat_h) uses -1 as an unconstrained query flag (querying height with no width constraint). If a child actor lacks explicit min-width/min-height constraints and encounters conflicting alignment flags, preferred size calculations can return invalid or sentinel values.
2. Why Does the Container Collapse to Exactly 1px?
In Clutter's layout allocation engine (clutter_actor_allocate), allocation box heights are clamped to minimum bounds:
$$\text{allocated_height} = \max(\text{min_height}, \text{calculated_space})$$
-
StScrollViewdefines a minimum preferred heightminH = 1px(its baseline padding/border constraint). - When parent
ClutterBoxLayoutattempted to distribute available monitor height across siblings with an astronomical request ($4,294,967,296\text{ px}$), remaining space became zero or negative. - Clutter clamped the child allocation box to its declared minimum size — exactly 1px.
3. gfloat Precision at $2^{32}$ and Logged Values
In Clutter C layout math (clutter/clutter-box-layout.c), coordinates and sizes pass through gfloat (IEEE 754 float32).
In the $[2^{31}, 2^{32})$ range, single-precision floats have a representability step (ULP) of 256:
- $2^{32} = 4,294,967,296$
- $2^{32} - 256 = 4,294,967,040$
The logged values — 4,294,967,296 and 4,294,967,040 — match representable gfloat float32 numbers near the $2^{32}$ limit.
Investigation Boundary: These values were recorded in a GJS geometry inspection dump. The exact C call chain inside Mutter leading to the initial $2^{32}$ overflow remains a working hypothesis requiring deeper
gdbsource tracing.
3. Disproven Workarounds (Failed Approaches)
Before finding the correct fix, 3 plausible workarounds were tested, each creating regressions:
❌ Failed Approach #1: Hardcoding min-height in CSS
.my-scroll-view {
min-height: 300px;
}
-
Why it failed: The 1px collapse stopped, but
ScrollViewlost responsiveness: it stopped adapting dynamically to screen resolution changes and clipped overflowing content.
❌ Failed Approach #2: Invoking actor.queue_relayout() in Constructor
_init() {
super._init();
// Attempting to force relayout during construction
this.queue_relayout();
}
-
Why it failed: Calling
queue_relayout()inside_init()while GObject properties were partially initialized triggered a recursive layout cycle during startup. This resulted in an emptyAppFavoritesgrid race condition on boot.
❌ Failed Approach #3: Competing Geometry Owners
Setting actor geometry simultaneously from JavaScript (actor.set_width(...)) and CSS (stylesheet.css) created conflicting layout loops where JS and CSS layout managers continuously overwrote allocation boxes.
4. The Solution
Workaround vs Production Adaptive Fix
To test size constraint sensitivity during diagnosis, setting hardcoded bounds acts as an illustrative workaround:
grid.set_size(200, 300); // Diagnostic workaround to confirm constraint sensitivity
For the production adaptive fix:
- Single Geometry Owner: Manage dimensions and expansion in one location (JS via Clutter actor flags).
-
Align Container Constraints: Remove conflicting
x_alignandy_alignparameters from intermediate wrappers. -
Use Expansion Flags: Use
x_expand: trueandy_expand: trueso the parent layout manager handles dynamic sizing cleanly.
- const grid = new St.Widget({
- x_align: Clutter.ActorAlign.CENTER,
- y_align: Clutter.ActorAlign.FILL
- });
- grid.queue_relayout();
+ const grid = new St.Widget({
+ style_class: 'app-grid',
+ x_expand: true,
+ y_expand: true,
+ });
5. Verification
1. Repository Validation
Run bash verify.sh to check JS syntax across reproduction/, broken/, and fixed/, verify metadata structure, and execute tools/validate_cases.py:
bash cases/gnome-shell/st-boxlayout-invalid-natural-height/verify.sh
2. Runtime Verification in GNOME Shell
- Install and enable the extension in a GNOME Shell 42.9 session (Ubuntu 22.04.5 LTS).
- Open the side panel containing
St.ScrollView. - Confirm
appsScrollallocation height is $> 100\text{px}$ and icons render cleanly without collapsing.
Conclusion
The failure is real: a child actor reported an enormous preferred height near the 32-bit unsigned/float boundary, leaving St.ScrollView clamped to its minimum 1px allocation limit.
The practical fix was to align geometry rules and pass x_expand/y_expand cleanly to a single geometry owner. The exact low-level C mechanism generating $2^{32}$ remains an open topic for further gdb tracing.
Top comments (0)