Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(334)

Side by Side Diff: third_party/WebKit/Source/devtools/front_end/emulation/DeviceModeModel.js

Issue 2626143004: DevTools: move from Common module - Geometry and CSSShadowModel (Closed)
Patch Set: minimize test diff Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2015 The Chromium Authors. All rights reserved. 1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 /** 4 /**
5 * @implements {SDK.TargetManager.Observer} 5 * @implements {SDK.TargetManager.Observer}
6 * @unrestricted 6 * @unrestricted
7 */ 7 */
8 Emulation.DeviceModeModel = class { 8 Emulation.DeviceModeModel = class {
9 /** 9 /**
10 * @param {function()} updateCallback 10 * @param {function()} updateCallback
11 */ 11 */
12 constructor(updateCallback) { 12 constructor(updateCallback) {
13 this._updateCallback = updateCallback; 13 this._updateCallback = updateCallback;
14 this._screenRect = new Common.Rect(0, 0, 1, 1); 14 this._screenRect = new UI.Rect(0, 0, 1, 1);
15 this._visiblePageRect = new Common.Rect(0, 0, 1, 1); 15 this._visiblePageRect = new UI.Rect(0, 0, 1, 1);
16 this._availableSize = new Size(1, 1); 16 this._availableSize = new UI.Size(1, 1);
17 this._preferredSize = new Size(1, 1); 17 this._preferredSize = new UI.Size(1, 1);
18 this._initialized = false; 18 this._initialized = false;
19 this._deviceMetricsThrottler = new Common.Throttler(0); 19 this._deviceMetricsThrottler = new Common.Throttler(0);
20 this._appliedDeviceSize = new Size(1, 1); 20 this._appliedDeviceSize = new UI.Size(1, 1);
21 this._appliedDeviceScaleFactor = window.devicePixelRatio; 21 this._appliedDeviceScaleFactor = window.devicePixelRatio;
22 this._appliedUserAgentType = Emulation.DeviceModeModel.UA.Desktop; 22 this._appliedUserAgentType = Emulation.DeviceModeModel.UA.Desktop;
23 23
24 this._scaleSetting = Common.settings.createSetting('emulation.deviceScale', 1); 24 this._scaleSetting = Common.settings.createSetting('emulation.deviceScale', 1);
25 // We've used to allow zero before. 25 // We've used to allow zero before.
26 if (!this._scaleSetting.get()) 26 if (!this._scaleSetting.get())
27 this._scaleSetting.set(1); 27 this._scaleSetting.set(1);
28 this._scaleSetting.addChangeListener(this._scaleSettingChanged, this); 28 this._scaleSetting.addChangeListener(this._scaleSettingChanged, this);
29 29
30 this._widthSetting = Common.settings.createSetting('emulation.deviceWidth', 400); 30 this._widthSetting = Common.settings.createSetting('emulation.deviceWidth', 400);
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after
80 * @param {string} value 80 * @param {string} value
81 * @return {boolean} 81 * @return {boolean}
82 */ 82 */
83 static deviceScaleFactorValidator(value) { 83 static deviceScaleFactorValidator(value) {
84 if (!value || (/^[\d]+(\.\d+)?|\.\d+$/.test(value) && value >= 0 && value <= 10)) 84 if (!value || (/^[\d]+(\.\d+)?|\.\d+$/.test(value) && value >= 0 && value <= 10))
85 return true; 85 return true;
86 return false; 86 return false;
87 } 87 }
88 88
89 /** 89 /**
90 * @param {!Size} availableSize 90 * @param {!UI.Size} availableSize
91 * @param {!Size} preferredSize 91 * @param {!UI.Size} preferredSize
92 */ 92 */
93 setAvailableSize(availableSize, preferredSize) { 93 setAvailableSize(availableSize, preferredSize) {
94 this._availableSize = availableSize; 94 this._availableSize = availableSize;
95 this._preferredSize = preferredSize; 95 this._preferredSize = preferredSize;
96 this._initialized = true; 96 this._initialized = true;
97 this._calculateAndEmulate(false); 97 this._calculateAndEmulate(false);
98 } 98 }
99 99
100 /** 100 /**
101 * @param {!Emulation.DeviceModeModel.Type} type 101 * @param {!Emulation.DeviceModeModel.Type} type
(...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after
203 203
204 /** 204 /**
205 * @return {string} 205 * @return {string}
206 */ 206 */
207 outlineImage() { 207 outlineImage() {
208 return (this._device && this._mode && this._deviceOutlineSetting.get()) ? th is._device.outlineImage(this._mode) : 208 return (this._device && this._mode && this._deviceOutlineSetting.get()) ? th is._device.outlineImage(this._mode) :
209 '' ; 209 '' ;
210 } 210 }
211 211
212 /** 212 /**
213 * @return {!Common.Rect} 213 * @return {!UI.Rect}
214 */ 214 */
215 outlineRect() { 215 outlineRect() {
216 return this._outlineRect; 216 return this._outlineRect;
217 } 217 }
218 218
219 /** 219 /**
220 * @return {!Common.Rect} 220 * @return {!UI.Rect}
221 */ 221 */
222 screenRect() { 222 screenRect() {
223 return this._screenRect; 223 return this._screenRect;
224 } 224 }
225 225
226 /** 226 /**
227 * @return {!Common.Rect} 227 * @return {!UI.Rect}
228 */ 228 */
229 visiblePageRect() { 229 visiblePageRect() {
230 return this._visiblePageRect; 230 return this._visiblePageRect;
231 } 231 }
232 232
233 /** 233 /**
234 * @return {number} 234 * @return {number}
235 */ 235 */
236 scale() { 236 scale() {
237 return this._scale; 237 return this._scale;
238 } 238 }
239 239
240 /** 240 /**
241 * @return {number} 241 * @return {number}
242 */ 242 */
243 fitScale() { 243 fitScale() {
244 return this._fitScale; 244 return this._fitScale;
245 } 245 }
246 246
247 /** 247 /**
248 * @return {!Size} 248 * @return {!UI.Size}
249 */ 249 */
250 appliedDeviceSize() { 250 appliedDeviceSize() {
251 return this._appliedDeviceSize; 251 return this._appliedDeviceSize;
252 } 252 }
253 253
254 /** 254 /**
255 * @return {number} 255 * @return {number}
256 */ 256 */
257 appliedDeviceScaleFactor() { 257 appliedDeviceScaleFactor() {
258 return this._appliedDeviceScaleFactor; 258 return this._appliedDeviceScaleFactor;
(...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after
364 } 364 }
365 365
366 /** 366 /**
367 * @return {number} 367 * @return {number}
368 */ 368 */
369 _preferredScaledHeight() { 369 _preferredScaledHeight() {
370 return Math.floor(this._preferredSize.height / (this._scaleSetting.get() || 1)); 370 return Math.floor(this._preferredSize.height / (this._scaleSetting.get() || 1));
371 } 371 }
372 372
373 /** 373 /**
374 * @return {!Insets} 374 * @return {!UI.Insets}
375 */ 375 */
376 _currentOutline() { 376 _currentOutline() {
377 var outline = new Insets(0, 0, 0, 0); 377 var outline = new UI.Insets(0, 0, 0, 0);
378 if (this._type !== Emulation.DeviceModeModel.Type.Device) 378 if (this._type !== Emulation.DeviceModeModel.Type.Device)
379 return outline; 379 return outline;
380 var orientation = this._device.orientationByName(this._mode.orientation); 380 var orientation = this._device.orientationByName(this._mode.orientation);
381 if (this._deviceOutlineSetting.get()) 381 if (this._deviceOutlineSetting.get())
382 outline = orientation.outlineInsets || outline; 382 outline = orientation.outlineInsets || outline;
383 return outline; 383 return outline;
384 } 384 }
385 385
386 /** 386 /**
387 * @return {!Insets} 387 * @return {!UI.Insets}
388 */ 388 */
389 _currentInsets() { 389 _currentInsets() {
390 if (this._type !== Emulation.DeviceModeModel.Type.Device) 390 if (this._type !== Emulation.DeviceModeModel.Type.Device)
391 return new Insets(0, 0, 0, 0); 391 return new UI.Insets(0, 0, 0, 0);
392 return this._mode.insets; 392 return this._mode.insets;
393 } 393 }
394 394
395 /** 395 /**
396 * @param {boolean} resetPageScaleFactor 396 * @param {boolean} resetPageScaleFactor
397 */ 397 */
398 _calculateAndEmulate(resetPageScaleFactor) { 398 _calculateAndEmulate(resetPageScaleFactor) {
399 if (!this._target) 399 if (!this._target)
400 this._onTargetAvailable = this._calculateAndEmulate.bind(this, resetPageSc aleFactor); 400 this._onTargetAvailable = this._calculateAndEmulate.bind(this, resetPageSc aleFactor);
401 401
402 if (this._type === Emulation.DeviceModeModel.Type.Device) { 402 if (this._type === Emulation.DeviceModeModel.Type.Device) {
403 var orientation = this._device.orientationByName(this._mode.orientation); 403 var orientation = this._device.orientationByName(this._mode.orientation);
404 var outline = this._currentOutline(); 404 var outline = this._currentOutline();
405 var insets = this._currentInsets(); 405 var insets = this._currentInsets();
406 this._fitScale = this._calculateFitScale(orientation.width, orientation.he ight, outline, insets); 406 this._fitScale = this._calculateFitScale(orientation.width, orientation.he ight, outline, insets);
407 if (this._device.mobile()) { 407 if (this._device.mobile()) {
408 this._appliedUserAgentType = 408 this._appliedUserAgentType =
409 this._device.touch() ? Emulation.DeviceModeModel.UA.Mobile : Emulati on.DeviceModeModel.UA.MobileNoTouch; 409 this._device.touch() ? Emulation.DeviceModeModel.UA.Mobile : Emulati on.DeviceModeModel.UA.MobileNoTouch;
410 } else { 410 } else {
411 this._appliedUserAgentType = 411 this._appliedUserAgentType =
412 this._device.touch() ? Emulation.DeviceModeModel.UA.DesktopTouch : E mulation.DeviceModeModel.UA.Desktop; 412 this._device.touch() ? Emulation.DeviceModeModel.UA.DesktopTouch : E mulation.DeviceModeModel.UA.Desktop;
413 } 413 }
414 this._applyDeviceMetrics( 414 this._applyDeviceMetrics(
415 new Size(orientation.width, orientation.height), insets, outline, this ._scaleSetting.get(), 415 new UI.Size(orientation.width, orientation.height), insets, outline, t his._scaleSetting.get(),
416 this._device.deviceScaleFactor, this._device.mobile(), 416 this._device.deviceScaleFactor, this._device.mobile(),
417 this._mode.orientation === Emulation.EmulatedDevice.Horizontal ? 'land scapePrimary' : 'portraitPrimary', 417 this._mode.orientation === Emulation.EmulatedDevice.Horizontal ? 'land scapePrimary' : 'portraitPrimary',
418 resetPageScaleFactor); 418 resetPageScaleFactor);
419 this._applyUserAgent(this._device.userAgent); 419 this._applyUserAgent(this._device.userAgent);
420 this._applyTouch(this._device.touch(), this._device.mobile()); 420 this._applyTouch(this._device.touch(), this._device.mobile());
421 } else if (this._type === Emulation.DeviceModeModel.Type.None) { 421 } else if (this._type === Emulation.DeviceModeModel.Type.None) {
422 this._fitScale = this._calculateFitScale(this._availableSize.width, this._ availableSize.height); 422 this._fitScale = this._calculateFitScale(this._availableSize.width, this._ availableSize.height);
423 this._appliedUserAgentType = Emulation.DeviceModeModel.UA.Desktop; 423 this._appliedUserAgentType = Emulation.DeviceModeModel.UA.Desktop;
424 this._applyDeviceMetrics( 424 this._applyDeviceMetrics(
425 this._availableSize, new Insets(0, 0, 0, 0), new Insets(0, 0, 0, 0), 1 , 0, false, '', resetPageScaleFactor); 425 this._availableSize, new UI.Insets(0, 0, 0, 0), new UI.Insets(0, 0, 0, 0), 1, 0, false, '',
426 resetPageScaleFactor);
426 this._applyUserAgent(''); 427 this._applyUserAgent('');
427 this._applyTouch(false, false); 428 this._applyTouch(false, false);
428 } else if (this._type === Emulation.DeviceModeModel.Type.Responsive) { 429 } else if (this._type === Emulation.DeviceModeModel.Type.Responsive) {
429 var screenWidth = this._widthSetting.get(); 430 var screenWidth = this._widthSetting.get();
430 if (!screenWidth || screenWidth > this._preferredScaledWidth()) 431 if (!screenWidth || screenWidth > this._preferredScaledWidth())
431 screenWidth = this._preferredScaledWidth(); 432 screenWidth = this._preferredScaledWidth();
432 var screenHeight = this._heightSetting.get(); 433 var screenHeight = this._heightSetting.get();
433 if (!screenHeight || screenHeight > this._preferredScaledHeight()) 434 if (!screenHeight || screenHeight > this._preferredScaledHeight())
434 screenHeight = this._preferredScaledHeight(); 435 screenHeight = this._preferredScaledHeight();
435 var mobile = this._uaSetting.get() === Emulation.DeviceModeModel.UA.Mobile || 436 var mobile = this._uaSetting.get() === Emulation.DeviceModeModel.UA.Mobile ||
436 this._uaSetting.get() === Emulation.DeviceModeModel.UA.MobileNoTouch; 437 this._uaSetting.get() === Emulation.DeviceModeModel.UA.MobileNoTouch;
437 var defaultDeviceScaleFactor = mobile ? Emulation.DeviceModeModel.defaultM obileScaleFactor : 0; 438 var defaultDeviceScaleFactor = mobile ? Emulation.DeviceModeModel.defaultM obileScaleFactor : 0;
438 this._fitScale = this._calculateFitScale(this._widthSetting.get(), this._h eightSetting.get()); 439 this._fitScale = this._calculateFitScale(this._widthSetting.get(), this._h eightSetting.get());
439 this._appliedUserAgentType = this._uaSetting.get(); 440 this._appliedUserAgentType = this._uaSetting.get();
440 this._applyDeviceMetrics( 441 this._applyDeviceMetrics(
441 new Size(screenWidth, screenHeight), new Insets(0, 0, 0, 0), new Inset s(0, 0, 0, 0), this._scaleSetting.get(), 442 new UI.Size(screenWidth, screenHeight), new UI.Insets(0, 0, 0, 0), new UI.Insets(0, 0, 0, 0),
442 this._deviceScaleFactorSetting.get() || defaultDeviceScaleFactor, mobi le, 443 this._scaleSetting.get(), this._deviceScaleFactorSetting.get() || defa ultDeviceScaleFactor, mobile,
443 screenHeight >= screenWidth ? 'portraitPrimary' : 'landscapePrimary', resetPageScaleFactor); 444 screenHeight >= screenWidth ? 'portraitPrimary' : 'landscapePrimary', resetPageScaleFactor);
444 this._applyUserAgent(mobile ? Emulation.DeviceModeModel._defaultMobileUser Agent : ''); 445 this._applyUserAgent(mobile ? Emulation.DeviceModeModel._defaultMobileUser Agent : '');
445 this._applyTouch( 446 this._applyTouch(
446 this._uaSetting.get() === Emulation.DeviceModeModel.UA.DesktopTouch || 447 this._uaSetting.get() === Emulation.DeviceModeModel.UA.DesktopTouch ||
447 this._uaSetting.get() === Emulation.DeviceModeModel.UA.Mobile, 448 this._uaSetting.get() === Emulation.DeviceModeModel.UA.Mobile,
448 this._uaSetting.get() === Emulation.DeviceModeModel.UA.Mobile); 449 this._uaSetting.get() === Emulation.DeviceModeModel.UA.Mobile);
449 } 450 }
450 if (this._target) 451 if (this._target)
451 this._target.renderingAgent().setShowViewportSizeOnResize(this._type === E mulation.DeviceModeModel.Type.None); 452 this._target.renderingAgent().setShowViewportSizeOnResize(this._type === E mulation.DeviceModeModel.Type.None);
452 this._updateCallback.call(null); 453 this._updateCallback.call(null);
453 } 454 }
454 455
455 /** 456 /**
456 * @param {number} screenWidth 457 * @param {number} screenWidth
457 * @param {number} screenHeight 458 * @param {number} screenHeight
458 * @param {!Insets=} outline 459 * @param {!UI.Insets=} outline
459 * @param {!Insets=} insets 460 * @param {!UI.Insets=} insets
460 * @return {number} 461 * @return {number}
461 */ 462 */
462 _calculateFitScale(screenWidth, screenHeight, outline, insets) { 463 _calculateFitScale(screenWidth, screenHeight, outline, insets) {
463 var outlineWidth = outline ? outline.left + outline.right : 0; 464 var outlineWidth = outline ? outline.left + outline.right : 0;
464 var outlineHeight = outline ? outline.top + outline.bottom : 0; 465 var outlineHeight = outline ? outline.top + outline.bottom : 0;
465 var insetsWidth = insets ? insets.left + insets.right : 0; 466 var insetsWidth = insets ? insets.left + insets.right : 0;
466 var insetsHeight = insets ? insets.top + insets.bottom : 0; 467 var insetsHeight = insets ? insets.top + insets.bottom : 0;
467 var scale = Math.min( 468 var scale = Math.min(
468 screenWidth ? this._preferredSize.width / (screenWidth + outlineWidth) : 1, 469 screenWidth ? this._preferredSize.width / (screenWidth + outlineWidth) : 1,
469 screenHeight ? this._preferredSize.height / (screenHeight + outlineHeigh t) : 1); 470 screenHeight ? this._preferredSize.height / (screenHeight + outlineHeigh t) : 1);
(...skipping 23 matching lines...) Expand all
493 } 494 }
494 495
495 /** 496 /**
496 * @param {string} userAgent 497 * @param {string} userAgent
497 */ 498 */
498 _applyUserAgent(userAgent) { 499 _applyUserAgent(userAgent) {
499 SDK.multitargetNetworkManager.setUserAgentOverride(userAgent); 500 SDK.multitargetNetworkManager.setUserAgentOverride(userAgent);
500 } 501 }
501 502
502 /** 503 /**
503 * @param {!Size} screenSize 504 * @param {!UI.Size} screenSize
504 * @param {!Insets} insets 505 * @param {!UI.Insets} insets
505 * @param {!Insets} outline 506 * @param {!UI.Insets} outline
506 * @param {number} scale 507 * @param {number} scale
507 * @param {number} deviceScaleFactor 508 * @param {number} deviceScaleFactor
508 * @param {boolean} mobile 509 * @param {boolean} mobile
509 * @param {string} screenOrientation 510 * @param {string} screenOrientation
510 * @param {boolean} resetPageScaleFactor 511 * @param {boolean} resetPageScaleFactor
511 */ 512 */
512 _applyDeviceMetrics( 513 _applyDeviceMetrics(
513 screenSize, 514 screenSize,
514 insets, 515 insets,
515 outline, 516 outline,
516 scale, 517 scale,
517 deviceScaleFactor, 518 deviceScaleFactor,
518 mobile, 519 mobile,
519 screenOrientation, 520 screenOrientation,
520 resetPageScaleFactor) { 521 resetPageScaleFactor) {
521 screenSize.width = Math.max(1, Math.floor(screenSize.width)); 522 screenSize.width = Math.max(1, Math.floor(screenSize.width));
522 screenSize.height = Math.max(1, Math.floor(screenSize.height)); 523 screenSize.height = Math.max(1, Math.floor(screenSize.height));
523 524
524 var pageWidth = screenSize.width - insets.left - insets.right; 525 var pageWidth = screenSize.width - insets.left - insets.right;
525 var pageHeight = screenSize.height - insets.top - insets.bottom; 526 var pageHeight = screenSize.height - insets.top - insets.bottom;
526 527
527 var positionX = insets.left; 528 var positionX = insets.left;
528 var positionY = insets.top; 529 var positionY = insets.top;
529 var screenOrientationAngle = screenOrientation === 'landscapePrimary' ? 90 : 0; 530 var screenOrientationAngle = screenOrientation === 'landscapePrimary' ? 90 : 0;
530 531
531 this._appliedDeviceSize = screenSize; 532 this._appliedDeviceSize = screenSize;
532 this._appliedDeviceScaleFactor = deviceScaleFactor || window.devicePixelRati o; 533 this._appliedDeviceScaleFactor = deviceScaleFactor || window.devicePixelRati o;
533 this._screenRect = new Common.Rect( 534 this._screenRect = new UI.Rect(
534 Math.max(0, (this._availableSize.width - screenSize.width * scale) / 2), outline.top * scale, 535 Math.max(0, (this._availableSize.width - screenSize.width * scale) / 2), outline.top * scale,
535 screenSize.width * scale, screenSize.height * scale); 536 screenSize.width * scale, screenSize.height * scale);
536 this._outlineRect = new Common.Rect( 537 this._outlineRect = new UI.Rect(
537 this._screenRect.left - outline.left * scale, 0, (outline.left + screenS ize.width + outline.right) * scale, 538 this._screenRect.left - outline.left * scale, 0, (outline.left + screenS ize.width + outline.right) * scale,
538 (outline.top + screenSize.height + outline.bottom) * scale); 539 (outline.top + screenSize.height + outline.bottom) * scale);
539 this._visiblePageRect = new Common.Rect( 540 this._visiblePageRect = new UI.Rect(
540 positionX * scale, positionY * scale, 541 positionX * scale, positionY * scale,
541 Math.min(pageWidth * scale, this._availableSize.width - this._screenRect .left - positionX * scale), 542 Math.min(pageWidth * scale, this._availableSize.width - this._screenRect .left - positionX * scale),
542 Math.min(pageHeight * scale, this._availableSize.height - this._screenRe ct.top - positionY * scale)); 543 Math.min(pageHeight * scale, this._availableSize.height - this._screenRe ct.top - positionY * scale));
543 this._scale = scale; 544 this._scale = scale;
544 545
545 if (scale === 1 && this._availableSize.width >= screenSize.width && 546 if (scale === 1 && this._availableSize.width >= screenSize.width &&
546 this._availableSize.height >= screenSize.height) { 547 this._availableSize.height >= screenSize.height) {
547 // When we have enough space, no page size override is required. This will speed things up and remove lag. 548 // When we have enough space, no page size override is required. This will speed things up and remove lag.
548 pageWidth = 0; 549 pageWidth = 0;
549 pageHeight = 0; 550 pageHeight = 0;
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after
626 627
627 Emulation.DeviceModeModel.MinDeviceSize = 50; 628 Emulation.DeviceModeModel.MinDeviceSize = 50;
628 Emulation.DeviceModeModel.MaxDeviceSize = 9999; 629 Emulation.DeviceModeModel.MaxDeviceSize = 9999;
629 630
630 631
631 Emulation.DeviceModeModel._defaultMobileUserAgent = 632 Emulation.DeviceModeModel._defaultMobileUserAgent =
632 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 ( KHTML, like Gecko) Chrome/%s Mobile Safari/537.36'; 633 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 ( KHTML, like Gecko) Chrome/%s Mobile Safari/537.36';
633 Emulation.DeviceModeModel._defaultMobileUserAgent = 634 Emulation.DeviceModeModel._defaultMobileUserAgent =
634 SDK.MultitargetNetworkManager.patchUserAgentWithChromeVersion(Emulation.Devi ceModeModel._defaultMobileUserAgent); 635 SDK.MultitargetNetworkManager.patchUserAgentWithChromeVersion(Emulation.Devi ceModeModel._defaultMobileUserAgent);
635 Emulation.DeviceModeModel.defaultMobileScaleFactor = 2; 636 Emulation.DeviceModeModel.defaultMobileScaleFactor = 2;
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698