tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

layerTreeView.js (27938B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 function toFixed(num, fixed) {
      6  fixed = fixed || 0;
      7  fixed = Math.pow(10, fixed);
      8  return Math.floor(num * fixed) / fixed;
      9 }
     10 function createElement(name, props) {
     11  var el = document.createElement(name);
     12 
     13  for (var key in props) {
     14    if (key === "style") {
     15      for (var styleName in props.style) {
     16        el.style[styleName] = props.style[styleName];
     17      }
     18    } else {
     19      el[key] = props[key];
     20    }
     21  }
     22 
     23  return el;
     24 }
     25 
     26 function parseDisplayList(lines) {
     27  var root = {
     28    line: "DisplayListRoot 0",
     29    name: "DisplayListRoot",
     30    address: "0x0",
     31    frame: "Root",
     32    children: [],
     33  };
     34 
     35  var objectAtIndentation = {
     36    "-1": root,
     37  };
     38 
     39  for (var i = 0; i < lines.length; i++) {
     40    var line = lines[i];
     41 
     42    var layerObject = {
     43      line,
     44      children: [],
     45    };
     46    if (!root) {
     47      root = layerObject;
     48    }
     49 
     50    var matches = line.match(
     51      "(\\s*)(\\w+)\\sp=(\\w+)\\sf=(.*?)\\((.*?)\\)\\s(z=(\\w+)\\s)?(.*?)?( layer=(\\w+))?$"
     52    );
     53    if (!matches) {
     54      dump("Failed to match: " + line + "\n");
     55      continue;
     56    }
     57 
     58    var indentation = Math.floor(matches[1].length / 2);
     59    objectAtIndentation[indentation] = layerObject;
     60    var parent = objectAtIndentation[indentation - 1];
     61    if (parent) {
     62      parent.children.push(layerObject);
     63    }
     64 
     65    layerObject.name = matches[2];
     66    layerObject.address = matches[3]; // Use 0x prefix to be consistent with layer dump
     67    layerObject.frame = matches[4];
     68    layerObject.contentDescriptor = matches[5];
     69    layerObject.z = matches[7];
     70    var rest = matches[8];
     71    if (matches[10]) {
     72      // WrapList don't provide a layer
     73      layerObject.layer = matches[10];
     74    }
     75    layerObject.rest = rest;
     76 
     77    // the content node name doesn't have a prefix, this makes the parsing easier
     78    rest = "content" + rest;
     79 
     80    var nesting = 0;
     81    var startIndex;
     82    var lastSpace = -1;
     83    for (var j = 0; j < rest.length; j++) {
     84      if (rest.charAt(j) == "(") {
     85        nesting++;
     86        if (nesting == 1) {
     87          startIndex = j;
     88        }
     89      } else if (rest.charAt(j) == ")") {
     90        nesting--;
     91        if (nesting == 0) {
     92          var name = rest.substring(lastSpace + 1, startIndex);
     93          var value = rest.substring(startIndex + 1, j);
     94 
     95          var rectMatches = value.match("^(.*?),(.*?),(.*?),(.*?)$");
     96          if (rectMatches) {
     97            layerObject[name] = [
     98              parseFloat(rectMatches[1]),
     99              parseFloat(rectMatches[2]),
    100              parseFloat(rectMatches[3]),
    101              parseFloat(rectMatches[4]),
    102            ];
    103          } else {
    104            layerObject[name] = value;
    105          }
    106        }
    107      } else if (nesting == 0 && rest.charAt(j) == " ") {
    108        lastSpace = j;
    109      }
    110    }
    111    // dump("FIELDS: " + JSON.stringify(fields) + "\n");
    112  }
    113  return root;
    114 }
    115 
    116 function trim(s) {
    117  return (s || "").replace(/^\s+|\s+$/g, "");
    118 }
    119 
    120 function getDataURI(str) {
    121  if (str.indexOf("data:image/png;base64,") == 0) {
    122    return str;
    123  }
    124 
    125  var matches = str.match(
    126    "data:image/lz4bgra;base64,([0-9]+),([0-9]+),([0-9]+),(.*)"
    127  );
    128  if (!matches) {
    129    return null;
    130  }
    131 
    132  var canvas = document.createElement("canvas");
    133  var w = parseInt(matches[1]);
    134  var stride = parseInt(matches[2]);
    135  var h = parseInt(matches[3]);
    136  canvas.width = w;
    137  canvas.height = h;
    138 
    139  // TODO handle stride
    140 
    141  var binary_string = window.atob(matches[4]);
    142  var len = binary_string.length;
    143  var bytes = new Uint8Array(len);
    144  var decoded = new Uint8Array(stride * h);
    145  for (var i = 0; i < len; i++) {
    146    var ascii = binary_string.charCodeAt(i);
    147    bytes[i] = ascii;
    148  }
    149 
    150  var ctxt = canvas.getContext("2d");
    151  var out = ctxt.createImageData(w, h);
    152  // This is actually undefined throughout the tree and it isn't clear what it
    153  // should be. Since this is only development code, leave it alone for now.
    154  // eslint-disable-next-line no-undef
    155  LZ4_uncompressChunk(bytes, decoded);
    156 
    157  for (var x = 0; x < w; x++) {
    158    for (var y = 0; y < h; y++) {
    159      out.data[4 * x + 4 * y * w + 0] = decoded[4 * x + y * stride + 2];
    160      out.data[4 * x + 4 * y * w + 1] = decoded[4 * x + y * stride + 1];
    161      out.data[4 * x + 4 * y * w + 2] = decoded[4 * x + y * stride + 0];
    162      out.data[4 * x + 4 * y * w + 3] = decoded[4 * x + y * stride + 3];
    163    }
    164  }
    165 
    166  ctxt.putImageData(out, 0, 0);
    167  return canvas.toDataURL();
    168 }
    169 
    170 function parseLayers(layersDumpLines) {
    171  function parseMatrix2x3(str) {
    172    str = trim(str);
    173 
    174    // Something like '[ 1 0; 0 1; 0 158; ]'
    175    var matches = str.match("^\\[ (.*?) (.*?); (.*?) (.*?); (.*?) (.*?); \\]$");
    176    if (!matches) {
    177      return null;
    178    }
    179 
    180    var matrix = [
    181      [parseFloat(matches[1]), parseFloat(matches[2])],
    182      [parseFloat(matches[3]), parseFloat(matches[4])],
    183      [parseFloat(matches[5]), parseFloat(matches[6])],
    184    ];
    185 
    186    return matrix;
    187  }
    188  function parseColor(str) {
    189    str = trim(str);
    190 
    191    // Something like 'rgba(0, 0, 0, 0)'
    192    var colorMatches = str.match("^rgba\\((.*), (.*), (.*), (.*)\\)$");
    193    if (!colorMatches) {
    194      return null;
    195    }
    196 
    197    var color = {
    198      r: colorMatches[1],
    199      g: colorMatches[2],
    200      b: colorMatches[3],
    201      a: colorMatches[4],
    202    };
    203    return color;
    204  }
    205  function parseFloat_cleo(str) {
    206    str = trim(str);
    207 
    208    // Something like 2.000
    209    if (parseFloat(str) == str) {
    210      return parseFloat(str);
    211    }
    212 
    213    return null;
    214  }
    215  function parseRect2D(str) {
    216    str = trim(str);
    217 
    218    // Something like '(x=0, y=0, w=2842, h=158)'
    219    var rectMatches = str.match("^\\(x=(.*?), y=(.*?), w=(.*?), h=(.*?)\\)$");
    220    if (!rectMatches) {
    221      return null;
    222    }
    223 
    224    var rect = [
    225      parseFloat(rectMatches[1]),
    226      parseFloat(rectMatches[2]),
    227      parseFloat(rectMatches[3]),
    228      parseFloat(rectMatches[4]),
    229    ];
    230    return rect;
    231  }
    232  function parseRegion(str) {
    233    str = trim(str);
    234 
    235    // Something like '< (x=0, y=0, w=2842, h=158); (x=0, y=1718, w=2842, h=500); >'
    236    if (str.charAt(0) != "<" || str.charAt(str.length - 1) != ">") {
    237      return null;
    238    }
    239 
    240    var region = [];
    241    str = trim(str.substring(1, str.length - 1));
    242    while (str != "") {
    243      var rectMatches = str.match(
    244        "^\\(x=(.*?), y=(.*?), w=(.*?), h=(.*?)\\);(.*)$"
    245      );
    246      if (!rectMatches) {
    247        return null;
    248      }
    249 
    250      var rect = [
    251        parseFloat(rectMatches[1]),
    252        parseFloat(rectMatches[2]),
    253        parseFloat(rectMatches[3]),
    254        parseFloat(rectMatches[4]),
    255      ];
    256      str = trim(rectMatches[5]);
    257      region.push(rect);
    258    }
    259    return region;
    260  }
    261 
    262  var LAYERS_LINE_REGEX = "(\\s*)(\\w+)\\s\\((\\w+)\\)(.*)";
    263 
    264  var root;
    265  var objectAtIndentation = [];
    266  for (var i = 0; i < layersDumpLines.length; i++) {
    267    // 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); >]'
    268    var line = layersDumpLines[i].name || layersDumpLines[i];
    269 
    270    var tileMatches = line.match("(\\s*)Tile \\(x=(.*), y=(.*)\\): (.*)");
    271    if (tileMatches) {
    272      let indentation = Math.floor(matches[1].length / 2);
    273      var x = tileMatches[2];
    274      var y = tileMatches[3];
    275      var dataUri = tileMatches[4];
    276      let parent = objectAtIndentation[indentation - 1];
    277      var tiles = parent.tiles || {};
    278 
    279      tiles[x] = tiles[x] || {};
    280      tiles[x][y] = dataUri;
    281 
    282      parent.tiles = tiles;
    283 
    284      continue;
    285    }
    286 
    287    var surfaceMatches = line.match("(\\s*)Surface: (.*)");
    288    if (surfaceMatches) {
    289      let indentation = Math.floor(matches[1].length / 2);
    290      let parent =
    291        objectAtIndentation[indentation - 1] ||
    292        objectAtIndentation[indentation - 2];
    293 
    294      var surfaceURI = surfaceMatches[2];
    295      if (parent.surfaceURI != null) {
    296        console.log(
    297          "error: surfaceURI already set for this layer " + parent.line
    298        );
    299      }
    300      parent.surfaceURI = surfaceURI;
    301 
    302      // Look for the buffer-rect offset
    303      var contentHostLine =
    304        layersDumpLines[i - 2].name || layersDumpLines[i - 2];
    305      let matches = contentHostLine.match(LAYERS_LINE_REGEX);
    306      if (matches) {
    307        var contentHostRest = matches[4];
    308        parent.contentHostProp = {};
    309        parseProperties(contentHostRest, parent.contentHostProp);
    310      }
    311 
    312      continue;
    313    }
    314 
    315    var layerObject = {
    316      line,
    317      children: [],
    318    };
    319    if (!root) {
    320      root = layerObject;
    321    }
    322 
    323    let matches = line.match(LAYERS_LINE_REGEX);
    324    if (!matches) {
    325      continue; // Something like a texturehost dump. Safe to ignore
    326    }
    327 
    328    if (
    329      matches[2].includes("TiledContentHost") ||
    330      matches[2].includes("ContentHost") ||
    331      matches[2].includes("ContentClient") ||
    332      matches[2].includes("MemoryTextureHost") ||
    333      matches[2].includes("ImageHost")
    334    ) {
    335      continue; // We're already pretty good at visualizing these
    336    }
    337 
    338    var indentation = Math.floor(matches[1].length / 2);
    339    objectAtIndentation[indentation] = layerObject;
    340    for (var c = indentation + 1; c < objectAtIndentation.length; c++) {
    341      objectAtIndentation[c] = null;
    342    }
    343    if (indentation > 0) {
    344      var parent = objectAtIndentation[indentation - 1];
    345      while (!parent) {
    346        indentation--;
    347        parent = objectAtIndentation[indentation - 1];
    348      }
    349 
    350      parent.children.push(layerObject);
    351    }
    352 
    353    layerObject.name = matches[2];
    354    layerObject.address = matches[3];
    355 
    356    var rest = matches[4];
    357 
    358    function parseProperties(rest, layerObject) {
    359      var fields = [];
    360      var nesting = 0;
    361      var startIndex;
    362      for (let j = 0; j < rest.length; j++) {
    363        if (rest.charAt(j) == "[") {
    364          nesting++;
    365          if (nesting == 1) {
    366            startIndex = j;
    367          }
    368        } else if (rest.charAt(j) == "]") {
    369          nesting--;
    370          if (nesting == 0) {
    371            fields.push(rest.substring(startIndex + 1, j));
    372          }
    373        }
    374      }
    375 
    376      for (let j = 0; j < fields.length; j++) {
    377        // Something like 'valid=< (x=0, y=0, w=1920, h=2218); >' or 'opaqueContent'
    378        var field = fields[j];
    379        // dump("FIELD: " + field + "\n");
    380        var parts = field.split("=", 2);
    381        var fieldName = parts[0];
    382        rest = field.substring(fieldName.length + 1);
    383        if (parts.length == 1) {
    384          layerObject[fieldName] = "true";
    385          layerObject[fieldName].type = "bool";
    386          continue;
    387        }
    388        var float = parseFloat_cleo(rest);
    389        if (float) {
    390          layerObject[fieldName] = float;
    391          layerObject[fieldName].type = "float";
    392          continue;
    393        }
    394        var region = parseRegion(rest);
    395        if (region) {
    396          layerObject[fieldName] = region;
    397          layerObject[fieldName].type = "region";
    398          continue;
    399        }
    400        var rect = parseRect2D(rest);
    401        if (rect) {
    402          layerObject[fieldName] = rect;
    403          layerObject[fieldName].type = "rect2d";
    404          continue;
    405        }
    406        var matrix = parseMatrix2x3(rest);
    407        if (matrix) {
    408          layerObject[fieldName] = matrix;
    409          layerObject[fieldName].type = "matrix2x3";
    410          continue;
    411        }
    412        var color = parseColor(rest);
    413        if (color) {
    414          layerObject[fieldName] = color;
    415          layerObject[fieldName].type = "color";
    416          continue;
    417        }
    418        if (rest[0] == "{" && rest[rest.length - 1] == "}") {
    419          var object = {};
    420          parseProperties(rest.substring(1, rest.length - 2).trim(), object);
    421          layerObject[fieldName] = object;
    422          layerObject[fieldName].type = "object";
    423          continue;
    424        }
    425        fieldName = fieldName.split(" ")[0];
    426        layerObject[fieldName] = rest[0];
    427        layerObject[fieldName].type = "string";
    428      }
    429    }
    430    parseProperties(rest, layerObject);
    431 
    432    if (!layerObject["shadow-transform"]) {
    433      // No shadow transform = identify
    434      layerObject["shadow-transform"] = [
    435        [1, 0],
    436        [0, 1],
    437        [0, 0],
    438      ];
    439    }
    440 
    441    // Compute screenTransformX/screenTransformY
    442    // TODO Fully support transforms
    443    if (layerObject["shadow-transform"] && layerObject.transform) {
    444      layerObject["screen-transform"] = [
    445        layerObject["shadow-transform"][2][0],
    446        layerObject["shadow-transform"][2][1],
    447      ];
    448      var currIndentation = indentation - 1;
    449      while (currIndentation >= 0) {
    450        var transform =
    451          objectAtIndentation[currIndentation]["shadow-transform"] ||
    452          objectAtIndentation[currIndentation].transform;
    453        if (transform) {
    454          layerObject["screen-transform"][0] += transform[2][0];
    455          layerObject["screen-transform"][1] += transform[2][1];
    456        }
    457        currIndentation--;
    458      }
    459    }
    460 
    461    // dump("Fields: " + JSON.stringify(fields) + "\n");
    462  }
    463  root.compositeTime = layersDumpLines.compositeTime;
    464  // dump("OBJECTS: " + JSON.stringify(root) + "\n");
    465  return root;
    466 }
    467 function populateLayers(
    468  root,
    469  displayList,
    470  pane,
    471  previewParent,
    472  hasSeenRoot,
    473  contentScale,
    474  rootPreviewParent
    475 ) {
    476  contentScale = contentScale || 1;
    477  rootPreviewParent = rootPreviewParent || previewParent;
    478 
    479  function getDisplayItemForLayer(displayList) {
    480    var items = [];
    481    if (!displayList) {
    482      return items;
    483    }
    484    if (displayList.layer == root.address) {
    485      items.push(displayList);
    486    }
    487    for (var i = 0; i < displayList.children.length; i++) {
    488      var subDisplayItems = getDisplayItemForLayer(displayList.children[i]);
    489      for (let j = 0; j < subDisplayItems.length; j++) {
    490        items.push(subDisplayItems[j]);
    491      }
    492    }
    493    return items;
    494  }
    495  var elem = createElement("div", {
    496    className: "layerObjectDescription",
    497    textContent: root.line,
    498    style: {
    499      whiteSpace: "pre",
    500    },
    501    onmouseover() {
    502      if (this.layerViewport) {
    503        this.layerViewport.classList.add("layerHover");
    504      }
    505    },
    506    onmouseout() {
    507      if (this.layerViewport) {
    508        this.layerViewport.classList.remove("layerHover");
    509      }
    510    },
    511  });
    512  var icon = createElement("img", {
    513    src: "show.png",
    514    style: {
    515      width: "12px",
    516      height: "12px",
    517      marginLeft: "4px",
    518      marginRight: "4px",
    519      cursor: "pointer",
    520    },
    521    onclick() {
    522      if (this.layerViewport) {
    523        if (this.layerViewport.style.visibility == "hidden") {
    524          this.layerViewport.style.visibility = "";
    525          this.src = "show.png";
    526        } else {
    527          this.layerViewport.style.visibility = "hidden";
    528          this.src = "hide.png";
    529        }
    530      }
    531    },
    532  });
    533  elem.insertBefore(icon, elem.firstChild);
    534  pane.appendChild(elem);
    535 
    536  if (root["shadow-visible"] || root.visible) {
    537    var visibleRegion = root["shadow-visible"] || root.visible;
    538    var layerViewport = createElement("div", {
    539      id: root.address + "_viewport",
    540      style: {
    541        position: "absolute",
    542        pointerEvents: "none",
    543      },
    544    });
    545    elem.layerViewport = layerViewport;
    546    icon.layerViewport = layerViewport;
    547    var layerViewportMatrix = [1, 0, 0, 1, 0, 0];
    548    if (root["shadow-clip"] || root.clip) {
    549      var clip = root["shadow-clip"] || root.clip;
    550      var clipElem = createElement("div", {
    551        id: root.address + "_clip",
    552        style: {
    553          left: clip[0] + "px",
    554          top: clip[1] + "px",
    555          width: clip[2] + "px",
    556          height: clip[3] + "px",
    557          position: "absolute",
    558          overflow: "hidden",
    559          pointerEvents: "none",
    560        },
    561      });
    562      layerViewportMatrix[4] += -clip[0];
    563      layerViewportMatrix[5] += -clip[1];
    564      layerViewport.style.transform =
    565        "translate(-" + clip[0] + "px, -" + clip[1] + "px)";
    566    }
    567    if (root["shadow-transform"] || root.transform) {
    568      var matrix = root["shadow-transform"] || root.transform;
    569      layerViewportMatrix[0] = matrix[0][0];
    570      layerViewportMatrix[1] = matrix[0][1];
    571      layerViewportMatrix[2] = matrix[1][0];
    572      layerViewportMatrix[3] = matrix[1][1];
    573      layerViewportMatrix[4] += matrix[2][0];
    574      layerViewportMatrix[5] += matrix[2][1];
    575    }
    576    layerViewport.style.transform =
    577      "matrix(" +
    578      layerViewportMatrix[0] +
    579      "," +
    580      layerViewportMatrix[1] +
    581      "," +
    582      layerViewportMatrix[2] +
    583      "," +
    584      layerViewportMatrix[3] +
    585      "," +
    586      layerViewportMatrix[4] +
    587      "," +
    588      layerViewportMatrix[5] +
    589      ")";
    590    if (!hasSeenRoot) {
    591      hasSeenRoot = true;
    592      layerViewport.style.transform =
    593        "scale(" + 1 / contentScale + "," + 1 / contentScale + ")";
    594    }
    595    if (clipElem) {
    596      previewParent.appendChild(clipElem);
    597      clipElem.appendChild(layerViewport);
    598    } else {
    599      previewParent.appendChild(layerViewport);
    600    }
    601    previewParent = layerViewport;
    602    for (let i = 0; i < visibleRegion.length; i++) {
    603      let rect2d = visibleRegion[i];
    604      var layerPreview = createElement("div", {
    605        id: root.address + "_visible_part" + i + "-" + visibleRegion.length,
    606        className: "layerPreview",
    607        style: {
    608          position: "absolute",
    609          left: rect2d[0] + "px",
    610          top: rect2d[1] + "px",
    611          width: rect2d[2] + "px",
    612          height: rect2d[3] + "px",
    613          overflow: "hidden",
    614          border: "solid 1px black",
    615          background:
    616            'url("noise.png"), linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.2))',
    617        },
    618      });
    619      layerViewport.appendChild(layerPreview);
    620 
    621      function isInside(rect1, rect2) {
    622        if (
    623          rect1[0] + rect1[2] < rect2[0] &&
    624          rect2[0] + rect2[2] < rect1[0] &&
    625          rect1[1] + rect1[3] < rect2[1] &&
    626          rect2[1] + rect2[3] < rect1[1]
    627        ) {
    628          return true;
    629        }
    630        return true;
    631      }
    632 
    633      var hasImg = false;
    634      // Add tile img objects for this part
    635      var previewOffset = rect2d;
    636 
    637      if (root.tiles) {
    638        hasImg = true;
    639        for (var x in root.tiles) {
    640          for (var y in root.tiles[x]) {
    641            if (isInside(rect2d, [x, y, 512, 512])) {
    642              var tileImgElem = createElement("img", {
    643                src: getDataURI(root.tiles[x][y]),
    644                style: {
    645                  position: "absolute",
    646                  left: x - previewOffset[0] + "px",
    647                  top: y - previewOffset[1] + "px",
    648                  pointerEvents: "auto",
    649                },
    650              });
    651              layerPreview.appendChild(tileImgElem);
    652            }
    653          }
    654        }
    655        layerPreview.style.background = "";
    656      } else if (root.surfaceURI) {
    657        hasImg = true;
    658        var offsetX = 0;
    659        var offsetY = 0;
    660        if (root.contentHostProp && root.contentHostProp["buffer-rect"]) {
    661          offsetX = root.contentHostProp["buffer-rect"][0];
    662          offsetY = root.contentHostProp["buffer-rect"][1];
    663        }
    664        var surfaceImgElem = createElement("img", {
    665          src: getDataURI(root.surfaceURI),
    666          style: {
    667            position: "absolute",
    668            left: offsetX - previewOffset[0] + "px",
    669            top: offsetY - previewOffset[1] + "px",
    670            pointerEvents: "auto",
    671          },
    672        });
    673        layerPreview.appendChild(surfaceImgElem);
    674        layerPreview.style.background = "";
    675      } else if (root.color) {
    676        hasImg = true;
    677        layerPreview.style.background =
    678          "rgba(" +
    679          root.color.r +
    680          ", " +
    681          root.color.g +
    682          ", " +
    683          root.color.b +
    684          ", " +
    685          root.color.a +
    686          ")";
    687      }
    688 
    689      if (hasImg || true) {
    690        layerPreview.mouseoverElem = elem;
    691        layerPreview.onmouseenter = function () {
    692          this.mouseoverElem.onmouseover();
    693        };
    694        layerPreview.onmouseout = function () {
    695          this.mouseoverElem.onmouseout();
    696        };
    697      }
    698    }
    699 
    700    var layerDisplayItems = getDisplayItemForLayer(displayList);
    701    for (let i = 0; i < layerDisplayItems.length; i++) {
    702      var displayItem = layerDisplayItems[i];
    703      var displayElem = createElement("div", {
    704        className: "layerObjectDescription",
    705        textContent: "            " + trim(displayItem.line),
    706        style: {
    707          whiteSpace: "pre",
    708        },
    709        displayItem,
    710        layerViewport,
    711        onmouseover() {
    712          if (this.diPreview) {
    713            this.diPreview.classList.add("displayHover");
    714 
    715            var description = "";
    716            if (this.displayItem.contentDescriptor) {
    717              description += "Content: " + this.displayItem.contentDescriptor;
    718            } else {
    719              description += "Content: Unknown";
    720            }
    721            description +=
    722              "<br>Item: " +
    723              this.displayItem.name +
    724              " (" +
    725              this.displayItem.address +
    726              ")";
    727            description +=
    728              "<br>Layer: " + root.name + " (" + root.address + ")";
    729            if (this.displayItem.frame) {
    730              description += "<br>Frame: " + this.displayItem.frame;
    731            }
    732            if (this.displayItem.layerBounds) {
    733              description +=
    734                "<br>Bounds: [" +
    735                toFixed(this.displayItem.layerBounds[0] / 60, 2) +
    736                ", " +
    737                toFixed(this.displayItem.layerBounds[1] / 60, 2) +
    738                ", " +
    739                toFixed(this.displayItem.layerBounds[2] / 60, 2) +
    740                ", " +
    741                toFixed(this.displayItem.layerBounds[3] / 60, 2) +
    742                "] (CSS Pixels)";
    743            }
    744            if (this.displayItem.z) {
    745              description += "<br>Z: " + this.displayItem.z;
    746            }
    747            // At the end
    748            if (this.displayItem.rest) {
    749              description += "<br>" + this.displayItem.rest;
    750            }
    751 
    752            var box = this.diPreview.getBoundingClientRect();
    753            this.diPreview.tooltip = createElement("div", {
    754              className: "csstooltip",
    755              innerHTML: description,
    756              style: {
    757                top:
    758                  Math.min(
    759                    box.bottom,
    760                    document.documentElement.clientHeight - 150
    761                  ) + "px",
    762                left: box.left + "px",
    763              },
    764            });
    765 
    766            document.body.appendChild(this.diPreview.tooltip);
    767          }
    768        },
    769        onmouseout() {
    770          if (this.diPreview) {
    771            this.diPreview.classList.remove("displayHover");
    772            document.body.removeChild(this.diPreview.tooltip);
    773          }
    774        },
    775      });
    776 
    777      icon = createElement("img", {
    778        style: {
    779          width: "12px",
    780          height: "12px",
    781          marginLeft: "4px",
    782          marginRight: "4px",
    783        },
    784      });
    785      displayElem.insertBefore(icon, displayElem.firstChild);
    786      pane.appendChild(displayElem);
    787      // bounds doesn't adjust for within the layer. It's not a bad fallback but
    788      // will have the wrong offset
    789      let rect2d = displayItem.layerBounds || displayItem.bounds;
    790      if (rect2d) {
    791        // This doesn't place them corectly
    792        var appUnitsToPixels = 60 / contentScale;
    793        let diPreview = createElement("div", {
    794          id: "displayitem_" + displayItem.content + "_" + displayItem.address,
    795          className: "layerPreview",
    796          style: {
    797            position: "absolute",
    798            left: rect2d[0] / appUnitsToPixels + "px",
    799            top: rect2d[1] / appUnitsToPixels + "px",
    800            width: rect2d[2] / appUnitsToPixels + "px",
    801            height: rect2d[3] / appUnitsToPixels + "px",
    802            border: "solid 1px gray",
    803            pointerEvents: "auto",
    804          },
    805          displayElem,
    806          onmouseover() {
    807            this.displayElem.onmouseover();
    808          },
    809          onmouseout() {
    810            this.displayElem.onmouseout();
    811          },
    812        });
    813 
    814        layerViewport.appendChild(diPreview);
    815        displayElem.diPreview = diPreview;
    816      }
    817    }
    818  }
    819 
    820  for (var i = 0; i < root.children.length; i++) {
    821    populateLayers(
    822      root.children[i],
    823      displayList,
    824      pane,
    825      previewParent,
    826      hasSeenRoot,
    827      contentScale,
    828      rootPreviewParent
    829    );
    830  }
    831 }
    832 
    833 // This function takes a stdout snippet and finds the frames
    834 function parseMultiLineDump(log) {
    835  var container = createElement("div", {
    836    style: {
    837      height: "100%",
    838      position: "relative",
    839    },
    840  });
    841 
    842  var layerManagerFirstLine = "[a-zA-Z]*LayerManager \\(.*$\n";
    843  var nextLineStartWithSpace = "([ \\t].*$\n)*";
    844  var layersRegex = "(" + layerManagerFirstLine + nextLineStartWithSpace + ")";
    845 
    846  var startLine = "Painting --- after optimization:\n";
    847  var endLine = "Painting --- layer tree:";
    848  var displayListRegex = "(" + startLine + "(.*\n)*?" + endLine + ")";
    849 
    850  var regex = new RegExp(layersRegex + "|" + displayListRegex, "gm");
    851  var matches = log.match(regex);
    852  console.log(matches);
    853  window.matches = matches;
    854 
    855  var matchList = createElement("span", {
    856    style: {
    857      height: "95%",
    858      width: "10%",
    859      position: "relative",
    860      border: "solid black 2px",
    861      display: "inline-block",
    862      float: "left",
    863      overflow: "auto",
    864    },
    865  });
    866  container.appendChild(matchList);
    867  var contents = createElement("span", {
    868    style: {
    869      height: "95%",
    870      width: "88%",
    871      display: "inline-block",
    872    },
    873    textContent: "Click on a frame on the left to view the layer tree",
    874  });
    875  container.appendChild(contents);
    876 
    877  var lastDisplayList = null;
    878  var frameID = 1;
    879  for (let i = 0; i < matches.length; i++) {
    880    var currMatch = matches[i];
    881 
    882    if (currMatch.indexOf(startLine) == 0) {
    883      // Display list match
    884      var matchLines = matches[i].split("\n");
    885      lastDisplayList = parseDisplayList(matchLines);
    886    } else {
    887      // Layer tree match:
    888      let displayList = lastDisplayList;
    889      lastDisplayList = null;
    890      var currFrameDiv = createElement("a", {
    891        style: {
    892          padding: "3px",
    893          display: "block",
    894        },
    895        href: "#",
    896        textContent: "LayerTree " + frameID++,
    897        onclick() {
    898          contents.innerHTML = "";
    899          var matchLines = matches[i].split("\n");
    900          var dumpDiv = parseDump(matchLines, displayList);
    901          contents.appendChild(dumpDiv);
    902        },
    903      });
    904      matchList.appendChild(currFrameDiv);
    905    }
    906  }
    907 
    908  return container;
    909 }
    910 
    911 function parseDump(log, displayList, compositeTitle, compositeTime) {
    912  compositeTitle |= "";
    913  compositeTime |= 0;
    914 
    915  var container = createElement("div", {
    916    style: {
    917      background: "white",
    918      height: "100%",
    919      position: "relative",
    920    },
    921  });
    922 
    923  if (compositeTitle == null && compositeTime == null) {
    924    var titleDiv = createElement("div", {
    925      className: "treeColumnHeader",
    926      style: {
    927        width: "100%",
    928      },
    929      textContent:
    930        compositeTitle +
    931        (compositeTitle ? " (near " + compositeTime.toFixed(0) + " ms)" : ""),
    932    });
    933    container.appendChild(titleDiv);
    934  }
    935 
    936  var mainDiv = createElement("div", {
    937    style: {
    938      position: "absolute",
    939      top: "16px",
    940      left: "0px",
    941      right: "0px",
    942      bottom: "0px",
    943    },
    944  });
    945  container.appendChild(mainDiv);
    946 
    947  var layerListPane = createElement("div", {
    948    style: {
    949      cssFloat: "left",
    950      height: "100%",
    951      width: "300px",
    952      overflowY: "scroll",
    953    },
    954  });
    955  mainDiv.appendChild(layerListPane);
    956 
    957  var previewDiv = createElement("div", {
    958    style: {
    959      position: "absolute",
    960      left: "300px",
    961      right: "0px",
    962      top: "0px",
    963      bottom: "0px",
    964      overflow: "auto",
    965    },
    966  });
    967  mainDiv.appendChild(previewDiv);
    968 
    969  var root = parseLayers(log);
    970  populateLayers(root, displayList, layerListPane, previewDiv);
    971  return container;
    972 }