diff --git a/chrome.html b/chrome.html index 2a03174..9a0188b 100644 --- a/chrome.html +++ b/chrome.html @@ -36,7 +36,14 @@ content means scrollHeight is the natural chrome height, and syncHeight shrinks correctly after every menu-close. */ body { margin: 0; background: var(--bg); color: var(--ink); user-select: none; overflow: hidden; } - .tabs { display: flex; align-items: flex-end; gap: 4px; padding: 6px 8px 0; height: 34px; overflow: hidden; } + /* The tab row doubles as the window's title bar on Windows (main.js hides + the native frame): the row is a drag region, everything clickable in it + opts out, and --wco-reserve keeps tabs clear of the native minimise / + maximise / close overlay (set from navigator.windowControlsOverlay, or a + fixed 140px when that API isn't exposed). */ + .tabs { display: flex; align-items: flex-end; gap: 4px; padding: 6px 8px 0; padding-right: calc(8px + var(--wco-reserve, 0px)); + height: 34px; overflow: hidden; -webkit-app-region: drag; } + .tabs > *, .grouppop, .ctxmenu { -webkit-app-region: no-drag; } /* Same-size tabs: each tab claims an equal share of the row, capped at 200px so 2 tabs don't stretch across the whole window. min-width lets many tabs shrink cleanly while still showing a couple letters. */ @@ -46,7 +53,12 @@ .tab.dragging { opacity: .45; cursor: grabbing; } .tab.dropbefore { box-shadow: -2px 0 0 var(--acid); } .tab.dropafter { box-shadow: 2px 0 0 var(--acid); } - .tab.active { background: var(--active); color: var(--ink); } + /* The selected tab: brighter fill plus an accent stripe and outline, so it + reads at a glance among a dozen same-size tabs. A grouped tab keeps its + group colour on the stripe and still gets the accent outline. */ + .tab.active { background: var(--active); color: var(--ink); border-color: rgb(from var(--acid) r g b / .7); + box-shadow: inset 0 2px 0 var(--acid); } + .tab.active.grp { box-shadow: inset 0 2px 0 var(--gc, var(--acid)); } .tab .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; } .tab .fav { width: 14px; height: 14px; flex: none; border-radius: 2px; object-fit: contain; } .tab .spin { width: 10px; height: 10px; flex: none; border: 1.5px solid #ffffff2e; border-top-color: var(--acid); border-radius: 50%; animation: spin .7s linear infinite; } @@ -467,6 +479,24 @@ const $ = (id) => document.getElementById(id); const T = window.theseus; function syncHeight() { requestAnimationFrame(() => T.setChromeHeight(document.body.scrollHeight)); } + // Reserve room for the native window-controls overlay at the right of the + // tab row (Windows only — the frame is hidden there, see main.js). Use the + // overlay's real geometry when Chromium exposes it to this view; otherwise + // the standard three 46px buttons. + (function reserveWindowControls() { + if (!/Windows/i.test(navigator.userAgent)) return; + const apply = () => { + const wco = navigator.windowControlsOverlay; + let reserve = 140; + try { + if (wco && wco.visible) { const r = wco.getTitlebarAreaRect(); reserve = Math.max(0, Math.round(window.innerWidth - (r.x + r.width))); } + } catch {} + document.documentElement.style.setProperty("--wco-reserve", reserve + "px"); + }; + apply(); + try { navigator.windowControlsOverlay?.addEventListener("geometrychange", apply); } catch {} + window.addEventListener("resize", apply); + })(); const RELOAD_SVG = ``; const STOP_SVG = ``; diff --git a/main.js b/main.js index bd32c46..14bfbb9 100644 --- a/main.js +++ b/main.js @@ -2800,8 +2800,18 @@ function createTab(initial, opts = {}) { try { wc.setWebRTCIPHandlingPolicy(webrtcPolicy()); } catch {} try { wc.setBackgroundThrottling(settings.backgroundThrottle); } catch {} applyFingerprint(wc); - const tab = { id, view, title: opts.settings ? "Settings" : "New Tab", url: "", favicon: null, prov: null, settings: !!opts.settings, muted: false, group: null }; - tabs.push(tab); + const tab = { id, view, title: opts.settings ? "Settings" : "New Tab", url: "", favicon: null, prov: null, settings: !!opts.settings, muted: false, group: null, openerId: opts.after ?? null }; + // A tab opened from a link goes right after the tab it came from — and + // after any siblings that tab already opened, so a run of links from one + // page reads left-to-right — instead of at the far end of the strip. + // Everything else (+ button, restore, add-on requests) appends as before. + const openerIdx = opts.after != null ? tabs.findIndex((t) => t.id === opts.after) : -1; + if (openerIdx < 0) tabs.push(tab); + else { + let at = openerIdx + 1; + while (at < tabs.length && tabs[at].openerId === opts.after) at++; + tabs.splice(at, 0, tab); + } win.contentView.addChildView(view); wc.on("page-title-updated", (_e, title) => { tab.title = title; emitTabs(); @@ -2910,7 +2920,7 @@ function createTab(initial, opts = {}) { }); // Links that open a new tab: target="_blank", window.open, Ctrl/middle-click. wc.setWindowOpenHandler(({ url, disposition }) => { - if (url && url !== "about:blank") createTab(url, { background: disposition === "background-tab" }); + if (url && url !== "about:blank") createTab(url, { background: disposition === "background-tab", after: tab.id }); return { action: "deny" }; }); // Right-click context menu. @@ -2918,8 +2928,8 @@ function createTab(initial, opts = {}) { const items = []; if (p.linkURL) { items.push( - { label: "Open link in new tab", click: () => createTab(p.linkURL) }, - { label: "Open link in new background tab", click: () => createTab(p.linkURL, { background: true }) }, + { label: "Open link in new tab", click: () => createTab(p.linkURL, { after: tab.id }) }, + { label: "Open link in new background tab", click: () => createTab(p.linkURL, { background: true, after: tab.id }) }, { label: "Open link in new window", click: () => openLinkWindow(p.linkURL) }, { label: "Copy link address", click: () => clipboard.writeText(p.linkURL) }, { type: "separator" }, @@ -2930,7 +2940,7 @@ function createTab(initial, opts = {}) { // preset savePath, so Electron shows the native Save As dialog. if (p.mediaType === "image" && p.srcURL) { items.push( - { label: "Open image in new tab", click: () => createTab(p.srcURL) }, + { label: "Open image in new tab", click: () => createTab(p.srcURL, { after: tab.id }) }, { label: "Save image as…", click: () => wc.downloadURL(p.srcURL) }, { label: "Copy image", click: () => { try { wc.copyImageAt(p.x, p.y); } catch {} } }, { label: "Copy image address", click: () => clipboard.writeText(p.srcURL) }, @@ -2947,7 +2957,7 @@ function createTab(initial, opts = {}) { if (raw) { const excerpt = raw.length > 40 ? raw.slice(0, 40) + "…" : raw; items.push( - { label: `Search for "${excerpt.replace(/&/g, "&&")}"`, click: () => createTab(SEARCH(raw)) }, + { label: `Search for "${excerpt.replace(/&/g, "&&")}"`, click: () => createTab(SEARCH(raw), { after: tab.id }) }, { type: "separator" }, ); } @@ -3000,14 +3010,28 @@ function createWindow() { // until chrome.html painted, which is the "white strip over a dark // window" users saw on a slow launch. const uiBg = nativeTheme.shouldUseDarkColors ? "#0f1420" : "#e6e8ec"; + // On Windows the tab strip lives in the title bar: the frame is hidden and + // the native minimise/maximise/close buttons are drawn as an overlay over + // our chrome, whose tab row is a drag region (chrome.html). That returns + // the OS title bar's height to the page. Kept native elsewhere. + const titleBarOverlay = { color: uiBg, symbolColor: nativeTheme.shouldUseDarkColors ? "#e7eaf1" : "#1a1f28", height: 40 }; + const frameOpts = process.platform === "win32" ? { titleBarStyle: "hidden", titleBarOverlay } : {}; win = new BrowserWindow({ - width: 1220, height: 840, title: "Theseus Navigator", backgroundColor: uiBg, + width: 1220, height: 840, title: "Theseus Navigator", backgroundColor: uiBg, ...frameOpts, // Taskbar / titlebar icon. Packaged builds ship build/icon.ico as // extraResource; dev reads the source file directly. icon: app.isPackaged ? path.join(process.resourcesPath, "icon.ico") : path.join(__dirname, "build", "icon.ico"), }); + // Keep the overlay buttons on the theme when it flips at runtime. + if (process.platform === "win32") { + nativeTheme.on("updated", () => { + if (!win || win.isDestroyed()) return; + const dark = nativeTheme.shouldUseDarkColors; + try { win.setTitleBarOverlay({ color: dark ? "#0f1420" : "#e6e8ec", symbolColor: dark ? "#e7eaf1" : "#1a1f28", height: 40 }); } catch {} + }); + } chrome = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "preload.js") } }); try { chrome.setBackgroundColor(uiBg); } catch {} win.contentView.addChildView(chrome); @@ -3332,7 +3356,7 @@ ipcMain.handle("tab-context-menu-popup", (e, tabId, rect) => { const bmDisabled = !t.url; const template = [ { label: "Reload", click: () => { try { t.view.webContents.reload(); } catch {} } }, - { label: "Duplicate", click: () => { if (t.url) createTab(t.url); else createTab(); } }, + { label: "Duplicate", click: () => { if (t.url) createTab(t.url, { after: t.id }); else createTab(null, { after: t.id }); } }, { label: "Group", submenu: groupSubmenu }, { label: "Add to Bookmarks", enabled: !bmDisabled, click: () => { if (bmDisabled) return;