Bug 1349275 - refactored moveInfobar function; r=pbro

- Added `getViewportDimensions`
- Added `getComputedStylePropertyValue` to `CanvasFrameAnonymousContentHelper`
- Refactored totally `moveInfobar` to works with both APZ enabled and new
  positioned absolutely highlighters
- Updated `AutoRefreshHighlighter` for having a `scrollUpdate` method.
- Updated tests

MozReview-Commit-ID: 5m31ZzRzLXr

--HG--
extra : rebase_source : c5abac64217ee0b86413594461fb6a50d5df655e
This commit is contained in:
Matteo Ferretti 2017-03-28 12:40:22 +02:00
parent c4f1f8e321
commit c2450fbc4d
8 changed files with 198 additions and 94 deletions

View File

@ -12,8 +12,8 @@ const TEST_URL = "data:text/html;charset=utf-8,<div>zoom me</div>";
// TEST_LEVELS entries should contain the zoom level to test.
const TEST_LEVELS = [2, 1, .5];
// Returns the expected style attribute value to check for on the root highlighter
// element, for the values given.
// Returns the expected style attribute value to check for on the highlighter's elements
// node, for the values given.
const expectedStyle = (w, h, z) =>
(z !== 1 ? `transform-origin:top left; transform:scale(${1 / z}); ` : "") +
`position:absolute; width:${w * z}px;height:${h * z}px; ` +
@ -40,7 +40,7 @@ add_task(function* () {
info("Check that the highlighter root wrapper node was scaled down");
let style = yield getRootNodeStyle(testActor);
let style = yield getElementsNodeStyle(testActor);
let { width, height } = yield testActor.getWindowDimensions();
is(style, expectedStyle(width, height, level),
"The style attribute of the root element is correct");
@ -60,8 +60,8 @@ function* hoverContainer(container, inspector) {
yield onHighlight;
}
function* getRootNodeStyle(testActor) {
function* getElementsNodeStyle(testActor) {
let value = yield testActor.getHighlighterNodeAttribute(
"box-model-root", "style");
"box-model-elements", "style");
return value;
}

View File

@ -18,14 +18,16 @@ add_task(function* () {
tag: "div",
id: "top",
classes: ".class1.class2",
dims: "500" + " \u00D7 " + "100"
dims: "500" + " \u00D7 " + "100",
arrowed: true
},
{
selector: "#vertical",
position: "overlap",
position: "top",
tag: "div",
id: "vertical",
classes: ""
classes: "",
arrowed: false
// No dims as they will vary between computers
},
{
@ -34,13 +36,15 @@ add_task(function* () {
tag: "div",
id: "bottom",
classes: "",
dims: "500" + " \u00D7 " + "100"
dims: "500" + " \u00D7 " + "100",
arrowed: true
},
{
selector: "body",
position: "bottom",
tag: "body",
classes: ""
classes: "",
arrowed: true
// No dims as they will vary between computers
},
{
@ -48,7 +52,8 @@ add_task(function* () {
position: "bottom",
tag: "clipPath",
id: "clip",
classes: ""
classes: "",
arrowed: false
// No dims as element is not displayed and we just want to test tag name
},
];
@ -81,6 +86,11 @@ function* testPosition(test, inspector, testActor) {
"box-model-infobar-classes");
is(classes, test.classes, "node " + test.selector + ": classes match.");
let arrowed = !(yield testActor.getHighlighterNodeAttribute(
"box-model-infobar-container", "hide-arrow"));
is(arrowed, test.arrowed, "node " + test.selector + ": arrow visibility match.");
if (test.dims) {
let dims = yield testActor.getHighlighterNodeTextContent(
"box-model-infobar-dimensions");

View File

@ -14,7 +14,7 @@ add_task(function* () {
let testData = {
selector: "body",
position: "overlap",
style: "top:0px",
style: "position:fixed",
};
yield testPositionAndStyle(testData, inspector, testActor);
@ -28,7 +28,7 @@ function* testPositionAndStyle(test, inspector, testActor) {
let style = yield testActor.getHighlighterNodeAttribute(
"box-model-infobar-container", "style");
is(style.split(";")[0], test.style,
is(style.split(";")[0].trim(), test.style,
"Infobar shows on top of the page when page isn't scrolled");
yield testActor.scrollWindow(0, 500);
@ -36,6 +36,6 @@ function* testPositionAndStyle(test, inspector, testActor) {
style = yield testActor.getHighlighterNodeAttribute(
"box-model-infobar-container", "style");
is(style.split(";")[0], test.style,
is(style.split(";")[0].trim(), test.style,
"Infobar shows on top of the page even if the page is scrolled");
}

View File

@ -33,6 +33,7 @@
--highlighter-bubble-text-color: hsl(216, 33%, 97%);
--highlighter-bubble-background-color: hsl(214, 13%, 24%);
--highlighter-bubble-border-color: rgba(255, 255, 255, 0.2);
--highlighter-bubble-arrow-size: 8px;
}
/**
@ -141,15 +142,11 @@
border: 1px solid var(--highlighter-bubble-border-color);
}
:-moz-native-anonymous [class$=infobar-container][hide-arrow] > [class$=infobar] {
margin: 7px 0;
}
/* Arrows */
:-moz-native-anonymous [class$=infobar-container] > [class$=infobar]:before {
left: calc(50% - 8px);
border: 8px solid var(--highlighter-bubble-border-color);
left: calc(50% - var(--highlighter-bubble-arrow-size));
border: var(--highlighter-bubble-arrow-size) solid var(--highlighter-bubble-border-color);
}
:-moz-native-anonymous [class$=infobar-container] > [class$=infobar]:after {

View File

@ -72,6 +72,7 @@ function AutoRefreshHighlighter(highlighterEnv) {
this.currentQuads = {};
this._winDimensions = getWindowDimensions(this.win);
this._scroll = { x: this.win.pageXOffset, y: this.win.pageYOffset };
this.update = this.update.bind(this);
}
@ -192,6 +193,21 @@ AutoRefreshHighlighter.prototype = {
return areQuadsDifferent(oldQuads, this.currentQuads, getCurrentZoom(this.win));
},
/**
* Update the knowledge we have of the current window's scrolling offset, both
* horizontal and vertical, and return `true` if they have changed since.
* @return {Boolean}
*/
_hasWindowScrolled: function () {
let { pageXOffset, pageYOffset } = this.win;
let hasChanged = this._scroll.x !== pageXOffset ||
this._scroll.y !== pageYOffset;
this._scroll = { x: pageXOffset, y: pageYOffset };
return hasChanged;
},
/**
* Update the knowledge we have of the current window's dimensions and return `true`
* if they have changed since.
@ -212,6 +228,12 @@ AutoRefreshHighlighter.prototype = {
update: function () {
if (!this._isNodeValid(this.currentNode) ||
(!this._hasMoved() && !this._haveWindowDimensionsChanged())) {
// At this point we're not calling the `_update` method. However, if the window has
// scrolled, we want to invoke `_scrollUpdate`.
if (this._hasWindowScrolled()) {
this._scrollUpdate();
}
return;
}
@ -230,10 +252,17 @@ AutoRefreshHighlighter.prototype = {
// To be implemented by sub classes
// When called, sub classes should update the highlighter shown for
// this.currentNode
// This is called as a result of a page scroll, zoom or repaint
// This is called as a result of a page zoom or repaint
throw new Error("Custom highlighter class had to implement _update method");
},
_scrollUpdate: function () {
// Can be implemented by sub classes
// When called, sub classes can upate the highlighter shown for
// this.currentNode
// This is called as a result of a page scroll
},
_hide: function () {
// To be implemented by sub classes
// When called, sub classes should actually hide the highlighter

View File

@ -352,6 +352,10 @@ BoxModelHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
return shown;
},
_scrollUpdate: function () {
this._moveInfobar();
},
/**
* Hide the highlighter, the outline and the infobar.
*/
@ -508,7 +512,7 @@ BoxModelHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
}
// Un-zoom the root wrapper if the page was zoomed.
let rootId = this.ID_CLASS_PREFIX + "root";
let rootId = this.ID_CLASS_PREFIX + "elements";
this.markup.scaleRootElement(this.currentNode, rootId);
return true;

View File

@ -5,7 +5,7 @@
"use strict";
const { Cc, Ci, Cu } = require("chrome");
const { getCurrentZoom, getWindowDimensions,
const { getCurrentZoom, getWindowDimensions, getViewportDimensions,
getRootBindingParent } = require("devtools/shared/layout/utils");
const { on, emit } = require("sdk/event/core");
@ -38,10 +38,6 @@ const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const STYLESHEET_URI = "resource://devtools/server/actors/" +
"highlighters.css";
// How high is the infobar (px).
const INFOBAR_HEIGHT = 34;
// What's the size of the infobar arrow (px).
const INFOBAR_ARROW_SIZE = 9;
const _tokens = Symbol("classList/tokens");
@ -249,10 +245,11 @@ function CanvasFrameAnonymousContentHelper(highlighterEnv, nodeBuilder) {
this.highlighterEnv.on("window-ready", this._onWindowReady);
this.listeners = new Map();
this.elements = new Map();
}
CanvasFrameAnonymousContentHelper.prototype = {
destroy: function () {
destroy() {
this._remove();
this.highlighterEnv.off("window-ready", this._onWindowReady);
this.highlighterEnv = this.nodeBuilder = this._content = null;
@ -260,9 +257,10 @@ CanvasFrameAnonymousContentHelper.prototype = {
this.anonymousContentGlobal = null;
this._removeAllListeners();
this.elements.clear();
},
_insert: function () {
_insert() {
let doc = this.highlighterEnv.document;
// Wait for DOMContentLoaded before injecting the anonymous content.
if (doc.readyState != "interactive" && doc.readyState != "complete") {
@ -317,53 +315,52 @@ CanvasFrameAnonymousContentHelper.prototype = {
* - when first attaching to a page
* - when swapping frame loaders (moving tabs, toggling RDM)
*/
_onWindowReady: function (e, {isTopLevel}) {
_onWindowReady(e, {isTopLevel}) {
if (isTopLevel) {
this._remove();
this._removeAllListeners();
this.elements.clear();
this._insert();
this.anonymousContentDocument = this.highlighterEnv.document;
}
},
getTextContentForElement: function (id) {
if (!this.content) {
return null;
}
return this.content.getTextContentForElement(id);
getComputedStylePropertyValue(id, property) {
return this.content && this.content.getComputedStylePropertyValue(id, property);
},
setTextContentForElement: function (id, text) {
getTextContentForElement(id) {
return this.content && this.content.getTextContentForElement(id);
},
setTextContentForElement(id, text) {
if (this.content) {
this.content.setTextContentForElement(id, text);
}
},
setAttributeForElement: function (id, name, value) {
setAttributeForElement(id, name, value) {
if (this.content) {
this.content.setAttributeForElement(id, name, value);
}
},
getAttributeForElement: function (id, name) {
if (!this.content) {
return null;
}
return this.content.getAttributeForElement(id, name);
getAttributeForElement(id, name) {
return this.content && this.content.getAttributeForElement(id, name);
},
removeAttributeForElement: function (id, name) {
removeAttributeForElement(id, name) {
if (this.content) {
this.content.removeAttributeForElement(id, name);
}
},
hasAttributeForElement: function (id, name) {
hasAttributeForElement(id, name) {
return typeof this.getAttributeForElement(id, name) === "string";
},
getCanvasContext: function (id, type = "2d") {
return this.content ? this.content.getCanvasContext(id, type) : null;
getCanvasContext(id, type = "2d") {
return this.content && this.content.getCanvasContext(id, type);
},
/**
@ -402,7 +399,7 @@ CanvasFrameAnonymousContentHelper.prototype = {
* @param {String} type
* @param {Function} handler
*/
addEventListenerForElement: function (id, type, handler) {
addEventListenerForElement(id, type, handler) {
if (typeof id !== "string") {
throw new Error("Expected a string ID in addEventListenerForElement but" +
" got: " + id);
@ -426,7 +423,7 @@ CanvasFrameAnonymousContentHelper.prototype = {
* @param {String} id
* @param {String} type
*/
removeEventListenerForElement: function (id, type) {
removeEventListenerForElement(id, type) {
let listeners = this.listeners.get(type);
if (!listeners) {
return;
@ -440,7 +437,7 @@ CanvasFrameAnonymousContentHelper.prototype = {
}
},
handleEvent: function (event) {
handleEvent(event) {
let listeners = this.listeners.get(event.type);
if (!listeners) {
return;
@ -477,7 +474,7 @@ CanvasFrameAnonymousContentHelper.prototype = {
}
},
_removeAllListeners: function () {
_removeAllListeners() {
if (this.highlighterEnv && this.highlighterEnv.pageListenerTarget) {
let target = this.highlighterEnv.pageListenerTarget;
for (let [type] of this.listeners) {
@ -487,14 +484,18 @@ CanvasFrameAnonymousContentHelper.prototype = {
this.listeners.clear();
},
getElement: function (id) {
getElement(id) {
if (this.elements.has(id)) {
return this.elements.get(id);
}
let classList = new ClassList(this.getAttributeForElement(id, "class"));
on(classList, "update", () => {
this.setAttributeForElement(id, "class", classList.toString());
});
return {
let element = {
getTextContent: () => this.getTextContentForElement(id),
setTextContent: text => this.setTextContentForElement(id, text),
setAttribute: (name, val) => this.setAttributeForElement(id, name, val),
@ -508,8 +509,15 @@ CanvasFrameAnonymousContentHelper.prototype = {
removeEventListener: (type, handler) => {
return this.removeEventListenerForElement(id, type, handler);
},
computedStyle: {
getPropertyValue: property => this.getComputedStylePropertyValue(id, property)
},
classList
};
this.elements.set(id, element);
return element;
},
get content() {
@ -540,7 +548,7 @@ CanvasFrameAnonymousContentHelper.prototype = {
* should be used to read the current zoom value.
* @param {String} id The ID of the root element inserted with this API.
*/
scaleRootElement: function (node, id) {
scaleRootElement(node, id) {
let boundaryWindow = this.highlighterEnv.window;
let zoom = getCurrentZoom(node);
// Hide the root element and force the reflow in order to get the proper window's
@ -582,55 +590,91 @@ exports.CanvasFrameAnonymousContentHelper = CanvasFrameAnonymousContentHelper;
* The window object.
*/
function moveInfobar(container, bounds, win) {
let winHeight = win.innerHeight * getCurrentZoom(win);
let winWidth = win.innerWidth * getCurrentZoom(win);
let winScrollY = win.scrollY;
let zoom = getCurrentZoom(win);
let viewport = getViewportDimensions(win);
// Ensure that containerBottom and containerTop are at least zero to avoid
// showing tooltips outside the viewport.
let containerBottom = Math.max(0, bounds.bottom) + INFOBAR_ARROW_SIZE;
let containerTop = Math.min(winHeight, bounds.top);
let { computedStyle } = container;
// Can the bar be above the node?
let top;
if (containerTop < INFOBAR_HEIGHT) {
// No. Can we move the bar under the node?
if (containerBottom + INFOBAR_HEIGHT > winHeight) {
// No. Let's move it inside. Can we show it at the top of the element?
if (containerTop < winScrollY) {
// No. Window is scrolled past the top of the element.
top = 0;
} else {
// Yes. Show it at the top of the element
top = containerTop;
}
container.setAttribute("position", "overlap");
} else {
// Yes. Let's move it under the node.
top = containerBottom;
container.setAttribute("position", "bottom");
}
} else {
// Yes. Let's move it on top of the node.
top = containerTop - INFOBAR_HEIGHT;
container.setAttribute("position", "top");
// To simplify, we use the same arrow's size value as margin's value for all four sides.
let margin = parseFloat(computedStyle
.getPropertyValue("--highlighter-bubble-arrow-size"));
let containerHeight = parseFloat(computedStyle.getPropertyValue("height"));
let containerWidth = parseFloat(computedStyle.getPropertyValue("width"));
let containerHalfWidth = containerWidth / 2;
let viewportWidth = viewport.width * zoom;
let viewportHeight = viewport.height * zoom;
let { pageXOffset, pageYOffset } = win;
pageYOffset *= zoom;
pageXOffset *= zoom;
containerHeight += margin;
// Defines the boundaries for the infobar.
let topBoundary = margin;
let bottomBoundary = viewportHeight - containerHeight;
let leftBoundary = containerHalfWidth + margin;
let rightBoundary = viewportWidth - containerHalfWidth - margin;
// Set the default values.
let top = bounds.y - containerHeight;
let bottom = bounds.bottom + margin;
let left = bounds.x + bounds.width / 2;
let isOverlapTheNode = false;
let positionAttribute = "top";
let position = "absolute";
// Here we start the math.
// We basically want to position absolutely the infobar, except when is pointing to a
// node that is offscreen or partially offscreen, in a way that the infobar can't
// be placed neither on top nor on bottom.
// In such cases, the infobar will overlap the node, and to limit the latency given
// by APZ (See Bug 1312103) it will be positioned as "fixed".
// It's a sort of "position: sticky" (but positioned as absolute instead of relative).
let canBePlacedOnTop = top >= pageYOffset;
let canBePlacedOnBottom = bottomBoundary + pageYOffset - bottom > 0;
if (!canBePlacedOnTop && canBePlacedOnBottom) {
top = bottom;
positionAttribute = "bottom";
}
// Align the bar with the box's center if possible.
let left = bounds.right - bounds.width / 2;
// Make sure the while infobar is visible.
let buffer = 100;
if (left < buffer) {
left = buffer;
container.setAttribute("hide-arrow", "true");
} else if (left > winWidth - buffer) {
left = winWidth - buffer;
let isOffscreenOnTop = top < topBoundary + pageYOffset;
let isOffscreenOnBottom = top > bottomBoundary + pageYOffset;
let isOffscreenOnLeft = left < leftBoundary + pageXOffset;
let isOffscreenOnRight = left > rightBoundary + pageXOffset;
if (isOffscreenOnTop) {
top = topBoundary;
isOverlapTheNode = true;
} else if (isOffscreenOnBottom) {
top = bottomBoundary;
isOverlapTheNode = true;
} else if (isOffscreenOnLeft || isOffscreenOnRight) {
isOverlapTheNode = true;
top -= pageYOffset;
}
if (isOverlapTheNode) {
left = Math.min(Math.max(leftBoundary, left - pageXOffset), rightBoundary);
position = "fixed";
container.setAttribute("hide-arrow", "true");
} else {
position = "absolute";
container.removeAttribute("hide-arrow");
}
let style = "top:" + top + "px;left:" + left + "px;";
container.setAttribute("style", style);
// We need to scale the infobar Independently from the highlighter's container;
// otherwise the `position: fixed` won't work, since "any value other than `none` for
// the transform, results in the creation of both a stacking context and a containing
// block. The object acts as a containing block for fixed positioned descendants."
// (See https://www.w3.org/TR/css-transforms-1/#transform-rendering)
container.setAttribute("style", `
position:${position};
transform-origin: 0 0;
transform: scale(${1 / zoom}) translate(${left}px, ${top}px)`);
container.setAttribute("position", positionAttribute);
}
exports.moveInfobar = moveInfobar;

View File

@ -676,6 +676,26 @@ function getWindowDimensions(window) {
}
exports.getWindowDimensions = getWindowDimensions;
/**
* Returns the viewport's dimensions for the `window` given.
*
* @return {Object} An object with `width` and `height` properties, representing the
* number of pixels for the viewport's size.
*/
function getViewportDimensions(window) {
let windowUtils = utilsFor(window);
let scrollbarHeight = {};
let scrollbarWidth = {};
windowUtils.getScrollbarSize(false, scrollbarWidth, scrollbarHeight);
let width = window.innerWidth - scrollbarWidth.value;
let height = window.innerHeight - scrollbarHeight.value;
return { width, height };
}
exports.getViewportDimensions = getViewportDimensions;
/**
* Returns the max size allowed for a surface like textures or canvas.
* If no `webgl` context is available, DEFAULT_MAX_SURFACE_SIZE is returned instead.