| OLD | NEW |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 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 #include "core/dom/IntersectionObservation.h" | 5 #include "core/dom/IntersectionGeometry.h" |
| 6 | 6 |
| 7 #include "core/dom/Element.h" |
| 7 #include "core/dom/ElementRareData.h" | 8 #include "core/dom/ElementRareData.h" |
| 8 #include "core/dom/IntersectionObserver.h" | |
| 9 #include "core/frame/FrameView.h" | 9 #include "core/frame/FrameView.h" |
| 10 #include "core/frame/LocalFrame.h" | 10 #include "core/frame/LocalFrame.h" |
| 11 #include "core/layout/LayoutBox.h" | 11 #include "core/layout/LayoutBox.h" |
| 12 #include "core/layout/LayoutView.h" | 12 #include "core/layout/LayoutView.h" |
| 13 #include "core/layout/api/LayoutAPIShim.h" |
| 14 #include "core/layout/api/LayoutViewItem.h" |
| 13 #include "core/paint/PaintLayer.h" | 15 #include "core/paint/PaintLayer.h" |
| 14 | 16 |
| 15 namespace blink { | 17 namespace blink { |
| 16 | 18 |
| 17 IntersectionObservation::IntersectionObservation(IntersectionObserver& observer, | 19 namespace { |
| 18 Element& target, | 20 |
| 19 bool shouldReportRootBounds) | 21 bool isContainingBlockChainDescendant(LayoutObject* descendant, |
| 20 : m_observer(observer), | 22 LayoutObject* ancestor) { |
| 21 m_target(&target), | 23 LocalFrame* ancestorFrame = ancestor->document().frame(); |
| 22 m_shouldReportRootBounds(shouldReportRootBounds), | 24 LocalFrame* descendantFrame = descendant->document().frame(); |
| 23 m_lastThresholdIndex(0) {} | 25 |
| 24 | 26 if (ancestor->isLayoutView()) |
| 25 void IntersectionObservation::applyRootMargin(LayoutRect& rect) const { | 27 return descendantFrame && descendantFrame->tree().top() == ancestorFrame; |
| 26 if (m_shouldReportRootBounds) | 28 |
| 27 m_observer->applyRootMargin(rect); | 29 if (ancestorFrame != descendantFrame) |
| 28 } | 30 return false; |
| 29 | 31 |
| 30 void IntersectionObservation::initializeGeometry( | 32 while (descendant && descendant != ancestor) |
| 31 IntersectionGeometry& geometry) const { | 33 descendant = descendant->containingBlock(); |
| 32 initializeTargetRect(geometry.targetRect); | 34 return descendant; |
| 33 geometry.intersectionRect = geometry.targetRect; | 35 } |
| 34 initializeRootRect(geometry.rootRect); | 36 |
| 35 geometry.doesIntersect = true; | 37 void mapRectUpToDocument(LayoutRect& rect, |
| 36 } | 38 const LayoutObject& layoutObject, |
| 37 | 39 const Document& document) { |
| 38 void IntersectionObservation::initializeTargetRect(LayoutRect& rect) const { | |
| 39 DCHECK(m_target); | |
| 40 LayoutObject* targetLayoutObject = target()->layoutObject(); | |
| 41 DCHECK(targetLayoutObject && targetLayoutObject->isBoxModelObject()); | |
| 42 rect = LayoutRect( | |
| 43 toLayoutBoxModelObject(targetLayoutObject)->borderBoundingBox()); | |
| 44 } | |
| 45 | |
| 46 void IntersectionObservation::initializeRootRect(LayoutRect& rect) const { | |
| 47 LayoutObject* rootLayoutObject = m_observer->rootLayoutObject(); | |
| 48 if (rootLayoutObject->isLayoutView()) | |
| 49 rect = LayoutRect( | |
| 50 toLayoutView(rootLayoutObject)->frameView()->visibleContentRect()); | |
| 51 else if (rootLayoutObject->isBox() && rootLayoutObject->hasOverflowClip()) | |
| 52 rect = LayoutRect(toLayoutBox(rootLayoutObject)->contentBoxRect()); | |
| 53 else | |
| 54 rect = LayoutRect( | |
| 55 toLayoutBoxModelObject(rootLayoutObject)->borderBoundingBox()); | |
| 56 applyRootMargin(rect); | |
| 57 } | |
| 58 | |
| 59 void IntersectionObservation::clipToRoot(IntersectionGeometry& geometry) const { | |
| 60 // Map and clip rect into root element coordinates. | |
| 61 // TODO(szager): the writing mode flipping needs a test. | |
| 62 DCHECK(m_target); | |
| 63 LayoutBox* rootLayoutObject = toLayoutBox(m_observer->rootLayoutObject()); | |
| 64 LayoutObject* targetLayoutObject = target()->layoutObject(); | |
| 65 | |
| 66 geometry.doesIntersect = targetLayoutObject->mapToVisualRectInAncestorSpace( | |
| 67 rootLayoutObject, geometry.intersectionRect, EdgeInclusive); | |
| 68 if (rootLayoutObject->hasOverflowClip()) | |
| 69 geometry.intersectionRect.move(-rootLayoutObject->scrolledContentOffset()); | |
| 70 | |
| 71 if (!geometry.doesIntersect) | |
| 72 return; | |
| 73 LayoutRect rootClipRect(geometry.rootRect); | |
| 74 rootLayoutObject->flipForWritingMode(rootClipRect); | |
| 75 geometry.doesIntersect &= | |
| 76 geometry.intersectionRect.inclusiveIntersect(rootClipRect); | |
| 77 } | |
| 78 | |
| 79 static void mapRectUpToDocument(LayoutRect& rect, | |
| 80 const LayoutObject& layoutObject, | |
| 81 const Document& document) { | |
| 82 FloatQuad mappedQuad = layoutObject.localToAbsoluteQuad( | 40 FloatQuad mappedQuad = layoutObject.localToAbsoluteQuad( |
| 83 FloatQuad(FloatRect(rect)), UseTransforms | ApplyContainerFlip); | 41 FloatQuad(FloatRect(rect)), UseTransforms | ApplyContainerFlip); |
| 84 rect = LayoutRect(mappedQuad.boundingBox()); | 42 rect = LayoutRect(mappedQuad.boundingBox()); |
| 85 } | 43 } |
| 86 | 44 |
| 87 static void mapRectDownToDocument(LayoutRect& rect, | 45 void mapRectDownToDocument(LayoutRect& rect, |
| 88 LayoutBoxModelObject& layoutObject, | 46 LayoutBoxModelObject& layoutObject, |
| 89 const Document& document) { | 47 const Document& document) { |
| 90 FloatQuad mappedQuad = document.layoutView()->ancestorToLocalQuad( | 48 FloatQuad mappedQuad = document.layoutView()->ancestorToLocalQuad( |
| 91 &layoutObject, FloatQuad(FloatRect(rect)), | 49 &layoutObject, FloatQuad(FloatRect(rect)), |
| 92 UseTransforms | ApplyContainerFlip | TraverseDocumentBoundaries); | 50 UseTransforms | ApplyContainerFlip | TraverseDocumentBoundaries); |
| 93 rect = LayoutRect(mappedQuad.boundingBox()); | 51 rect = LayoutRect(mappedQuad.boundingBox()); |
| 94 } | 52 } |
| 95 | 53 |
| 96 void IntersectionObservation::mapTargetRectToTargetFrameCoordinates( | 54 LayoutUnit computeMargin(const Length& length, LayoutUnit referenceLength) { |
| 97 LayoutRect& rect) const { | 55 if (length.type() == Percent) { |
| 98 LayoutObject& targetLayoutObject = *target()->layoutObject(); | 56 return LayoutUnit( |
| 99 Document& targetDocument = target()->document(); | 57 static_cast<int>(referenceLength.toFloat() * length.percent() / 100.0)); |
| 58 } |
| 59 DCHECK_EQ(length.type(), Fixed); |
| 60 return LayoutUnit(length.intValue()); |
| 61 } |
| 62 |
| 63 } // namespace |
| 64 |
| 65 IntersectionGeometry::IntersectionGeometry( |
| 66 Node* root, |
| 67 Element* target, |
| 68 const Vector<Length>& rootMargin, |
| 69 ReportRootBounds shouldReportRootBounds) |
| 70 : m_root(root), |
| 71 m_target(target), |
| 72 m_rootMargin(rootMargin), |
| 73 m_shouldReportRootBounds(shouldReportRootBounds) { |
| 74 DCHECK(m_target); |
| 75 DCHECK(rootMargin.isEmpty() || rootMargin.size() == 4); |
| 76 } |
| 77 |
| 78 IntersectionGeometry::~IntersectionGeometry() {} |
| 79 |
| 80 Element* IntersectionGeometry::root() const { |
| 81 if (m_root && !m_root->isDocumentNode()) |
| 82 return toElement(m_root); |
| 83 return nullptr; |
| 84 } |
| 85 |
| 86 LayoutObject* IntersectionGeometry::getRootLayoutObject() const { |
| 87 DCHECK(m_root); |
| 88 if (m_root->isDocumentNode()) { |
| 89 return LayoutAPIShim::layoutObjectFrom( |
| 90 toDocument(m_root)->layoutViewItem()); |
| 91 } |
| 92 return toElement(m_root)->layoutObject(); |
| 93 } |
| 94 |
| 95 void IntersectionGeometry::initializeGeometry() { |
| 96 initializeTargetRect(); |
| 97 m_intersectionRect = m_targetRect; |
| 98 initializeRootRect(); |
| 99 m_doesIntersect = true; |
| 100 } |
| 101 |
| 102 void IntersectionGeometry::initializeTargetRect() { |
| 103 LayoutObject* targetLayoutObject = m_target->layoutObject(); |
| 104 DCHECK(targetLayoutObject && targetLayoutObject->isBoxModelObject()); |
| 105 m_targetRect = LayoutRect( |
| 106 toLayoutBoxModelObject(targetLayoutObject)->borderBoundingBox()); |
| 107 } |
| 108 |
| 109 void IntersectionGeometry::initializeRootRect() { |
| 110 LayoutObject* rootLayoutObject = getRootLayoutObject(); |
| 111 if (rootLayoutObject->isLayoutView()) { |
| 112 m_rootRect = LayoutRect( |
| 113 toLayoutView(rootLayoutObject)->frameView()->visibleContentRect()); |
| 114 } else if (rootLayoutObject->isBox() && rootLayoutObject->hasOverflowClip()) { |
| 115 m_rootRect = LayoutRect(toLayoutBox(rootLayoutObject)->contentBoxRect()); |
| 116 } else { |
| 117 m_rootRect = LayoutRect( |
| 118 toLayoutBoxModelObject(rootLayoutObject)->borderBoundingBox()); |
| 119 } |
| 120 applyRootMargin(); |
| 121 } |
| 122 |
| 123 void IntersectionGeometry::applyRootMargin() { |
| 124 if (m_rootMargin.isEmpty()) |
| 125 return; |
| 126 |
| 127 // TODO(szager): Make sure the spec is clear that left/right margins are |
| 128 // resolved against width and not height. |
| 129 LayoutUnit topMargin = computeMargin(m_rootMargin[0], m_rootRect.height()); |
| 130 LayoutUnit rightMargin = computeMargin(m_rootMargin[1], m_rootRect.width()); |
| 131 LayoutUnit bottomMargin = computeMargin(m_rootMargin[2], m_rootRect.height()); |
| 132 LayoutUnit leftMargin = computeMargin(m_rootMargin[3], m_rootRect.width()); |
| 133 |
| 134 m_rootRect.setX(m_rootRect.x() - leftMargin); |
| 135 m_rootRect.setWidth(m_rootRect.width() + leftMargin + rightMargin); |
| 136 m_rootRect.setY(m_rootRect.y() - topMargin); |
| 137 m_rootRect.setHeight(m_rootRect.height() + topMargin + bottomMargin); |
| 138 } |
| 139 |
| 140 void IntersectionGeometry::clipToRoot() { |
| 141 // Map and clip rect into root element coordinates. |
| 142 // TODO(szager): the writing mode flipping needs a test. |
| 143 LayoutBox* rootLayoutObject = toLayoutBox(getRootLayoutObject()); |
| 144 LayoutObject* targetLayoutObject = m_target->layoutObject(); |
| 145 |
| 146 m_doesIntersect = targetLayoutObject->mapToVisualRectInAncestorSpace( |
| 147 rootLayoutObject, m_intersectionRect, EdgeInclusive); |
| 148 if (rootLayoutObject->hasOverflowClip()) |
| 149 m_intersectionRect.move(-rootLayoutObject->scrolledContentOffset()); |
| 150 |
| 151 if (!m_doesIntersect) |
| 152 return; |
| 153 LayoutRect rootClipRect(m_rootRect); |
| 154 rootLayoutObject->flipForWritingMode(rootClipRect); |
| 155 m_doesIntersect &= m_intersectionRect.inclusiveIntersect(rootClipRect); |
| 156 } |
| 157 |
| 158 void IntersectionGeometry::mapTargetRectToTargetFrameCoordinates() { |
| 159 LayoutObject& targetLayoutObject = *m_target->layoutObject(); |
| 160 Document& targetDocument = m_target->document(); |
| 100 LayoutSize scrollPosition = | 161 LayoutSize scrollPosition = |
| 101 LayoutSize(targetDocument.view()->getScrollOffset()); | 162 LayoutSize(targetDocument.view()->getScrollOffset()); |
| 102 mapRectUpToDocument(rect, targetLayoutObject, targetDocument); | 163 mapRectUpToDocument(m_targetRect, targetLayoutObject, targetDocument); |
| 103 rect.move(-scrollPosition); | 164 m_targetRect.move(-scrollPosition); |
| 104 } | 165 } |
| 105 | 166 |
| 106 void IntersectionObservation::mapRootRectToRootFrameCoordinates( | 167 void IntersectionGeometry::mapRootRectToRootFrameCoordinates() { |
| 107 LayoutRect& rect) const { | 168 LayoutObject& rootLayoutObject = *getRootLayoutObject(); |
| 108 LayoutObject& rootLayoutObject = *m_observer->rootLayoutObject(); | |
| 109 Document& rootDocument = rootLayoutObject.document(); | 169 Document& rootDocument = rootLayoutObject.document(); |
| 110 LayoutSize scrollPosition = | 170 LayoutSize scrollPosition = |
| 111 LayoutSize(rootDocument.view()->getScrollOffset()); | 171 LayoutSize(rootDocument.view()->getScrollOffset()); |
| 112 mapRectUpToDocument(rect, rootLayoutObject, rootLayoutObject.document()); | 172 mapRectUpToDocument(m_rootRect, rootLayoutObject, |
| 113 rect.move(-scrollPosition); | 173 rootLayoutObject.document()); |
| 114 } | 174 m_rootRect.move(-scrollPosition); |
| 115 | 175 } |
| 116 void IntersectionObservation::mapRootRectToTargetFrameCoordinates( | 176 |
| 117 LayoutRect& rect) const { | 177 void IntersectionGeometry::mapRootRectToTargetFrameCoordinates() { |
| 118 LayoutObject& rootLayoutObject = *m_observer->rootLayoutObject(); | 178 LayoutObject& rootLayoutObject = *getRootLayoutObject(); |
| 119 Document& targetDocument = target()->document(); | 179 Document& targetDocument = m_target->document(); |
| 120 LayoutSize scrollPosition = | 180 LayoutSize scrollPosition = |
| 121 LayoutSize(targetDocument.view()->getScrollOffset()); | 181 LayoutSize(targetDocument.view()->getScrollOffset()); |
| 122 | 182 |
| 123 if (&targetDocument == &rootLayoutObject.document()) | 183 if (&targetDocument == &rootLayoutObject.document()) { |
| 124 mapRectUpToDocument(rect, rootLayoutObject, targetDocument); | 184 mapRectUpToDocument(m_intersectionRect, rootLayoutObject, targetDocument); |
| 125 else | 185 } else { |
| 126 mapRectDownToDocument(rect, toLayoutBoxModelObject(rootLayoutObject), | 186 mapRectDownToDocument(m_intersectionRect, |
| 187 toLayoutBoxModelObject(rootLayoutObject), |
| 127 targetDocument); | 188 targetDocument); |
| 128 | 189 } |
| 129 rect.move(-scrollPosition); | 190 |
| 130 } | 191 m_intersectionRect.move(-scrollPosition); |
| 131 | 192 } |
| 132 static bool isContainingBlockChainDescendant(LayoutObject* descendant, | 193 |
| 133 LayoutObject* ancestor) { | 194 void IntersectionGeometry::computeGeometry() { |
| 134 LocalFrame* ancestorFrame = ancestor->document().frame(); | |
| 135 LocalFrame* descendantFrame = descendant->document().frame(); | |
| 136 | |
| 137 if (ancestor->isLayoutView()) | |
| 138 return descendantFrame && descendantFrame->tree().top() == ancestorFrame; | |
| 139 | |
| 140 if (ancestorFrame != descendantFrame) | |
| 141 return false; | |
| 142 | |
| 143 while (descendant && descendant != ancestor) | |
| 144 descendant = descendant->containingBlock(); | |
| 145 return descendant; | |
| 146 } | |
| 147 | |
| 148 bool IntersectionObservation::computeGeometry( | |
| 149 IntersectionGeometry& geometry) const { | |
| 150 // In the first few lines here, before initializeGeometry is called, "return | 195 // In the first few lines here, before initializeGeometry is called, "return |
| 151 // true" effectively means "if the previous observed state was that root and | 196 // true" effectively means "if the previous observed state was that root and |
| 152 // target were intersecting, then generate a notification indicating that they | 197 // target were intersecting, then generate a notification indicating that they |
| 153 // are no longer intersecting." This happens, for example, when root or | 198 // are no longer intersecting." This happens, for example, when root or |
| 154 // target is removed from the DOM tree and not reinserted before the next | 199 // target is removed from the DOM tree and not reinserted before the next |
| 155 // frame is generated, or display:none is set on the root or target. | 200 // frame is generated, or display:none is set on the root or target. |
| 156 Element* targetElement = target(); | 201 if (!m_target->isConnected()) |
| 157 if (!targetElement) | 202 return; |
| 158 return false; | 203 Element* rootElement = root(); |
| 159 if (!targetElement->isConnected()) | |
| 160 return true; | |
| 161 DCHECK(m_observer); | |
| 162 Element* rootElement = m_observer->root(); | |
| 163 if (rootElement && !rootElement->isConnected()) | 204 if (rootElement && !rootElement->isConnected()) |
| 164 return true; | 205 return; |
| 165 | 206 |
| 166 LayoutObject* rootLayoutObject = m_observer->rootLayoutObject(); | 207 LayoutObject* rootLayoutObject = getRootLayoutObject(); |
| 167 if (!rootLayoutObject || !rootLayoutObject->isBoxModelObject()) | 208 if (!rootLayoutObject || !rootLayoutObject->isBoxModelObject()) |
| 168 return true; | 209 return; |
| 169 // TODO(szager): Support SVG | 210 // TODO(szager): Support SVG |
| 170 LayoutObject* targetLayoutObject = targetElement->layoutObject(); | 211 LayoutObject* targetLayoutObject = m_target->layoutObject(); |
| 171 if (!targetLayoutObject) | 212 if (!targetLayoutObject) |
| 172 return true; | 213 return; |
| 173 if (!targetLayoutObject->isBoxModelObject() && !targetLayoutObject->isText()) | 214 if (!targetLayoutObject->isBoxModelObject() && !targetLayoutObject->isText()) |
| 174 return true; | 215 return; |
| 175 if (!isContainingBlockChainDescendant(targetLayoutObject, rootLayoutObject)) | 216 if (!isContainingBlockChainDescendant(targetLayoutObject, rootLayoutObject)) |
| 176 return true; | 217 return; |
| 177 | 218 |
| 178 initializeGeometry(geometry); | 219 initializeGeometry(); |
| 179 | 220 |
| 180 clipToRoot(geometry); | 221 clipToRoot(); |
| 181 | 222 |
| 182 mapTargetRectToTargetFrameCoordinates(geometry.targetRect); | 223 mapTargetRectToTargetFrameCoordinates(); |
| 183 | 224 |
| 184 if (geometry.doesIntersect) | 225 if (m_doesIntersect) |
| 185 mapRootRectToTargetFrameCoordinates(geometry.intersectionRect); | 226 mapRootRectToTargetFrameCoordinates(); |
| 186 else | 227 else |
| 187 geometry.intersectionRect = LayoutRect(); | 228 m_intersectionRect = LayoutRect(); |
| 188 | 229 |
| 189 // Small optimization: if we're not going to report root bounds, don't bother | 230 // Small optimization: if we're not going to report root bounds, don't bother |
| 190 // transforming them to the frame. | 231 // transforming them to the frame. |
| 191 if (m_shouldReportRootBounds) | 232 if (m_shouldReportRootBounds == ReportRootBounds::kShouldReportRootBounds) |
| 192 mapRootRectToRootFrameCoordinates(geometry.rootRect); | 233 mapRootRectToRootFrameCoordinates(); |
| 193 | 234 } |
| 194 return true; | 235 |
| 195 } | 236 DEFINE_TRACE(IntersectionGeometry) { |
| 196 | 237 visitor->trace(m_root); |
| 197 void IntersectionObservation::computeIntersectionObservations( | |
| 198 DOMHighResTimeStamp timestamp) { | |
| 199 IntersectionGeometry geometry; | |
| 200 if (!computeGeometry(geometry)) | |
| 201 return; | |
| 202 | |
| 203 // Some corner cases for threshold index: | |
| 204 // - If target rect is zero area, because it has zero width and/or zero | |
| 205 // height, | |
| 206 // only two states are recognized: | |
| 207 // - 0 means not intersecting. | |
| 208 // - 1 means intersecting. | |
| 209 // No other threshold crossings are possible. | |
| 210 // - Otherwise: | |
| 211 // - If root and target do not intersect, the threshold index is 0. | |
| 212 // - If root and target intersect but the intersection has zero-area | |
| 213 // (i.e., they have a coincident edge or corner), we consider the | |
| 214 // intersection to have "crossed" a zero threshold, but not crossed | |
| 215 // any non-zero threshold. | |
| 216 unsigned newThresholdIndex; | |
| 217 float newVisibleRatio = 0; | |
| 218 if (geometry.targetRect.isEmpty()) { | |
| 219 newThresholdIndex = geometry.doesIntersect ? 1 : 0; | |
| 220 } else if (!geometry.doesIntersect) { | |
| 221 newThresholdIndex = 0; | |
| 222 } else { | |
| 223 float intersectionArea = | |
| 224 geometry.intersectionRect.size().width().toFloat() * | |
| 225 geometry.intersectionRect.size().height().toFloat(); | |
| 226 float targetArea = geometry.targetRect.size().width().toFloat() * | |
| 227 geometry.targetRect.size().height().toFloat(); | |
| 228 newVisibleRatio = intersectionArea / targetArea; | |
| 229 newThresholdIndex = observer().firstThresholdGreaterThan(newVisibleRatio); | |
| 230 } | |
| 231 if (m_lastThresholdIndex != newThresholdIndex) { | |
| 232 IntRect snappedRootBounds = pixelSnappedIntRect(geometry.rootRect); | |
| 233 IntRect* rootBoundsPointer = | |
| 234 m_shouldReportRootBounds ? &snappedRootBounds : nullptr; | |
| 235 IntersectionObserverEntry* newEntry = new IntersectionObserverEntry( | |
| 236 timestamp, newVisibleRatio, pixelSnappedIntRect(geometry.targetRect), | |
| 237 rootBoundsPointer, pixelSnappedIntRect(geometry.intersectionRect), | |
| 238 target()); | |
| 239 observer().enqueueIntersectionObserverEntry(*newEntry); | |
| 240 setLastThresholdIndex(newThresholdIndex); | |
| 241 } | |
| 242 } | |
| 243 | |
| 244 void IntersectionObservation::disconnect() { | |
| 245 IntersectionObserver* observer = m_observer; | |
| 246 clearRootAndRemoveFromTarget(); | |
| 247 observer->removeObservation(*this); | |
| 248 } | |
| 249 | |
| 250 void IntersectionObservation::clearRootAndRemoveFromTarget() { | |
| 251 if (m_target) | |
| 252 target()->ensureIntersectionObserverData().removeObservation(observer()); | |
| 253 m_observer.clear(); | |
| 254 } | |
| 255 | |
| 256 DEFINE_TRACE(IntersectionObservation) { | |
| 257 visitor->trace(m_observer); | |
| 258 visitor->trace(m_target); | 238 visitor->trace(m_target); |
| 259 } | 239 } |
| 260 | 240 |
| 261 } // namespace blink | 241 } // namespace blink |
| OLD | NEW |