Bug 1358712 - Get rid of synchronous layout flushes when calculating where to put the StatusPanel. r?felipe
MozReview-Commit-ID: KHagFdaRAzF
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7818,68 +7818,148 @@ function duplicateTabIn(aTab, where, del
break;
}
}
var MousePosTracker = {
_listeners: new Set(),
_x: 0,
_y: 0,
+ _mostRecentEvent: null,
+
get _windowUtils() {
delete this._windowUtils;
return this._windowUtils = window.getInterface(Ci.nsIDOMWindowUtils);
},
+ /**
+ * Registers a listener, and then waits for the next refresh
+ * driver tick before running the listener to see if the
+ * mouse is within the listener's target rect.
+ *
+ * @param listener (object)
+ * A listener is expected to expose the following properties:
+ *
+ * getMouseTargetRect (function)
+ * Returns the rect that the MousePosTracker needs to alert
+ * the listener about if the mouse happens to be within it.
+ *
+ * onTrackingStarted (function, optional)
+ * Called after the next refresh driver tick after listening,
+ * when the mouse's initial position relative to the MouseTargetRect
+ * can be computed. If the listener is removed before the refresh
+ * driver tick, this might never be called.
+ *
+ * onMouseEnter (function, optional)
+ * The function to be called if the mouse enters the rect
+ * returned by getMouseTargetRect. MousePosTracker always
+ * runs this inside of a requestAnimationFrame, since it
+ * assumes that the notification is used to update the DOM.
+ *
+ * onMouseLeave (function, optional)
+ * The function to be called if the mouse exits the rect
+ * returned by getMouseTargetRect. MousePosTracker always
+ * runs this inside of a requestAnimationFrame, since it
+ * assumes that the notification is used to update the DOM.
+ */
addListener(listener) {
- if (this._listeners.has(listener))
+ if (this._listeners.has(listener)) {
return;
+ }
listener._hover = false;
this._listeners.add(listener);
- this._callListener(listener);
+ // We're adding some asynchronicity here, during which the listener
+ // might be removed. At each step, we need to ensure that the listener
+ // is still registered before proceeding.
+ window.promiseDocumentFlushed(() => {
+ if (this._listeners.has(listener)) {
+ this._callListeners([listener]);
+ window.requestAnimationFrame(() => {
+ if (this._listeners.has(listener) && listener.onTrackingStarted) {
+ listener.onTrackingStarted();
+ }
+ });
+ }
+ });
},
removeListener(listener) {
this._listeners.delete(listener);
},
handleEvent(event) {
- var fullZoom = this._windowUtils.fullZoom;
- this._x = event.screenX / fullZoom - window.mozInnerScreenX;
- this._y = event.screenY / fullZoom - window.mozInnerScreenY;
-
- this._listeners.forEach(function(listener) {
+ let firstEvent = !this._mostRecentEvent;
+ this._mostRecentEvent = event;
+
+ if (firstEvent) {
+ window.promiseDocumentFlushed(() => {
+ this.onDocumentFlushed();
+ this._mostRecentEvent = null;
+ });
+ }
+ },
+
+ onDocumentFlushed() {
+ let event = this._mostRecentEvent;
+
+ if (event) {
+ let fullZoom = this._windowUtils.fullZoom;
+ this._x = event.screenX / fullZoom - window.mozInnerScreenX;
+ this._y = event.screenY / fullZoom - window.mozInnerScreenY;
+
+ this._callListeners(this._listeners);
+ }
+ },
+
+ _callListeners(listeners) {
+ let functionsToCall = [];
+ for (let listener of listeners) {
+ let rect;
try {
- this._callListener(listener);
+ rect = listener.getMouseTargetRect();
} catch (e) {
Cu.reportError(e);
+ continue;
}
- }, this);
- },
-
- _callListener(listener) {
- let rect = listener.getMouseTargetRect();
- let hover = this._x >= rect.left &&
- this._x <= rect.right &&
- this._y >= rect.top &&
- this._y <= rect.bottom;
-
- if (hover == listener._hover)
- return;
-
- listener._hover = hover;
-
- if (hover) {
- if (listener.onMouseEnter)
- listener.onMouseEnter();
- } else if (listener.onMouseLeave) {
- listener.onMouseLeave();
- }
- }
+
+ let hover = this._x >= rect.left &&
+ this._x <= rect.right &&
+ this._y >= rect.top &&
+ this._y <= rect.bottom;
+
+ if (hover == listener._hover) {
+ continue;
+ }
+
+ listener._hover = hover;
+ if (hover) {
+ if (listener.onMouseEnter) {
+ functionsToCall.push(listener.onMouseEnter.bind(listener));
+ }
+ } else if (listener.onMouseLeave) {
+ functionsToCall.push(listener.onMouseLeave.bind(listener));
+ }
+ }
+
+ // _callListeners is being called from within a promiseDocumentFlushed,
+ // where we are expressly forbidden from dirtying styles or layout. Since
+ // the onMouseEnter or onMouseLeave functions are liable to do such
+ // dirtying, we run them inside a requestAnimationFrame callback instead.
+ window.requestAnimationFrame(() => {
+ for (let fn of functionsToCall) {
+ try {
+ fn();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ });
+ },
};
var ToolbarIconColor = {
_windowState: {
"active": false,
"fullscreen": false,
"tabsintitlebar": false
},
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -4637,28 +4637,34 @@ var StatusPanel = {
window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.getBoundsWithoutFlushing(this.panel).width + "px";
} else {
this.panel.style.minWidth = "";
}
if (val) {
+ this._mouseTargetRect = null;
this._labelElement.value = val;
- this.panel.removeAttribute("inactive");
- this._mouseTargetRect = null;
MousePosTracker.addListener(this);
+ // The inactive state for the panel will be removed in onTrackingStarted,
+ // once the initial position of the mouse relative to the StatusPanel
+ // is figured out (to avoid both flicker and sync flushing).
} else {
this.panel.setAttribute("inactive", "true");
MousePosTracker.removeListener(this);
}
return val;
},
+ onTrackingStarted() {
+ this.panel.removeAttribute("inactive");
+ },
+
getMouseTargetRect() {
if (!this._mouseTargetRect) {
this._calcMouseTargetRect();
}
return this._mouseTargetRect;
},
onMouseEnter() {