patch 9.2.0360: Cannot handle mouse-clicks in the tabpanel

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>
This commit is contained in:
Yasuhiro Matsumoto
2026-04-16 20:29:33 +00:00
committed by Christian Brabandt
parent 25e9fc44a8
commit 1c299f2631
9 changed files with 273 additions and 27 deletions
+9 -9
View 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)
+3 -3
View 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 ~
+4
View 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);
+43 -14
View File
@@ -20,9 +20,11 @@
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
+9
View 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++;
}
+2
View 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;
+131 -1
View 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)
{
+70
View 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
+2
View File
@@ -734,6 +734,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
360,
/**/
359,
/**/