-
Notifications
You must be signed in to change notification settings - Fork 466
feat: implement drag-to-pop-out functionality for tabs #2798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Add tauri-plugin-drag-as-window dependency from drag-rs library - Register drag-as-window plugin in Tauri app initialization - Add drag-as-window:default and webview permissions - Add @crabnebula/tauri-plugin-drag-as-window npm package - Create DraggableTabItem component for drag-to-pop-out behavior - Create useDragAsWindow hook for tab pop-out functionality - Add tabToInput helper function to convert Tab to TabInput - Integrate drag-as-window with tab bar in Header component Users can now drag tabs vertically out of the window to create a new pop-out window containing that tab's content. Co-Authored-By: yujonglee <[email protected]>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
✅ Deploy Preview for hyprnote canceled.
|
✅ Deploy Preview for howto-fix-macos-audio-selection canceled.
|
✅ Deploy Preview for hyprnote-storybook canceled.
|
Co-Authored-By: yujonglee <[email protected]>
…pture
- Create DraggableReorderItem component that integrates with Reorder.Item
- Use useDragControls to manually control when reorder drag starts
- Set dragListener={false} on Reorder.Item to prevent automatic pointer capture
- Detect drag direction: horizontal starts reorder, vertical starts pop-out
- Add proper error handling with .catch() and .finally() for dragAsWindow
- Lower threshold to 15px for more responsive drag detection
Co-Authored-By: yujonglee <[email protected]>
- Add WebkitAppRegion: 'no-drag' style to prevent Tauri's drag region from blocking pointer events on macOS - Add setPointerCapture in onPointerDown to ensure we receive move events even when pointer leaves element - Store native pointerdown event and use it for dragControls.start() as Framer Motion expects - Release pointer capture before starting either Framer Motion drag or native dragAsWindow - Update handlePointerUp and handlePointerCancel to accept PointerEvent and release capture Co-Authored-By: yujonglee <[email protected]>
- Add data-tauri-drag-region='false' attribute for explicit Tauri opt-out on macOS - Guard all releasePointerCapture calls with hasPointerCapture to prevent exceptions - Remove inline comments following code style conventions Co-Authored-By: yujonglee <[email protected]>
macOS native drag APIs may require the drag to be initiated from the initial user gesture (pointerdown) rather than from pointermove. This adds an alternative approach: hold Alt/Option while clicking a tab to immediately trigger the pop-out drag. This tests whether calling dragAsWindow from pointerdown fixes the macOS issue. The vertical drag detection from pointermove is still available as a fallback for platforms where it works. Co-Authored-By: yujonglee <[email protected]>
Co-Authored-By: yujonglee <[email protected]>
1. Drag preview styling: Use data-tab-pill attribute to find the styled tab element (with rounded corners, background, etc.) instead of capturing the plain wrapper element. This makes the drag preview look correct with rounded corners and proper text alignment. 2. Pop-out URL routing: Change the pop-out window URL from / to /app/main so it matches a valid route in the TanStack Router configuration. This fixes the 404 error when opening pop-out windows. Co-Authored-By: yujonglee <[email protected]>
| const handlePointerUp = useCallback((e: React.PointerEvent) => { | ||
| if ( | ||
| containerRef.current && | ||
| containerRef.current.hasPointerCapture(e.pointerId) | ||
| ) { | ||
| containerRef.current.releasePointerCapture(e.pointerId); | ||
| } | ||
| dragStartPosRef.current = null; | ||
| hasStartedReorderRef.current = false; | ||
| pointerDownEventRef.current = null; | ||
| }, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing isDraggingRef reset in handlePointerUp causes drag functionality to break. If a user initiates a vertical drag (which sets isDraggingRef.current = true on line 125), then releases the pointer before the async dragAsWindow operation completes, the ref remains true. This blocks all subsequent pointer move handling due to the early return on line 100, preventing any further drag operations until the async operation eventually completes.
Fix: Add isDraggingRef.current = false; in the handlePointerUp callback:
const handlePointerUp = useCallback((e: React.PointerEvent) => {
if (
containerRef.current &&
containerRef.current.hasPointerCapture(e.pointerId)
) {
containerRef.current.releasePointerCapture(e.pointerId);
}
isDraggingRef.current = false; // Add this line
dragStartPosRef.current = null;
hasStartedReorderRef.current = false;
pointerDownEventRef.current = null;
}, []);| const handlePointerUp = useCallback((e: React.PointerEvent) => { | |
| if ( | |
| containerRef.current && | |
| containerRef.current.hasPointerCapture(e.pointerId) | |
| ) { | |
| containerRef.current.releasePointerCapture(e.pointerId); | |
| } | |
| dragStartPosRef.current = null; | |
| hasStartedReorderRef.current = false; | |
| pointerDownEventRef.current = null; | |
| }, []); | |
| const handlePointerUp = useCallback((e: React.PointerEvent) => { | |
| if ( | |
| containerRef.current && | |
| containerRef.current.hasPointerCapture(e.pointerId) | |
| ) { | |
| containerRef.current.releasePointerCapture(e.pointerId); | |
| } | |
| isDraggingRef.current = false; | |
| dragStartPosRef.current = null; | |
| hasStartedReorderRef.current = false; | |
| pointerDownEventRef.current = null; | |
| }, []); | |
Spotted by Graphite Agent
Is this helpful? React 👍 or 👎 to let us know.
- Add Popout(String) variant to AppWindow enum in Rust - Update Display, FromStr, and WindowImpl implementations for Popout - Create /app/popout route with layout and index components - Create PopoutContent component to render tab content in popout windows - Update drag-to-pop-out code to use /app/popout route instead of /app/main - Regenerate TypeScript bindings with new Popout type Co-Authored-By: yujonglee <[email protected]>
feat: implement drag-to-pop-out functionality for tabs
Summary
This PR adds the ability to drag tabs out of the window to create a new pop-out window. It integrates the
tauri-plugin-drag-as-windowfrom CrabNebula's drag-rs library.Changes:
tauri-plugin-drag-as-windowRust dependency and registered the plugin@crabnebula/tauri-plugin-drag-as-windownpm packageDraggableReorderItemcomponent that wrapsReorder.Itemwith drag direction detectiontabToInputhelper to convert Tab state to TabInput for serializationdrag-as-window:default,core:webview:allow-create-webview-window)Updates since last revision
Popoutwindow type: Instead of reusing theMainwindow type, added a newPopout(String)variant to theAppWindowenum in Rust with properDisplay,FromStr, andWindowImplimplementations/app/popoutroute: New dedicated route for popout windows with its own layout (_layout.tsx) and index (_layout.index.tsx) that provides necessary context providersPopoutContentcomponent: New component (components/popout/content.tsx) that renders tab content in a simplified popout UI with traffic lights and a title header/app/main?popout=true&tab=...to/app/popout?tab=...bindings.gen.tsto include the newPopouttypePrevious fixes: Drag preview styling with
data-tab-pillattribute; Alt+click pop-out trigger;data-tauri-drag-region="false"attribute; pointer capture for reliable move eventsReview & Testing Checklist for Human
/app/popout- The new window should load without 404 and render the tab content correctlyapps/desktop/src/hooks/useDragAsWindow.tsappears to be unused dead codeRecommended test plan:
ONBOARDING=0 pnpm -F desktop tauri dev/app/popout?tab=...without errorsNotes
persister.test.tsalso fail on themainbranch and are unrelated to this PRPopoutwindow type uses a unique string ID in the formatpopout-{tab.type}-{timestamp}