mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-07-03 11:12:30 +08:00
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:
@@ -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
|
||||
|
||||
13
kitty/glfw.c
13
kitty/glfw.c
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user