Skip to content

gtk: improve split sizing - #13414

Merged
jcollie merged 1 commit into
ghostty-org:mainfrom
dkinzler:gtk/better-splits
Aug 3, 2026
Merged

jcollie merged 1 commit 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.

@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
@@ -1269,10 +1249,60 @@ const SplitTreeSplit = extern struct {
};
paned.setPosition(desired_pos);
} 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.

@dkinzler
dkinzler requested a review from tristan957 July 22, 2026 17:35
@tristan957

Copy link
Copy Markdown
Member

Can you fix the commit message formatting for the last two commits?

@dkinzler
dkinzler force-pushed the gtk/better-splits branch from 8e9588c to ec9ff3d Compare July 28, 2026 16:38
@dkinzler

Copy link
Copy Markdown
Contributor Author

Done, sorry about that.

@tristan957

Copy link
Copy Markdown
Member

Can you merge the commits where it makes sense? For instance if they are fixing up things in a previous commit.

@dkinzler
dkinzler force-pushed the gtk/better-splits branch from ec9ff3d to 5c535bf Compare July 28, 2026 17:18
@dkinzler

Copy link
Copy Markdown
Contributor Author

Of course, that makes sense. Done.

@tristan957 tristan957 added this to the 1.4.0 milestone Jul 28, 2026
@tristan957

Copy link
Copy Markdown
Member

I added a milestone. I am not that familiar with the split tree code at the moment. It'll take me some time get at the bones of this change, but everything seems to make sense, and the video is definitely an improvement.

@jcollie jcollie left a comment

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.

This LGTM but it should be rebased on the latest main so that it's past the Zig 0.16 changes (the rebase is trivial).

For widget resizes the split ratio is now synced directly from the
propMaxPosition callback in the SplitTreeSplit widget, 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.

Moved logic to sync split ratio between gtk.Paned widget and split tree
into new syncSplitRatio function in the SplitTreeSplit widget. Added
debug assertions and log warnings to syncSplitRatio where we look up
the split tree.
@dkinzler
dkinzler force-pushed the gtk/better-splits branch from 5c535bf to 90da3ab Compare August 1, 2026 19:29
@dkinzler
dkinzler requested a review from jcollie August 1, 2026 19:33

@jcollie jcollie left a comment

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.

LGTM but let's see if @tristan957 has any further feedback.

@tristan957

Copy link
Copy Markdown
Member

LGTM but let's see if @tristan957 has any further feedback.

No feedback. Feel free to merge!

@jcollie
jcollie merged commit da581e0 into ghostty-org:main Aug 3, 2026
98 checks passed
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.

3 participants