diff --git a/documentation/BIG_PICTURE_MODE.md b/documentation/BIG_PICTURE_MODE.md index 6791cc0..a585940 100644 --- a/documentation/BIG_PICTURE_MODE.md +++ b/documentation/BIG_PICTURE_MODE.md @@ -2,6 +2,13 @@ Nebula Browser includes a **Big Picture Mode** - a controller-friendly, console-style interface designed for Steam Deck, handheld devices, and living room setups. +## 🚀 Single Window Architecture + +Big Picture Mode now opens **in the main window** instead of a separate window. This design: +- **Keeps resources low** - No extra Electron window process +- **Prevents SteamOS conflicts** - When auto-launching in Gaming Mode, Steam won't create a desktop mode alongside, preventing Steam from overriding controls to emulate keyboard/mouse +- **Seamless switching** - Navigate between Desktop and Big Picture modes smoothly + ## ⚠️ Steam Deck: Disabling Mouse Emulation If Steam is emulating mouse/keyboard input with the joysticks (overriding native controller support), you need to configure Steam Input: @@ -129,10 +136,13 @@ const suggested = await window.bigPictureAPI.isSuggested(); // Get screen information const info = await window.bigPictureAPI.getScreenInfo(); -// Launch Big Picture Mode +// Check if currently in Big Picture Mode +const isActive = await window.bigPictureAPI.isActive(); + +// Launch Big Picture Mode (navigates main window) await window.bigPictureAPI.launch(); -// Exit Big Picture Mode +// Exit Big Picture Mode (navigates back to desktop UI) await window.bigPictureAPI.exit(); ``` diff --git a/main.js b/main.js index 0047011..759b1ac 100644 --- a/main.js +++ b/main.js @@ -322,7 +322,8 @@ const STEAM_DECK_WIDTH = 1280; const STEAM_DECK_HEIGHT = 800; const HANDHELD_THRESHOLD = 1366; // Consider screens smaller than this as "handheld" -let bigPictureWindow = null; +// Track if main window is currently in Big Picture Mode (no separate window anymore) +let isInBigPictureMode = false; /** * Check if the current display is likely a Steam Deck or similar handheld @@ -362,89 +363,78 @@ function getScreenInfo() { } /** - * Create Big Picture Mode window + * Launch Big Picture Mode in the main window (no separate window) + * This keeps resources low and prevents SteamOS from creating desktop mode alongside. */ -function createBigPictureWindow() { - if (bigPictureWindow && !bigPictureWindow.isDestroyed()) { - bigPictureWindow.focus(); - return bigPictureWindow; +function launchBigPictureMode() { + const windows = BrowserWindow.getAllWindows(); + const mainWindow = windows[0]; + + if (!mainWindow || mainWindow.isDestroyed()) { + console.warn('[BigPicture] No main window available'); + return null; } - - const primaryDisplay = screen.getPrimaryDisplay(); - const { width, height } = primaryDisplay.workAreaSize; - - bigPictureWindow = new BrowserWindow({ - width: width, - height: height, - fullscreen: true, - frame: false, - show: false, - backgroundColor: '#0a0a0f', - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - nodeIntegration: false, - contextIsolation: true, - webviewTag: true, - spellcheck: false, - webSecurity: true, - }, - icon: process.platform === 'darwin' - ? path.join(__dirname, 'assets/images/Logos/Nebula-Favicon.icns') - : path.join(__dirname, 'assets/images/Logos/Nebula-favicon.png'), - title: 'Nebula - Big Picture Mode' - }); - - bigPictureWindow.loadFile('renderer/bigpicture.html'); - - bigPictureWindow.once('ready-to-show', () => { - bigPictureWindow.show(); - - // Apply saved display scale to big picture window - try { - const configPath = path.join(app.getPath('userData'), 'localStorage'); - const dbPath = path.join(configPath, 'leveldb'); - // Note: localStorage will be applied via CSS in bigpicture.js since it reads from localStorage directly - // The IPC handler will apply setZoomFactor when adjustments are made - console.log('[BigPicture] Window ready - display scale will be applied from localStorage'); - } catch (err) { - console.warn('[BigPicture] Failed to apply saved display scale on ready:', err); - } - - console.log('[BigPicture] Window ready'); - }); - - bigPictureWindow.on('closed', () => { - bigPictureWindow = null; - console.log('[BigPicture] Window closed'); - }); - - return bigPictureWindow; + + if (isInBigPictureMode) { + console.log('[BigPicture] Already in Big Picture Mode'); + mainWindow.focus(); + return mainWindow; + } + + isInBigPictureMode = true; + + // Enter fullscreen for Big Picture experience + mainWindow.setFullScreen(true); + mainWindow.setTitle('Nebula - Big Picture Mode'); + + // Navigate to Big Picture UI + mainWindow.loadFile('renderer/bigpicture.html'); + + console.log('[BigPicture] Launched in main window'); + return mainWindow; } /** - * Exit Big Picture Mode and return to desktop UI + * Exit Big Picture Mode and return to desktop UI in the same window */ function exitBigPictureMode() { - if (bigPictureWindow && !bigPictureWindow.isDestroyed()) { - bigPictureWindow.close(); - bigPictureWindow = null; + const windows = BrowserWindow.getAllWindows(); + const mainWindow = windows[0]; + + if (!mainWindow || mainWindow.isDestroyed()) { + console.warn('[BigPicture] No main window to exit from'); + return; } - // Ensure main window exists and is focused - const windows = BrowserWindow.getAllWindows(); - const mainWindow = windows.find(w => w !== bigPictureWindow); - if (mainWindow) { - mainWindow.focus(); - } else if (windows.length === 0) { - createWindow(); + if (!isInBigPictureMode) { + console.log('[BigPicture] Not in Big Picture Mode'); + return; } + + isInBigPictureMode = false; + + // Exit fullscreen and restore normal window + mainWindow.setFullScreen(false); + mainWindow.setTitle('Nebula'); + + // Navigate back to desktop UI + mainWindow.loadFile('renderer/index.html'); + + // Maximize on Windows after exiting fullscreen + if (process.platform === 'win32') { + setTimeout(() => { + try { mainWindow.maximize(); } catch {} + }, 100); + } + + console.log('[BigPicture] Exited to desktop mode'); } // IPC handlers for Big Picture Mode ipcMain.handle('get-screen-info', () => getScreenInfo()); ipcMain.handle('launch-bigpicture', () => { - createBigPictureWindow(); + launchBigPictureMode(); return { success: true }; }); @@ -457,6 +447,11 @@ ipcMain.handle('is-bigpicture-suggested', () => { return isSteamDeckDisplay(); }); +// Check if currently in Big Picture Mode +ipcMain.handle('is-in-bigpicture', () => { + return isInBigPictureMode; +}); + ipcMain.on('exit-bigpicture', () => { exitBigPictureMode(); }); @@ -480,10 +475,15 @@ ipcMain.handle('webview-send-input-event', async (event, { webContentsId, inputE // ============================================================================= -function createWindow(startUrl) { +function createWindow(startUrl, bigPictureMode = false) { // Capture high‑resolution startup timing markers const perfMarks = { createWindow_called: performance.now() }; + // Track Big Picture Mode state if starting in that mode + if (bigPictureMode) { + isInBigPictureMode = true; + } + // Get the available screen size (avoid full workArea allocation jank by starting slightly smaller then maximizing later if desired) const { width, height } = screen.getPrimaryDisplay().workAreaSize; const initialWidth = Math.min(width, Math.round(width * 0.9)); @@ -491,11 +491,11 @@ function createWindow(startUrl) { // Window is created hidden; we only show after first meaningful paint to avoid OS‑level pointer jank while Chromium initializes let windowOptions = { - width: initialWidth, - height: initialHeight, + width: bigPictureMode ? width : initialWidth, + height: bigPictureMode ? height : initialHeight, show: false, useContentSize: true, - backgroundColor: '#121212', // avoids white flash & early extra paints + backgroundColor: bigPictureMode ? '#0a0a0f' : '#121212', // Big Picture uses darker bg resizable: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), @@ -517,7 +517,7 @@ function createWindow(startUrl) { partition: 'persist:main', sandbox: false }, - fullscreen: false, + fullscreen: bigPictureMode, // Start in fullscreen for Big Picture Mode autoHideMenuBar: true, icon: process.platform === 'darwin' ? path.join(__dirname, 'assets/images/Logos/Nebula-Favicon.icns') @@ -621,7 +621,13 @@ function createWindow(startUrl) { }); }); - win.loadFile('renderer/index.html'); + // Load appropriate UI based on mode (Big Picture or Desktop) + if (bigPictureMode) { + win.loadFile('renderer/bigpicture.html'); + win.setTitle('Nebula - Big Picture Mode'); + } else { + win.loadFile('renderer/index.html'); + } perfMarks.loadFile_issued = performance.now(); // if caller passed in a URL, forward it to the renderer after load @@ -762,11 +768,11 @@ app.whenReady().then(() => { const t0 = performance.now(); // If launched via SteamOS Gaming Mode / gamepad UI, default to Big Picture Mode. - // Desktop launches remain unchanged. + // Desktop launches remain unchanged. Big Picture now opens in main window to keep resources low. const startInBigPicture = shouldStartInBigPictureMode(); if (startInBigPicture) { - console.log('[Startup] Detected game mode launch; starting in Big Picture Mode'); - createBigPictureWindow(); + console.log('[Startup] Detected game mode launch; starting in Big Picture Mode (in main window)'); + createWindow(null, true); // Pass bigPictureMode flag } else { createWindow(); } diff --git a/preload.js b/preload.js index b072151..9d5d17d 100644 --- a/preload.js +++ b/preload.js @@ -442,14 +442,18 @@ contextBridge.exposeInMainWorld('aboutAPI', { }); // Big Picture Mode API - Steam Deck / Console UI +// Note: Big Picture Mode now opens in the main window (not a separate window) to keep resources low +// and prevent SteamOS from creating desktop mode alongside when auto-launching. contextBridge.exposeInMainWorld('bigPictureAPI', { // Get screen info to determine if Big Picture Mode is recommended getScreenInfo: () => ipcRenderer.invoke('get-screen-info'), // Check if device is likely a Steam Deck or handheld isSuggested: () => ipcRenderer.invoke('is-bigpicture-suggested'), - // Launch Big Picture Mode + // Check if currently in Big Picture Mode + isActive: () => ipcRenderer.invoke('is-in-bigpicture'), + // Launch Big Picture Mode (navigates main window to Big Picture UI) launch: () => ipcRenderer.invoke('launch-bigpicture'), - // Exit Big Picture Mode + // Exit Big Picture Mode (navigates main window back to desktop UI) exit: () => ipcRenderer.invoke('exit-bigpicture'), // Navigate to URL (from Big Picture Mode) navigate: (url) => ipcRenderer.send('bigpicture-navigate', url),