Theseus: move a tab to a new window

Right-click a tab and pick Move to new window, or drag it out of the tab
bar and let go off the window or over the page. The tab's own page moves
over, so sign-ins, scroll position and typed text stay as they were.

Each window has its own toolbar and tabs. The quick links, side panels
and sidebar stay in the first window and pass to another one if it
closes; the shared pop-ups (site info, downloads, password fill, page
dialogs, approvals) open in whichever window asks. Messages from a
window and events from its tabs run in that window's state, so the
existing single-window code keeps working. Closing a window's last tab
closes that window. The session file keeps every window with its place
on screen, and still opens in older builds as the first window.
This commit is contained in:
Local Dev 2026-10-07 23:08:20 +02:00
parent a29eaae876
commit f803491844
3 changed files with 543 additions and 175 deletions

View file

@ -1642,8 +1642,17 @@
// chrome view taller and opened a gap under the toolbar).
el.addEventListener("contextmenu", (e) => { e.preventDefault(); if (T.tabContextMenuPopup) T.tabContextMenuPopup(id, { x: e.clientX, y: e.clientY }); });
// Drag-reorder — drop side chosen by pointer half (matches Chrome UX).
el.addEventListener("dragstart", (e) => { dragId = id; try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(id)); } catch {} el.classList.add("dragging"); });
el.addEventListener("dragend", () => { el.classList.remove("dragging"); el.parentElement?.querySelectorAll(".tab").forEach((r) => r.classList.remove("dropbefore", "dropafter")); dragId = null; });
// Its own data type, not text/plain: let go over a page, a plain-text tab
// id would have been typed into whatever field was under the pointer.
el.addEventListener("dragstart", (e) => { dragId = id; try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("application/x-theseus-tab", String(id)); } catch {} el.classList.add("dragging"); });
el.addEventListener("dragend", (e) => {
el.classList.remove("dragging"); el.parentElement?.querySelectorAll(".tab").forEach((r) => r.classList.remove("dropbefore", "dropafter"));
// Dropped where nothing took it (off the window, or over the page): a
// new window. Esc reports (0,0) on Windows; that is a cancel, not a drop.
const dropped = e.dataTransfer && e.dataTransfer.dropEffect === "none" && (e.screenX || e.screenY);
if (dropped && dragId === id && T.detachTab) T.detachTab(id, e.screenX, e.screenY);
dragId = null;
});
el.addEventListener("dragover", (e) => {
if (dragId == null || id === dragId) return;
e.preventDefault(); e.dataTransfer.dropEffect = "move";

702
main.js

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,9 @@ contextBridge.exposeInMainWorld("theseus", {
closeTab: (id) => ipcRenderer.invoke("close-tab", id),
switchTab: (id) => ipcRenderer.invoke("switch-tab", id),
moveTab: (id, targetId, place) => ipcRenderer.invoke("move-tab", id, targetId, place),
// A tab dragged out of the bar and dropped where nothing took it: main opens
// it in a new window if the point (screen coordinates) is off the toolbar.
detachTab: (id, x, y) => ipcRenderer.invoke("tab-detach", id, x, y),
suggestAddress: (query, rect) => ipcRenderer.invoke("suggest-address", query, rect),
closeAddressPicker: () => ipcRenderer.invoke("close-address-picker"),
addressCursor: (dir) => ipcRenderer.invoke("address-cursor", dir),