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