patch 9.2.0334: GTK: window geometry shrinks with with client-side decorations
Commit:
https://github.com/vim/vim/commit/dd40b1af5b5800cab81cb4b6566f028282e74031
Author: Gary Johnson <
gary...@spocom.com>
Date: Fri Apr 10 21:23:38 2026 +0000
patch 9.2.0334: GTK: window geometry shrinks with with client-side decorations
Problem: On GTK3 with client-side decorations the window opens with
wrong &columns/&lines, and each :tabnew/:tabclose cycle
shrinks the size further.
Solution: Measure and compensate for the CSD frame offset, discard
spurious configure events from tabline show/hide
(Gary Johnson).
closes: #19853
Co-authored-by: Copilot <
22355621...@users.noreply.github.com>
Signed-off-by: Gary Johnson <
gary...@spocom.com>
Signed-off-by: Christian Brabandt <
c...@256bit.org>
diff --git a/src/gui.c b/src/gui.c
index 1d60cd7f3..f62bf697e 100644
--- a/src/gui.c
+++ b/src/gui.c
@@ -1659,9 +1659,11 @@ again:
gui_may_resize_shell(void)
{
if (new_pixel_height)
+ {
// careful: gui_resize_shell() may postpone the resize again if we
// were called indirectly by it
gui_resize_shell(new_pixel_width, new_pixel_height);
+ }
}
int
@@ -3737,12 +3739,24 @@ gui_init_which_components(char_u *oldval UNUSED)
// Don't do this while starting up though.
// Don't change Rows when adding menu/toolbar/tabline.
// Don't change Columns when adding vertical toolbar.
- if (!gui.starting && need_set_size != (RESIZE_VERT | RESIZE_HOR))
- (void)char_avail();
- if ((need_set_size & RESIZE_VERT) == 0)
- Rows = prev_Rows;
- if ((need_set_size & RESIZE_HOR) == 0)
- Columns = prev_Columns;
+ {
+ // Save the size gui_set_shellsize() determined before
+ // char_avail() processes spurious configure events that may
+ // corrupt Rows/Columns.
+ long post_Rows = Rows;
+ long post_Columns = Columns;
+
+ if (!gui.starting && need_set_size != (RESIZE_VERT | RESIZE_HOR))
+ (void)char_avail();
+ if ((need_set_size & RESIZE_VERT) == 0)
+ Rows = prev_Rows;
+ else
+ Rows = post_Rows;
+ if ((need_set_size & RESIZE_HOR) == 0)
+ Columns = prev_Columns;
+ else
+ Columns = post_Columns;
+ }
#endif
}
// When the console tabline appears or disappears the window positions
@@ -3793,14 +3807,29 @@ gui_update_tabline(void)
out_flush();
if (!showit != !shown)
+ {
+ // Save Rows/Columns before showing/hiding the tabline.
+ // gui_mch_show_tabline() processes GTK events (via
+ // gui_mch_update) which may trigger a spurious configure event
+ // that temporarily corrupts Rows. Restore the original values so
+ // that gui_set_shellsize() uses the correct row/column count.
+ int save_Rows = Rows;
+ int save_Columns = Columns;
+
gui_mch_show_tabline(showit);
+ Rows = save_Rows;
+ Columns = save_Columns;
+
+ // Resize the outer window to compensate for the tabline height
+ // change. This is also needed when called from draw_tabline()
+ // (e.g. after :tabclose), where no caller will do the resize.
+ // When called from gui_init_which_components() the subsequent
+ // gui_set_shellsize() call will redo this to the same dimensions,
+ // which is harmless.
+ gui_set_shellsize(FALSE, showit, RESIZE_VERT);
+ }
if (showit != 0)
gui_mch_update_tabline();
-
- // When the tabs change from hidden to shown or from shown to
- // hidden the size of the text area should remain the same.
- if (!showit != !shown)
- gui_set_shellsize(FALSE, showit, RESIZE_VERT);
}
}
diff --git a/src/gui_gtk_x11.c b/src/gui_gtk_x11.c
index 21e8768ab..5a9645a56 100644
--- a/src/gui_gtk_x11.c
+++ b/src/gui_gtk_x11.c
@@ -408,11 +408,13 @@ static int using_gnome = 0;
* Width and height are of gui.mainwin.
*/
typedef struct resize_history {
- int used; // If true, can't match for discard. Only matches once.
+ int used; // If true, can't match for discard. Only matches once.
int width;
int height;
+ int from_shellsize; // TRUE if recorded by gui_mch_set_shellsize (not
+ // a pre-record from gui_mch_show_tabline).
# ifdef ENABLE_RESIZE_HISTORY_LOG
- int seq; // for ch_log messages
+ int seq; // for ch_log messages
# endif
struct resize_history *next;
} resize_hist_T;
@@ -422,13 +424,28 @@ static resize_hist_T *latest_resize_hist;
// list of stale resize requests
static resize_hist_T *old_resize_hists;
+// On Wayland (and GTK3 with CSD), gtk_window_resize(w, h) results in
+// gtk_window_get_size() returning (w - csd_w, h - csd_h). These offsets are
+// computed once from the first confirmed resize response and applied to all
+// subsequent gtk_window_resize() calls so the form gets the intended size.
+static int mch_csd_width = 0;
+static int mch_csd_height = 0;
+
+// Expected form widget width for the most recent gui_mch_set_shellsize()
+// call (set only after mch_csd_width is known). Used in
+// form_configure_event() to clamp slightly-narrow configure responses that
+// result from CSD under-compensation, preventing column loss.
+static int mch_pending_form_w = 0;
+
/*
* Used when calling gtk_window_resize().
* Create a resize request history item, put previous request on stale list.
* Width/height are the size of the request for the gui.mainwin.
+ * from_shellsize is TRUE when called from gui_mch_set_shellsize (vs a
+ * stale pre-record from gui_mch_show_tabline).
*/
static void
-alloc_resize_hist(int width, int height)
+alloc_resize_hist(int width, int height, int from_shellsize)
{
// alloc a new resize hist, save current in list of old history
resize_hist_T *prev_hist = latest_resize_hist;
@@ -436,6 +453,7 @@ alloc_resize_hist(int width, int height)
new_hist->width = width;
new_hist->height = height;
+ new_hist->from_shellsize = from_shellsize;
latest_resize_hist = new_hist;
// previous hist item becomes head of list
@@ -3102,7 +3120,25 @@ get_item_dimensions(GtkWidget *widget, GtkOrientation orientation)
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
+ if (allocation.height > 1)
+ return allocation.height;
+
+ // Allocation hasn't been updated yet (widget just became visible,
+ // e.g. tab bar shown asynchronously on Wayland). Query the preferred
+ // height so the caller gets a valid value before the layout pass
+ // runs. Use the maximum of minimum and natural height: GTK may
+ // allocate min_h even when natural_h is smaller (e.g. GtkNotebook
+ // tab bar has min_h > natural_h due to CSS).
+# if GTK_CHECK_VERSION(3,0,0)
+ {
+ gint min_h = 0, natural_h = 0;
+
+ gtk_widget_get_preferred_height(widget, &min_h, &natural_h);
+ return MAX(min_h, natural_h);
+ }
+# else
return allocation.height;
+# endif
# else
if (orientation == GTK_ORIENTATION_HORIZONTAL)
return widget->allocation.height;
@@ -3147,7 +3183,11 @@ get_menu_tool_height(void)
height += get_item_dimensions(gui.toolbar, GTK_ORIENTATION_HORIZONTAL);
#endif
#ifdef FEAT_GUI_TABLINE
- if (gui.tabline != NULL)
+ // Only include the tabline height when tabs are actually shown. After
+ // gtk_notebook_set_show_tabs(FALSE) the widget allocation is not updated
+ // until the GTK main loop runs, so reading it would give a stale value.
+ if (gui.tabline != NULL
+ && gtk_notebook_get_show_tabs(GTK_NOTEBOOK(gui.tabline)))
height += get_item_dimensions(gui.tabline, GTK_ORIENTATION_HORIZONTAL);
#endif
@@ -3513,6 +3553,19 @@ gui_mch_show_tabline(int showit)
if (!showit != !gtk_notebook_get_show_tabs(GTK_NOTEBOOK(gui.tabline)))
{
+# ifdef TRACK_RESIZE_HISTORY
+ // When the tabline visibility changes, GTK defers the formwin
+ // relayout. The resulting configure event fires while the mainwin is
+ // still at its current size. Pre-record that size so it becomes a
+ // stale (discardable) entry once gui_mch_set_shellsize() records the
+ // new target size.
+ {
+ int w, h;
+
+ gtk_window_get_size(GTK_WINDOW(gui.mainwin), &w, &h);
+ alloc_resize_hist(w, h, FALSE);
+ }
+# endif
// Note: this may cause a resize event
gtk_notebook_set_show_tabs(GTK_NOTEBOOK(gui.tabline), showit);
update_window_manager_hints(0, 0);
@@ -4284,6 +4337,22 @@ gui_mch_new_colors(void)
}
}
+/*
+ * One-shot idle callback to issue a corrective gtk_window_resize() after the
+ * startup configure event has been processed. At startup
+ * gui_mch_set_shellsize() runs before the CSD offsets are known (they are
+ * measured from the first configure response), so the window ends up
+ * mch_csd_height pixels too short and mch_csd_width pixels too narrow. This
+ * callback re-issues the shellsize with the now-known offsets so the physical
+ * window matches Vim's model.
+ */
+ static gboolean
+startup_resize_correction_cb(gpointer data UNUSED)
+{
+ gui_set_shellsize(FALSE, FALSE, RESIZE_BOTH);
+ return FALSE; // one-shot
+}
+
/*
* This signal informs us about the need to rearrange our sub-widgets.
*/
@@ -4321,6 +4390,41 @@ form_configure_event(GtkWidget *widget UNUSED,
&& match_stale_width_height(w, h))
// discard stale event
return TRUE;
+
+ // On Wayland (GTK3 CSD), gtk_window_resize(w, req_h) results in
+ // gtk_window_get_size() returning (req_w - csd_w, req_h - csd_h).
+ // Compute the offsets once from the first confirmed resize response.
+ if ((mch_csd_height == 0 || mch_csd_width == 0)
+ && latest_resize_hist != NULL
+ && !latest_resize_hist->used
+ && latest_resize_hist->from_shellsize
+ && gui.char_height > 0)
+ {
+ int pot_csd_w = latest_resize_hist->width - w;
+ int pot_csd_h = latest_resize_hist->height - h;
+
+ if (pot_csd_w > 0 && pot_csd_w < gui.char_width)
+ {
+ mch_csd_width = pot_csd_w;
+ // The resize that triggered this startup event was issued without
+ // CSD compensation; retroactively set the expected form width so
+ // that the clamp below corrects Columns for this same event.
+ mch_pending_form_w = (int)Columns * gui.char_width
+ + gui_get_base_width();
+ }
+ if (pot_csd_h > 0 && pot_csd_h < gui.char_height)
+ {
+ mch_csd_height = pot_csd_h;
+ // Similarly, correct the form height for this event so that Rows
+ // is computed correctly despite the missing CSD compensation.
+ usable_height += mch_csd_height;
+ }
+ // The window was resized without CSD compensation and is physically
+ // too small. Schedule a corrective resize (now that offsets are
+ // known) so the window actually fits the geometry Vim has just set.
+ if ((mch_csd_height > 0 || mch_csd_width > 0) && gtk_socket_id == 0)
+ g_idle_add(startup_resize_correction_cb, NULL);
+ }
clear_resize_hists();
#endif
@@ -4354,9 +4458,22 @@ form_configure_event(GtkWidget *widget UNUSED,
if (gtk_socket_id != 0)
usable_height -= (gui.char_height - (gui.char_height/2)); // sic.
- gui_gtk_form_freeze(GTK_FORM(gui.formwin));
- gui_resize_shell(event->width, usable_height);
- gui_gtk_form_thaw(GTK_FORM(gui.formwin));
+ // If the configure event delivers a form width that is slightly less than
+ // the width we intended (mch_pending_form_w), the difference is within
+ // one char_width and is due to CSD under-compensation. Clamp to the
+ // intended width so that column count does not drift downward.
+ {
+ int use_width = event->width;
+
+#ifdef TRACK_RESIZE_HISTORY
+ if (mch_pending_form_w > event->width
+ && mch_pending_form_w - event->width < gui.char_width)
+ use_width = mch_pending_form_w;
+#endif
+ gui_gtk_form_freeze(GTK_FORM(gui.formwin));
+ gui_resize_shell(use_width, usable_height);
+ gui_gtk_form_thaw(GTK_FORM(gui.formwin));
+ }
return TRUE;
}
@@ -4855,8 +4972,22 @@ gui_mch_set_shellsize(int width, int height,
width += get_menu_tool_width();
height += get_menu_tool_height();
+ // Compensate for CSD frame: on Wayland (GTK3 with CSD), the compositor
+ // subtracts the decoration margin from the requested size. mch_csd_width
+ // and mch_csd_height are computed from the first startup resize response
+ // and are 0 on X11.
+ width += mch_csd_width;
+ height += mch_csd_height;
+
+ // Record the form width we expect the compositor to deliver. Used in
+ // form_configure_event() to clamp configure responses that are slightly
+ // narrower than intended (due to CSD startup underestimate), preventing
+ // column loss. Only meaningful once mch_csd_width has been measured.
+ mch_pending_form_w = (mch_csd_width > 0)
+ ? width - get_menu_tool_width() - mch_csd_width : 0;
+
#ifdef TRACK_RESIZE_HISTORY
- alloc_resize_hist(width, height); // track the resize request
+ alloc_resize_hist(width, height, TRUE);
#endif
if (gtk_socket_id == 0)
gtk_window_resize(GTK_WINDOW(gui.mainwin), width, height);
diff --git a/src/testdir/test_gui.vim b/src/testdir/test_gui.vim
index c686094ca..1098e00ff 100644
--- a/src/testdir/test_gui.vim
+++ b/src/testdir/test_gui.vim
@@ -1889,4 +1889,82 @@ func Test_guioptions_clipboard()
let &guioptions = save_guioptions
endfunc
+" Tests for GUI window geometry: initial size from -geometry option and
+" size stability after :tabnew / :tabclose.
+"
+" Background: on GTK3 with client-side decorations (Wayland), the window
+" compositor subtracts the CSD frame from the requested size, causing the
+" window to open a few pixels too small (wrong &columns/&lines) and to shrink
+" further with each :tabnew/:tabclose cycle.
+
+" Test that a GUI window opened with -geometry=WxH has exactly W columns
+" and H lines.
+"
+" Without the CSD fix, on GTK3/Wayland the compositor subtracts the frame
+" margin from the requested pixel size, so the window is a character cell too
+" narrow and too short.
+func Test_geometry_exact_size()
+ CheckCanRunGui
+ CheckFeature gui_gtk
+
+ let after =<< trim [CODE]
+ call writefile([string(&columns), string(&lines)], 'Xtest_geomsize')
+ qall
+ [CODE]
+
+ " Hide the menu bar so it does not widen the minimum window size.
+ if RunVim(['set guioptions-=m'], after, '-f -g -geometry 40x15')
+ let result = readfile('Xtest_geomsize')
+ call assert_equal('40', result[0], 'columns should match -geometry width')
+ call assert_equal('15', result[1], 'lines should match -geometry height')
+ endif
+
+ call delete('Xtest_geomsize')
+endfunc
+
+" Test that the window size is unchanged after opening and closing a tab.
+"
+" Each :tabnew/:tabclose cycle triggers a tabline show/hide, which causes
+" asynchronous GTK layout events. Without the fix, stale configure events
+" from these layout passes are mis-interpreted as user resizes, reducing
+" &columns and &lines with every cycle. Three cycles are performed to
+" amplify any drift.
+func Test_tabnew_tabclose_size_stable()
+ CheckCanRunGui
+ CheckFeature gui_gtk
+
+ let after =<< trim [CODE]
+ let cols0 = &columns
+ let rows0 = &lines
+ tabnew
+ sleep 300m
+ tabclose
+ sleep 300m
+ tabnew
+ sleep 300m
+ tabclose
+ sleep 300m
+ tabnew
+ sleep 300m
+ tabclose
+ sleep 300m
+ call writefile([string(cols0), string(rows0), string(&columns), string(&lines)], 'Xtest_tabsize')
+ qall
+ [CODE]
+
+ if RunVim(['set guioptions-=m'], after, '-f -g -geometry 40x15')
+ let result = readfile('Xtest_tabsize')
+ call assert_equal('40', result[0], 'initial columns should match -geometry width')
+ call assert_equal('15', result[1], 'initial lines should match -geometry height')
+ call assert_equal(result[0], result[2],
+ \ 'columns changed after 3x tabnew/tabclose: '
+ \ .. result[0] .. ' -> ' .. result[2])
+ call assert_equal(result[1], result[3],
+ \ 'lines changed after 3x tabnew/tabclose: '
+ \ .. result[1] .. ' -> ' .. result[3])
+ endif
+
+ call delete('Xtest_tabsize')
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index 13448796f..83eb6ebec 100644
--- a/src/version.c
+++ b/src/version.c
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 334,
/**/
333,
/**/