Get tab re-ordering on drag in same window to work

Still need to implement multiple windows and detach and the actual drop
event.
This commit is contained in:
Kovid Goyal
2026-02-20 12:22:56 +05:30
parent 7123f727fc
commit 0b6636eb0f
5 changed files with 120 additions and 19 deletions

View File

@@ -86,6 +86,7 @@ from .fast_data_types import (
get_boss,
get_options,
get_os_window_size,
get_tab_being_dragged,
glfw_get_monitor_workarea,
global_font_size,
grab_keyboard,
@@ -1890,6 +1891,14 @@ class Boss:
if tm is not None:
tm.update_tab_bar_data()
def on_drop_move(self, os_window_id: int, x: int, y: int, from_self: bool) -> None:
if (tm := self.os_window_map.get(os_window_id)) is None:
return
if from_self and (tab_id := get_tab_being_dragged()) and (tab := self.tab_for_id(tab_id)):
if (source_tm := tab.tab_manager_ref()) and (state := source_tm.tab_drag_state) and state.tab_being_dragged:
for tm in self.all_tab_managers:
tm.on_tab_drop_move(state.tab_being_dragged, x, y)
def on_drop(self, os_window_id: int, drop: dict[str, bytes] | int, from_self: bool, x: int, y: int) -> None:
if isinstance(drop, int):
import errno

View File

@@ -647,6 +647,9 @@ window_focus_callback(GLFWwindow *w, int focused) {
static int
is_droppable_mime(const char *mime) {
static char tab_mime[64] = {0};
if (!tab_mime[0]) snprintf(tab_mime, sizeof(tab_mime), "application/net.kovidgoyal.kitty-tab-%d", getpid());
if (strcmp(mime, tab_mime) == 0) return 4;
if (strcmp(mime, "text/uri-list") == 0) return 3;
if (strcmp(mime, "text/plain;charset=utf-8") == 0) return 2;
if (strcmp(mime, "text/plain") == 0) return 1;
@@ -721,12 +724,18 @@ read_drop_data(GLFWwindow *window, GLFWDropEvent *ev) {
static void
on_drop(GLFWwindow *window, GLFWDropEvent *ev) {
if (!set_callback_window(window)) return;
OSWindow *os_window = global_state.callback_os_window;
switch (ev->type) {
case GLFW_DROP_ENTER:
case GLFW_DROP_MOVE:
global_state.callback_os_window->last_drag_event.x = (int)(ev->xpos * global_state.callback_os_window->viewport_x_ratio);
global_state.callback_os_window->last_drag_event.y = (int)(ev->ypos * global_state.callback_os_window->viewport_y_ratio);
os_window->last_drag_event.x = (int)(ev->xpos * os_window->viewport_x_ratio);
os_window->last_drag_event.y = (int)(ev->ypos * os_window->viewport_y_ratio);
on_mouse_position_update(ev->xpos, ev->ypos);
if (global_state.drag_source.is_active) {
call_boss(on_drop_move, "KiiO",
os_window->id, os_window->last_drag_event.x, os_window->last_drag_event.y,
ev->from_self ? Py_True : Py_False);
}
/* fallthrough */
case GLFW_DROP_STATUS_UPDATE:
update_allowed_mimes_for_drop(ev);

View File

@@ -9,46 +9,46 @@ out vec4 output_color;
void main() {
// The input texture contains sRGB colors with premultiplied alpha.
// We need to output unpremultiplied sRGB colors with proper downscaling.
// For proper downscaling, we need to:
// 1. Sample neighboring pixels
// 2. Convert from sRGB to linear (unpremultiplying first)
// 3. Average in linear space
// 4. Convert back to sRGB
// 5. Output unpremultiplied
// Calculate the texel size
vec2 texel_size = 1.0 / src_size;
// Sample a 2x2 grid for better quality downscaling
// This provides basic bilinear-like filtering in linear space
vec2 tc = texcoord;
vec4 s00 = texture(image, tc + vec2(-0.25, -0.25) * texel_size);
vec4 s10 = texture(image, tc + vec2( 0.25, -0.25) * texel_size);
vec4 s01 = texture(image, tc + vec2(-0.25, 0.25) * texel_size);
vec4 s11 = texture(image, tc + vec2( 0.25, 0.25) * texel_size);
// Unpremultiply and convert to linear for each sample
vec3 linear00 = s00.a > 0.0 ? srgb2linear(s00.rgb / s00.a) : vec3(0.0);
vec3 linear10 = s10.a > 0.0 ? srgb2linear(s10.rgb / s10.a) : vec3(0.0);
vec3 linear01 = s01.a > 0.0 ? srgb2linear(s01.rgb / s01.a) : vec3(0.0);
vec3 linear11 = s11.a > 0.0 ? srgb2linear(s11.rgb / s11.a) : vec3(0.0);
// Average the alpha values
float avg_alpha = (s00.a + s10.a + s01.a + s11.a) * 0.25;
// For proper downsampling with transparency, weight colors by their alpha
// This ensures partially transparent pixels contribute proportionally
vec3 weighted_sum = linear00 * s00.a + linear10 * s10.a + linear01 * s01.a + linear11 * s11.a;
float total_weight = s00.a + s10.a + s01.a + s11.a;
// Calculate the weighted average color in linear space
vec3 avg_linear = total_weight > 0.0 ? weighted_sum / total_weight : vec3(0.0);
// Convert back to sRGB
vec3 srgb_color = linear2srgb(avg_linear);
// Output unpremultiplied sRGB color
output_color = vec4(srgb_color, avg_alpha);
}

View File

@@ -40,6 +40,7 @@ class TabBarData(NamedTuple):
is_active: bool
needs_attention: bool
tab_id: int
os_window_id: int
num_windows: int
num_window_groups: int
layout_name: str
@@ -54,6 +55,10 @@ class TabBarData(NamedTuple):
session_name: str
active_session_name: str
is_being_moved: bool = False
drop_idx: int = 0
pending_drop_idx: int = -1
class DrawData(NamedTuple):
leading_spaces: int
@@ -568,6 +573,7 @@ class TabBar:
def __init__(self, os_window_id: int):
self.os_window_id = os_window_id
self.last_laid_out_tabs: Sequence[TabBarData] = ()
self.num_tabs = 1
self.data_buffer_size = 0
self.blank_rects: tuple[Border, ...] = ()
@@ -727,6 +733,7 @@ class TabBar:
s = self.screen
last_tab = data[-1] if data else None
ed = ExtraData()
self.last_laid_out_tabs = data
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

View File

@@ -396,7 +396,7 @@ class Tab: # {{{
if w.has_activity_since_last_focus:
has_activity_since_last_focus = True
return TabBarData(
title, is_active, needs_attention, t.id,
title, is_active, needs_attention, t.id, t.os_window_id,
len(t), t.num_window_groups, t.current_layout.name or '',
has_activity_since_last_focus, t.active_fg, t.active_bg,
t.inactive_fg, t.inactive_bg, t.num_of_windows_with_progress,
@@ -1103,6 +1103,8 @@ class TabManager: # {{{
total_progress: int = 0
has_indeterminate_progress: bool = False
tab_drag_state: TabDragState | None = None
tab_being_dropped: TabBarData | None = None
last_drop_move_x: int = -1
def __init__(self, os_window_id: int, args: CLIOptions, wm_class: str, wm_name: str, startup_session: SessionType | None = None):
self.os_window_id = os_window_id
@@ -1194,9 +1196,13 @@ class TabManager: # {{{
@property
def tab_bar_should_be_visible(self) -> bool:
if self.tab_being_dropped is not None:
return True
count = get_options().tab_bar_min_tabs
if count < 1:
return True
if self.tab_drag_state is not None and self.tab_drag_state.drag_started:
count += 1
for t in self.tabs_to_be_shown_in_tab_bar:
count -= 1
if count < 1:
@@ -1272,7 +1278,7 @@ class TabManager: # {{{
f = get_options().tab_bar_filter
if f:
at = self.active_tab
m = set(get_boss().match_tabs(f, all_tabs=self))
m = frozenset(get_boss().match_tabs(f, all_tabs=self))
return (t for t in self if t is at or t in m)
return self.tabs
@@ -1532,20 +1538,91 @@ class TabManager: # {{{
removed_tab.destroy()
@property
def tab_bar_data(self) -> tuple[TabBarData, ...]:
def tab_bar_data(self) -> Sequence[TabBarData]:
at = self.active_tab
return tuple(t.data_for_tab_bar(t is at) for t in self.tabs_to_be_shown_in_tab_bar)
state = self.tab_drag_state
dropped_tab_idx = dragged_tab_id = -1
tab_being_dragged_from_here = False
if state is not None and state.drag_started:
dragged_tab_id = state.tab_id
tab_being_dragged_from_here = True
if self.tab_being_dropped is None and not tab_being_dragged_from_here:
return tuple(t.data_for_tab_bar(t is at) for t in self.tabs_to_be_shown_in_tab_bar)
if self.tab_being_dropped is not None and self.tab_being_dropped.os_window_id == self.os_window_id:
dropped_tab_idx = self.tab_being_dropped.drop_idx
if dropped_tab_idx < 0:
return tuple(t.data_for_tab_bar(t is at) for t in self.tabs_to_be_shown_in_tab_bar if t.id != dragged_tab_id)
assert self.tab_being_dropped is not None
ans: list[TabBarData] = []
drop_idx = -1
for i, tab in enumerate(self.tabs_to_be_shown_in_tab_bar):
if i == dropped_tab_idx:
drop_idx = len(ans)
ans.append(self.tab_being_dropped)
if tab.id != dragged_tab_id:
ans.append(tab.data_for_tab_bar(at is tab))
if drop_idx < 0:
drop_idx = len(ans)
ans.append(self.tab_being_dropped)
if (tgt := self.tab_being_dropped.pending_drop_idx) > -1:
self.tab_being_dropped = self.tab_being_dropped._replace(pending_drop_idx=-1)
tgt = max(0, min(tgt, len(ans)-1))
if tgt != drop_idx:
ans[drop_idx], ans[tgt] = ans[tgt], ans[drop_idx]
return ans
def on_tab_drop_move(self, tab_data: TabBarData, x: int, y: int) -> None:
if tab_data.os_window_id == self.os_window_id:
tid = self.tab_bar.tab_id_at(x)
all_tabs = tuple(t.tab_id for t in self.tab_bar.last_laid_out_tabs)
try:
idx = all_tabs.index(tid)
except ValueError:
idx = -1
if idx < 0:
idx = len(all_tabs) if x > 20 else 0
if self.tab_being_dropped is None:
tab_data = tab_data._replace(drop_idx=idx)
self.last_drop_move_x = x
else:
if x == self.last_drop_move_x:
return
mouse_moved_left = x < self.last_drop_move_x
self.last_drop_move_x = x
tab_data = tab_data._replace(pending_drop_idx=idx)
cur_tab_data, self.tab_being_dropped = self.tab_being_dropped, tab_data
new_tabs = tuple(t.tab_id for t in self.tab_bar_data)
self.tab_being_dropped = cur_tab_data
if new_tabs == all_tabs:
return
try:
old_idx = all_tabs.index(tab_data.tab_id)
except Exception:
pass
else:
new_idx = new_tabs.index(tab_data.tab_id)
tab_moved_left = new_idx < old_idx
if mouse_moved_left != tab_moved_left or new_idx == old_idx:
return
tab_data = tab_data._replace(pending_drop_idx=idx)
self.tab_being_dropped = tab_data
self.layout_tab_bar()
elif self.tab_being_dropped is not None:
self.tab_being_dropped = None
self.layout_tab_bar()
def start_tab_drag(self, pixels: bytes, width: int, height: int) -> None:
if (state := self.tab_drag_state) is None:
return
for i, tab in enumerate(self.tabs_to_be_shown_in_tab_bar):
if tab.id == state.tab_id:
td = tab.data_for_tab_bar(tab is self.active_tab)
td = tab.data_for_tab_bar(tab is self.active_tab)._replace(
is_being_moved=True, drop_idx=i
)
title = apply_title_template(self.tab_bar.draw_data, td, i+1)
title = re.sub(r'\x1b\[.+?[a-zA-Z]', '', title).strip() # strip CSI codes
title = replace_c0_codes_except_nl_space_tab(title.encode()).decode()
drag_data = {
'text/plain': replace_c0_codes_except_nl_space_tab(title.encode()),
f'application/net.kovidgoyal.kitty-tab-{os.getpid()}': str(tab.id).encode(),
}
start_drag_with_data(self.os_window_id, drag_data, pixels, width, height)
@@ -1585,7 +1662,6 @@ class TabManager: # {{{
else:
if self.tab_drag_state is None or not self.tab_drag_state.drag_started:
self.set_active_tab(tab)
set_tab_being_dragged(0)
elif button == GLFW_MOUSE_BUTTON_MIDDLE:
if action == GLFW_RELEASE and self.recent_mouse_events:
p = self.recent_mouse_events[-1]