From 399e8da5b4bfc9ae7c8c86598a1707967690bfef Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Sun, 28 Dec 2025 10:50:52 +1300 Subject: [PATCH] Add scroll normalization for consistent scroll speed Introduces CSS and JavaScript to normalize scroll speed across all sites by intercepting wheel events and applying a consistent scroll delta. The normalization is injected into webviews on load to ensure uniform scrolling behavior regardless of site-specific overrides. --- renderer/bigpicture.js | 101 ++++++++++++++++++++++++++++++++++++++ renderer/script.js | 107 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 207 insertions(+), 1 deletion(-) diff --git a/renderer/bigpicture.js b/renderer/bigpicture.js index 8c26c2f..97a98f4 100644 --- a/renderer/bigpicture.js +++ b/renderer/bigpicture.js @@ -5,6 +5,104 @@ const ipcRenderer = window.electronAPI; +// ============================================================================= +// SCROLL NORMALIZATION (consistent scroll speed across all sites) +// ============================================================================= + +const SCROLL_NORMALIZATION_CSS = ` + /* Disable smooth scrolling behavior that some sites force */ + *, *::before, *::after { + scroll-behavior: auto !important; + } + html, body { + scroll-behavior: auto !important; + } +`; + +const SCROLL_NORMALIZATION_JS = ` +(function() { + if (window.__nebulaScrollNormalized) return; + window.__nebulaScrollNormalized = true; + + // Consistent scroll amount in pixels per wheel delta unit + const SCROLL_SPEED = 100; + + // Intercept wheel events to normalize scroll speed + document.addEventListener('wheel', function(e) { + // Don't interfere if modifier keys are pressed (zoom, horizontal scroll, etc.) + if (e.ctrlKey || e.metaKey || e.altKey) return; + + // Get the scroll target + let target = e.target; + let scrollable = null; + + // Find the nearest scrollable element + while (target && target !== document.body && target !== document.documentElement) { + const style = window.getComputedStyle(target); + const overflowY = style.overflowY; + const overflowX = style.overflowX; + + if ((overflowY === 'auto' || overflowY === 'scroll') && target.scrollHeight > target.clientHeight) { + scrollable = target; + break; + } + if ((overflowX === 'auto' || overflowX === 'scroll') && target.scrollWidth > target.clientWidth && e.shiftKey) { + scrollable = target; + break; + } + target = target.parentElement; + } + + // If no scrollable container found, use the document + if (!scrollable) { + scrollable = document.scrollingElement || document.documentElement || document.body; + } + + // Calculate normalized scroll delta + // deltaMode: 0 = pixels, 1 = lines, 2 = pages + let deltaY = e.deltaY; + let deltaX = e.deltaX; + + if (e.deltaMode === 1) { + // Line mode - multiply by line height approximation + deltaY *= SCROLL_SPEED; + deltaX *= SCROLL_SPEED; + } else if (e.deltaMode === 2) { + // Page mode - multiply by viewport height + deltaY *= window.innerHeight; + deltaX *= window.innerWidth; + } else { + // Pixel mode - normalize to consistent speed + // Clamp the delta to prevent extremely fast scrolling from some sites + const sign = deltaY > 0 ? 1 : -1; + deltaY = sign * Math.min(Math.abs(deltaY), SCROLL_SPEED * 3); + + const signX = deltaX > 0 ? 1 : -1; + deltaX = signX * Math.min(Math.abs(deltaX), SCROLL_SPEED * 3); + } + + // Apply scroll + e.preventDefault(); + scrollable.scrollBy({ + top: deltaY, + left: e.shiftKey ? deltaX : 0, + behavior: 'auto' + }); + }, { passive: false, capture: true }); +})(); +`; + +// Function to apply scroll normalization to a webview +function applyScrollNormalization(webview) { + try { + webview.insertCSS(SCROLL_NORMALIZATION_CSS); + webview.executeJavaScript(SCROLL_NORMALIZATION_JS); + console.log('[BigPicture] Applied scroll normalization to webview'); + } catch (err) { + console.warn('[BigPicture] Failed to apply scroll normalization:', err); + } +} + // ============================================================================= // CONFIGURATION // ============================================================================= @@ -1489,6 +1587,9 @@ function navigateTo(url) { state.webviewContentsId = webview.getWebContentsId(); console.log('[BigPicture] WebContents ID:', state.webviewContentsId); + // Apply scroll normalization for consistent scroll speed + applyScrollNormalization(webview); + // Inject script to detect input field focus and notify the host injectInputFocusDetection(webview); } catch (err) { diff --git a/renderer/script.js b/renderer/script.js index 6bcab5a..69830c0 100644 --- a/renderer/script.js +++ b/renderer/script.js @@ -3,6 +3,103 @@ const ipcRenderer = window.electronAPI; const DEBUG = false; const debug = (...args) => { if (DEBUG) console.log(...args); }; +// Scroll normalization CSS and JS to ensure consistent scroll speed across all sites +const SCROLL_NORMALIZATION_CSS = ` + /* Disable smooth scrolling behavior that some sites force */ + *, *::before, *::after { + scroll-behavior: auto !important; + } + html, body { + scroll-behavior: auto !important; + } +`; + +const SCROLL_NORMALIZATION_JS = ` +(function() { + if (window.__nebulaScrollNormalized) return; + window.__nebulaScrollNormalized = true; + + // Consistent scroll amount in pixels per wheel delta unit + const SCROLL_SPEED = 100; + + // Intercept wheel events to normalize scroll speed + document.addEventListener('wheel', function(e) { + // Don't interfere if modifier keys are pressed (zoom, horizontal scroll, etc.) + if (e.ctrlKey || e.metaKey || e.altKey) return; + + // Get the scroll target + let target = e.target; + let scrollable = null; + + // Find the nearest scrollable element + while (target && target !== document.body && target !== document.documentElement) { + const style = window.getComputedStyle(target); + const overflowY = style.overflowY; + const overflowX = style.overflowX; + + if ((overflowY === 'auto' || overflowY === 'scroll') && target.scrollHeight > target.clientHeight) { + scrollable = target; + break; + } + if ((overflowX === 'auto' || overflowX === 'scroll') && target.scrollWidth > target.clientWidth && e.shiftKey) { + scrollable = target; + break; + } + target = target.parentElement; + } + + // If no scrollable container found, use the document + if (!scrollable) { + scrollable = document.scrollingElement || document.documentElement || document.body; + } + + // Calculate normalized scroll delta + // deltaMode: 0 = pixels, 1 = lines, 2 = pages + let deltaY = e.deltaY; + let deltaX = e.deltaX; + + if (e.deltaMode === 1) { + // Line mode - multiply by line height approximation + deltaY *= SCROLL_SPEED; + deltaX *= SCROLL_SPEED; + } else if (e.deltaMode === 2) { + // Page mode - multiply by viewport height + deltaY *= window.innerHeight; + deltaX *= window.innerWidth; + } else { + // Pixel mode - normalize to consistent speed + // Clamp the delta to prevent extremely fast scrolling from some sites + const sign = deltaY > 0 ? 1 : -1; + deltaY = sign * Math.min(Math.abs(deltaY), SCROLL_SPEED * 3); + + const signX = deltaX > 0 ? 1 : -1; + deltaX = signX * Math.min(Math.abs(deltaX), SCROLL_SPEED * 3); + } + + // Apply scroll + e.preventDefault(); + scrollable.scrollBy({ + top: deltaY, + left: e.shiftKey ? deltaX : 0, + behavior: 'auto' + }); + }, { passive: false, capture: true }); +})(); +`; + +// Function to apply scroll normalization to a webview +function applyScrollNormalization(webview) { + try { + // Inject CSS to disable smooth scrolling + webview.insertCSS(SCROLL_NORMALIZATION_CSS); + // Inject JS to normalize wheel scroll speed + webview.executeJavaScript(SCROLL_NORMALIZATION_JS); + debug('[Scroll] Applied scroll normalization to webview'); + } catch (err) { + console.warn('[Scroll] Failed to apply scroll normalization:', err); + } +} + // Site history management using localStorage function getSiteHistory() { try { @@ -285,8 +382,11 @@ function createTab(inputUrl) { if (e.favicons.length > 0) updateTabMetadata(id, 'favicon', e.favicons[0]); }); - // Send bookmarks to home page when it loads + // Send bookmarks to home page when it loads and apply scroll normalization webview.addEventListener('dom-ready', () => { + // Apply scroll normalization to all sites for consistent scrolling + applyScrollNormalization(webview); + if (inputUrl === 'browser://home') { webview.executeJavaScript(` if (window.receiveBookmarks) { @@ -594,6 +694,11 @@ function convertHomeTabToWebview(tabId, inputUrl, resolvedUrl) { webview.addEventListener('did-finish-load', () => { scheduleUpdateNavButtons(); }); + + // Apply scroll normalization when webview is ready + webview.addEventListener('dom-ready', () => { + applyScrollNormalization(webview); + }); webview.addEventListener('new-window', e => { // Unified behavior: always open http(s) targets in a new tab (no extra window)