From: Yasuhiro Matsumoto Date: Tue, 21 Apr 2026 20:20:30 +0000 (+0000) Subject: patch 9.2.0386: No scroll/scrollbar support in the tabpanel X-Git-Tag: v9.2.0386^0 X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=2ea4a7c3b73468683249fe194707c8d7e67ecdb3;p=thirdparty%2Fvim.git 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 Signed-off-by: Christian Brabandt --- diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index c02d40eaa8..d2611db095 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -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'. diff --git a/runtime/doc/tabpage.txt b/runtime/doc/tabpage.txt index 3e307b648a..3130c185f8 100644 --- a/runtime/doc/tabpage.txt +++ b/runtime/doc/tabpage.txt @@ -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 || or || 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* diff --git a/runtime/doc/tags b/runtime/doc/tags index 2b0e6cab39..c4cc01bdc9 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -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* diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index 90d563c902..90cd1a3efb 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -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 ~ ----------------- diff --git a/src/buffer.c b/src/buffer.c index 7ea9e93511..44e504c537 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -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 %. 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; diff --git a/src/mouse.c b/src/mouse.c index f6ab770040..93fdd31824 100644 --- a/src/mouse.c +++ b/src/mouse.c @@ -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. diff --git a/src/proto/tabpanel.pro b/src/proto/tabpanel.pro index d62d13f3a1..72726d89ea 100644 --- a/src/proto/tabpanel.pro +++ b/src/proto/tabpanel.pro @@ -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 : */ diff --git a/src/tabpanel.c b/src/tabpanel.c index 4138f9760a..07be48e44c 100644 --- a/src/tabpanel.c +++ b/src/tabpanel.c @@ -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 diff --git a/src/testdir/test_tabpanel.vim b/src/testdir/test_tabpanel.vim index 7803a0eb4d..427360aeda 100644 --- a/src/testdir/test_tabpanel.vim +++ b/src/testdir/test_tabpanel.vim @@ -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 diff --git a/src/version.c b/src/version.c index aa38e1a35c..36a8da0782 100644 --- a/src/version.c +++ b/src/version.c @@ -729,6 +729,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 386, /**/ 385, /**/