diff options
Diffstat (limited to 'gfx/layers/layerviewer')
-rw-r--r-- | gfx/layers/layerviewer/hide.png | bin | 0 -> 3079 bytes | |||
-rw-r--r-- | gfx/layers/layerviewer/index.html | 51 | ||||
-rw-r--r-- | gfx/layers/layerviewer/layerTreeView.js | 972 | ||||
-rw-r--r-- | gfx/layers/layerviewer/noise.png | bin | 0 -> 2118 bytes | |||
-rw-r--r-- | gfx/layers/layerviewer/show.png | bin | 0 -> 3187 bytes | |||
-rw-r--r-- | gfx/layers/layerviewer/tree.css | 39 |
6 files changed, 1062 insertions, 0 deletions
diff --git a/gfx/layers/layerviewer/hide.png b/gfx/layers/layerviewer/hide.png Binary files differnew file mode 100644 index 0000000000..9a92e2c1b1 --- /dev/null +++ b/gfx/layers/layerviewer/hide.png diff --git a/gfx/layers/layerviewer/index.html b/gfx/layers/layerviewer/index.html new file mode 100644 index 0000000000..000e40b8bd --- /dev/null +++ b/gfx/layers/layerviewer/index.html @@ -0,0 +1,51 @@ +<!-- 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/. --> + +<!DOCTYPE html> +<html> + <head> + <title>GFX Display List & Layer Visualizer</title> + <meta charset="utf-8"> + <link rel="stylesheet" type="text/css" href="tree.css"> + <script src="layerTreeView.js"></script> + <style> + +.csstooltip +{ + z-index: 5; + background: white; + border: solid 1px black; + position: absolute; + padding: 5px; + margin: 5px; + max-width: 300px; +} + </style> + </head> + <body> + <h1>GFX Layers dump visualizer:</h1> + Paste your display list or layers dump into this textarea:<br> + <textarea id="input_layers_dump" style="width:100%; height: 80%;" cols="80" rows="10"> +ClientLayerManager (0x1264f5000) + ClientContainerLayer (0x1263fe200) [visible=< (x=0, y=0, w=1457, h=1163); >] [opaqueContent] [metrics0={ [cb=(x=0.000000, y=0.000000, w=1457.000000, h=1163.000000)] [sr=(x=0.000000, y=0.000000, w=1457.000000, h=1163.000000)] [s=(0,0)] [dp=(x=0.000000, y=0.000000, w=1457.000000, h=1163.000000)] [cdp=(x=0.000000, y=0.000000, w=0.000000, h=0.000000)] [color=rgba(0, 0, 0, 0.000000)] [scrollId=3] [z=1] }] + ClientTiledPaintedLayer (0x1263b3600) [bounds=(x=-1, y=0, w=1458, h=1163)] [visible=< (x=0, y=0, w=1457, h=79); >] { hitregion=< (x=0, y=0, w=1457, h=47); (x=-1, y=47, w=1458, h=24); (x=0, y=71, w=1457, h=1092); > dispatchtocontentregion=< (x=68, y=9, w=1375, h=31); (x=944, y=47, w=280, h=24); >} [opaqueContent] [valid=< (x=0, y=0, w=1457, h=79); >] + SingleTiledContentClient (0x126f80680) + ClientContainerLayer (0x122a33f00) [clip=(x=0, y=79, w=1457, h=1084)] [visible=< (x=0, y=79, w=1457, h=1084); >] [opaqueContent] + ClientTiledPaintedLayer (0x11e11a700) [bounds=(x=0, y=79, w=1457, h=1084)] [visible=< (x=0, y=79, w=1457, h=1084); >] { hitregion=< (x=0, y=79, w=1457, h=1084); > dispatchtocontentregion=< (x=0, y=125, w=1457, h=1034); >} [opaqueContent] [valid=< (x=0, y=79, w=1457, h=1084); >] + SingleTiledContentClient (0x1226d52c0) + </textarea> + <br> + <input type="button" value="Process pasted log" onclick="log_pasted()" /> + <br> + <br> + Help: To get a layers dump go to about:config and set layout.display-list.dump;true or layers.dump;true. + <script> +function log_pasted() { + var container = parseMultiLineDump(document.getElementById("input_layers_dump").value); + document.body.innerHTML = ""; + document.body.appendChild(container); +} + </script> + </body> +</html> diff --git a/gfx/layers/layerviewer/layerTreeView.js b/gfx/layers/layerviewer/layerTreeView.js new file mode 100644 index 0000000000..5cf21c71b4 --- /dev/null +++ b/gfx/layers/layerviewer/layerTreeView.js @@ -0,0 +1,972 @@ +/* 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/. */ + +function toFixed(num, fixed) { + fixed = fixed || 0; + fixed = Math.pow(10, fixed); + return Math.floor(num * fixed) / fixed; +} +function createElement(name, props) { + var el = document.createElement(name); + + for (var key in props) { + if (key === "style") { + for (var styleName in props.style) { + el.style[styleName] = props.style[styleName]; + } + } else { + el[key] = props[key]; + } + } + + return el; +} + +function parseDisplayList(lines) { + var root = { + line: "DisplayListRoot 0", + name: "DisplayListRoot", + address: "0x0", + frame: "Root", + children: [], + }; + + var objectAtIndentation = { + "-1": root, + }; + + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + + var layerObject = { + line, + children: [], + }; + if (!root) { + root = layerObject; + } + + var matches = line.match( + "(\\s*)(\\w+)\\sp=(\\w+)\\sf=(.*?)\\((.*?)\\)\\s(z=(\\w+)\\s)?(.*?)?( layer=(\\w+))?$" + ); + if (!matches) { + dump("Failed to match: " + line + "\n"); + continue; + } + + var indentation = Math.floor(matches[1].length / 2); + objectAtIndentation[indentation] = layerObject; + var parent = objectAtIndentation[indentation - 1]; + if (parent) { + parent.children.push(layerObject); + } + + layerObject.name = matches[2]; + layerObject.address = matches[3]; // Use 0x prefix to be consistent with layer dump + layerObject.frame = matches[4]; + layerObject.contentDescriptor = matches[5]; + layerObject.z = matches[7]; + var rest = matches[8]; + if (matches[10]) { + // WrapList don't provide a layer + layerObject.layer = matches[10]; + } + layerObject.rest = rest; + + // the content node name doesn't have a prefix, this makes the parsing easier + rest = "content" + rest; + + var nesting = 0; + var startIndex; + var lastSpace = -1; + for (var j = 0; j < rest.length; j++) { + if (rest.charAt(j) == "(") { + nesting++; + if (nesting == 1) { + startIndex = j; + } + } else if (rest.charAt(j) == ")") { + nesting--; + if (nesting == 0) { + var name = rest.substring(lastSpace + 1, startIndex); + var value = rest.substring(startIndex + 1, j); + + var rectMatches = value.match("^(.*?),(.*?),(.*?),(.*?)$"); + if (rectMatches) { + layerObject[name] = [ + parseFloat(rectMatches[1]), + parseFloat(rectMatches[2]), + parseFloat(rectMatches[3]), + parseFloat(rectMatches[4]), + ]; + } else { + layerObject[name] = value; + } + } + } else if (nesting == 0 && rest.charAt(j) == " ") { + lastSpace = j; + } + } + // dump("FIELDS: " + JSON.stringify(fields) + "\n"); + } + return root; +} + +function trim(s) { + return (s || "").replace(/^\s+|\s+$/g, ""); +} + +function getDataURI(str) { + if (str.indexOf("data:image/png;base64,") == 0) { + return str; + } + + var matches = str.match( + "data:image/lz4bgra;base64,([0-9]+),([0-9]+),([0-9]+),(.*)" + ); + if (!matches) { + return null; + } + + var canvas = document.createElement("canvas"); + var w = parseInt(matches[1]); + var stride = parseInt(matches[2]); + var h = parseInt(matches[3]); + canvas.width = w; + canvas.height = h; + + // TODO handle stride + + var binary_string = window.atob(matches[4]); + var len = binary_string.length; + var bytes = new Uint8Array(len); + var decoded = new Uint8Array(stride * h); + for (var i = 0; i < len; i++) { + var ascii = binary_string.charCodeAt(i); + bytes[i] = ascii; + } + + var ctxt = canvas.getContext("2d"); + var out = ctxt.createImageData(w, h); + // This is actually undefined throughout the tree and it isn't clear what it + // should be. Since this is only development code, leave it alone for now. + // eslint-disable-next-line no-undef + LZ4_uncompressChunk(bytes, decoded); + + for (var x = 0; x < w; x++) { + for (var y = 0; y < h; y++) { + out.data[4 * x + 4 * y * w + 0] = decoded[4 * x + y * stride + 2]; + out.data[4 * x + 4 * y * w + 1] = decoded[4 * x + y * stride + 1]; + out.data[4 * x + 4 * y * w + 2] = decoded[4 * x + y * stride + 0]; + out.data[4 * x + 4 * y * w + 3] = decoded[4 * x + y * stride + 3]; + } + } + + ctxt.putImageData(out, 0, 0); + return canvas.toDataURL(); +} + +function parseLayers(layersDumpLines) { + function parseMatrix2x3(str) { + str = trim(str); + + // Something like '[ 1 0; 0 1; 0 158; ]' + var matches = str.match("^\\[ (.*?) (.*?); (.*?) (.*?); (.*?) (.*?); \\]$"); + if (!matches) { + return null; + } + + var matrix = [ + [parseFloat(matches[1]), parseFloat(matches[2])], + [parseFloat(matches[3]), parseFloat(matches[4])], + [parseFloat(matches[5]), parseFloat(matches[6])], + ]; + + return matrix; + } + function parseColor(str) { + str = trim(str); + + // Something like 'rgba(0, 0, 0, 0)' + var colorMatches = str.match("^rgba\\((.*), (.*), (.*), (.*)\\)$"); + if (!colorMatches) { + return null; + } + + var color = { + r: colorMatches[1], + g: colorMatches[2], + b: colorMatches[3], + a: colorMatches[4], + }; + return color; + } + function parseFloat_cleo(str) { + str = trim(str); + + // Something like 2.000 + if (parseFloat(str) == str) { + return parseFloat(str); + } + + return null; + } + function parseRect2D(str) { + str = trim(str); + + // Something like '(x=0, y=0, w=2842, h=158)' + var rectMatches = str.match("^\\(x=(.*?), y=(.*?), w=(.*?), h=(.*?)\\)$"); + if (!rectMatches) { + return null; + } + + var rect = [ + parseFloat(rectMatches[1]), + parseFloat(rectMatches[2]), + parseFloat(rectMatches[3]), + parseFloat(rectMatches[4]), + ]; + return rect; + } + function parseRegion(str) { + str = trim(str); + + // Something like '< (x=0, y=0, w=2842, h=158); (x=0, y=1718, w=2842, h=500); >' + if (str.charAt(0) != "<" || str.charAt(str.length - 1) != ">") { + return null; + } + + var region = []; + str = trim(str.substring(1, str.length - 1)); + while (str != "") { + var rectMatches = str.match( + "^\\(x=(.*?), y=(.*?), w=(.*?), h=(.*?)\\);(.*)$" + ); + if (!rectMatches) { + return null; + } + + var rect = [ + parseFloat(rectMatches[1]), + parseFloat(rectMatches[2]), + parseFloat(rectMatches[3]), + parseFloat(rectMatches[4]), + ]; + str = trim(rectMatches[5]); + region.push(rect); + } + return region; + } + + var LAYERS_LINE_REGEX = "(\\s*)(\\w+)\\s\\((\\w+)\\)(.*)"; + + var root; + var objectAtIndentation = []; + for (var i = 0; i < layersDumpLines.length; i++) { + // Something like 'ThebesLayerComposite (0x12104cc00) [shadow-visible=< (x=0, y=0, w=1920, h=158); >] [visible=< (x=0, y=0, w=1920, h=158); >] [opaqueContent] [valid=< (x=0, y=0, w=1920, h=2218); >]' + var line = layersDumpLines[i].name || layersDumpLines[i]; + + var tileMatches = line.match("(\\s*)Tile \\(x=(.*), y=(.*)\\): (.*)"); + if (tileMatches) { + let indentation = Math.floor(matches[1].length / 2); + var x = tileMatches[2]; + var y = tileMatches[3]; + var dataUri = tileMatches[4]; + let parent = objectAtIndentation[indentation - 1]; + var tiles = parent.tiles || {}; + + tiles[x] = tiles[x] || {}; + tiles[x][y] = dataUri; + + parent.tiles = tiles; + + continue; + } + + var surfaceMatches = line.match("(\\s*)Surface: (.*)"); + if (surfaceMatches) { + let indentation = Math.floor(matches[1].length / 2); + let parent = + objectAtIndentation[indentation - 1] || + objectAtIndentation[indentation - 2]; + + var surfaceURI = surfaceMatches[2]; + if (parent.surfaceURI != null) { + console.log( + "error: surfaceURI already set for this layer " + parent.line + ); + } + parent.surfaceURI = surfaceURI; + + // Look for the buffer-rect offset + var contentHostLine = + layersDumpLines[i - 2].name || layersDumpLines[i - 2]; + let matches = contentHostLine.match(LAYERS_LINE_REGEX); + if (matches) { + var contentHostRest = matches[4]; + parent.contentHostProp = {}; + parseProperties(contentHostRest, parent.contentHostProp); + } + + continue; + } + + var layerObject = { + line, + children: [], + }; + if (!root) { + root = layerObject; + } + + let matches = line.match(LAYERS_LINE_REGEX); + if (!matches) { + continue; // Something like a texturehost dump. Safe to ignore + } + + if ( + matches[2].includes("TiledContentHost") || + matches[2].includes("ContentHost") || + matches[2].includes("ContentClient") || + matches[2].includes("MemoryTextureHost") || + matches[2].includes("ImageHost") + ) { + continue; // We're already pretty good at visualizing these + } + + var indentation = Math.floor(matches[1].length / 2); + objectAtIndentation[indentation] = layerObject; + for (var c = indentation + 1; c < objectAtIndentation.length; c++) { + objectAtIndentation[c] = null; + } + if (indentation > 0) { + var parent = objectAtIndentation[indentation - 1]; + while (!parent) { + indentation--; + parent = objectAtIndentation[indentation - 1]; + } + + parent.children.push(layerObject); + } + + layerObject.name = matches[2]; + layerObject.address = matches[3]; + + var rest = matches[4]; + + function parseProperties(rest, layerObject) { + var fields = []; + var nesting = 0; + var startIndex; + for (let j = 0; j < rest.length; j++) { + if (rest.charAt(j) == "[") { + nesting++; + if (nesting == 1) { + startIndex = j; + } + } else if (rest.charAt(j) == "]") { + nesting--; + if (nesting == 0) { + fields.push(rest.substring(startIndex + 1, j)); + } + } + } + + for (let j = 0; j < fields.length; j++) { + // Something like 'valid=< (x=0, y=0, w=1920, h=2218); >' or 'opaqueContent' + var field = fields[j]; + // dump("FIELD: " + field + "\n"); + var parts = field.split("=", 2); + var fieldName = parts[0]; + rest = field.substring(fieldName.length + 1); + if (parts.length == 1) { + layerObject[fieldName] = "true"; + layerObject[fieldName].type = "bool"; + continue; + } + var float = parseFloat_cleo(rest); + if (float) { + layerObject[fieldName] = float; + layerObject[fieldName].type = "float"; + continue; + } + var region = parseRegion(rest); + if (region) { + layerObject[fieldName] = region; + layerObject[fieldName].type = "region"; + continue; + } + var rect = parseRect2D(rest); + if (rect) { + layerObject[fieldName] = rect; + layerObject[fieldName].type = "rect2d"; + continue; + } + var matrix = parseMatrix2x3(rest); + if (matrix) { + layerObject[fieldName] = matrix; + layerObject[fieldName].type = "matrix2x3"; + continue; + } + var color = parseColor(rest); + if (color) { + layerObject[fieldName] = color; + layerObject[fieldName].type = "color"; + continue; + } + if (rest[0] == "{" && rest[rest.length - 1] == "}") { + var object = {}; + parseProperties(rest.substring(1, rest.length - 2).trim(), object); + layerObject[fieldName] = object; + layerObject[fieldName].type = "object"; + continue; + } + fieldName = fieldName.split(" ")[0]; + layerObject[fieldName] = rest[0]; + layerObject[fieldName].type = "string"; + } + } + parseProperties(rest, layerObject); + + if (!layerObject["shadow-transform"]) { + // No shadow transform = identify + layerObject["shadow-transform"] = [ + [1, 0], + [0, 1], + [0, 0], + ]; + } + + // Compute screenTransformX/screenTransformY + // TODO Fully support transforms + if (layerObject["shadow-transform"] && layerObject.transform) { + layerObject["screen-transform"] = [ + layerObject["shadow-transform"][2][0], + layerObject["shadow-transform"][2][1], + ]; + var currIndentation = indentation - 1; + while (currIndentation >= 0) { + var transform = + objectAtIndentation[currIndentation]["shadow-transform"] || + objectAtIndentation[currIndentation].transform; + if (transform) { + layerObject["screen-transform"][0] += transform[2][0]; + layerObject["screen-transform"][1] += transform[2][1]; + } + currIndentation--; + } + } + + // dump("Fields: " + JSON.stringify(fields) + "\n"); + } + root.compositeTime = layersDumpLines.compositeTime; + // dump("OBJECTS: " + JSON.stringify(root) + "\n"); + return root; +} +function populateLayers( + root, + displayList, + pane, + previewParent, + hasSeenRoot, + contentScale, + rootPreviewParent +) { + contentScale = contentScale || 1; + rootPreviewParent = rootPreviewParent || previewParent; + + function getDisplayItemForLayer(displayList) { + var items = []; + if (!displayList) { + return items; + } + if (displayList.layer == root.address) { + items.push(displayList); + } + for (var i = 0; i < displayList.children.length; i++) { + var subDisplayItems = getDisplayItemForLayer(displayList.children[i]); + for (let j = 0; j < subDisplayItems.length; j++) { + items.push(subDisplayItems[j]); + } + } + return items; + } + var elem = createElement("div", { + className: "layerObjectDescription", + textContent: root.line, + style: { + whiteSpace: "pre", + }, + onmouseover() { + if (this.layerViewport) { + this.layerViewport.classList.add("layerHover"); + } + }, + onmouseout() { + if (this.layerViewport) { + this.layerViewport.classList.remove("layerHover"); + } + }, + }); + var icon = createElement("img", { + src: "show.png", + style: { + width: "12px", + height: "12px", + marginLeft: "4px", + marginRight: "4px", + cursor: "pointer", + }, + onclick() { + if (this.layerViewport) { + if (this.layerViewport.style.visibility == "hidden") { + this.layerViewport.style.visibility = ""; + this.src = "show.png"; + } else { + this.layerViewport.style.visibility = "hidden"; + this.src = "hide.png"; + } + } + }, + }); + elem.insertBefore(icon, elem.firstChild); + pane.appendChild(elem); + + if (root["shadow-visible"] || root.visible) { + var visibleRegion = root["shadow-visible"] || root.visible; + var layerViewport = createElement("div", { + id: root.address + "_viewport", + style: { + position: "absolute", + pointerEvents: "none", + }, + }); + elem.layerViewport = layerViewport; + icon.layerViewport = layerViewport; + var layerViewportMatrix = [1, 0, 0, 1, 0, 0]; + if (root["shadow-clip"] || root.clip) { + var clip = root["shadow-clip"] || root.clip; + var clipElem = createElement("div", { + id: root.address + "_clip", + style: { + left: clip[0] + "px", + top: clip[1] + "px", + width: clip[2] + "px", + height: clip[3] + "px", + position: "absolute", + overflow: "hidden", + pointerEvents: "none", + }, + }); + layerViewportMatrix[4] += -clip[0]; + layerViewportMatrix[5] += -clip[1]; + layerViewport.style.transform = + "translate(-" + clip[0] + "px, -" + clip[1] + "px)"; + } + if (root["shadow-transform"] || root.transform) { + var matrix = root["shadow-transform"] || root.transform; + layerViewportMatrix[0] = matrix[0][0]; + layerViewportMatrix[1] = matrix[0][1]; + layerViewportMatrix[2] = matrix[1][0]; + layerViewportMatrix[3] = matrix[1][1]; + layerViewportMatrix[4] += matrix[2][0]; + layerViewportMatrix[5] += matrix[2][1]; + } + layerViewport.style.transform = + "matrix(" + + layerViewportMatrix[0] + + "," + + layerViewportMatrix[1] + + "," + + layerViewportMatrix[2] + + "," + + layerViewportMatrix[3] + + "," + + layerViewportMatrix[4] + + "," + + layerViewportMatrix[5] + + ")"; + if (!hasSeenRoot) { + hasSeenRoot = true; + layerViewport.style.transform = + "scale(" + 1 / contentScale + "," + 1 / contentScale + ")"; + } + if (clipElem) { + previewParent.appendChild(clipElem); + clipElem.appendChild(layerViewport); + } else { + previewParent.appendChild(layerViewport); + } + previewParent = layerViewport; + for (let i = 0; i < visibleRegion.length; i++) { + let rect2d = visibleRegion[i]; + var layerPreview = createElement("div", { + id: root.address + "_visible_part" + i + "-" + visibleRegion.length, + className: "layerPreview", + style: { + position: "absolute", + left: rect2d[0] + "px", + top: rect2d[1] + "px", + width: rect2d[2] + "px", + height: rect2d[3] + "px", + overflow: "hidden", + border: "solid 1px black", + background: + 'url("noise.png"), linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.2))', + }, + }); + layerViewport.appendChild(layerPreview); + + function isInside(rect1, rect2) { + if ( + rect1[0] + rect1[2] < rect2[0] && + rect2[0] + rect2[2] < rect1[0] && + rect1[1] + rect1[3] < rect2[1] && + rect2[1] + rect2[3] < rect1[1] + ) { + return true; + } + return true; + } + + var hasImg = false; + // Add tile img objects for this part + var previewOffset = rect2d; + + if (root.tiles) { + hasImg = true; + for (var x in root.tiles) { + for (var y in root.tiles[x]) { + if (isInside(rect2d, [x, y, 512, 512])) { + var tileImgElem = createElement("img", { + src: getDataURI(root.tiles[x][y]), + style: { + position: "absolute", + left: x - previewOffset[0] + "px", + top: y - previewOffset[1] + "px", + pointerEvents: "auto", + }, + }); + layerPreview.appendChild(tileImgElem); + } + } + } + layerPreview.style.background = ""; + } else if (root.surfaceURI) { + hasImg = true; + var offsetX = 0; + var offsetY = 0; + if (root.contentHostProp && root.contentHostProp["buffer-rect"]) { + offsetX = root.contentHostProp["buffer-rect"][0]; + offsetY = root.contentHostProp["buffer-rect"][1]; + } + var surfaceImgElem = createElement("img", { + src: getDataURI(root.surfaceURI), + style: { + position: "absolute", + left: offsetX - previewOffset[0] + "px", + top: offsetY - previewOffset[1] + "px", + pointerEvents: "auto", + }, + }); + layerPreview.appendChild(surfaceImgElem); + layerPreview.style.background = ""; + } else if (root.color) { + hasImg = true; + layerPreview.style.background = + "rgba(" + + root.color.r + + ", " + + root.color.g + + ", " + + root.color.b + + ", " + + root.color.a + + ")"; + } + + if (hasImg || true) { + layerPreview.mouseoverElem = elem; + layerPreview.onmouseenter = function() { + this.mouseoverElem.onmouseover(); + }; + layerPreview.onmouseout = function() { + this.mouseoverElem.onmouseout(); + }; + } + } + + var layerDisplayItems = getDisplayItemForLayer(displayList); + for (let i = 0; i < layerDisplayItems.length; i++) { + var displayItem = layerDisplayItems[i]; + var displayElem = createElement("div", { + className: "layerObjectDescription", + textContent: " " + trim(displayItem.line), + style: { + whiteSpace: "pre", + }, + displayItem, + layerViewport, + onmouseover() { + if (this.diPreview) { + this.diPreview.classList.add("displayHover"); + + var description = ""; + if (this.displayItem.contentDescriptor) { + description += "Content: " + this.displayItem.contentDescriptor; + } else { + description += "Content: Unknown"; + } + description += + "<br>Item: " + + this.displayItem.name + + " (" + + this.displayItem.address + + ")"; + description += + "<br>Layer: " + root.name + " (" + root.address + ")"; + if (this.displayItem.frame) { + description += "<br>Frame: " + this.displayItem.frame; + } + if (this.displayItem.layerBounds) { + description += + "<br>Bounds: [" + + toFixed(this.displayItem.layerBounds[0] / 60, 2) + + ", " + + toFixed(this.displayItem.layerBounds[1] / 60, 2) + + ", " + + toFixed(this.displayItem.layerBounds[2] / 60, 2) + + ", " + + toFixed(this.displayItem.layerBounds[3] / 60, 2) + + "] (CSS Pixels)"; + } + if (this.displayItem.z) { + description += "<br>Z: " + this.displayItem.z; + } + // At the end + if (this.displayItem.rest) { + description += "<br>" + this.displayItem.rest; + } + + var box = this.diPreview.getBoundingClientRect(); + this.diPreview.tooltip = createElement("div", { + className: "csstooltip", + innerHTML: description, + style: { + top: + Math.min( + box.bottom, + document.documentElement.clientHeight - 150 + ) + "px", + left: box.left + "px", + }, + }); + + document.body.appendChild(this.diPreview.tooltip); + } + }, + onmouseout() { + if (this.diPreview) { + this.diPreview.classList.remove("displayHover"); + document.body.removeChild(this.diPreview.tooltip); + } + }, + }); + + icon = createElement("img", { + style: { + width: "12px", + height: "12px", + marginLeft: "4px", + marginRight: "4px", + }, + }); + displayElem.insertBefore(icon, displayElem.firstChild); + pane.appendChild(displayElem); + // bounds doesn't adjust for within the layer. It's not a bad fallback but + // will have the wrong offset + let rect2d = displayItem.layerBounds || displayItem.bounds; + if (rect2d) { + // This doesn't place them corectly + var appUnitsToPixels = 60 / contentScale; + let diPreview = createElement("div", { + id: "displayitem_" + displayItem.content + "_" + displayItem.address, + className: "layerPreview", + style: { + position: "absolute", + left: rect2d[0] / appUnitsToPixels + "px", + top: rect2d[1] / appUnitsToPixels + "px", + width: rect2d[2] / appUnitsToPixels + "px", + height: rect2d[3] / appUnitsToPixels + "px", + border: "solid 1px gray", + pointerEvents: "auto", + }, + displayElem, + onmouseover() { + this.displayElem.onmouseover(); + }, + onmouseout() { + this.displayElem.onmouseout(); + }, + }); + + layerViewport.appendChild(diPreview); + displayElem.diPreview = diPreview; + } + } + } + + for (var i = 0; i < root.children.length; i++) { + populateLayers( + root.children[i], + displayList, + pane, + previewParent, + hasSeenRoot, + contentScale, + rootPreviewParent + ); + } +} + +// This function takes a stdout snippet and finds the frames +function parseMultiLineDump(log) { + var container = createElement("div", { + style: { + height: "100%", + position: "relative", + }, + }); + + var layerManagerFirstLine = "[a-zA-Z]*LayerManager \\(.*$\n"; + var nextLineStartWithSpace = "([ \\t].*$\n)*"; + var layersRegex = "(" + layerManagerFirstLine + nextLineStartWithSpace + ")"; + + var startLine = "Painting --- after optimization:\n"; + var endLine = "Painting --- layer tree:"; + var displayListRegex = "(" + startLine + "(.*\n)*?" + endLine + ")"; + + var regex = new RegExp(layersRegex + "|" + displayListRegex, "gm"); + var matches = log.match(regex); + console.log(matches); + window.matches = matches; + + var matchList = createElement("span", { + style: { + height: "95%", + width: "10%", + position: "relative", + border: "solid black 2px", + display: "inline-block", + float: "left", + overflow: "auto", + }, + }); + container.appendChild(matchList); + var contents = createElement("span", { + style: { + height: "95%", + width: "88%", + display: "inline-block", + }, + textContent: "Click on a frame on the left to view the layer tree", + }); + container.appendChild(contents); + + var lastDisplayList = null; + var frameID = 1; + for (let i = 0; i < matches.length; i++) { + var currMatch = matches[i]; + + if (currMatch.indexOf(startLine) == 0) { + // Display list match + var matchLines = matches[i].split("\n"); + lastDisplayList = parseDisplayList(matchLines); + } else { + // Layer tree match: + let displayList = lastDisplayList; + lastDisplayList = null; + var currFrameDiv = createElement("a", { + style: { + padding: "3px", + display: "block", + }, + href: "#", + textContent: "LayerTree " + frameID++, + onclick() { + contents.innerHTML = ""; + var matchLines = matches[i].split("\n"); + var dumpDiv = parseDump(matchLines, displayList); + contents.appendChild(dumpDiv); + }, + }); + matchList.appendChild(currFrameDiv); + } + } + + return container; +} + +function parseDump(log, displayList, compositeTitle, compositeTime) { + compositeTitle |= ""; + compositeTime |= 0; + + var container = createElement("div", { + style: { + background: "white", + height: "100%", + position: "relative", + }, + }); + + if (compositeTitle == null && compositeTime == null) { + var titleDiv = createElement("div", { + className: "treeColumnHeader", + style: { + width: "100%", + }, + textContent: + compositeTitle + + (compositeTitle ? " (near " + compositeTime.toFixed(0) + " ms)" : ""), + }); + container.appendChild(titleDiv); + } + + var mainDiv = createElement("div", { + style: { + position: "absolute", + top: "16px", + left: "0px", + right: "0px", + bottom: "0px", + }, + }); + container.appendChild(mainDiv); + + var layerListPane = createElement("div", { + style: { + cssFloat: "left", + height: "100%", + width: "300px", + overflowY: "scroll", + }, + }); + mainDiv.appendChild(layerListPane); + + var previewDiv = createElement("div", { + style: { + position: "absolute", + left: "300px", + right: "0px", + top: "0px", + bottom: "0px", + overflow: "auto", + }, + }); + mainDiv.appendChild(previewDiv); + + var root = parseLayers(log); + populateLayers(root, displayList, layerListPane, previewDiv); + return container; +} diff --git a/gfx/layers/layerviewer/noise.png b/gfx/layers/layerviewer/noise.png Binary files differnew file mode 100644 index 0000000000..01d340aaa9 --- /dev/null +++ b/gfx/layers/layerviewer/noise.png diff --git a/gfx/layers/layerviewer/show.png b/gfx/layers/layerviewer/show.png Binary files differnew file mode 100644 index 0000000000..7038b660c8 --- /dev/null +++ b/gfx/layers/layerviewer/show.png diff --git a/gfx/layers/layerviewer/tree.css b/gfx/layers/layerviewer/tree.css new file mode 100644 index 0000000000..18e5881c00 --- /dev/null +++ b/gfx/layers/layerviewer/tree.css @@ -0,0 +1,39 @@ +/* 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/. */ + +html, body { + height: 100%; + overflow: hidden; +} +.layerObjectDescription:hover { + background-color: #E8E8E8; +} + +.layerHover > .layerPreview::after { + position: absolute; + top: 0; right: 0; bottom: 0; left: 0; + background-color: inherit; + content: ""; + background-color: rgba(0,0,0,0.2); + box-shadow: -2px 2px 0 #FFF; +} + +@keyframes layerHoverAnimation { + 0% { transform: scale(1); } + 50% { transform: scale(1.2); } + 100% { transform: scale(1); } +} + +.displayHover { + background: rgba(0, 128, 0, 0.8); +} + +.layerHover > .layerPreview { + animation: layerHoverAnimation 200ms; + animation-transform-origin: 50% 50%; + background: gold !important; + box-shadow: 10px 10px 5px #888888; + border-color: blue !important; + z-index: 10; +} |