patch 9.2.0334: GTK: window geometry shrinks with with client-side decorations

Commit: 
https://github.com/vim/vim/commit/dd40b1af5b5800cab81cb4b6566f028282e74031
Author: Gary Johnson <[email protected]>
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 <[email protected]>
    Signed-off-by: Gary Johnson <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

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,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1wBJQT-00DqHt-MX%40256bit.org.

Raspunde prin e-mail lui