1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 ChromeUtils.defineESModuleGetters(lazy, {
9 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
12 export class AutoScrollChild extends JSWindowActorChild {
16 this._scrollable = null;
22 this._lastFrame = null;
23 this._autoscrollHandledByApz = false;
24 this._scrollId = null;
26 this.observer = new AutoScrollObserver(this);
27 this.autoscrollLoop = this.autoscrollLoop.bind(this);
30 isAutoscrollBlocker(event) {
31 let mmPaste = Services.prefs.getBoolPref("middlemouse.paste");
32 let mmScrollbarPosition = Services.prefs.getBoolPref(
33 "middlemouse.scrollbarPosition"
35 let node = event.originalTarget;
36 let content = node.ownerGlobal;
38 // If the node is in editable document or content, we don't want to start
41 if (node.ownerDocument?.designMode == "on") {
45 node.nodeType === content.Node.ELEMENT_NODE ? node : node.parentElement;
46 if (element.isContentEditable) {
51 // Don't start if we're on a link.
52 let [href] = lazy.BrowserUtils.hrefAndLinkNodeForClickEvent(event);
57 // Or if we're pasting into an input field of sorts.
58 let closestInput = mmPaste && node.closest("input,textarea");
60 content.HTMLInputElement.isInstance(closestInput) ||
61 content.HTMLTextAreaElement.isInstance(closestInput)
66 // Or if we're on a scrollbar or XUL <tree>
68 (mmScrollbarPosition &&
69 content.XULElement.isInstance(
70 node.closest("scrollbar,scrollcorner")
72 content.XULElement.isInstance(node.closest("treechildren"))
79 isScrollableElement(aNode) {
80 let content = aNode.ownerGlobal;
81 if (content.HTMLElement.isInstance(aNode)) {
82 return !content.HTMLSelectElement.isInstance(aNode) || aNode.multiple;
85 return content.XULElement.isInstance(aNode);
88 computeWindowScrollDirection(global) {
89 if (!global.scrollbars.visible) {
92 if (global.scrollMaxX != global.scrollMinX) {
93 return global.scrollMaxY != global.scrollMinY ? "NSEW" : "EW";
95 if (global.scrollMaxY != global.scrollMinY) {
101 computeNodeScrollDirection(node) {
102 if (!this.isScrollableElement(node)) {
106 let global = node.ownerGlobal;
108 // this is a list of overflow property values that allow scrolling
109 const scrollingAllowed = ["scroll", "auto"];
111 let cs = global.getComputedStyle(node);
112 let overflowx = cs.getPropertyValue("overflow-x");
113 let overflowy = cs.getPropertyValue("overflow-y");
114 // we already discarded non-multiline selects so allow vertical
115 // scroll for multiline ones directly without checking for a
119 (global.HTMLSelectElement.isInstance(node) ||
120 scrollingAllowed.includes(overflowy));
122 // do not allow horizontal scrolling for select elements, it leads
123 // to visual artifacts and is not the expected behavior anyway
125 !global.HTMLSelectElement.isInstance(node) &&
126 node.scrollLeftMin != node.scrollLeftMax &&
127 scrollingAllowed.includes(overflowx)
129 return scrollVert ? "NSEW" : "EW";
139 findNearestScrollableElement(aNode) {
140 // go upward in the DOM and find any parent element that has a overflow
141 // area and can therefore be scrolled
142 this._scrollable = null;
143 for (let node = aNode; node; node = node.flattenedTreeParentNode) {
144 // do not use overflow based autoscroll for <html> and <body>
145 // Elements or non-html/non-xul elements such as svg or Document nodes
146 // also make sure to skip select elements that are not multiline
147 let direction = this.computeNodeScrollDirection(node);
149 this._scrolldir = direction;
150 this._scrollable = node;
155 if (!this._scrollable) {
156 let direction = this.computeWindowScrollDirection(aNode.ownerGlobal);
158 this._scrolldir = direction;
159 this._scrollable = aNode.ownerGlobal;
160 } else if (aNode.ownerGlobal.frameElement) {
161 // Note, in case of out of process iframes frameElement is null, and
162 // a caller is supposed to communicate to iframe's parent on its own to
163 // support cross process scrolling.
164 this.findNearestScrollableElement(aNode.ownerGlobal.frameElement);
169 async startScroll(event) {
170 this.findNearestScrollableElement(event.originalTarget);
171 if (!this._scrollable) {
172 this.sendAsyncMessage("Autoscroll:MaybeStartInParent", {
173 browsingContextId: this.browsingContext.id,
174 screenX: event.screenX,
175 screenY: event.screenY,
180 let content = event.originalTarget.ownerGlobal;
182 // In some configurations like Print Preview, content.performance
183 // (which we use below) is null. Autoscrolling is broken in Print
184 // Preview anyways (see bug 1393494), so just don't start it at all.
185 if (!content.performance) {
189 let domUtils = content.windowUtils;
190 let scrollable = this._scrollable;
191 if (scrollable instanceof Ci.nsIDOMWindow) {
192 // getViewId() needs an element to operate on.
193 scrollable = scrollable.document.documentElement;
195 this._scrollId = null;
197 this._scrollId = domUtils.getViewId(scrollable);
199 // No view ID - leave this._scrollId as null. Receiving side will check.
201 let presShellId = domUtils.getPresShellId();
202 let { autoscrollEnabled, usingApz } = await this.sendQuery(
205 scrolldir: this._scrolldir,
206 screenXDevPx: event.screenX * content.devicePixelRatio,
207 screenYDevPx: event.screenY * content.devicePixelRatio,
208 scrollId: this._scrollId,
210 browsingContext: this.browsingContext,
213 if (!autoscrollEnabled) {
214 this._scrollable = null;
218 Services.els.addSystemEventListener(this.document, "mousemove", this, true);
219 Services.els.addSystemEventListener(this.document, "mouseup", this, true);
220 this.document.addEventListener("pagehide", this, true);
222 this._startX = event.screenX;
223 this._startY = event.screenY;
224 this._screenX = event.screenX;
225 this._screenY = event.screenY;
226 this._scrollErrorX = 0;
227 this._scrollErrorY = 0;
228 this._autoscrollHandledByApz = usingApz;
231 // If the browser didn't hand the autoscroll off to APZ,
232 // scroll here in the main thread.
233 this.startMainThreadScroll();
235 // Even if the browser did hand the autoscroll to APZ,
236 // APZ might reject it in which case it will notify us
237 // and we need to take over.
238 Services.obs.addObserver(this.observer, "autoscroll-rejected-by-apz");
241 if (Cu.isInAutomation) {
242 Services.obs.notifyObservers(content, "autoscroll-start");
246 startMainThreadScroll() {
247 let content = this.document.defaultView;
248 this._lastFrame = content.performance.now();
249 content.requestAnimationFrame(this.autoscrollLoop);
253 if (this._scrollable) {
254 this._scrollable.mozScrollSnap();
255 this._scrollable = null;
257 Services.els.removeSystemEventListener(
263 Services.els.removeSystemEventListener(
269 this.document.removeEventListener("pagehide", this, true);
270 if (this._autoscrollHandledByApz) {
271 Services.obs.removeObserver(
273 "autoscroll-rejected-by-apz"
279 accelerate(curr, start) {
281 var val = (curr - start) / speed;
284 return val * Math.sqrt(val) - 1;
287 return val * Math.sqrt(-val) + 1;
294 return Math.floor(num);
296 return Math.ceil(num);
299 autoscrollLoop(timestamp) {
300 if (!this._scrollable) {
301 // Scrolling has been canceled
305 // avoid long jumps when the browser hangs for more than
307 const maxTimeDelta = 100;
308 var timeDelta = Math.min(maxTimeDelta, timestamp - this._lastFrame);
309 // we used to scroll |accelerate()| pixels every 20ms (50fps)
310 var timeCompensation = timeDelta / 20;
311 this._lastFrame = timestamp;
313 var actualScrollX = 0;
314 var actualScrollY = 0;
315 // don't bother scrolling vertically when the scrolldir is only horizontal
316 // and the other way around
317 if (this._scrolldir != "EW") {
318 var y = this.accelerate(this._screenY, this._startY) * timeCompensation;
319 var desiredScrollY = this._scrollErrorY + y;
320 actualScrollY = this.roundToZero(desiredScrollY);
321 this._scrollErrorY = desiredScrollY - actualScrollY;
323 if (this._scrolldir != "NS") {
324 var x = this.accelerate(this._screenX, this._startX) * timeCompensation;
325 var desiredScrollX = this._scrollErrorX + x;
326 actualScrollX = this.roundToZero(desiredScrollX);
327 this._scrollErrorX = desiredScrollX - actualScrollX;
330 this._scrollable.scrollBy({
336 this._scrollable.ownerGlobal.requestAnimationFrame(this.autoscrollLoop);
339 canStartAutoScrollWith(event) {
342 event.defaultPrevented ||
343 event.button !== 1 ||
344 event.clickEventPrevented()
349 for (const modifier of ["shift", "alt", "ctrl", "meta"]) {
351 event[modifier + "Key"] &&
352 Services.prefs.getBoolPref(
353 `general.autoscroll.prevent_to_start.${modifier}Key`,
364 switch (event.type) {
366 this._screenX = event.screenX;
367 this._screenY = event.screenY;
371 this.canStartAutoScrollWith(event) &&
373 !this.isAutoscrollBlocker(event)
375 this.startScroll(event);
381 Services.prefs.getBoolPref("general.autoscroll", false)
383 // Middle mouse click event shouldn't be fired in web content for
384 // compatibility with Chrome.
385 event.preventClickEvent();
389 if (this._scrollable) {
390 var doc = this._scrollable.ownerDocument || this._scrollable.document;
391 if (doc == event.target) {
392 this.sendAsyncMessage("Autoscroll:Cancel");
400 receiveMessage(msg) {
403 case "Autoscroll:MaybeStart":
404 for (let child of this.browsingContext.children) {
405 if (data.browsingContextId == child.id) {
407 screenX: data.screenX,
408 screenY: data.screenY,
409 originalTarget: child.embedderElement,
415 case "Autoscroll:Stop": {
422 rejectedByApz(data) {
423 // The caller passes in the scroll id via 'data'.
424 if (data == this._scrollId) {
425 this._autoscrollHandledByApz = false;
426 this.startMainThreadScroll();
427 Services.obs.removeObserver(this.observer, "autoscroll-rejected-by-apz");
432 class AutoScrollObserver {
437 observe(subject, topic, data) {
438 if (topic === "autoscroll-rejected-by-apz") {
439 this.actor.rejectedByApz(data);