diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index fbf655ab9..ee609ea25 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -811,6 +811,7 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int * } if (send_cell_data_to_gpu(WD.vao_idx, WD.screen, os_window)) needs_render = true; if (WD.screen->start_visual_bell_at != 0) needs_render = true; + if (WD.screen->start_drag_overlay_at != 0) needs_render = true; // Prepare window title bar screen data for GPU WindowRenderData *trd = &w->window_title_render_data; if (trd->screen && trd->geometry.bottom > trd->geometry.top && trd->geometry.right > trd->geometry.left) { @@ -877,6 +878,7 @@ render_prepared_os_window(OSWindow *os_window, unsigned int active_window_id, co if (is_active_window) active_window = w; draw_cells(&WD, os_window, is_active_window, false, num_of_visible_windows == 1, w); if (WD.screen->start_visual_bell_at != 0) set_maximum_wait(ANIMATION_SAMPLE_WAIT); + if (WD.screen->start_drag_overlay_at != 0) set_maximum_wait(ANIMATION_SAMPLE_WAIT); WindowRenderData *trd = &w->window_title_render_data; if (trd->screen && trd->geometry.right > trd->geometry.left && trd->geometry.bottom > trd->geometry.top) draw_cells(trd, os_window, i == tab->active_window, true, false, NULL); diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 9bb66ab95..7b99c1660 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1536,6 +1536,9 @@ def spawn( pass +def set_window_drag_overlay(os_window_id: int, tab_id: int, window_id: int, quadrant: int) -> None: ... + + def set_window_padding(os_window_id: int, tab_id: int, window_id: int, left: int, top: int, right: int, bottom: int) -> None: pass diff --git a/kitty/screen.h b/kitty/screen.h index 092aeaaf2..317b534c0 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -129,6 +129,8 @@ typedef struct { ScreenModes modes, saved_modes; ColorProfile *color_profile; monotonic_t start_visual_bell_at; + monotonic_t start_drag_overlay_at; // 0 = inactive + uint8_t drag_overlay_quadrant; // 1=left 2=right 3=top 4=bottom 0=none uint8_t *write_buf; size_t write_buf_sz, write_buf_used; diff --git a/kitty/shaders.c b/kitty/shaders.c index 1e5d4c8fd..d35f36a40 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -787,6 +787,36 @@ draw_visual_bell(const UIRenderData *ui) { #undef COLOR } +static void +draw_drag_preview_overlay(const UIRenderData *ui) { + Screen *screen = ui->screen; + if (!screen->start_drag_overlay_at || !screen->drag_overlay_quadrant) return; + const monotonic_t elapsed = monotonic() - screen->start_drag_overlay_at; + const monotonic_t fade_ms = ms_to_monotonic_t(150ll); + float intensity = elapsed >= fade_ms ? 1.0f : (float)elapsed / (float)fade_ms; + GLfloat left = -1.f, top = 1.f, right = 1.f, bottom = -1.f; + switch (screen->drag_overlay_quadrant) { + case 1: right = 0.f; break; // left half + case 2: left = 0.f; break; // right half + case 3: bottom = 0.f; break; // top half + case 4: top = 0.f; break; // bottom half + case 5: break; // full window (swap) + default: return; + } + bind_program(TINT_PROGRAM); + float a = intensity * 0.25f; +#define COLOR(name, fallback) colorprofile_to_color_with_fallback(screen->color_profile, \ + screen->color_profile->overridden.name, screen->color_profile->configured.name, \ + screen->color_profile->overridden.fallback, screen->color_profile->configured.fallback) + color_type hint = !IS_SPECIAL_COLOR(highlight_bg) ? COLOR(visual_bell_color, highlight_bg) : COLOR(visual_bell_color, default_fg); +#undef COLOR +#define C(shift) (srgb_color((hint >> shift) & 0xFF) * a) + glUniform4f(tint_program_layout.uniforms.tint_color, C(16), C(8), C(0), a); +#undef C + glUniform4f(tint_program_layout.uniforms.edges, left, top, right, bottom); + draw_quad(true, 0); +} + static bool has_scrollbar(Window *w, Screen *screen) { if (screen->linebuf != screen->main_linebuf || !screen->historybuf->count) return false; @@ -1209,6 +1239,7 @@ draw_cells(const WindowRenderData *srd, OSWindow *os_window, bool is_active_wind ui.screen_left, ui.screen_top, ui.screen_width, ui.screen_height, ui.full_framebuffer_height); if (ui.os_window->needs_layers) draw_cells_with_layers(&ui, srd->vao_idx); else draw_cells_without_layers(&ui, srd->vao_idx); + draw_drag_preview_overlay(&ui); restore_viewport(); } // }}} diff --git a/kitty/state.c b/kitty/state.c index e378dcc2a..11a2adc8c 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -864,6 +864,25 @@ PYWRAP1(set_tab_bar_render_data) { Py_RETURN_NONE; } +PYWRAP1(set_window_drag_overlay) { + id_type os_window_id, tab_id, window_id; + int quadrant; + PA("KKKi", &os_window_id, &tab_id, &window_id, &quadrant); + WITH_WINDOW(os_window_id, tab_id, window_id) + Screen *s = window->render_data.screen; + if (s) { + if (quadrant == 0) { + s->start_drag_overlay_at = 0; + s->drag_overlay_quadrant = 0; + } else if (s->drag_overlay_quadrant != (uint8_t)quadrant) { + s->start_drag_overlay_at = monotonic(); + s->drag_overlay_quadrant = (uint8_t)quadrant; + } + } + END_WITH_WINDOW + Py_RETURN_NONE; +} + PYWRAP1(set_window_title_bar_render_data) { WindowGeometry g = {0}; id_type os_window_id, tab_id, window_id; @@ -1632,6 +1651,7 @@ static PyMethodDef module_methods[] = { MW(set_tab_bar_render_data, METH_VARARGS), MW(set_window_title_bar_render_data, METH_VARARGS), MW(set_window_render_data, METH_VARARGS), + MW(set_window_drag_overlay, METH_VARARGS), MW(set_window_padding, METH_VARARGS), MW(viewport_for_window, METH_VARARGS), MW(cell_size_for_window, METH_VARARGS), diff --git a/kitty/tabs.py b/kitty/tabs.py index 65ffe3225..dc62919f3 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -1124,6 +1124,7 @@ class TabBeingDropped(NamedTuple): class WindowBeingDropped(NamedTuple): window_id: int # the window whose title bar is currently highlighted as a drop target + quadrant: int = 0 # 0=none, 1=left, 2=right, 3=top, 4=bottom, 5=full(swap) class TabManager: # {{{ @@ -1881,21 +1882,26 @@ class TabManager: # {{{ return win return None - def _set_drag_target_window(self, window_id: int) -> None: - """Highlight window_id's title bar as the drop target; 0 clears.""" + def _set_drag_target_window(self, window_id: int, quadrant: int = 0) -> None: + """Highlight window_id's title bar as the drop target; 0 clears. quadrant!=0 shows quadrant overlay instead.""" + from .fast_data_types import set_window_drag_overlay boss = get_boss() prev_id = self.window_being_dropped.window_id if self.window_being_dropped else 0 - if prev_id == window_id: + prev_quadrant = self.window_being_dropped.quadrant if self.window_being_dropped else 0 + if prev_id == window_id and prev_quadrant == quadrant: return if prev_id and (prev_w := boss.window_id_map.get(prev_id)): prev_w.is_drag_target = False + set_window_drag_overlay(self.os_window_id, prev_w.tab_id, prev_id, 0) if prev_w._title_bar_screen is not None: tab = prev_w.tabref() prev_w.update_title_bar(is_active=tab is not None and tab.active_window is prev_w) if window_id and (new_w := boss.window_id_map.get(window_id)): - new_w.is_drag_target = True - new_w.update_title_bar(is_active=True) - self.window_being_dropped = WindowBeingDropped(window_id=window_id) + if quadrant == 5: + new_w.is_drag_target = True + new_w.update_title_bar(is_active=True) + set_window_drag_overlay(self.os_window_id, new_w.tab_id, window_id, quadrant) + self.window_being_dropped = WindowBeingDropped(window_id=window_id, quadrant=quadrant) else: self.window_being_dropped = None @@ -1912,21 +1918,23 @@ class TabManager: # {{{ return self._set_drag_target_tab(0) dest_window = self._find_window_at(x, y) - if dest_window and dest_window.id != window_id and dest_window.show_title_bar: - from .fast_data_types import cell_size_for_window - _, ch = cell_size_for_window(self.os_window_id) - g = dest_window.geometry - opts = get_options() - tb_top = g.top if opts.window_title_bar == 'top' else g.bottom - ch - tb_bottom = tb_top + ch - from .fast_data_types import viewport_for_window - central = viewport_for_window(self.os_window_id)[0] - rel_y = y - central.top - in_title_bar = tb_top <= rel_y < tb_bottom - target_id = dest_window.id if in_title_bar else 0 + if dest_window and dest_window.id != window_id: + quadrant = 5 + active_tab = self.active_tab + if active_tab is not None and hasattr(active_tab.current_layout, 'insert_window_next_to'): + from .fast_data_types import viewport_for_window as _vfw + central = _vfw(self.os_window_id)[0] + rel_x = x - central.left + rel_y = y - central.top + g = dest_window.geometry + dx = rel_x - (g.left + g.right) / 2 + dy = rel_y - (g.top + g.bottom) / 2 + quad_map = {'left': 1, 'right': 2, 'top': 3, 'bottom': 4} + direction = ('right' if dx > 0 else 'left') if abs(dx) >= abs(dy) else ('bottom' if dy > 0 else 'top') + quadrant = quad_map[direction] + self._set_drag_target_window(dest_window.id, quadrant) else: - target_id = 0 - self._set_drag_target_window(target_id) + self._set_drag_target_window(0) def on_window_drop(self, x: int, y: int, window_id: int) -> None: from .fast_data_types import cell_size_for_window, viewport_for_window