blob: 47a0abee6dfdeb01d58a950dd10f01349ae743bd [file]
<!-- The <debugger-app-sk> custom element declaration.
The main application element for the Skia Debugger.
Attributes:
None.
Events:
None.
Methods:
None.
-->
<link rel=import href="/res/imp/bower_components/iron-flex-layout/iron-flex-layout.html">
<link rel=import href="/res/imp/bower_components/iron-icon/iron-icon.html">
<link rel=import href="/res/imp/bower_components/iron-icons/iron-icons.html">
<link rel=import href="/res/imp/bower_components/paper-checkbox/paper-checkbox.html">
<link rel=import href="/res/imp/bower_components/paper-drawer-panel/paper-drawer-panel.html">
<link rel=import href="/res/imp/bower_components/paper-header-panel/paper-header-panel.html">
<link rel=import href="/res/imp/bower_components/paper-icon-button/paper-icon-button.html">
<link rel=import href="/res/imp/bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel=import href="/res/imp/bower_components/paper-radio-group/paper-radio-group.html">
<link rel=import href="/res/imp/bower_components/paper-radio-button/paper-radio-button.html">
<link rel=import href="/res/imp/bower_components/paper-input/paper-input.html">
<link rel=import href="/res/imp/bower_components/paper-toolbar/paper-toolbar.html">
<link rel=import href="/res/imp/bower_components/paper-tabs/paper-tabs.html">
<link rel=import href="/res/imp/bower_components/iron-pages/iron-pages.html">
<link rel=import href="/res/common/imp/9/canvas-layers.html">
<link rel=import href="/res/common/imp/9/crosshair.html">
<link rel=import href="/res/common/imp/9/dbg-info.html">
<link rel=import href="/res/common/imp/9/details-summary.html">
<link rel=import href="/res/common/imp/9/error-toast-sk.html">
<link rel=import href="/res/common/imp/9/play.html">
<link rel=import href="/res/common/imp/9/zoom.html">
<dom-module id="debugger-app-sk">
<style type="text/css" media="screen">
:root {
--paper-toolbar-background: #1f78b4;
}
:host {
display: block;
height: 100%
}
#content {
margin: 0.5em;
}
op-sk {
display: block;
}
play-sk {
margin: 5px auto;
}
#center {
overflow: hidden;
}
#rendered {
margin: 0 10px;
overflow: auto;
}
* {
font-family: Helvetica,Arial,'Bitstream Vera Sans',sans-serif;
}
.hidden {
display: none;
}
.shortcuts {
margin-left: 3em;
margin-bottom: 2em;
}
.shortcuts td {
padding-left: 1em;
}
.shortcuts tr {
padding-bottom: 0.5em;
}
#colors {
margin: 1em 0;
}
#colorBreakPoint {
margin-left: 3em;
}
#download {
margin: 0 3em;
color: #1f78b4;
}
dbg-info-sk,
#srgbMode,
#clip {
margin-left: 3em;
margin-bottom: 1em;
display: block;
}
paper-radio-button {
margin-left: 3em;
margin-bottom: 1em;
padding: 0;
}
#gpuBatchBounds,
paper-toggle-button {
margin: 0.5em 1em;
}
.opBoundColor {
color: #E31A1C;
opacity: 0.75;
}
.otherBoundColor {
color: #FF7F00;
opacity: 0.75;
}
.totalBoundColor {
color: #6A3D9A;
opacity: 0.75;
}
paper-tabs {
--paper-tabs-selection-bar-color: #1f78b4;
}
paper-tab[disabled] {
color: lightgray;
}
</style>
<template>
<paper-header-panel>
<paper-toolbar>
<div>Skia Debugger</div>
</paper-toolbar>
<div id=content>
<h2>Upload an SKP here.</h2>
<div class="layout horizontal">
<form enctype="multipart/form-data" method="post" id=post_skp action="/new" name="fileinfo">
<label>SKP to send:</label>
<input type="file" name="file" required />
<input type="submit" value="Upload" />
</form>
<a href="/download" id=download class=hidden>Download</a>
</div>
<hr/>
<div class="layout horizontal">
<div id=left class="layout vertical flex-2">
<paper-input id="fast" on-change="_fastFilter" label="Filter"></paper-input>
<play-sk id=play></play-sk>
<commands-sk id=commands grouping=50></commands-sk>
</div>
<div id=center class="layout vertical flex-3">
<div class="layout horizontal">
<paper-toggle-button on-iron-change="_gpuChange" id=gpu checked="{{_gpuChecked(_cmd.mode)}}">GPU</paper-toggle-button>
<paper-checkbox disabled="{{_gpuNotChecked(_cmd.mode)}}" checked="{{_isTrue(_cmd.drawGpuBatchBounds)}}" id=gpuBatchBounds on-tap="_gpuBatchBounds">Display Batch Bounds</paper-checkbox>
</div>
<div id=rendered>
<paper-tabs selected="{{tab_selected}}">
<paper-tab>Picture</paper-tab>
<paper-tab disabled=[[_isEmpty(bitmap)]]>Image</paper-tab>
</paper-tabs>
<iron-pages selected="{{tab_selected}}">
<div>
<canvas-layers-sk layers='["crosshair"]' id=layers on-tap="_crosshairClick">
<img id=img src="" class=hidden>
</canvas-layers-sk>
<crosshair-sk id=crosshair target=layers name=crosshair update_on=move></crosshair-sk>
</div>
<div>
<img id=standAloneImg src$="[[bitmap]]">
</div>
</iron-pages>
</div>
</div>
<div id=right class="layout vertical flex-2">
<details-sk open>
<summary-sk>
Color Mode
</summary-sk>
<paper-radio-group class="layout vertical" on-paper-radio-group-changed="_colorModeChange" id=colorMode selected="[[_cmd.colorMode]]">
<paper-radio-button name="0">Linear 32-bit (Legacy)</paper-radio-button>
<paper-radio-button name="1">sRGB 32-bit</paper-radio-button>
<paper-radio-button name="2">Linear half-precision float</paper-radio-button>
</paper-radio-group>
<paper-checkbox id=srgbMode checked="{{_isTrue(_cmd.srgbMode)}}" on-tap="_srgbMode">Treat SkColor as sRGB</paper-checkbox>
</details-sk>
<details-sk open>
<summary-sk>
Clip and Matrix
</summary-sk>
<paper-checkbox id=clip on-tap="_clipHandler">Show Clip</paper-checkbox>
<dbg-info-sk info="{{ info }}"></dbg-info-sk>
</details-sk>
<details-sk id=batchBoundsLegend class=hidden>
<summary-sk>
Batch Bounds Legend
</summary-sk>
<table class=shortcuts>
<tr><td class=opBoundColor>Bounds for the current op.</td></tr>
<tr><td class=otherBoundColor>Bounds for other ops in the same batch.</td></tr>
<tr><td class=totalBoundColor>Total bounds of the current batch.</td></tr>
</table>
</details-sk>
<div id=position><b>Postion:</b> ({{ x }}, {{ y }})</div>
<div id=colors><b>Color:</b> {{ rgb }} {{ hex }} </div>
<div>
<paper-checkbox disabled id=colorBreakPoint on-tap="_breakPoint">Break on change.</paper-checkbox> <span id=prevColor></span>
</div>
<details-sk id=keyboardshortcuts class=hidden>
<summary-sk>
Keyboard shortcuts
</summary-sk>
<table class=shortcuts>
<tr><th>H</th><td>Left</td></tr>
<tr><th>L</th><td>Right</td></tr>
<tr><th>J</th><td>Down</td></tr>
<tr><th>K</th><td>Up</td></tr>
<tr><td colspan=2>Click the image again to turn off keyboard navigation.</td></tr>
</table>
</details-sk>
<zoom-sk source=img pixels=20 id=zoom></zoom-sk>
</div>
</div>
<error-toast-sk></error-toast-sk>
</div>
</paper-header-panel>
</template>
</dom-module>
<script>
(function () {
var INDENTERS = {
"Save": { icon: "icons:save", color: "#B2DF8A" },
"SaveLayer": { icon: "icons:content-copy", color: "#FDBF6F" },
"BeginDrawPicture": { icon: "image:image", color: "#A6CEE3" },
};
var OUTDENTERS = ["Restore", "EndDrawPicture"];
Polymer({
is: "debugger-app-sk",
properties: {
tab_selected: {
type: Number,
value: 0,
reflectToAttribute: false,
},
bitmap: {
type: String,
value: "",
reflectToAttribute: false,
},
},
ready: function() {
// _targetItem is the index of the op we are in the process of moving to.
// The index is the offset of the op in the this._filtered.commands array.
// That is, we've requested to move to this op, but the image might not
// be loaded yet.
this._targetItem = 0;
// The original JSON response from the server.
this._cmd = {
version: 1,
commands: [],
};
// _filtered contains the .commands that match the current filter, or all
// of the commands if no filter is active.
//
// NB: There is a distintion in the code below between an op's item vs
// index. That is, an ops index never changes, it is the index
// number that the server understands, and is the location of the op
// in this._cmd.commands.
//
// The op item changes, it is the location of the op in
// this._filtered.commands. Some functions use index, some use
// item.
this._filtered = {
version: 1,
commands: [],
};
// Load the test image served by the debugger.
this._setCacheBreakingURL();
this.$.post_skp.action= window.location.origin + "/new";
this.$.commands.addEventListener('op-selected', function(e) {
// Only force reloading the image if necessary.
var item = this._findItemFromIndex(e.detail.index);
if (this._targetItem != item) {
this._targetItem = item;
this._setCacheBreakingURL();
} else {
// We know if we've gotten here that the element wasn't selected by
// a UI action, i.e. we know we're here because we are 'run'ing.
this.$.commands.scrollToTop(e.detail.index);
}
}.bind(this));
this.$.commands.addEventListener('op-toggled', function(e) {
// Toggle the op and the trigger a reload of the image.
var value = e.detail.checked ? "1" : "0";
sk.post("/cmd/" + e.detail.index + "/" + value, "").then(function() {
this._setCacheBreakingURL();
}.bind(this)).catch(sk.errorMessage);
}.bind(this));
this._refreshPage();
this.$.crosshair.addEventListener('crosshair', function(e) {
this.$.zoom.x = e.detail.x;
this.$.zoom.y = e.detail.y;
}.bind(this));
this.$.play.prePlayCallback(function() {
if (!this.$.colorBreakPoint.disabled && this.$.colorBreakPoint.checked) {
sk.get("/break/" + this._targetItem + "/" + this.x + "/" + this.y).then(JSON.parse).then(function(json) {
if (json.endOp != -1) {
this.$.prevColor.textContent = json.startColor + " -> " + json.endColor;
var item = this._findItemFromIndex(json.endOp);
this._targetItem = item;
this._setCacheBreakingURL();
} else {
sk.errorMessage("Pixel at (" + this.x + ", " + this.y + ") never changes.");
}
}.bind(this)).catch(sk.errorMessage);
return false;
}
return true;
}.bind(this));
this.$.zoom.addEventListener('zoom-point', function(e) {
this.set("rgb", e.detail.rgb);
this.set("hex", e.detail.hex);
this.set("x", e.detail.x);
this.set("y", e.detail.y);
}.bind(this));
this.$.zoom.addEventListener('zoom-loaded', function(e) {
this.$.play.movedTo(this._targetItem);
}.bind(this));
this.$.img.addEventListener('load', function() {
this.$.download.href = window.location.origin + "/download";
this.$.download.classList.remove("hidden");
this.$.commands.item = this._targetItem;
}.bind(this));
this.$.play.addEventListener('moveto', function(e) {
this._targetItem = e.detail.item;
this._setCacheBreakingURL();
}.bind(this));
this.$.play.addEventListener('mode-changed-manually', function(e) {
// Clear the breakpoint info if the user presses a button.
this.$.prevColor.textContent = "";
}.bind(this));
},
attached: function() {
document.body.addEventListener('keydown', this._keyDownHandler.bind(this), true);
},
_colorModeChange: function(e) {
// Ignore if this._cmd hasn't been loaded yet.
if (!this._cmd) {
return
}
// Don't do anything if the radio group and the mode already agree.
if (e.target.selected == this._cmd.colorMode) {
return
}
sk.post("/colorMode/" + e.target.selected, "").then(function() {
// Reloads the image and the JSON for all the commands.
this._refreshPage();
}.bind(this)).catch(function(err) {
this.$.colorMode.selected = 0;
sk.errorMessage(err);
}.bind(this));
},
_srgbMode: function(e) {
// Ignore if this._cmd hasn't been loaded yet.
if (!this._cmd) {
return
}
// Don't do anything if the toggle and the mode already agree.
if (this.$.srgbMode.checked === this._cmd.srgbMode) {
return
}
sk.post("/srgbMode/" + (this.$.srgbMode.checked ? 1 : 0), "").then(function() {
// Reloads the image and the JSON for all the commands.
this._refreshPage();
}.bind(this)).catch(function(err) {
this.$.srgbMode.checked = !this.$.srgbMode.checked;
sk.errorMessage(err);
}.bind(this));
},
_gpuChange: function(e) {
// Ignore if this._cmd hasn't been loaded yet.
if (!this._cmd) {
return
}
// Don't do anything if the toggle and the mode already agree.
if (e.target.checked == (this._cmd.mode == "gpu")) {
return
}
sk.post("/enableGPU/" + (e.target.checked ? 1 : 0), "").then(function() {
// Reloads the image and the JSON for all the commands.
this._refreshPage();
}.bind(this)).catch(function(err) {
this.$.gpu.checked = !this.$.gpu.checked;
sk.errorMessage(err);
}.bind(this));
},
_gpuChecked: function(mode) {
return mode == "gpu";
},
_gpuNotChecked: function(mode) {
return !this._gpuChecked(mode);
},
_isTrue: function(b) {
return !!b;
},
_isEmpty: function(s) {
return s.length == 0;
},
_gpuBatchBounds: function(e) {
// Ignore if this._cmd hasn't been loaded yet.
if (!this._cmd) {
return
}
this.$.batchBoundsLegend.classList.toggle("hidden", !this.$.gpuBatchBounds.checked);
// Don't do anything if the toggle and the mode already agree.
if (this.$.gpuBatchBounds.checked === this._cmd.drawGpuBatchBounds) {
return
}
sk.post("/batchBounds/" + (this.$.gpuBatchBounds.checked ? 1 : 0), "").then(function() {
// Reloads the image and the JSON for all the commands.
this._refreshPage();
}.bind(this)).catch(function(err) {
this.$.gpuBatchBounds.checked = !this.$.gpuBatchBounds.checked;
sk.errorMessage(err);
}.bind(this));
},
_breakPoint: function() {
if (this.$.colorBreakPoint.checked) {
// We want to be able to jump to any op when the breakpoint
// triggers, so we remove all filtering.
this.$.fast.value = "";
this._fastFilter();
}
},
_findItemFromIndex: function(index) {
var item = 0;
for (var i = 0; i < this._filtered.commands.length ; i++) {
if (this._filtered.commands[i]._index == index) {
item = i;
break;
}
}
return item;
},
_fastFilter: function() {
var filters = this.$.fast.value.trim().toLowerCase().split(/\s+/);
var matches = function(s) {
s = s.toLowerCase();
for (var i = 0; i < filters.length; i++) {
if (s.indexOf(filters[i]) >= 0) {
return true;
}
}
return false;
};
var filtered = {
version: 1,
commands: [],
}
this._cmd.commands.forEach(function(c) {
if (matches(JSON.stringify(c.details).toLowerCase())) {
filtered.commands.push(c);
}
}.bind(this));
this._setFiltered(filtered);
},
_refreshPage: function() {
this.$.img.classList.remove("hidden");
this._setCacheBreakingURL();
sk.get("/info").then(JSON.parse).then(function(json) {
this.set('info', json);
}.bind(this)).catch(sk.errorMessage);
sk.get("/cmd").then(JSON.parse).then(function(json) {
this._cmd = this._addDepth(json);
if (this.$.fast.value) {
this._fastFilter();
} else {
var filtered = {
version: 1,
commands: this._cmd.commands.slice(),
};
this._setFiltered(filtered);
}
}.bind(this)).catch(sk.errorMessage);
},
_setFiltered: function(filtered) {
this.$.commands.cmd = filtered;
this._filtered = filtered;
this.$.play.size = filtered.commands.length;
this._targetItem = filtered.commands.length - 1;
this._setCacheBreakingURL();
},
_hasBitmap: function(s) {
return !!s;
},
_clipHandler: function() {
var alpha = this.$.clip.checked ? 128 : 0;
sk.post("/clipAlpha/" + alpha).then(function() {
this._setCacheBreakingURL();
}.bind(this)).catch(sk.errorMessage);
},
_setCacheBreakingURL: function() {
// Get op index from item number using this._filtered.
if (this._filtered.commands.length == 0) {
return
}
var index = this._filtered.commands[this._targetItem]._index;
var op = this._filtered.commands[this._targetItem];
var path = "/img/" + index + "?";
this.bitmap = op.details.bitmap ? op.details.bitmap.data : "";
sk.get("/info/" + index).then(JSON.parse).then(function(json) {
this.set('info', json);
}.bind(this)).catch(sk.errorMessage);
this.$.img.src = window.location.origin + path + "?" + new Date().getTime();
},
_crosshairClick: function() {
if (this.$.crosshair.update_on === "move") {
this.$.crosshair.update_on = "click";
this.$.colorBreakPoint.disabled = false;
this.$.keyboardshortcuts.classList.remove("hidden");
} else {
this.$.crosshair.update_on = "move";
this.$.colorBreakPoint.disabled = true;
this.$.keyboardshortcuts.classList.add("hidden");
}
},
_keyDownHandler: function(e) {
var c = String.fromCharCode(e.keyCode);
switch (c) {
case "J":
this.$.crosshair.y = this.$.crosshair.y+1;
break;
case "K":
this.$.crosshair.y = this.$.crosshair.y-1;
break;
case "H":
this.$.crosshair.x = this.$.crosshair.x-1;
break;
case "L":
this.$.crosshair.x = this.$.crosshair.x+1;
break;
}
if ("JKHL".indexOf(c) != -1 ) {
e.stopPropagation();
}
},
_json: function(s) {
return JSON.stringify(s, null, 2);
},
// _addDepth cycles through the commands and set a depth based on Save/Restore pairs.
_addDepth: function(cmd) {
var commands = cmd.commands;
var depth = 0;
var prefixes = [];
for (var i = 0; i < commands.length; i++) {
commands[i] = {
details: commands[i],
_index: i,
};
commands[i]._depth = depth;
commands[i]._prefix = prefixes.slice();
if (commands[i].details.command in INDENTERS) {
depth++;
prefixes.push(INDENTERS[commands[i].details.command]);
} else if (OUTDENTERS.indexOf(commands[i].details.command) != -1) {
depth--;
prefixes.pop();
commands[i]._depth = depth;
commands[i]._prefix = prefixes.slice();
}
}
return cmd;
}
});
})();
</script>