| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/page/scrolling/snap_coordinator.h" |
| |
| #include "third_party/blink/renderer/core/dom/dom_node_ids.h" |
| #include "third_party/blink/renderer/core/dom/element.h" |
| #include "third_party/blink/renderer/core/dom/node.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/frame/web_feature.h" |
| #include "third_party/blink/renderer/core/layout/layout_block.h" |
| #include "third_party/blink/renderer/core/layout/layout_box.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h" |
| #include "third_party/blink/renderer/platform/geometry/length_functions.h" |
| #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" |
| |
| namespace blink { |
| namespace { |
| // This is experimentally determined and corresponds to the UA decided |
| // parameter as mentioned in spec. |
| constexpr float kProximityRatio = 1.0 / 3.0; |
| } // namespace |
| // TODO(sunyunjia): Move the static functions to an anonymous namespace. |
| |
| SnapCoordinator::SnapCoordinator() : snap_containers_() {} |
| |
| SnapCoordinator::~SnapCoordinator() = default; |
| |
| // Returns the layout box's next ancestor that can be a snap container. |
| // The origin may be either a snap area or a snap container. |
| LayoutBox* FindSnapContainer(const LayoutBox& origin_box) { |
| // According to the new spec |
| // https://drafts.csswg.org/css-scroll-snap/#snap-model |
| // "Snap positions must only affect the nearest ancestor (on the element’s |
| // containing block chain) scroll container". |
| Element* document_element = origin_box.GetDocument().documentElement(); |
| LayoutBox* box = origin_box.ContainingBlock(); |
| while (box && !box->IsScrollContainer() && !IsA<LayoutView>(box) && |
| box->GetNode() != document_element) { |
| box = box->ContainingBlock(); |
| } |
| |
| // If we reach to document element then we dispatch to layout view. |
| if (box && box->GetNode() == document_element) |
| return origin_box.GetDocument().GetLayoutView(); |
| |
| return box; |
| } |
| |
| // Snap types are categorized according to the spec |
| // https://drafts.csswg.org/css-scroll-snap-1/#snap-axis |
| cc::ScrollSnapType GetPhysicalSnapType(const LayoutBox& snap_container) { |
| cc::ScrollSnapType scroll_snap_type = |
| snap_container.Style()->GetScrollSnapType(); |
| if (scroll_snap_type.axis == cc::SnapAxis::kInline) { |
| if (snap_container.Style()->IsHorizontalWritingMode()) |
| scroll_snap_type.axis = cc::SnapAxis::kX; |
| else |
| scroll_snap_type.axis = cc::SnapAxis::kY; |
| } |
| if (scroll_snap_type.axis == cc::SnapAxis::kBlock) { |
| if (snap_container.Style()->IsHorizontalWritingMode()) |
| scroll_snap_type.axis = cc::SnapAxis::kY; |
| else |
| scroll_snap_type.axis = cc::SnapAxis::kX; |
| } |
| // Writing mode does not affect the cases where axis is kX, kY or kBoth. |
| return scroll_snap_type; |
| } |
| |
| // Adding a snap container means that the element's descendant snap areas need |
| // to be reassigned to it. First we find the snap container ancestor of the new |
| // snap container, then check its snap areas to see if their closest ancestor is |
| // changed to the new snap container. |
| // E.g., if A1 and A2's ancestor, C2, is C1's descendant and becomes scrollable |
| // then C2 becomes a snap container then the snap area assignments change: |
| // Before After adding C2 |
| // C1 C1 |
| // | | |
| // +-------+ +-----+ |
| // | | | C2 | |
| // A1 A2 A3 +---+ A3 |
| // | | |
| // A1 A2 |
| void SnapCoordinator::AddSnapContainer(LayoutBox& snap_container) { |
| snap_containers_.insert(&snap_container); |
| |
| LayoutBox* ancestor_snap_container = FindSnapContainer(snap_container); |
| // If an ancestor doesn't exist then this means that the element is being |
| // attached now; this means that it won't have any descendants that are |
| // assigned to an ancestor snap container. |
| if (!ancestor_snap_container) { |
| DCHECK(!snap_container.Parent()); |
| return; |
| } |
| SnapAreaSet* snap_areas = ancestor_snap_container->SnapAreas(); |
| if (!snap_areas) |
| return; |
| Vector<LayoutBox*> snap_areas_to_reassign; |
| for (auto* snap_area : *snap_areas) { |
| if (FindSnapContainer(*snap_area) == &snap_container) |
| snap_areas_to_reassign.push_back(snap_area); |
| } |
| for (auto* snap_area : snap_areas_to_reassign) |
| snap_area->SetSnapContainer(&snap_container); |
| |
| // The new snap container will not have attached its ScrollableArea yet, so we |
| // cannot invalidate the snap container data at this point. However, the snap |
| // container data is set to needing an update by default, so we only need to |
| // update the flag for the ancestor. |
| if (snap_areas_to_reassign.size()) { |
| ancestor_snap_container->GetScrollableArea() |
| ->SetSnapContainerDataNeedsUpdate(true); |
| } |
| } |
| |
| void SnapCoordinator::RemoveSnapContainer(LayoutBox& snap_container) { |
| LayoutBox* ancestor_snap_container = FindSnapContainer(snap_container); |
| |
| // We remove the snap container if it is no longer scrollable, or if the |
| // element is detached. |
| // - If it is no longer scrollable, then we reassign its snap areas to the |
| // next ancestor snap container. |
| // - If it is detached, then we simply clear its snap areas since they will be |
| // detached as well. |
| if (ancestor_snap_container) { |
| ancestor_snap_container->GetScrollableArea() |
| ->SetSnapContainerDataNeedsUpdate(true); |
| snap_container.ReassignSnapAreas(*ancestor_snap_container); |
| } else { |
| DCHECK(!snap_container.Parent()); |
| snap_container.ClearSnapAreas(); |
| } |
| // We don't need to update the old snap container's data since the |
| // corresponding ScrollableArea is being removed, and thus the snap container |
| // data is removed too. |
| snap_container.SetNeedsPaintPropertyUpdate(); |
| snap_containers_.erase(&snap_container); |
| } |
| |
| void SnapCoordinator::SnapContainerDidChange(LayoutBox& snap_container) { |
| // Scroll snap properties have no effect on the document element instead they |
| // are propagated to (See Document::PropagateStyleToViewport) and handled by |
| // the LayoutView. |
| if (snap_container.GetNode() == |
| snap_container.GetDocument().documentElement()) |
| return; |
| |
| PaintLayerScrollableArea* scrollable_area = |
| snap_container.GetScrollableArea(); |
| // Per specification snap positions only affect *scroll containers* [1]. So if |
| // the layout box is not a scroll container we ignore it here even if it has |
| // non-none scroll-snap-type. Note that in blink, existence of scrollable area |
| // directly maps to being a scroll container in the specification. [1] |
| // https://drafts.csswg.org/css-scroll-snap/#overview |
| if (!scrollable_area) { |
| DCHECK(!snap_containers_.Contains(&snap_container)); |
| return; |
| } |
| |
| // Note that even if scroll snap type is 'none' we continue to maintain its |
| // snap container entry as long as the element is a scroller. This is because |
| // while the scroller does not snap, it still captures the snap areas in its |
| // subtree for whom it is the nearest ancestor scroll container per spec [1]. |
| // |
| // [1] https://drafts.csswg.org/css-scroll-snap/#overview |
| scrollable_area->SetSnapContainerDataNeedsUpdate(true); |
| } |
| |
| void SnapCoordinator::SnapAreaDidChange(LayoutBox& snap_area, |
| cc::ScrollSnapAlign scroll_snap_align) { |
| LayoutBox* old_container = snap_area.SnapContainer(); |
| if (scroll_snap_align.alignment_inline == cc::SnapAlignment::kNone && |
| scroll_snap_align.alignment_block == cc::SnapAlignment::kNone) { |
| snap_area.SetSnapContainer(nullptr); |
| if (old_container) |
| old_container->GetScrollableArea()->SetSnapContainerDataNeedsUpdate(true); |
| return; |
| } |
| |
| // If there is no ancestor snap container then this means that this snap |
| // area is being detached. In the worst case, the layout view is the |
| // ancestor snap container, which should exist as long as the document is |
| // not destroyed. |
| if (LayoutBox* new_container = FindSnapContainer(snap_area)) { |
| snap_area.SetSnapContainer(new_container); |
| // TODO(sunyunjia): consider keep the SnapAreas in a map so it is |
| // easier to update. |
| new_container->GetScrollableArea()->SetSnapContainerDataNeedsUpdate(true); |
| if (old_container && old_container != new_container) |
| old_container->GetScrollableArea()->SetSnapContainerDataNeedsUpdate(true); |
| } |
| } |
| |
| void SnapCoordinator::ResnapAllContainersIfNeeded() { |
| for (const auto* container : snap_containers_) { |
| if (!container->GetScrollableArea()->NeedsResnap()) |
| continue; |
| |
| auto* scrollable_area = ScrollableArea::GetForScrolling(container); |
| ScrollOffset initial_offset = scrollable_area->GetScrollOffset(); |
| scrollable_area->SnapAfterLayout(); |
| container->GetScrollableArea()->SetNeedsResnap(false); |
| |
| // If this is the first time resnapping all containers then this means this |
| // is the initial layout. We record whenever the initial scroll offset |
| // changes as a result of snapping. |
| // TODO(majidvp): This is here to measure potential web-compat impact of |
| // launching this feature. We should remove it once it is launched. |
| // crbug.com/866127 |
| if (!did_first_resnap_all_containers_ && |
| scrollable_area->GetScrollOffset() != initial_offset) { |
| UseCounter::Count(container->GetDocument(), |
| WebFeature::kScrollSnapCausesScrollOnInitialLayout); |
| } |
| } |
| did_first_resnap_all_containers_ = true; |
| } |
| |
| void SnapCoordinator::UpdateAllSnapContainerDataIfNeeded() { |
| for (auto* container : snap_containers_) { |
| if (container->GetScrollableArea()->SnapContainerDataNeedsUpdate()) |
| UpdateSnapContainerData(*container); |
| } |
| SetAnySnapContainerDataNeedsUpdate(false); |
| } |
| |
| void SnapCoordinator::UpdateSnapContainerData(LayoutBox& snap_container) { |
| ScrollableArea* scrollable_area = |
| ScrollableArea::GetForScrolling(&snap_container); |
| const auto* old_snap_container_data = scrollable_area->GetSnapContainerData(); |
| auto snap_type = GetPhysicalSnapType(snap_container); |
| scrollable_area->SetSnapContainerDataNeedsUpdate(false); |
| |
| // Scrollers that don't have any snap areas assigned to them and don't snap |
| // require no further processing. These are the most common types and thus |
| // returning as early as possible ensures efficiency. |
| if (!old_snap_container_data && snap_type.is_none) |
| return; |
| |
| cc::SnapContainerData snap_container_data(snap_type); |
| |
| DCHECK(snap_containers_.Contains(&snap_container)); |
| |
| // When snap type is 'none' we don't perform any snapping so there is no need |
| // to keep the area data up to date. So just update the type and skip updating |
| // areas as an optimization. |
| if (!snap_container_data.scroll_snap_type().is_none) { |
| FloatPoint max_position = scrollable_area->ScrollOffsetToPosition( |
| scrollable_area->MaximumScrollOffset()); |
| snap_container_data.set_max_position( |
| gfx::ScrollOffset(max_position.X(), max_position.Y())); |
| |
| // Scroll-padding represents inward offsets from the corresponding edge of |
| // the scrollport. |
| // https://drafts.csswg.org/css-scroll-snap-1/#scroll-padding Scrollport is |
| // the visual viewport of the scroll container (through which the scrollable |
| // overflow region can be viewed) coincides with its padding box. |
| // https://drafts.csswg.org/css-overflow-3/#scrollport. So we use the |
| // LayoutRect of the padding box here. The coordinate is relative to the |
| // container's border box. |
| PhysicalRect container_rect(snap_container.PhysicalPaddingBoxRect()); |
| |
| const ComputedStyle* container_style = snap_container.Style(); |
| LayoutRectOutsets container_padding( |
| // The percentage of scroll-padding is different from that of normal |
| // padding, as scroll-padding resolves the percentage against |
| // corresponding dimension of the scrollport[1], while the normal |
| // padding resolves that against "width".[2,3] We use |
| // MinimumValueForLength here to ensure kAuto is resolved to |
| // LayoutUnit() which is the correct behavior for padding. |
| // [1] https://drafts.csswg.org/css-scroll-snap-1/#scroll-padding |
| // "relative to the corresponding dimension of the scroll |
| // container’s |
| // scrollport" |
| // [2] https://drafts.csswg.org/css-box/#padding-props |
| // [3] See for example LayoutBoxModelObject::ComputedCSSPadding where it |
| // uses |MinimumValueForLength| but against the "width". |
| MinimumValueForLength(container_style->ScrollPaddingTop(), |
| container_rect.Height()), |
| MinimumValueForLength(container_style->ScrollPaddingRight(), |
| container_rect.Width()), |
| MinimumValueForLength(container_style->ScrollPaddingBottom(), |
| container_rect.Height()), |
| MinimumValueForLength(container_style->ScrollPaddingLeft(), |
| container_rect.Width())); |
| container_rect.Contract(container_padding); |
| snap_container_data.set_rect(FloatRect(container_rect)); |
| |
| if (snap_container_data.scroll_snap_type().strictness == |
| cc::SnapStrictness::kProximity) { |
| PhysicalSize size = container_rect.size; |
| size.Scale(kProximityRatio); |
| gfx::ScrollOffset range(size.width.ToFloat(), size.height.ToFloat()); |
| snap_container_data.set_proximity_range(range); |
| } |
| |
| cc::TargetSnapAreaElementIds new_target_ids; |
| const cc::TargetSnapAreaElementIds old_target_ids = |
| old_snap_container_data |
| ? old_snap_container_data->GetTargetSnapAreaElementIds() |
| : cc::TargetSnapAreaElementIds(); |
| |
| if (SnapAreaSet* snap_areas = snap_container.SnapAreas()) { |
| for (const LayoutBox* snap_area : *snap_areas) { |
| cc::SnapAreaData snap_area_data = |
| CalculateSnapAreaData(*snap_area, snap_container); |
| // The target snap elements should be preserved in the new container |
| // only if the respective snap areas are still present. |
| if (old_target_ids.x == snap_area_data.element_id) |
| new_target_ids.x = old_target_ids.x; |
| if (old_target_ids.y == snap_area_data.element_id) |
| new_target_ids.y = old_target_ids.y; |
| |
| snap_container_data.AddSnapAreaData(snap_area_data); |
| } |
| } |
| snap_container_data.SetTargetSnapAreaElementIds(new_target_ids); |
| } |
| if (!old_snap_container_data || |
| *old_snap_container_data != snap_container_data) { |
| snap_container.SetNeedsPaintPropertyUpdate(); |
| scrollable_area->SetSnapContainerData(snap_container_data); |
| // If the snap container data changed then we need to resnap. |
| scrollable_area->SetNeedsResnap(true); |
| } |
| } |
| |
| static cc::ScrollSnapAlign GetPhysicalAlignment( |
| const ComputedStyle& area_style, |
| const ComputedStyle& container_style) { |
| cc::ScrollSnapAlign align = area_style.GetScrollSnapAlign(); |
| if (container_style.IsHorizontalWritingMode()) |
| return align; |
| |
| cc::SnapAlignment tmp = align.alignment_inline; |
| align.alignment_inline = align.alignment_block; |
| align.alignment_block = tmp; |
| |
| if (container_style.IsFlippedBlocksWritingMode()) { |
| if (align.alignment_inline == cc::SnapAlignment::kStart) { |
| align.alignment_inline = cc::SnapAlignment::kEnd; |
| } else if (align.alignment_inline == cc::SnapAlignment::kEnd) { |
| align.alignment_inline = cc::SnapAlignment::kStart; |
| } |
| } |
| return align; |
| } |
| |
| cc::SnapAreaData SnapCoordinator::CalculateSnapAreaData( |
| const LayoutBox& snap_area, |
| const LayoutBox& snap_container) { |
| const ComputedStyle* container_style = snap_container.Style(); |
| const ComputedStyle* area_style = snap_area.Style(); |
| cc::SnapAreaData snap_area_data; |
| |
| // We assume that the snap_container is the snap_area's ancestor in layout |
| // tree, as the snap_container is found by walking up the layout tree in |
| // FindSnapContainer(). Under this assumption, |
| // snap_area.LocalToAncestorRect() returns the snap_area's position relative |
| // to the snap container's border box, while ignoring scroll offset. |
| PhysicalRect area_rect = snap_area.PhysicalBorderBoxRect(); |
| area_rect = snap_area.LocalToAncestorRect( |
| area_rect, &snap_container, |
| kTraverseDocumentBoundaries | kIgnoreScrollOffset); |
| |
| LayoutRectOutsets area_margin( |
| area_style->ScrollMarginTop(), area_style->ScrollMarginRight(), |
| area_style->ScrollMarginBottom(), area_style->ScrollMarginLeft()); |
| area_rect.Expand(area_margin); |
| snap_area_data.rect = FloatRect(area_rect); |
| |
| cc::ScrollSnapAlign align = |
| GetPhysicalAlignment(*area_style, *container_style); |
| snap_area_data.scroll_snap_align = align; |
| |
| snap_area_data.must_snap = |
| (area_style->ScrollSnapStop() == EScrollSnapStop::kAlways); |
| |
| snap_area_data.element_id = CompositorElementIdFromDOMNodeId( |
| DOMNodeIds::IdForNode(snap_area.GetNode())); |
| |
| return snap_area_data; |
| } |
| |
| #ifndef NDEBUG |
| |
| void SnapCoordinator::ShowSnapAreaMap() { |
| for (auto* const container : snap_containers_) |
| ShowSnapAreasFor(container); |
| } |
| |
| void SnapCoordinator::ShowSnapAreasFor(const LayoutBox* container) { |
| LOG(INFO) << *container->GetNode(); |
| if (SnapAreaSet* snap_areas = container->SnapAreas()) { |
| for (auto* const snap_area : *snap_areas) { |
| LOG(INFO) << " " << *snap_area->GetNode(); |
| } |
| } |
| } |
| |
| void SnapCoordinator::ShowSnapDataFor(const LayoutBox* snap_container) { |
| if (!snap_container) |
| return; |
| ScrollableArea* scrollable_area = |
| ScrollableArea::GetForScrolling(snap_container); |
| const auto* optional_data = |
| scrollable_area ? scrollable_area->GetSnapContainerData() : nullptr; |
| if (optional_data) |
| LOG(INFO) << *optional_data; |
| } |
| |
| #endif |
| |
| } // namespace blink |