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

Unified Diff: third_party/WebKit/Source/core/input/TouchEventManager.cpp

Issue 1892653003: Extract touch handling logic from EventHandler (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebased Created 4 years, 8 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 side-by-side diff with in-line comments
Download patch
Index: third_party/WebKit/Source/core/input/TouchEventManager.cpp
diff --git a/third_party/WebKit/Source/core/input/TouchEventManager.cpp b/third_party/WebKit/Source/core/input/TouchEventManager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1bd4f5ebf38f6ecbd2b1399a0f53bf2316c8d4db
--- /dev/null
+++ b/third_party/WebKit/Source/core/input/TouchEventManager.cpp
@@ -0,0 +1,513 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "core/input/TouchEventManager.h"
+
+#include "core/dom/Document.h"
+#include "core/events/TouchEvent.h"
+#include "core/frame/EventHandlerRegistry.h"
+#include "core/frame/FrameHost.h"
+#include "core/frame/FrameView.h"
+#include "core/html/HTMLCanvasElement.h"
+#include "core/input/EventHandler.h"
+#include "core/input/TouchActionUtil.h"
+#include "core/page/ChromeClient.h"
+#include "core/page/Page.h"
+#include "platform/Histogram.h"
+#include "platform/PlatformTouchEvent.h"
+
+
+
+namespace blink {
+
+namespace {
+
+bool hasTouchHandlers(const EventHandlerRegistry& registry)
+{
+ return registry.hasEventHandlers(EventHandlerRegistry::TouchStartOrMoveEventBlocking)
+ || registry.hasEventHandlers(EventHandlerRegistry::TouchStartOrMoveEventPassive)
+ || registry.hasEventHandlers(EventHandlerRegistry::TouchEndOrCancelEventBlocking)
+ || registry.hasEventHandlers(EventHandlerRegistry::TouchEndOrCancelEventPassive);
+}
+
+const AtomicString& touchEventNameForTouchPointState(PlatformTouchPoint::TouchState state)
+{
+ switch (state) {
+ case PlatformTouchPoint::TouchReleased:
+ return EventTypeNames::touchend;
+ case PlatformTouchPoint::TouchCancelled:
+ return EventTypeNames::touchcancel;
+ case PlatformTouchPoint::TouchPressed:
+ return EventTypeNames::touchstart;
+ case PlatformTouchPoint::TouchMoved:
+ return EventTypeNames::touchmove;
+ case PlatformTouchPoint::TouchStationary:
+ // Fall through to default
+ default:
+ ASSERT_NOT_REACHED();
+ return emptyAtom;
+ }
+}
+
+// These offsets change indicies into the ListenerHistogram
+// enumeration. The addition of a series of offsets then
+// produces the resulting ListenerHistogram value.
+const size_t kTouchTargetHistogramRootScrollerOffset = 4;
+const size_t kTouchTargetHistogramScrollableDocumentOffset = 2;
+const size_t kTouchTargetHistogramHandledOffset = 1;
+
+enum TouchTargetAndDispatchResultType {
+ NonRootScrollerNonScrollableNotHandled, // Non-root-scroller, non-scrollable document, not handled.
+ NonRootScrollerNonScrollableHandled, // Non-root-scroller, non-scrollable document, handled application.
+ NonRootScrollerScrollableDocumentNotHandled, // Non-root-scroller, scrollable document, not handled.
+ NonRootScrollerScrollableDocumentHandled, // Non-root-scroller, scrollable document, handled application.
+ RootScrollerNonScrollableNotHandled, // Root-scroller, non-scrollable document, not handled.
+ RootScrollerNonScrollableHandled, // Root-scroller, non-scrollable document, handled.
+ RootScrollerScrollableDocumentNotHandled, // Root-scroller, scrollable document, not handled.
+ RootScrollerScrollableDocumentHandled, // Root-scroller, scrollable document, handled.
+ TouchTargetAndDispatchResultTypeMax,
+};
+
+TouchTargetAndDispatchResultType toTouchTargetHistogramValue(EventTarget* eventTarget, DispatchEventResult dispatchResult)
+{
+ int result = 0;
+ Document* document = nullptr;
+
+ if (const LocalDOMWindow* domWindow = eventTarget->toLocalDOMWindow()) {
+ // Treat the window as a root scroller as well.
+ document = domWindow->document();
+ result += kTouchTargetHistogramRootScrollerOffset;
+ } else if (Node* node = eventTarget->toNode()) {
+ // Report if the target node is the document or body.
+ if (node->isDocumentNode() || static_cast<Node*>(node->document().documentElement()) == node || static_cast<Node*>(node->document().body()) == node) {
+ result += kTouchTargetHistogramRootScrollerOffset;
+ }
+ document = &node->document();
+ }
+
+ if (document) {
+ FrameView* view = document->view();
+ if (view && view->isScrollable())
+ result += kTouchTargetHistogramScrollableDocumentOffset;
+ }
+
+ if (dispatchResult != DispatchEventResult::NotCanceled)
+ result += kTouchTargetHistogramHandledOffset;
+ return static_cast<TouchTargetAndDispatchResultType>(result);
+}
+
+enum TouchEventDispatchResultType {
+ UnhandledTouches, // Unhandled touch events.
+ HandledTouches, // Handled touch events.
+ TouchEventDispatchResultTypeMax,
+};
+
+// Defining this class type local to dispatchTouchEvents() and annotating
+// it with STACK_ALLOCATED(), runs into MSVC(VS 2013)'s C4822 warning
+// that the local class doesn't provide a local definition for 'operator new'.
+// Which it intentionally doesn't and shouldn't.
+//
+// Work around such toolchain bugginess by lifting out the type, thereby
+// taking it out of C4822's reach.
+class ChangedTouches final {
+ STACK_ALLOCATED();
+public:
+ // The touches corresponding to the particular change state this struct
+ // instance represents.
+ Member<TouchList> m_touches;
+
+ using EventTargetSet = HeapHashSet<Member<EventTarget>>;
+ // Set of targets involved in m_touches.
+ EventTargetSet m_targets;
+};
+
+} // namespace
+
+TouchEventManager::TouchEventManager(LocalFrame* frame)
+: m_frame(frame)
+{
+ clear();
+}
+
+TouchEventManager::~TouchEventManager()
+{
+}
+
+WebInputEventResult TouchEventManager::dispatchTouchEvents(
+ const PlatformTouchEvent& event,
+ const HeapVector<TouchInfo>& touchInfos,
+ bool allTouchReleased)
+{
+ bool touchStartOrFirstTouchMove = false;
+ if (event.type() == PlatformEvent::TouchStart) {
+ m_waitingForFirstTouchMove = true;
+ touchStartOrFirstTouchMove = true;
+ } else if (event.type() == PlatformEvent::TouchMove) {
+ touchStartOrFirstTouchMove = m_waitingForFirstTouchMove;
+ m_waitingForFirstTouchMove = false;
+ }
+
+ // Build up the lists to use for the |touches|, |targetTouches| and
+ // |changedTouches| attributes in the JS event. See
+ // http://www.w3.org/TR/touch-events/#touchevent-interface for how these
+ // lists fit together.
+
+ // Holds the complete set of touches on the screen.
+ TouchList* touches = TouchList::create();
+
+ // A different view on the 'touches' list above, filtered and grouped by
+ // event target. Used for the |targetTouches| list in the JS event.
+ using TargetTouchesHeapMap = HeapHashMap<EventTarget*, Member<TouchList>>;
+ TargetTouchesHeapMap touchesByTarget;
+
+ // Array of touches per state, used to assemble the |changedTouches| list.
+ ChangedTouches changedTouches[PlatformTouchPoint::TouchStateEnd];
+
+ for (unsigned i = 0; i < touchInfos.size(); ++i) {
+ const TouchInfo& touchInfo = touchInfos[i];
+ const PlatformTouchPoint& point = touchInfo.point;
+ PlatformTouchPoint::TouchState pointState = point.state();
+
+ if (touchInfo.consumed)
+ continue;
+
+ Touch* touch = Touch::create(
+ touchInfo.targetFrame.get(),
+ touchInfo.touchNode.get(),
+ point.id(),
+ point.screenPos(),
+ touchInfo.adjustedPagePoint,
+ touchInfo.adjustedRadius,
+ point.rotationAngle(),
+ point.force(),
+ touchInfo.region);
+
+ // Ensure this target's touch list exists, even if it ends up empty, so
+ // it can always be passed to TouchEvent::Create below.
+ TargetTouchesHeapMap::iterator targetTouchesIterator = touchesByTarget.find(touchInfo.touchNode.get());
+ if (targetTouchesIterator == touchesByTarget.end()) {
+ touchesByTarget.set(touchInfo.touchNode.get(), TouchList::create());
+ targetTouchesIterator = touchesByTarget.find(touchInfo.touchNode.get());
+ }
+
+ // |touches| and |targetTouches| should only contain information about
+ // touches still on the screen, so if this point is released or
+ // cancelled it will only appear in the |changedTouches| list.
+ if (pointState != PlatformTouchPoint::TouchReleased && pointState != PlatformTouchPoint::TouchCancelled) {
+ touches->append(touch);
+ targetTouchesIterator->value->append(touch);
+ }
+
+ // Now build up the correct list for |changedTouches|.
+ // Note that any touches that are in the TouchStationary state (e.g. if
+ // the user had several points touched but did not move them all) should
+ // never be in the |changedTouches| list so we do not handle them
+ // explicitly here. See https://bugs.webkit.org/show_bug.cgi?id=37609
+ // for further discussion about the TouchStationary state.
+ if (pointState != PlatformTouchPoint::TouchStationary && touchInfo.knownTarget) {
+ ASSERT(pointState < PlatformTouchPoint::TouchStateEnd);
+ if (!changedTouches[pointState].m_touches)
+ changedTouches[pointState].m_touches = TouchList::create();
+ changedTouches[pointState].m_touches->append(touch);
+ changedTouches[pointState].m_targets.add(touchInfo.touchNode);
+ }
+ }
+
+ if (allTouchReleased) {
+ m_touchSequenceDocument.clear();
+ m_touchSequenceUserGestureToken.clear();
+ }
+
+ WebInputEventResult eventResult = WebInputEventResult::NotHandled;
+
+ // Now iterate through the |changedTouches| list and |m_targets| within it,
+ // sending TouchEvents to the targets as required.
+ for (unsigned state = 0; state != PlatformTouchPoint::TouchStateEnd; ++state) {
+ if (!changedTouches[state].m_touches)
+ continue;
+
+ const AtomicString& eventName(touchEventNameForTouchPointState(static_cast<PlatformTouchPoint::TouchState>(state)));
+ for (const auto& eventTarget : changedTouches[state].m_targets) {
+ EventTarget* touchEventTarget = eventTarget;
+ TouchEvent* touchEvent = TouchEvent::create(
+ touches, touchesByTarget.get(touchEventTarget), changedTouches[state].m_touches.get(),
+ eventName, touchEventTarget->toNode()->document().domWindow(),
+ event.getModifiers(), event.cancelable(), event.causesScrollingIfUncanceled(), event.timestamp());
+
+ DispatchEventResult domDispatchResult = touchEventTarget->dispatchEvent(touchEvent);
+
+ // Only report for top level documents with a single touch on
+ // touch-start or the first touch-move.
+ if (touchStartOrFirstTouchMove && touchInfos.size() == 1 && event.cancelable() && !m_frame->document()->ownerElement()) {
+ DEFINE_STATIC_LOCAL(EnumerationHistogram, rootDocumentListenerHistogram, ("Event.Touch.TargetAndDispatchResult", TouchTargetAndDispatchResultTypeMax));
+ rootDocumentListenerHistogram.count(toTouchTargetHistogramValue(eventTarget, domDispatchResult));
+
+ // Count the handled touch starts and first touch moves before and after the page is fully loaded respectively.
+ if (m_frame->document()->isLoadCompleted()) {
+ DEFINE_STATIC_LOCAL(EnumerationHistogram, touchDispositionsAfterPageLoadHistogram, ("Event.Touch.TouchDispositionsAfterPageLoad", TouchEventDispatchResultTypeMax));
+ touchDispositionsAfterPageLoadHistogram.count((domDispatchResult != DispatchEventResult::NotCanceled) ? HandledTouches : UnhandledTouches);
+ } else {
+ DEFINE_STATIC_LOCAL(EnumerationHistogram, touchDispositionsBeforePageLoadHistogram, ("Event.Touch.TouchDispositionsBeforePageLoad", TouchEventDispatchResultTypeMax));
+ touchDispositionsBeforePageLoadHistogram.count((domDispatchResult != DispatchEventResult::NotCanceled) ? HandledTouches : UnhandledTouches);
+ }
+ }
+ eventResult = EventHandler::mergeEventResult(eventResult,
+ EventHandler::toWebInputEventResult(domDispatchResult));
+ }
+ }
+ return eventResult;
+}
+
+void TouchEventManager::updateTargetAndRegionMapsForTouchStarts(
+ HeapVector<TouchInfo>& touchInfos)
+{
+ for (auto& touchInfo : touchInfos) {
+ // Touch events implicitly capture to the touched node, and don't change
+ // active/hover states themselves (Gesture events do). So we only need
+ // to hit-test on touchstart and when the target could be different than
+ // the corresponding pointer event target.
+ if (touchInfo.point.state() == PlatformTouchPoint::TouchPressed) {
+ HitTestRequest::HitTestRequestType hitType = HitTestRequest::TouchEvent | HitTestRequest::ReadOnly | HitTestRequest::Active;
+ LayoutPoint pagePoint = roundedLayoutPoint(m_frame->view()->rootFrameToContents(touchInfo.point.pos()));
+ HitTestResult result;
+ if (!m_touchSequenceDocument) {
+ result = m_frame->eventHandler().hitTestResultAtPoint(pagePoint, hitType);
+ } else if (m_touchSequenceDocument->frame()) {
+ LayoutPoint framePoint = roundedLayoutPoint(m_touchSequenceDocument->frame()->view()->rootFrameToContents(touchInfo.point.pos()));
+ result = EventHandler::hitTestResultInFrame(m_touchSequenceDocument->frame(), framePoint, hitType);
+ } else {
+ continue;
+ }
+
+ Node* node = result.innerNode();
+ if (!node)
+ continue;
+ if (isHTMLCanvasElement(node)) {
+ std::pair<Element*, String> regionInfo = toHTMLCanvasElement(node)->getControlAndIdIfHitRegionExists(result.pointInInnerNodeFrame());
+ if (regionInfo.first)
+ node = regionInfo.first;
+ touchInfo.region = regionInfo.second;
+ }
+ // Touch events should not go to text nodes.
+ if (node->isTextNode())
+ node = FlatTreeTraversal::parent(*node);
+ touchInfo.touchNode = node;
+
+ if (!m_touchSequenceDocument) {
+ // Keep track of which document should receive all touch events
+ // in the active sequence. This must be a single document to
+ // ensure we don't leak Nodes between documents.
+ m_touchSequenceDocument = &(touchInfo.touchNode->document());
+ ASSERT(m_touchSequenceDocument->frame()->view());
+ }
+
+ // Ideally we'd ASSERT(!m_targetForTouchID.contains(point.id())
+ // since we shouldn't get a touchstart for a touch that's already
+ // down. However EventSender allows this to be violated and there's
+ // some tests that take advantage of it. There may also be edge
+ // cases in the browser where this happens.
+ // See http://crbug.com/345372.
+ m_targetForTouchID.set(touchInfo.point.id(), touchInfo.touchNode);
+
+ m_regionForTouchID.set(touchInfo.point.id(), touchInfo.region);
+
+ TouchAction effectiveTouchAction =
+ TouchActionUtil::computeEffectiveTouchAction(
+ *touchInfo.touchNode);
+ if (effectiveTouchAction != TouchActionAuto)
+ m_frame->page()->chromeClient().setTouchAction(effectiveTouchAction);
+ }
+ }
+}
+
+void TouchEventManager::setAllPropertiesOfTouchInfos(
+ HeapVector<TouchInfo>& touchInfos)
+{
+ for (auto& touchInfo : touchInfos) {
+ PlatformTouchPoint::TouchState pointState = touchInfo.point.state();
+ Node* touchNode = nullptr;
+ String regionID;
+
+ if (pointState == PlatformTouchPoint::TouchReleased
+ || pointState == PlatformTouchPoint::TouchCancelled) {
+ // The target should be the original target for this touch, so get
+ // it from the hashmap. As it's a release or cancel we also remove
+ // it from the map.
+ touchNode = m_targetForTouchID.take(touchInfo.point.id());
+ regionID = m_regionForTouchID.take(touchInfo.point.id());
+ } else {
+ // No hittest is performed on move or stationary, since the target
+ // is not allowed to change anyway.
+ touchNode = m_targetForTouchID.get(touchInfo.point.id());
+ regionID = m_regionForTouchID.get(touchInfo.point.id());
+ }
+
+ LocalFrame* targetFrame = nullptr;
+ bool knownTarget = false;
+ if (touchNode) {
+ Document& doc = touchNode->document();
+ // If the target node has moved to a new document while it was being touched,
+ // we can't send events to the new document because that could leak nodes
+ // from one document to another. See http://crbug.com/394339.
+ if (&doc == m_touchSequenceDocument.get()) {
+ targetFrame = doc.frame();
+ knownTarget = true;
+ }
+ }
+ if (!knownTarget) {
+ // If we don't have a target registered for the point it means we've
+ // missed our opportunity to do a hit test for it (due to some
+ // optimization that prevented blink from ever seeing the
+ // touchstart), or that the touch started outside the active touch
+ // sequence document. We should still include the touch in the
+ // Touches list reported to the application (eg. so it can
+ // differentiate between a one and two finger gesture), but we won't
+ // actually dispatch any events for it. Set the target to the
+ // Document so that there's some valid node here. Perhaps this
+ // should really be LocalDOMWindow, but in all other cases the target of
+ // a Touch is a Node so using the window could be a breaking change.
+ // Since we know there was no handler invoked, the specific target
+ // should be completely irrelevant to the application.
+ touchNode = m_touchSequenceDocument;
+ targetFrame = m_touchSequenceDocument->frame();
+ }
+ ASSERT(targetFrame);
+
+ // pagePoint should always be in the target element's document coordinates.
+ FloatPoint pagePoint = targetFrame->view()->rootFrameToContents(
+ touchInfo.point.pos());
+ float scaleFactor = 1.0f / targetFrame->pageZoomFactor();
+
+ touchInfo.touchNode = touchNode;
+ touchInfo.targetFrame = targetFrame;
+ touchInfo.adjustedPagePoint = pagePoint.scaledBy(scaleFactor);
+ touchInfo.adjustedRadius = touchInfo.point.radius().scaledBy(scaleFactor);
+ touchInfo.knownTarget = knownTarget;
+ touchInfo.consumed = false;
+ touchInfo.region = regionID;
+ }
+}
+
+bool TouchEventManager::generateTouchInfosAfterHittest(
+ const PlatformTouchEvent& event,
+ HeapVector<TouchInfo>& touchInfos)
+{
+ bool newTouchSequence = true;
+ bool allTouchReleased = true;
+
+ for (const auto& point : event.touchPoints()) {
+ if (point.state() != PlatformTouchPoint::TouchPressed)
+ newTouchSequence = false;
+ if (point.state() != PlatformTouchPoint::TouchReleased && point.state() != PlatformTouchPoint::TouchCancelled)
+ allTouchReleased = false;
+ }
+ if (newTouchSequence) {
+ // Ideally we'd ASSERT(!m_touchSequenceDocument) here since we should
+ // have cleared the active document when we saw the last release. But we
+ // have some tests that violate this, ClusterFuzz could trigger it, and
+ // there may be cases where the browser doesn't reliably release all
+ // touches. http://crbug.com/345372 tracks this.
+ m_touchSequenceDocument.clear();
+ m_touchSequenceUserGestureToken.clear();
+ }
+
+ ASSERT(m_frame->view());
+ if (m_touchSequenceDocument && (!m_touchSequenceDocument->frame() || !m_touchSequenceDocument->frame()->view())) {
+ // If the active touch document has no frame or view, it's probably being destroyed
+ // so we can't dispatch events.
+ return false;
+ }
+
+ for (const auto& point : event.touchPoints()) {
+ TouchEventManager::TouchInfo touchInfo;
+ touchInfo.point = point;
+ touchInfos.append(touchInfo);
+ }
+
+ updateTargetAndRegionMapsForTouchStarts(touchInfos);
+
+ m_touchPressed = !allTouchReleased;
+
+ // If there's no document receiving touch events, or no handlers on the
+ // document set to receive the events, then we can skip all the rest of
+ // this work.
+ if (!m_touchSequenceDocument || !m_touchSequenceDocument->frameHost() || !hasTouchHandlers(m_touchSequenceDocument->frameHost()->eventHandlerRegistry()) || !m_touchSequenceDocument->frame()) {
+ if (allTouchReleased) {
+ m_touchSequenceDocument.clear();
+ m_touchSequenceUserGestureToken.clear();
+ }
+ return false;
+ }
+
+ setAllPropertiesOfTouchInfos(touchInfos);
+
+ return true;
+}
+
+WebInputEventResult TouchEventManager::handleTouchEvent(
+ const PlatformTouchEvent& event,
+ const HeapVector<TouchInfo>& touchInfos)
+{
+ // Note that the disposition of any pointer events affects only the generation of touch
+ // events. If all pointer events were handled (and hence no touch events were fired), that
+ // is still equivalent to the touch events going unhandled because pointer event handler
+ // don't block scroll gesture generation.
+
+ // TODO(crbug.com/507408): If PE handlers always call preventDefault, we won't see TEs until after
+ // scrolling starts because the scrolling would suppress upcoming PEs. This sudden "break" in TE
+ // suppression can make the visible TEs inconsistent (e.g. touchmove without a touchstart).
+
+ bool allTouchReleased = true;
+ for (const auto& point : event.touchPoints()) {
+ if (point.state() != PlatformTouchPoint::TouchReleased
+ && point.state() != PlatformTouchPoint::TouchCancelled)
+ allTouchReleased = false;
+ }
+
+ // Whether a touch should be considered a "user gesture" or not is a tricky question.
+ // https://docs.google.com/document/d/1oF1T3O7_E4t1PYHV6gyCwHxOi3ystm0eSL5xZu7nvOg/edit#
+ // TODO(rbyers): Disable user gesture in some cases but retain logging for now (crbug.com/582140).
+ OwnPtr<UserGestureIndicator> gestureIndicator;
+ if (event.touchPoints().size() == 1
+ && event.touchPoints()[0].state() == PlatformTouchPoint::TouchReleased
+ && !event.causesScrollingIfUncanceled()) {
+ // This is a touchend corresponding to a tap, definitely a user gesture. So don't supply
+ // a UserGestureUtilizedCallback.
+ gestureIndicator = adoptPtr(new UserGestureIndicator(DefinitelyProcessingUserGesture));
+ } else {
+ // This is some other touch event that perhaps shouldn't be considered a user gesture. So
+ // use a UserGestureUtilizedCallback to get metrics / deprecation warnings.
+ if (m_touchSequenceUserGestureToken)
+ gestureIndicator = adoptPtr(new UserGestureIndicator(m_touchSequenceUserGestureToken.release(), &m_touchSequenceDocument->frame()->eventHandler()));
+ else
+ gestureIndicator = adoptPtr(new UserGestureIndicator(DefinitelyProcessingUserGesture, &m_touchSequenceDocument->frame()->eventHandler()));
+ m_touchSequenceUserGestureToken = UserGestureIndicator::currentToken();
+ }
+
+ return dispatchTouchEvents(event, touchInfos, allTouchReleased);
+}
+
+void TouchEventManager::clear()
+{
+ m_touchSequenceDocument.clear();
+ m_touchSequenceUserGestureToken.clear();
+ m_targetForTouchID.clear();
+ m_regionForTouchID.clear();
+ m_touchPressed = false;
+ m_waitingForFirstTouchMove = false;
+}
+
+bool TouchEventManager::isAnyTouchActive() const
+{
+ return m_touchPressed;
+}
+
+DEFINE_TRACE(TouchEventManager)
+{
+ visitor->trace(m_frame);
+ visitor->trace(m_touchSequenceDocument);
+ visitor->trace(m_targetForTouchID);
+}
+
+} // namespace blink

Powered by Google App Engine
This is Rietveld 408576698