| <!-- 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> |