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_boss,
|
||||||
get_options,
|
get_options,
|
||||||
get_os_window_size,
|
get_os_window_size,
|
||||||
|
get_tab_being_dragged,
|
||||||
glfw_get_monitor_workarea,
|
glfw_get_monitor_workarea,
|
||||||
global_font_size,
|
global_font_size,
|
||||||
grab_keyboard,
|
grab_keyboard,
|
||||||
@@ -1890,6 +1891,14 @@ class Boss:
|
|||||||
if tm is not None:
|
if tm is not None:
|
||||||
tm.update_tab_bar_data()
|
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:
|
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):
|
if isinstance(drop, int):
|
||||||
import errno
|
import errno
|
||||||
|
|||||||
13
kitty/glfw.c
13
kitty/glfw.c
@@ -647,6 +647,9 @@ window_focus_callback(GLFWwindow *w, int focused) {
|
|||||||
|
|
||||||
static int
|
static int
|
||||||
is_droppable_mime(const char *mime) {
|
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/uri-list") == 0) return 3;
|
||||||
if (strcmp(mime, "text/plain;charset=utf-8") == 0) return 2;
|
if (strcmp(mime, "text/plain;charset=utf-8") == 0) return 2;
|
||||||
if (strcmp(mime, "text/plain") == 0) return 1;
|
if (strcmp(mime, "text/plain") == 0) return 1;
|
||||||
@@ -721,12 +724,18 @@ read_drop_data(GLFWwindow *window, GLFWDropEvent *ev) {
|
|||||||
static void
|
static void
|
||||||
on_drop(GLFWwindow *window, GLFWDropEvent *ev) {
|
on_drop(GLFWwindow *window, GLFWDropEvent *ev) {
|
||||||
if (!set_callback_window(window)) return;
|
if (!set_callback_window(window)) return;
|
||||||
|
OSWindow *os_window = global_state.callback_os_window;
|
||||||
switch (ev->type) {
|
switch (ev->type) {
|
||||||
case GLFW_DROP_ENTER:
|
case GLFW_DROP_ENTER:
|
||||||
case GLFW_DROP_MOVE:
|
case GLFW_DROP_MOVE:
|
||||||
global_state.callback_os_window->last_drag_event.x = (int)(ev->xpos * global_state.callback_os_window->viewport_x_ratio);
|
os_window->last_drag_event.x = (int)(ev->xpos * 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.y = (int)(ev->ypos * os_window->viewport_y_ratio);
|
||||||
on_mouse_position_update(ev->xpos, ev->ypos);
|
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 */
|
/* fallthrough */
|
||||||
case GLFW_DROP_STATUS_UPDATE:
|
case GLFW_DROP_STATUS_UPDATE:
|
||||||
update_allowed_mimes_for_drop(ev);
|
update_allowed_mimes_for_drop(ev);
|
||||||
|
|||||||
@@ -9,46 +9,46 @@ out vec4 output_color;
|
|||||||
void main() {
|
void main() {
|
||||||
// The input texture contains sRGB colors with premultiplied alpha.
|
// The input texture contains sRGB colors with premultiplied alpha.
|
||||||
// We need to output unpremultiplied sRGB colors with proper downscaling.
|
// We need to output unpremultiplied sRGB colors with proper downscaling.
|
||||||
|
|
||||||
// For proper downscaling, we need to:
|
// For proper downscaling, we need to:
|
||||||
// 1. Sample neighboring pixels
|
// 1. Sample neighboring pixels
|
||||||
// 2. Convert from sRGB to linear (unpremultiplying first)
|
// 2. Convert from sRGB to linear (unpremultiplying first)
|
||||||
// 3. Average in linear space
|
// 3. Average in linear space
|
||||||
// 4. Convert back to sRGB
|
// 4. Convert back to sRGB
|
||||||
// 5. Output unpremultiplied
|
// 5. Output unpremultiplied
|
||||||
|
|
||||||
// Calculate the texel size
|
// Calculate the texel size
|
||||||
vec2 texel_size = 1.0 / src_size;
|
vec2 texel_size = 1.0 / src_size;
|
||||||
|
|
||||||
// Sample a 2x2 grid for better quality downscaling
|
// Sample a 2x2 grid for better quality downscaling
|
||||||
// This provides basic bilinear-like filtering in linear space
|
// This provides basic bilinear-like filtering in linear space
|
||||||
vec2 tc = texcoord;
|
vec2 tc = texcoord;
|
||||||
|
|
||||||
vec4 s00 = texture(image, tc + vec2(-0.25, -0.25) * texel_size);
|
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 s10 = texture(image, tc + vec2( 0.25, -0.25) * texel_size);
|
||||||
vec4 s01 = 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);
|
vec4 s11 = texture(image, tc + vec2( 0.25, 0.25) * texel_size);
|
||||||
|
|
||||||
// Unpremultiply and convert to linear for each sample
|
// Unpremultiply and convert to linear for each sample
|
||||||
vec3 linear00 = s00.a > 0.0 ? srgb2linear(s00.rgb / s00.a) : vec3(0.0);
|
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 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 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);
|
vec3 linear11 = s11.a > 0.0 ? srgb2linear(s11.rgb / s11.a) : vec3(0.0);
|
||||||
|
|
||||||
// Average the alpha values
|
// Average the alpha values
|
||||||
float avg_alpha = (s00.a + s10.a + s01.a + s11.a) * 0.25;
|
float avg_alpha = (s00.a + s10.a + s01.a + s11.a) * 0.25;
|
||||||
|
|
||||||
// For proper downsampling with transparency, weight colors by their alpha
|
// For proper downsampling with transparency, weight colors by their alpha
|
||||||
// This ensures partially transparent pixels contribute proportionally
|
// This ensures partially transparent pixels contribute proportionally
|
||||||
vec3 weighted_sum = linear00 * s00.a + linear10 * s10.a + linear01 * s01.a + linear11 * s11.a;
|
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;
|
float total_weight = s00.a + s10.a + s01.a + s11.a;
|
||||||
|
|
||||||
// Calculate the weighted average color in linear space
|
// Calculate the weighted average color in linear space
|
||||||
vec3 avg_linear = total_weight > 0.0 ? weighted_sum / total_weight : vec3(0.0);
|
vec3 avg_linear = total_weight > 0.0 ? weighted_sum / total_weight : vec3(0.0);
|
||||||
|
|
||||||
// Convert back to sRGB
|
// Convert back to sRGB
|
||||||
vec3 srgb_color = linear2srgb(avg_linear);
|
vec3 srgb_color = linear2srgb(avg_linear);
|
||||||
|
|
||||||
// Output unpremultiplied sRGB color
|
// Output unpremultiplied sRGB color
|
||||||
output_color = vec4(srgb_color, avg_alpha);
|
output_color = vec4(srgb_color, avg_alpha);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class TabBarData(NamedTuple):
|
|||||||
is_active: bool
|
is_active: bool
|
||||||
needs_attention: bool
|
needs_attention: bool
|
||||||
tab_id: int
|
tab_id: int
|
||||||
|
os_window_id: int
|
||||||
num_windows: int
|
num_windows: int
|
||||||
num_window_groups: int
|
num_window_groups: int
|
||||||
layout_name: str
|
layout_name: str
|
||||||
@@ -54,6 +55,10 @@ class TabBarData(NamedTuple):
|
|||||||
session_name: str
|
session_name: str
|
||||||
active_session_name: str
|
active_session_name: str
|
||||||
|
|
||||||
|
is_being_moved: bool = False
|
||||||
|
drop_idx: int = 0
|
||||||
|
pending_drop_idx: int = -1
|
||||||
|
|
||||||
|
|
||||||
class DrawData(NamedTuple):
|
class DrawData(NamedTuple):
|
||||||
leading_spaces: int
|
leading_spaces: int
|
||||||
@@ -568,6 +573,7 @@ class TabBar:
|
|||||||
|
|
||||||
def __init__(self, os_window_id: int):
|
def __init__(self, os_window_id: int):
|
||||||
self.os_window_id = os_window_id
|
self.os_window_id = os_window_id
|
||||||
|
self.last_laid_out_tabs: Sequence[TabBarData] = ()
|
||||||
self.num_tabs = 1
|
self.num_tabs = 1
|
||||||
self.data_buffer_size = 0
|
self.data_buffer_size = 0
|
||||||
self.blank_rects: tuple[Border, ...] = ()
|
self.blank_rects: tuple[Border, ...] = ()
|
||||||
@@ -727,6 +733,7 @@ class TabBar:
|
|||||||
s = self.screen
|
s = self.screen
|
||||||
last_tab = data[-1] if data else None
|
last_tab = data[-1] if data else None
|
||||||
ed = ExtraData()
|
ed = ExtraData()
|
||||||
|
self.last_laid_out_tabs = data
|
||||||
|
|
||||||
def draw_tab(i: int, tab: TabBarData, cell_ranges: list[TabExtent], max_tab_length: int) -> None:
|
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.prev_tab = data[i - 1] if i > 0 else None
|
||||||
|
|||||||
@@ -396,7 +396,7 @@ class Tab: # {{{
|
|||||||
if w.has_activity_since_last_focus:
|
if w.has_activity_since_last_focus:
|
||||||
has_activity_since_last_focus = True
|
has_activity_since_last_focus = True
|
||||||
return TabBarData(
|
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 '',
|
len(t), t.num_window_groups, t.current_layout.name or '',
|
||||||
has_activity_since_last_focus, t.active_fg, t.active_bg,
|
has_activity_since_last_focus, t.active_fg, t.active_bg,
|
||||||
t.inactive_fg, t.inactive_bg, t.num_of_windows_with_progress,
|
t.inactive_fg, t.inactive_bg, t.num_of_windows_with_progress,
|
||||||
@@ -1103,6 +1103,8 @@ class TabManager: # {{{
|
|||||||
total_progress: int = 0
|
total_progress: int = 0
|
||||||
has_indeterminate_progress: bool = False
|
has_indeterminate_progress: bool = False
|
||||||
tab_drag_state: TabDragState | None = None
|
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):
|
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
|
self.os_window_id = os_window_id
|
||||||
@@ -1194,9 +1196,13 @@ class TabManager: # {{{
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def tab_bar_should_be_visible(self) -> bool:
|
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
|
count = get_options().tab_bar_min_tabs
|
||||||
if count < 1:
|
if count < 1:
|
||||||
return True
|
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:
|
for t in self.tabs_to_be_shown_in_tab_bar:
|
||||||
count -= 1
|
count -= 1
|
||||||
if count < 1:
|
if count < 1:
|
||||||
@@ -1272,7 +1278,7 @@ class TabManager: # {{{
|
|||||||
f = get_options().tab_bar_filter
|
f = get_options().tab_bar_filter
|
||||||
if f:
|
if f:
|
||||||
at = self.active_tab
|
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 (t for t in self if t is at or t in m)
|
||||||
return self.tabs
|
return self.tabs
|
||||||
|
|
||||||
@@ -1532,20 +1538,91 @@ class TabManager: # {{{
|
|||||||
removed_tab.destroy()
|
removed_tab.destroy()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tab_bar_data(self) -> tuple[TabBarData, ...]:
|
def tab_bar_data(self) -> Sequence[TabBarData]:
|
||||||
at = self.active_tab
|
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:
|
def start_tab_drag(self, pixels: bytes, width: int, height: int) -> None:
|
||||||
if (state := self.tab_drag_state) is None:
|
if (state := self.tab_drag_state) is None:
|
||||||
return
|
return
|
||||||
for i, tab in enumerate(self.tabs_to_be_shown_in_tab_bar):
|
for i, tab in enumerate(self.tabs_to_be_shown_in_tab_bar):
|
||||||
if tab.id == state.tab_id:
|
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 = 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 = 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 = {
|
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(),
|
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)
|
start_drag_with_data(self.os_window_id, drag_data, pixels, width, height)
|
||||||
@@ -1585,7 +1662,6 @@ class TabManager: # {{{
|
|||||||
else:
|
else:
|
||||||
if self.tab_drag_state is None or not self.tab_drag_state.drag_started:
|
if self.tab_drag_state is None or not self.tab_drag_state.drag_started:
|
||||||
self.set_active_tab(tab)
|
self.set_active_tab(tab)
|
||||||
set_tab_being_dragged(0)
|
|
||||||
elif button == GLFW_MOUSE_BUTTON_MIDDLE:
|
elif button == GLFW_MOUSE_BUTTON_MIDDLE:
|
||||||
if action == GLFW_RELEASE and self.recent_mouse_events:
|
if action == GLFW_RELEASE and self.recent_mouse_events:
|
||||||
p = self.recent_mouse_events[-1]
|
p = self.recent_mouse_events[-1]
|
||||||
|
|||||||
Reference in New Issue
Block a user