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
3 // found in the LICENSE file.
4 
5 #include "third_party/blink/renderer/core/layout/scroll_anchor.h"
6 
7 #include <algorithm>
8 #include <memory>
9 
10 #include "third_party/blink/renderer/core/css/css_markup.h"
11 #include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h"
12 #include "third_party/blink/renderer/core/dom/element_traversal.h"
13 #include "third_party/blink/renderer/core/dom/nth_index_cache.h"
14 #include "third_party/blink/renderer/core/dom/static_node_list.h"
15 #include "third_party/blink/renderer/core/editing/editing_utilities.h"
16 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
17 #include "third_party/blink/renderer/core/frame/root_frame_viewport.h"
18 #include "third_party/blink/renderer/core/frame/web_feature.h"
19 #include "third_party/blink/renderer/core/layout/layout_block_flow.h"
20 #include "third_party/blink/renderer/core/layout/layout_box.h"
21 #include "third_party/blink/renderer/core/layout/layout_table.h"
22 #include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
23 #include "third_party/blink/renderer/core/paint/paint_layer.h"
24 #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
25 #include "third_party/blink/renderer/platform/instrumentation/histogram.h"
26 #include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
27 
28 namespace blink {
29 
30 // With 100 unique strings, a 2^12 slot table has a false positive rate of ~2%.
31 using ClassnameFilter = BloomFilter<12>;
32 using Corner = ScrollAnchor::Corner;
33 
ScrollAnchor()34 ScrollAnchor::ScrollAnchor()
35     : anchor_object_(nullptr),
36       corner_(Corner::kTopLeft),
37       scroll_anchor_disabling_style_changed_(false),
38       queued_(false) {}
39 
ScrollAnchor(ScrollableArea * scroller)40 ScrollAnchor::ScrollAnchor(ScrollableArea* scroller) : ScrollAnchor() {
41   SetScroller(scroller);
42 }
43 
44 ScrollAnchor::~ScrollAnchor() = default;
45 
SetScroller(ScrollableArea * scroller)46 void ScrollAnchor::SetScroller(ScrollableArea* scroller) {
47   DCHECK_NE(scroller_, scroller);
48   DCHECK(scroller);
49   DCHECK(scroller->IsRootFrameViewport() ||
50          scroller->IsPaintLayerScrollableArea());
51   scroller_ = scroller;
52   ClearSelf();
53 }
54 
ScrollerLayoutBox(const ScrollableArea * scroller)55 static LayoutBox* ScrollerLayoutBox(const ScrollableArea* scroller) {
56   LayoutBox* box = scroller->GetLayoutBox();
57   DCHECK(box);
58   return box;
59 }
60 
61 
62 // TODO(skobes): Storing a "corner" doesn't make much sense anymore since we
63 // adjust only on the block flow axis.  This could probably be refactored to
64 // simply measure the movement of the block-start edge.
CornerToAnchor(const ScrollableArea * scroller)65 static Corner CornerToAnchor(const ScrollableArea* scroller) {
66   const ComputedStyle* style = ScrollerLayoutBox(scroller)->Style();
67   if (style->IsFlippedBlocksWritingMode())
68     return Corner::kTopRight;
69   return Corner::kTopLeft;
70 }
71 
CornerPointOfRect(LayoutRect rect,Corner which_corner)72 static LayoutPoint CornerPointOfRect(LayoutRect rect, Corner which_corner) {
73   switch (which_corner) {
74     case Corner::kTopLeft:
75       return rect.MinXMinYCorner();
76     case Corner::kTopRight:
77       return rect.MaxXMinYCorner();
78   }
79   NOTREACHED();
80   return LayoutPoint();
81 }
82 
83 // Bounds of the LayoutObject relative to the scroller's visible content rect.
RelativeBounds(const LayoutObject * layout_object,const ScrollableArea * scroller)84 static LayoutRect RelativeBounds(const LayoutObject* layout_object,
85                                  const ScrollableArea* scroller) {
86   PhysicalRect local_bounds;
87   if (const auto* box = DynamicTo<LayoutBox>(layout_object)) {
88     local_bounds = box->PhysicalBorderBoxRect();
89     // If we clip overflow then we can use the `PhysicalBorderBoxRect()`
90     // as our bounds. If not, we expand the bounds by the layout overflow and
91     // lowest floating object.
92     if (!layout_object->ShouldClipOverflowAlongEitherAxis()) {
93       // BorderBoxRect doesn't include overflow content and floats.
94       LayoutUnit max_y =
95           std::max(local_bounds.Bottom(), box->LayoutOverflowRect().MaxY());
96       auto* layout_block_flow = DynamicTo<LayoutBlockFlow>(layout_object);
97       if (layout_block_flow && layout_block_flow->ContainsFloats()) {
98         // Note that lowestFloatLogicalBottom doesn't include floating
99         // grandchildren.
100         max_y = std::max(max_y, layout_block_flow->LowestFloatLogicalBottom());
101       }
102       local_bounds.ShiftBottomEdgeTo(max_y);
103     }
104   } else if (layout_object->IsText()) {
105     const auto* text = To<LayoutText>(layout_object);
106     // TODO(kojii): |PhysicalLinesBoundingBox()| cannot compute, and thus
107     // returns (0, 0) when changes are made that |DeleteLineBoxes()| or clear
108     // |SetPaintFragment()|, e.g., |SplitFlow()|. crbug.com/965352
109     local_bounds.Unite(text->PhysicalLinesBoundingBox());
110   } else {
111     // Only LayoutBox and LayoutText are supported.
112     NOTREACHED();
113   }
114 
115   LayoutRect relative_bounds = LayoutRect(
116       scroller
117           ->LocalToVisibleContentQuad(FloatRect(local_bounds), layout_object)
118           .BoundingBox());
119 
120   return relative_bounds;
121 }
122 
ComputeRelativeOffset(const LayoutObject * layout_object,const ScrollableArea * scroller,Corner corner)123 static LayoutPoint ComputeRelativeOffset(const LayoutObject* layout_object,
124                                          const ScrollableArea* scroller,
125                                          Corner corner) {
126   LayoutPoint offset =
127       CornerPointOfRect(RelativeBounds(layout_object, scroller), corner);
128   const LayoutBox* scroller_box = ScrollerLayoutBox(scroller);
129   return scroller_box->FlipForWritingMode(PhysicalOffset(offset));
130 }
131 
CandidateMayMoveWithScroller(const LayoutObject * candidate,const ScrollableArea * scroller)132 static bool CandidateMayMoveWithScroller(const LayoutObject* candidate,
133                                          const ScrollableArea* scroller) {
134   if (const ComputedStyle* style = candidate->Style()) {
135     if (style->HasViewportConstrainedPosition() ||
136         style->HasStickyConstrainedPosition())
137       return false;
138   }
139 
140   LayoutObject::AncestorSkipInfo skip_info(ScrollerLayoutBox(scroller));
141   candidate->Container(&skip_info);
142   return !skip_info.AncestorSkipped();
143 }
144 
IsOnlySiblingWithTagName(Element * element)145 static bool IsOnlySiblingWithTagName(Element* element) {
146   DCHECK(element);
147   return (1U == NthIndexCache::NthOfTypeIndex(*element)) &&
148          (1U == NthIndexCache::NthLastOfTypeIndex(*element));
149 }
150 
UniqueClassnameAmongSiblings(Element * element)151 static const AtomicString UniqueClassnameAmongSiblings(Element* element) {
152   DCHECK(element);
153 
154   auto classname_filter = std::make_unique<ClassnameFilter>();
155 
156   Element* parent_element = ElementTraversal::FirstAncestor(*element);
157   Element* sibling_element =
158       parent_element ? ElementTraversal::FirstChild(*parent_element) : element;
159   // Add every classname of every sibling to our bloom filter, starting from the
160   // leftmost sibling, but skipping |element|.
161   for (; sibling_element;
162        sibling_element = ElementTraversal::NextSibling(*sibling_element)) {
163     if (sibling_element->HasClass() && sibling_element != element) {
164       const SpaceSplitString& class_names = sibling_element->ClassNames();
165       for (wtf_size_t i = 0; i < class_names.size(); ++i) {
166         classname_filter->Add(class_names[i].Impl()->ExistingHash());
167       }
168     }
169   }
170 
171   const SpaceSplitString& class_names = element->ClassNames();
172   for (wtf_size_t i = 0; i < class_names.size(); ++i) {
173     // MayContain allows for false positives, but a false positive is relatively
174     // harmless; it just means we have to choose a different classname, or in
175     // the worst case a different selector.
176     if (!classname_filter->MayContain(class_names[i].Impl()->ExistingHash())) {
177       return class_names[i];
178     }
179   }
180 
181   return AtomicString();
182 }
183 
184 // Calculate a simple selector for |element| that uniquely identifies it among
185 // its siblings. If present, the element's id will be used; otherwise, less
186 // specific selectors are preferred to more specific ones. The ordering of
187 // selector preference is:
188 // 1. ID
189 // 2. Tag name
190 // 3. Class name
191 // 4. nth-child
UniqueSimpleSelectorAmongSiblings(Element * element)192 static const String UniqueSimpleSelectorAmongSiblings(Element* element) {
193   DCHECK(element);
194 
195   if (element->HasID() &&
196       !element->GetDocument().ContainsMultipleElementsWithId(
197           element->GetIdAttribute())) {
198     StringBuilder builder;
199     builder.Append("#");
200     SerializeIdentifier(element->GetIdAttribute(), builder);
201     return builder.ToAtomicString();
202   }
203 
204   if (IsOnlySiblingWithTagName(element)) {
205     StringBuilder builder;
206     SerializeIdentifier(element->TagQName().ToString(), builder);
207     return builder.ToAtomicString();
208   }
209 
210   if (element->HasClass()) {
211     AtomicString unique_classname = UniqueClassnameAmongSiblings(element);
212     if (!unique_classname.IsEmpty()) {
213       return AtomicString(".") + unique_classname;
214     }
215   }
216 
217   return ":nth-child(" +
218          String::Number(NthIndexCache::NthChildIndex(*element)) + ")";
219 }
220 
221 // Computes a selector that uniquely identifies |anchor_node|. This is done
222 // by computing a selector that uniquely identifies each ancestor among its
223 // sibling elements, terminating at a definitively unique ancestor. The
224 // definitively unique ancestor is either the first ancestor with an id or
225 // the root of the document. The computed selectors are chained together with
226 // the child combinator(>) to produce a compound selector that is
227 // effectively a path through the DOM tree to |anchor_node|.
ComputeUniqueSelector(Node * anchor_node)228 static const String ComputeUniqueSelector(Node* anchor_node) {
229   DCHECK(anchor_node);
230   // The scroll anchor can be a pseudo element, but pseudo elements aren't part
231   // of the DOM and can't be used as part of a selector. We fail in this case;
232   // success isn't possible.
233   if (anchor_node->IsPseudoElement()) {
234     return String();
235   }
236 
237   TRACE_EVENT0("blink", "ScrollAnchor::SerializeAnchor");
238   SCOPED_BLINK_UMA_HISTOGRAM_TIMER(
239       "Layout.ScrollAnchor.TimeToComputeAnchorNodeSelector");
240 
241   Vector<String> selector_list;
242   for (Element* element = ElementTraversal::FirstAncestorOrSelf(*anchor_node);
243        element; element = ElementTraversal::FirstAncestor(*element)) {
244     selector_list.push_back(UniqueSimpleSelectorAmongSiblings(element));
245     if (element->HasID() &&
246         !element->GetDocument().ContainsMultipleElementsWithId(
247             element->GetIdAttribute())) {
248       break;
249     }
250   }
251 
252   StringBuilder builder;
253   size_t i = 0;
254   // We added the selectors tree-upward order from left to right, but css
255   // selectors are written tree-downward from left to right. We reverse the
256   // order of iteration to get a properly ordered compound selector.
257   for (auto reverse_iterator = selector_list.rbegin();
258        reverse_iterator != selector_list.rend(); ++reverse_iterator, ++i) {
259     if (i)
260       builder.Append(">");
261     builder.Append(*reverse_iterator);
262   }
263 
264   DEFINE_STATIC_LOCAL(CustomCountHistogram, selector_length_histogram,
265                       ("Layout.ScrollAnchor.SerializedAnchorSelectorLength", 1,
266                        kMaxSerializedSelectorLength, 50));
267   selector_length_histogram.Count(builder.length());
268 
269   if (builder.length() > kMaxSerializedSelectorLength) {
270     return String();
271   }
272 
273   return builder.ToString();
274 }
275 
GetVisibleRect(ScrollableArea * scroller)276 static LayoutRect GetVisibleRect(ScrollableArea* scroller) {
277   auto visible_rect =
278       ScrollerLayoutBox(scroller)->OverflowClipRect(LayoutPoint());
279 
280   const ComputedStyle* style = ScrollerLayoutBox(scroller)->Style();
281   LayoutRectOutsets scroll_padding(
282       MinimumValueForLength(style->ScrollPaddingTop(), visible_rect.Height()),
283       MinimumValueForLength(style->ScrollPaddingRight(), visible_rect.Width()),
284       MinimumValueForLength(style->ScrollPaddingBottom(),
285                             visible_rect.Height()),
286       MinimumValueForLength(style->ScrollPaddingLeft(), visible_rect.Width()));
287   visible_rect.Contract(scroll_padding);
288   return visible_rect;
289 }
290 
Examine(const LayoutObject * candidate) const291 ScrollAnchor::ExamineResult ScrollAnchor::Examine(
292     const LayoutObject* candidate) const {
293   if (candidate == ScrollerLayoutBox(scroller_))
294     return ExamineResult(kContinue);
295 
296   if (candidate->StyleRef().OverflowAnchor() == EOverflowAnchor::kNone)
297     return ExamineResult(kSkip);
298 
299   if (candidate->IsLayoutInline())
300     return ExamineResult(kContinue);
301 
302   // Anonymous blocks are not in the DOM tree and it may be hard for
303   // developers to reason about the anchor node.
304   if (candidate->IsAnonymous())
305     return ExamineResult(kContinue);
306 
307   if (!candidate->IsText() && !candidate->IsBox())
308     return ExamineResult(kSkip);
309 
310   if (!CandidateMayMoveWithScroller(candidate, scroller_))
311     return ExamineResult(kSkip);
312 
313   LayoutRect candidate_rect = RelativeBounds(candidate, scroller_);
314   LayoutRect visible_rect = GetVisibleRect(scroller_);
315 
316   bool occupies_space =
317       candidate_rect.Width() > 0 && candidate_rect.Height() > 0;
318   if (occupies_space && visible_rect.Intersects(candidate_rect)) {
319     return ExamineResult(
320         visible_rect.Contains(candidate_rect) ? kReturn : kConstrain,
321         CornerToAnchor(scroller_));
322   } else {
323     return ExamineResult(kSkip);
324   }
325 }
326 
FindAnchor()327 void ScrollAnchor::FindAnchor() {
328   TRACE_EVENT0("blink", "ScrollAnchor::findAnchor");
329   SCOPED_BLINK_UMA_HISTOGRAM_TIMER("Layout.ScrollAnchor.TimeToFindAnchor");
330 
331   bool found_priority_anchor = FindAnchorInPriorityCandidates();
332   if (!found_priority_anchor)
333     FindAnchorRecursive(ScrollerLayoutBox(scroller_));
334 
335   if (anchor_object_) {
336     anchor_object_->SetIsScrollAnchorObject();
337     saved_relative_offset_ =
338         ComputeRelativeOffset(anchor_object_, scroller_, corner_);
339     anchor_is_cv_auto_without_layout_ =
340         DisplayLockUtilities::IsAutoWithoutLayout(*anchor_object_);
341   }
342 }
343 
FindAnchorInPriorityCandidates()344 bool ScrollAnchor::FindAnchorInPriorityCandidates() {
345   auto* scroller_box = ScrollerLayoutBox(scroller_);
346   if (!scroller_box)
347     return false;
348 
349   auto& document = scroller_box->GetDocument();
350 
351   // Focused area.
352   LayoutObject* candidate = nullptr;
353   ExamineResult result{kSkip};
354   auto* focused_element = document.FocusedElement();
355   if (focused_element && HasEditableStyle(*focused_element)) {
356     candidate = PriorityCandidateFromNode(focused_element);
357     if (candidate) {
358       result = ExaminePriorityCandidate(candidate);
359       if (result.viable) {
360         anchor_object_ = candidate;
361         corner_ = result.corner;
362         return true;
363       }
364     }
365   }
366 
367   // Active find-in-page match.
368   candidate =
369       PriorityCandidateFromNode(document.GetFindInPageActiveMatchNode());
370   result = ExaminePriorityCandidate(candidate);
371   if (result.viable) {
372     anchor_object_ = candidate;
373     corner_ = result.corner;
374     return true;
375   }
376   return false;
377 }
378 
PriorityCandidateFromNode(const Node * node) const379 LayoutObject* ScrollAnchor::PriorityCandidateFromNode(const Node* node) const {
380   while (node) {
381     if (auto* layout_object = node->GetLayoutObject()) {
382       if (!layout_object->IsAnonymous() &&
383           (!layout_object->IsInline() ||
384            layout_object->IsAtomicInlineLevel())) {
385         return layout_object;
386       }
387     }
388     node = FlatTreeTraversal::Parent(*node);
389   }
390   return nullptr;
391 }
392 
ExaminePriorityCandidate(const LayoutObject * candidate) const393 ScrollAnchor::ExamineResult ScrollAnchor::ExaminePriorityCandidate(
394     const LayoutObject* candidate) const {
395   auto* ancestor = candidate;
396   auto* scroller_box = ScrollerLayoutBox(scroller_);
397   while (ancestor && ancestor != scroller_box) {
398     if (ancestor->StyleRef().OverflowAnchor() == EOverflowAnchor::kNone)
399       return ExamineResult(kSkip);
400 
401     if (!CandidateMayMoveWithScroller(ancestor, scroller_))
402       return ExamineResult(kSkip);
403 
404     ancestor = ancestor->Parent();
405   }
406   return ancestor ? Examine(candidate) : ExamineResult(kSkip);
407 }
408 
FindAnchorRecursive(LayoutObject * candidate)409 bool ScrollAnchor::FindAnchorRecursive(LayoutObject* candidate) {
410   ExamineResult result = Examine(candidate);
411   if (result.viable) {
412     anchor_object_ = candidate;
413     corner_ = result.corner;
414   }
415 
416   if (result.status == kReturn)
417     return true;
418 
419   if (result.status == kSkip)
420     return false;
421 
422   for (LayoutObject* child = candidate->SlowFirstChild(); child;
423        child = child->NextSibling()) {
424     if (FindAnchorRecursive(child))
425       return true;
426   }
427 
428   // Make a separate pass to catch positioned descendants with a static DOM
429   // parent that we skipped over (crbug.com/692701).
430   if (auto* layouy_block = DynamicTo<LayoutBlock>(candidate)) {
431     if (TrackedLayoutBoxListHashSet* positioned_descendants =
432             layouy_block->PositionedObjects()) {
433       for (LayoutBox* descendant : *positioned_descendants) {
434         if (descendant->Parent() != candidate) {
435           if (FindAnchorRecursive(descendant))
436             return true;
437         }
438       }
439     }
440   }
441 
442   if (result.status == kConstrain)
443     return true;
444 
445   DCHECK_EQ(result.status, kContinue);
446   return false;
447 }
448 
ComputeScrollAnchorDisablingStyleChanged()449 bool ScrollAnchor::ComputeScrollAnchorDisablingStyleChanged() {
450   LayoutObject* current = AnchorObject();
451   if (!current)
452     return false;
453 
454   LayoutObject* scroller_box = ScrollerLayoutBox(scroller_);
455   while (true) {
456     DCHECK(current);
457     if (current->ScrollAnchorDisablingStyleChanged())
458       return true;
459     if (current == scroller_box)
460       return false;
461     current = current->Parent();
462   }
463 }
464 
NotifyBeforeLayout()465 void ScrollAnchor::NotifyBeforeLayout() {
466   if (queued_) {
467     scroll_anchor_disabling_style_changed_ |=
468         ComputeScrollAnchorDisablingStyleChanged();
469     return;
470   }
471   DCHECK(scroller_);
472   ScrollOffset scroll_offset = scroller_->GetScrollOffset();
473   float block_direction_scroll_offset =
474       ScrollerLayoutBox(scroller_)->IsHorizontalWritingMode()
475           ? scroll_offset.Height()
476           : scroll_offset.Width();
477   if (block_direction_scroll_offset == 0) {
478     ClearSelf();
479     return;
480   }
481 
482   if (!anchor_object_) {
483     // FindAnchor() and ComputeRelativeOffset() query a box's borders as part of
484     // its geometry. But when collapsed, table borders can depend on internal
485     // parts, which get sorted during a layout pass. When a table with dirty
486     // internal structure is checked as an anchor candidate, a DCHECK was hit.
487     FindAnchor();
488     if (!anchor_object_)
489       return;
490   }
491 
492   scroll_anchor_disabling_style_changed_ =
493       ComputeScrollAnchorDisablingStyleChanged();
494 
495   LocalFrameView* frame_view = ScrollerLayoutBox(scroller_)->GetFrameView();
496   auto* root_frame_viewport = DynamicTo<RootFrameViewport>(scroller_.Get());
497   ScrollableArea* owning_scroller = root_frame_viewport
498                                         ? &root_frame_viewport->LayoutViewport()
499                                         : scroller_.Get();
500   frame_view->EnqueueScrollAnchoringAdjustment(owning_scroller);
501   queued_ = true;
502 }
503 
ComputeAdjustment() const504 IntSize ScrollAnchor::ComputeAdjustment() const {
505   // The anchor node can report fractional positions, but it is DIP-snapped when
506   // painting (crbug.com/610805), so we must round the offsets to determine the
507   // visual delta. If we scroll by the delta in LayoutUnits, the snapping of the
508   // anchor node may round differently from the snapping of the scroll position.
509   // (For example, anchor moving from 2.4px -> 2.6px is really 2px -> 3px, so we
510   // should scroll by 1px instead of 0.2px.) This is true regardless of whether
511   // the ScrollableArea actually uses fractional scroll positions.
512   IntSize delta = RoundedIntSize(ComputeRelativeOffset(anchor_object_,
513                                                        scroller_, corner_)) -
514                   RoundedIntSize(saved_relative_offset_);
515 
516   LayoutRect anchor_rect = RelativeBounds(anchor_object_, scroller_);
517 
518   // Only adjust on the block layout axis.
519   const LayoutBox* scroller_box = ScrollerLayoutBox(scroller_);
520   if (scroller_box->IsHorizontalWritingMode())
521     delta.SetWidth(0);
522   else
523     delta.SetHeight(0);
524 
525   if (anchor_is_cv_auto_without_layout_) {
526     // See the effect delta would have on the anchor rect.
527     // If the anchor is now off-screen (in block direction) then make sure it's
528     // just at the edge.
529     anchor_rect.Move(-delta);
530     if (scroller_box->IsHorizontalWritingMode()) {
531       if (anchor_rect.MaxY() < 0)
532         delta.SetHeight(delta.Height() + anchor_rect.MaxY().ToInt());
533     } else {
534       // For the flipped blocks writing mode, we need to adjust the offset to
535       // align the opposite edge of the block (MaxX edge instead of X edge).
536       if (scroller_box->HasFlippedBlocksWritingMode()) {
537         auto visible_rect = GetVisibleRect(scroller_);
538         if (anchor_rect.X() > visible_rect.MaxX()) {
539           delta.SetWidth(delta.Width() - (anchor_rect.X().ToInt() -
540                                           visible_rect.MaxX().ToInt()));
541         }
542       } else if (anchor_rect.MaxX() < 0) {
543         delta.SetWidth(delta.Width() + anchor_rect.MaxX().ToInt());
544       }
545     }
546   }
547 
548   // If block direction is flipped, delta is a logical value, so flip it to
549   // make it physical.
550   if (!scroller_box->IsHorizontalWritingMode() &&
551       scroller_box->HasFlippedBlocksWritingMode()) {
552     delta.SetWidth(-delta.Width());
553   }
554   return delta;
555 }
556 
Adjust()557 void ScrollAnchor::Adjust() {
558   if (!queued_)
559     return;
560   queued_ = false;
561   DCHECK(scroller_);
562   if (!anchor_object_)
563     return;
564   IntSize adjustment = ComputeAdjustment();
565 
566   // We should pick a new anchor if we had an unlaid-out content-visibility
567   // auto. It should have been laid out, so if it is still the best candidate,
568   // we will select it without this boolean set.
569   if (anchor_is_cv_auto_without_layout_)
570     ClearSelf();
571 
572   if (adjustment.IsZero())
573     return;
574 
575   if (scroll_anchor_disabling_style_changed_) {
576     // Note that we only clear if the adjustment would have been non-zero.
577     // This minimizes redundant calls to findAnchor.
578     // TODO(skobes): add UMA metric for this.
579     ClearSelf();
580 
581     DEFINE_STATIC_LOCAL(EnumerationHistogram, suppressed_by_sanaclap_histogram,
582                         ("Layout.ScrollAnchor.SuppressedBySanaclap", 2));
583     suppressed_by_sanaclap_histogram.Count(1);
584 
585     return;
586   }
587 
588   scroller_->SetScrollOffset(
589       scroller_->GetScrollOffset() + FloatSize(adjustment),
590       mojom::blink::ScrollType::kAnchoring);
591 
592   // Update UMA metric.
593   DEFINE_STATIC_LOCAL(EnumerationHistogram, adjusted_offset_histogram,
594                       ("Layout.ScrollAnchor.AdjustedScrollOffset", 2));
595   adjusted_offset_histogram.Count(1);
596   UseCounter::Count(ScrollerLayoutBox(scroller_)->GetDocument(),
597                     WebFeature::kScrollAnchored);
598 }
599 
RestoreAnchor(const SerializedAnchor & serialized_anchor)600 bool ScrollAnchor::RestoreAnchor(const SerializedAnchor& serialized_anchor) {
601   if (!scroller_ || !serialized_anchor.IsValid()) {
602     return false;
603   }
604 
605   SCOPED_BLINK_UMA_HISTOGRAM_TIMER("Layout.ScrollAnchor.TimeToRestoreAnchor");
606   DEFINE_STATIC_LOCAL(EnumerationHistogram, restoration_status_histogram,
607                       ("Layout.ScrollAnchor.RestorationStatus", kStatusCount));
608 
609   if (anchor_object_ && serialized_anchor.selector == saved_selector_) {
610     return true;
611   }
612 
613   if (anchor_object_) {
614     return false;
615   }
616 
617   Document* document = &(ScrollerLayoutBox(scroller_)->GetDocument());
618 
619   // This is a considered and deliberate usage of DummyExceptionStateForTesting.
620   // We really do want to always swallow it. Here's why:
621   // 1) We have no one to propagate an exception to.
622   // 2) We don't want to rely on having an isolate(which normal ExceptionState
623   // does), as this requires setting up and using javascript/v8. This is
624   // undesirable since it needlessly prevents us from running when javascript is
625   // disabled, and causes proxy objects to be prematurely
626   // initialized(crbug.com/810897).
627   DummyExceptionStateForTesting exception_state;
628   StaticElementList* found_elements = document->QuerySelectorAll(
629       AtomicString(serialized_anchor.selector), exception_state);
630 
631   if (exception_state.HadException()) {
632     restoration_status_histogram.Count(kFailedBadSelector);
633     return false;
634   }
635 
636   if (found_elements->length() < 1) {
637     restoration_status_histogram.Count(kFailedNoMatches);
638     return false;
639   }
640 
641   for (unsigned index = 0; index < found_elements->length(); index++) {
642     Element* anchor_element = found_elements->item(index);
643     LayoutObject* anchor_object = anchor_element->GetLayoutObject();
644 
645     if (!anchor_object) {
646       continue;
647     }
648 
649     // There are scenarios where the layout object we find is non-box and
650     // non-text; this can happen, e.g., if the original anchor object was a text
651     // element of a non-box element like <code>. The generated selector can't
652     // directly locate the text object, resulting in a loss of precision.
653     // Instead we scroll the object we do find into the same relative position
654     // and attempt to re-find the anchor. The user-visible effect should end up
655     // roughly the same.
656     ScrollOffset current_offset = scroller_->GetScrollOffset();
657     FloatRect bounding_box = anchor_object->AbsoluteBoundingBoxFloatRect();
658     FloatPoint location_point =
659         anchor_object->Style()->IsFlippedBlocksWritingMode()
660             ? bounding_box.MaxXMinYCorner()
661             : bounding_box.Location();
662     FloatPoint desired_point = location_point + current_offset;
663 
664     ScrollOffset desired_offset =
665         ScrollOffset(desired_point.X(), desired_point.Y());
666     ScrollOffset delta =
667         ScrollOffset(RoundedIntSize(serialized_anchor.relative_offset));
668     desired_offset -= delta;
669     scroller_->SetScrollOffset(desired_offset,
670                                mojom::blink::ScrollType::kAnchoring);
671     FindAnchor();
672 
673     // If the above FindAnchor call failed, reset the scroll position and try
674     // again with the next found element.
675     if (!anchor_object_) {
676       scroller_->SetScrollOffset(current_offset,
677                                  mojom::blink::ScrollType::kAnchoring);
678       continue;
679     }
680 
681     saved_selector_ = serialized_anchor.selector;
682     restoration_status_histogram.Count(kSuccess);
683 
684     return true;
685   }
686 
687   restoration_status_histogram.Count(kFailedNoValidMatches);
688   return false;
689 }
690 
GetSerializedAnchor()691 const SerializedAnchor ScrollAnchor::GetSerializedAnchor() {
692   // It's safe to return saved_selector_ before checking anchor_object_, since
693   // clearing anchor_object_ also clears saved_selector_.
694   if (!saved_selector_.IsEmpty()) {
695     DCHECK(anchor_object_);
696     return SerializedAnchor(
697         saved_selector_,
698         ComputeRelativeOffset(anchor_object_, scroller_, corner_));
699   }
700 
701   if (!anchor_object_) {
702     FindAnchor();
703     if (!anchor_object_)
704       return SerializedAnchor();
705   }
706 
707   DCHECK(anchor_object_->GetNode());
708   SerializedAnchor new_anchor(
709       ComputeUniqueSelector(anchor_object_->GetNode()),
710       ComputeRelativeOffset(anchor_object_, scroller_, corner_));
711 
712   if (new_anchor.IsValid()) {
713     saved_selector_ = new_anchor.selector;
714   }
715 
716   return new_anchor;
717 }
718 
ClearSelf()719 void ScrollAnchor::ClearSelf() {
720   LayoutObject* anchor_object = anchor_object_;
721   anchor_object_ = nullptr;
722   saved_selector_ = String();
723 
724   if (anchor_object)
725     anchor_object->MaybeClearIsScrollAnchorObject();
726 }
727 
Dispose()728 void ScrollAnchor::Dispose() {
729   if (scroller_) {
730     LocalFrameView* frame_view = ScrollerLayoutBox(scroller_)->GetFrameView();
731     auto* root_frame_viewport = DynamicTo<RootFrameViewport>(scroller_.Get());
732     ScrollableArea* owning_scroller =
733         root_frame_viewport ? &root_frame_viewport->LayoutViewport()
734                             : scroller_.Get();
735     frame_view->DequeueScrollAnchoringAdjustment(owning_scroller);
736     scroller_.Clear();
737   }
738   anchor_object_ = nullptr;
739   saved_selector_ = String();
740 }
741 
Clear()742 void ScrollAnchor::Clear() {
743   LayoutObject* layout_object =
744       anchor_object_ ? anchor_object_ : ScrollerLayoutBox(scroller_);
745   PaintLayer* layer = nullptr;
746   if (LayoutObject* parent = layout_object->Parent())
747     layer = parent->EnclosingLayer();
748 
749   // Walk up the layer tree to clear any scroll anchors.
750   while (layer) {
751     if (PaintLayerScrollableArea* scrollable_area =
752             layer->GetScrollableArea()) {
753       ScrollAnchor* anchor = scrollable_area->GetScrollAnchor();
754       DCHECK(anchor);
755       anchor->ClearSelf();
756     }
757     layer = layer->Parent();
758   }
759 }
760 
RefersTo(const LayoutObject * layout_object) const761 bool ScrollAnchor::RefersTo(const LayoutObject* layout_object) const {
762   return anchor_object_ == layout_object;
763 }
764 
NotifyRemoved(LayoutObject * layout_object)765 void ScrollAnchor::NotifyRemoved(LayoutObject* layout_object) {
766   if (anchor_object_ == layout_object)
767     ClearSelf();
768 }
769 
770 }  // namespace blink
771