| Index: cc/CCLayerTreeHostImpl.cpp | 
| =================================================================== | 
| --- cc/CCLayerTreeHostImpl.cpp	(revision 160422) | 
| +++ cc/CCLayerTreeHostImpl.cpp	(working copy) | 
| @@ -52,6 +52,110 @@ | 
|  | 
| namespace cc { | 
|  | 
| +CCPinchZoomViewport::CCPinchZoomViewport() | 
| +    : m_pageScaleFactor(1) | 
| +    , m_pageScaleDelta(1) | 
| +    , m_sentPageScaleDelta(1) | 
| +    , m_minPageScaleFactor(0) | 
| +    , m_maxPageScaleFactor(0) | 
| +{ | 
| +} | 
| + | 
| +float CCPinchZoomViewport::totalPageScaleFactor() const | 
| +{ | 
| +    return m_pageScaleFactor * m_pageScaleDelta; | 
| +} | 
| + | 
| +void CCPinchZoomViewport::setPageScaleDelta(float delta) | 
| +{ | 
| +    // Clamp to the current min/max limits. | 
| +    float totalPageScaleFactor = m_pageScaleFactor * delta; | 
| +    if (m_minPageScaleFactor && totalPageScaleFactor < m_minPageScaleFactor) | 
| +        delta = m_minPageScaleFactor / m_pageScaleFactor; | 
| +    else if (m_maxPageScaleFactor && totalPageScaleFactor > m_maxPageScaleFactor) | 
| +        delta = m_maxPageScaleFactor / m_pageScaleFactor; | 
| + | 
| +    if (delta == m_pageScaleDelta) | 
| +        return; | 
| + | 
| +    m_pageScaleDelta = delta; | 
| +} | 
| + | 
| +bool CCPinchZoomViewport::setPageScaleFactorAndLimits(float pageScaleFactor, float minPageScaleFactor, float maxPageScaleFactor) | 
| +{ | 
| +    ASSERT(pageScaleFactor); | 
| + | 
| +    if (m_sentPageScaleDelta == 1 && pageScaleFactor == m_pageScaleFactor && minPageScaleFactor == m_minPageScaleFactor && maxPageScaleFactor == m_maxPageScaleFactor) | 
| +        return false; | 
| + | 
| +    m_minPageScaleFactor = minPageScaleFactor; | 
| +    m_maxPageScaleFactor = maxPageScaleFactor; | 
| + | 
| +    m_pageScaleFactor = pageScaleFactor; | 
| +    return true; | 
| +} | 
| + | 
| +FloatRect CCPinchZoomViewport::bounds() const | 
| +{ | 
| +    FloatSize scaledViewportSize = m_layoutViewportSize; | 
| +    scaledViewportSize.scale(1 / totalPageScaleFactor()); | 
| + | 
| +    FloatRect bounds(FloatPoint(0, 0), scaledViewportSize); | 
| +    bounds.setLocation(m_pinchViewportScrollDelta); | 
| + | 
| +    return bounds; | 
| +} | 
| + | 
| +FloatSize CCPinchZoomViewport::applyScroll(FloatSize& delta) | 
| +{ | 
| +    FloatSize overflow; | 
| +    FloatRect pinchedBounds = bounds(); | 
| + | 
| +    pinchedBounds.move(delta); | 
| +    if (pinchedBounds.x() < 0) { | 
| +        overflow.setWidth(pinchedBounds.x()); | 
| +        pinchedBounds.setX(0); | 
| +    } | 
| + | 
| +    if (pinchedBounds.y() < 0) { | 
| +        overflow.setHeight(pinchedBounds.y()); | 
| +        pinchedBounds.setY(0); | 
| +    } | 
| + | 
| +    if (pinchedBounds.maxX() > m_layoutViewportSize.width()) { | 
| +        overflow.setWidth( | 
| +            pinchedBounds.maxX() - m_layoutViewportSize.width()); | 
| +        pinchedBounds.move( | 
| +            m_layoutViewportSize.width() - pinchedBounds.maxX(), 0); | 
| +    } | 
| + | 
| +    if (pinchedBounds.maxY() > m_layoutViewportSize.height()) { | 
| +        overflow.setHeight( | 
| +            pinchedBounds.maxY() - m_layoutViewportSize.height()); | 
| +        pinchedBounds.move( | 
| +            0, m_layoutViewportSize.height() - pinchedBounds.maxY()); | 
| +    } | 
| +    m_pinchViewportScrollDelta = pinchedBounds.location(); | 
| + | 
| +    return overflow; | 
| +} | 
| + | 
| +WebTransformationMatrix CCPinchZoomViewport::implTransform() const | 
| +{ | 
| +    WebTransformationMatrix transform; | 
| +    transform.scale(m_pageScaleDelta); | 
| + | 
| +    // If the pinch state is applied in the impl, then push it to the | 
| +    // impl transform, otherwise the scale is handled by WebCore. | 
| +    if (CCSettings::pageScalePinchZoomEnabled()) { | 
| +        transform.scale(m_pageScaleFactor); | 
| +        transform.translate(-m_pinchViewportScrollDelta.x(), | 
| +                            -m_pinchViewportScrollDelta.y()); | 
| +    } | 
| + | 
| +    return transform; | 
| +} | 
| + | 
| class CCLayerTreeHostImplTimeSourceAdapter : public CCTimeSourceClient { | 
| public: | 
| static PassOwnPtr<CCLayerTreeHostImplTimeSourceAdapter> create(CCLayerTreeHostImpl* layerTreeHostImpl, PassRefPtr<CCDelayBasedTimeSource> timeSource) | 
| @@ -120,11 +224,6 @@ | 
| , m_visible(true) | 
| , m_contentsTexturesPurged(false) | 
| , m_memoryAllocationLimitBytes(CCPrioritizedTextureManager::defaultMemoryAllocationLimit()) | 
| -    , m_pageScale(1) | 
| -    , m_pageScaleDelta(1) | 
| -    , m_sentPageScaleDelta(1) | 
| -    , m_minPageScale(0) | 
| -    , m_maxPageScale(0) | 
| , m_backgroundColor(0) | 
| , m_hasTransparentBackground(false) | 
| , m_needsAnimateLayers(false) | 
| @@ -202,10 +301,10 @@ | 
| return; | 
|  | 
| IntSize scrollTotal = flooredIntSize(m_rootScrollLayerImpl->scrollPosition() + m_rootScrollLayerImpl->scrollDelta()); | 
| -    scrollTotal.scale(m_pageScaleDelta); | 
| -    float scaleTotal = m_pageScale * m_pageScaleDelta; | 
| +    scrollTotal.scale(m_pinchZoomViewport.pageScaleDelta()); | 
| +    float scaleTotal = m_pinchZoomViewport.totalPageScaleFactor(); | 
| IntSize scaledContentSize = contentSize(); | 
| -    scaledContentSize.scale(m_pageScaleDelta); | 
| +    scaledContentSize.scale(m_pinchZoomViewport.pageScaleDelta()); | 
|  | 
| m_pageScaleAnimation = CCPageScaleAnimation::create(scrollTotal, scaleTotal, m_deviceViewportSize, scaledContentSize, startTime); | 
|  | 
| @@ -240,6 +339,13 @@ | 
| } | 
| } | 
|  | 
| +void CCLayerTreeHostImpl::updateRootScrollLayerImplTransform() | 
| +{ | 
| +    if (m_rootScrollLayerImpl) { | 
| +        m_rootScrollLayerImpl->setImplTransform(implTransform()); | 
| +    } | 
| +} | 
| + | 
| void CCLayerTreeHostImpl::calculateRenderSurfaceLayerList(CCLayerList& renderSurfaceLayerList) | 
| { | 
| ASSERT(renderSurfaceLayerList.isEmpty()); | 
| @@ -247,6 +353,8 @@ | 
| ASSERT(m_renderer); // For maxTextureSize. | 
|  | 
| { | 
| +        updateRootScrollLayerImplTransform(); | 
| + | 
| TRACE_EVENT0("cc", "CCLayerTreeHostImpl::calcDrawEtc"); | 
| CCLayerTreeHostCommon::calculateDrawTransforms(m_rootLayerImpl.get(), deviceViewportSize(), m_deviceScaleFactor, &m_layerSorter, rendererCapabilities().maxTextureSize, renderSurfaceLayerList); | 
| CCLayerTreeHostCommon::calculateVisibleRects(renderSurfaceLayerList); | 
| @@ -778,6 +886,8 @@ | 
| m_layoutViewportSize = layoutViewportSize; | 
| m_deviceViewportSize = deviceViewportSize; | 
|  | 
| +    m_pinchZoomViewport.setLayoutViewportSize(FloatSize(layoutViewportSize)); | 
| + | 
| updateMaxScrollPosition(); | 
|  | 
| if (m_renderer) | 
| @@ -811,48 +921,34 @@ | 
| updateMaxScrollPosition(); | 
| } | 
|  | 
| +float CCLayerTreeHostImpl::pageScaleFactor() const | 
| +{ | 
| +    return m_pinchZoomViewport.pageScaleFactor(); | 
| +} | 
|  | 
| -void CCLayerTreeHostImpl::setPageScaleFactorAndLimits(float pageScale, float minPageScale, float maxPageScale) | 
| +void CCLayerTreeHostImpl::setPageScaleFactorAndLimits(float pageScaleFactor, float minPageScaleFactor, float maxPageScaleFactor) | 
| { | 
| -    if (!pageScale) | 
| -        return; | 
| +    if (!pageScaleFactor) | 
| +      return; | 
|  | 
| -    if (m_sentPageScaleDelta == 1 && pageScale == m_pageScale && minPageScale == m_minPageScale && maxPageScale == m_maxPageScale) | 
| -        return; | 
| +    float pageScaleChange = pageScaleFactor / m_pinchZoomViewport.pageScaleFactor(); | 
| +    m_pinchZoomViewport.setPageScaleFactorAndLimits(pageScaleFactor, minPageScaleFactor, maxPageScaleFactor); | 
|  | 
| -    m_minPageScale = minPageScale; | 
| -    m_maxPageScale = maxPageScale; | 
| +    if (!CCSettings::pageScalePinchZoomEnabled()) { | 
| +        if (pageScaleChange != 1) | 
| +            adjustScrollsForPageScaleChange(m_rootScrollLayerImpl, pageScaleChange); | 
| +    } | 
|  | 
| -    float pageScaleChange = pageScale / m_pageScale; | 
| -    m_pageScale = pageScale; | 
| - | 
| -    if (pageScaleChange != 1) | 
| -        adjustScrollsForPageScaleChange(m_rootScrollLayerImpl, pageScaleChange); | 
| - | 
| // Clamp delta to limits and refresh display matrix. | 
| -    setPageScaleDelta(m_pageScaleDelta / m_sentPageScaleDelta); | 
| -    m_sentPageScaleDelta = 1; | 
| -    if (m_rootScrollLayerImpl) | 
| -        m_rootScrollLayerImpl->setPageScaleDelta(m_pageScaleDelta); | 
| +    setPageScaleDelta(m_pinchZoomViewport.pageScaleDelta() / m_pinchZoomViewport.sentPageScaleDelta()); | 
| +    m_pinchZoomViewport.setSentPageScaleDelta(1); | 
| } | 
|  | 
| void CCLayerTreeHostImpl::setPageScaleDelta(float delta) | 
| { | 
| -    // Clamp to the current min/max limits. | 
| -    float finalMagnifyScale = m_pageScale * delta; | 
| -    if (m_minPageScale && finalMagnifyScale < m_minPageScale) | 
| -        delta = m_minPageScale / m_pageScale; | 
| -    else if (m_maxPageScale && finalMagnifyScale > m_maxPageScale) | 
| -        delta = m_maxPageScale / m_pageScale; | 
| +    m_pinchZoomViewport.setPageScaleDelta(delta); | 
|  | 
| -    if (delta == m_pageScaleDelta) | 
| -        return; | 
| - | 
| -    m_pageScaleDelta = delta; | 
| - | 
| updateMaxScrollPosition(); | 
| -    if (m_rootScrollLayerImpl) | 
| -        m_rootScrollLayerImpl->setPageScaleDelta(m_pageScaleDelta); | 
| } | 
|  | 
| void CCLayerTreeHostImpl::updateMaxScrollPosition() | 
| @@ -868,11 +964,22 @@ | 
| viewBounds.scale(m_deviceScaleFactor); | 
| } | 
| } | 
| -    viewBounds.scale(1 / m_pageScaleDelta); | 
|  | 
| -    // maxScroll is computed in physical pixels, but scroll positions are in layout pixels. | 
| -    IntSize maxScroll = contentSize() - expandedIntSize(viewBounds); | 
| +    IntSize contentBounds = contentSize(); | 
| +    if (CCSettings::pageScalePinchZoomEnabled()) { | 
| +        // Pinch with pageScale scrolls entirely in layout space.  contentSize | 
| +        // returns the bounds including the page scale factor, so calculate the | 
| +        // pre page-scale layout size here. | 
| +        float pageScaleFactor = m_pinchZoomViewport.pageScaleFactor(); | 
| +        contentBounds.setWidth(contentBounds.width() / pageScaleFactor); | 
| +        contentBounds.setHeight(contentBounds.height() / pageScaleFactor); | 
| +    } else { | 
| +        viewBounds.scale(1 / m_pinchZoomViewport.pageScaleDelta()); | 
| +    } | 
| + | 
| +    IntSize maxScroll = contentBounds - expandedIntSize(viewBounds); | 
| maxScroll.scale(1 / m_deviceScaleFactor); | 
| + | 
| // The viewport may be larger than the contents in some cases, such as | 
| // having a vertical scrollbar but no horizontal overflow. | 
| maxScroll.clampNegativeToZero(); | 
| @@ -960,7 +1067,7 @@ | 
| return ScrollIgnored; | 
| } | 
|  | 
| -static FloatSize scrollLayerWithScreenSpaceDelta(CCLayerImpl& layerImpl, const FloatPoint& screenSpacePoint, const FloatSize& screenSpaceDelta) | 
| +static FloatSize scrollLayerWithScreenSpaceDelta(CCPinchZoomViewport* viewport, CCLayerImpl& layerImpl, const FloatPoint& screenSpacePoint, const FloatSize& screenSpaceDelta) | 
| { | 
| // Layers with non-invertible screen space transforms should not have passed the scroll hit | 
| // test in the first place. | 
| @@ -982,8 +1089,11 @@ | 
|  | 
| // Apply the scroll delta. | 
| FloatSize previousDelta(layerImpl.scrollDelta()); | 
| -    layerImpl.scrollBy(localEndPoint - localStartPoint); | 
| +    FloatSize unscrolled = layerImpl.scrollBy(localEndPoint - localStartPoint); | 
|  | 
| +    if (viewport) | 
| +        viewport->applyScroll(unscrolled); | 
| + | 
| // Calculate the applied scroll delta in screen space coordinates. | 
| FloatPoint actualLocalEndPoint = localStartPoint + layerImpl.scrollDelta() - previousDelta; | 
| FloatPoint actualScreenSpaceEndPoint = CCMathUtil::mapPoint(layerImpl.screenSpaceTransform(), actualLocalEndPoint, endClipped); | 
| @@ -1014,9 +1124,10 @@ | 
| if (!layerImpl->scrollable()) | 
| continue; | 
|  | 
| +        CCPinchZoomViewport* viewport = layerImpl == m_rootScrollLayerImpl ? &m_pinchZoomViewport : 0; | 
| FloatSize appliedDelta; | 
| if (m_scrollDeltaIsInScreenSpace) | 
| -            appliedDelta = scrollLayerWithScreenSpaceDelta(*layerImpl, viewportPoint, pendingDelta); | 
| +            appliedDelta = scrollLayerWithScreenSpaceDelta(viewport, *layerImpl, viewportPoint, pendingDelta); | 
| else | 
| appliedDelta = scrollLayerWithLocalDelta(*layerImpl, pendingDelta); | 
|  | 
| @@ -1081,15 +1192,24 @@ | 
|  | 
| // Keep the center-of-pinch anchor specified by (x, y) in a stable | 
| // position over the course of the magnify. | 
| -    FloatPoint previousScaleAnchor(m_previousPinchAnchor.x() / m_pageScaleDelta, m_previousPinchAnchor.y() / m_pageScaleDelta); | 
| -    setPageScaleDelta(m_pageScaleDelta * magnifyDelta); | 
| -    FloatPoint newScaleAnchor(anchor.x() / m_pageScaleDelta, anchor.y() / m_pageScaleDelta); | 
| +    float pageScaleDelta = m_pinchZoomViewport.pageScaleDelta(); | 
| +    FloatPoint previousScaleAnchor(m_previousPinchAnchor.x() / pageScaleDelta, | 
| +                                   m_previousPinchAnchor.y() / pageScaleDelta); | 
| +    setPageScaleDelta(pageScaleDelta * magnifyDelta); | 
| +    pageScaleDelta = m_pinchZoomViewport.pageScaleDelta(); | 
| +    FloatPoint newScaleAnchor(anchor.x() / pageScaleDelta, anchor.y() / pageScaleDelta); | 
| FloatSize move = previousScaleAnchor - newScaleAnchor; | 
|  | 
| m_previousPinchAnchor = anchor; | 
|  | 
| -    m_rootScrollLayerImpl->scrollBy(roundedIntSize(move)); | 
| +    if (CCSettings::pageScalePinchZoomEnabled()) { | 
| +        // Compute the application of the delta with respect to the current page zoom of the page. | 
| +        move.scale(1 / (m_pinchZoomViewport.pageScaleFactor() * m_deviceScaleFactor)); | 
| +    } | 
|  | 
| +    FloatSize scrollOverflow = CCSettings::pageScalePinchZoomEnabled() ? m_pinchZoomViewport.applyScroll(move) : move; | 
| +    m_rootScrollLayerImpl->scrollBy(roundedIntSize(scrollOverflow)); | 
| + | 
| if (m_rootScrollLayerImpl->scrollbarAnimationController()) | 
| m_rootScrollLayerImpl->scrollbarAnimationController()->didPinchGestureUpdate(); | 
|  | 
| @@ -1111,7 +1231,7 @@ | 
| { | 
| float pageScale = m_pageScaleAnimation->finalPageScale(); | 
| IntSize scrollOffset = m_pageScaleAnimation->finalScrollOffset(); | 
| -    scrollOffset.scale(m_pageScale / pageScale); | 
| +    scrollOffset.scale(m_pinchZoomViewport.pageScaleFactor() / pageScale); | 
| makeScrollAndScaleSet(scrollInfo, scrollOffset, pageScale); | 
| } | 
|  | 
| @@ -1124,27 +1244,27 @@ | 
| // significant amount. This also ensures only one fake delta set will be | 
| // sent. | 
| const float pinchZoomOutSensitivity = 0.95f; | 
| -    if (m_pageScaleDelta > pinchZoomOutSensitivity) | 
| +    if (m_pinchZoomViewport.pageScaleDelta() > pinchZoomOutSensitivity) | 
| return; | 
|  | 
| // Compute where the scroll offset/page scale would be if fully pinch-zoomed | 
| // out from the anchor point. | 
| IntSize scrollBegin = flooredIntSize(m_rootScrollLayerImpl->scrollPosition() + m_rootScrollLayerImpl->scrollDelta()); | 
| -    scrollBegin.scale(m_pageScaleDelta); | 
| -    float scaleBegin = m_pageScale * m_pageScaleDelta; | 
| -    float pageScaleDeltaToSend = m_minPageScale / m_pageScale; | 
| +    scrollBegin.scale(m_pinchZoomViewport.pageScaleDelta()); | 
| +    float scaleBegin = m_pinchZoomViewport.totalPageScaleFactor(); | 
| +    float pageScaleDeltaToSend = m_pinchZoomViewport.minPageScaleFactor() / m_pinchZoomViewport.pageScaleFactor(); | 
| FloatSize scaledContentsSize = contentSize(); | 
| scaledContentsSize.scale(pageScaleDeltaToSend); | 
|  | 
| FloatSize anchor = toSize(m_previousPinchAnchor); | 
| FloatSize scrollEnd = scrollBegin + anchor; | 
| -    scrollEnd.scale(m_minPageScale / scaleBegin); | 
| +    scrollEnd.scale(m_pinchZoomViewport.minPageScaleFactor() / scaleBegin); | 
| scrollEnd -= anchor; | 
| scrollEnd = scrollEnd.shrunkTo(roundedIntSize(scaledContentsSize - m_deviceViewportSize)).expandedTo(FloatSize(0, 0)); | 
| scrollEnd.scale(1 / pageScaleDeltaToSend); | 
| scrollEnd.scale(m_deviceScaleFactor); | 
|  | 
| -    makeScrollAndScaleSet(scrollInfo, roundedIntSize(scrollEnd), m_minPageScale); | 
| +    makeScrollAndScaleSet(scrollInfo, roundedIntSize(scrollEnd), m_pinchZoomViewport.minPageScaleFactor()); | 
| } | 
|  | 
| void CCLayerTreeHostImpl::makeScrollAndScaleSet(CCScrollAndScaleSet* scrollInfo, const IntSize& scrollOffset, float pageScale) | 
| @@ -1157,7 +1277,8 @@ | 
| scroll.scrollDelta = scrollOffset - toSize(m_rootScrollLayerImpl->scrollPosition()); | 
| scrollInfo->scrolls.append(scroll); | 
| m_rootScrollLayerImpl->setSentScrollDelta(scroll.scrollDelta); | 
| -    m_sentPageScaleDelta = scrollInfo->pageScaleDelta = pageScale / m_pageScale; | 
| +    scrollInfo->pageScaleDelta = pageScale / m_pinchZoomViewport.pageScaleFactor(); | 
| +    m_pinchZoomViewport.setSentPageScaleDelta(scrollInfo->pageScaleDelta); | 
| } | 
|  | 
| static void collectScrollDeltas(CCScrollAndScaleSet* scrollInfo, CCLayerImpl* layerImpl) | 
| @@ -1183,20 +1304,31 @@ | 
| OwnPtr<CCScrollAndScaleSet> scrollInfo = adoptPtr(new CCScrollAndScaleSet()); | 
|  | 
| if (m_pinchGestureActive || m_pageScaleAnimation) { | 
| -        m_sentPageScaleDelta = scrollInfo->pageScaleDelta = 1; | 
| -        if (m_pinchGestureActive) | 
| -            computePinchZoomDeltas(scrollInfo.get()); | 
| -        else if (m_pageScaleAnimation.get()) | 
| -            computeDoubleTapZoomDeltas(scrollInfo.get()); | 
| +        scrollInfo->pageScaleDelta = 1; | 
| +        m_pinchZoomViewport.setSentPageScaleDelta(1); | 
| +        // FIXME(aelias): Make these painting optimizations compatible with | 
| +        // compositor-side scaling. | 
| +        if (!CCSettings::pageScalePinchZoomEnabled()) { | 
| +            if (m_pinchGestureActive) | 
| +                computePinchZoomDeltas(scrollInfo.get()); | 
| +            else if (m_pageScaleAnimation.get()) | 
| +                computeDoubleTapZoomDeltas(scrollInfo.get()); | 
| +        } | 
| return scrollInfo.release(); | 
| } | 
|  | 
| collectScrollDeltas(scrollInfo.get(), m_rootLayerImpl.get()); | 
| -    m_sentPageScaleDelta = scrollInfo->pageScaleDelta = m_pageScaleDelta; | 
| +    scrollInfo->pageScaleDelta = m_pinchZoomViewport.pageScaleDelta(); | 
| +    m_pinchZoomViewport.setSentPageScaleDelta(scrollInfo->pageScaleDelta); | 
|  | 
| return scrollInfo.release(); | 
| } | 
|  | 
| +WebTransformationMatrix CCLayerTreeHostImpl::implTransform() const | 
| +{ | 
| +    return m_pinchZoomViewport.implTransform(); | 
| +} | 
| + | 
| void CCLayerTreeHostImpl::setFullRootLayerDamage() | 
| { | 
| if (m_rootLayerImpl) { | 
| @@ -1213,9 +1345,9 @@ | 
|  | 
| IntSize scrollTotal = flooredIntSize(m_rootScrollLayerImpl->scrollPosition() + m_rootScrollLayerImpl->scrollDelta()); | 
|  | 
| -    setPageScaleDelta(m_pageScaleAnimation->pageScaleAtTime(monotonicTime) / m_pageScale); | 
| +    setPageScaleDelta(m_pageScaleAnimation->pageScaleAtTime(monotonicTime) / m_pinchZoomViewport.pageScaleFactor()); | 
| IntSize nextScroll = m_pageScaleAnimation->scrollOffsetAtTime(monotonicTime); | 
| -    nextScroll.scale(1 / m_pageScaleDelta); | 
| +    nextScroll.scale(1 / m_pinchZoomViewport.pageScaleDelta()); | 
| m_rootScrollLayerImpl->scrollBy(nextScroll - scrollTotal); | 
| m_client->setNeedsRedrawOnImplThread(); | 
|  | 
|  |