Skip to content

Add mouse-based tab drag functionality for reordering and detachment#9296

Open
seijikohara wants to merge 2 commits intokovidgoyal:masterfrom
seijikohara:feature/tab-drag-reorder
Open

Add mouse-based tab drag functionality for reordering and detachment#9296
seijikohara wants to merge 2 commits intokovidgoyal:masterfrom
seijikohara:feature/tab-drag-reorder

Conversation

@seijikohara
Copy link

@seijikohara seijikohara commented Dec 17, 2025

Summary

Implements mouse-based tab dragging for reordering within the tab bar, moving tabs between OS windows, and detaching tabs into new windows on macOS using the GLFW Drag API.

Refs #7410

Changes in this version

  • Rebased onto latest upstream/master - Adapted to new chunked drop API (GLFWDropData, glfwReadDropData, glfwFinishDrop)
  • Code review cleanup - Removed Ghostty references, redundant docstrings, simplified error handling
  • Cross-platform stubs - Added _glfwPlatformSetDragTitle and _glfwPlatformSetDragImagePlacement stubs for X11, Wayland, and null platforms
  • GLFW documentation fixes - Corrected callback signatures and parameter types

Features

Tab Reordering

  • Drag tabs horizontally within the tab bar to change order
  • Drop position determined by tab midpoints with visual [+] indicator

Tab Detachment

  • Drag tab outside the tab bar to detach into new OS window
  • New window appears centered at drop location
  • Window position clamped to stay within monitor boundaries

Cross-Window Tab Transfer

  • Drag tab to another kitty window to transfer it
  • Only same-instance transfers accepted (PID in MIME type)

Drag Image

  • Displays tab title in 24pt bold text for readability
  • Includes scaled thumbnail of window content (20%, max 300px width)
  • Title remains readable even when macOS shrinks image outside windows

Configuration

enable_tab_drag (default: 5)

The value is the drag threshold in pixels - the distance the mouse must move before a drag begins. A negative value disables tab dragging entirely.

Examples:

  • enable_tab_drag 5 — default behavior
  • enable_tab_drag -1 — disable tab dragging

Technical Implementation

GLFW Drag API Integration

  • Uses glfwStartDrag with MIME type application/net.kovidgoyal.kitty-tab-{PID}
  • drag_callback handles ENTER/MOVE/LEAVE/STATUS_UPDATE events
  • drop_callback uses new chunked API (glfwGetDropMimeTypes, glfwReadDropData, glfwFinishDrop)
  • drag_end_callback handles drops outside kitty windows (detach)

New GLFW APIs Added

  • glfwSetDragTitle(window, title) - Sets title displayed above drag thumbnail
  • glfwSetDragEndCallback(window, callback) - Called when drag ends (accepted or rejected)
  • glfwSetDragImagePlacement(window, placement) - Controls thumbnail position (above/below cursor)

Commits

  1. Add mouse-based tab drag functionality - Core drag detection, tab reordering, visual feedback
  2. Implement GLFW Drag API integration - Native macOS drag session, cross-window drops, detachment
  3. Clean up after code review - Style fixes, documentation, cross-platform stubs

Testing

  • kitty_tests/tab_drag.py: 3 test cases (drop index, tab reorder, cross-window targeting)
  • Manual testing on macOS:
    • Drag reorder within tab bar ✓
    • Detach to new window at drop location ✓
    • Cross-window tab transfer ✓
    • Monitor boundary clamping ✓
    • Thumbnail + title drag image ✓
  • All existing tests pass

Limitations

  • macOS only (X11/Wayland have stub implementations, not yet functional)
  • Drag image shrinks when outside windows (macOS behavior), but title remains readable

@kovidgoyal
Copy link
Owner

Before reviewing the code, some initial feedback:

  1. Make a single config option for this, taking two numbers

enable_tab_drag x y

Where x is the drag threshold with negative numbers disabling tab dragging.
y is the threshold distance for how far out of the tab bar the tab should be
dragged to detach it. y should be optional defaulting to some small positive
number, negative disables detach.

  1. The issue with dragging to another OS Window should be easily fixable at least on
    macOS/X11 where you can get OS Window positions. So if a drag is in process, and
    the mouse co-ords are over another OS window it can be routed to that OS
    window. On Wayland its a bit more involved thanks to Wayland's boneheaded
    design. You would need to actually start a drag event and listen for a drag and
    drop events in the target window. Since I am guessing you are on macOS, you can
    leave that off for now. Maybe I will get to it someday, or as usual Wayland
    users will suffer.

@seijikohara
Copy link
Author

Thanks for the feedback! I've addressed both points:

  1. Config consolidation: Merged the three options into enable_tab_drag x y, where x is the drag threshold and y is the optional detach threshold (default 20). Non-positive values disable the respective behavior.

  2. Cross-window tab drag: Implemented on macOS using get_os_window_pos/get_os_window_size to resolve the target OS window from cursor coordinates. X11/Wayland support is left for future work.

I also added a drag thumbnail preview — a semi-transparent NSPanel showing the tab content while dragging outside the tab bar (macOS only). The branch has been rebased onto the latest master.

@kovidgoyal
Copy link
Owner

OK I reviewed the code in detail. Here are my comments:

  1. This is a rather hacky way to implement drag and drop. Not blaming you here,
    as GLFW didnt have the API for proper drag and drop. To remedy this I have
    added GLFW API for drag support. You can now create a proper drag and drop
    session. See drag_callback and glfwStartDrag and drop_callback. Note this is AI
    generated and reviewed by me but not tested, so if you have any issues let me
    know or feel free to fix them yourself.

  2. Now that we have proper drag and drop, there is no need for the
    extra detach setting. Instead simply detect when the tab is dropped on the same
    OS window and make it a re-arrange. If it is dropped on another OS Window, make
    it a tab move. And finally if it is dropped on no OS Window make it a detach.
    You can use a special MIME type such as application/net.kovidgoyal.kitty-tab or
    similar. As the payload it can have JSON serialized object with source os
    window id, tab id, tab title, is_active_tab in source window and any other
    fields that might be necessary for (5)

  3. I dont know if using a thumbnail of the full os window is a good idea. When
    reduced to thumbnail size it can be hard to distinguish which tab it is. Instead
    maybe just use the tab title. You can render it as an image using the same
    mechanism as draw_window_title() in glfw.c. This also has the advantage that
    you dont have to render the image post a call to render_os_window(), you can
    just render it directly when creating the drag. That said I am OK with still
    using the full os window thumbnail but in that case change the render function
    to do a single alloc for both src and dest buffers and use RAII_ALLOC

  4. When the tab is dropped it should become the currently active tab by calling
    self._set_active_tab if it was the active tab when the drag started.

  5. Rather than having a green line indicating where in the tab bar the tab will
    be dropped, I suggest refactor tab_bar.py to check if there is a drop
    candidate and draw its tab as an "extra" tab at the position it will be
    dropped.

5b) You need to handle the case when dropping onto an OS window with a hidden
tab bar because it has too few tabs (see min_tab_bar_tabs and tab_bar_filter)

Other than these, great work! I am impressed how far you got on your own.

@kovidgoyal
Copy link
Owner

Oh and you also need to handle drag and drop between two kitty instances. In this case I suppose you can just reject the drop. maybe store the pid of the kitty process in the mime entry in the drag data like application/net.kovidgoyal.kitty-tab-1234 where 1234 is the PID. Then in drag_callback you can reject the drag if the PID does not match the current processes PID.

@seijikohara seijikohara force-pushed the feature/tab-drag-reorder branch 3 times, most recently from c744bfd to c62341d Compare February 5, 2026 22:29
Implement mouse-based tab dragging:
- Drag tabs horizontally within the tab bar to reorder
- Drag outside tab bar to detach into a new OS window
- Drag to another OS window to move the tab there (macOS)
- Visual feedback: dimmed dragged tab + green drop indicator
- Coordinate conversion handles Retina/HiDPI displays

Configuration: enable_tab_drag <drag_threshold> [detach_threshold]
- First number: drag threshold in pixels (negative disables)
- Second number: detach threshold (optional, default 20, negative
  disables detach while allowing reorder)

Cross-window drag converts framebuffer coordinates to screen
coordinates using the width/framebuffer_width ratio, then performs
hit-testing against all other OS windows.

Refs kovidgoyal#7410
This commit adds mouse-based tab dragging functionality using the GLFW
Drag API. Users can now drag tabs to reorder them within the tab bar,
move tabs between OS windows, or detach tabs into new windows.

GLFW changes:
- Add drag end callback for handling drops outside windows
- Add glfwSetDragTitle API to display tab title on drag image
- Store mouse down event for valid drag initiation on macOS
- Fix memory management in NSBitmapImageRep, NSImage, NSDraggingItem
- Restrict drag operations to within application context
- Disable snap-back animation on drag cancel/fail
- Convert macOS screen coordinates to GLFW coordinates

kitty changes:
- Implement drag/drop callbacks for tab MIME type handling
- Add drop indicator as placeholder tab [+] using theme colors
- Track dragging tab state for external drop handling
- Create new OS window when tab is dropped outside all windows
- Position new window centered at drop location
- Clamp window position to stay within monitor boundaries
- Simplify enable_tab_drag config to drag_threshold only
- Add thumbnail caching for drag image generation
@seijikohara seijikohara force-pushed the feature/tab-drag-reorder branch from c62341d to 398eca8 Compare February 5, 2026 22:41
@seijikohara
Copy link
Author

seijikohara commented Feb 5, 2026

@kovidgoyal Ready for review.

Changes in second commit : 398eca8

  • Integrated GLFW Drag API: glfwStartDrag, drag_callback, drop_callback, drag_end_callback
  • Added glfwSetDragTitle and glfwSetDragImagePlacement APIs
  • Tab MIME: application/net.kovidgoyal.kitty-tab-{PID} (same-instance only)
  • Adapted to new chunked drop API (GLFWDropData, glfwReadDropData, glfwFinishDrop)
  • Cross-platform stubs for X11/Wayland/null

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants