Source/WebInspectorUI/ChangeLog

 12018-11-04 Devin Rousso <drousso@apple.com>
 2
 3 Web Inspector: show save/restore stack for recorded 2D Canvases
 4 https://bugs.webkit.org/show_bug.cgi?id=175283
 5 <rdar://problem/34040756>
 6
 7 Reviewed by NOBODY (OOPS!).
 8
 9 Instead of using plain objects for holding the `WI.Recording`'s state, use a model object
 10 so that more data can be passed around. When visualizing the state, show any previously
 11 saved states in a `WI.DetailsSection` underneath the current state. If there are no saved
 12 states (meaning there is only the current state), don't use a `WI.DetailsSection`.
 13
 14 * UserInterface/Models/RecordingState.js: Added.
 15 (WI.RecordingState):
 16 (WI.RecordingState.fromContext):
 17 (WI.RecordingState.async swizzleInitialState):
 18 (WI.RecordingState.prototype.get source):
 19 (WI.RecordingState.prototype.has):
 20 (WI.RecordingState.prototype.get return):
 21 (WI.RecordingState.prototype.toJSON):
 22 (WI.RecordingState.prototype.[Symbol.iterator]):
 23
 24 * UserInterface/Models/Recording.js:
 25 (WI.Recording.prototype.async _process):
 26 (WI.Recording.prototype.async _swizzleState): Deleted.
 27
 28 * UserInterface/Models/RecordingAction.js:
 29 (WI.RecordingAction.prototype.process):
 30 (WI.RecordingAction.deriveCurrentState): Deleted.
 31
 32 * UserInterface/Views/RecordingContentView.js:
 33 (WI.RecordingContentView.prototype._generateContentCanvas2D):
 34
 35 * UserInterface/Views/RecordingStateDetailsSidebarPanel.js:
 36 (WI.RecordingStateDetailsSidebarPanel):
 37 (WI.RecordingStateDetailsSidebarPanel.prototype.get scrollElement):
 38 (WI.RecordingStateDetailsSidebarPanel.prototype.sizeDidChange): Added.
 39 (WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D):
 40 (WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D.isColorProperty): Deleted.
 41 (WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D.createInlineSwatch): Deleted.
 42 * UserInterface/Views/RecordingStateDetailsSidebarPanel.css:
 43 (.sidebar > .panel.details.recording-state > .content .details-section > .header .source): Added.
 44 (.sidebar > .panel.details.recording-state > .content .data-grid tr.modified): Added.
 45 (.sidebar > .panel.details.recording-state > .content .data-grid tr:not(.selected).non-standard .name-column): Added.
 46 (.sidebar > .panel.details.recording-state > .content .data-grid tr:not(.selected) .unavailable): Added.
 47 (.sidebar > .panel.details.recording-state > .content .data-grid .inline-swatch): Added.
 48 (.sidebar > .panel.details.recording-state > .content > .data-grid tr.modified): Deleted.
 49 (.sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected).non-standard): Deleted.
 50 (.sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected) .unavailable): Deleted.
 51 (.sidebar > .panel.details.recording-state > .content > .data-grid .inline-swatch): Deleted.
 52
 53 * UserInterface/Main.html:
 54 * UserInterface/Test.html:
 55
 56 * Localizations/en.lproj/localizedStrings.js:
 57
1582018-11-03 Devin Rousso <drousso@apple.com>
259
360 Web Inspector: Canvas: capture changes to <canvas> that would affect the recorded context

Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js

@@localizedStrings["%s \u2014 %s"] = "%s \u2014 %s";
5454localizedStrings["%s cannot be modified"] = "%s cannot be modified";
5555localizedStrings["%s delay"] = "%s delay";
5656localizedStrings["%s interval"] = "%s interval";
 57localizedStrings["(Action %s)"] = "(Action %s)";
5758localizedStrings["(Disk)"] = "(Disk)";
5859localizedStrings["(Index)"] = "(Index)";
5960localizedStrings["(Memory)"] = "(Memory)";

@@localizedStrings["Count"] = "Count";
252253localizedStrings["Create Breakpoint"] = "Create Breakpoint";
253254localizedStrings["Create a new tab"] = "Create a new tab";
254255localizedStrings["Current"] = "Current";
 256localizedStrings["Current State"] = "Current State";
255257localizedStrings["Custom"] = "Custom";
256258localizedStrings["DNS"] = "DNS";
257259localizedStrings["DOM Content Loaded \u2014 %s"] = "DOM Content Loaded \u2014 %s";

@@localizedStrings["Role"] = "Role";
745747localizedStrings["Run %d"] = "Run %d";
746748localizedStrings["Running the ā€œ%sā€œ audit"] = "Running the ā€œ%sā€œ audit";
747749localizedStrings["Samples"] = "Samples";
 750localizedStrings["Save %d"] = "Save %d";
748751localizedStrings["Save File"] = "Save File";
749752localizedStrings["Save Selected"] = "Save Selected";
750753localizedStrings["Save configuration"] = "Save configuration";
 754localizedStrings["Saved States"] = "Saved States";
751755localizedStrings["Scheduling:"] = "Scheduling:";
752756localizedStrings["Scheme"] = "Scheme";
753757localizedStrings["Scope"] = "Scope";

Source/WebInspectorUI/UserInterface/Main.html

413413 <script src="Models/RecordingAction.js"></script>
414414 <script src="Models/RecordingFrame.js"></script>
415415 <script src="Models/RecordingInitialStateAction.js"></script>
 416 <script src="Models/RecordingState.js"></script>
416417 <script src="Models/Redirect.js"></script>
417418 <script src="Models/RenderingFrameTimelineRecord.js"></script>
418419 <script src="Models/ResourceCollection.js"></script>

Source/WebInspectorUI/UserInterface/Models/Recording.js

@@WI.Recording = class Recording extends WI.Object
375375 let initialContent = await WI.ImageUtilities.promisifyLoad(this._initialState.content);
376376 this._processContext.drawImage(initialContent, 0, 0);
377377
378  for (let state of this._initialState.states) {
379  let swizzledState = await this._swizzleState(state);
380  for (let [key, value] of Object.entries(swizzledState)) {
381  try {
382  if (WI.RecordingAction.isFunctionForType(this._type, key))
383  this._processContext[key](...value);
384  else
385  this._processContext[key] = value;
386  } catch { }
387  }
 378 for (let initialState of this._initialState.states) {
 379 let state = await WI.RecordingState.swizzleInitialState(this, initialState);
 380 state.apply(this._type, this._processContext);
388381
389382 // The last state represents the current state, which should not be saved.
390  if (state !== this._initialState.states.lastValue) {
 383 if (initialState !== this._initialState.states.lastValue) {
391384 this._processContext.save();
392  this._processStates.push(WI.RecordingAction.deriveCurrentState(this._type, this._processContext));
 385 this._processStates.push(WI.RecordingState.fromContext(this._type, this._processContext));
393386 }
394387 }
395388 }

@@WI.Recording = class Recording extends WI.Object
456449 this._processContext = null;
457450 this._processing = false;
458451 }
459 
460  async _swizzleState(state)
461  {
462  let swizzledState = {};
463 
464  for (let [key, value] of Object.entries(state)) {
465  // COMPATIBILITY (iOS 12.0): Recording.InitialState.states did not exist yet
466  let keyIndex = parseInt(key);
467  if (!isNaN(keyIndex))
468  key = await this.swizzle(keyIndex, WI.Recording.Swizzle.String);
469 
470  switch (key) {
471  case "setTransform":
472  value = [await this.swizzle(value, WI.Recording.Swizzle.DOMMatrix)];
473  break;
474 
475  case "fillStyle":
476  case "strokeStyle":
477  let [gradient, pattern, string] = await Promise.all([
478  this.swizzle(value, WI.Recording.Swizzle.CanvasGradient),
479  this.swizzle(value, WI.Recording.Swizzle.CanvasPattern),
480  this.swizzle(value, WI.Recording.Swizzle.String),
481  ]);
482  if (gradient && !pattern)
483  value = gradient;
484  else if (pattern && !gradient)
485  value = pattern;
486  else
487  value = string;
488  break;
489 
490  case "direction":
491  case "font":
492  case "globalCompositeOperation":
493  case "imageSmoothingQuality":
494  case "lineCap":
495  case "lineJoin":
496  case "shadowColor":
497  case "textAlign":
498  case "textBaseline":
499  value = await this.swizzle(value, WI.Recording.Swizzle.String);
500  break;
501 
502  case "globalAlpha":
503  case "lineWidth":
504  case "miterLimit":
505  case "shadowOffsetX":
506  case "shadowOffsetY":
507  case "shadowBlur":
508  case "lineDashOffset":
509  value = await this.swizzle(value, WI.Recording.Swizzle.Number);
510  break;
511 
512  case "setPath":
513  value = [await this.swizzle(value[0], WI.Recording.Swizzle.Path2D)];
514  break;
515  }
516 
517  if (value === undefined || (Array.isArray(value) && value.includes(undefined)))
518  continue;
519 
520  swizzledState[key] = value;
521  }
522 
523  return swizzledState;
524  }
525452};
526453
527454WI.Recording.Event = {

Source/WebInspectorUI/UserInterface/Models/RecordingAction.js

@@WI.RecordingAction = class RecordingAction extends WI.Object
135135 return null;
136136 }
137137
138  static deriveCurrentState(type, context)
139  {
140  if (type === WI.Recording.Type.Canvas2D) {
141  let matrix = context.getTransform();
142 
143  let state = {};
144 
145  if (WI.ImageUtilities.supportsCanvasPathDebugging()) {
146  state.currentX = context.currentX;
147  state.currentY = context.currentY;
148  }
149 
150  state.direction = context.direction;
151  state.fillStyle = context.fillStyle;
152  state.font = context.font;
153  state.globalAlpha = context.globalAlpha;
154  state.globalCompositeOperation = context.globalCompositeOperation;
155  state.imageSmoothingEnabled = context.imageSmoothingEnabled;
156  state.imageSmoothingQuality = context.imageSmoothingQuality;
157  state.lineCap = context.lineCap;
158  state.lineDash = context.getLineDash();
159  state.lineDashOffset = context.lineDashOffset;
160  state.lineJoin = context.lineJoin;
161  state.lineWidth = context.lineWidth;
162  state.miterLimit = context.miterLimit;
163  state.shadowBlur = context.shadowBlur;
164  state.shadowColor = context.shadowColor;
165  state.shadowOffsetX = context.shadowOffsetX;
166  state.shadowOffsetY = context.shadowOffsetY;
167  state.strokeStyle = context.strokeStyle;
168  state.textAlign = context.textAlign;
169  state.textBaseline = context.textBaseline;
170  state.transform = [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f];
171  state.webkitImageSmoothingEnabled = context.webkitImageSmoothingEnabled;
172  state.webkitLineDash = context.webkitLineDash;
173  state.webkitLineDashOffset = context.webkitLineDashOffset;
174 
175  if (WI.ImageUtilities.supportsCanvasPathDebugging())
176  state.setPath = [context.getPath()];
177 
178  return state;
179  }
180 
181  return null;
182  }
183 
184138 static _prototypeForType(type)
185139 {
186140 if (type === WI.Recording.Type.Canvas2D)

@@WI.RecordingAction = class RecordingAction extends WI.Object
261215 }
262216
263217 if (recording.type === WI.Recording.Type.Canvas2D) {
264  let currentState = WI.RecordingAction.deriveCurrentState(recording.type, context);
 218 let currentState = WI.RecordingState.fromContext(recording.type, context, {source: this});
265219 console.assert(currentState);
266220
267221 if (this.name === "save")

@@WI.RecordingAction = class RecordingAction extends WI.Object
274228
275229 let lastState = null;
276230 if (lastAction) {
277  lastState = lastAction.states.lastValue;
278  for (let key in currentState) {
279  if (!(key in lastState) || (currentState[key] !== lastState[key] && !Object.shallowEqual(currentState[key], lastState[key])))
280  this._stateModifiers.add(key);
 231 let previousState = lastAction.states.lastValue;
 232 for (let [name, value] of currentState) {
 233 let previousValue = previousState.get(name);
 234 if (value !== previousValue && !Object.shallowEqual(value, previousValue))
 235 this._stateModifiers.add(name);
281236 }
282237 }
283238

Source/WebInspectorUI/UserInterface/Models/RecordingState.js

 1/*
 2 * Copyright (C) 2018 Apple Inc. All rights reserved.
 3 *
 4 * Redistribution and use in source and binary forms, with or without
 5 * modification, are permitted provided that the following conditions
 6 * are met:
 7 * 1. Redistributions of source code must retain the above copyright
 8 * notice, this list of conditions and the following disclaimer.
 9 * 2. Redistributions in binary form must reproduce the above copyright
 10 * notice, this list of conditions and the following disclaimer in the
 11 * documentation and/or other materials provided with the distribution.
 12 *
 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 23 * THE POSSIBILITY OF SUCH DAMAGE.
 24 */
 25
 26WI.RecordingState = class RecordingState
 27{
 28 constructor(data, {source} = {})
 29 {
 30 this._data = data;
 31 this._source = source || null;
 32 }
 33
 34 // Static
 35
 36 static fromContext(type, context, options = {})
 37 {
 38 if (type !== WI.Recording.Type.Canvas2D)
 39 return null;
 40
 41 let matrix = context.getTransform();
 42
 43 let data = {};
 44
 45 if (WI.ImageUtilities.supportsCanvasPathDebugging()) {
 46 data.currentX = context.currentX;
 47 data.currentY = context.currentY;
 48 }
 49
 50 data.direction = context.direction;
 51 data.fillStyle = context.fillStyle;
 52 data.font = context.font;
 53 data.globalAlpha = context.globalAlpha;
 54 data.globalCompositeOperation = context.globalCompositeOperation;
 55 data.imageSmoothingEnabled = context.imageSmoothingEnabled;
 56 data.imageSmoothingQuality = context.imageSmoothingQuality;
 57 data.lineCap = context.lineCap;
 58 data.lineDash = context.getLineDash();
 59 data.lineDashOffset = context.lineDashOffset;
 60 data.lineJoin = context.lineJoin;
 61 data.lineWidth = context.lineWidth;
 62 data.miterLimit = context.miterLimit;
 63 data.shadowBlur = context.shadowBlur;
 64 data.shadowColor = context.shadowColor;
 65 data.shadowOffsetX = context.shadowOffsetX;
 66 data.shadowOffsetY = context.shadowOffsetY;
 67 data.strokeStyle = context.strokeStyle;
 68 data.textAlign = context.textAlign;
 69 data.textBaseline = context.textBaseline;
 70 data.transform = [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f];
 71 data.webkitImageSmoothingEnabled = context.webkitImageSmoothingEnabled;
 72 data.webkitLineDash = context.webkitLineDash;
 73 data.webkitLineDashOffset = context.webkitLineDashOffset;
 74
 75 if (WI.ImageUtilities.supportsCanvasPathDebugging())
 76 data.setPath = [context.getPath()];
 77
 78 return new WI.RecordingState(data, options);
 79 }
 80
 81 static async swizzleInitialState(recording, initialState)
 82 {
 83 if (recording.type === WI.Recording.Type.Canvas2D) {
 84 let swizzledState = {};
 85
 86 for (let [name, value] of Object.entries(initialState)) {
 87 // COMPATIBILITY (iOS 12.0): Recording.InitialState.states did not exist yet
 88 let nameIndex = parseInt(name);
 89 if (!isNaN(nameIndex))
 90 name = await recording.swizzle(nameIndex, WI.Recording.Swizzle.String);
 91
 92 switch (name) {
 93 case "setTransform":
 94 value = [await recording.swizzle(value, WI.Recording.Swizzle.DOMMatrix)];
 95 break;
 96
 97 case "fillStyle":
 98 case "strokeStyle":
 99 var [gradient, pattern, string] = await Promise.all([
 100 recording.swizzle(value, WI.Recording.Swizzle.CanvasGradient),
 101 recording.swizzle(value, WI.Recording.Swizzle.CanvasPattern),
 102 recording.swizzle(value, WI.Recording.Swizzle.String),
 103 ]);
 104 if (gradient && !pattern)
 105 value = gradient;
 106 else if (pattern && !gradient)
 107 value = pattern;
 108 else
 109 value = string;
 110 break;
 111
 112 case "direction":
 113 case "font":
 114 case "globalCompositeOperation":
 115 case "imageSmoothingQuality":
 116 case "lineCap":
 117 case "lineJoin":
 118 case "shadowColor":
 119 case "textAlign":
 120 case "textBaseline":
 121 value = await recording.swizzle(value, WI.Recording.Swizzle.String);
 122 break;
 123
 124 case "globalAlpha":
 125 case "lineWidth":
 126 case "miterLimit":
 127 case "shadowOffsetX":
 128 case "shadowOffsetY":
 129 case "shadowBlur":
 130 case "lineDashOffset":
 131 value = await recording.swizzle(value, WI.Recording.Swizzle.Number);
 132 break;
 133
 134 case "setPath":
 135 value = [await recording.swizzle(value[0], WI.Recording.Swizzle.Path2D)];
 136 break;
 137 }
 138
 139 if (value === undefined || (Array.isArray(value) && value.includes(undefined)))
 140 continue;
 141
 142 swizzledState[name] = value;
 143 }
 144
 145 return new WI.RecordingState(swizzledState);
 146 }
 147
 148 return null;
 149 }
 150
 151 // Public
 152
 153 get source() { return this._source; }
 154
 155 has(name)
 156 {
 157 return name in this._data;
 158 }
 159
 160 get(name)
 161 {
 162 return this._data[name];
 163 }
 164
 165 apply(type, context)
 166 {
 167 for (let [name, value] of this) {
 168 if (!(name in context))
 169 continue;
 170
 171 // Skip internal state used for path debugging.
 172 if (name === "currentX" || name === "currentY")
 173 continue;
 174
 175 try {
 176 if (WI.RecordingAction.isFunctionForType(type, name))
 177 context[name](...value);
 178 else
 179 context[name] = value;
 180 } catch { }
 181 }
 182 }
 183
 184 toJSON()
 185 {
 186 return this._data;
 187 }
 188
 189 [Symbol.iterator]()
 190 {
 191 return Object.entries(this._data)[Symbol.iterator]();
 192 }
 193};

Source/WebInspectorUI/UserInterface/Test.html

174174 <script src="Models/RecordingAction.js"></script>
175175 <script src="Models/RecordingFrame.js"></script>
176176 <script src="Models/RecordingInitialStateAction.js"></script>
 177 <script src="Models/RecordingState.js"></script>
177178 <script src="Models/Redirect.js"></script>
178179 <script src="Models/RenderingFrameTimelineRecord.js"></script>
179180 <script src="Models/ResourceCollection.js"></script>

Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js

@@WI.RecordingContentView = class RecordingContentView extends WI.ContentView
229229 }
230230
231231 for (let state of snapshot.states) {
232  for (let name in state) {
233  if (!(name in snapshot.context))
234  continue;
235 
236  // Skip internal state used for path debugging.
237  if (name === "currentX" || name === "currentY")
238  continue;
239 
240  try {
241  if (WI.RecordingAction.isFunctionForType(this.representedObject.type, name))
242  snapshot.context[name](...state[name]);
243  else
244  snapshot.context[name] = state[name];
245  } catch {
246  delete state[name];
247  }
248  }
 232 state.apply(this.representedObject.type, snapshot.context);
249233
250234 ++saveCount;
251235 snapshot.context.save();

Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.css

2727 min-height: 100%;
2828}
2929
30 .sidebar > .panel.details.recording-state > .content > .data-grid tr.modified {
 30.sidebar > .panel.details.recording-state > .content .details-section > .header .source {
 31 -webkit-margin-start: 4px;
 32 color: var(--text-color-gray-medium);
 33}
 34
 35.sidebar > .panel.details.recording-state > .content .data-grid tr.modified {
3136 background-color: var(--value-changed-highlight);
3237}
3338
34 .sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected).non-standard {
 39.sidebar > .panel.details.recording-state > .content .data-grid tr:not(.selected).non-standard .name-column {
3540 opacity: 0.5;
3641}
3742
38 .sidebar > .panel.details.recording-state > .content > .data-grid tr:not(.selected) .unavailable {
 43.sidebar > .panel.details.recording-state > .content .data-grid tr:not(.selected) .unavailable {
3944 color: grey;
4045}
4146
42 .sidebar > .panel.details.recording-state > .content > .data-grid .inline-swatch {
 47.sidebar > .panel.details.recording-state > .content .data-grid .inline-swatch {
4348 vertical-align: -1px;
4449}
4550
4651@media (prefers-dark-interface) {
47  .sidebar > .panel.details.recording-state > .content > .data-grid tr.modified {
 52 .sidebar > .panel.details.recording-state > .content .data-grid tr.modified {
4853 color: var(--green-highlight-text-color);
4954 }
5055}

Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.js

@@WI.RecordingStateDetailsSidebarPanel = class RecordingStateDetailsSidebarPanel e
3232 this._recording = null;
3333 this._action = null;
3434
35  this._dataGrid = null;
 35 this._dataGrids = [];
3636 }
3737
3838 // Public

@@WI.RecordingStateDetailsSidebarPanel = class RecordingStateDetailsSidebarPanel e
7878
7979 get scrollElement()
8080 {
81  return this._dataGrid.scrollContainer;
 81 if (this._dataGrids.length === 1)
 82 return this._dataGrids[0].scrollContainer;
 83 return super.scrollElement;
 84 }
 85
 86 sizeDidChange()
 87 {
 88 super.sizeDidChange();
 89
 90 if (this._dataGrids.length === 1)
 91 return;
 92
 93 // FIXME: <https://webkit.org/b/152269> Web Inspector: Convert DetailsSection classes to use View
 94 for (let dataGrid of this._dataGrids)
 95 dataGrid.sizeDidChange();
8296 }
8397
8498 // Private
8599
86100 _generateDetailsCanvas2D(action)
87101 {
88  if (!this._dataGrid) {
89  this._dataGrid = new WI.DataGrid({
90  name: {title: WI.UIString("Name")},
91  value: {title: WI.UIString("Value")},
92  });
93  }
94  if (!this._dataGrid.parentView)
95  this.contentView.addSubview(this._dataGrid);
 102 if (this._dataGrids.length === 1)
 103 this.contentView.removeSubview(this._dataGrids[0]);
 104
 105 this.contentView.element.removeChildren();
96106
97  this._dataGrid.removeChildren();
 107 this._dataGrids = [];
98108
99109 let currentState = action.states.lastValue;
100110 console.assert(currentState);
101111 if (!currentState)
102112 return;
103113
104  function isColorProperty(name) {
105  return name === "fillStyle" || name === "strokeStyle" || name === "shadowColor";
106  }
107 
108  function createInlineSwatch(value) {
109  let color = WI.Color.fromString(value);
110  if (!color)
111  return null;
 114 let createStateDataGrid = (state) => {
 115 let dataGrid = new WI.DataGrid({
 116 name: {title: WI.UIString("Name")},
 117 value: {title: WI.UIString("Value")},
 118 });
 119 this._dataGrids.push(dataGrid);
 120
 121 for (let [name, value] of state) {
 122 // Skip internal state used for path debugging.
 123 if (name === "setPath")
 124 continue;
 125
 126 if (typeof value === "object") {
 127 let isGradient = value instanceof CanvasGradient;
 128 let isPattern = value instanceof CanvasPattern;
 129 if (isGradient || isPattern) {
 130 let textElement = document.createElement("span");
 131 textElement.classList.add("unavailable");
 132
 133 let image = null;
 134 if (isGradient) {
 135 textElement.textContent = WI.unlocalizedString("CanvasGradient");
 136 image = WI.ImageUtilities.imageFromCanvasGradient(value, 100, 100);
 137 } else if (isPattern) {
 138 textElement.textContent = WI.unlocalizedString("CanvasPattern");
 139 image = value.__image;
 140 }
 141
 142 let fragment = document.createDocumentFragment();
 143 if (image) {
 144 let swatch = new WI.InlineSwatch(WI.InlineSwatch.Type.Image, image);
 145 fragment.appendChild(swatch.element);
 146 }
 147 fragment.appendChild(textElement);
 148 value = fragment;
 149 } else {
 150 if (value instanceof DOMMatrix)
 151 value = [value.a, value.b, value.c, value.d, value.e, value.f];
 152
 153 value = JSON.stringify(value);
 154 }
 155 } else if (name === "fillStyle" || name === "strokeStyle" || name === "shadowColor") {
 156 let color = WI.Color.fromString(value);
 157 const readOnly = true;
 158 let swatch = new WI.InlineSwatch(WI.InlineSwatch.Type.Color, color, readOnly);
 159 value = document.createElement("span");
 160 value.append(swatch.element, color.toString());
 161 }
112162
113  const readOnly = true;
114  return new WI.InlineSwatch(WI.InlineSwatch.Type.Color, color, readOnly);
115  }
 163 let classNames = [];
 164 if (state === currentState && !action.isGetter && action.stateModifiers.has(name))
 165 classNames.push("modified");
 166 if (name.startsWith("webkit"))
 167 classNames.push("non-standard");
116168
117  for (let name in currentState) {
118  // Skip internal state used for path debugging.
119  if (name === "setPath")
120  continue;
121 
122  let value = currentState[name];
123  if (typeof value === "object") {
124  let isGradient = value instanceof CanvasGradient;
125  let isPattern = value instanceof CanvasPattern;
126  if (isGradient || isPattern) {
127  let textElement = document.createElement("span");
128  textElement.classList.add("unavailable");
129 
130  let image = null;
131  if (isGradient) {
132  textElement.textContent = WI.unlocalizedString("CanvasGradient");
133  image = WI.ImageUtilities.imageFromCanvasGradient(value, 100, 100);
134  } else if (isPattern) {
135  textElement.textContent = WI.unlocalizedString("CanvasPattern");
136  image = value.__image;
137  }
 169 const hasChildren = false;
 170 dataGrid.appendChild(new WI.DataGridNode({name, value}, hasChildren, classNames));
 171 }
138172
139  let fragment = document.createDocumentFragment();
140  if (image) {
141  let swatch = new WI.InlineSwatch(WI.InlineSwatch.Type.Image, image);
142  fragment.appendChild(swatch.element);
 173 dataGrid.updateLayoutIfNeeded();
 174 return dataGrid;
 175 };
 176
 177 let createStateSection = (state, index = NaN) => {
 178 let isCurrentState = isNaN(index);
 179
 180 let dataGrid = createStateDataGrid(state);
 181 let row = new WI.DetailsSectionDataGridRow(dataGrid);
 182 let group = new WI.DetailsSectionGroup([row]);
 183
 184 let identifier = isCurrentState ? "recording-current-state" : `recording-saved-state-${index + 1}`;
 185 const title = null;
 186 const optionsElement = null;
 187 let defaultCollapsedSettingValue = !isCurrentState;
 188 let section = new WI.DetailsSection(identifier, title, [group], optionsElement, defaultCollapsedSettingValue);
 189
 190 if (isCurrentState)
 191 section.title = WI.UIString("Current State");
 192 else {
 193 section.title = WI.UIString("Save %d").format(index + 1);
 194
 195 if (state.source) {
 196 let sourceIndex = this._recording.actions.indexOf(state.source);
 197 if (sourceIndex >= 0) {
 198 let sourceElement = section.titleElement.appendChild(document.createElement("span"));
 199 sourceElement.classList.add("source");
 200 sourceElement.textContent = WI.UIString("(Action %s)").format(sourceIndex);
143201 }
144  fragment.appendChild(textElement);
145  value = fragment;
146  } else {
147  if (value instanceof DOMMatrix)
148  value = [value.a, value.b, value.c, value.d, value.e, value.f];
149 
150  value = JSON.stringify(value);
151202 }
152  } else if (isColorProperty(name)) {
153  let swatch = createInlineSwatch(value);
154  let label = swatch.value.toString();
155  value = document.createElement("span");
156  value.append(swatch.element, label);
157203 }
158204
159  let classNames = [];
160  if (!action.isGetter && action.stateModifiers.has(name))
161  classNames.push("modified");
162  if (name.startsWith("webkit"))
163  classNames.push("non-standard");
 205 return section;
 206 };
 207
 208 if (action.states.length === 1) {
 209 this.contentView.addSubview(createStateDataGrid(currentState));
 210 return;
 211 }
 212
 213 let currentStateSection = createStateSection(currentState);
 214 this.contentView.element.appendChild(currentStateSection.element);
164215
165  const hasChildren = false;
166  this._dataGrid.appendChild(new WI.DataGridNode({name, value}, hasChildren, classNames));
 216 let savedStateSections = [];
 217 for (let i = action.states.length - 2; i >= 0; --i) {
 218 let savedStateSection = createStateSection(action.states[i], i);
 219 savedStateSections.push(savedStateSection);
167220 }
 221
 222 let savedStatesGroup = new WI.DetailsSectionGroup(savedStateSections);
 223 let savedStatesSection = new WI.DetailsSection("recording-saved-states", WI.UIString("Saved States"), [savedStatesGroup]);
 224 this.contentView.element.appendChild(savedStatesSection.element);
168225 }
169226};

LayoutTests/ChangeLog

 12018-11-04 Devin Rousso <drousso@apple.com>
 2
 3 Web Inspector: show save/restore stack for recorded 2D Canvases
 4 https://bugs.webkit.org/show_bug.cgi?id=175283
 5 <rdar://problem/34040756>
 6
 7 Reviewed by NOBODY (OOPS!).
 8
 9 * inspector/canvas/recording-2d.html:
 10 * inspector/canvas/resources/recording-utilities.js:
 11 (TestPage.registerInitializer.log):
 12 (TestPage.registerInitializer.async logRecording):
 13
1142018-11-04 Zalan Bujtas <zalan@apple.com>
215
316 [LFC][BFC] Add support for percentage height in quirks mode.

LayoutTests/inspector/canvas/recording-2d.html

@@function test() {
519519
520520 async function logStates(recording) {
521521 async function compare(index, expected) {
522  let swizzledState = await recording._swizzleState(recording.initialState.states[index]);
523  InspectorTest.expectEqual(swizzledState["fillStyle"], expected, `State ${index} should match expected fillStyle value.`)
 522 let state = await WI.RecordingState.swizzleInitialState(recording, recording.initialState.states[index]);
 523 InspectorTest.expectEqual(state.get("fillStyle"), expected, `State ${index} should match expected fillStyle value.`)
524524 }
525525
526526 await compare(0, "#000000");

LayoutTests/inspector/canvas/resources/recording-utilities.js

11TestPage.registerInitializer(() => {
22 function log(object, indent) {
3  for (let key of Object.keys(object)) {
4  let value = object[key];
 3 for (let [name, value] of object) {
54 if (typeof value === "string")
65 value = sanitizeURL(value);
76 else if (Array.isArray(value) && value[0] instanceof DOMMatrix)
87 value[0] = [value[0].a, value[0].b, value[0].c, value[0].d, value[0].e, value[0].f];
9  InspectorTest.log(indent + key + ": " + JSON.stringify(value));
 8 InspectorTest.log(indent + name + ": " + JSON.stringify(value));
109 }
1110 }
1211

@@TestPage.registerInitializer(() => {
1413 InspectorTest.log("initialState:");
1514
1615 InspectorTest.log(" attributes:");
17  log(recording.initialState.attributes, " ");
 16 log(Object.entries(recording.initialState.attributes), " ");
1817
1918 let currentState = recording.initialState.states.lastValue;
2019 if (currentState) {
2120 InspectorTest.log(" current state:");
22  let swizzledState = await recording._swizzleState(currentState);
23  log(swizzledState, " ");
 21 let state = await WI.RecordingState.swizzleInitialState(recording, currentState);
 22 log(state, " ");
2423 }
2524
2625 InspectorTest.log(" parameters:");
27  log(recording.initialState.parameters, " ");
 26 log(Object.entries(recording.initialState.parameters), " ");
2827
2928 InspectorTest.log(" content: " + JSON.stringify(recording.initialState.content));
3029