patch 9.2.0386: No scroll/scrollbar support in the tabpanel

Problem:  No scroll/scrollbar support in the tabpanel
Solution: Add support for it (Yasuhiro Matsumoto)

closes: #19979

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-21 20:20:30 +00:00
committed by Christian Brabandt
parent 10040bc9cd
commit 2ea4a7c3b7
10 changed files with 396 additions and 21 deletions
+18 -1
View File
@@ -1,4 +1,4 @@
*options.txt* For Vim version 9.2. Last change: 2026 Apr 20
*options.txt* For Vim version 9.2. Last change: 2026 Apr 21
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -9065,6 +9065,23 @@ A jump table for the options with a short description can be found at |Q_op|.
tab panel will not be displayed.
(default 20)
scroll Enable mouse wheel scrolling over the tabpanel
area when the tab list exceeds the visible
screen height. The scroll step is controlled
by 'mousescroll'. When disabled (the default),
the tabpanel shows the page containing the
current tab, with no way to view tabs outside
that page.
scrollbar Reserve a one-column scrollbar in the tabpanel
showing the current scroll position. The
scrollbar uses the |hl-PmenuSbar| and
|hl-PmenuThumb| highlight groups for the track
and thumb respectively. Clicking on the
scrollbar column jumps the thumb to that
position; the thumb can also be dragged.
Implies "scroll".
vert Use a vertical separator for tabpanel.
The vertical separator character is taken from
"tpl_vert" in 'fillchars'.
+32 -1
View File
@@ -1,4 +1,4 @@
*tabpage.txt* For Vim version 9.2. Last change: 2026 Feb 14
*tabpage.txt* For Vim version 9.2. Last change: 2026 Apr 21
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -482,6 +482,37 @@ The vertical separator character is taken from "tpl_vert" in 'fillchars'.
You can customize the appearance of the tab page labels using the highlight
groups: |hl-TabPanel| |hl-TabPanelSel| |hl-TabPanelFill|
SCROLLING IN THE TABPANEL: *tabpanel-scroll*
When the total height of the tab list exceeds the visible screen height, the
tabpanel by default displays the "page" that contains the current tab and
offers no way to view tabs outside that page.
To make the tabpanel scrollable, add "scroll" to 'tabpanelopt': >
:set tabpanelopt+=scroll
With "scroll" enabled, mouse wheel events over the tabpanel area scroll the
tab list up or down. The scroll step follows the 'mousescroll' setting.
Wheel events inside the tabpanel area are consumed by the tabpanel and do not
trigger |<ScrollWheelUp>| or |<ScrollWheelDown>| mappings.
To additionally show a vertical scrollbar indicating the current scroll
position, use "scrollbar": >
:set tabpanelopt+=scrollbar
The "scrollbar" value implies "scroll". A one-column scrollbar is reserved at
the edge of the tabpanel; clicking on the scrollbar column moves the thumb to
the click position, and the thumb can be dragged to scroll continuously.
When "vert" is combined with "scrollbar", the scrollbar is drawn next to the
vertical separator, on the panel side.
The scrollbar uses the |hl-PmenuSbar| highlight group for the track and
|hl-PmenuThumb| for the thumb.
The scroll offset is remembered across redraws but is reset when "scroll" or
"scrollbar" is toggled off and back on.
==============================================================================
6. Setting 'guitablabel' *setting-guitablabel*
+1
View File
@@ -10889,6 +10889,7 @@ tabpagebuflist() builtin.txt /*tabpagebuflist()*
tabpagenr() builtin.txt /*tabpagenr()*
tabpagewinnr() builtin.txt /*tabpagewinnr()*
tabpanel tabpage.txt /*tabpanel*
tabpanel-scroll tabpage.txt /*tabpanel-scroll*
tag tagsrch.txt /*tag*
tag-! tagsrch.txt /*tag-!*
tag-binary-search tagsrch.txt /*tag-binary-search*
+3
View File
@@ -52616,6 +52616,9 @@ Other ~
- Allow mouse clickable regions in the 'statusline', 'tabline' and the
'tabpanel' using the |stl-%[FuncName]| atom.
- Enable reflow support in the |:terminal|.
- Added "scroll" and "scrollbar" sub-options to 'tabpanelopt' so the
tabpanel can scroll when the tab list exceeds the visible screen
height.
Platform specific ~
-----------------
+8 -1
View File
@@ -4946,6 +4946,9 @@ build_stl_str_hl_local(
maxwid = 50;
}
}
// Keep the uncapped value for %N[FuncName] click-region IDs; the 50
// cap below applies only when minwid is used as a padding width.
int raw_minwid = minwid * l;
minwid = (minwid > 50 ? 50 : minwid) * l;
if (*s == '(')
{
@@ -5306,7 +5309,11 @@ build_stl_str_hl_local(
{
stl_items[curitem].stl_type = ClickFunc;
stl_items[curitem].stl_start = p;
stl_items[curitem].stl_minwid = minwid;
// The stl_minwid field is overloaded: it may be the
// "min" part of %<min>.<max> used for padding, or an
// identifier passed to the %N[FuncName] callback. Store
// the uncapped value so IDs above 50 are preserved.
stl_items[curitem].stl_minwid = raw_minwid;
stl_items[curitem].stl_clickfunc =
vim_strnsave(s, rb - s);
s = rb + 1;
+54 -1
View File
@@ -240,6 +240,9 @@ do_mouse(
int in_status_line; // mouse in status line
static int in_tab_line = FALSE; // mouse clicked in tab line
static int in_tabpanel = FALSE; // mouse clicked in tabpanel
#ifdef FEAT_TABPANEL
static bool in_tabpanel_scrollbar = false; // dragging tabpanel scrollbar
#endif
int in_sep_line; // mouse in vertical separator line
int c1, c2;
#if defined(FEAT_FOLDING)
@@ -346,6 +349,9 @@ do_mouse(
got_click = TRUE;
in_tab_line = FALSE;
in_tabpanel = FALSE;
#ifdef FEAT_TABPANEL
in_tabpanel_scrollbar = false;
#endif
}
else
{
@@ -354,15 +360,31 @@ do_mouse(
if (!is_drag) // release, reset got_click
{
got_click = FALSE;
if (in_tab_line || in_tabpanel)
if (in_tab_line || in_tabpanel
#ifdef FEAT_TABPANEL
|| in_tabpanel_scrollbar
#endif
)
{
in_tab_line = FALSE;
in_tabpanel = FALSE;
#ifdef FEAT_TABPANEL
in_tabpanel_scrollbar = false;
#endif
return FALSE;
}
}
}
#ifdef FEAT_TABPANEL
// Continue a scrollbar drag before any tab-selection handling.
if (is_drag && in_tabpanel_scrollbar)
{
tabpanel_drag_scrollbar(mouse_row);
return FALSE;
}
#endif
// CTRL right mouse button does CTRL-T
if (is_click && (mod_mask & MOD_MASK_CTRL) && which_button == MOUSE_RIGHT)
{
@@ -494,6 +516,15 @@ do_mouse(
if (mouse_col < firstwin->w_wincol
|| mouse_col >= firstwin->w_wincol + topframe->fr_width)
{
// A click on the scrollbar column starts a drag interaction and
// preempts tab-selection.
if (is_click && !is_drag && mouse_on_tabpanel_scrollbar())
{
in_tabpanel_scrollbar = TRUE;
tabpanel_drag_scrollbar(mouse_row);
return FALSE;
}
// Dispatch 'tabpanel' %[FuncName] click regions before falling through
// to tab-page selection. On drag events fall through to the normal
// tab-drag handling.
@@ -1276,6 +1307,17 @@ ins_mousescroll(int dir)
cap.oap = &oa;
cap.arg = dir;
#ifdef FEAT_TABPANEL
if (mouse_row >= 0 && mouse_col >= 0
&& (dir == MSCR_UP || dir == MSCR_DOWN)
&& mouse_on_tabpanel())
{
(void)tabpanel_scroll(dir == MSCR_UP ? 1 : -1,
mouse_vert_step > 0 ? mouse_vert_step : 3);
return;
}
#endif
switch (dir)
{
case MSCR_UP:
@@ -2409,6 +2451,17 @@ nv_mousescroll(cmdarg_T *cap)
{
win_T *old_curwin = curwin;
#ifdef FEAT_TABPANEL
if (mouse_row >= 0 && mouse_col >= 0
&& (cap->arg == MSCR_UP || cap->arg == MSCR_DOWN)
&& mouse_on_tabpanel())
{
(void)tabpanel_scroll(cap->arg == MSCR_UP ? 1 : -1,
mouse_vert_step > 0 ? mouse_vert_step : 3);
return;
}
#endif
if (mouse_row >= 0 && mouse_col >= 0)
{
// Find the window at the mouse pointer coordinates.
+4
View File
@@ -4,4 +4,8 @@ int tabpanel_width(void);
int tabpanel_leftcol(void);
void draw_tabpanel(void);
int get_tabpagenr_on_tabpanel(void);
bool mouse_on_tabpanel(void);
bool mouse_on_tabpanel_scrollbar(void);
bool tabpanel_drag_scrollbar(int screen_row);
bool tabpanel_scroll(int dir, int count);
/* vim: set ft=c : */
+213 -16
View File
@@ -20,6 +20,7 @@ static void do_by_tplmode(int tplmode, int col_start, int col_end,
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);
static void draw_tabpanel_scrollbar(int screen_col);
// set pcurtab_row. don't redraw tabpanel.
#define TPLMODE_GET_CURTAB_ROW 0
@@ -31,6 +32,7 @@ static void tabpanel_append_click_regions(stl_clickrec_T *clicktab,
#define TPL_FILLCHAR ' '
#define VERT_LEN 1
#define SCROLL_LEN 1
// tpl_align's values
#define ALIGN_LEFT 0
@@ -40,7 +42,12 @@ static char_u *opt_name = (char_u *)"tabpanel";
static int opt_scope = OPT_LOCAL;
static int tpl_align = ALIGN_LEFT;
static int tpl_columns = 20;
static int tpl_is_vert = FALSE;
static bool tpl_is_vert = false;
static bool tpl_scroll = false;
static bool tpl_scrollbar = false;
static int tpl_scroll_offset = 0;
static int tpl_total_rows = 0;
static int tpl_scrollbar_col = -1; // screen column of scrollbar, -1 if none
typedef struct {
win_T *wp;
@@ -61,7 +68,9 @@ tabpanelopt_changed(void)
char_u *p;
int new_align = ALIGN_LEFT;
long new_columns = 20;
int new_is_vert = FALSE;
bool new_is_vert = false;
bool new_scroll = false;
bool new_scrollbar = false;
p = p_tplo;
while (*p != NUL)
@@ -92,7 +101,18 @@ tabpanelopt_changed(void)
else if (STRNCMP(p, "vert", 4) == 0)
{
p += 4;
new_is_vert = TRUE;
new_is_vert = true;
}
else if (STRNCMP(p, "scrollbar", 9) == 0)
{
p += 9;
new_scrollbar = true;
new_scroll = true;
}
else if (STRNCMP(p, "scroll", 6) == 0)
{
p += 6;
new_scroll = true;
}
if (*p != ',' && *p != NUL)
@@ -104,6 +124,10 @@ tabpanelopt_changed(void)
tpl_align = new_align;
tpl_columns = new_columns;
tpl_is_vert = new_is_vert;
if (tpl_scroll != new_scroll)
tpl_scroll_offset = 0;
tpl_scroll = new_scroll;
tpl_scrollbar = new_scrollbar;
shell_new_columns();
return OK;
@@ -266,39 +290,65 @@ draw_tabpanel(void)
// Reset got_int to avoid build_stl_str_hl() isn't evaluated.
got_int = FALSE;
int sb_len = tpl_scrollbar ? SCROLL_LEN : 0;
int sb_screen_col = -1;
if (tpl_is_vert)
{
if (is_right)
{
// draw main contents in tabpanel
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, VERT_LEN,
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, VERT_LEN + sb_len,
maxwidth - VERT_LEN, &curtab_row, NULL);
do_by_tplmode(TPLMODE_REDRAW, VERT_LEN, maxwidth, &curtab_row,
NULL);
do_by_tplmode(TPLMODE_REDRAW, VERT_LEN + sb_len, maxwidth,
&curtab_row, NULL);
// draw vert separator in tabpanel
for (vsrow = 0; vsrow < Rows; vsrow++)
screen_putchar(curwin->w_fill_chars.tpl_vert, vsrow,
topframe->fr_width, vs_attr);
if (tpl_scrollbar)
sb_screen_col = topframe->fr_width + VERT_LEN;
}
else
{
// draw main contents in tabpanel
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, 0, maxwidth - VERT_LEN,
&curtab_row, NULL);
do_by_tplmode(TPLMODE_REDRAW, 0, maxwidth - VERT_LEN,
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, 0,
maxwidth - VERT_LEN - sb_len, &curtab_row, NULL);
do_by_tplmode(TPLMODE_REDRAW, 0, maxwidth - VERT_LEN - sb_len,
&curtab_row, NULL);
// draw vert separator in tabpanel
for (vsrow = 0; vsrow < Rows; vsrow++)
screen_putchar(curwin->w_fill_chars.tpl_vert, vsrow,
maxwidth - VERT_LEN, vs_attr);
if (tpl_scrollbar)
sb_screen_col = maxwidth - VERT_LEN - SCROLL_LEN;
}
}
else
{
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, 0, maxwidth, &curtab_row, NULL);
do_by_tplmode(TPLMODE_REDRAW, 0, maxwidth, &curtab_row, NULL);
if (is_right)
{
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, sb_len, maxwidth,
&curtab_row, NULL);
do_by_tplmode(TPLMODE_REDRAW, sb_len, maxwidth, &curtab_row, NULL);
if (tpl_scrollbar)
sb_screen_col = topframe->fr_width;
}
else
{
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, 0, maxwidth - sb_len,
&curtab_row, NULL);
do_by_tplmode(TPLMODE_REDRAW, 0, maxwidth - sb_len,
&curtab_row, NULL);
if (tpl_scrollbar)
sb_screen_col = maxwidth - SCROLL_LEN;
}
}
tpl_scrollbar_col = sb_screen_col;
if (sb_screen_col >= 0)
draw_tabpanel_scrollbar(sb_screen_col);
got_int |= saved_got_int;
// A user function may reset KeyTyped, restore it.
@@ -556,8 +606,13 @@ do_by_tplmode(
args.col_end = col_end;
if (tplmode != TPLMODE_GET_CURTAB_ROW && args.maxrow > 0)
while (args.offsetrow + args.maxrow <= *pcurtab_row)
args.offsetrow += args.maxrow;
{
if (tpl_scroll)
args.offsetrow = tpl_scroll_offset;
else
while (args.offsetrow + args.maxrow <= *pcurtab_row)
args.offsetrow += args.maxrow;
}
tp = first_tabpage;
@@ -579,8 +634,13 @@ do_by_tplmode(
if (tplmode == TPLMODE_GET_CURTAB_ROW)
{
*pcurtab_row = row;
do_unlet((char_u *)"g:actual_curtabpage", TRUE);
break;
// When scroll mode is active keep iterating so tpl_total_rows
// receives the true content height; otherwise bail out early.
if (!tpl_scroll)
{
do_unlet((char_u *)"g:actual_curtabpage", TRUE);
break;
}
}
}
else
@@ -608,7 +668,8 @@ do_by_tplmode(
stl_hlrec_T *tabtab;
stl_clickrec_T *clicktab = NULL;
if (args.maxrow <= row - args.offsetrow)
if (tplmode != TPLMODE_GET_CURTAB_ROW
&& args.maxrow <= row - args.offsetrow)
break;
buf[0] = NUL;
@@ -677,6 +738,142 @@ do_by_tplmode(
// fill the area of TabPanelFill.
screen_fill_tailing_area(tplmode, MAX(row - args.offsetrow, 0), args.maxrow,
args.col_start, args.col_end, attr_tplf);
// Capture the true content height during the GET_CURTAB_ROW pass, which
// ignores maxrow and therefore walks every tab. REDRAW stops at the
// visible edge so its "row" is clamped and unusable here.
if (tplmode == TPLMODE_GET_CURTAB_ROW && tpl_scroll)
tpl_total_rows = row;
}
/*
* Draw the tabpanel scrollbar (track + thumb) at screen column 'screen_col'.
* The scrollbar spans the full screen height. The thumb position and size
* are derived from tpl_scroll_offset, tpl_total_rows and Rows.
*/
static void
draw_tabpanel_scrollbar(int screen_col)
{
int attr_sb = HL_ATTR(HLF_PSB);
int attr_thumb = HL_ATTR(HLF_PST);
int thumb_top = 0;
int thumb_height = 0;
if (tpl_total_rows > Rows && Rows > 0)
{
thumb_height = Rows * Rows / tpl_total_rows;
if (thumb_height < 1)
thumb_height = 1;
thumb_top = Rows * tpl_scroll_offset / tpl_total_rows;
if (thumb_top + thumb_height > Rows)
thumb_top = Rows - thumb_height;
if (thumb_top < 0)
thumb_top = 0;
}
for (int r = 0; r < Rows; r++)
{
bool on_thumb = thumb_height > 0
&& r >= thumb_top && r < thumb_top + thumb_height;
screen_putchar(TPL_FILLCHAR, r, screen_col,
on_thumb ? attr_thumb : attr_sb);
}
}
/*
* Return true if the mouse is currently positioned over the tabpanel area.
*/
bool
mouse_on_tabpanel(void)
{
if (tabpanel_width() == 0)
return false;
return mouse_col < firstwin->w_wincol
|| mouse_col >= firstwin->w_wincol + topframe->fr_width;
}
/*
* Return true if the mouse is currently on the scrollbar column.
* The scrollbar column is tracked by draw_tabpanel() and is -1 when the
* scrollbar is not enabled or not yet drawn.
*/
bool
mouse_on_tabpanel_scrollbar(void)
{
return tpl_scrollbar && tpl_scrollbar_col >= 0
&& mouse_col == tpl_scrollbar_col;
}
/*
* Move the scrollbar thumb so it is vertically centred on screen row
* 'screen_row', updating tpl_scroll_offset accordingly. Used for both
* initial clicks and subsequent drag events.
* Returns true if the event was consumed (offset changed or not).
*/
bool
tabpanel_drag_scrollbar(int screen_row)
{
int thumb_height;
int max_offset;
int track_range;
int thumb_top;
int new_offset;
if (!tpl_scrollbar || Rows <= 0 || tpl_total_rows <= Rows)
return false;
thumb_height = Rows * Rows / tpl_total_rows;
if (thumb_height < 1)
thumb_height = 1;
track_range = Rows - thumb_height;
if (track_range <= 0)
return true;
max_offset = tpl_total_rows - Rows;
thumb_top = screen_row - thumb_height / 2;
if (thumb_top < 0)
thumb_top = 0;
if (thumb_top > track_range)
thumb_top = track_range;
new_offset = thumb_top * max_offset / track_range;
if (new_offset != tpl_scroll_offset)
{
tpl_scroll_offset = new_offset;
redraw_tabpanel = TRUE;
}
return true;
}
/*
* Scroll the tabpanel by 'count' rows in direction 'dir' (1 = down, -1 = up).
* Returns true if the offset changed and a redraw was scheduled.
* Has no effect unless 'tabpanelopt' contains "scroll".
*/
bool
tabpanel_scroll(int dir, int count)
{
int max_offset;
int new_offset;
if (!tpl_scroll || tabpanel_width() == 0)
return false;
max_offset = tpl_total_rows - Rows;
if (max_offset < 0)
max_offset = 0;
new_offset = tpl_scroll_offset + (dir > 0 ? count : -count);
if (new_offset < 0)
new_offset = 0;
if (new_offset > max_offset)
new_offset = max_offset;
if (new_offset == tpl_scroll_offset)
return false;
tpl_scroll_offset = new_offset;
redraw_tabpanel = TRUE;
return true;
}
#endif // FEAT_TABPANEL
+61 -1
View File
@@ -962,8 +962,68 @@ func Test_tabpanel_large_columns()
call assert_fails(':set tabpanelopt=columns:-1', 'E474:')
endfunc
func Test_tabpanel_scrollopt_accepted()
" 'scroll' / 'scrollbar' must be accepted in 'tabpanelopt'.
set tabpanelopt=scroll
call assert_equal('scroll', &tabpanelopt)
set tabpanelopt=scrollbar
call assert_equal('scrollbar', &tabpanelopt)
" Combination with other values.
set tabpanelopt=align:right,scroll
call assert_equal('align:right,scroll', &tabpanelopt)
set tabpanelopt=columns:15,vert,scrollbar
call assert_equal('columns:15,vert,scrollbar', &tabpanelopt)
set tabpanelopt=align:right,columns:12,vert,scrollbar
call assert_equal('align:right,columns:12,vert,scrollbar', &tabpanelopt)
" Unknown values must still fail.
call assert_fails(':set tabpanelopt=scrol', 'E474:')
call assert_fails(':set tabpanelopt=scrollbarx', 'E474:')
call s:reset()
endfunc
func Test_tabpanel_scroll_many_tabs()
let save_lines = &lines
let save_showtabpanel = &showtabpanel
let save_tabpanelopt = &tabpanelopt
" Make the screen short so the tab list exceeds the visible height.
set lines=8
set showtabpanel=2
set tabpanelopt=scroll
for i in range(20)
tabnew
endfor
" Should not crash with many tabs and scroll enabled.
redraw!
" Switching to scrollbar resets the offset but must also not crash.
set tabpanelopt=scrollbar
redraw!
" Disabling scroll returns to normal behavior.
set tabpanelopt=
redraw!
" Right alignment with scrollbar.
set tabpanelopt=align:right,scrollbar
redraw!
" Vertical separator with scrollbar.
set tabpanelopt=columns:10,vert,scrollbar
redraw!
" Cleanup.
%bwipeout!
let &tabpanelopt = save_tabpanelopt
let &showtabpanel = save_showtabpanel
let &lines = save_lines
endfunc
func Test_tabpanel_variable_height()
CheckFeature tabpanel
let save_lines = &lines
let save_showtabpanel = &showtabpanel
+2
View File
@@ -729,6 +729,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
386,
/**/
385,
/**/