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.
