diff --git a/main.js b/main.js index 887dd40c..ddc7f8d0 100644 --- a/main.js +++ b/main.js @@ -2638,21 +2638,76 @@ function pushNav(prov) { emitPwAvailability(); } +// ---- Fullscreen ------------------------------------------------------------ +// Two ways in: a page asks for HTML fullscreen (video players: Electron puts +// the whole window in fullscreen for it) or the user presses F11. Nothing +// used to own either, so the toolbar stayed on top of a fullscreen video, +// and a page that left fullscreen while its tab was hidden, or a fullscreen +// tab that was closed or switched away from, left the window in fullscreen +// with no title-bar buttons, the taskbar covered and no key to get out. +let fsTabId = null; // tab whose page holds HTML fullscreen (toolbar + sidebar hidden for it) +let userFullscreen = false; // F11: window fullscreen with the toolbar kept +function enterHtmlFullscreen(tab) { + fsTabId = tab.id; + try { if (!win.isFullScreen()) win.setFullScreen(true); } catch {} + layout(); +} +// forced: Theseus is leaving on the page's behalf (tab switched or closed, +// F11) and must take the window out of fullscreen itself. When the page +// leaves on its own, Electron has already taken the window out by the time +// leave-html-full-screen fires; a second exit during that transition +// restored the window maximized (2026-09-28), so that path only checks +// later that the exit really happened. +function leaveHtmlFullscreen(tab, forced = false) { + if (!tab || fsTabId !== tab.id) return; + fsTabId = null; + if (forced) { + // The page keeps its own fullscreen state (a player's controls, the + // fullscreenchange event): tell it. + try { tab.view.webContents.executeJavaScript("document.fullscreenElement && document.exitFullscreen(); 0", true).catch(() => {}); } catch {} + try { if (!userFullscreen && win.isFullScreen()) win.setFullScreen(false); } catch {} + } else scheduleFullscreenCheck(); + layout(); +} +// A fullscreen the window did not ask for and no page holds — the stuck +// state described above — is left. Checked on a timer: during a fullscreen +// exit the window still reports fullscreen for a moment, and exiting again +// right then is what restored it maximized. +let fsCheckTimer = null; +function scheduleFullscreenCheck() { + clearTimeout(fsCheckTimer); + fsCheckTimer = setTimeout(() => { + fsCheckTimer = null; + if (!winAlive() || fsTabId != null || userFullscreen) return; + try { if (win.isFullScreen()) win.setFullScreen(false); } catch {} + }, 600); +} +function toggleUserFullscreen() { + if (!winAlive()) return; + userFullscreen = !userFullscreen; + if (fsTabId != null) leaveHtmlFullscreen(tabs.find((t) => t.id === fsTabId), true); + try { win.setFullScreen(userFullscreen); } catch {} + layout(); +} function layout() { if (!winAlive()) return; + if (fsTabId == null && !userFullscreen) { try { if (win.isFullScreen()) scheduleFullscreenCheck(); } catch {} } const { width, height } = win.getContentBounds(); - chrome.setBounds({ x: 0, y: 0, width, height: CHROME_H }); - const bodyH = Math.max(0, height - CHROME_H); + const fsTab = fsTabId != null ? tabs.find((t) => t.id === fsTabId) : null; + if (!fsTab && fsTabId != null) fsTabId = null; + const chromeH = fsTab ? 0 : CHROME_H; + chrome.setBounds({ x: 0, y: 0, width, height: chromeH }); + const bodyH = Math.max(0, height - chromeH); // Sidebar (when visible) claims a fixed slice on the right; the tab views // shrink to fit alongside it. When hidden, tabs get the full width. - const sideW = sidebarVisible ? sidebarW : 0; + const sideW = sidebarVisible && !fsTab ? sidebarW : 0; const tabW = Math.max(0, width - sideW); - for (const t of tabs) t.view.setBounds({ x: 0, y: CHROME_H, width: tabW, height: bodyH }); - if (sidebar) sidebar.setBounds({ x: tabW, y: CHROME_H, width: sideW, height: bodyH }); + for (const t of tabs) t.view.setBounds({ x: 0, y: chromeH, width: tabW, height: bodyH }); + if (sidebar) sidebar.setBounds({ x: tabW, y: chromeH, width: sideW, height: bodyH }); // Approval overlay sits exactly over the tab area — the page underneath // keeps running; only pointer input is intercepted. - if (approvalPop) approvalPop.setBounds({ x: 0, y: CHROME_H, width: tabW, height: bodyH }); - if (jsDialogPop) jsDialogPop.setBounds({ x: 0, y: CHROME_H, width: tabW, height: bodyH }); + if (approvalPop) approvalPop.setBounds({ x: 0, y: chromeH, width: tabW, height: bodyH }); + if (jsDialogPop) jsDialogPop.setBounds({ x: 0, y: chromeH, width: tabW, height: bodyH }); positionPopover(); positionEnginePicker(); positionDownloads(); @@ -3032,6 +3087,8 @@ function installDownloadTracker() { function setActive(id) { const switching = id !== activeId; activeId = id; + // A fullscreen video does not follow the user to another tab. + if (switching && fsTabId != null && fsTabId !== id) leaveHtmlFullscreen(tabs.find((t) => t.id === fsTabId), true); if (popVisible) showPopover(false); // don't carry a stale popover across tabs if (epVisible) showEnginePicker(false); if (linkStatusVisible) showLinkStatus(""); // clear any lingering hover pill @@ -3312,6 +3369,9 @@ function createTab(initial, opts = {}) { const next = (urls && urls[0]) || null; if (tab.favicon !== next) { tab.favicon = next; emitTabs(); } }); + // HTML fullscreen (see enterHtmlFullscreen): the page gets the whole window. + wc.on("enter-html-full-screen", () => enterHtmlFullscreen(tab)); + wc.on("leave-html-full-screen", () => leaveHtmlFullscreen(tab)); // Chromium fires found-in-page on every findInPage call + on subsequent // match walks. Forward to chrome so the find bar shows "N of M". wc.on("found-in-page", (_e, r) => { @@ -3557,6 +3617,7 @@ function closeTab(id) { const i = tabs.findIndex((t) => t.id === id); if (i < 0) return; const [t] = tabs.splice(i, 1); + if (fsTabId === id) leaveHtmlFullscreen(t, true); dismissJsDialogFor(id); win.contentView.removeChildView(t.view); t.view.webContents.destroy?.(); @@ -3683,6 +3744,17 @@ function createWindow() { chrome.webContents.once("dom-ready", onChromeReady); setTimeout(onChromeReady, 8000); win.on("resize", layout); + win.on("enter-full-screen", layout); + // Fullscreen left from outside (the OS, or Electron on the page's behalf) + // while a page still held it: put the toolbar back and tell the page. + win.on("leave-full-screen", () => { + userFullscreen = false; + if (fsTabId != null) { // the window is already on its way out: only the page needs telling + const t = tabs.find((x) => x.id === fsTabId); fsTabId = null; + try { t?.view.webContents.executeJavaScript("document.fullscreenElement && document.exitFullscreen(); 0", true).catch(() => {}); } catch {} + } + layout(); + }); // The browser window can close while app windows (webapps.js) keep the // process alive. Capture the session while the tabs are still here, then // let go of the window's views: tab events (hover → update-target-url, @@ -6512,6 +6584,13 @@ app.on("web-contents-created", (_event, wc) => { // the right edge with whatever we've already positioned there. Users // who prefer a detached window can still drag out from inside the // DevTools frontend itself. + // F11 toggles window fullscreen, the toolbar kept (Chrome hides it too, + // Theseus keeps it). Also the way out of a fullscreen the user did not + // ask for. + if (input.key === "F11" && !input.control && !input.alt && !input.shift) { + toggleUserFullscreen(); + return e.preventDefault(); + } const isI = input.key === "I" || input.key === "i"; if (input.key === "F12" || (input.control && input.shift && isI)) { const t = activeTab();