feat(editor): drag an editor tab out to a standalone window (#470)

Dragging an editor tab past the window edge, or choosing Open in New Window from its menu, now opens the file in its own window and closes the tab here. A text file is saved first and a custom document runs its Save / Do not Save / Cancel guard once; if saving or opening fails, the tab stays. The new window is forced even when the project is already open elsewhere, and a file outside the project opens as LightEdit.

Dragging out tool windows is not covered. Refs #454

Co-authored-by: a-rookie-of-C-language <[email protected]>
Цей коміт міститься в:
arookieofcтаa-rookie-of-C-language authored and GitHub committed 2026-10-31 00:57:53 +08:00
1 джерело 986b0ade58
коміт 2754cf327f
12 files changed
+345 -12

No files matched your search

+30 -4
Переглянути файл
@@ -51,6 +51,19 @@ export interface DragSession {
cancel(): void;
}
/**
* Whether client point (`x`, `y`) is outside `rect` (the window's viewport):
* a tab drag released there detaches into a new window instead of reordering.
* Pure, so detach tests don't need a DOM.
*/
export function isOutsideWindow(
rect: { left: number; top: number; right: number; bottom: number },
x: number,
y: number,
): boolean {
return x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom;
}
export function dragSession(data: DragData): DragSession {
let current: DropTarget | null = null;
const enter = (t: DropTarget | null) => {
@@ -84,16 +97,25 @@ export function setDragRequiresAlt(fn: () => boolean) {
}
export const dragRequiresAlt = () => requiresAlt();
export interface PointerDrop {
x: number;
y: number;
/** True when a registered target accepted the drop. */
accepted: boolean;
}
/**
* Starts a drag from a primary-button press on `source` once the pointer moved
* past the threshold; a ghost copy of `source` follows the pointer. Escape
* cancels. `onEnd` gets whether the drag happened (to swallow the click).
* cancels. `onEnd` gets whether the drag happened (to swallow the click) and,
* when it did, where the pointer was released and whether a target accepted it
* (an editor tab released outside the window detaches into a new window).
*/
export function startPointerDrag(
e: { button: number; clientX: number; clientY: number; altKey: boolean },
source: HTMLElement,
data: DragData,
opts: { onStart?: () => void; onEnd?: (dragged: boolean) => void } = {},
opts: { onStart?: () => void; onEnd?: (dragged: boolean, drop?: PointerDrop) => void } = {},
) {
if (e.button !== 0 || (requiresAlt() && !e.altKey)) return;
const start = { x: e.clientX, y: e.clientY };
@@ -124,8 +146,12 @@ export function startPointerDrag(
const onUp = (ev: PointerEvent) => {
const dragged = !!ghost;
cleanup();
if (dragged) session.drop(ev.clientX, ev.clientY);
opts.onEnd?.(dragged);
if (!dragged) {
opts.onEnd?.(false);
return;
}
const accepted = session.drop(ev.clientX, ev.clientY);
opts.onEnd?.(true, { x: ev.clientX, y: ev.clientY, accepted });
};
const onKey = (ev: KeyboardEvent) => {
if (ev.key !== "Escape" || !ghost) return;