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

Side by Side Diff: third_party/WebKit/Source/core/paint/PaintLayerClipper.cpp

Issue 2238883006: SPv2: Use GeometryMapper to implement PaintLayerClipper. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: none Created 4 years, 2 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 /* 1 /*
2 * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Apple Inc. All rights 2 * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Apple Inc. All rights
3 * reserved. 3 * reserved.
4 * 4 *
5 * Portions are Copyright (C) 1998 Netscape Communications Corporation. 5 * Portions are Copyright (C) 1998 Netscape Communications Corporation.
6 * 6 *
7 * Other contributors: 7 * Other contributors:
8 * Robert O'Callahan <roc+@cs.cmu.edu> 8 * Robert O'Callahan <roc+@cs.cmu.edu>
9 * David Baron <dbaron@fas.harvard.edu> 9 * David Baron <dbaron@fas.harvard.edu>
10 * Christian Biesinger <cbiesinger@web.de> 10 * Christian Biesinger <cbiesinger@web.de>
(...skipping 30 matching lines...) Expand all
41 * If you do not delete the provisions above, a recipient may use your 41 * If you do not delete the provisions above, a recipient may use your
42 * version of this file under any of the LGPL, the MPL or the GPL. 42 * version of this file under any of the LGPL, the MPL or the GPL.
43 */ 43 */
44 44
45 #include "core/paint/PaintLayerClipper.h" 45 #include "core/paint/PaintLayerClipper.h"
46 46
47 #include "core/frame/FrameView.h" 47 #include "core/frame/FrameView.h"
48 #include "core/frame/Settings.h" 48 #include "core/frame/Settings.h"
49 #include "core/layout/LayoutView.h" 49 #include "core/layout/LayoutView.h"
50 #include "core/layout/svg/LayoutSVGRoot.h" 50 #include "core/layout/svg/LayoutSVGRoot.h"
51 #include "core/paint/ObjectPaintProperties.h"
51 #include "core/paint/PaintLayer.h" 52 #include "core/paint/PaintLayer.h"
52 53
53 namespace blink { 54 namespace blink {
54 55
55 static void adjustClipRectsForChildren(const LayoutBoxModelObject& layoutObject, 56 static void adjustClipRectsForChildren(const LayoutBoxModelObject& layoutObject,
56 ClipRects& clipRects) { 57 ClipRects& clipRects) {
57 EPosition position = layoutObject.styleRef().position(); 58 EPosition position = layoutObject.styleRef().position();
58 // A fixed object is essentially the root of its containing block hierarchy, 59 // A fixed object is essentially the root of its containing block hierarchy,
59 // so when we encounter such an object, we reset our clip rects to the 60 // so when we encounter such an object, we reset our clip rects to the
60 // fixedClipRect. 61 // fixedClipRect.
(...skipping 16 matching lines...) Expand all
77 (layoutObject.isSVGRoot() && 78 (layoutObject.isSVGRoot() &&
78 toLayoutSVGRoot(&layoutObject)->shouldApplyViewportClip())); 79 toLayoutSVGRoot(&layoutObject)->shouldApplyViewportClip()));
79 LayoutView* view = layoutObject.view(); 80 LayoutView* view = layoutObject.view();
80 DCHECK(view); 81 DCHECK(view);
81 if (clipRects.fixed() && context.rootLayer->layoutObject() == view) 82 if (clipRects.fixed() && context.rootLayer->layoutObject() == view)
82 offset -= toIntSize(view->frameView()->scrollPosition()); 83 offset -= toIntSize(view->frameView()->scrollPosition());
83 if (layoutObject.hasOverflowClip() || 84 if (layoutObject.hasOverflowClip() ||
84 (layoutObject.isSVGRoot() && 85 (layoutObject.isSVGRoot() &&
85 toLayoutSVGRoot(&layoutObject)->shouldApplyViewportClip()) || 86 toLayoutSVGRoot(&layoutObject)->shouldApplyViewportClip()) ||
86 (layoutObject.styleRef().containsPaint() && layoutObject.isBox())) { 87 (layoutObject.styleRef().containsPaint() && layoutObject.isBox())) {
87 ClipRect newOverflowClip = 88 ClipRect newOverflowClip;
89 newOverflowClip =
Xianzhu 2016/10/06 00:13:32 Is this change necessary?
chrishtr 2016/10/06 17:11:08 Reverted.
88 toLayoutBox(layoutObject) 90 toLayoutBox(layoutObject)
89 .overflowClipRect(offset, context.overlayScrollbarClipBehavior); 91 .overflowClipRect(offset, context.overlayScrollbarClipBehavior);
90 newOverflowClip.setHasRadius(layoutObject.styleRef().hasBorderRadius()); 92 newOverflowClip.setHasRadius(layoutObject.styleRef().hasBorderRadius());
91 clipRects.setOverflowClipRect( 93 clipRects.setOverflowClipRect(
92 intersection(newOverflowClip, clipRects.overflowClipRect())); 94 intersection(newOverflowClip, clipRects.overflowClipRect()));
93 if (layoutObject.isPositioned()) 95 if (layoutObject.isPositioned())
94 clipRects.setPosClipRect( 96 clipRects.setPosClipRect(
95 intersection(newOverflowClip, clipRects.posClipRect())); 97 intersection(newOverflowClip, clipRects.posClipRect()));
96 if (layoutObject.isLayoutView()) 98 if (layoutObject.isLayoutView())
97 clipRects.setFixedClipRect( 99 clipRects.setFixedClipRect(
(...skipping 10 matching lines...) Expand all
108 clipRects.setPosClipRect( 110 clipRects.setPosClipRect(
109 intersection(newClip, clipRects.posClipRect()).setIsClippedByClipCss()); 111 intersection(newClip, clipRects.posClipRect()).setIsClippedByClipCss());
110 clipRects.setOverflowClipRect( 112 clipRects.setOverflowClipRect(
111 intersection(newClip, clipRects.overflowClipRect()) 113 intersection(newClip, clipRects.overflowClipRect())
112 .setIsClippedByClipCss()); 114 .setIsClippedByClipCss());
113 clipRects.setFixedClipRect(intersection(newClip, clipRects.fixedClipRect()) 115 clipRects.setFixedClipRect(intersection(newClip, clipRects.fixedClipRect())
114 .setIsClippedByClipCss()); 116 .setIsClippedByClipCss());
115 } 117 }
116 } 118 }
117 119
120 PaintLayerClipper::PaintLayerClipper(const PaintLayer& layer,
121 bool useGeometryMapper)
122 : m_layer(layer),
123 m_geometryMapper(useGeometryMapper ? new GeometryMapper : nullptr) {}
124
118 ClipRects* PaintLayerClipper::clipRectsIfCached( 125 ClipRects* PaintLayerClipper::clipRectsIfCached(
119 const ClipRectsContext& context) const { 126 const ClipRectsContext& context) const {
120 DCHECK(context.usesCache()); 127 DCHECK(context.usesCache());
121 if (!m_layer.clipRectsCache()) 128 if (!m_layer.clipRectsCache())
122 return nullptr; 129 return nullptr;
123 ClipRectsCache::Entry& entry = 130 ClipRectsCache::Entry& entry =
124 m_layer.clipRectsCache()->get(context.cacheSlot()); 131 m_layer.clipRectsCache()->get(context.cacheSlot());
125 // FIXME: We used to ASSERT that we always got a consistent root layer. 132 // FIXME: We used to ASSERT that we always got a consistent root layer.
126 // We should add a test that has an inconsistent root. See 133 // We should add a test that has an inconsistent root. See
127 // http://crbug.com/366118 for an example. 134 // http://crbug.com/366118 for an example.
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
164 // the cache. 171 // the cache.
165 ClipRects* parentClipRects = nullptr; 172 ClipRects* parentClipRects = nullptr;
166 if (context.rootLayer != &m_layer && m_layer.parent()) 173 if (context.rootLayer != &m_layer && m_layer.parent())
167 parentClipRects = &m_layer.parent()->clipper().getClipRects(context); 174 parentClipRects = &m_layer.parent()->clipper().getClipRects(context);
168 RefPtr<ClipRects> clipRects = ClipRects::create(); 175 RefPtr<ClipRects> clipRects = ClipRects::create();
169 calculateClipRects(context, *clipRects); 176 calculateClipRects(context, *clipRects);
170 return storeClipRectsInCache(context, parentClipRects, *clipRects); 177 return storeClipRectsInCache(context, parentClipRects, *clipRects);
171 } 178 }
172 179
173 void PaintLayerClipper::clearClipRectsIncludingDescendants() { 180 void PaintLayerClipper::clearClipRectsIncludingDescendants() {
181 if (m_geometryMapper)
182 m_geometryMapper.reset(new GeometryMapper);
174 m_layer.clearClipRectsCache(); 183 m_layer.clearClipRectsCache();
175 184
176 for (PaintLayer* layer = m_layer.firstChild(); layer; 185 for (PaintLayer* layer = m_layer.firstChild(); layer;
177 layer = layer->nextSibling()) { 186 layer = layer->nextSibling()) {
178 layer->clipper().clearClipRectsIncludingDescendants(); 187 layer->clipper().clearClipRectsIncludingDescendants();
179 } 188 }
180 } 189 }
181 190
182 void PaintLayerClipper::clearClipRectsIncludingDescendants( 191 void PaintLayerClipper::clearClipRectsIncludingDescendants(
183 ClipRectsCacheSlot cacheSlot) { 192 ClipRectsCacheSlot cacheSlot) {
193 if (m_geometryMapper)
194 m_geometryMapper.reset(new GeometryMapper);
195
184 if (ClipRectsCache* cache = m_layer.clipRectsCache()) 196 if (ClipRectsCache* cache = m_layer.clipRectsCache())
185 cache->clear(cacheSlot); 197 cache->clear(cacheSlot);
186 198
187 for (PaintLayer* layer = m_layer.firstChild(); layer; 199 for (PaintLayer* layer = m_layer.firstChild(); layer;
188 layer = layer->nextSibling()) { 200 layer = layer->nextSibling()) {
189 layer->clipper().clearClipRectsIncludingDescendants(cacheSlot); 201 layer->clipper().clearClipRectsIncludingDescendants(cacheSlot);
190 } 202 }
191 } 203 }
192 204
193 LayoutRect PaintLayerClipper::localClipRect( 205 LayoutRect PaintLayerClipper::localClipRect(
194 const PaintLayer* clippingRootLayer) const { 206 const PaintLayer* clippingRootLayer) const {
207 ClipRectsContext context(clippingRootLayer, PaintingClipRects);
208 if (m_geometryMapper) {
209 ClipRect clipRect = applyOverflowClipToBackgroundRectWithGeometryMapper(
210 context, clipRectWithGeometryMapper(context, false));
211
212 // The rect now needs to be transformed to the local space of this PaintLaye r.
Xianzhu 2016/10/06 00:13:32 Nit: 80 chars
chrishtr 2016/10/06 17:11:08 Fixed.
213 bool success = false;
214 FloatRect clippedRectInLocalSpace =
215 m_geometryMapper->mapRectToDestinationSpace(
216 FloatRect(clipRect.rect()), clippingRootLayer->layoutObject()
217 ->objectPaintProperties()
218 ->localBorderBoxProperties()
219 ->propertyTreeState,
220 m_layer.layoutObject()
221 ->objectPaintProperties()
222 ->localBorderBoxProperties()
223 ->propertyTreeState,
224 success);
225 DCHECK(success);
226
227 return LayoutRect(clippedRectInLocalSpace);
228 }
229
195 LayoutRect layerBounds; 230 LayoutRect layerBounds;
196 ClipRect backgroundRect, foregroundRect; 231 ClipRect backgroundRect, foregroundRect;
197 ClipRectsContext context(clippingRootLayer, PaintingClipRects);
198 calculateRects(context, LayoutRect(LayoutRect::infiniteIntRect()), 232 calculateRects(context, LayoutRect(LayoutRect::infiniteIntRect()),
199 layerBounds, backgroundRect, foregroundRect); 233 layerBounds, backgroundRect, foregroundRect);
200 234
201 LayoutRect clipRect = backgroundRect.rect(); 235 LayoutRect clipRect = backgroundRect.rect();
202 // TODO(chrishtr): avoid converting to IntRect and back. 236 // TODO(chrishtr): avoid converting to IntRect and back.
203 if (clipRect == LayoutRect(LayoutRect::infiniteIntRect())) 237 if (clipRect == LayoutRect(LayoutRect::infiniteIntRect()))
204 return clipRect; 238 return clipRect;
205 239
206 LayoutPoint clippingRootOffset; 240 LayoutPoint clippingRootOffset;
207 m_layer.convertToLayerCoords(clippingRootLayer, clippingRootOffset); 241 m_layer.convertToLayerCoords(clippingRootLayer, clippingRootOffset);
208 clipRect.moveBy(-clippingRootOffset); 242 clipRect.moveBy(-clippingRootOffset);
209 243
210 return clipRect; 244 return clipRect;
211 } 245 }
212 246
247 #ifdef CHECK_CLIP_RECTS
248 #define CHECK_RECTS_EQ(expected, actual) \
249 CHECK((expected.isEmpty() && actual.isEmpty()) || expected == actual) \
250 << "expected=" << expected.toString() << " actual=" << actual.toString()
251 #endif
252
253 void PaintLayerClipper::mapLocalToRootWithGeometryMapper(
254 const ClipRectsContext& context,
255 LayoutRect& layoutRect) const {
256 DCHECK(m_geometryMapper);
257 bool success;
258
259 const ObjectPaintProperties::PropertyTreeStateWithOffset*
260 layerBorderBoxProperties = m_layer.layoutObject()
261 ->objectPaintProperties()
262 ->localBorderBoxProperties();
263 FloatRect localRect(layoutRect);
264 localRect.moveBy(FloatPoint(layerBorderBoxProperties->paintOffset));
265
266 layoutRect = LayoutRect(m_geometryMapper->mapRectToDestinationSpace(
267 localRect, layerBorderBoxProperties->propertyTreeState,
268 context.rootLayer->layoutObject()
269 ->objectPaintProperties()
270 ->localBorderBoxProperties()
271 ->propertyTreeState,
272 success));
273 DCHECK(success);
274 }
275
276 void PaintLayerClipper::calculateRectsWithGeometryMapper(
277 const ClipRectsContext& context,
278 const LayoutRect& paintDirtyRect,
279 LayoutRect& layerBounds,
280 ClipRect& backgroundRect,
281 ClipRect& foregroundRect,
282 const LayoutPoint* offsetFromRoot) const {
283 backgroundRect = applyOverflowClipToBackgroundRectWithGeometryMapper(
284 context, clipRectWithGeometryMapper(context, false));
285 backgroundRect.move(
286 context.subPixelAccumulation); // TODO(chrishtr): is this needed?
287 backgroundRect.intersect(paintDirtyRect);
288
289 foregroundRect.move(
290 context.subPixelAccumulation); // TODO(chrishtr): is this needed?
291 foregroundRect = clipRectWithGeometryMapper(context, true);
292 foregroundRect.intersect(paintDirtyRect);
293 LayoutPoint offset;
294 if (offsetFromRoot)
295 offset = *offsetFromRoot;
296 else
297 m_layer.convertToLayerCoords(context.rootLayer, offset);
298 layerBounds = LayoutRect(offset, LayoutSize(m_layer.size()));
299
300 #ifdef CHECK_CLIP_RECTS
301 ClipRect testBackgroundRect, testForegroundRect;
302 LayoutRect testLayerBounds;
303 PaintLayerClipper(m_layer, false)
304 .calculateRects(context, paintDirtyRect, testLayerBounds,
305 testBackgroundRect, testForegroundRect);
306 CHECK_RECTS_EQ(testBackgroundRect, backgroundRect);
307 CHECK_RECTS_EQ(testForegroundRect, foregroundRect);
308 CHECK_RECTS_EQ(testLayerBounds, layerBounds);
309 #endif
310 }
311
213 void PaintLayerClipper::calculateRects( 312 void PaintLayerClipper::calculateRects(
214 const ClipRectsContext& context, 313 const ClipRectsContext& context,
215 const LayoutRect& paintDirtyRect, 314 const LayoutRect& paintDirtyRect,
216 LayoutRect& layerBounds, 315 LayoutRect& layerBounds,
217 ClipRect& backgroundRect, 316 ClipRect& backgroundRect,
218 ClipRect& foregroundRect, 317 ClipRect& foregroundRect,
219 const LayoutPoint* offsetFromRoot) const { 318 const LayoutPoint* offsetFromRoot) const {
319 if (m_geometryMapper) {
320 calculateRectsWithGeometryMapper(context, paintDirtyRect, layerBounds,
321 backgroundRect, foregroundRect,
322 offsetFromRoot);
323 return;
324 }
325
220 bool isClippingRoot = &m_layer == context.rootLayer; 326 bool isClippingRoot = &m_layer == context.rootLayer;
221 LayoutBoxModelObject& layoutObject = *m_layer.layoutObject(); 327 LayoutBoxModelObject& layoutObject = *m_layer.layoutObject();
222 328
223 if (!isClippingRoot && m_layer.parent()) { 329 if (!isClippingRoot && m_layer.parent()) {
224 backgroundRect = backgroundClipRect(context); 330 backgroundRect = backgroundClipRect(context);
225 backgroundRect.move(context.subPixelAccumulation); 331 backgroundRect.move(context.subPixelAccumulation);
226 backgroundRect.intersect(paintDirtyRect); 332 backgroundRect.intersect(paintDirtyRect);
227 } else { 333 } else {
228 backgroundRect = paintDirtyRect; 334 backgroundRect = paintDirtyRect;
229 } 335 }
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after
320 EPosition position) { 426 EPosition position) {
321 if (position == FixedPosition) 427 if (position == FixedPosition)
322 return parentRects.fixedClipRect(); 428 return parentRects.fixedClipRect();
323 429
324 if (position == AbsolutePosition) 430 if (position == AbsolutePosition)
325 return parentRects.posClipRect(); 431 return parentRects.posClipRect();
326 432
327 return parentRects.overflowClipRect(); 433 return parentRects.overflowClipRect();
328 } 434 }
329 435
436 ClipRect PaintLayerClipper::clipRectWithGeometryMapper(
437 const ClipRectsContext& context,
438 bool isForeground) const {
439 DCHECK(m_geometryMapper);
440 LayoutRect source(LayoutRect::infiniteIntRect());
441 bool success = false;
442 const ObjectPaintProperties* properties =
443 m_layer.layoutObject()->objectPaintProperties();
444 PropertyTreeState propertyTreeState =
445 properties->localBorderBoxProperties()->propertyTreeState;
446
447 if (properties->cssClip())
448 propertyTreeState.setClip(properties->cssClip());
449
450 const LayoutObject& layoutObject = *m_layer.layoutObject();
451 if (shouldRespectOverflowClip(context) && isForeground &&
452 (layoutObject.hasOverflowClip() ||
453 layoutObject.styleRef().containsPaint())) {
454 if (properties->overflowClip())
455 propertyTreeState.setClip(properties->overflowClip());
456 }
457
458 const ObjectPaintProperties* ancestorProperties =
459 context.rootLayer->layoutObject()->objectPaintProperties();
460 PropertyTreeState destinationPropertyTreeState =
461 ancestorProperties->localBorderBoxProperties()->propertyTreeState;
462 if (!context.rootLayer->clipper().shouldRespectOverflowClip(context)) {
463 if (ancestorProperties->overflowClip())
464 destinationPropertyTreeState.setClip(ancestorProperties->overflowClip());
465 }
466 FloatRect clippedRectInRootLayerSpace =
467 m_geometryMapper->mapToVisualRectInDestinationSpace(
468 FloatRect(source), propertyTreeState, destinationPropertyTreeState,
469 success);
470 DCHECK(success);
471 return ClipRect(LayoutRect(clippedRectInRootLayerSpace));
472 }
473
474 ClipRect PaintLayerClipper::applyOverflowClipToBackgroundRectWithGeometryMapper(
475 const ClipRectsContext& context,
476 const ClipRect& clip) const {
477 const LayoutObject& layoutObject = *m_layer.layoutObject();
478 FloatRect clipRect(clip.rect());
479 if ((layoutObject.hasOverflowClip() ||
480 layoutObject.styleRef().containsPaint()) &&
481 shouldRespectOverflowClip(context)) {
482 LayoutRect layerBoundsWithVisualOverflow =
483 layoutObject.isLayoutView()
484 ? toLayoutView(layoutObject).viewRect()
485 : toLayoutBox(layoutObject).visualOverflowRect();
486 toLayoutBox(layoutObject)
487 .flipForWritingMode(
488 layerBoundsWithVisualOverflow); // PaintLayer are in physical coord inates, so the overflow has to be flipped.
Xianzhu 2016/10/06 00:13:32 Nit: 80 chars
chrishtr 2016/10/06 17:11:08 Fixed.
489 mapLocalToRootWithGeometryMapper(context, layerBoundsWithVisualOverflow);
490 clipRect.intersect(FloatRect(layerBoundsWithVisualOverflow));
491 }
492
493 return ClipRect(LayoutRect(clipRect));
494 }
495
330 ClipRect PaintLayerClipper::backgroundClipRect( 496 ClipRect PaintLayerClipper::backgroundClipRect(
331 const ClipRectsContext& context) const { 497 const ClipRectsContext& context) const {
498 if (m_geometryMapper) {
499 ClipRect backgroundClipRect = clipRectWithGeometryMapper(context, false);
500 #ifdef CHECK_CLIP_RECTS
501 ClipRect testBackgroundClipRect =
502 PaintLayerClipper(m_layer, false).backgroundClipRect(context);
503 CHECK_RECTS_EQ(testBackgroundClipRect, backgroundClipRect);
504 #endif
505 return backgroundClipRect;
506 }
332 DCHECK(m_layer.parent()); 507 DCHECK(m_layer.parent());
333 LayoutView* layoutView = m_layer.layoutObject()->view(); 508 LayoutView* layoutView = m_layer.layoutObject()->view();
334 DCHECK(layoutView); 509 DCHECK(layoutView);
335 510
336 RefPtr<ClipRects> parentClipRects = ClipRects::create(); 511 RefPtr<ClipRects> parentClipRects = ClipRects::create();
337 if (&m_layer == context.rootLayer) 512 if (&m_layer == context.rootLayer)
338 parentClipRects->reset(LayoutRect(LayoutRect::infiniteIntRect())); 513 parentClipRects->reset(LayoutRect(LayoutRect::infiniteIntRect()));
339 else 514 else
340 m_layer.parent()->clipper().getOrCalculateClipRects(context, 515 m_layer.parent()->clipper().getOrCalculateClipRects(context,
341 *parentClipRects); 516 *parentClipRects);
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after
373 context.respectOverflowClipForViewport == IgnoreOverflowClip) 548 context.respectOverflowClipForViewport == IgnoreOverflowClip)
374 return false; 549 return false;
375 550
376 return true; 551 return true;
377 } 552 }
378 553
379 ClipRects& PaintLayerClipper::paintingClipRects( 554 ClipRects& PaintLayerClipper::paintingClipRects(
380 const PaintLayer* rootLayer, 555 const PaintLayer* rootLayer,
381 ShouldRespectOverflowClipType respectOverflowClip, 556 ShouldRespectOverflowClipType respectOverflowClip,
382 const LayoutSize& subpixelAccumulation) const { 557 const LayoutSize& subpixelAccumulation) const {
558 DCHECK(!m_geometryMapper);
383 ClipRectsContext context(rootLayer, PaintingClipRects, 559 ClipRectsContext context(rootLayer, PaintingClipRects,
384 IgnoreOverlayScrollbarSize, subpixelAccumulation); 560 IgnoreOverlayScrollbarSize, subpixelAccumulation);
385 if (respectOverflowClip == IgnoreOverflowClip) 561 if (respectOverflowClip == IgnoreOverflowClip)
386 context.setIgnoreOverflowClip(); 562 context.setIgnoreOverflowClip();
387 return getClipRects(context); 563 return getClipRects(context);
388 } 564 }
389 565
390 } // namespace blink 566 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698