From ab58541e07fedf4a3dc63e943482d648f2b9acff Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Tue, 14 Apr 2026 20:32:57 +0900 Subject: [PATCH 1/3] Add scroll/scrollbar support to tabpanel New 'tabpanelopt' tokens: - "scroll": enable mouse wheel scrolling over the tabpanel area - "scrollbar": reserve a 1-column bar showing scroll position (implies scroll) The tabpanel previously had no way to view content that exceeded the visible row count. With these options, wheel events in the panel region adjust a persistent scroll offset, and the optional scrollbar renders the thumb using PmenuSbar/PmenuThumb highlight groups. Total content height is measured during the GET_CURTAB_ROW pass, which runs through every tab without the REDRAW-time maxrow clamp. --- src/mouse.c | 22 ++++++ src/proto/tabpanel.pro | 2 + src/tabpanel.c | 168 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 179 insertions(+), 13 deletions(-) diff --git a/src/mouse.c b/src/mouse.c index 45ffd4b513ae37..6bd752d48d8312 100644 --- a/src/mouse.c +++ b/src/mouse.c @@ -1276,6 +1276,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 +2420,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 d62d13f3a1099b..1d5104d26fc81e 100644 --- a/src/proto/tabpanel.pro +++ b/src/proto/tabpanel.pro @@ -4,4 +4,6 @@ int tabpanel_width(void); int tabpanel_leftcol(void); void draw_tabpanel(void); int get_tabpagenr_on_tabpanel(void); +int mouse_on_tabpanel(void); +int tabpanel_scroll(int dir, int count); /* vim: set ft=c : */ diff --git a/src/tabpanel.c b/src/tabpanel.c index 4138f9760a7795..0e5fba79b08e31 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 @@ -41,6 +43,10 @@ static int opt_scope = OPT_LOCAL; static int tpl_align = ALIGN_LEFT; static int tpl_columns = 20; static int tpl_is_vert = FALSE; +static int tpl_scroll = FALSE; +static int tpl_scrollbar = FALSE; +static int tpl_scroll_offset = 0; +static int tpl_total_rows = 0; typedef struct { win_T *wp; @@ -62,6 +68,8 @@ tabpanelopt_changed(void) int new_align = ALIGN_LEFT; long new_columns = 20; int new_is_vert = FALSE; + int new_scroll = FALSE; + int new_scrollbar = FALSE; p = p_tplo; while (*p != NUL) @@ -94,6 +102,17 @@ tabpanelopt_changed(void) p += 4; 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) return FAIL; @@ -104,6 +123,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 +289,64 @@ 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; + } } + 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 +604,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 +632,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 +666,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 +736,89 @@ 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++) + { + int 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. + */ + int +mouse_on_tabpanel(void) +{ + if (tabpanel_width() == 0) + return FALSE; + return mouse_col < firstwin->w_wincol + || mouse_col >= firstwin->w_wincol + topframe->fr_width; +} + +/* + * 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". + */ + int +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 From efade9a399709aad8d05773609c685e9d69b41c7 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Tue, 14 Apr 2026 21:02:42 +0900 Subject: [PATCH 2/3] Make tabpanel scrollbar draggable Clicking on the scrollbar column now centres the thumb under the pointer and starts a drag; subsequent drag events track the pointer until release. Scrollbar clicks preempt tab selection. The scrollbar column is cached by draw_tabpanel() so mouse.c can tell a scrollbar click from a regular tabpanel click without recomputing layout. --- src/mouse.c | 33 ++++++++++++++++++++++++- src/proto/tabpanel.pro | 2 ++ src/tabpanel.c | 55 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/mouse.c b/src/mouse.c index 6bd752d48d8312..920587cbd8e4e1 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 int 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. diff --git a/src/proto/tabpanel.pro b/src/proto/tabpanel.pro index 1d5104d26fc81e..49efe88d217c1f 100644 --- a/src/proto/tabpanel.pro +++ b/src/proto/tabpanel.pro @@ -5,5 +5,7 @@ int tabpanel_leftcol(void); void draw_tabpanel(void); int get_tabpagenr_on_tabpanel(void); int mouse_on_tabpanel(void); +int mouse_on_tabpanel_scrollbar(void); +int tabpanel_drag_scrollbar(int screen_row); int tabpanel_scroll(int dir, int count); /* vim: set ft=c : */ diff --git a/src/tabpanel.c b/src/tabpanel.c index 0e5fba79b08e31..7c8c021669a9ad 100644 --- a/src/tabpanel.c +++ b/src/tabpanel.c @@ -47,6 +47,7 @@ static int tpl_scroll = FALSE; static int 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; @@ -344,6 +345,7 @@ draw_tabpanel(void) } } + tpl_scrollbar_col = sb_screen_col; if (sb_screen_col >= 0) draw_tabpanel_scrollbar(sb_screen_col); @@ -790,6 +792,59 @@ mouse_on_tabpanel(void) || 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. + */ + int +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). + */ + int +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. From f067dc31de2c87ac7b7612a3f9ee3f260cb9defc Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 15 Apr 2026 08:21:35 +0900 Subject: [PATCH 3/3] Add tests for tabpanel scroll/scrollbar options Verify that 'tabpanelopt' accepts the new "scroll" and "scrollbar" values alone and in combination with "align:", "columns:" and "vert", and that a screen-overflowing tab list renders without crashes. --- src/testdir/test_tabpanel.vim | 61 +++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/testdir/test_tabpanel.vim b/src/testdir/test_tabpanel.vim index 7803a0eb4dc3cf..53fa5db5963ae9 100644 --- a/src/testdir/test_tabpanel.vim +++ b/src/testdir/test_tabpanel.vim @@ -962,6 +962,67 @@ 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