chrome-devtools-frontend 1.0.1667564 → 1.0.1668390

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.agents/skills/devtools-setting-migration/SKILL.md +290 -0
  2. package/.agents/skills/ui-eng-vision-widget-promoter/SKILL.md +11 -4
  3. package/.agents/skills/ui-widgets/SKILL.md +25 -1
  4. package/docs/ui_engineering.md +44 -1
  5. package/front_end/core/common/Settings.ts +11 -4
  6. package/front_end/core/root/Runtime.ts +5 -0
  7. package/front_end/core/sdk/CSSModel.test.api.ts +102 -0
  8. package/front_end/core/sdk/CSSModel.ts +3 -2
  9. package/front_end/core/sdk/DebuggerModel.ts +3 -2
  10. package/front_end/core/sdk/SDKSettings.ts +19 -0
  11. package/front_end/core/sdk/sdk-meta.ts +0 -62
  12. package/front_end/core/sdk/sdk.ts +2 -0
  13. package/front_end/entrypoints/main/MainImpl.ts +10 -3
  14. package/front_end/foundation/Universe.ts +7 -0
  15. package/front_end/models/ai_assistance/AiAgent2.ts +1 -1
  16. package/front_end/models/ai_assistance/BuiltInAi.ts +11 -6
  17. package/front_end/models/ai_assistance/ChangeManager.ts +17 -1
  18. package/front_end/models/javascript_metadata/NativeFunctions.js +22 -9
  19. package/front_end/models/source_map_scopes/NamesResolver.ts +23 -17
  20. package/front_end/panels/application/ServiceWorkerUpdateCycleView.ts +78 -118
  21. package/front_end/panels/application/preloading/components/preloadingDetailsReportView.css +13 -13
  22. package/front_end/panels/console/ConsoleInsightTeaser.ts +3 -2
  23. package/front_end/panels/emulation/DeviceModeToolbar.ts +2 -2
  24. package/front_end/panels/emulation/DeviceModeView.ts +63 -104
  25. package/front_end/panels/sensors/LocationsSettingsTab.ts +244 -34
  26. package/front_end/panels/sensors/locationsSettingsTab.css +74 -0
  27. package/front_end/panels/sources/DebuggerPlugin.ts +2 -2
  28. package/front_end/panels/sources/sources-meta.ts +55 -0
  29. package/front_end/third_party/chromium/README.chromium +1 -1
  30. package/front_end/ui/legacy/Dialog.ts +93 -4
  31. package/front_end/ui/legacy/InspectorView.ts +1 -5
  32. package/front_end/ui/legacy/PopoverHelper.ts +5 -5
  33. package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
  34. package/package.json +1 -1
@@ -10,21 +10,21 @@
10
10
 
11
11
  devtools-report {
12
12
  flex-grow: 1;
13
- }
14
13
 
15
- button.link {
16
- color: var(--sys-color-primary);
17
- text-decoration: underline;
18
- padding: 0;
19
- border: none;
20
- background: none;
21
- font-family: inherit;
22
- font-size: inherit;
23
- height: 16px;
24
- }
14
+ button.link {
15
+ color: var(--sys-color-primary);
16
+ text-decoration: underline;
17
+ padding: 0;
18
+ border: none;
19
+ background: none;
20
+ font-family: inherit;
21
+ font-size: inherit;
22
+ height: 16px;
23
+ }
25
24
 
26
- button.link devtools-icon {
27
- vertical-align: sub;
25
+ button.link devtools-icon {
26
+ vertical-align: sub;
27
+ }
28
28
  }
29
29
 
30
30
  .link {
@@ -420,7 +420,8 @@ export class ConsoleInsightTeaser extends UI.Widget.Widget {
420
420
  #callShowTooltip = false;
421
421
  #startTime = 0;
422
422
 
423
- constructor(uuid: string, consoleViewMessage: ConsoleViewMessage, element?: HTMLElement, view?: View) {
423
+ constructor(uuid: string, consoleViewMessage: ConsoleViewMessage, element?: HTMLElement, view?: View,
424
+ builtInAi: AiAssistanceModel.BuiltInAi.BuiltInAi = AiAssistanceModel.BuiltInAi.BuiltInAi.instance()) {
424
425
  super(element);
425
426
  this.#view = view ?? DEFAULT_VIEW;
426
427
  this.#uuid = uuid;
@@ -430,7 +431,7 @@ export class ConsoleInsightTeaser extends UI.Widget.Widget {
430
431
  this.#boundOnAidaAvailabilityChange = this.#onAidaAvailabilityChange.bind(this);
431
432
  this.#boundOnDownloadProgressChange = this.#onDownloadProgressChange.bind(this);
432
433
  this.#boundOnSessionCreation = this.#onSessionCreation.bind(this);
433
- this.#builtInAi = AiAssistanceModel.BuiltInAi.BuiltInAi.instance();
434
+ this.#builtInAi = builtInAi;
434
435
  this.#state = this.#builtInAi.hasSession() ? State.READY : State.NO_MODEL;
435
436
  this.#callShowTooltip = true;
436
437
  this.requestUpdate();
@@ -512,8 +512,8 @@ export class DeviceModeToolbar extends UI.Widget.Widget {
512
512
  super.wasShown();
513
513
  // TODO(crbug.com/407750803): Revisit once DeviceModeView is migrated.
514
514
  this.performUpdate(); // Trigger a manual update eagerly, DeviceModeView needs to measure our height.
515
+ this.restore();
515
516
  }
516
-
517
517
  override performUpdate(): void {
518
518
  if (!this.model) {
519
519
  return;
@@ -1078,7 +1078,7 @@ export class DeviceModeToolbar extends UI.Widget.Widget {
1078
1078
  return !this.model ? '' : `${(this.model.scale() * 100).toFixed(0)}`;
1079
1079
  }
1080
1080
 
1081
- restore(): void {
1081
+ private restore(): void {
1082
1082
  if (!this.model) {
1083
1083
  return;
1084
1084
  }
@@ -1,15 +1,16 @@
1
1
  // Copyright 2015 The Chromium Authors
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
- /* eslint-disable @devtools/no-imperative-dom-api */
5
4
 
6
5
  import * as Common from '../../core/common/common.js';
7
6
  import * as Host from '../../core/host/host.js';
8
7
  import * as i18n from '../../core/i18n/i18n.js';
9
8
  import * as Platform from '../../core/platform/platform.js';
9
+ import * as TextUtils from '../../core/text_utils/text_utils.js';
10
10
  import type * as Protocol from '../../generated/protocol.js';
11
11
  import * as EmulationModel from '../../models/emulation/emulation.js';
12
12
  import * as Geometry from '../../models/geometry/geometry.js';
13
+ import * as Workspace from '../../models/workspace/workspace.js';
13
14
  import * as UI from '../../ui/legacy/legacy.js';
14
15
  import {Directives, html, nothing, render} from '../../ui/lit/lit.js';
15
16
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
@@ -60,16 +61,6 @@ const UIStrings = {
60
61
  const str_ = i18n.i18n.registerUIStrings('panels/emulation/DeviceModeView.ts', UIStrings);
61
62
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
62
63
 
63
- export interface DeviceModeViewRefs {
64
- toolbar: DeviceModeToolbar;
65
- bottomRightResizerElement: HTMLElement;
66
- bottomLeftResizerElement: HTMLElement;
67
- rightResizerElement: HTMLElement;
68
- leftResizerElement: HTMLElement;
69
- bottomResizerElement: HTMLElement;
70
- pageArea: HTMLElement;
71
- }
72
-
73
64
  export interface DeviceModeViewInput {
74
65
  model: EmulationModel.DeviceModeModel.DeviceModeModel;
75
66
  showMediaInspectorSetting: Common.Settings.Setting<boolean>;
@@ -78,10 +69,12 @@ export interface DeviceModeViewInput {
78
69
  outlineImageLoaded: boolean;
79
70
  screenImage: string;
80
71
  screenImageLoaded: boolean;
72
+ resizable: boolean;
81
73
  showRulers: boolean;
82
74
  showMediaInspector: boolean;
83
75
  scale: number;
84
76
  cachedCssScreenRect?: EmulationModel.DeviceModeModel.Rect;
77
+ cachedCssVisiblePageRect?: EmulationModel.DeviceModeModel.Rect;
85
78
  cachedOutlineRect?: EmulationModel.DeviceModeModel.Rect;
86
79
  onApplyPresetSize: (size: number, e: Event) => void;
87
80
  bottomRightResizer: UI.ResizerWidget.ResizerWidget;
@@ -101,7 +94,7 @@ export interface DeviceModeViewInput {
101
94
 
102
95
  export type DeviceModeViewView = (
103
96
  input: DeviceModeViewInput,
104
- output: DeviceModeViewRefs,
97
+ output: undefined,
105
98
  target: HTMLElement,
106
99
  ) => void;
107
100
 
@@ -130,7 +123,7 @@ function resizerRef(
130
123
 
131
124
  export const DEFAULT_DEVICE_MODE_VIEW: DeviceModeViewView = (
132
125
  input: DeviceModeViewInput,
133
- output: DeviceModeViewRefs,
126
+ _output: undefined,
134
127
  target: HTMLElement,
135
128
  ): void => {
136
129
  const sizes = [320, 375, 425, 768, 1024, 1440, 2560];
@@ -145,8 +138,7 @@ export const DEFAULT_DEVICE_MODE_VIEW: DeviceModeViewView = (
145
138
  ];
146
139
  // clang-format off
147
140
  render(html`
148
- <devtools-widget ${UI.Widget.widget(DeviceModeToolbar, {model: input.model})}
149
- ${UI.Widget.widgetRef(DeviceModeToolbar, t => { output.toolbar = t; })}></devtools-widget>
141
+ ${UI.Widget.widget(DeviceModeToolbar, {model: input.model})}
150
142
  <div class=${classMap({
151
143
  'device-mode-content-clip': true,
152
144
  vbox: true,
@@ -199,33 +191,44 @@ export const DEFAULT_DEVICE_MODE_VIEW: DeviceModeViewView = (
199
191
  @load=${(): void => input.onScreenImageLoaded(true)}
200
192
  @error=${(): void => input.onScreenImageLoaded(false)}>
201
193
  <div class="device-mode-resizer device-mode-bottom-right-resizer"
194
+ ?hidden=${!input.resizable}
202
195
  jslog=${VisualLogging.slider('device-mode-resizer').track({drag: true})}
203
196
  ${ref(input.bottomRightResizerRef)}>
204
197
  <div></div>
205
198
  </div>
206
199
  <div class="device-mode-resizer device-mode-bottom-left-resizer"
200
+ ?hidden=${!input.resizable}
207
201
  jslog=${VisualLogging.slider('device-mode-resizer').track({drag: true})}
208
202
  ${ref(input.bottomLeftResizerRef)}>
209
203
  <div></div>
210
204
  </div>
211
205
  <div class="device-mode-resizer device-mode-right-resizer"
206
+ ?hidden=${!input.resizable}
212
207
  jslog=${VisualLogging.slider('device-mode-resizer').track({drag: true})}
213
208
  ${ref(input.rightResizerRef)}>
214
209
  <div></div>
215
210
  </div>
216
211
  <div class="device-mode-resizer device-mode-left-resizer"
212
+ ?hidden=${!input.resizable}
217
213
  jslog=${VisualLogging.slider('device-mode-resizer').track({drag: true})}
218
214
  ${ref(input.leftResizerRef)}>
219
215
  <div></div>
220
216
  </div>
221
217
  <div class="device-mode-resizer device-mode-bottom-resizer"
218
+ ?hidden=${!input.resizable}
222
219
  jslog=${VisualLogging.slider('device-mode-resizer').track({drag: true})}
223
220
  title=${i18nString(UIStrings.doubleclickForFullHeight)}
224
221
  ${ref(input.bottomResizerRef)}
225
222
  @dblclick=${input.onDoubleclickBottomResizer}>
226
223
  <div></div>
227
224
  </div>
228
- <div class="device-mode-page-area" ${ref((el?: Element) => { if (el instanceof HTMLElement) { output.pageArea = el; } })}><slot></slot></div>
225
+ <div class="device-mode-page-area"
226
+ style=${styleMap(input.cachedCssVisiblePageRect ? {
227
+ left: `${input.cachedCssVisiblePageRect.left}px`,
228
+ top: `${input.cachedCssVisiblePageRect.top}px`,
229
+ width: `${input.cachedCssVisiblePageRect.width}px`,
230
+ height: `${input.cachedCssVisiblePageRect.height}px`,
231
+ } : {})}><slot></slot></div>
229
232
  </div>
230
233
  ${input.showRulers ? html`
231
234
  <devtools-widget class="device-mode-ruler-top device-mode-ruler"
@@ -247,7 +250,11 @@ export const DEFAULT_DEVICE_MODE_VIEW: DeviceModeViewView = (
247
250
  ` : nothing}
248
251
  </div>
249
252
  </div>
250
- `, target);
253
+ `, target, {
254
+ container: {
255
+ classes: ['device-mode-view'],
256
+ },
257
+ });
251
258
  // clang-format on
252
259
  };
253
260
 
@@ -256,34 +263,16 @@ export class DeviceModeView extends UI.Widget.VBox {
256
263
  private model: EmulationModel.DeviceModeModel.DeviceModeModel;
257
264
  private showMediaInspectorSetting: Common.Settings.Setting<boolean>;
258
265
  private showRulersSetting: Common.Settings.Setting<boolean>;
259
- pageArea!: HTMLElement;
260
- rightResizerElement!: HTMLElement;
261
- leftResizerElement!: HTMLElement;
262
- bottomResizerElement!: HTMLElement;
263
- bottomRightResizerElement!: HTMLElement;
264
- bottomLeftResizerElement!: HTMLElement;
265
266
  private readonly bottomRightResizer = this.createResizer(2, 1);
266
267
  private readonly bottomLeftResizer = this.createResizer(-2, 1);
267
268
  private readonly rightResizer = this.createResizer(2, 0);
268
269
  private readonly leftResizer = this.createResizer(-2, 0);
269
270
  private readonly bottomResizer = this.createResizer(0, 1);
270
- private readonly bottomRightResizerRef = resizerRef(this.bottomRightResizer, el => {
271
- this.bottomRightResizerElement = el;
272
- });
273
- private readonly bottomLeftResizerRef = resizerRef(this.bottomLeftResizer, el => {
274
- this.bottomLeftResizerElement = el;
275
- });
276
- private readonly rightResizerRef = resizerRef(this.rightResizer, el => {
277
- this.rightResizerElement = el;
278
- });
279
- private readonly leftResizerRef = resizerRef(this.leftResizer, el => {
280
- this.leftResizerElement = el;
281
- });
282
- private readonly bottomResizerRef = resizerRef(this.bottomResizer, el => {
283
- this.bottomResizerElement = el;
284
- });
285
- private cachedResizable!: boolean|undefined;
286
- toolbar!: DeviceModeToolbar;
271
+ private readonly bottomRightResizerRef = resizerRef(this.bottomRightResizer);
272
+ private readonly bottomLeftResizerRef = resizerRef(this.bottomLeftResizer);
273
+ private readonly rightResizerRef = resizerRef(this.rightResizer);
274
+ private readonly leftResizerRef = resizerRef(this.leftResizer);
275
+ private readonly bottomResizerRef = resizerRef(this.bottomResizer);
287
276
  private slowPositionStart?: {
288
277
  x: number,
289
278
  y: number,
@@ -295,8 +284,6 @@ export class DeviceModeView extends UI.Widget.VBox {
295
284
  private cachedMediaInspectorVisible?: boolean;
296
285
  private cachedShowRulers?: boolean;
297
286
  private cachedScale?: number;
298
- private handleWidth?: number;
299
- private handleHeight?: number;
300
287
  #outlineImageLoaded = false;
301
288
  #lastOutlineImageSrc?: string;
302
289
  #screenImageLoaded = false;
@@ -308,7 +295,6 @@ export class DeviceModeView extends UI.Widget.VBox {
308
295
  this.#view = view;
309
296
 
310
297
  this.setMinimumSize(150, 150);
311
- this.element.classList.add('device-mode-view');
312
298
  this.registerRequiredCSS(deviceModeViewStyles);
313
299
 
314
300
  this.model = EmulationModel.DeviceModeModel.DeviceModeModel.instance();
@@ -339,11 +325,13 @@ export class DeviceModeView extends UI.Widget.VBox {
339
325
  outlineImageLoaded: this.#outlineImageLoaded,
340
326
  screenImage: this.model.screenImage(),
341
327
  screenImageLoaded: this.#screenImageLoaded,
328
+ resizable: this.model.type() === EmulationModel.DeviceModeModel.Type.Responsive,
342
329
  showRulers: this.showRulersSetting.get() && this.model.type() !== EmulationModel.DeviceModeModel.Type.None,
343
330
  showMediaInspector:
344
331
  this.showMediaInspectorSetting.get() && this.model.type() !== EmulationModel.DeviceModeModel.Type.None,
345
332
  scale: this.model.scale(),
346
333
  cachedCssScreenRect: this.cachedCssScreenRect,
334
+ cachedCssVisiblePageRect: this.cachedCssVisiblePageRect,
347
335
  cachedOutlineRect: this.cachedOutlineRect,
348
336
  onApplyPresetSize: (width: number, e: Event): void => {
349
337
  this.model.emulate(EmulationModel.DeviceModeModel.Type.Responsive, null, null);
@@ -364,7 +352,7 @@ export class DeviceModeView extends UI.Widget.VBox {
364
352
  onOutlineImageLoaded: (success: boolean): void => this.onOutlineImageLoaded(success),
365
353
  onScreenImageLoaded: (success: boolean): void => this.onScreenImageLoaded(success),
366
354
  };
367
- this.#view(input, this, this.contentElement);
355
+ this.#view(input, undefined, this.contentElement);
368
356
  }
369
357
 
370
358
  private onOutlineImageLoaded(success: boolean): void {
@@ -380,6 +368,7 @@ export class DeviceModeView extends UI.Widget.VBox {
380
368
  this.requestUpdate();
381
369
  }
382
370
  }
371
+
383
372
  private createResizer(widthFactor: number, heightFactor: number): UI.ResizerWidget.ResizerWidget {
384
373
  const resizer = new UI.ResizerWidget.ResizerWidget();
385
374
  let cursor: 'nwse-resize'|'nesw-resize'|('ew-resize' | 'ns-resize') = widthFactor ? 'ew-resize' : 'ns-resize';
@@ -450,13 +439,6 @@ export class DeviceModeView extends UI.Widget.VBox {
450
439
  }
451
440
 
452
441
  private updateUI(): void {
453
- function applyRect(element: HTMLElement, rect: EmulationModel.DeviceModeModel.Rect): void {
454
- element.style.left = rect.left + 'px';
455
- element.style.top = rect.top + 'px';
456
- element.style.width = rect.width + 'px';
457
- element.style.height = rect.height + 'px';
458
- }
459
-
460
442
  if (!this.isShowing()) {
461
443
  return;
462
444
  }
@@ -474,7 +456,6 @@ export class DeviceModeView extends UI.Widget.VBox {
474
456
 
475
457
  const cssVisiblePageRect = this.model.visiblePageRect().scale(1 / zoomFactor);
476
458
  if (!this.cachedCssVisiblePageRect || !cssVisiblePageRect.isEqual(this.cachedCssVisiblePageRect)) {
477
- applyRect(this.pageArea, cssVisiblePageRect);
478
459
  callDoResize = true;
479
460
  this.cachedCssVisiblePageRect = cssVisiblePageRect;
480
461
  }
@@ -488,16 +469,6 @@ export class DeviceModeView extends UI.Widget.VBox {
488
469
  }
489
470
  }
490
471
 
491
- const resizable = this.model.type() === EmulationModel.DeviceModeModel.Type.Responsive;
492
- if (resizable !== this.cachedResizable) {
493
- this.rightResizerElement.classList.toggle('hidden', !resizable);
494
- this.leftResizerElement.classList.toggle('hidden', !resizable);
495
- this.bottomResizerElement.classList.toggle('hidden', !resizable);
496
- this.bottomRightResizerElement.classList.toggle('hidden', !resizable);
497
- this.bottomLeftResizerElement.classList.toggle('hidden', !resizable);
498
- this.cachedResizable = resizable;
499
- }
500
-
501
472
  const mediaInspectorVisible =
502
473
  this.showMediaInspectorSetting.get() && this.model.type() !== EmulationModel.DeviceModeModel.Type.None;
503
474
  if (mediaInspectorVisible !== this.cachedMediaInspectorVisible) {
@@ -518,7 +489,6 @@ export class DeviceModeView extends UI.Widget.VBox {
518
489
  }
519
490
 
520
491
  this.requestUpdate();
521
- this.toolbar.requestUpdate();
522
492
  if (callDoResize) {
523
493
  this.doResize();
524
494
  }
@@ -545,29 +515,18 @@ export class DeviceModeView extends UI.Widget.VBox {
545
515
  }
546
516
  const zoomFactor = UI.ZoomManager.ZoomManager.instance().zoomFactor();
547
517
  const rect = contentArea.getBoundingClientRect();
518
+ const handleWidth = this.contentElement.querySelector<HTMLElement>('.device-mode-right-resizer')?.offsetWidth || 20;
519
+ const handleHeight =
520
+ this.contentElement.querySelector<HTMLElement>('.device-mode-bottom-resizer')?.offsetHeight || 20;
548
521
  const availableSize =
549
522
  new Geometry.Size(Math.max(rect.width * zoomFactor, 1), Math.max(rect.height * zoomFactor, 1));
550
- const preferredSize = new Geometry.Size(
551
- Math.max((rect.width - 2 * (this.handleWidth || 0)) * zoomFactor, 1),
552
- Math.max((rect.height - (this.handleHeight || 0)) * zoomFactor, 1));
523
+ const preferredSize = new Geometry.Size(Math.max((rect.width - 2 * handleWidth) * zoomFactor, 1),
524
+ Math.max((rect.height - handleHeight) * zoomFactor, 1));
553
525
  this.model.setAvailableSize(availableSize, preferredSize);
554
526
  }
555
527
 
556
- private measureHandles(): void {
557
- const hidden = this.rightResizerElement.classList.contains('hidden');
558
- this.rightResizerElement.classList.toggle('hidden', false);
559
- this.bottomResizerElement.classList.toggle('hidden', false);
560
- this.handleWidth = this.rightResizerElement.offsetWidth;
561
- this.handleHeight = this.bottomResizerElement.offsetHeight;
562
- this.rightResizerElement.classList.toggle('hidden', hidden);
563
- this.bottomResizerElement.classList.toggle('hidden', hidden);
564
- }
565
-
566
528
  private zoomChanged(): void {
567
- delete this.handleWidth;
568
- delete this.handleHeight;
569
529
  if (this.isShowing()) {
570
- this.measureHandles();
571
530
  this.contentAreaResized();
572
531
  }
573
532
  }
@@ -580,8 +539,6 @@ export class DeviceModeView extends UI.Widget.VBox {
580
539
 
581
540
  override wasShown(): void {
582
541
  super.wasShown();
583
- this.measureHandles();
584
- this.toolbar.restore();
585
542
  }
586
543
 
587
544
  override willHide(): void {
@@ -609,11 +566,10 @@ export class DeviceModeView extends UI.Widget.VBox {
609
566
  const contentLeft = screenRect.left + visiblePageRect.left - outlineRect.left;
610
567
  const contentTop = screenRect.top + visiblePageRect.top - outlineRect.top;
611
568
 
612
- const canvas = document.createElement('canvas');
613
- canvas.width = Math.floor(outlineRect.width);
614
- // Cap the height to not hit the GPU limit.
615
- // https://crbug.com/1260828
616
- canvas.height = Math.min((1 << 14), Math.floor(outlineRect.height));
569
+ const canvas = new OffscreenCanvas(Math.floor(outlineRect.width),
570
+ // Cap the height to not hit the GPU limit.
571
+ // https://crbug.com/1260828
572
+ Math.min((1 << 14), Math.floor(outlineRect.height)));
617
573
  const ctx = canvas.getContext('2d', {willReadFrequently: true});
618
574
  if (!ctx) {
619
575
  throw new Error('Could not get 2d context from canvas.');
@@ -627,7 +583,7 @@ export class DeviceModeView extends UI.Widget.VBox {
627
583
  await this.paintImage(ctx, this.model.screenImage(), screenRect.relativeTo(outlineRect));
628
584
  }
629
585
  ctx.drawImage(pageImage, Math.floor(contentLeft), Math.floor(contentTop));
630
- this.saveScreenshot((canvas));
586
+ void this.saveScreenshot(canvas);
631
587
  };
632
588
  }
633
589
 
@@ -651,23 +607,22 @@ export class DeviceModeView extends UI.Widget.VBox {
651
607
  const pageImage = new Image();
652
608
  pageImage.src = 'data:image/png;base64,' + screenshot;
653
609
  pageImage.onload = () => {
654
- const canvas = document.createElement('canvas');
655
- canvas.width = pageImage.naturalWidth;
656
- // Cap the height to not hit the GPU limit.
657
- // https://crbug.com/1260828
658
- canvas.height = Math.min((1 << 14), Math.floor(pageImage.naturalHeight));
610
+ const canvas = new OffscreenCanvas(pageImage.naturalWidth,
611
+ // Cap the height to not hit the GPU limit.
612
+ // https://crbug.com/1260828
613
+ Math.min((1 << 14), Math.floor(pageImage.naturalHeight)));
659
614
  const ctx = canvas.getContext('2d', {willReadFrequently: true});
660
615
  if (!ctx) {
661
616
  throw new Error('Could not get 2d context for base64 screenshot.');
662
617
  }
663
618
  ctx.imageSmoothingEnabled = false;
664
619
  ctx.drawImage(pageImage, 0, 0);
665
- this.saveScreenshot((canvas));
620
+ void this.saveScreenshot(canvas);
666
621
  };
667
622
  }
668
623
 
669
- private paintImage(ctx: CanvasRenderingContext2D, src: string, rect: EmulationModel.DeviceModeModel.Rect):
670
- Promise<void> {
624
+ private paintImage(ctx: OffscreenCanvasRenderingContext2D|CanvasRenderingContext2D, src: string,
625
+ rect: EmulationModel.DeviceModeModel.Rect): Promise<void> {
671
626
  return new Promise(resolve => {
672
627
  const image = new Image();
673
628
  image.crossOrigin = 'Anonymous';
@@ -680,7 +635,7 @@ export class DeviceModeView extends UI.Widget.VBox {
680
635
  });
681
636
  }
682
637
 
683
- private saveScreenshot(canvas: HTMLCanvasElement): void {
638
+ private async saveScreenshot(canvas: OffscreenCanvas): Promise<void> {
684
639
  const url = this.model.inspectedURL();
685
640
  let fileName = '';
686
641
  if (url) {
@@ -692,15 +647,19 @@ export class DeviceModeView extends UI.Widget.VBox {
692
647
  if (device && this.model.type() === EmulationModel.DeviceModeModel.Type.Device) {
693
648
  fileName += `(${device.title})`;
694
649
  }
695
- const link = document.createElement('a');
696
- link.download = fileName + '.png';
697
- canvas.toBlob(blob => {
698
- if (blob === null) {
699
- return;
700
- }
701
- link.href = URL.createObjectURL(blob);
702
- link.click();
650
+ fileName += '.png';
651
+ const blob = await canvas.convertToBlob({type: 'image/png'});
652
+ const dataUrl = await new Promise<string>((resolve, reject) => {
653
+ const reader = new FileReader();
654
+ reader.onloadend = () => resolve(reader.result as string);
655
+ reader.onerror = reject;
656
+ reader.readAsDataURL(blob);
703
657
  });
658
+ const base64 = dataUrl.slice(dataUrl.indexOf(',') + 1);
659
+ const contentData = new TextUtils.ContentData.ContentData(base64, /* isBase64=*/ true, 'image/png');
660
+ await Workspace.FileManager.FileManager.instance().save(fileName as Platform.DevToolsPath.RawPathString,
661
+ contentData, /* forceSaveAs=*/ true);
662
+ Workspace.FileManager.FileManager.instance().close(fileName as Platform.DevToolsPath.RawPathString);
704
663
  }
705
664
  }
706
665