Skip to content

gtk: improve split sizing#13414

Open
dkinzler wants to merge 3 commits into
ghostty-org:mainfrom
dkinzler:gtk/better-splits
Open

gtk: improve split sizing#13414
dkinzler wants to merge 3 commits into
ghostty-org:mainfrom
dkinzler:gtk/better-splits

Conversation

@dkinzler

Copy link
Copy Markdown
Contributor

This PR improves the way splits/surfaces are sized in the GTK app, which eliminates flickering and slightly improves performance.

Fixes #13328, #12709, #11187.
Related #8208 (closed) but some later comments mention flickering issues persisting.
Builds on top of the changes in #12698.

Previously an idle callback was used to sync the split ratio between the GTK widget tree and the split tree data structure that represents the split layout. The widget tree contains a SplitTreeSplit widget, which wraps a GtkPaned widget, for every split. During size allocation a GtkPaned widget first computes the initial position of the divider and thereby the size for its two children. We get notified of that position (and the max possible position) via the propPosition/propMaxPosition callbacks in SplitTreeSplit and set up an idle callback (the onIdle function) to update the position if it does not match the desired split ratio. Since the initial position is often not correct, especially in nested layouts or if the ratio is not 0.5, a surface will first be shown with the wrong size for a few frames until the idle callback runs and corrects the sizing. In nested layouts it might take multiple rounds of size allocation and idle callbacks until every surface gets the correct size. This causes flickering as widgets eventually snap to another size, which is especially noticeable if the layout changes quickly e.g. when resizing a split using keybinds.

To fix this, the divider position will now be corrected directly from the propMaxPosition callback, which runs during GTK size allocation, right after a GtkPaned computes the initial position and right before it uses the position to allocate sizes for its two children. With this change every surface will be sized correctly during the first round of size allocation.
The idle callback is still used to update the ratio in the split tree when a split is resized by manually dragging the divider in the UI. The logic to sync the split ratio was moved to the new syncSplitRatio function which is called from both propMaxPosition and onIdle.

This is kind of hacky, but I reviewed the GTK source code in detail to verify that this is safe (see the various code comments for more details). I also tested extensively on both Hyprland and KDE Plasma: creating deeply nested layouts, resizing with both keybinds and dragging dividers by hand, with multiple tabs, resizing the entire window, resizing entire subtrees to 0 and back. Everything seems to work fine.

For performance testing I used sysprof which can also collect GTK stats. When creating/deleting/resizing splits I can measure a slight but consistent increase in GTK FPS (+5 to 10) on my system. Other than that CPU usage and FPS seem to be the same before and after. I guess this makes sense, while we added a bit of work to the GTK loop during size allocation, we avoid surfaces being resized.

For the flickering, here's a side-by-side comparison. Left is before the changes, right is after.

comparison.mp4

AI Disclosure: no AI was used.

Moved logic to sync split ratio between gtk.Paned widget and split tree into new syncSplitRatio function in SplitTreeSplit widget.

For widget resizes the split ratio is now synced directly from the propMaxPosition callback instead of an idle callback. With this change all surfaces will be sized correctly from the start, in a single round of size allocation in GTK. Previously, surfaces would initially be shown with the wrong size for a few frames until the idle callback ran, resulting in visible flickering. This was especially visible when resizing a split quickly by holding down the resize keybind.
@dkinzler
dkinzler requested a review from a team as a code owner July 22, 2026 11:47
Comment thread src/apprt/gtk/class/split_tree.zig Outdated
} else {
// If only position changed, this is a manual human update and
// we need to write our update back to the tree.
// Update ratio of the split tree node. This is the most dangerous

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add some debug asserts here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't hurt, added assertions for all the assumptions in the comment.

Comment thread src/apprt/gtk/class/split_tree.zig Outdated
// Sync split ratio between the gtk.Paned widget and the split tree.
// If to_paned=true, update the gtk.Paned widget to match the ratio
// in the split tree and vice-versa for to_paned=false.
fn syncSplitRatio(self: *Self, to_paned: bool) void {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love to_paned. Maybe a well named enum would be easier to understand when reading the code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to use an enum, that's definitely better. Alternatively I also thought about just having a function that returns all the parameters (min, max, position, current_ratio etc.) and then doing the rest directly in propMaxPosition/onIdle, but that is also kind of awkward.

…Direction enum argument to specify whether we sync from widget to split tree or vice-versa. This make the code more readable. Added debug assertions to syncSplitRatio where we look up the split tree.
@dkinzler
dkinzler requested a review from tristan957 July 22, 2026 17:35
…get or data model cannot be found, we would continue running with a null pointer in builds without safety checks. Using unreachable was the wrong choice.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants