Files
vim-mirror/src/tabpanel.c
T
Hirohito Higashi 5ef1eec5c5 patch 9.2.0467: multi-line statusline loses highlighting attributes
Problem:  In a multi-line statusline (and 'tabpanel'), %#XX# / %N*
          set on one row do not persist on subsequent rows.
          build_stl_str_hl_local() rebuilds stl_items[] from scratch
          on every line break ("%@" or "\n"), so the highlight is
          reset at each row boundary even though within a row it
          stays until %* (or another %# / %*).
Solution: Carry the last Highlight item's stl_minwid across line
          breaks via a new in/out int* parameter "carry_hl".  At the
          start of each row, pre-insert a Highlight item from the
          carried value so the row begins under the same highlight;
          before returning, update the carried value with the row's
          final Highlight item.  Apply the same carry to the
          tabpanel rendering loop (Hirohito Higashi).

related: #19123
closes:  #20180

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
2026-05-10 18:14:01 +00:00

1018 lines
25 KiB
C

/* vi:set ts=8 sts=4 sw=4 noet:
*
* VIM - Vi IMproved by Bram Moolenaar
*
* Do ":help uganda" in Vim to read copying and usage conditions.
* Do ":help credits" in Vim to see a list of people who contributed.
* See README.txt for an overview of the Vim source code.
*/
/*
* tabpanel.c:
*/
#include "vim.h"
#if defined(FEAT_TABPANEL)
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);
static void draw_tabpanel_scrollbar(int screen_col);
// set pcurtab_row. don't redraw tabpanel.
#define TPLMODE_GET_CURTAB_ROW 0
// set ptabpagenr. don't redraw tabpanel.
#define TPLMODE_GET_TABPAGENR 1
// redraw tabpanel.
#define TPLMODE_REDRAW 2
#define TPL_FILLCHAR ' '
#define VERT_LEN 1
#define SCROLL_LEN 1
// tpl_align's values
#define ALIGN_LEFT 0
#define ALIGN_RIGHT 1
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 bool tpl_is_vert = 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
static tabpage_T *tpl_last_curtab = NULL; // last curtab seen by draw_tabpanel
typedef struct {
win_T *wp;
win_T *cwp;
char_u *user_defined;
int maxrow;
int offsetrow;
int *prow;
int *pcol;
int attr;
int col_start;
int col_end;
} tabpanel_T;
int
tabpanelopt_changed(void)
{
char_u *p;
int new_align = ALIGN_LEFT;
long new_columns = 20;
bool new_is_vert = false;
bool new_scrollbar = false;
p = p_tplo;
while (*p != NUL)
{
if (STRNCMP(p, "align:", 6) == 0)
{
p += 6;
if (STRNCMP(p, "left", 4) == 0)
{
p += 4;
new_align = ALIGN_LEFT;
}
else if (STRNCMP(p, "right", 5) == 0)
{
p += 5;
new_align = ALIGN_RIGHT;
}
else
return FAIL;
}
else if (STRNCMP(p, "columns:", 8) == 0 && VIM_ISDIGIT(p[8]))
{
p += 8;
new_columns = getdigits(&p);
if (new_columns < 0 || new_columns > 1000)
return FAIL;
}
else if (STRNCMP(p, "vert", 4) == 0)
{
p += 4;
new_is_vert = true;
}
else if (STRNCMP(p, "scrollbar", 9) == 0)
{
p += 9;
new_scrollbar = true;
}
if (*p != ',' && *p != NUL)
return FAIL;
if (*p == ',')
++p;
}
tpl_align = new_align;
tpl_columns = new_columns;
tpl_is_vert = new_is_vert;
tpl_scrollbar = new_scrollbar;
// Re-center the current tab on the next redraw.
tpl_last_curtab = NULL;
shell_new_columns();
return OK;
}
/*
* Drop any internal reference to "tp", so draw_tabpanel() never compares
* against a dangling pointer after the tabpage has been freed.
*/
void
tabpanel_forget_tabpage(const tabpage_T *tp)
{
if (tpl_last_curtab == tp)
tpl_last_curtab = NULL;
}
/*
* Return the width of tabpanel.
*/
int
tabpanel_width(void)
{
switch (p_stpl)
{
case 0:
return 0;
case 1:
if (first_tabpage->tp_next == NULL)
return 0;
}
if (Columns < tpl_columns)
return 0;
else
return tpl_columns;
}
/*
* Return the offset of a window considering the width of tabpanel.
*/
int
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++;
}
}
/*
* Ensure the current tab is visible by adjusting tpl_scroll_offset when
* the selected tab has changed since the previous redraw. Mouse wheel or
* scrollbar drag operations leave curtab unchanged, so the user's chosen
* offset is preserved in those cases.
*/
static void
follow_curtab_if_needed(int curtab_row)
{
if (Rows <= 0 || curtab == tpl_last_curtab)
return;
if (curtab_row < tpl_scroll_offset)
tpl_scroll_offset = curtab_row;
else if (curtab_row >= tpl_scroll_offset + Rows)
tpl_scroll_offset = curtab_row - Rows + 1;
int max_offset = tpl_total_rows > Rows ? tpl_total_rows - Rows : 0;
if (tpl_scroll_offset < 0)
tpl_scroll_offset = 0;
else if (tpl_scroll_offset > max_offset)
tpl_scroll_offset = max_offset;
}
/*
* draw the tabpanel.
*/
void
draw_tabpanel(void)
{
int saved_KeyTyped = KeyTyped;
int saved_got_int = got_int;
int maxwidth = tabpanel_width();
int vs_attr = HL_ATTR(HLF_C);
int curtab_row = 0;
int vsrow = 0;
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;
int sb_len = tpl_scrollbar ? SCROLL_LEN : 0;
int sb_screen_col = -1;
// The scrollbar is always placed at the right edge of the tabpanel,
// regardless of 'align'. The vertical separator sits at the panel's
// boundary with the buffer area (left edge for align:right, right edge
// for align:left).
if (tpl_is_vert)
{
if (is_right)
{
// Panel on the right: vert at panel's left edge, scrollbar at
// panel's right edge (= screen's right edge).
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, VERT_LEN,
maxwidth - sb_len, &curtab_row, NULL);
follow_curtab_if_needed(curtab_row);
do_by_tplmode(TPLMODE_REDRAW, VERT_LEN, maxwidth - sb_len,
&curtab_row, NULL);
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 + maxwidth - SCROLL_LEN;
}
else
{
// Panel on the left: scrollbar just left of vert, vert at
// panel's right edge (boundary with buffer).
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, 0,
maxwidth - VERT_LEN - sb_len, &curtab_row, NULL);
follow_curtab_if_needed(curtab_row);
do_by_tplmode(TPLMODE_REDRAW, 0, maxwidth - VERT_LEN - sb_len,
&curtab_row, NULL);
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
{
if (is_right)
{
// Panel on the right, no vert: scrollbar at screen's right edge.
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, 0, maxwidth - sb_len,
&curtab_row, NULL);
follow_curtab_if_needed(curtab_row);
do_by_tplmode(TPLMODE_REDRAW, 0, maxwidth - sb_len,
&curtab_row, NULL);
if (tpl_scrollbar)
sb_screen_col = topframe->fr_width + maxwidth - SCROLL_LEN;
}
else
{
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, 0, maxwidth - sb_len,
&curtab_row, NULL);
follow_curtab_if_needed(curtab_row);
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.
KeyTyped = saved_KeyTyped;
tpl_last_curtab = curtab;
redraw_tabpanel = FALSE;
}
/*
* Return tabpagenr when clicking and dragging in tabpanel.
*/
int
get_tabpagenr_on_tabpanel(void)
{
int maxwidth = tabpanel_width();
int curtab_row = 0;
int tabpagenr = 0;
if (maxwidth == 0)
return -1;
do_by_tplmode(TPLMODE_GET_CURTAB_ROW, 0, maxwidth, &curtab_row, NULL);
do_by_tplmode(TPLMODE_GET_TABPAGENR, 0, maxwidth, &curtab_row,
&tabpagenr);
return tabpagenr;
}
/*
* Fill tailing area between {start_row} and {end_row - 1}.
*/
static void
screen_fill_tailing_area(
int tplmode,
int row_start,
int row_end,
int col_start,
int col_end,
int attr)
{
int is_right = tpl_align == ALIGN_RIGHT;
if (tplmode == TPLMODE_REDRAW)
screen_fill(row_start, row_end,
(is_right ? topframe->fr_width : 0) + col_start,
(is_right ? topframe->fr_width : 0) + col_end,
TPL_FILLCHAR, TPL_FILLCHAR, attr);
}
/*
* screen_puts_len() for tabpanel.
*/
static void
screen_puts_len_for_tabpanel(
int tplmode,
char_u *p,
int len,
int attr,
tabpanel_T *pargs)
{
int j;
int chlen;
int chcells;
char_u buf[IOSIZE];
char_u *temp;
for (j = 0; j < len;)
{
if (tplmode != TPLMODE_GET_CURTAB_ROW
&& pargs->maxrow <= *pargs->prow - pargs->offsetrow)
break;
if (has_mbyte)
chlen = (*mb_ptr2len)(p + j);
else
chlen = 1;
for (int k = 0; k < chlen; k++)
buf[k] = p[j + k];
buf[chlen] = NUL;
j += chlen;
// Make all characters printable.
temp = transstr(buf);
if (temp != NULL)
{
vim_strncpy(buf, temp, sizeof(buf) - 1);
vim_free(temp);
}
if (has_mbyte)
chcells = (*mb_ptr2cells)(buf);
else
chcells = 1;
if (pargs->col_end < (*pargs->pcol) + chcells)
{
// fill the tailing area of current row.
if (*pargs->prow - pargs->offsetrow >= 0
&& *pargs->prow - pargs->offsetrow < pargs->maxrow)
screen_fill_tailing_area(tplmode,
*pargs->prow - pargs->offsetrow,
*pargs->prow - pargs->offsetrow + 1,
*pargs->pcol, pargs->col_end, attr);
*pargs->pcol = pargs->col_end;
if (pargs->col_end < chcells)
break;
}
if (*pargs->pcol + chcells <= pargs->col_end)
{
int off = (tpl_align == ALIGN_RIGHT)
? topframe->fr_width
: 0;
if (tplmode == TPLMODE_REDRAW
&& (*pargs->prow - pargs->offsetrow >= 0
&& *pargs->prow - pargs->offsetrow < pargs->maxrow))
screen_puts(buf, *pargs->prow - pargs->offsetrow,
*pargs->pcol + off, attr);
*pargs->pcol += chcells;
}
}
}
/*
* default tabpanel drawing behavior if 'tabpanel' option is empty.
*/
static void
draw_tabpanel_default(int tplmode, tabpanel_T *pargs)
{
int modified;
int wincount;
int len = 0;
char_u buf[2] = { NUL, NUL };
modified = FALSE;
for (wincount = 0; pargs->wp != NULL;
pargs->wp = pargs->wp->w_next, ++wincount)
if (bufIsChanged(pargs->wp->w_buffer))
modified = TRUE;
if (modified || wincount > 1)
{
if (wincount > 1)
{
vim_snprintf((char *)NameBuff, MAXPATHL, "%d", wincount);
len = (int)STRLEN(NameBuff);
screen_puts_len_for_tabpanel(tplmode, NameBuff, len,
#if defined(FEAT_SYN_HL)
hl_combine_attr(pargs->attr, HL_ATTR(HLF_T)),
#else
pargs->attr,
#endif
pargs);
}
if (modified)
{
buf[0] = '+';
screen_puts_len_for_tabpanel(tplmode, buf, 1, pargs->attr, pargs);
}
buf[0] = TPL_FILLCHAR;
screen_puts_len_for_tabpanel(tplmode, buf, 1, pargs->attr, pargs);
}
get_trans_bufname(pargs->cwp->w_buffer);
shorten_dir(NameBuff);
len = (int)STRLEN(NameBuff);
screen_puts_len_for_tabpanel(tplmode, NameBuff, len, pargs->attr, pargs);
// fill the tailing area of current row.
if (*pargs->prow - pargs->offsetrow >= 0
&& *pargs->prow - pargs->offsetrow < pargs->maxrow)
screen_fill_tailing_area(tplmode, *pargs->prow - pargs->offsetrow,
*pargs->prow - pargs->offsetrow + 1,
*pargs->pcol, pargs->col_end, pargs->attr);
*pargs->pcol = pargs->col_end;
}
/*
* Draw tabpanel content with highlight handling.
* Processes hltab entries and fills tailing area.
*/
static void
draw_tabpanel_with_highlight(
int tplmode,
char_u *buf,
stl_hlrec_T *hltab,
tabpanel_T *pargs)
{
char_u *p;
int curattr;
int n;
curattr = pargs->attr;
p = buf;
for (n = 0; hltab[n].start != NULL; n++)
{
screen_puts_len_for_tabpanel(tplmode, p, (int)(hltab[n].start - p),
curattr, pargs);
p = hltab[n].start;
if (hltab[n].userhl == 0)
curattr = pargs->attr;
else if (hltab[n].userhl < 0)
curattr = syn_id2attr(-hltab[n].userhl);
#ifdef FEAT_TERMINAL
else if (pargs->wp != NULL && pargs->wp != curwin
&& bt_terminal(pargs->wp->w_buffer)
&& pargs->wp->w_status_height != 0)
curattr = highlight_stltermnc[hltab[n].userhl - 1];
else if (pargs->wp != NULL && bt_terminal(pargs->wp->w_buffer)
&& pargs->wp->w_status_height != 0)
curattr = highlight_stlterm[hltab[n].userhl - 1];
#endif
else if (pargs->wp != NULL && pargs->wp != curwin
&& pargs->wp->w_status_height != 0)
curattr = highlight_stlnc[hltab[n].userhl - 1];
else
curattr = highlight_user[hltab[n].userhl - 1];
}
screen_puts_len_for_tabpanel(tplmode, p, (int)STRLEN(p), curattr, pargs);
// fill the tailing area of current row.
if (*pargs->prow - pargs->offsetrow >= 0
&& *pargs->prow - pargs->offsetrow < pargs->maxrow)
screen_fill_tailing_area(tplmode, *pargs->prow - pargs->offsetrow,
*pargs->prow - pargs->offsetrow + 1, *pargs->pcol,
pargs->col_end, curattr);
*pargs->pcol = pargs->col_end;
}
/*
* do something by tplmode for drawing tabpanel.
*/
static void
do_by_tplmode(
int tplmode,
int col_start,
int col_end,
int *pcurtab_row,
int *ptabpagenr)
{
int attr_tplf = HL_ATTR(HLF_TPLF);
int attr_tpls = HL_ATTR(HLF_TPLS);
int attr_tpl = HL_ATTR(HLF_TPL);
int col = col_start;
int row = 0;
tabpage_T *tp = NULL;
typval_T v;
tabpanel_T args;
args.maxrow = Rows;
args.offsetrow = 0;
args.col_start = col_start;
args.col_end = col_end;
if (tplmode != TPLMODE_GET_CURTAB_ROW && args.maxrow > 0)
args.offsetrow = tpl_scroll_offset;
tp = first_tabpage;
for (row = 0; tp != NULL; row++)
{
if (tplmode != TPLMODE_GET_CURTAB_ROW
&& args.maxrow <= row - args.offsetrow)
break;
col = col_start;
v.v_type = VAR_NUMBER;
v.vval.v_number = tabpage_index(tp);
set_var((char_u *)"g:actual_curtabpage", &v, TRUE);
if (tp->tp_topframe == topframe)
{
args.attr = attr_tpls;
if (tplmode == TPLMODE_GET_CURTAB_ROW)
// Capture the row of the current tab and keep iterating so
// tpl_total_rows receives the true content height below.
*pcurtab_row = row;
}
else
args.attr = attr_tpl;
if (tp == curtab)
{
args.cwp = curwin;
args.wp = firstwin;
}
else
{
args.cwp = tp->tp_curwin;
args.wp = tp->tp_firstwin;
}
char_u *usefmt = vim_strsave(p_tpl);
if (usefmt != NULL && *usefmt != NUL)
{
int carry_hl = 0;
while (*usefmt != NUL)
{
char_u buf[IOSIZE];
stl_hlrec_T *hltab;
stl_hlrec_T *tabtab;
stl_clickrec_T *clicktab = NULL;
if (tplmode != TPLMODE_GET_CURTAB_ROW
&& args.maxrow <= row - args.offsetrow)
break;
buf[0] = NUL;
#ifdef ENABLE_STL_MODE_MULTI_NL
(void)build_stl_str_hl_mline_nl
#else
(void)build_stl_str_hl_mline
#endif
(args.cwp, buf, sizeof(buf),
&usefmt, opt_name, opt_scope, TPL_FILLCHAR,
args.col_end - args.col_start, &hltab, &tabtab,
tplmode == TPLMODE_REDRAW ? &clicktab : NULL,
&carry_hl);
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)
{
row++;
col = col_start;
}
}
}
else
{
args.user_defined = NULL;
args.prow = &row;
args.pcol = &col;
draw_tabpanel_default(tplmode, &args);
}
vim_free(usefmt);
do_unlet((char_u *)"g:actual_curtabpage", TRUE);
tp = tp->tp_next;
if ((tplmode == TPLMODE_GET_TABPAGENR)
&& (mouse_row <= (row - args.offsetrow)))
{
*ptabpagenr = v.vval.v_number;
break;
}
}
// 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_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)
{
int max_offset = tpl_total_rows - Rows;
int track_range;
thumb_height = Rows * Rows / tpl_total_rows;
if (thumb_height < 1)
thumb_height = 1;
// Map tpl_scroll_offset onto the track: at offset 0 the thumb's top
// is at row 0, at the maximum offset its bottom reaches the last
// row. This is the exact inverse of tabpanel_drag_scrollbar().
track_range = Rows - thumb_height;
if (track_range > 0 && max_offset > 0)
thumb_top = track_range * tpl_scroll_offset / max_offset;
else
thumb_top = 0;
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.
*/
bool
tabpanel_scroll(int dir, int count)
{
int max_offset;
int new_offset;
if (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;
}
/*
* Set the tabpanel scroll offset to "offset" (clamped to the valid range).
* Returns true if the offset changed and a redraw was scheduled.
*/
bool
tabpanel_set_offset(int offset)
{
int max_offset;
if (tabpanel_width() == 0)
return false;
max_offset = tpl_total_rows - Rows;
if (max_offset < 0)
max_offset = 0;
if (offset < 0)
offset = 0;
if (offset > max_offset)
offset = max_offset;
if (offset == tpl_scroll_offset)
return false;
tpl_scroll_offset = offset;
redraw_tabpanel = TRUE;
return true;
}
/*
* "tabpanel_getinfo()" function
*/
void
f_tabpanel_getinfo(typval_T *argvars UNUSED, typval_T *rettv)
{
dict_T *d;
int max_offset;
if (rettv_dict_alloc(rettv) == FAIL)
return;
d = rettv->vval.v_dict;
max_offset = tpl_total_rows - Rows;
if (max_offset < 0)
max_offset = 0;
dict_add_string(d, "align",
(char_u *)(tpl_align == ALIGN_RIGHT ? "right" : "left"));
dict_add_number(d, "columns", tabpanel_width());
dict_add_bool(d, "scrollbar", tpl_scrollbar);
dict_add_number(d, "offset", tpl_scroll_offset);
dict_add_number(d, "total", tpl_total_rows);
dict_add_number(d, "max_offset", max_offset);
}
/*
* "tabpanel_scroll()" function
*/
void
f_tabpanel_scroll(typval_T *argvars, typval_T *rettv)
{
varnumber_T n;
int absolute = 0;
bool changed;
rettv->v_type = VAR_BOOL;
rettv->vval.v_number = VVAL_FALSE;
if (in_vim9script()
&& (check_for_number_arg(argvars, 0) == FAIL
|| check_for_opt_dict_arg(argvars, 1) == FAIL))
return;
n = tv_get_number_chk(&argvars[0], NULL);
if (argvars[1].v_type != VAR_UNKNOWN)
{
if (argvars[1].v_type != VAR_DICT || argvars[1].vval.v_dict == NULL)
{
emsg(_(e_dictionary_required));
return;
}
absolute = dict_get_bool(argvars[1].vval.v_dict, "absolute", FALSE);
}
// Clamp to int range to avoid signed overflow when casting and negating.
if (n > INT_MAX)
n = INT_MAX;
else if (n < -INT_MAX)
n = -INT_MAX;
if (absolute)
changed = tabpanel_set_offset((int)n);
else
changed = tabpanel_scroll(n >= 0 ? 1 : -1,
(int)(n >= 0 ? n : -n));
rettv->vval.v_number = changed ? VVAL_TRUE : VVAL_FALSE;
}
#endif // FEAT_TABPANEL