]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0360: Cannot handle mouse-clicks in the tabpanel v9.2.0360
authorYasuhiro Matsumoto <mattn.jp@gmail.com>
Thu, 16 Apr 2026 20:29:33 +0000 (20:29 +0000)
committerChristian Brabandt <cb@256bit.org>
Thu, 16 Apr 2026 20:33:00 +0000 (20:33 +0000)
Problem:  Cannot handle mouse-clicks in the tabpanel
Solution: Add support using the %[FuncName] atom for the tabpanel
          (Yasuhiro Matsumoto)

Extend the statusline/tabline click region mechanism to work with
'tabpanel'. The callback receives a dict with "area" set to "tabpanel"
and a "tabnr" key indicating which tab page label was clicked.

closes: #19960

Signed-off-by: Yasuhiro Matsumoto <mattn.jp@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
runtime/doc/options.txt
runtime/doc/version9.txt
src/globals.h
src/mouse.c
src/screen.c
src/structs.h
src/tabpanel.c
src/testdir/test_tabpanel.vim
src/version.c

index 31a9629c0b54d8602f36efe4d153b9051ee656c2..62d7ef8dfd932d7cd761597e50f3dcfec8ad9f40 100644 (file)
@@ -1,4 +1,4 @@
-*options.txt*  For Vim version 9.2.  Last change: 2026 Apr 15
+*options.txt*  For Vim version 9.2.  Last change: 2026 Apr 16
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -8689,7 +8689,7 @@ A jump table for the options with a short description can be found at |Q_op|.
                                                        *stl-%[FuncName]*
        %[ defines clickable regions in the statusline.  When the user clicks
        on a region with the mouse, the specified function is called.  The
-       same syntax can also be used in 'tabline'.
+       same syntax can also be used in 'tabline' and 'tabpanel'.
 
          %[FuncName]   Start of a clickable region.  "FuncName" is the name
                        of a Vim function to call when the region is clicked.
@@ -8708,16 +8708,16 @@ A jump table for the options with a short description can be found at |Q_op|.
          "mods"        Modifier keys: combination of "s" (shift), "c" (ctrl),
                        "a" (alt).  Empty string if no modifiers.
          "winid"       |window-ID| of the window whose statusline was clicked,
-                       or 0 when the click was in 'tabline'.
-         "area"        "statusline" or "tabline".  Indicates which option the
-                       clicked region belongs to.  Useful when a single
-                       callback is shared between 'statusline' and 'tabline'.
+                       or 0 when the click was in 'tabline' or 'tabpanel'.
+         "area"        "statusline", "tabline", or "tabpanel".  Indicates
+                       which option the clicked region belongs to.
+         "tabnr"       (tabpanel only) Tab page number of the clicked label.
 
        If the function returns non-zero, the statusline is redrawn.
        Dragging the statusline to resize the window still works even when
-       click handlers are defined.  When used in 'tabline', clicks in
-       %[FuncName] regions are dispatched to the callback instead of the
-       default tab page selection behavior.
+       click handlers are defined.  When used in 'tabline' or 'tabpanel',
+       clicks in %[FuncName] regions are dispatched to the callback
+       instead of the default tab-selection behavior.
 
        Example: >
            func! ClickFile(info)
index ee24e290104d6e651c51d55d5f22d2e4041d81cc..7fa53f87d8e10c2eaccb5ea6ea940814a63fb202 100644 (file)
@@ -1,4 +1,4 @@
-*version9.txt* For Vim version 9.2.  Last change: 2026 Apr 15
+*version9.txt* For Vim version 9.2.  Last change: 2026 Apr 16
 
 
                  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -52613,8 +52613,8 @@ Other ~
   pairs individually (e.g. 'listchars', 'fillchars', 'diffopt').
 - |system()| and |systemlist()| functions accept a list as first argument,
   bypassing the shell completely.
-- Allow mouse clickable regions in the |status-line| using the
-  |stl-%[FuncName]| atom.
+- Allow mouse clickable regions in the 'statusline', 'tabline' and the
+  'tabpanel' using the |stl-%[FuncName]| atom.
 - Enable reflow support in the |:terminal|.
 
 Platform specific ~
index 20450aa68c1f1d4cc935b4cd611fb03aa4a6aab6..9a6c285937cb0656ae4635f7af36668aaf08a298 100644 (file)
@@ -108,6 +108,10 @@ EXTERN short       *TabPageIdxs INIT(= NULL);
 EXTERN stl_click_region_T *tabline_stl_click INIT(= NULL);
 EXTERN int     tabline_stl_click_count INIT(= 0);
 
+// Click regions for 'tabpanel' (%[FuncName]).
+EXTERN stl_click_region_T *tabpanel_stl_click INIT(= NULL);
+EXTERN int     tabpanel_stl_click_count INIT(= 0);
+
 #ifdef FEAT_PROP_POPUP
 // Array with size Rows x Columns containing zindex of popups.
 EXTERN short   *popup_mask INIT(= NULL);
index 1077c5ee7281259ec65a18e73b4b9bfce040451b..45ffd4b513ae37e19d43538b94fa37de3f36301a 100644 (file)
 static long mouse_hor_step = 6;
 static long mouse_vert_step = 3;
 static win_T *dragwin = NULL;  // window being dragged
-static int stl_click_handler(win_T *wp, int mcol, int which_button, int mods);
+static int stl_click_handler(win_T *wp, int mrow, int mcol, int which_button,
+                                                                   int mods);
 static int stl_click_handler_regions(stl_click_region_T *regions,
-                                   int region_count, int winid, int mcol,
+                                   int region_count, int winid,
+                                   char_u *area_name, int mrow, int mcol,
                                    int which_button, int mods);
 
     void
@@ -492,6 +494,17 @@ do_mouse(
     if (mouse_col < firstwin->w_wincol
                || mouse_col >= firstwin->w_wincol + topframe->fr_width)
     {
+       // Dispatch 'tabpanel' %[FuncName] click regions before falling through
+       // to tab-page selection.  On drag events fall through to the normal
+       // tab-drag handling.
+       if (is_click && !is_drag
+               && stl_click_handler_regions(tabpanel_stl_click,
+                                           tabpanel_stl_click_count,
+                                           0, (char_u *)"tabpanel",
+                                           mouse_row, mouse_col,
+                                           which_button, mod_mask))
+           return FALSE;
+
        tp_label.is_panel = true;
        tp_label.just_in = true;
        tp_label.nr = get_tabpagenr_on_tabpanel();
@@ -511,8 +524,9 @@ do_mouse(
        if (is_click && !is_drag
                && stl_click_handler_regions(tabline_stl_click,
                                            tabline_stl_click_count,
-                                           0, mouse_col, which_button,
-                                           mod_mask))
+                                           0, (char_u *)"tabline",
+                                           mouse_row, mouse_col,
+                                           which_button, mod_mask))
            return FALSE;
 
        tp_label.just_in = true;
@@ -778,7 +792,7 @@ do_mouse(
     // Check for statusline click handler early, before visual mode or
     // other button-specific handling can interfere.
     if (in_status_line && is_click && !is_drag
-           && stl_click_handler(dragwin, mouse_col,
+           && stl_click_handler(dragwin, mouse_row, mouse_col,
                                                which_button, mod_mask))
     {
 #ifdef FEAT_MOUSESHAPE
@@ -1663,6 +1677,8 @@ stl_click_handler_regions(
        stl_click_region_T  *regions,
        int                 region_count,
        int                 winid,
+       char_u              *area_name,
+       int                 mrow,
        int                 mcol,
        int                 which_button,
        int                 mods)
@@ -1682,10 +1698,12 @@ stl_click_handler_regions(
     if (regions == NULL || region_count == 0)
        return FALSE;
 
-    // Find the click region at the given column.
+    // Find the click region at the given row and column.
     for (n = 0; n < region_count; n++)
     {
-       if (col >= regions[n].col_start && col < regions[n].col_end)
+       if (regions[n].row == mrow
+               && col >= regions[n].col_start
+               && col < regions[n].col_end)
            break;
     }
     if (n >= region_count || regions[n].funcname == NULL)
@@ -1728,10 +1746,13 @@ stl_click_handler_regions(
     dict_add_number(info, "winid", winid);
 
     // "area": which option the clicked region belongs to.  Lets a shared
-    // dispatcher distinguish 'statusline' from 'tabline' (and future areas)
-    // without having to overload winid == 0.
-    dict_add_string(info, "area",
-           winid == 0 ? (char_u *)"tabline" : (char_u *)"statusline");
+    // dispatcher distinguish 'statusline', 'tabline' and 'tabpanel' without
+    // having to overload winid == 0.
+    dict_add_string(info, "area", area_name);
+
+    // Expose tab page number for 'tabpanel' regions.
+    if (regions[n].tabnr > 0)
+       dict_add_number(info, "tabnr", regions[n].tabnr);
 
     // Call the function with the info dict as argument.
     argvars[0].v_type = VAR_DICT;
@@ -1755,9 +1776,14 @@ stl_click_handler_regions(
     {
        // Make sure the tabline gets redrawn too when the callback asks for
        // a redraw (redraw_statuslines() only redraws the tabline when
-       // redraw_tabline is set).
+       // redraw_tabline is set).  For tabpanel the whole screen needs to be
+       // refreshed.
        if (winid == 0)
            redraw_tabline = TRUE;
+# ifdef FEAT_TABPANEL
+       if (STRCMP(area_name, "tabpanel") == 0)
+           redraw_all_later(UPD_NOT_VALID);
+# endif
        redraw_statuslines();
     }
 
@@ -1766,6 +1792,8 @@ stl_click_handler_regions(
     (void)regions;
     (void)region_count;
     (void)winid;
+    (void)area_name;
+    (void)mrow;
     (void)mcol;
     (void)which_button;
     (void)mods;
@@ -1778,12 +1806,13 @@ stl_click_handler_regions(
  * Returns TRUE if the function was called and handled the click.
  */
     static int
-stl_click_handler(win_T *wp, int mcol, int which_button, int mods)
+stl_click_handler(win_T *wp, int mrow, int mcol, int which_button, int mods)
 {
     if (wp == NULL)
        return FALSE;
     return stl_click_handler_regions(wp->w_stl_click, wp->w_stl_click_count,
-                                       wp->w_id, mcol, which_button, mods);
+                               wp->w_id, (char_u *)"statusline",
+                               mrow, mcol, which_button, mods);
 }
 
 // dragwin is declared near the top of the file
index eb7cc9cdd1176830e8fd746fcec6f719b347f6b6..b9bde1d574f5dc9a8df8dd48c78e0ce81d9a91b0 100644 (file)
@@ -1492,8 +1492,13 @@ win_redr_custom(
        stl_click_region_T  **out_regions;
        int                 *out_count;
        int                 base_col;
+       int                 base_row;
        int                 click_count = 0;
 
+       // clicktab reflects the last iteration of the draw loop above, so
+       // the regions belong to the last drawn row.
+       base_row = row + stlh_cnt - 1;
+
        if (wp != NULL)
        {
            out_regions = &wp->w_stl_click;
@@ -1547,11 +1552,13 @@ win_redr_custom(
                    // Close previous region if there was one.
                    if (cur_funcname != NULL)
                    {
+                       regions[rcount].row = base_row;
                        regions[rcount].col_start = region_start;
                        regions[rcount].col_end = base_col + len;
                        regions[rcount].funcname =
                                            vim_strsave(cur_funcname);
                        regions[rcount].minwid = cur_minwid;
+                       regions[rcount].tabnr = 0;
                        rcount++;
                    }
 
@@ -1563,11 +1570,13 @@ win_redr_custom(
                // Close final region if it extends to the end.
                if (cur_funcname != NULL)
                {
+                   regions[rcount].row = base_row;
                    regions[rcount].col_start = region_start;
                    regions[rcount].col_end = base_col + maxwidth;
                    regions[rcount].funcname =
                                        vim_strsave(cur_funcname);
                    regions[rcount].minwid = cur_minwid;
+                   regions[rcount].tabnr = 0;
                    rcount++;
                }
 
index 648ceec90551e69797afd5472fa004ff08dd2cce..e76c651d2ef38649981fbe517c1fd2a71b3a6e5a 100644 (file)
@@ -1451,10 +1451,12 @@ typedef struct {
  * Per-window resolved click regions (screen column based).
  */
 typedef struct {
+    int                row;            // screen row where region lives
     int                col_start;      // screen column where region starts
     int                col_end;        // screen column where region ends
     char_u     *funcname;      // function name (allocated copy)
     int                minwid;         // minwid value
+    int                tabnr;          // tab page number (tabpanel only, 0 otherwise)
 } stl_click_region_T;
 
 
index 51929a8b1c120e04075d24093cc281a95df0df57..4138f9760a7795a9b7f264a91f20416e43fad37d 100644 (file)
@@ -17,6 +17,9 @@
 
 static void do_by_tplmode(int tplmode, int col_start, int col_end,
        int *pcurtab_row, int *ptabpagenr);
+static void tabpanel_free_click_regions(void);
+static void tabpanel_append_click_regions(stl_clickrec_T *clicktab,
+       char_u *buf, int row, int col_start, int col_end, int tabnr);
 
 // set pcurtab_row. don't redraw tabpanel.
 #define TPLMODE_GET_CURTAB_ROW 0
@@ -135,6 +138,108 @@ tabpanel_leftcol(void)
     return tpl_align == ALIGN_RIGHT ? 0 : tabpanel_width();
 }
 
+/*
+ * Free previously resolved 'tabpanel' click regions.
+ */
+    static void
+tabpanel_free_click_regions(void)
+{
+    int n;
+
+    if (tabpanel_stl_click != NULL)
+    {
+       for (n = 0; n < tabpanel_stl_click_count; n++)
+           vim_free(tabpanel_stl_click[n].funcname);
+       VIM_CLEAR(tabpanel_stl_click);
+    }
+    tabpanel_stl_click_count = 0;
+}
+
+/*
+ * Convert click records produced by build_stl_str_hl() for one line of
+ * 'tabpanel' into screen-column based regions and append them to the global
+ * tabpanel_stl_click array.  The caller keeps ownership of the funcname
+ * strings inside "clicktab" — this function makes its own copies.
+ */
+    static void
+tabpanel_append_click_regions(
+       stl_clickrec_T  *clicktab,
+       char_u          *buf,
+       int             row,
+       int             col_start,
+       int             col_end,
+       int             tabnr)
+{
+    int                count = 0;
+    int                n;
+    int                base_col;
+    int                acc_width = 0;
+    int                max_w = col_end - col_start;
+    char_u     *p;
+    char_u     *cur_funcname = NULL;
+    int                cur_minwid = 0;
+    int                region_start_col;
+    stl_click_region_T *new_arr;
+    int                limit;
+
+    if (clicktab == NULL)
+       return;
+
+    for (n = 0; clicktab[n].start != NULL; n++)
+       count++;
+    if (count == 0)
+       return;
+
+    base_col = (tpl_align == ALIGN_RIGHT ? topframe->fr_width : 0) + col_start;
+    region_start_col = base_col;
+
+    // Grow the global array to make room for up to "count" more regions
+    // (one close for each record plus a possible trailing region).
+    new_arr = vim_realloc(tabpanel_stl_click,
+           sizeof(stl_click_region_T) * (tabpanel_stl_click_count + count + 1));
+    if (new_arr == NULL)
+       return;
+    tabpanel_stl_click = new_arr;
+
+    p = buf;
+    for (n = 0; clicktab[n].start != NULL; n++)
+    {
+       acc_width += vim_strnsize(p, (int)(clicktab[n].start - p));
+       p = clicktab[n].start;
+       limit = acc_width < max_w ? acc_width : max_w;
+
+       if (cur_funcname != NULL)
+       {
+           stl_click_region_T *r =
+                               &tabpanel_stl_click[tabpanel_stl_click_count];
+           r->row = row;
+           r->col_start = region_start_col;
+           r->col_end = base_col + limit;
+           r->funcname = vim_strsave(cur_funcname);
+           r->minwid = cur_minwid;
+           r->tabnr = tabnr;
+           tabpanel_stl_click_count++;
+       }
+
+       cur_funcname = clicktab[n].funcname;
+       cur_minwid = clicktab[n].minwid;
+       region_start_col = base_col + limit;
+    }
+
+    // Close the final region if it extends to the end.
+    if (cur_funcname != NULL)
+    {
+       stl_click_region_T *r = &tabpanel_stl_click[tabpanel_stl_click_count];
+       r->row = row;
+       r->col_start = region_start_col;
+       r->col_end = base_col + max_w;
+       r->funcname = vim_strsave(cur_funcname);
+       r->minwid = cur_minwid;
+       r->tabnr = tabnr;
+       tabpanel_stl_click_count++;
+    }
+}
+
 /*
  * draw the tabpanel.
  */
@@ -150,7 +255,13 @@ draw_tabpanel(void)
     int is_right = tpl_align == ALIGN_RIGHT;
 
     if (maxwidth == 0)
+    {
+       tabpanel_free_click_regions();
        return;
+    }
+
+    // Discard old click regions — they'll be rebuilt during redraw below.
+    tabpanel_free_click_regions();
 
     // Reset got_int to avoid build_stl_str_hl() isn't evaluated.
     got_int = FALSE;
@@ -495,6 +606,7 @@ do_by_tplmode(
                char_u  buf[IOSIZE];
                stl_hlrec_T     *hltab;
                stl_hlrec_T     *tabtab;
+               stl_clickrec_T  *clicktab = NULL;
 
                if (args.maxrow <= row - args.offsetrow)
                    break;
@@ -508,13 +620,31 @@ do_by_tplmode(
                        (args.cwp, buf, sizeof(buf),
                        &usefmt, opt_name, opt_scope, TPL_FILLCHAR,
                        args.col_end - args.col_start, &hltab, &tabtab,
-                       NULL);
+                       tplmode == TPLMODE_REDRAW ? &clicktab : NULL);
 
                args.prow = &row;
                args.pcol = &col;
 
                draw_tabpanel_with_highlight(tplmode, buf, hltab, &args);
 
+               // Record any %[FuncName] click regions for this line once
+               // the text has been drawn.  Only visible rows participate.
+               if (tplmode == TPLMODE_REDRAW && clicktab != NULL)
+               {
+                   int screen_row = row - args.offsetrow;
+                   int m;
+
+                   if (screen_row >= 0 && screen_row < args.maxrow)
+                       tabpanel_append_click_regions(clicktab, buf,
+                               screen_row, args.col_start, args.col_end,
+                               (int)v.vval.v_number);
+                   // We took ownership of the click records — free the
+                   // function names (matches the non-NULL clicktab path in
+                   // build_stl_str_hl()).
+                   for (m = 0; clicktab[m].start != NULL; m++)
+                       vim_free(clicktab[m].funcname);
+               }
+
                // Move to next line for %@
                if (*usefmt != NUL)
                {
index 4bb7f39eb04e16bc145f073dde4323c95f185cba..7803a0eb4dc3cf3ceb936c5dc47310029205cf23 100644 (file)
@@ -249,6 +249,76 @@ function Test_tabpanel_mouse()
   let &showtabline = save_showtabline
 endfunc
 
+func g:TplClickTestFunc(info)
+  let g:tpl_click_info = a:info
+  return 0
+endfunc
+
+function Test_tabpanel_click_handler()
+  let save_mouse = &mouse
+  let save_stal = &showtabline
+  let save_stpl = &showtabpanel
+  let save_tpl = &tabpanel
+  let save_tplo = &tabpanelopt
+  set mouse=a
+  set showtabline=0
+  set showtabpanel=2
+  set tabpanelopt=columns:16
+  tabnew
+  tabnew
+
+  " Place two adjacent %[FuncName] regions on every tab label.
+  set tabpanel=%1[TplClickTestFunc][A]%[]%2[TplClickTestFunc][B]%[]
+  redraw!
+
+  " Click on [A] region in the first tab label (row 1).
+  call test_setmouse(1, 2)
+  call feedkeys("\<LeftMouse>\<LeftRelease>", 'xt')
+  call assert_true(exists('g:tpl_click_info'))
+  call assert_equal('l', g:tpl_click_info.button)
+  call assert_equal(1, g:tpl_click_info.nclicks)
+  call assert_equal(1, g:tpl_click_info.minwid)
+  call assert_equal(0, g:tpl_click_info.winid)
+  call assert_equal('tabpanel', g:tpl_click_info.area)
+  call assert_equal(1, g:tpl_click_info.tabnr)
+  unlet! g:tpl_click_info
+
+  " Click on [B] region in the second tab label (row 2).
+  call test_setmouse(2, 5)
+  call feedkeys("\<LeftMouse>\<LeftRelease>", 'xt')
+  call assert_true(exists('g:tpl_click_info'))
+  call assert_equal(2, g:tpl_click_info.minwid)
+  call assert_equal(2, g:tpl_click_info.tabnr)
+  unlet! g:tpl_click_info
+
+  " Middle click on [A] in tab 3.
+  call test_setmouse(3, 2)
+  call feedkeys("\<MiddleMouse>\<MiddleRelease>", 'xt')
+  call assert_true(exists('g:tpl_click_info'))
+  call assert_equal('m', g:tpl_click_info.button)
+  call assert_equal(1, g:tpl_click_info.minwid)
+  call assert_equal(3, g:tpl_click_info.tabnr)
+  unlet! g:tpl_click_info
+
+  " A click outside any region (but still in the panel) must not fire the
+  " callback, and should fall through to the normal tab selection.
+  set tabpanel=xxx%1[TplClickTestFunc][Y]%[]
+  redraw!
+  tabfirst
+  call test_setmouse(2, 1)
+  call feedkeys("\<LeftMouse>\<LeftRelease>", 'xt')
+  call assert_false(exists('g:tpl_click_info'))
+  call assert_equal(2, tabpagenr())
+
+  tabonly!
+  call s:reset()
+  let &tabpanel = save_tpl
+  let &tabpanelopt = save_tplo
+  let &showtabpanel = save_stpl
+  let &showtabline = save_stal
+  let &mouse = save_mouse
+endfunc
+
 function Test_tabpanel_drawing()
   CheckScreendump
 
index b015465ed522bc48bac2f1b7a1c89de0edf5f8d5..2361deb7de0caa30feeb104cda93f2ab5d69ce78 100644 (file)
@@ -734,6 +734,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    360,
 /**/
     359,
 /**/