mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-06-29 20:32:38 +00:00
Merge branch 'bvolpato/vertical-tab-bar-side' of https://github.com/bvolpato/kitty
This commit is contained in:
commit
54a7841d18
13 changed files with 397 additions and 78 deletions
|
|
@ -2017,7 +2017,7 @@ class Boss:
|
|||
window.on_drop(drop)
|
||||
break
|
||||
elif tab_bar.left <= x < tab_bar.right and tab_bar.top <= y < tab_bar.bottom:
|
||||
if (tab_id := tm.tab_bar.tab_id_at(x)) and (tab := self.tab_for_id(tab_id)) and (w := tab.active_window):
|
||||
if (tab_id := tm.tab_bar.tab_id_at(x, y)) and (tab := self.tab_for_id(tab_id)) and (w := tab.active_window):
|
||||
w.on_drop(drop)
|
||||
|
||||
def on_drag_source_finished(
|
||||
|
|
|
|||
|
|
@ -1010,6 +1010,8 @@ mouse_region(bool detect_borders, bool detect_title_bar) {
|
|||
const bool in_central = mouse_in_region(¢ral);
|
||||
if (!in_central) {
|
||||
if (
|
||||
(tab_bar.left < central.left && w->mouse_x < central.left) ||
|
||||
(tab_bar.right > central.right && w->mouse_x >= central.right) ||
|
||||
(tab_bar.top < central.top && w->mouse_y < central.top) ||
|
||||
(tab_bar.bottom > central.bottom && w->mouse_y >= central.bottom)
|
||||
) ans.in_tab_bar = true;
|
||||
|
|
|
|||
|
|
@ -1616,18 +1616,22 @@ agr('tabbar', 'Tab bar')
|
|||
|
||||
opt('tab_bar_edge', 'bottom',
|
||||
option_type='tab_bar_edge', ctype='int',
|
||||
long_text='The edge to show the tab bar on, :code:`top` or :code:`bottom`.'
|
||||
long_text='The edge to show the tab bar on, :code:`top`, :code:`bottom`, :code:`left` or :code:`right`.'
|
||||
)
|
||||
|
||||
opt('tab_bar_margin_width', '0.0',
|
||||
option_type='positive_float',
|
||||
long_text='The margin to the left and right of the tab bar (in pts).'
|
||||
long_text='''
|
||||
The margin perpendicular to the tab bar edge (in pts). For tab bars on the
|
||||
top or bottom this is the margin to the left and right. For tab bars on the
|
||||
left or right this is the margin above and below.
|
||||
'''
|
||||
)
|
||||
|
||||
opt('tab_bar_margin_height', '0.0 0.0',
|
||||
option_type='tab_bar_margin_height', ctype='!tab_bar_margin_height',
|
||||
long_text='''
|
||||
The margin above and below the tab bar (in pts). The first number is the margin
|
||||
The margin along the tab bar edge (in pts). The first number is the margin
|
||||
between the edge of the OS Window and the tab bar. The second number is the
|
||||
margin between the tab bar and the contents of the current tab.
|
||||
'''
|
||||
|
|
@ -1675,11 +1679,12 @@ tab navigation actions such as :ac:`goto_tab`, :ac:`next_tab`, :ac:`previous_tab
|
|||
are automatically restricted to work only on matching tabs.
|
||||
''')
|
||||
|
||||
opt('tab_bar_align', 'left',
|
||||
choices=('left', 'center', 'right'),
|
||||
opt('tab_bar_align', 'start',
|
||||
choices=('start', 'center', 'end', 'left', 'right'),
|
||||
long_text='''
|
||||
The horizontal alignment of the tab bar, can be one of: :code:`left`,
|
||||
:code:`center`, :code:`right`.
|
||||
The alignment of the tab bar, can be one of: :code:`start`, :code:`center`,
|
||||
:code:`end`, :code:`left`, :code:`right`. The values :code:`left` and
|
||||
:code:`right` are aliases for :code:`start` and :code:`end` respectively.
|
||||
'''
|
||||
)
|
||||
|
||||
|
|
@ -1746,10 +1751,12 @@ this is rendered.
|
|||
)
|
||||
|
||||
opt('tab_title_max_length', '0',
|
||||
option_type='positive_int',
|
||||
option_type='positive_int', ctype='int',
|
||||
long_text='''
|
||||
The maximum number of cells that can be used to render the text in a tab.
|
||||
A value of zero means that no limit is applied.
|
||||
A value of zero means that no limit is applied. For vertical tab bars, kitty
|
||||
uses a default sidebar width sized for about twenty title cells when this is
|
||||
left unset.
|
||||
'''
|
||||
)
|
||||
|
||||
|
|
|
|||
4
kitty/options/parse.py
generated
4
kitty/options/parse.py
generated
|
|
@ -1356,7 +1356,7 @@ class Parser:
|
|||
raise ValueError(f"The value {val} is not a valid choice for tab_bar_align")
|
||||
ans["tab_bar_align"] = val
|
||||
|
||||
choices_for_tab_bar_align = frozenset(('left', 'center', 'right'))
|
||||
choices_for_tab_bar_align = frozenset(('start', 'center', 'end', 'left', 'right'))
|
||||
|
||||
def tab_bar_background(self, val: str, ans: dict[str, typing.Any]) -> None:
|
||||
ans['tab_bar_background'] = to_color_or_none(val)
|
||||
|
|
@ -1558,7 +1558,7 @@ class Parser:
|
|||
raise ValueError(f"The value {val} is not a valid choice for window_title_bar_align")
|
||||
ans["window_title_bar_align"] = val
|
||||
|
||||
choices_for_window_title_bar_align = choices_for_tab_bar_align
|
||||
choices_for_window_title_bar_align = frozenset(('left', 'center', 'right'))
|
||||
|
||||
def window_title_bar_inactive_background(self, val: str, ans: dict[str, typing.Any]) -> None:
|
||||
ans['window_title_bar_inactive_background'] = to_color_or_none(val)
|
||||
|
|
|
|||
15
kitty/options/to-c-generated.h
generated
15
kitty/options/to-c-generated.h
generated
|
|
@ -1110,6 +1110,19 @@ convert_from_opts_tab_bar_style(PyObject *py_opts, Options *opts) {
|
|||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_python_tab_title_max_length(PyObject *val, Options *opts) {
|
||||
opts->tab_title_max_length = PyLong_AsLong(val);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_opts_tab_title_max_length(PyObject *py_opts, Options *opts) {
|
||||
PyObject *ret = PyObject_GetAttrString(py_opts, "tab_title_max_length");
|
||||
if (ret == NULL) return;
|
||||
convert_from_python_tab_title_max_length(ret, opts);
|
||||
Py_DECREF(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
convert_from_python_tab_bar_background(PyObject *val, Options *opts) {
|
||||
opts->tab_bar_background = color_or_none_as_int(val);
|
||||
|
|
@ -1659,6 +1672,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) {
|
|||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_tab_bar_style(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_tab_title_max_length(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_tab_bar_background(py_opts, opts);
|
||||
if (PyErr_Occurred()) return false;
|
||||
convert_from_opts_tab_bar_margin_color(py_opts, opts);
|
||||
|
|
|
|||
6
kitty/options/types.py
generated
6
kitty/options/types.py
generated
|
|
@ -32,7 +32,7 @@ choices_for_pointer_shape_when_grabbed = choices_for_default_pointer_shape
|
|||
choices_for_progress_bar = typing.Literal['left', 'right', 'top', 'bottom', 'hidden']
|
||||
choices_for_scrollbar = typing.Literal['scrolled', 'always', 'never', 'hovered', 'scrolled-and-hovered']
|
||||
choices_for_strip_trailing_spaces = typing.Literal['always', 'never', 'smart']
|
||||
choices_for_tab_bar_align = typing.Literal['left', 'center', 'right']
|
||||
choices_for_tab_bar_align = typing.Literal['start', 'center', 'end', 'left', 'right']
|
||||
choices_for_tab_bar_style = typing.Literal['fade', 'hidden', 'powerline', 'separator', 'slant', 'custom']
|
||||
choices_for_tab_powerline_style = typing.Literal['angled', 'round', 'slanted']
|
||||
choices_for_tab_switch_strategy = typing.Literal['last', 'left', 'previous', 'right']
|
||||
|
|
@ -41,7 +41,7 @@ choices_for_undercurl_style = typing.Literal['thin-sparse', 'thin-dense', 'thick
|
|||
choices_for_underline_hyperlinks = typing.Literal['hover', 'always', 'never']
|
||||
choices_for_window_logo_position = choices_for_placement_strategy
|
||||
choices_for_window_title_bar = typing.Literal['top', 'bottom']
|
||||
choices_for_window_title_bar_align = choices_for_tab_bar_align
|
||||
choices_for_window_title_bar_align = typing.Literal['left', 'center', 'right']
|
||||
|
||||
option_names = (
|
||||
'action_alias',
|
||||
|
|
@ -666,7 +666,7 @@ class Options:
|
|||
strip_trailing_spaces: choices_for_strip_trailing_spaces = 'never'
|
||||
sync_to_monitor: bool = True
|
||||
tab_activity_symbol: str = ''
|
||||
tab_bar_align: choices_for_tab_bar_align = 'left'
|
||||
tab_bar_align: choices_for_tab_bar_align = 'start'
|
||||
tab_bar_background: kitty.fast_data_types.Color | None = None
|
||||
tab_bar_edge: int = 8
|
||||
tab_bar_filter: str = ''
|
||||
|
|
|
|||
|
|
@ -751,7 +751,12 @@ def tab_separator(x: str) -> str:
|
|||
|
||||
|
||||
def tab_bar_edge(x: str) -> int:
|
||||
return {'top': defines.TOP_EDGE, 'bottom': defines.BOTTOM_EDGE}.get(x.lower(), defines.BOTTOM_EDGE)
|
||||
return {
|
||||
'left': defines.LEFT_EDGE,
|
||||
'top': defines.TOP_EDGE,
|
||||
'right': defines.RIGHT_EDGE,
|
||||
'bottom': defines.BOTTOM_EDGE,
|
||||
}.get(x.lower(), defines.BOTTOM_EDGE)
|
||||
|
||||
|
||||
def tab_font_style(x: str) -> tuple[bool, bool]:
|
||||
|
|
|
|||
|
|
@ -691,29 +691,76 @@ pyset_borders_rects(PyObject *self UNUSED, PyObject *args) {
|
|||
}
|
||||
|
||||
|
||||
static unsigned
|
||||
vertical_tab_bar_cols(const OSWindow *os_window, long margin_outer, long margin_inner) {
|
||||
unsigned cell_width = MAX(1u, os_window->fonts_data->fcm.cell_width);
|
||||
long available_width = (long)os_window->viewport_width - margin_outer - margin_inner;
|
||||
if (available_width <= 0) return 0;
|
||||
unsigned available_cols = MAX(1u, (unsigned)available_width / cell_width);
|
||||
unsigned title_cols = OPT(tab_title_max_length) > 0 ? (unsigned)OPT(tab_title_max_length) : 20u;
|
||||
unsigned desired_cols = title_cols + 8u;
|
||||
unsigned soft_max = available_cols / 3u;
|
||||
if (soft_max < 6u) soft_max = available_cols;
|
||||
return MAX(1u, MIN(available_cols, MIN(desired_cols, MAX(1u, soft_max))));
|
||||
}
|
||||
|
||||
void
|
||||
os_window_regions(const OSWindow *os_window, Region *central, Region *tab_bar) {
|
||||
if (!OPT(tab_bar_hidden) && os_window->num_tabs && !os_window->has_too_few_tabs) {
|
||||
long margin_outer = pt_to_px_for_os_window(OPT(tab_bar_margin_height.outer), os_window);
|
||||
long margin_inner = pt_to_px_for_os_window(OPT(tab_bar_margin_height.inner), os_window);
|
||||
central->left = 0; central->right = os_window->viewport_width;
|
||||
unsigned tab_bar_height = os_window->fonts_data->fcm.cell_height + margin_inner + margin_outer;
|
||||
central->top = 0; central->bottom = os_window->viewport_height;
|
||||
switch(OPT(tab_bar_edge)) {
|
||||
case TOP_EDGE:
|
||||
case TOP_EDGE: {
|
||||
unsigned tab_bar_height = os_window->fonts_data->fcm.cell_height + margin_inner + margin_outer;
|
||||
central->top = tab_bar_height;
|
||||
central->bottom = os_window->viewport_height;
|
||||
central->top = MIN(central->top, central->bottom);
|
||||
tab_bar->top = margin_outer;
|
||||
tab_bar->left = central->left; tab_bar->right = central->right;
|
||||
tab_bar->bottom = tab_bar->top + os_window->fonts_data->fcm.cell_height;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
case LEFT_EDGE: {
|
||||
unsigned left_cols = vertical_tab_bar_cols(os_window, margin_outer, margin_inner);
|
||||
if (!left_cols) {
|
||||
zero_at_ptr(tab_bar);
|
||||
return;
|
||||
}
|
||||
unsigned left_width = left_cols * os_window->fonts_data->fcm.cell_width;
|
||||
central->left = MIN((long)(left_width + margin_inner + margin_outer), (long)central->right);
|
||||
tab_bar->left = margin_outer;
|
||||
tab_bar->right = tab_bar->left + left_width;
|
||||
tab_bar->top = central->top;
|
||||
tab_bar->bottom = central->bottom;
|
||||
break;
|
||||
}
|
||||
case RIGHT_EDGE: {
|
||||
unsigned right_cols = vertical_tab_bar_cols(os_window, margin_outer, margin_inner);
|
||||
if (!right_cols) {
|
||||
zero_at_ptr(tab_bar);
|
||||
return;
|
||||
}
|
||||
unsigned right_width = right_cols * os_window->fonts_data->fcm.cell_width;
|
||||
central->right = MAX(0, (long)os_window->viewport_width - (long)(right_width + margin_inner + margin_outer));
|
||||
tab_bar->left = central->right + margin_inner;
|
||||
tab_bar->right = tab_bar->left + right_width;
|
||||
tab_bar->top = central->top;
|
||||
tab_bar->bottom = central->bottom;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
unsigned tab_bar_height = os_window->fonts_data->fcm.cell_height + margin_inner + margin_outer;
|
||||
central->top = 0;
|
||||
long bottom = os_window->viewport_height - tab_bar_height;
|
||||
central->bottom = MAX(0, bottom);
|
||||
tab_bar->top = central->bottom + margin_inner;
|
||||
tab_bar->left = central->left; tab_bar->right = central->right;
|
||||
tab_bar->bottom = tab_bar->top + os_window->fonts_data->fcm.cell_height;
|
||||
break;
|
||||
}
|
||||
}
|
||||
tab_bar->left = central->left; tab_bar->right = central->right;
|
||||
tab_bar->bottom = tab_bar->top + os_window->fonts_data->fcm.cell_height;
|
||||
} else {
|
||||
zero_at_ptr(tab_bar);
|
||||
central->left = 0; central->top = 0; central->right = os_window->viewport_width;
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ typedef struct Options {
|
|||
bool dynamic_background_opacity;
|
||||
float inactive_text_alpha;
|
||||
Edge tab_bar_edge;
|
||||
int tab_title_max_length;
|
||||
DisableLigature disable_ligatures;
|
||||
bool force_ltr;
|
||||
bool resize_in_steps;
|
||||
|
|
|
|||
218
kitty/tab_bar.py
218
kitty/tab_bar.py
|
|
@ -17,8 +17,11 @@ from .fast_data_types import (
|
|||
BOTTOM_EDGE,
|
||||
DECAWM,
|
||||
Color,
|
||||
LEFT_EDGE,
|
||||
Region,
|
||||
RIGHT_EDGE,
|
||||
Screen,
|
||||
TOP_EDGE,
|
||||
background_opacity_of,
|
||||
cell_size_for_window,
|
||||
get_boss,
|
||||
|
|
@ -101,6 +104,31 @@ def as_rgb(x: int) -> int:
|
|||
return (x << 8) | 2
|
||||
|
||||
|
||||
VERTICAL_EDGES = frozenset({LEFT_EDGE, RIGHT_EDGE})
|
||||
MAX_VERTICAL_TAB_LINES = 2
|
||||
|
||||
|
||||
def is_vertical_edge(edge: int) -> bool:
|
||||
return edge in VERTICAL_EDGES
|
||||
|
||||
|
||||
def edge_name(edge: int) -> EdgeLiteral:
|
||||
return {
|
||||
LEFT_EDGE: 'left',
|
||||
TOP_EDGE: 'top',
|
||||
RIGHT_EDGE: 'right',
|
||||
BOTTOM_EDGE: 'bottom',
|
||||
}.get(edge, 'bottom')
|
||||
|
||||
|
||||
def normalized_tab_bar_align(align: str) -> str:
|
||||
if align == 'left':
|
||||
return 'start'
|
||||
if align == 'right':
|
||||
return 'end'
|
||||
return align
|
||||
|
||||
|
||||
@lru_cache
|
||||
def report_template_failure(template: str, e: str) -> None:
|
||||
log_error(f'Invalid tab title template: "{template}" with error: {e}')
|
||||
|
|
@ -342,7 +370,7 @@ def draw_tab_with_slant(
|
|||
extra_data: ExtraData
|
||||
) -> int:
|
||||
orig_fg = screen.cursor.fg
|
||||
left_sep, right_sep = ('', '') if draw_data.tab_bar_edge == 'top' else ('', '')
|
||||
left_sep, right_sep = ('', '') if draw_data.tab_bar_edge in ('top', 'left') else ('', '')
|
||||
tab_bg = screen.cursor.bg
|
||||
slant_fg = as_rgb(color_as_int(draw_data.default_bg))
|
||||
|
||||
|
|
@ -567,10 +595,18 @@ class CellRange(NamedTuple):
|
|||
|
||||
class TabExtent(NamedTuple):
|
||||
tab_id: int
|
||||
cell_range: CellRange
|
||||
x: CellRange
|
||||
y: CellRange = CellRange(0, 0)
|
||||
|
||||
def shifted(self, shift: int) -> 'TabExtent':
|
||||
return TabExtent(self.tab_id, CellRange(self.cell_range.start + shift, self.cell_range.end + shift))
|
||||
def shifted(self, x: int = 0, y: int = 0) -> 'TabExtent':
|
||||
return TabExtent(
|
||||
self.tab_id,
|
||||
CellRange(self.x.start + x, self.x.end + x),
|
||||
CellRange(self.y.start + y, self.y.end + y),
|
||||
)
|
||||
|
||||
def contains(self, x: int, y: int) -> bool:
|
||||
return self.x.start <= x <= self.x.end and self.y.start <= y <= self.y.end
|
||||
|
||||
|
||||
class TabBar:
|
||||
|
|
@ -588,6 +624,8 @@ class TabBar:
|
|||
def apply_options(self) -> None:
|
||||
opts = get_options()
|
||||
self.dirty = True
|
||||
self.tab_bar_edge = opts.tab_bar_edge
|
||||
self.is_vertical = is_vertical_edge(opts.tab_bar_edge)
|
||||
self.margin_width = pt_to_px(opts.tab_bar_margin_width, self.os_window_id)
|
||||
self.cell_width, cell_height = cell_size_for_window(self.os_window_id)
|
||||
if not hasattr(self, 'screen'):
|
||||
|
|
@ -618,7 +656,7 @@ class TabBar:
|
|||
opts.active_tab_title_template,
|
||||
opts.tab_activity_symbol,
|
||||
opts.tab_powerline_style,
|
||||
'bottom' if opts.tab_bar_edge == BOTTOM_EDGE else 'top',
|
||||
edge_name(opts.tab_bar_edge),
|
||||
opts.tab_title_max_length, self.os_window_id,
|
||||
)
|
||||
ts = opts.tab_bar_style
|
||||
|
|
@ -632,9 +670,16 @@ class TabBar:
|
|||
self.draw_func = load_custom_draw_tab()
|
||||
else:
|
||||
self.draw_func = draw_tab_with_fade
|
||||
if opts.tab_bar_align == 'center':
|
||||
self.tab_bar_align = normalized_tab_bar_align(opts.tab_bar_align)
|
||||
if self.tab_bar_align == 'center':
|
||||
self.align_factor = 2
|
||||
elif self.tab_bar_align == 'end':
|
||||
self.align_factor = 1
|
||||
else:
|
||||
self.align_factor = 0
|
||||
if self.tab_bar_align == 'center':
|
||||
self.align: Callable[[], None] = partial(self.align_with_factor, 2)
|
||||
elif opts.tab_bar_align == 'right':
|
||||
elif self.tab_bar_align == 'end':
|
||||
self.align = self.align_with_factor
|
||||
else:
|
||||
self.align = lambda: None
|
||||
|
|
@ -690,51 +735,96 @@ class TabBar:
|
|||
blank_rects: list[Border] = []
|
||||
bg = BorderColor.tab_bar_margin_color if opts.tab_bar_margin_color is not None else BorderColor.default_bg
|
||||
if opts.tab_bar_margin_height:
|
||||
if opts.tab_bar_edge == BOTTOM_EDGE:
|
||||
if self.is_vertical:
|
||||
if opts.tab_bar_edge == LEFT_EDGE:
|
||||
if opts.tab_bar_margin_height.outer:
|
||||
blank_rects.append(Border(0, 0, tab_bar.left, vh, bg))
|
||||
if opts.tab_bar_margin_height.inner:
|
||||
blank_rects.append(Border(tab_bar.right, 0, central.left, vh, bg))
|
||||
else:
|
||||
if opts.tab_bar_margin_height.outer:
|
||||
blank_rects.append(Border(tab_bar.right, 0, vw, vh, bg))
|
||||
if opts.tab_bar_margin_height.inner:
|
||||
blank_rects.append(Border(central.right, 0, tab_bar.left, vh, bg))
|
||||
elif opts.tab_bar_edge == BOTTOM_EDGE:
|
||||
if opts.tab_bar_margin_height.outer:
|
||||
blank_rects.append(Border(0, tab_bar.bottom, vw, vh, bg))
|
||||
if opts.tab_bar_margin_height.inner:
|
||||
blank_rects.append(Border(0, central.bottom, vw, tab_bar.top, bg))
|
||||
else: # top
|
||||
else: # top
|
||||
if opts.tab_bar_margin_height.outer:
|
||||
blank_rects.append(Border(0, 0, vw, tab_bar.top, bg))
|
||||
if opts.tab_bar_margin_height.inner:
|
||||
blank_rects.append(Border(0, tab_bar.bottom, vw, central.top, bg))
|
||||
g = self.window_geometry
|
||||
left_bg = right_bg = bg
|
||||
if opts.tab_bar_margin_color is None and (
|
||||
opacity := background_opacity_of(self.os_window_id)) is not None and opacity >= 1:
|
||||
left_bg = BorderColor.tab_bar_left_edge_color
|
||||
right_bg = BorderColor.tab_bar_right_edge_color
|
||||
if g.left > 0:
|
||||
blank_rects.append(Border(0, g.top, g.left, g.bottom, left_bg))
|
||||
if g.right < vw:
|
||||
blank_rects.append(Border(g.right, g.top, vw, g.bottom, right_bg))
|
||||
if self.is_vertical:
|
||||
if g.left > tab_bar.left:
|
||||
blank_rects.append(Border(tab_bar.left, g.top, g.left, g.bottom, bg))
|
||||
if g.right < tab_bar.right:
|
||||
blank_rects.append(Border(g.right, g.top, tab_bar.right, g.bottom, bg))
|
||||
if g.top > tab_bar.top:
|
||||
blank_rects.append(Border(g.left, tab_bar.top, g.right, g.top, bg))
|
||||
if g.bottom < tab_bar.bottom:
|
||||
blank_rects.append(Border(g.left, g.bottom, g.right, tab_bar.bottom, bg))
|
||||
else:
|
||||
left_bg = right_bg = bg
|
||||
if opts.tab_bar_margin_color is None and (
|
||||
opacity := background_opacity_of(self.os_window_id)) is not None and opacity >= 1:
|
||||
left_bg = BorderColor.tab_bar_left_edge_color
|
||||
right_bg = BorderColor.tab_bar_right_edge_color
|
||||
if g.left > tab_bar.left:
|
||||
blank_rects.append(Border(tab_bar.left, g.top, g.left, g.bottom, left_bg))
|
||||
if g.right < tab_bar.right:
|
||||
blank_rects.append(Border(g.right, g.top, tab_bar.right, g.bottom, right_bg))
|
||||
if g.top > tab_bar.top:
|
||||
blank_rects.append(Border(g.left, tab_bar.top, g.right, g.top, bg))
|
||||
if g.bottom < tab_bar.bottom:
|
||||
blank_rects.append(Border(g.left, g.bottom, g.right, tab_bar.bottom, bg))
|
||||
self.blank_rects = tuple(blank_rects)
|
||||
|
||||
def layout(self) -> None:
|
||||
central, tab_bar, vw, vh, cell_width, cell_height = viewport_for_window(self.os_window_id)
|
||||
if tab_bar.width < 2:
|
||||
if self.is_vertical:
|
||||
if tab_bar.width < cell_width or tab_bar.height < cell_height:
|
||||
return
|
||||
elif tab_bar.width < 2:
|
||||
return
|
||||
self.cell_width = cell_width
|
||||
self.cell_height = cell_height
|
||||
s = self.screen
|
||||
available_width = tab_bar.width - 2 * self.margin_width
|
||||
ncells = max(4, available_width // cell_width)
|
||||
s.resize(1, ncells)
|
||||
s.reset_mode(DECAWM)
|
||||
cell_area_width = ncells * cell_width
|
||||
available_width_for_left_margin = max(0, tab_bar.width - self.margin_width - cell_area_width)
|
||||
extra_width = max(0, tab_bar.width - 2 * self.margin_width - cell_area_width)
|
||||
left_margin = min(self.margin_width + extra_width // 2, available_width_for_left_margin)
|
||||
if self.is_vertical:
|
||||
available_height = tab_bar.height - 2 * self.margin_width
|
||||
nlines = max(1, available_height // cell_height)
|
||||
ncols = max(1, tab_bar.width // cell_width)
|
||||
s.resize(nlines, ncols)
|
||||
s.reset_mode(DECAWM)
|
||||
cell_area_height = nlines * cell_height
|
||||
available_height_for_top_margin = max(0, tab_bar.height - self.margin_width - cell_area_height)
|
||||
extra_height = max(0, tab_bar.height - 2 * self.margin_width - cell_area_height)
|
||||
top_margin = min(self.margin_width + extra_height // 2, available_height_for_top_margin)
|
||||
self.window_geometry = g = WindowGeometry(
|
||||
tab_bar.left, tab_bar.top + top_margin, tab_bar.right, tab_bar.top + top_margin + cell_area_height, s.columns, s.lines)
|
||||
else:
|
||||
available_width = tab_bar.width - 2 * self.margin_width
|
||||
ncells = max(4, available_width // cell_width)
|
||||
s.resize(1, ncells)
|
||||
s.reset_mode(DECAWM)
|
||||
cell_area_width = ncells * cell_width
|
||||
available_width_for_left_margin = max(0, tab_bar.width - self.margin_width - cell_area_width)
|
||||
extra_width = max(0, tab_bar.width - 2 * self.margin_width - cell_area_width)
|
||||
left_margin = min(self.margin_width + extra_width // 2, available_width_for_left_margin)
|
||||
self.window_geometry = g = WindowGeometry(
|
||||
left_margin, tab_bar.top, left_margin + cell_area_width, tab_bar.bottom, s.columns, s.lines)
|
||||
self.laid_out_once = True
|
||||
self.window_geometry = g = WindowGeometry(
|
||||
left_margin, tab_bar.top, left_margin + cell_area_width, tab_bar.bottom, s.columns, s.lines)
|
||||
self.update_blank_rects(central, tab_bar, vw, vh)
|
||||
set_tab_bar_render_data(self.os_window_id, self.screen, *g[:4])
|
||||
|
||||
def update(self, data: Sequence[TabBarData]) -> None:
|
||||
if not self.laid_out_once:
|
||||
return
|
||||
if self.is_vertical:
|
||||
self.update_vertical(data)
|
||||
return
|
||||
s = self.screen
|
||||
last_tab = data[-1] if data else None
|
||||
ed = ExtraData()
|
||||
|
|
@ -743,14 +833,14 @@ class TabBar:
|
|||
def draw_tab(i: int, tab: TabBarData, cell_ranges: list[TabExtent], max_tab_length: int) -> None:
|
||||
ed.prev_tab = data[i - 1] if i > 0 else None
|
||||
ed.next_tab = data[i + 1] if i + 1 < len(data) else None
|
||||
s.cursor.bg = as_rgb(self.draw_data.tab_bg(t))
|
||||
s.cursor.fg = as_rgb(self.draw_data.tab_fg(t))
|
||||
s.cursor.bold, s.cursor.italic = self.active_font_style if t.is_active else self.inactive_font_style
|
||||
s.cursor.bg = as_rgb(self.draw_data.tab_bg(tab))
|
||||
s.cursor.fg = as_rgb(self.draw_data.tab_fg(tab))
|
||||
s.cursor.bold, s.cursor.italic = self.active_font_style if tab.is_active else self.inactive_font_style
|
||||
before = s.cursor.x
|
||||
end = self.draw_func(self.draw_data, s, t, before, max_tab_length, i + 1, t is last_tab, ed)
|
||||
end = self.draw_func(self.draw_data, s, tab, before, max_tab_length, i + 1, tab is last_tab, ed)
|
||||
s.cursor.bg = s.cursor.fg = 0
|
||||
cell_ranges.append(TabExtent(tab_id=tab.tab_id, cell_range=CellRange(before, end)))
|
||||
if not ed.for_layout and t is not last_tab and s.cursor.x > s.columns - max_tab_lengths[i+1]:
|
||||
cell_ranges.append(TabExtent(tab_id=tab.tab_id, x=CellRange(before, end)))
|
||||
if not ed.for_layout and tab is not last_tab and s.cursor.x > s.columns - max_tab_lengths[i+1]:
|
||||
# Stop if there is no space for next tab
|
||||
s.cursor.x = s.columns - 2
|
||||
s.cursor.bg = as_rgb(color_as_int(self.draw_data.default_bg))
|
||||
|
|
@ -801,24 +891,72 @@ class TabBar:
|
|||
self.align()
|
||||
update_tab_bar_edge_colors(self.os_window_id)
|
||||
|
||||
def update_vertical(self, data: Sequence[TabBarData]) -> None:
|
||||
s = self.screen
|
||||
self.last_laid_out_tabs = data
|
||||
self.tab_extents = ()
|
||||
s.cursor.x = s.cursor.y = 0
|
||||
s.erase_in_display(2, False)
|
||||
if not data:
|
||||
return
|
||||
max_tab_length = max(1, s.columns - 1)
|
||||
tab_line_height = max(1, min(MAX_VERTICAL_TAB_LINES, s.lines // max(1, len(data))))
|
||||
rows_to_draw = min(len(data), max(1, s.lines // tab_line_height))
|
||||
draw_ellipsis = len(data) > rows_to_draw and s.lines > 1
|
||||
if draw_ellipsis:
|
||||
tab_line_height = 1
|
||||
rows_to_draw = min(len(data), s.lines)
|
||||
rows_to_draw -= 1
|
||||
total_lines = rows_to_draw * tab_line_height + int(draw_ellipsis)
|
||||
if self.tab_bar_align == 'center':
|
||||
start_row = max(0, (s.lines - total_lines) // 2)
|
||||
elif self.tab_bar_align == 'end':
|
||||
start_row = max(0, s.lines - total_lines)
|
||||
else:
|
||||
start_row = 0
|
||||
cr: list[TabExtent] = []
|
||||
for i, t in enumerate(data[:rows_to_draw]):
|
||||
s.cursor.x = 0
|
||||
row = start_row + i * tab_line_height
|
||||
s.cursor.y = row
|
||||
s.cursor.bg = as_rgb(self.draw_data.tab_bg(t))
|
||||
s.cursor.fg = as_rgb(self.draw_data.tab_fg(t))
|
||||
s.cursor.bold, s.cursor.italic = self.active_font_style if t.is_active else self.inactive_font_style
|
||||
self.draw_func(self.draw_data, s, t, 0, max_tab_length, i + 1, True, ExtraData())
|
||||
cr.append(TabExtent(tab_id=t.tab_id, x=CellRange(0, s.columns - 1), y=CellRange(row, min(s.lines - 1, row + tab_line_height - 1))))
|
||||
if draw_ellipsis:
|
||||
s.cursor.x = 0
|
||||
s.cursor.y = start_row + rows_to_draw * tab_line_height
|
||||
s.cursor.bg = as_rgb(color_as_int(self.draw_data.default_bg))
|
||||
s.cursor.fg = as_rgb(0xff0000)
|
||||
s.draw('…')
|
||||
self.tab_extents = tuple(cr)
|
||||
|
||||
def align_with_factor(self, factor: int = 1) -> None:
|
||||
if not self.tab_extents:
|
||||
return
|
||||
end = self.tab_extents[-1].cell_range[1]
|
||||
end = self.tab_extents[-1].x.end
|
||||
if end < self.screen.columns - 1:
|
||||
shift = (self.screen.columns - end) // factor
|
||||
self.screen.cursor.x = 0
|
||||
self.screen.insert_characters(shift)
|
||||
self.tab_extents = tuple(te.shifted(shift) for te in self.tab_extents)
|
||||
self.tab_extents = tuple(te.shifted(x=shift) for te in self.tab_extents)
|
||||
|
||||
def destroy(self) -> None:
|
||||
self.screen.reset_callbacks()
|
||||
del self.screen
|
||||
|
||||
def tab_id_at(self, x: int) -> int:
|
||||
def tab_id_at(self, x: int, y: int = 0) -> int:
|
||||
if self.laid_out_once:
|
||||
x = (x - self.window_geometry.left) // self.cell_width
|
||||
g = self.window_geometry
|
||||
if not (g.left <= x < g.right and g.top <= y < g.bottom):
|
||||
return 0
|
||||
x = (x - g.left) // self.cell_width
|
||||
y = (y - g.top) // self.cell_height
|
||||
for te in self.tab_extents:
|
||||
if te.cell_range.start <= x <= te.cell_range.end:
|
||||
if te.contains(x, y):
|
||||
return te.tab_id
|
||||
return 0
|
||||
|
||||
def drag_axis_coordinate(self, x: int, y: int) -> int:
|
||||
return y if self.is_vertical else x
|
||||
|
|
|
|||
|
|
@ -1171,7 +1171,7 @@ class Tab: # {{{
|
|||
class TabBeingDropped(NamedTuple):
|
||||
data: TabBarData
|
||||
tab_ids: Sequence[int] = ()
|
||||
last_drop_move_x: int = -1
|
||||
last_drop_move_coordinate: int = -1
|
||||
|
||||
|
||||
class WindowBeingDropped(NamedTuple):
|
||||
|
|
@ -1703,27 +1703,29 @@ class TabManager: # {{{
|
|||
tab_data = tab.data_for_tab_bar(tab is get_boss().active_tab)
|
||||
if tab_id not in all_tabs:
|
||||
all_tabs.append(tab_id)
|
||||
_, _, start_x, _ = get_tab_being_dragged()
|
||||
self.tab_being_dropped = TabBeingDropped(data=tab_data, tab_ids=all_tabs, last_drop_move_x=int(start_x))
|
||||
mouse_moved_left = False
|
||||
_, _, start_x, start_y = get_tab_being_dragged()
|
||||
start_coordinate = self.tab_bar.drag_axis_coordinate(int(start_x), int(start_y))
|
||||
self.tab_being_dropped = TabBeingDropped(data=tab_data, tab_ids=all_tabs, last_drop_move_coordinate=start_coordinate)
|
||||
force_update = True
|
||||
if x == self.tab_being_dropped.last_drop_move_x and not force_update:
|
||||
coordinate = self.tab_bar.drag_axis_coordinate(x, y)
|
||||
if coordinate == self.tab_being_dropped.last_drop_move_coordinate and not force_update:
|
||||
return
|
||||
mouse_moved_left = x < self.tab_being_dropped.last_drop_move_x
|
||||
mouse_moved_towards_start = coordinate < self.tab_being_dropped.last_drop_move_coordinate
|
||||
old_tab_ids = self.tab_being_dropped.tab_ids
|
||||
idx_under_mouse = -1
|
||||
if (tab_id_under_mouse := self.tab_bar.tab_id_at(x)):
|
||||
if (tab_id_under_mouse := self.tab_bar.tab_id_at(x, y)):
|
||||
with suppress(Exception):
|
||||
idx_under_mouse = old_tab_ids.index(tab_id_under_mouse)
|
||||
if idx_under_mouse < 0:
|
||||
idx_under_mouse = 0 if x < 20 else len(old_tab_ids) - 1
|
||||
start = self.tab_bar.window_geometry.top if self.tab_bar.is_vertical else self.tab_bar.window_geometry.left
|
||||
idx_under_mouse = 0 if coordinate < start else len(old_tab_ids) - 1
|
||||
old_idx_under_mouse = old_tab_ids.index(tab_id)
|
||||
idx_moved_left = old_idx_under_mouse > idx_under_mouse
|
||||
idx_moved_towards_start = old_idx_under_mouse > idx_under_mouse
|
||||
new_tab_ids = old_tab_ids
|
||||
if mouse_moved_left == idx_moved_left:
|
||||
if mouse_moved_towards_start == idx_moved_towards_start:
|
||||
new_tab_ids = list(old_tab_ids)
|
||||
new_tab_ids[idx_under_mouse], new_tab_ids[old_idx_under_mouse] = new_tab_ids[old_idx_under_mouse], new_tab_ids[idx_under_mouse]
|
||||
self.tab_being_dropped = self.tab_being_dropped._replace(last_drop_move_x=x, tab_ids=new_tab_ids)
|
||||
self.tab_being_dropped = self.tab_being_dropped._replace(last_drop_move_coordinate=coordinate, tab_ids=new_tab_ids)
|
||||
if force_update or self.tab_being_dropped.tab_ids != old_tab_ids:
|
||||
self.layout_tab_bar()
|
||||
|
||||
|
|
@ -1803,11 +1805,11 @@ class TabManager: # {{{
|
|||
self.recent_tab_bar_mouse_events.clear()
|
||||
return
|
||||
|
||||
tab_id_at_x = self.tab_bar.tab_id_at(int(x))
|
||||
self.recent_tab_bar_mouse_events.add(button, modifiers, action, x, y, tab_id_at_x)
|
||||
tab_id_at_pointer = self.tab_bar.tab_id_at(int(x), int(y))
|
||||
self.recent_tab_bar_mouse_events.add(button, modifiers, action, x, y, tab_id_at_pointer)
|
||||
drag_started = get_tab_being_dragged()[1]
|
||||
is_left_release = button == GLFW_MOUSE_BUTTON_LEFT and action == GLFW_RELEASE
|
||||
if tab_id_at_x < 0: # synthetic tab (e.g. "+" new-tab button)
|
||||
if tab_id_at_pointer < 0: # synthetic tab (e.g. "+" new-tab button)
|
||||
if is_left_release and not drag_started:
|
||||
set_tab_being_dragged() # clear potential drag from a press on a tab
|
||||
if self.recent_tab_bar_mouse_events.click_count(GLFW_MOUSE_BUTTON_LEFT) == 1:
|
||||
|
|
@ -1816,7 +1818,7 @@ class TabManager: # {{{
|
|||
return
|
||||
if drag_started:
|
||||
return
|
||||
tab = self.tab_for_id(tab_id_at_x)
|
||||
tab = self.tab_for_id(tab_id_at_pointer)
|
||||
if tab is None:
|
||||
if is_left_release:
|
||||
set_tab_being_dragged() # clear potential drag from a press on a tab
|
||||
|
|
@ -1978,7 +1980,7 @@ class TabManager: # {{{
|
|||
tab_bar = viewport_for_window(self.os_window_id)[1]
|
||||
if tab_bar.left <= x < tab_bar.right and tab_bar.top <= y < tab_bar.bottom:
|
||||
self._set_drag_target_window(0)
|
||||
self._set_drag_target_tab(self.tab_bar.tab_id_at(x))
|
||||
self._set_drag_target_tab(self.tab_bar.tab_id_at(x, y))
|
||||
return
|
||||
self._set_drag_target_tab(0)
|
||||
dest_window = self._find_window_at(x, y)
|
||||
|
|
@ -2033,7 +2035,7 @@ class TabManager: # {{{
|
|||
# Case 1: Drop on tab bar → move to that tab
|
||||
in_tab_bar = tab_bar.left <= x < tab_bar.right and tab_bar.top <= y < tab_bar.bottom
|
||||
if in_tab_bar:
|
||||
if (tab_id := self.tab_bar.tab_id_at(x)) and (dest_tab := self.tab_for_id(tab_id)):
|
||||
if (tab_id := self.tab_bar.tab_id_at(x, y)) and (dest_tab := self.tab_for_id(tab_id)):
|
||||
boss._move_window_to(w, target_tab_id=dest_tab.id)
|
||||
else:
|
||||
boss._move_window_to(w, target_tab_id='new')
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ def launcher(self):
|
|||
def conf_parsing(self):
|
||||
from kitty.config import defaults, load_config
|
||||
from kitty.constants import is_macos
|
||||
from kitty.fast_data_types import LEFT_EDGE, RIGHT_EDGE
|
||||
from kitty.fonts import FontModification, ModificationType, ModificationUnit, ModificationValue
|
||||
from kitty.options.utils import to_modifiers
|
||||
bad_lines = []
|
||||
|
|
@ -254,6 +255,18 @@ def conf_parsing(self):
|
|||
self.ae(opts.url_excluded_characters, "'''")
|
||||
opts = p("url_excluded_characters abc'")
|
||||
self.ae(opts.url_excluded_characters, "abc'")
|
||||
opts = p('tab_bar_edge left')
|
||||
self.ae(opts.tab_bar_edge, LEFT_EDGE)
|
||||
opts = p('tab_bar_edge right')
|
||||
self.ae(opts.tab_bar_edge, RIGHT_EDGE)
|
||||
opts = p('tab_bar_align start')
|
||||
self.ae(opts.tab_bar_align, 'start')
|
||||
opts = p('tab_bar_align end')
|
||||
self.ae(opts.tab_bar_align, 'end')
|
||||
opts = p('tab_bar_align left')
|
||||
self.ae(opts.tab_bar_align, 'left')
|
||||
opts = p('tab_bar_align right')
|
||||
self.ae(opts.tab_bar_align, 'right')
|
||||
opts = p('clear_all_shortcuts y', 'map f1 next_window')
|
||||
self.ae(len(opts.keyboard_modes[''].keymap), 1)
|
||||
opts = p('clear_all_mouse_actions y', 'mouse_map left click ungrabbed mouse_click_url_or_select')
|
||||
|
|
|
|||
89
kitty_tests/tab_bar.py
Normal file
89
kitty_tests/tab_bar.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env python
|
||||
# License: GPL v3 Copyright: 2026, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from kitty.fast_data_types import LEFT_EDGE, Region
|
||||
from kitty.tab_bar import TabBar, TabBarData
|
||||
|
||||
from . import BaseTest
|
||||
|
||||
|
||||
def region(left: int, top: int, right: int, bottom: int) -> Region:
|
||||
return Region((left, top, right, bottom, right - left, bottom - top))
|
||||
|
||||
|
||||
class DummyBoss:
|
||||
class mappings:
|
||||
current_keyboard_mode_name = ''
|
||||
|
||||
def tab_for_id(self, tab_id: int) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class TestTabBar(BaseTest):
|
||||
|
||||
def test_vertical_tab_bar_hit_testing(self) -> None:
|
||||
self.set_options({
|
||||
'tab_bar_edge': LEFT_EDGE,
|
||||
'tab_bar_style': 'separator',
|
||||
'tab_title_template': '{title}',
|
||||
})
|
||||
central = region(120, 0, 400, 160)
|
||||
tab_bar = region(0, 0, 120, 160)
|
||||
geometries: list[tuple[int, int, int, int]] = []
|
||||
boss = DummyBoss()
|
||||
|
||||
with (
|
||||
patch('kitty.tab_bar.cell_size_for_window', return_value=(10, 20)),
|
||||
patch('kitty.tab_bar.viewport_for_window', return_value=(central, tab_bar, 400, 160, 10, 20)),
|
||||
patch('kitty.tab_bar.set_tab_bar_render_data', side_effect=lambda *args: geometries.append(args[2:6])),
|
||||
patch('kitty.tab_bar.get_boss', return_value=boss),
|
||||
):
|
||||
tb = TabBar(1)
|
||||
tb.layout()
|
||||
tb.update((
|
||||
TabBarData(title='one', tab_id=1, is_active=True),
|
||||
TabBarData(title='two', tab_id=2),
|
||||
TabBarData(title='three', tab_id=3),
|
||||
))
|
||||
|
||||
self.assertTrue(tb.is_vertical)
|
||||
self.ae(geometries[-1], (0, 0, 120, 160))
|
||||
self.ae(tb.drag_axis_coordinate(5, 35), 35)
|
||||
self.ae(tb.tab_id_at(5, 10), 1)
|
||||
self.ae(tb.tab_id_at(110, 35), 1)
|
||||
self.ae(tb.tab_id_at(60, 55), 2)
|
||||
self.ae(tb.tab_id_at(60, 95), 3)
|
||||
self.ae(tb.tab_id_at(60, 135), 0)
|
||||
self.ae(tb.tab_id_at(180, 10), 0)
|
||||
|
||||
def test_vertical_tab_bar_alignment(self) -> None:
|
||||
self.set_options({
|
||||
'tab_bar_align': 'end',
|
||||
'tab_bar_edge': LEFT_EDGE,
|
||||
'tab_bar_style': 'separator',
|
||||
'tab_title_template': '{title}',
|
||||
})
|
||||
central = region(120, 0, 400, 160)
|
||||
tab_bar = region(0, 0, 120, 160)
|
||||
boss = DummyBoss()
|
||||
|
||||
with (
|
||||
patch('kitty.tab_bar.cell_size_for_window', return_value=(10, 20)),
|
||||
patch('kitty.tab_bar.viewport_for_window', return_value=(central, tab_bar, 400, 160, 10, 20)),
|
||||
patch('kitty.tab_bar.set_tab_bar_render_data'),
|
||||
patch('kitty.tab_bar.get_boss', return_value=boss),
|
||||
):
|
||||
tb = TabBar(1)
|
||||
tb.layout()
|
||||
tb.update((
|
||||
TabBarData(title='one', tab_id=1, is_active=True),
|
||||
TabBarData(title='two', tab_id=2),
|
||||
))
|
||||
|
||||
self.ae(tb.tab_extents[0].y, (4, 5))
|
||||
self.ae(tb.tab_extents[1].y, (6, 7))
|
||||
self.ae(tb.tab_id_at(5, 10), 0)
|
||||
self.ae(tb.tab_id_at(5, 110), 1)
|
||||
self.ae(tb.tab_id_at(5, 150), 2)
|
||||
Loading…
Add table
Add a link
Reference in a new issue