shadereditor.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  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 file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
  6. const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
  7. const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
  8. const {SideMenuWidget} = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
  9. const promise = require("promise");
  10. const Services = require("Services");
  11. const EventEmitter = require("devtools/shared/event-emitter");
  12. const Tooltip = require("devtools/client/shared/widgets/tooltip/Tooltip");
  13. const Editor = require("devtools/client/sourceeditor/editor");
  14. const {LocalizationHelper} = require("devtools/shared/l10n");
  15. const {Heritage, WidgetMethods, setNamedTimeout} =
  16. require("devtools/client/shared/widgets/view-helpers");
  17. const {Task} = require("devtools/shared/task");
  18. // The panel's window global is an EventEmitter firing the following events:
  19. const EVENTS = {
  20. // When new programs are received from the server.
  21. NEW_PROGRAM: "ShaderEditor:NewProgram",
  22. PROGRAMS_ADDED: "ShaderEditor:ProgramsAdded",
  23. // When the vertex and fragment sources were shown in the editor.
  24. SOURCES_SHOWN: "ShaderEditor:SourcesShown",
  25. // When a shader's source was edited and compiled via the editor.
  26. SHADER_COMPILED: "ShaderEditor:ShaderCompiled",
  27. // When the UI is reset from tab navigation
  28. UI_RESET: "ShaderEditor:UIReset",
  29. // When the editor's error markers are all removed
  30. EDITOR_ERROR_MARKERS_REMOVED: "ShaderEditor:EditorCleaned"
  31. };
  32. XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
  33. const STRINGS_URI = "devtools/client/locales/shadereditor.properties";
  34. const HIGHLIGHT_TINT = [1, 0, 0.25, 1]; // rgba
  35. const TYPING_MAX_DELAY = 500; // ms
  36. const SHADERS_AUTOGROW_ITEMS = 4;
  37. const GUTTER_ERROR_PANEL_OFFSET_X = 7; // px
  38. const GUTTER_ERROR_PANEL_DELAY = 100; // ms
  39. const DEFAULT_EDITOR_CONFIG = {
  40. gutters: ["errors"],
  41. lineNumbers: true,
  42. showAnnotationRuler: true
  43. };
  44. /**
  45. * The current target and the WebGL Editor front, set by this tool's host.
  46. */
  47. var gToolbox, gTarget, gFront;
  48. /**
  49. * Initializes the shader editor controller and views.
  50. */
  51. function startupShaderEditor() {
  52. return promise.all([
  53. EventsHandler.initialize(),
  54. ShadersListView.initialize(),
  55. ShadersEditorsView.initialize()
  56. ]);
  57. }
  58. /**
  59. * Destroys the shader editor controller and views.
  60. */
  61. function shutdownShaderEditor() {
  62. return promise.all([
  63. EventsHandler.destroy(),
  64. ShadersListView.destroy(),
  65. ShadersEditorsView.destroy()
  66. ]);
  67. }
  68. /**
  69. * Functions handling target-related lifetime events.
  70. */
  71. var EventsHandler = {
  72. /**
  73. * Listen for events emitted by the current tab target.
  74. */
  75. initialize: function () {
  76. this._onHostChanged = this._onHostChanged.bind(this);
  77. this._onTabNavigated = this._onTabNavigated.bind(this);
  78. this._onProgramLinked = this._onProgramLinked.bind(this);
  79. this._onProgramsAdded = this._onProgramsAdded.bind(this);
  80. gToolbox.on("host-changed", this._onHostChanged);
  81. gTarget.on("will-navigate", this._onTabNavigated);
  82. gTarget.on("navigate", this._onTabNavigated);
  83. gFront.on("program-linked", this._onProgramLinked);
  84. this.reloadButton = $("#requests-menu-reload-notice-button");
  85. this.reloadButton.addEventListener("command", this._onReloadCommand);
  86. },
  87. /**
  88. * Remove events emitted by the current tab target.
  89. */
  90. destroy: function () {
  91. gToolbox.off("host-changed", this._onHostChanged);
  92. gTarget.off("will-navigate", this._onTabNavigated);
  93. gTarget.off("navigate", this._onTabNavigated);
  94. gFront.off("program-linked", this._onProgramLinked);
  95. this.reloadButton.removeEventListener("command", this._onReloadCommand);
  96. },
  97. /**
  98. * Handles a command event on reload button
  99. */
  100. _onReloadCommand() {
  101. gFront.setup({ reload: true });
  102. },
  103. /**
  104. * Handles a host change event on the parent toolbox.
  105. */
  106. _onHostChanged: function () {
  107. if (gToolbox.hostType == "side") {
  108. $("#shaders-pane").removeAttribute("height");
  109. }
  110. },
  111. /**
  112. * Called for each location change in the debugged tab.
  113. */
  114. _onTabNavigated: function (event, {isFrameSwitching}) {
  115. switch (event) {
  116. case "will-navigate": {
  117. // Make sure the backend is prepared to handle WebGL contexts.
  118. if (!isFrameSwitching) {
  119. gFront.setup({ reload: false });
  120. }
  121. // Reset UI.
  122. ShadersListView.empty();
  123. // When switching to an iframe, ensure displaying the reload button.
  124. // As the document has already been loaded without being hooked.
  125. if (isFrameSwitching) {
  126. $("#reload-notice").hidden = false;
  127. $("#waiting-notice").hidden = true;
  128. } else {
  129. $("#reload-notice").hidden = true;
  130. $("#waiting-notice").hidden = false;
  131. }
  132. $("#content").hidden = true;
  133. window.emit(EVENTS.UI_RESET);
  134. break;
  135. }
  136. case "navigate": {
  137. // Manually retrieve the list of program actors known to the server,
  138. // because the backend won't emit "program-linked" notifications
  139. // in the case of a bfcache navigation (since no new programs are
  140. // actually linked).
  141. gFront.getPrograms().then(this._onProgramsAdded);
  142. break;
  143. }
  144. }
  145. },
  146. /**
  147. * Called every time a program was linked in the debugged tab.
  148. */
  149. _onProgramLinked: function (programActor) {
  150. this._addProgram(programActor);
  151. window.emit(EVENTS.NEW_PROGRAM);
  152. },
  153. /**
  154. * Callback for the front's getPrograms() method.
  155. */
  156. _onProgramsAdded: function (programActors) {
  157. programActors.forEach(this._addProgram);
  158. window.emit(EVENTS.PROGRAMS_ADDED);
  159. },
  160. /**
  161. * Adds a program to the shaders list and unhides any modal notices.
  162. */
  163. _addProgram: function (programActor) {
  164. $("#waiting-notice").hidden = true;
  165. $("#reload-notice").hidden = true;
  166. $("#content").hidden = false;
  167. ShadersListView.addProgram(programActor);
  168. }
  169. };
  170. /**
  171. * Functions handling the sources UI.
  172. */
  173. var ShadersListView = Heritage.extend(WidgetMethods, {
  174. /**
  175. * Initialization function, called when the tool is started.
  176. */
  177. initialize: function () {
  178. this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), {
  179. showArrows: true,
  180. showItemCheckboxes: true
  181. });
  182. this._onProgramSelect = this._onProgramSelect.bind(this);
  183. this._onProgramCheck = this._onProgramCheck.bind(this);
  184. this._onProgramMouseOver = this._onProgramMouseOver.bind(this);
  185. this._onProgramMouseOut = this._onProgramMouseOut.bind(this);
  186. this.widget.addEventListener("select", this._onProgramSelect, false);
  187. this.widget.addEventListener("check", this._onProgramCheck, false);
  188. this.widget.addEventListener("mouseover", this._onProgramMouseOver, true);
  189. this.widget.addEventListener("mouseout", this._onProgramMouseOut, true);
  190. },
  191. /**
  192. * Destruction function, called when the tool is closed.
  193. */
  194. destroy: function () {
  195. this.widget.removeEventListener("select", this._onProgramSelect, false);
  196. this.widget.removeEventListener("check", this._onProgramCheck, false);
  197. this.widget.removeEventListener("mouseover", this._onProgramMouseOver, true);
  198. this.widget.removeEventListener("mouseout", this._onProgramMouseOut, true);
  199. },
  200. /**
  201. * Adds a program to this programs container.
  202. *
  203. * @param object programActor
  204. * The program actor coming from the active thread.
  205. */
  206. addProgram: function (programActor) {
  207. if (this.hasProgram(programActor)) {
  208. return;
  209. }
  210. // Currently, there's no good way of differentiating between programs
  211. // in a way that helps humans. It will be a good idea to implement a
  212. // standard of allowing debuggees to add some identifiable metadata to their
  213. // program sources or instances.
  214. let label = L10N.getFormatStr("shadersList.programLabel", this.itemCount);
  215. let contents = document.createElement("label");
  216. contents.className = "plain program-item";
  217. contents.setAttribute("value", label);
  218. contents.setAttribute("crop", "start");
  219. contents.setAttribute("flex", "1");
  220. // Append a program item to this container.
  221. this.push([contents], {
  222. index: -1, /* specifies on which position should the item be appended */
  223. attachment: {
  224. label: label,
  225. programActor: programActor,
  226. checkboxState: true,
  227. checkboxTooltip: L10N.getStr("shadersList.blackboxLabel")
  228. }
  229. });
  230. // Make sure there's always a selected item available.
  231. if (!this.selectedItem) {
  232. this.selectedIndex = 0;
  233. }
  234. // Prevent this container from growing indefinitely in height when the
  235. // toolbox is docked to the side.
  236. if (gToolbox.hostType == "side" && this.itemCount == SHADERS_AUTOGROW_ITEMS) {
  237. this._pane.setAttribute("height", this._pane.getBoundingClientRect().height);
  238. }
  239. },
  240. /**
  241. * Returns whether a program was already added to this programs container.
  242. *
  243. * @param object programActor
  244. * The program actor coming from the active thread.
  245. * @param boolean
  246. * True if the program was added, false otherwise.
  247. */
  248. hasProgram: function (programActor) {
  249. return !!this.attachments.filter(e => e.programActor == programActor).length;
  250. },
  251. /**
  252. * The select listener for the programs container.
  253. */
  254. _onProgramSelect: function ({ detail: sourceItem }) {
  255. if (!sourceItem) {
  256. return;
  257. }
  258. // The container is not empty and an actual item was selected.
  259. let attachment = sourceItem.attachment;
  260. function getShaders() {
  261. return promise.all([
  262. attachment.vs || (attachment.vs = attachment.programActor.getVertexShader()),
  263. attachment.fs || (attachment.fs = attachment.programActor.getFragmentShader())
  264. ]);
  265. }
  266. function getSources([vertexShaderActor, fragmentShaderActor]) {
  267. return promise.all([
  268. vertexShaderActor.getText(),
  269. fragmentShaderActor.getText()
  270. ]);
  271. }
  272. function showSources([vertexShaderText, fragmentShaderText]) {
  273. return ShadersEditorsView.setText({
  274. vs: vertexShaderText,
  275. fs: fragmentShaderText
  276. });
  277. }
  278. getShaders()
  279. .then(getSources)
  280. .then(showSources)
  281. .then(null, e => console.error(e));
  282. },
  283. /**
  284. * The check listener for the programs container.
  285. */
  286. _onProgramCheck: function ({ detail: { checked }, target }) {
  287. let sourceItem = this.getItemForElement(target);
  288. let attachment = sourceItem.attachment;
  289. attachment.isBlackBoxed = !checked;
  290. attachment.programActor[checked ? "unblackbox" : "blackbox"]();
  291. },
  292. /**
  293. * The mouseover listener for the programs container.
  294. */
  295. _onProgramMouseOver: function (e) {
  296. let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
  297. if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
  298. sourceItem.attachment.programActor.highlight(HIGHLIGHT_TINT);
  299. if (e instanceof Event) {
  300. e.preventDefault();
  301. e.stopPropagation();
  302. }
  303. }
  304. },
  305. /**
  306. * The mouseout listener for the programs container.
  307. */
  308. _onProgramMouseOut: function (e) {
  309. let sourceItem = this.getItemForElement(e.target, { noSiblings: true });
  310. if (sourceItem && !sourceItem.attachment.isBlackBoxed) {
  311. sourceItem.attachment.programActor.unhighlight();
  312. if (e instanceof Event) {
  313. e.preventDefault();
  314. e.stopPropagation();
  315. }
  316. }
  317. }
  318. });
  319. /**
  320. * Functions handling the editors displaying the vertex and fragment shaders.
  321. */
  322. var ShadersEditorsView = {
  323. /**
  324. * Initialization function, called when the tool is started.
  325. */
  326. initialize: function () {
  327. XPCOMUtils.defineLazyGetter(this, "_editorPromises", () => new Map());
  328. this._vsFocused = this._onFocused.bind(this, "vs", "fs");
  329. this._fsFocused = this._onFocused.bind(this, "fs", "vs");
  330. this._vsChanged = this._onChanged.bind(this, "vs");
  331. this._fsChanged = this._onChanged.bind(this, "fs");
  332. },
  333. /**
  334. * Destruction function, called when the tool is closed.
  335. */
  336. destroy: Task.async(function* () {
  337. this._destroyed = true;
  338. yield this._toggleListeners("off");
  339. for (let p of this._editorPromises.values()) {
  340. let editor = yield p;
  341. editor.destroy();
  342. }
  343. }),
  344. /**
  345. * Sets the text displayed in the vertex and fragment shader editors.
  346. *
  347. * @param object sources
  348. * An object containing the following properties
  349. * - vs: the vertex shader source code
  350. * - fs: the fragment shader source code
  351. * @return object
  352. * A promise resolving upon completion of text setting.
  353. */
  354. setText: function (sources) {
  355. let view = this;
  356. function setTextAndClearHistory(editor, text) {
  357. editor.setText(text);
  358. editor.clearHistory();
  359. }
  360. return Task.spawn(function* () {
  361. yield view._toggleListeners("off");
  362. yield promise.all([
  363. view._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)),
  364. view._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs))
  365. ]);
  366. yield view._toggleListeners("on");
  367. }).then(() => window.emit(EVENTS.SOURCES_SHOWN, sources));
  368. },
  369. /**
  370. * Lazily initializes and returns a promise for an Editor instance.
  371. *
  372. * @param string type
  373. * Specifies for which shader type should an editor be retrieved,
  374. * either are "vs" for a vertex, or "fs" for a fragment shader.
  375. * @return object
  376. * Returns a promise that resolves to an editor instance
  377. */
  378. _getEditor: function (type) {
  379. if (this._editorPromises.has(type)) {
  380. return this._editorPromises.get(type);
  381. }
  382. let deferred = promise.defer();
  383. this._editorPromises.set(type, deferred.promise);
  384. // Initialize the source editor and store the newly created instance
  385. // in the ether of a resolved promise's value.
  386. let parent = $("#" + type + "-editor");
  387. let editor = new Editor(DEFAULT_EDITOR_CONFIG);
  388. editor.config.mode = Editor.modes[type];
  389. if (this._destroyed) {
  390. deferred.resolve(editor);
  391. } else {
  392. editor.appendTo(parent).then(() => deferred.resolve(editor));
  393. }
  394. return deferred.promise;
  395. },
  396. /**
  397. * Toggles all the event listeners for the editors either on or off.
  398. *
  399. * @param string flag
  400. * Either "on" to enable the event listeners, "off" to disable them.
  401. * @return object
  402. * A promise resolving upon completion of toggling the listeners.
  403. */
  404. _toggleListeners: function (flag) {
  405. return promise.all(["vs", "fs"].map(type => {
  406. return this._getEditor(type).then(editor => {
  407. editor[flag]("focus", this["_" + type + "Focused"]);
  408. editor[flag]("change", this["_" + type + "Changed"]);
  409. });
  410. }));
  411. },
  412. /**
  413. * The focus listener for a source editor.
  414. *
  415. * @param string focused
  416. * The corresponding shader type for the focused editor (e.g. "vs").
  417. * @param string focused
  418. * The corresponding shader type for the other editor (e.g. "fs").
  419. */
  420. _onFocused: function (focused, unfocused) {
  421. $("#" + focused + "-editor-label").setAttribute("selected", "");
  422. $("#" + unfocused + "-editor-label").removeAttribute("selected");
  423. },
  424. /**
  425. * The change listener for a source editor.
  426. *
  427. * @param string type
  428. * The corresponding shader type for the focused editor (e.g. "vs").
  429. */
  430. _onChanged: function (type) {
  431. setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type));
  432. // Remove all the gutter markers and line classes from the editor.
  433. this._cleanEditor(type);
  434. },
  435. /**
  436. * Recompiles the source code for the shader being edited.
  437. * This function is fired at a certain delay after the user stops typing.
  438. *
  439. * @param string type
  440. * The corresponding shader type for the focused editor (e.g. "vs").
  441. */
  442. _doCompile: function (type) {
  443. Task.spawn(function* () {
  444. let editor = yield this._getEditor(type);
  445. let shaderActor = yield ShadersListView.selectedAttachment[type];
  446. try {
  447. yield shaderActor.compile(editor.getText());
  448. this._onSuccessfulCompilation();
  449. } catch (e) {
  450. this._onFailedCompilation(type, editor, e);
  451. }
  452. }.bind(this));
  453. },
  454. /**
  455. * Called uppon a successful shader compilation.
  456. */
  457. _onSuccessfulCompilation: function () {
  458. // Signal that the shader was compiled successfully.
  459. window.emit(EVENTS.SHADER_COMPILED, null);
  460. },
  461. /**
  462. * Called uppon an unsuccessful shader compilation.
  463. */
  464. _onFailedCompilation: function (type, editor, errors) {
  465. let lineCount = editor.lineCount();
  466. let currentLine = editor.getCursor().line;
  467. let listeners = { mouseover: this._onMarkerMouseOver };
  468. function matchLinesAndMessages(string) {
  469. return {
  470. // First number that is not equal to 0.
  471. lineMatch: string.match(/\d{2,}|[1-9]/),
  472. // The string after all the numbers, semicolons and spaces.
  473. textMatch: string.match(/[^\s\d:][^\r\n|]*/)
  474. };
  475. }
  476. function discardInvalidMatches(e) {
  477. // Discard empty line and text matches.
  478. return e.lineMatch && e.textMatch;
  479. }
  480. function sanitizeValidMatches(e) {
  481. return {
  482. // Drivers might yield confusing line numbers under some obscure
  483. // circumstances. Don't throw the errors away in those cases,
  484. // just display them on the currently edited line.
  485. line: e.lineMatch[0] > lineCount ? currentLine : e.lineMatch[0] - 1,
  486. // Trim whitespace from the beginning and the end of the message,
  487. // and replace all other occurences of double spaces to a single space.
  488. text: e.textMatch[0].trim().replace(/\s{2,}/g, " ")
  489. };
  490. }
  491. function sortByLine(first, second) {
  492. // Sort all the errors ascending by their corresponding line number.
  493. return first.line > second.line ? 1 : -1;
  494. }
  495. function groupSameLineMessages(accumulator, current) {
  496. // Group errors corresponding to the same line number to a single object.
  497. let previous = accumulator[accumulator.length - 1];
  498. if (!previous || previous.line != current.line) {
  499. return [...accumulator, {
  500. line: current.line,
  501. messages: [current.text]
  502. }];
  503. } else {
  504. previous.messages.push(current.text);
  505. return accumulator;
  506. }
  507. }
  508. function displayErrors({ line, messages }) {
  509. // Add gutter markers and line classes for every error in the source.
  510. editor.addMarker(line, "errors", "error");
  511. editor.setMarkerListeners(line, "errors", "error", listeners, messages);
  512. editor.addLineClass(line, "error-line");
  513. }
  514. (this._errors[type] = errors.link
  515. .split("ERROR")
  516. .map(matchLinesAndMessages)
  517. .filter(discardInvalidMatches)
  518. .map(sanitizeValidMatches)
  519. .sort(sortByLine)
  520. .reduce(groupSameLineMessages, []))
  521. .forEach(displayErrors);
  522. // Signal that the shader wasn't compiled successfully.
  523. window.emit(EVENTS.SHADER_COMPILED, errors);
  524. },
  525. /**
  526. * Event listener for the 'mouseover' event on a marker in the editor gutter.
  527. */
  528. _onMarkerMouseOver: function (line, node, messages) {
  529. if (node._markerErrorsTooltip) {
  530. return;
  531. }
  532. let tooltip = node._markerErrorsTooltip = new Tooltip(document);
  533. tooltip.defaultOffsetX = GUTTER_ERROR_PANEL_OFFSET_X;
  534. tooltip.setTextContent({ messages: messages });
  535. tooltip.startTogglingOnHover(node, () => true, {
  536. toggleDelay: GUTTER_ERROR_PANEL_DELAY
  537. });
  538. },
  539. /**
  540. * Removes all the gutter markers and line classes from the editor.
  541. */
  542. _cleanEditor: function (type) {
  543. this._getEditor(type).then(editor => {
  544. editor.removeAllMarkers("errors");
  545. this._errors[type].forEach(e => editor.removeLineClass(e.line));
  546. this._errors[type].length = 0;
  547. window.emit(EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
  548. });
  549. },
  550. _errors: {
  551. vs: [],
  552. fs: []
  553. }
  554. };
  555. /**
  556. * Localization convenience methods.
  557. */
  558. var L10N = new LocalizationHelper(STRINGS_URI);
  559. /**
  560. * Convenient way of emitting events from the panel window.
  561. */
  562. EventEmitter.decorate(this);
  563. /**
  564. * DOM query helper.
  565. */
  566. var $ = (selector, target = document) => target.querySelector(selector);