From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- devtools/client/shadereditor/moz.build | 10 + devtools/client/shadereditor/panel.js | 76 +++ devtools/client/shadereditor/shadereditor.js | 633 +++++++++++++++++++++ devtools/client/shadereditor/shadereditor.xul | 70 +++ devtools/client/shadereditor/test/.eslintrc.js | 6 + devtools/client/shadereditor/test/browser.ini | 47 ++ .../test/browser_se_aaa_run_first_leaktest.js | 17 + .../client/shadereditor/test/browser_se_bfcache.js | 60 ++ .../test/browser_se_editors-contents.js | 30 + .../test/browser_se_editors-error-gutter.js | 156 +++++ .../test/browser_se_editors-error-tooltip.js | 56 ++ .../test/browser_se_editors-lazy-init.js | 34 ++ .../shadereditor/test/browser_se_first-run.js | 43 ++ .../shadereditor/test/browser_se_navigation.js | 71 +++ .../test/browser_se_programs-blackbox-01.js | 169 ++++++ .../test/browser_se_programs-blackbox-02.js | 63 ++ .../shadereditor/test/browser_se_programs-cache.js | 41 ++ .../test/browser_se_programs-highlight-01.js | 93 +++ .../test/browser_se_programs-highlight-02.js | 49 ++ .../shadereditor/test/browser_se_programs-list.js | 87 +++ .../test/browser_se_shaders-edit-01.js | 73 +++ .../test/browser_se_shaders-edit-02.js | 74 +++ .../test/browser_se_shaders-edit-03.js | 85 +++ .../test/browser_webgl-actor-test-01.js | 16 + .../test/browser_webgl-actor-test-02.js | 21 + .../test/browser_webgl-actor-test-03.js | 26 + .../test/browser_webgl-actor-test-04.js | 27 + .../test/browser_webgl-actor-test-05.js | 27 + .../test/browser_webgl-actor-test-06.js | 64 +++ .../test/browser_webgl-actor-test-07.js | 61 ++ .../test/browser_webgl-actor-test-08.js | 37 ++ .../test/browser_webgl-actor-test-09.js | 89 +++ .../test/browser_webgl-actor-test-10.js | 44 ++ .../test/browser_webgl-actor-test-11.js | 25 + .../test/browser_webgl-actor-test-12.js | 27 + .../test/browser_webgl-actor-test-13.js | 67 +++ .../test/browser_webgl-actor-test-14.js | 46 ++ .../test/browser_webgl-actor-test-15.js | 133 +++++ .../test/browser_webgl-actor-test-16.js | 141 +++++ .../test/browser_webgl-actor-test-17.js | 46 ++ .../test/browser_webgl-actor-test-18.js | 31 + .../shadereditor/test/doc_blended-geometry.html | 136 +++++ .../shadereditor/test/doc_multiple-contexts.html | 112 ++++ .../test/doc_overlapping-geometry.html | 120 ++++ .../client/shadereditor/test/doc_shader-order.html | 83 +++ .../shadereditor/test/doc_simple-canvas.html | 125 ++++ devtools/client/shadereditor/test/head.js | 292 ++++++++++ 47 files changed, 3839 insertions(+) create mode 100644 devtools/client/shadereditor/moz.build create mode 100644 devtools/client/shadereditor/panel.js create mode 100644 devtools/client/shadereditor/shadereditor.js create mode 100644 devtools/client/shadereditor/shadereditor.xul create mode 100644 devtools/client/shadereditor/test/.eslintrc.js create mode 100644 devtools/client/shadereditor/test/browser.ini create mode 100644 devtools/client/shadereditor/test/browser_se_aaa_run_first_leaktest.js create mode 100644 devtools/client/shadereditor/test/browser_se_bfcache.js create mode 100644 devtools/client/shadereditor/test/browser_se_editors-contents.js create mode 100644 devtools/client/shadereditor/test/browser_se_editors-error-gutter.js create mode 100644 devtools/client/shadereditor/test/browser_se_editors-error-tooltip.js create mode 100644 devtools/client/shadereditor/test/browser_se_editors-lazy-init.js create mode 100644 devtools/client/shadereditor/test/browser_se_first-run.js create mode 100644 devtools/client/shadereditor/test/browser_se_navigation.js create mode 100644 devtools/client/shadereditor/test/browser_se_programs-blackbox-01.js create mode 100644 devtools/client/shadereditor/test/browser_se_programs-blackbox-02.js create mode 100644 devtools/client/shadereditor/test/browser_se_programs-cache.js create mode 100644 devtools/client/shadereditor/test/browser_se_programs-highlight-01.js create mode 100644 devtools/client/shadereditor/test/browser_se_programs-highlight-02.js create mode 100644 devtools/client/shadereditor/test/browser_se_programs-list.js create mode 100644 devtools/client/shadereditor/test/browser_se_shaders-edit-01.js create mode 100644 devtools/client/shadereditor/test/browser_se_shaders-edit-02.js create mode 100644 devtools/client/shadereditor/test/browser_se_shaders-edit-03.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-01.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-02.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-03.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-04.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-05.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-06.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-07.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-08.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-09.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-10.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-11.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-12.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-13.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-14.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-15.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-16.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-17.js create mode 100644 devtools/client/shadereditor/test/browser_webgl-actor-test-18.js create mode 100644 devtools/client/shadereditor/test/doc_blended-geometry.html create mode 100644 devtools/client/shadereditor/test/doc_multiple-contexts.html create mode 100644 devtools/client/shadereditor/test/doc_overlapping-geometry.html create mode 100644 devtools/client/shadereditor/test/doc_shader-order.html create mode 100644 devtools/client/shadereditor/test/doc_simple-canvas.html create mode 100644 devtools/client/shadereditor/test/head.js (limited to 'devtools/client/shadereditor') diff --git a/devtools/client/shadereditor/moz.build b/devtools/client/shadereditor/moz.build new file mode 100644 index 000000000..684fabc22 --- /dev/null +++ b/devtools/client/shadereditor/moz.build @@ -0,0 +1,10 @@ +# vim: set filetype=python: +# 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/. + +DevToolsModules( + 'panel.js' +) + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/devtools/client/shadereditor/panel.js b/devtools/client/shadereditor/panel.js new file mode 100644 index 000000000..92fac9646 --- /dev/null +++ b/devtools/client/shadereditor/panel.js @@ -0,0 +1,76 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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/. */ +"use strict"; + +const { Cc, Ci, Cu, Cr } = require("chrome"); +const promise = require("promise"); +const EventEmitter = require("devtools/shared/event-emitter"); +const { WebGLFront } = require("devtools/shared/fronts/webgl"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); + +function ShaderEditorPanel(iframeWindow, toolbox) { + this.panelWin = iframeWindow; + this._toolbox = toolbox; + this._destroyer = null; + + EventEmitter.decorate(this); +} + +exports.ShaderEditorPanel = ShaderEditorPanel; + +ShaderEditorPanel.prototype = { + /** + * Open is effectively an asynchronous constructor. + * + * @return object + * A promise that is resolved when the Shader Editor completes opening. + */ + open: function () { + let targetPromise; + + // Local debugging needs to make the target remote. + if (!this.target.isRemote) { + targetPromise = this.target.makeRemote(); + } else { + targetPromise = promise.resolve(this.target); + } + + return targetPromise + .then(() => { + this.panelWin.gToolbox = this._toolbox; + this.panelWin.gTarget = this.target; + this.panelWin.gFront = new WebGLFront(this.target.client, this.target.form); + return this.panelWin.startupShaderEditor(); + }) + .then(() => { + this.isReady = true; + this.emit("ready"); + return this; + }) + .then(null, function onError(aReason) { + DevToolsUtils.reportException("ShaderEditorPanel.prototype.open", aReason); + }); + }, + + // DevToolPanel API + + get target() { + return this._toolbox.target; + }, + + destroy: function () { + // Make sure this panel is not already destroyed. + if (this._destroyer) { + return this._destroyer; + } + + return this._destroyer = this.panelWin.shutdownShaderEditor().then(() => { + // Destroy front to ensure packet handler is removed from client + this.panelWin.gFront.destroy(); + this.emit("destroyed"); + }); + } +}; diff --git a/devtools/client/shadereditor/shadereditor.js b/devtools/client/shadereditor/shadereditor.js new file mode 100644 index 000000000..6b53302c4 --- /dev/null +++ b/devtools/client/shadereditor/shadereditor.js @@ -0,0 +1,633 @@ +/* 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/. */ +"use strict"; + +var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); +const {SideMenuWidget} = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm"); +const promise = require("promise"); +const Services = require("Services"); +const EventEmitter = require("devtools/shared/event-emitter"); +const Tooltip = require("devtools/client/shared/widgets/tooltip/Tooltip"); +const Editor = require("devtools/client/sourceeditor/editor"); +const {LocalizationHelper} = require("devtools/shared/l10n"); +const {Heritage, WidgetMethods, setNamedTimeout} = + require("devtools/client/shared/widgets/view-helpers"); +const {Task} = require("devtools/shared/task"); + +// The panel's window global is an EventEmitter firing the following events: +const EVENTS = { + // When new programs are received from the server. + NEW_PROGRAM: "ShaderEditor:NewProgram", + PROGRAMS_ADDED: "ShaderEditor:ProgramsAdded", + + // When the vertex and fragment sources were shown in the editor. + SOURCES_SHOWN: "ShaderEditor:SourcesShown", + + // When a shader's source was edited and compiled via the editor. + SHADER_COMPILED: "ShaderEditor:ShaderCompiled", + + // When the UI is reset from tab navigation + UI_RESET: "ShaderEditor:UIReset", + + // When the editor's error markers are all removed + EDITOR_ERROR_MARKERS_REMOVED: "ShaderEditor:EditorCleaned" +}; +XPCOMUtils.defineConstant(this, "EVENTS", EVENTS); + +const STRINGS_URI = "devtools/client/locales/shadereditor.properties"; +const HIGHLIGHT_TINT = [1, 0, 0.25, 1]; // rgba +const TYPING_MAX_DELAY = 500; // ms +const SHADERS_AUTOGROW_ITEMS = 4; +const GUTTER_ERROR_PANEL_OFFSET_X = 7; // px +const GUTTER_ERROR_PANEL_DELAY = 100; // ms +const DEFAULT_EDITOR_CONFIG = { + gutters: ["errors"], + lineNumbers: true, + showAnnotationRuler: true +}; + +/** + * The current target and the WebGL Editor front, set by this tool's host. + */ +var gToolbox, gTarget, gFront; + +/** + * Initializes the shader editor controller and views. + */ +function startupShaderEditor() { + return promise.all([ + EventsHandler.initialize(), + ShadersListView.initialize(), + ShadersEditorsView.initialize() + ]); +} + +/** + * Destroys the shader editor controller and views. + */ +function shutdownShaderEditor() { + return promise.all([ + EventsHandler.destroy(), + ShadersListView.destroy(), + ShadersEditorsView.destroy() + ]); +} + +/** + * Functions handling target-related lifetime events. + */ +var EventsHandler = { + /** + * Listen for events emitted by the current tab target. + */ + initialize: function () { + this._onHostChanged = this._onHostChanged.bind(this); + this._onTabNavigated = this._onTabNavigated.bind(this); + this._onProgramLinked = this._onProgramLinked.bind(this); + this._onProgramsAdded = this._onProgramsAdded.bind(this); + gToolbox.on("host-changed", this._onHostChanged); + gTarget.on("will-navigate", this._onTabNavigated); + gTarget.on("navigate", this._onTabNavigated); + gFront.on("program-linked", this._onProgramLinked); + this.reloadButton = $("#requests-menu-reload-notice-button"); + this.reloadButton.addEventListener("command", this._onReloadCommand); + }, + + /** + * Remove events emitted by the current tab target. + */ + destroy: function () { + gToolbox.off("host-changed", this._onHostChanged); + gTarget.off("will-navigate", this._onTabNavigated); + gTarget.off("navigate", this._onTabNavigated); + gFront.off("program-linked", this._onProgramLinked); + this.reloadButton.removeEventListener("command", this._onReloadCommand); + }, + + /** + * Handles a command event on reload button + */ + _onReloadCommand() { + gFront.setup({ reload: true }); + }, + + /** + * Handles a host change event on the parent toolbox. + */ + _onHostChanged: function () { + if (gToolbox.hostType == "side") { + $("#shaders-pane").removeAttribute("height"); + } + }, + + /** + * Called for each location change in the debugged tab. + */ + _onTabNavigated: function (event, {isFrameSwitching}) { + switch (event) { + case "will-navigate": { + // Make sure the backend is prepared to handle WebGL contexts. + if (!isFrameSwitching) { + gFront.setup({ reload: false }); + } + + // Reset UI. + ShadersListView.empty(); + // When switching to an iframe, ensure displaying the reload button. + // As the document has already been loaded without being hooked. + if (isFrameSwitching) { + $("#reload-notice").hidden = false; + $("#waiting-notice").hidden = true; + } else { + $("#reload-notice").hidden = true; + $("#waiting-notice").hidden = false; + } + + $("#content").hidden = true; + window.emit(EVENTS.UI_RESET); + + break; + } + case "navigate": { + // Manually retrieve the list of program actors known to the server, + // because the backend won't emit "program-linked" notifications + // in the case of a bfcache navigation (since no new programs are + // actually linked). + gFront.getPrograms().then(this._onProgramsAdded); + break; + } + } + }, + + /** + * Called every time a program was linked in the debugged tab. + */ + _onProgramLinked: function (programActor) { + this._addProgram(programActor); + window.emit(EVENTS.NEW_PROGRAM); + }, + + /** + * Callback for the front's getPrograms() method. + */ + _onProgramsAdded: function (programActors) { + programActors.forEach(this._addProgram); + window.emit(EVENTS.PROGRAMS_ADDED); + }, + + /** + * Adds a program to the shaders list and unhides any modal notices. + */ + _addProgram: function (programActor) { + $("#waiting-notice").hidden = true; + $("#reload-notice").hidden = true; + $("#content").hidden = false; + ShadersListView.addProgram(programActor); + } +}; + +/** + * Functions handling the sources UI. + */ +var ShadersListView = Heritage.extend(WidgetMethods, { + /** + * Initialization function, called when the tool is started. + */ + initialize: function () { + this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), { + showArrows: true, + showItemCheckboxes: true + }); + + this._onProgramSelect = this._onProgramSelect.bind(this); + this._onProgramCheck = this._onProgramCheck.bind(this); + this._onProgramMouseOver = this._onProgramMouseOver.bind(this); + this._onProgramMouseOut = this._onProgramMouseOut.bind(this); + + this.widget.addEventListener("select", this._onProgramSelect, false); + this.widget.addEventListener("check", this._onProgramCheck, false); + this.widget.addEventListener("mouseover", this._onProgramMouseOver, true); + this.widget.addEventListener("mouseout", this._onProgramMouseOut, true); + }, + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function () { + this.widget.removeEventListener("select", this._onProgramSelect, false); + this.widget.removeEventListener("check", this._onProgramCheck, false); + this.widget.removeEventListener("mouseover", this._onProgramMouseOver, true); + this.widget.removeEventListener("mouseout", this._onProgramMouseOut, true); + }, + + /** + * Adds a program to this programs container. + * + * @param object programActor + * The program actor coming from the active thread. + */ + addProgram: function (programActor) { + if (this.hasProgram(programActor)) { + return; + } + + // Currently, there's no good way of differentiating between programs + // in a way that helps humans. It will be a good idea to implement a + // standard of allowing debuggees to add some identifiable metadata to their + // program sources or instances. + let label = L10N.getFormatStr("shadersList.programLabel", this.itemCount); + let contents = document.createElement("label"); + contents.className = "plain program-item"; + contents.setAttribute("value", label); + contents.setAttribute("crop", "start"); + contents.setAttribute("flex", "1"); + + // Append a program item to this container. + this.push([contents], { + index: -1, /* specifies on which position should the item be appended */ + attachment: { + label: label, + programActor: programActor, + checkboxState: true, + checkboxTooltip: L10N.getStr("shadersList.blackboxLabel") + } + }); + + // Make sure there's always a selected item available. + if (!this.selectedItem) { + this.selectedIndex = 0; + } + + // Prevent this container from growing indefinitely in height when the + // toolbox is docked to the side. + if (gToolbox.hostType == "side" && this.itemCount == SHADERS_AUTOGROW_ITEMS) { + this._pane.setAttribute("height", this._pane.getBoundingClientRect().height); + } + }, + + /** + * Returns whether a program was already added to this programs container. + * + * @param object programActor + * The program actor coming from the active thread. + * @param boolean + * True if the program was added, false otherwise. + */ + hasProgram: function (programActor) { + return !!this.attachments.filter(e => e.programActor == programActor).length; + }, + + /** + * The select listener for the programs container. + */ + _onProgramSelect: function ({ detail: sourceItem }) { + if (!sourceItem) { + return; + } + // The container is not empty and an actual item was selected. + let attachment = sourceItem.attachment; + + function getShaders() { + return promise.all([ + attachment.vs || (attachment.vs = attachment.programActor.getVertexShader()), + attachment.fs || (attachment.fs = attachment.programActor.getFragmentShader()) + ]); + } + function getSources([vertexShaderActor, fragmentShaderActor]) { + return promise.all([ + vertexShaderActor.getText(), + fragmentShaderActor.getText() + ]); + } + function showSources([vertexShaderText, fragmentShaderText]) { + return ShadersEditorsView.setText({ + vs: vertexShaderText, + fs: fragmentShaderText + }); + } + + getShaders() + .then(getSources) + .then(showSources) + .then(null, e => console.error(e)); + }, + + /** + * The check listener for the programs container. + */ + _onProgramCheck: function ({ detail: { checked }, target }) { + let sourceItem = this.getItemForElement(target); + let attachment = sourceItem.attachment; + attachment.isBlackBoxed = !checked; + attachment.programActor[checked ? "unblackbox" : "blackbox"](); + }, + + /** + * The mouseover listener for the programs container. + */ + _onProgramMouseOver: function (e) { + let sourceItem = this.getItemForElement(e.target, { noSiblings: true }); + if (sourceItem && !sourceItem.attachment.isBlackBoxed) { + sourceItem.attachment.programActor.highlight(HIGHLIGHT_TINT); + + if (e instanceof Event) { + e.preventDefault(); + e.stopPropagation(); + } + } + }, + + /** + * The mouseout listener for the programs container. + */ + _onProgramMouseOut: function (e) { + let sourceItem = this.getItemForElement(e.target, { noSiblings: true }); + if (sourceItem && !sourceItem.attachment.isBlackBoxed) { + sourceItem.attachment.programActor.unhighlight(); + + if (e instanceof Event) { + e.preventDefault(); + e.stopPropagation(); + } + } + } +}); + +/** + * Functions handling the editors displaying the vertex and fragment shaders. + */ +var ShadersEditorsView = { + /** + * Initialization function, called when the tool is started. + */ + initialize: function () { + XPCOMUtils.defineLazyGetter(this, "_editorPromises", () => new Map()); + this._vsFocused = this._onFocused.bind(this, "vs", "fs"); + this._fsFocused = this._onFocused.bind(this, "fs", "vs"); + this._vsChanged = this._onChanged.bind(this, "vs"); + this._fsChanged = this._onChanged.bind(this, "fs"); + }, + + /** + * Destruction function, called when the tool is closed. + */ + destroy: Task.async(function* () { + this._destroyed = true; + yield this._toggleListeners("off"); + for (let p of this._editorPromises.values()) { + let editor = yield p; + editor.destroy(); + } + }), + + /** + * Sets the text displayed in the vertex and fragment shader editors. + * + * @param object sources + * An object containing the following properties + * - vs: the vertex shader source code + * - fs: the fragment shader source code + * @return object + * A promise resolving upon completion of text setting. + */ + setText: function (sources) { + let view = this; + function setTextAndClearHistory(editor, text) { + editor.setText(text); + editor.clearHistory(); + } + + return Task.spawn(function* () { + yield view._toggleListeners("off"); + yield promise.all([ + view._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)), + view._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs)) + ]); + yield view._toggleListeners("on"); + }).then(() => window.emit(EVENTS.SOURCES_SHOWN, sources)); + }, + + /** + * Lazily initializes and returns a promise for an Editor instance. + * + * @param string type + * Specifies for which shader type should an editor be retrieved, + * either are "vs" for a vertex, or "fs" for a fragment shader. + * @return object + * Returns a promise that resolves to an editor instance + */ + _getEditor: function (type) { + if (this._editorPromises.has(type)) { + return this._editorPromises.get(type); + } + + let deferred = promise.defer(); + this._editorPromises.set(type, deferred.promise); + + // Initialize the source editor and store the newly created instance + // in the ether of a resolved promise's value. + let parent = $("#" + type + "-editor"); + let editor = new Editor(DEFAULT_EDITOR_CONFIG); + editor.config.mode = Editor.modes[type]; + + if (this._destroyed) { + deferred.resolve(editor); + } else { + editor.appendTo(parent).then(() => deferred.resolve(editor)); + } + + return deferred.promise; + }, + + /** + * Toggles all the event listeners for the editors either on or off. + * + * @param string flag + * Either "on" to enable the event listeners, "off" to disable them. + * @return object + * A promise resolving upon completion of toggling the listeners. + */ + _toggleListeners: function (flag) { + return promise.all(["vs", "fs"].map(type => { + return this._getEditor(type).then(editor => { + editor[flag]("focus", this["_" + type + "Focused"]); + editor[flag]("change", this["_" + type + "Changed"]); + }); + })); + }, + + /** + * The focus listener for a source editor. + * + * @param string focused + * The corresponding shader type for the focused editor (e.g. "vs"). + * @param string focused + * The corresponding shader type for the other editor (e.g. "fs"). + */ + _onFocused: function (focused, unfocused) { + $("#" + focused + "-editor-label").setAttribute("selected", ""); + $("#" + unfocused + "-editor-label").removeAttribute("selected"); + }, + + /** + * The change listener for a source editor. + * + * @param string type + * The corresponding shader type for the focused editor (e.g. "vs"). + */ + _onChanged: function (type) { + setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type)); + + // Remove all the gutter markers and line classes from the editor. + this._cleanEditor(type); + }, + + /** + * Recompiles the source code for the shader being edited. + * This function is fired at a certain delay after the user stops typing. + * + * @param string type + * The corresponding shader type for the focused editor (e.g. "vs"). + */ + _doCompile: function (type) { + Task.spawn(function* () { + let editor = yield this._getEditor(type); + let shaderActor = yield ShadersListView.selectedAttachment[type]; + + try { + yield shaderActor.compile(editor.getText()); + this._onSuccessfulCompilation(); + } catch (e) { + this._onFailedCompilation(type, editor, e); + } + }.bind(this)); + }, + + /** + * Called uppon a successful shader compilation. + */ + _onSuccessfulCompilation: function () { + // Signal that the shader was compiled successfully. + window.emit(EVENTS.SHADER_COMPILED, null); + }, + + /** + * Called uppon an unsuccessful shader compilation. + */ + _onFailedCompilation: function (type, editor, errors) { + let lineCount = editor.lineCount(); + let currentLine = editor.getCursor().line; + let listeners = { mouseover: this._onMarkerMouseOver }; + + function matchLinesAndMessages(string) { + return { + // First number that is not equal to 0. + lineMatch: string.match(/\d{2,}|[1-9]/), + // The string after all the numbers, semicolons and spaces. + textMatch: string.match(/[^\s\d:][^\r\n|]*/) + }; + } + function discardInvalidMatches(e) { + // Discard empty line and text matches. + return e.lineMatch && e.textMatch; + } + function sanitizeValidMatches(e) { + return { + // Drivers might yield confusing line numbers under some obscure + // circumstances. Don't throw the errors away in those cases, + // just display them on the currently edited line. + line: e.lineMatch[0] > lineCount ? currentLine : e.lineMatch[0] - 1, + // Trim whitespace from the beginning and the end of the message, + // and replace all other occurences of double spaces to a single space. + text: e.textMatch[0].trim().replace(/\s{2,}/g, " ") + }; + } + function sortByLine(first, second) { + // Sort all the errors ascending by their corresponding line number. + return first.line > second.line ? 1 : -1; + } + function groupSameLineMessages(accumulator, current) { + // Group errors corresponding to the same line number to a single object. + let previous = accumulator[accumulator.length - 1]; + if (!previous || previous.line != current.line) { + return [...accumulator, { + line: current.line, + messages: [current.text] + }]; + } else { + previous.messages.push(current.text); + return accumulator; + } + } + function displayErrors({ line, messages }) { + // Add gutter markers and line classes for every error in the source. + editor.addMarker(line, "errors", "error"); + editor.setMarkerListeners(line, "errors", "error", listeners, messages); + editor.addLineClass(line, "error-line"); + } + + (this._errors[type] = errors.link + .split("ERROR") + .map(matchLinesAndMessages) + .filter(discardInvalidMatches) + .map(sanitizeValidMatches) + .sort(sortByLine) + .reduce(groupSameLineMessages, [])) + .forEach(displayErrors); + + // Signal that the shader wasn't compiled successfully. + window.emit(EVENTS.SHADER_COMPILED, errors); + }, + + /** + * Event listener for the 'mouseover' event on a marker in the editor gutter. + */ + _onMarkerMouseOver: function (line, node, messages) { + if (node._markerErrorsTooltip) { + return; + } + + let tooltip = node._markerErrorsTooltip = new Tooltip(document); + tooltip.defaultOffsetX = GUTTER_ERROR_PANEL_OFFSET_X; + tooltip.setTextContent({ messages: messages }); + tooltip.startTogglingOnHover(node, () => true, { + toggleDelay: GUTTER_ERROR_PANEL_DELAY + }); + }, + + /** + * Removes all the gutter markers and line classes from the editor. + */ + _cleanEditor: function (type) { + this._getEditor(type).then(editor => { + editor.removeAllMarkers("errors"); + this._errors[type].forEach(e => editor.removeLineClass(e.line)); + this._errors[type].length = 0; + window.emit(EVENTS.EDITOR_ERROR_MARKERS_REMOVED); + }); + }, + + _errors: { + vs: [], + fs: [] + } +}; + +/** + * Localization convenience methods. + */ +var L10N = new LocalizationHelper(STRINGS_URI); + +/** + * Convenient way of emitting events from the panel window. + */ +EventEmitter.decorate(this); + +/** + * DOM query helper. + */ +var $ = (selector, target = document) => target.querySelector(selector); diff --git a/devtools/client/shadereditor/shadereditor.xul b/devtools/client/shadereditor/shadereditor.xul new file mode 100644 index 000000000..dc7f764b7 --- /dev/null +++ b/devtools/client/shadereditor/shadereditor.xul @@ -0,0 +1,70 @@ + + + + + + + + %debuggerDTD; +]> + + + + + + + + + + + + + + + + + diff --git a/devtools/client/shadereditor/test/doc_multiple-contexts.html b/devtools/client/shadereditor/test/doc_multiple-contexts.html new file mode 100644 index 000000000..039ee62d0 --- /dev/null +++ b/devtools/client/shadereditor/test/doc_multiple-contexts.html @@ -0,0 +1,112 @@ + + + + + + + WebGL editor test page + + + + + + + + + + + + + + diff --git a/devtools/client/shadereditor/test/doc_overlapping-geometry.html b/devtools/client/shadereditor/test/doc_overlapping-geometry.html new file mode 100644 index 000000000..34be8f57a --- /dev/null +++ b/devtools/client/shadereditor/test/doc_overlapping-geometry.html @@ -0,0 +1,120 @@ + + + + + + + WebGL editor test page + + + + + + + + + + + + + + + diff --git a/devtools/client/shadereditor/test/doc_shader-order.html b/devtools/client/shadereditor/test/doc_shader-order.html new file mode 100644 index 000000000..a7cec53aa --- /dev/null +++ b/devtools/client/shadereditor/test/doc_shader-order.html @@ -0,0 +1,83 @@ + + + + + + + WebGL editor test page + + + + + + + + + + + + + diff --git a/devtools/client/shadereditor/test/doc_simple-canvas.html b/devtools/client/shadereditor/test/doc_simple-canvas.html new file mode 100644 index 000000000..2a709ad8e --- /dev/null +++ b/devtools/client/shadereditor/test/doc_simple-canvas.html @@ -0,0 +1,125 @@ + + + + + + + WebGL editor test page + + + + + + + + + + + + + diff --git a/devtools/client/shadereditor/test/head.js b/devtools/client/shadereditor/test/head.js new file mode 100644 index 000000000..754a0605d --- /dev/null +++ b/devtools/client/shadereditor/test/head.js @@ -0,0 +1,292 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +var { Task } = require("devtools/shared/task"); + +var Services = require("Services"); +var promise = require("promise"); +var { gDevTools } = require("devtools/client/framework/devtools"); +var { DebuggerClient } = require("devtools/shared/client/main"); +var { DebuggerServer } = require("devtools/server/main"); +var { WebGLFront } = require("devtools/shared/fronts/webgl"); +var DevToolsUtils = require("devtools/shared/DevToolsUtils"); +var flags = require("devtools/shared/flags"); +var { TargetFactory } = require("devtools/client/framework/target"); +var { Toolbox } = require("devtools/client/framework/toolbox"); +var { isWebGLSupported } = require("devtools/client/shared/webgl-utils"); +var mm = null; + +const FRAME_SCRIPT_UTILS_URL = "chrome://devtools/content/shared/frame-script-utils.js"; +const EXAMPLE_URL = "http://example.com/browser/devtools/client/shadereditor/test/"; +const SIMPLE_CANVAS_URL = EXAMPLE_URL + "doc_simple-canvas.html"; +const SHADER_ORDER_URL = EXAMPLE_URL + "doc_shader-order.html"; +const MULTIPLE_CONTEXTS_URL = EXAMPLE_URL + "doc_multiple-contexts.html"; +const OVERLAPPING_GEOMETRY_CANVAS_URL = EXAMPLE_URL + "doc_overlapping-geometry.html"; +const BLENDED_GEOMETRY_CANVAS_URL = EXAMPLE_URL + "doc_blended-geometry.html"; + +var gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); +// To enable logging for try runs, just set the pref to true. +Services.prefs.setBoolPref("devtools.debugger.log", false); + +// All tests are asynchronous. +waitForExplicitFinish(); + +var gToolEnabled = Services.prefs.getBoolPref("devtools.shadereditor.enabled"); + +flags.testing = true; + +registerCleanupFunction(() => { + info("finish() was called, cleaning up..."); + flags.testing = false; + Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); + Services.prefs.setBoolPref("devtools.shadereditor.enabled", gToolEnabled); + + // These tests use a lot of memory due to GL contexts, so force a GC to help + // fragmentation. + info("Forcing GC after shadereditor test."); + Cu.forceGC(); +}); + +/** + * Call manually in tests that use frame script utils after initializing + * the shader editor. Must be called after initializing so we can detect + * whether or not `content` is a CPOW or not. Call after init but before navigating + * to different pages, as bfcache and thus shader caching gets really strange if + * frame script attached in the middle of the test. + */ +function loadFrameScripts() { + if (Cu.isCrossProcessWrapper(content)) { + mm = gBrowser.selectedBrowser.messageManager; + mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false); + } +} + +function addTab(aUrl, aWindow) { + info("Adding tab: " + aUrl); + + let deferred = promise.defer(); + let targetWindow = aWindow || window; + let targetBrowser = targetWindow.gBrowser; + + targetWindow.focus(); + let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl); + let linkedBrowser = tab.linkedBrowser; + + BrowserTestUtils.browserLoaded(linkedBrowser).then(function () { + info("Tab added and finished loading: " + aUrl); + deferred.resolve(tab); + }); + + return deferred.promise; +} + +function removeTab(aTab, aWindow) { + info("Removing tab."); + + let deferred = promise.defer(); + let targetWindow = aWindow || window; + let targetBrowser = targetWindow.gBrowser; + let tabContainer = targetBrowser.tabContainer; + + tabContainer.addEventListener("TabClose", function onClose(aEvent) { + tabContainer.removeEventListener("TabClose", onClose, false); + info("Tab removed and finished closing."); + deferred.resolve(); + }, false); + + targetBrowser.removeTab(aTab); + return deferred.promise; +} + +function handleError(aError) { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + finish(); +} + +function ifWebGLSupported() { + ok(false, "You need to define a 'ifWebGLSupported' function."); + finish(); +} + +function ifWebGLUnsupported() { + todo(false, "Skipping test because WebGL isn't supported."); + finish(); +} + +function test() { + let generator = isWebGLSupported(document) ? ifWebGLSupported : ifWebGLUnsupported; + Task.spawn(generator).then(null, handleError); +} + +function createCanvas() { + return document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); +} + +function once(aTarget, aEventName, aUseCapture = false) { + info("Waiting for event: '" + aEventName + "' on " + aTarget + "."); + + let deferred = promise.defer(); + + for (let [add, remove] of [ + ["on", "off"], // Use event emitter before DOM events for consistency + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"] + ]) { + if ((add in aTarget) && (remove in aTarget)) { + aTarget[add](aEventName, function onEvent(...aArgs) { + aTarget[remove](aEventName, onEvent, aUseCapture); + deferred.resolve(...aArgs); + }, aUseCapture); + break; + } + } + + return deferred.promise; +} + +// Hack around `once`, as that only resolves to a single (first) argument +// and discards the rest. `onceSpread` is similar, except resolves to an +// array of all of the arguments in the handler. These should be consolidated +// into the same function, but many tests will need to be changed. +function onceSpread(aTarget, aEvent) { + let deferred = promise.defer(); + aTarget.once(aEvent, (...args) => deferred.resolve(args)); + return deferred.promise; +} + +function observe(aNotificationName, aOwnsWeak = false) { + info("Waiting for observer notification: '" + aNotificationName + "."); + + let deferred = promise.defer(); + + Services.obs.addObserver(function onNotification(...aArgs) { + Services.obs.removeObserver(onNotification, aNotificationName); + deferred.resolve.apply(deferred, aArgs); + }, aNotificationName, aOwnsWeak); + + return deferred.promise; +} + +function isApprox(aFirst, aSecond, aMargin = 1) { + return Math.abs(aFirst - aSecond) <= aMargin; +} + +function isApproxColor(aFirst, aSecond, aMargin) { + return isApprox(aFirst.r, aSecond.r, aMargin) && + isApprox(aFirst.g, aSecond.g, aMargin) && + isApprox(aFirst.b, aSecond.b, aMargin) && + isApprox(aFirst.a, aSecond.a, aMargin); +} + +function ensurePixelIs(aFront, aPosition, aColor, aWaitFlag = false, aSelector = "canvas") { + return Task.spawn(function* () { + let pixel = yield aFront.getPixel({ selector: aSelector, position: aPosition }); + if (isApproxColor(pixel, aColor)) { + ok(true, "Expected pixel is shown at: " + aPosition.toSource()); + return; + } + + if (aWaitFlag) { + yield aFront.waitForFrame(); + return ensurePixelIs(aFront, aPosition, aColor, aWaitFlag, aSelector); + } + + ok(false, "Expected pixel was not already shown at: " + aPosition.toSource()); + throw new Error("Expected pixel was not already shown at: " + aPosition.toSource()); + }); +} + +function navigateInHistory(aTarget, aDirection, aWaitForTargetEvent = "navigate") { + if (Cu.isCrossProcessWrapper(content)) { + if (!mm) { + throw new Error("`loadFrameScripts()` must be called before attempting to navigate in e10s."); + } + mm.sendAsyncMessage("devtools:test:history", { direction: aDirection }); + } + else { + executeSoon(() => content.history[aDirection]()); + } + return once(aTarget, aWaitForTargetEvent); +} + +function navigate(aTarget, aUrl, aWaitForTargetEvent = "navigate") { + executeSoon(() => aTarget.activeTab.navigateTo(aUrl)); + return once(aTarget, aWaitForTargetEvent); +} + +function reload(aTarget, aWaitForTargetEvent = "navigate") { + executeSoon(() => aTarget.activeTab.reload()); + return once(aTarget, aWaitForTargetEvent); +} + +function initBackend(aUrl) { + info("Initializing a shader editor front."); + + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + return Task.spawn(function* () { + let tab = yield addTab(aUrl); + let target = TargetFactory.forTab(tab); + + yield target.makeRemote(); + + let front = new WebGLFront(target.client, target.form); + return { target, front }; + }); +} + +function initShaderEditor(aUrl) { + info("Initializing a shader editor pane."); + + return Task.spawn(function* () { + let tab = yield addTab(aUrl); + let target = TargetFactory.forTab(tab); + + yield target.makeRemote(); + + Services.prefs.setBoolPref("devtools.shadereditor.enabled", true); + let toolbox = yield gDevTools.showToolbox(target, "shadereditor"); + let panel = toolbox.getCurrentPanel(); + return { target, panel }; + }); +} + +function teardown(aPanel) { + info("Destroying the specified shader editor."); + + return promise.all([ + once(aPanel, "destroyed"), + removeTab(aPanel.target.tab) + ]); +} + +// Due to `program-linked` events firing synchronously, we cannot +// just yield/chain them together, as then we miss all actors after the +// first event since they're fired consecutively. This allows us to capture +// all actors and returns an array containing them. +// +// Takes a `front` object that is an event emitter, the number of +// programs that should be listened to and waited on, and an optional +// `onAdd` function that calls with the entire actors array on program link +function getPrograms(front, count, onAdd) { + let actors = []; + let deferred = promise.defer(); + front.on("program-linked", function onLink(actor) { + if (actors.length !== count) { + actors.push(actor); + if (typeof onAdd === "function") onAdd(actors); + } + if (actors.length === count) { + front.off("program-linked", onLink); + deferred.resolve(actors); + } + }); + return deferred.promise; +} -- cgit v1.2.3