diff --git a/.formal-git/components b/.formal-git/components index 43efe01f52..ddb6a85737 100644 --- a/.formal-git/components +++ b/.formal-git/components @@ -17,4 +17,5 @@ scripts workflows winsign flatpak -configs \ No newline at end of file +configs +edgescroll \ No newline at end of file diff --git a/prefs/edgescroll.yaml b/prefs/edgescroll.yaml new file mode 100644 index 0000000000..ea8ca614ae --- /dev/null +++ b/prefs/edgescroll.yaml @@ -0,0 +1,7 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +- name: zen.edgescroll.enabled + value: true + diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml index ef85595fb6..6e7e3dad16 100644 --- a/src/browser/base/content/zen-assets.inc.xhtml +++ b/src/browser/base/content/zen-assets.inc.xhtml @@ -23,6 +23,7 @@ + @@ -57,3 +58,5 @@ + + diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index 7e0ef36d45..33760e42f2 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -61,6 +61,11 @@ content/browser/zen-components/ZenGlanceManager.mjs (../../zen/glance/ZenGlanceManager.mjs) content/browser/zen-styles/zen-glance.css (../../zen/glance/zen-glance.css) + content/browser/zen-components/ZenEdgeScrollManager.mjs (../../zen/edgescroll/ZenEdgeScrollManager.mjs) + content/browser/zen-styles/zen-edgescroll.css (../../zen/edgescroll/zen-edgescroll.css) + content/browser/zen-components/actors/ZenEdgeScrollChild.sys.mjs (../../zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs) + content/browser/zen-components/actors/ZenEdgeScrollParent.sys.mjs (../../zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs) + content/browser/zen-components/ZenFolders.mjs (../../zen/folders/ZenFolders.mjs) content/browser/zen-styles/zen-folders.css (../../zen/folders/zen-folders.css) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs new file mode 100644 index 0000000000..85ffadf4b0 --- /dev/null +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -0,0 +1,384 @@ +{ + const EDGE_INTERACTION_WIDTH_PX = Services.prefs.getIntPref( + 'zen.theme.content-element-separation', + 8 + ); + const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 2; + const SYNTHETIC_EVENT_Y_OFFSET_FROM_BOTTOM_EDGE = 2; + const ACTOR_NAME = 'ZenEdgeScroll'; // Name used for actor registration + + class ZenEdgeScrollManager extends ZenDOMOperatedFeature { + init() { + this.isSynthesizingDrag = false; + this.dragInitialModel = { + targetBrowserDuringDrag: null, + targetBrowsingContextDuringDrag: null, + }; + + this.triggerDivVertical = null; + this.triggerDivHorizontal = null; + + this._boundHandleMouseDown = this.handleMouseDown.bind(this); + this._boundHandleSyntheticDrag = this.handleSyntheticDrag.bind(this); + this._boundHandleSyntheticDragEnd = this.handleSyntheticDragEnd.bind(this); + this._boundHandleWheel = this.handleWheel.bind(this); + this._boundUpdateTriggerDivDisplay = this._updateTriggerDivDisplay.bind(this); + + if (this.triggerDivVertical !== null && this.triggerDivVertical !== null) { + console.warn('ZenEdgeScrollManager is already initialized.'); + return; + } + + // Create and append the edge scroll trigger div + this.triggerDivVertical = window.document.createElement('div'); + this.triggerDivVertical.id = 'zen-edgescroll-trigger-vertical'; + document.getElementById('zen-appcontent-wrapper').appendChild(this.triggerDivVertical); + this.triggerDivVertical.addEventListener('mousedown', this._boundHandleMouseDown, true); + this.triggerDivVertical.addEventListener('wheel', this._boundHandleWheel, { + capture: true, + passive: false, + }); + + this.triggerDivHorizontal = window.document.createElement('div'); + this.triggerDivHorizontal.id = 'zen-edgescroll-trigger-horizontal'; + document.getElementById('zen-appcontent-wrapper').appendChild(this.triggerDivHorizontal); + this.triggerDivHorizontal.addEventListener('mousedown', this._boundHandleMouseDown, true); + this.triggerDivHorizontal.addEventListener('wheel', this._boundHandleWheel, { + capture: true, + passive: false, + }); + + this._updateTriggerDivDisplay(); // Added: Set initial display state + Services.prefs.addObserver( + 'zen.tabs.vertical.right-side', + this._boundUpdateTriggerDivDisplay + ); // Added: Observe preference + } + + destroy() { + if (this.triggerDivVertical) { + this.triggerDivVertical.removeEventListener('mousedown', this._boundHandleMouseDown, true); + this.triggerDivVertical.removeEventListener('wheel', this._boundHandleWheel, true); + if (this.triggerDivVertical.parentNode) { + this.triggerDivVertical.parentNode.removeChild(this.triggerDivVertical); // Corrected removeChild call + } + this.triggerDivVertical = null; + } + if (this.triggerDivHorizontal) { + this.triggerDivHorizontal.removeEventListener( + 'mousedown', + this._boundHandleMouseDown, + true + ); + this.triggerDivHorizontal.removeEventListener('wheel', this._boundHandleWheel, true); + if (this.triggerDivHorizontal.parentNode) { + this.triggerDivHorizontal.parentNode.removeChild(this.triggerDivHorizontal); // Corrected removeChild call + } + this.triggerDivHorizontal = null; + } + // These listeners are added to window, not triggerDiv in handleMouseDown + window.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); + window.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); + Services.prefs.removeObserver( + 'zen.tabs.vertical.right-side', + this._boundUpdateTriggerDivDisplay + ); // Added: Remove observer + } + + _updateTriggerDivDisplay() { + // Added method + if (!this.triggerDivVertical) { + return; + } + if (window.gZenCompactModeManager && gZenCompactModeManager.sidebarIsOnRight) { + this.triggerDivVertical.style.display = 'none'; + } else { + this.triggerDivVertical.style.display = 'block'; + } + } + + _getParentActor() { + if (!gBrowser.selectedBrowser.browsingContext.currentWindowGlobal) { + return null; + } + try { + const actor = + gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(ACTOR_NAME); + return actor; + } catch (e) { + console.error(`Error getting actor ${ACTOR_NAME}:`, e); + return null; + } + } + + getGapZoneInfo(event) { + const windowWidth = window.innerWidth; + const eventClientY = event.clientY; + + let potentialTargetBrowser = null; + let potentialTargetBrowserRect = null; + + const selectedBrowser = gBrowser.selectedBrowser; + + if (event.target == this.triggerDivVertical) { + if (selectedBrowser && selectedBrowser.getAttribute('primary') === 'true') { + const selectedBrowserRect = selectedBrowser.getBoundingClientRect(); + if (selectedBrowserRect.width > 0 && selectedBrowserRect.height > 0) { + // Check if the browser's right edge is very close to the window's right edge + const isBrowserAtRightEdge = + windowWidth - selectedBrowserRect.right <= EDGE_INTERACTION_WIDTH_PX + 1; + const isEventYWithinBrowser = + eventClientY >= selectedBrowserRect.top && eventClientY <= selectedBrowserRect.bottom; + if (isBrowserAtRightEdge && isEventYWithinBrowser) { + potentialTargetBrowser = selectedBrowser; + potentialTargetBrowserRect = selectedBrowserRect; + } + } + } + } else if (event.target == this.triggerDivHorizontal) { + // For horizontal trigger, we can check if the event is close to the bottom edge + const selectedBrowserRect = selectedBrowser.getBoundingClientRect(); + if (selectedBrowserRect.width > 0 && selectedBrowserRect.height > 0) { + const isBrowserAtBottomEdge = + window.innerHeight - selectedBrowserRect.bottom <= EDGE_INTERACTION_WIDTH_PX + 1; + const isEventXWithinBrowser = + event.clientX >= selectedBrowserRect.left && event.clientX <= selectedBrowserRect.right; + if (isBrowserAtBottomEdge && isEventXWithinBrowser) { + potentialTargetBrowser = selectedBrowser; + potentialTargetBrowserRect = selectedBrowserRect; + } + } + } + return { + isInGap: true, + targetBrowser: potentialTargetBrowser, + browserRect: potentialTargetBrowserRect, + }; + } + + createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { + const clientXInContent = Math.max( + 0, + Math.min( + Math.floor(originalEvent.clientX - targetBrowserRect.left), + Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE) + ) + ); + const clientYInContent = Math.max( + 0, + Math.min( + Math.floor(originalEvent.clientY - targetBrowserRect.top), + Math.floor(targetBrowserRect.height - SYNTHETIC_EVENT_Y_OFFSET_FROM_BOTTOM_EDGE) + ) + ); + const screenX = Math.floor(window.screenX + targetBrowserRect.left + clientXInContent); + const screenY = Math.floor(window.screenY + targetBrowserRect.top + clientYInContent); + + return { + type: eventType, + clientX: clientXInContent, + clientY: clientYInContent, + screenX: screenX, + screenY: screenY, + button: originalEvent.button, + buttons: eventType === 'mousemove' || eventType === 'mousedown' ? 1 : 0, + ctrlKey: originalEvent.ctrlKey, + altKey: originalEvent.altKey, + shiftKey: originalEvent.shiftKey, + metaKey: originalEvent.metaKey, + }; + } + + handleMouseDown(event) { + if (event.button !== 0) return; + const gapInfo = this.getGapZoneInfo(event); + + if (!gapInfo.targetBrowser) { + return; + } + let targetBrowser = gapInfo.targetBrowser; + let targetBrowserRect = gapInfo.browserRect; + + const parentActor = this._getParentActor(); + if (!parentActor || !targetBrowser.browsingContext) { + return; + } + + event.preventDefault(); + this.isSynthesizingDrag = true; + this.dragInitialModel.targetBrowserDuringDrag = targetBrowser; + this.dragInitialModel.targetBrowsingContextDuringDrag = targetBrowser.browsingContext; + + const eventData = this.createSyntheticEventData(event, targetBrowserRect, 'mousedown'); + parentActor.sendEventToChild( + targetBrowser.browsingContext, + 'ZenEdgeScroll:SynthesizeMouseEvent', + eventData + ); + + window.addEventListener('mousemove', this._boundHandleSyntheticDrag, true); + window.addEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); + } + + handleSyntheticDrag(event) { + if (!this.isSynthesizingDrag || !this.dragInitialModel.targetBrowsingContextDuringDrag) + return; + + const targetBrowser = this.dragInitialModel.targetBrowserDuringDrag; + const targetBrowsingContext = this.dragInitialModel.targetBrowsingContextDuringDrag; + + if (gBrowser.selectedBrowser !== targetBrowser) { + this.handleSyntheticDragEnd(event); + return; + } + + const parentActor = this._getParentActor(); + if (!parentActor) { + this.handleSyntheticDragEnd(event); + return; + } + + event.preventDefault(); + event.stopPropagation(); + const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); + if (currentTargetBrowserRect.width === 0 || currentTargetBrowserRect.height === 0) { + this.handleSyntheticDragEnd(event); + return; + } + const eventData = this.createSyntheticEventData(event, currentTargetBrowserRect, 'mousemove'); + parentActor.sendEventToChild( + targetBrowsingContext, + 'ZenEdgeScroll:SynthesizeMouseEvent', + eventData + ); + } + + handleSyntheticDragEnd(event) { + if (this.isSynthesizingDrag && this.dragInitialModel.targetBrowsingContextDuringDrag) { + const targetBrowser = this.dragInitialModel.targetBrowserDuringDrag; + const targetBrowsingContext = this.dragInitialModel.targetBrowsingContextDuringDrag; + const parentActor = this._getParentActor(); + + if (parentActor && event) { + // If called by an event + event.preventDefault(); + event.stopPropagation(); + const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); + if (currentTargetBrowserRect.width > 0 && currentTargetBrowserRect.height > 0) { + const eventData = this.createSyntheticEventData( + event, + currentTargetBrowserRect, + 'mouseup' + ); + parentActor.sendEventToChild( + targetBrowsingContext, + 'ZenEdgeScroll:SynthesizeMouseEvent', + eventData + ); + } else { + } + } else if (parentActor && !event) { + // Called without event (e.g. drag cancelled) + // Optionally send a generic mouseup if needed, or just clean up. + } + } + this.isSynthesizingDrag = false; + this.dragInitialModel.targetBrowserDuringDrag = null; + this.dragInitialModel.targetBrowsingContextDuringDrag = null; + window.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); + window.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); + } + + handleWheel(event) { + const gapInfo = this.getGapZoneInfo(event); + + if (!gapInfo.targetBrowser) { + return; + } + + const targetBrowser = gapInfo.targetBrowser; + const targetBrowserRect = gapInfo.browserRect; + const parentActor = this._getParentActor(); + + event.preventDefault(); + event.stopPropagation(); + const wheelData = { + deltaX: event.deltaX, + deltaY: event.deltaY, + deltaZ: event.deltaZ, + deltaMode: event.deltaMode, + ctrlKey: event.ctrlKey, + altKey: event.altKey, + shiftKey: event.shiftKey, + metaKey: event.metaKey, + clientX: Math.max( + 0, + Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE) + ), + clientY: Math.max( + 0, + Math.min( + Math.floor(event.clientY - targetBrowserRect.top), + Math.floor(targetBrowserRect.height - 1) + ) + ), + }; + parentActor.sendEventToChild(targetBrowser.browsingContext, 'ZenEdgeScroll:DispatchWheel', { + wheelData, + }); + } + } + + // Actor Registration (must happen before manager instantiation if manager relies on actors being ready) + // This is modeled after ZenGlanceManager's registerWindowActors + function registerEdgeScrollActors() { + if (Services.prefs.getBoolPref('zen.edgescroll.enabled', true)) { + window.gZenEdgeScrollManager = new ZenEdgeScrollManager(); + + const actorConfig = { + parent: { + esModuleURI: 'chrome://browser/content/zen-components/actors/ZenEdgeScrollParent.sys.mjs', + }, + child: { + esModuleURI: 'chrome://browser/content/zen-components/actors/ZenEdgeScrollChild.sys.mjs', + }, + allFrames: true, + matches: [ + '*://*/*', + 'about:*', // For about: pages + 'data:*', // For testing purposes + ], + includeChrome: true, // <--- ENSURE THIS LINE IS PRESENT AND SET TO TRUE + }; + + if ( + window.gZenActorsManager && + typeof window.gZenActorsManager.addJSWindowActor === 'function' + ) { + window.gZenActorsManager.addJSWindowActor(ACTOR_NAME, actorConfig); + } else { + console.error(`Failed to register ${ACTOR_NAME} actors:`, e); + } + } + } + + registerEdgeScrollActors(); + + // Observe changes to the enabled pref and register/destroy the manager + const edgeScrollPrefObserver = { + observe(subject, topic, data) { + if (topic === 'nsPref:changed' && data === 'zen.edgescroll.enabled') { + if (window.gZenEdgeScrollManager) { + window.gZenEdgeScrollManager.destroy(); + window.gZenEdgeScrollManager = null; + } + if (Services.prefs.getBoolPref('zen.edgescroll.enabled', true)) { + registerEdgeScrollActors(); + if (window.gZenEdgeScrollManager) window.gZenEdgeScrollManager.init(); + } + } + }, + }; + + Services.prefs.addObserver('zen.edgescroll.enabled', edgeScrollPrefObserver, false); +} diff --git a/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs new file mode 100644 index 0000000000..54d14a8a01 --- /dev/null +++ b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs @@ -0,0 +1,112 @@ +/* eslint-env mozilla/frame-script */ +/* global content, Components */ // For linter + +const { utils: Cu, interfaces: Ci } = Components; + +export class ZenEdgeScrollChild extends JSWindowActorChild { + constructor() { + super(); + } + + receiveMessage(message) { + switch (message.name) { + case 'ZenEdgeScroll:SynthesizeMouseEvent': + this.handleSynthesizeMouseEvent(message.data); + break; + case 'ZenEdgeScroll:DispatchWheel': + this.handleDispatchWheel(message.data); + break; + default: + } + } + + handleSynthesizeMouseEvent(data) { + if (!data || !data.type) { + return; + } + + const contentWin = this.contentWindow; + if (!contentWin || !contentWin.windowUtils) { + return; + } + + let modifiers = 0; + if (data.altKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_ALT; + if (data.ctrlKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_CONTROL; + if (data.metaKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_META; + if (data.shiftKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_SHIFT; + + if (data.type === 'mousemove' && data.buttons === 1) { + modifiers |= Ci.nsIDOMWindowUtils.BUTTON_PRIMARY_ACTION; + } + + let clickCount = 0; + if (data.type === 'mousedown' || data.type === 'mouseup') { + clickCount = 1; + } + + try { + contentWin.windowUtils.sendMouseEvent( + data.type, + data.clientX / contentWin.devicePixelRatio, + data.clientY / contentWin.devicePixelRatio, + data.button, + clickCount, + modifiers, + false, + 0.5, + Ci.nsIDOMWindowUtils.INPUT_SOURCE_MOUSE, + false + ); + } catch (e) { + console.error('Error dispatching mouse event:', e); + } + } + + handleDispatchWheel({ wheelData }) { + if (!wheelData) { + return; + } + const contentWin = this.contentWindow; + if (!contentWin || !contentWin.windowUtils) { + return; + } + const doc = contentWin.document; + + const clientX = + typeof wheelData.clientX === 'number' + ? wheelData.clientX + : doc.documentElement.clientWidth / 2; + const clientY = + typeof wheelData.clientY === 'number' + ? wheelData.clientY + : doc.documentElement.clientHeight / 2; + + let modifiers = 0; + if (wheelData.altKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_ALT; + if (wheelData.ctrlKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_CONTROL; + if (wheelData.metaKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_META; + if (wheelData.shiftKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_SHIFT; + + try { + contentWin.windowUtils.sendWheelEvent( + clientX, + clientY, + wheelData.deltaX, + wheelData.deltaY, + wheelData.deltaZ, + wheelData.deltaMode, + modifiers, + 0, + 0, + 0 + ); + } catch (e) { + console.error('Error dispatching wheel event:', e); + } + } + + destroy() { + super.destroy(); + } +} diff --git a/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs b/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs new file mode 100644 index 0000000000..de66bc72f5 --- /dev/null +++ b/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs @@ -0,0 +1,25 @@ +/* global Services */ // For linter + +export class ZenEdgeScrollParent extends JSWindowActorParent { + constructor() { + super(); + } + + // This actor primarily sends messages to its children. + // It might receive messages if a child needs to query the parent for info. + async receiveMessage(message) { + // Handle any messages from child if needed in the future + } + + // Called by ZenEdgeScrollManager to send a message to a specific child actor + sendEventToChild(browsingContext, messageName, eventData) { + if (!browsingContext) { + return; + } + this.sendAsyncMessage(messageName, eventData); + } + + destroy() { + super.destroy(); + } +} diff --git a/src/zen/edgescroll/moz.build b/src/zen/edgescroll/moz.build new file mode 100644 index 0000000000..cf58b8671e --- /dev/null +++ b/src/zen/edgescroll/moz.build @@ -0,0 +1,10 @@ +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +FINAL_TARGET_FILES.actors += [ + "actors/ZenEdgeScrollChild.sys.mjs", + "actors/ZenEdgeScrollParent.sys.mjs", +] diff --git a/src/zen/edgescroll/zen-edgescroll.css b/src/zen/edgescroll/zen-edgescroll.css new file mode 100644 index 0000000000..07d5307e2b --- /dev/null +++ b/src/zen/edgescroll/zen-edgescroll.css @@ -0,0 +1,25 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#zen-edgescroll-trigger-vertical { + position: absolute; + top: 0px; + right: 0px; + width:var(--zen-element-separation); + height: 100%; + z-index: 2147483647; + user-select: none; +} + +#zen-edgescroll-trigger-horizontal { + position: absolute; + bottom: 0px; + left: 0px; + width: 100%; + height: var(--zen-element-separation); + z-index: 2147483646; + user-select: none; +} diff --git a/src/zen/moz.build b/src/zen/moz.build index b17cc79159..b28a881a50 100644 --- a/src/zen/moz.build +++ b/src/zen/moz.build @@ -8,4 +8,5 @@ DIRS += [ "mods", "tests", "toolkit", + "edgescroll", ] diff --git a/src/zen/tests/edgescroll/browser.toml b/src/zen/tests/edgescroll/browser.toml new file mode 100644 index 0000000000..2c451855a5 --- /dev/null +++ b/src/zen/tests/edgescroll/browser.toml @@ -0,0 +1,5 @@ +["browser_edgescroll_triggerdiv.js"] +["browser_edgescroll_wheel.js"] +["browser_edgescroll_click.js"] +["browser_edgescroll_drag.js"] + diff --git a/src/zen/tests/edgescroll/browser_edgescroll_click.js b/src/zen/tests/edgescroll/browser_edgescroll_click.js new file mode 100644 index 0000000000..70bff9a80e --- /dev/null +++ b/src/zen/tests/edgescroll/browser_edgescroll_click.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +// Globals defined by the mochitest environment: +/* global TestUtils, Services, SpecialPowers, BrowserTestUtils, gBrowser, window, document, MouseEvent, info, ok, is */ + +add_task(async function test_ZenEdgeScroll_ClickScrollsContent() { + await SpecialPowers.pushPrefEnv({ set: [['zen.edgescroll.enabled', true]] }); + const tallPage = ` + data:text/html, + + +
+ + + + + `; + await BrowserTestUtils.openNewForegroundTab(window.gBrowser, tallPage, true); + + // wait for trigger + const trigger = await TestUtils.waitForCondition( + () => document.getElementById('zen-edgescroll-trigger-vertical'), + 'Edge scroll trigger appears' + ); + + const browser = window.gBrowser.selectedBrowser; + await SpecialPowers.spawn(browser, [], () => content.scrollTo(0, 0)); + const initialScroll = await SpecialPowers.spawn(browser, [], () => content.scrollY); + + // simulate click on the edge trigger + const rect = trigger.getBoundingClientRect(); + const clientX = rect.left + rect.width / 2; + const clientY = rect.top + rect.height / 2; + // press down on the trigger + trigger.dispatchEvent(new MouseEvent('mousedown', { + clientX, + clientY, + bubbles: true, + cancelable: true, + })); + // release to complete the click + document.dispatchEvent(new MouseEvent('mouseup', { + clientX, + clientY, + bubbles: true, + cancelable: true, + })); + + // wait for synthetic scroll + await new Promise(r => setTimeout(r, 500)); + const newScroll = await SpecialPowers.spawn(browser, [], () => content.scrollY); + ok(newScroll > initialScroll, 'Clicking the edge trigger scrolls the content'); + + BrowserTestUtils.removeTab(window.gBrowser.selectedTab); +}); \ No newline at end of file diff --git a/src/zen/tests/edgescroll/browser_edgescroll_drag.js b/src/zen/tests/edgescroll/browser_edgescroll_drag.js new file mode 100644 index 0000000000..b0cdfb8382 --- /dev/null +++ b/src/zen/tests/edgescroll/browser_edgescroll_drag.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +// Globals defined by the mochitest environment: +/* global TestUtils, Services, SpecialPowers, BrowserTestUtils, gBrowser, window, document, MouseEvent, info, ok, is */ + +add_task(async function test_ZenEdgeScroll_DragScrollsContent() { + await SpecialPowers.pushPrefEnv({ set: [['zen.edgescroll.enabled', true]] }); + const tallPage = ` + data:text/html, + + + + + + + + `; + await BrowserTestUtils.openNewForegroundTab(window.gBrowser, tallPage, true); + + // wait for trigger + const trigger = await TestUtils.waitForCondition( + () => document.getElementById('zen-edgescroll-trigger-vertical'), + 'Edge scroll trigger appears' + ); + + const browser = window.gBrowser.selectedBrowser; + await SpecialPowers.spawn(browser, [], () => content.scrollTo(0, 0)); + const initialScroll = await SpecialPowers.spawn(browser, [], () => content.scrollY); + + // simulate click on the edge trigger + const rect = trigger.getBoundingClientRect(); + const clientX = rect.left + rect.width / 2; + const startY = rect.top + rect.height / 2; + const endY = startY + 200; + const steps = 10; + const deltaY = (endY - startY) / steps; + + // press down on the trigger + trigger.dispatchEvent( + new MouseEvent('mousedown', { + clientX, + clientY: startY, + bubbles: true, + cancelable: true, + }) + ); + + // gradually move in small steps + for (let i = 1; i <= steps; i++) { + document.dispatchEvent( + new MouseEvent('mousemove', { + clientX, + clientY: startY + deltaY * i, + bubbles: true, + cancelable: true, + }) + ); + // small delay between moves to simulate a slow drag + await new Promise((r) => setTimeout(r, 200)); + } + + // release to complete the drag + document.dispatchEvent( + new MouseEvent('mouseup', { + clientX, + clientY: endY, + bubbles: true, + cancelable: true, + }) + ); + + // wait for synthetic scroll + await new Promise((r) => setTimeout(r, 500)); + const newScroll = await SpecialPowers.spawn(browser, [], () => content.scrollY); + ok(newScroll > initialScroll, 'Clicking the edge trigger scrolls the content'); + + BrowserTestUtils.removeTab(window.gBrowser.selectedTab); +}); diff --git a/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js b/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js new file mode 100644 index 0000000000..294829da18 --- /dev/null +++ b/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +// Globals defined by the mochitest environment: +/* global TestUtils, Services, SpecialPowers, BrowserTestUtils, gBrowser, window, document, MouseEvent, info, ok, is */ + +add_task(async function test_ZenEdgeScroll_TriggerExists() { + await SpecialPowers.pushPrefEnv({ set: [['zen.edgescroll.enabled', true]] }); + // Open a simple page to initialize the edge-scroll manager + await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + ` + data:text/html, + + + + + + + + `, + true + ); + + // Wait for the trigger div to appear + await TestUtils.waitForCondition( + () => !!document.getElementById('zen-edgescroll-trigger-vertical'), + 'Edge scroll trigger div should be created' + ); + const trigger = document.getElementById('zen-edgescroll-trigger-vertical'); + ok(trigger, 'The zen-edgescroll-trigger-vertical div exists'); + + BrowserTestUtils.removeTab(window.gBrowser.selectedTab); +}); diff --git a/src/zen/tests/edgescroll/browser_edgescroll_wheel.js b/src/zen/tests/edgescroll/browser_edgescroll_wheel.js new file mode 100644 index 0000000000..02d18852fd --- /dev/null +++ b/src/zen/tests/edgescroll/browser_edgescroll_wheel.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +// Globals defined by the mochitest environment: +/* global TestUtils, Services, SpecialPowers, BrowserTestUtils, gBrowser, window, document, MouseEvent, info, ok, is */ + +add_task(async function test_ZenEdgeScroll_WheelScrollsContent() { + await SpecialPowers.pushPrefEnv({ set: [['zen.edgescroll.enabled', true]] }); + // add so scrolling actually works + const tallPage = ` + data:text/html, + + + + + + + + `; + await BrowserTestUtils.openNewForegroundTab(window.gBrowser, tallPage, true); + + // give the trigger a moment + await new Promise((r) => setTimeout(r, 100)); + + const trigger = await TestUtils.waitForCondition( + () => document.getElementById('zen-edgescroll-trigger-vertical'), + 'Edge scroll trigger appears' + ); + + const browser = window.gBrowser.selectedBrowser; + await SpecialPowers.spawn(browser, [], () => content.scrollTo(0, 0)); + const initialScroll = await SpecialPowers.spawn( + browser, + [], + () => content.document.documentElement.scrollTop + ); + + const rect = trigger.getBoundingClientRect(); + const wheelEvent = new WheelEvent('wheel', { + deltaY: 200, + clientY: rect.top + 20, + bubbles: true, + cancelable: true, + }); + trigger.dispatchEvent(wheelEvent); + + // give it time to scroll + await new Promise((r) => setTimeout(r, 500)); + + const newScroll = await SpecialPowers.spawn( + browser, + [], + () => content.document.documentElement.scrollTop + ); + info('Initial scroll:', initialScroll); + info('New scroll:', newScroll); + ok( + newScroll > initialScroll, + 'Content should scroll when wheel event is dispatched to the edge trigger' + ); + + BrowserTestUtils.removeTab(window.gBrowser.selectedTab); +}); diff --git a/src/zen/tests/moz.build b/src/zen/tests/moz.build index a68349d9af..78d043d442 100644 --- a/src/zen/tests/moz.build +++ b/src/zen/tests/moz.build @@ -5,6 +5,7 @@ BROWSER_CHROME_MANIFESTS += [ "compact_mode/browser.toml", "container_essentials/browser.toml", + "edgescroll/browser.toml", "glance/browser.toml", "pinned/browser.toml", "urlbar/browser.toml",