| // 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/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/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/scroll/scroll_snap_data.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_container_map_() {} |
| |
| SnapCoordinator::~SnapCoordinator() = default; |
| |
| SnapCoordinator* SnapCoordinator::Create() { |
| return MakeGarbageCollected<SnapCoordinator>(); |
| } |
| |
| // Returns the scroll container that can be affected by this snap area. |
| static LayoutBox* FindSnapContainer(const LayoutBox& snap_area) { |
| // 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* viewport_defining_element = |
| snap_area.GetDocument().ViewportDefiningElement(); |
| LayoutBox* box = snap_area.ContainingBlock(); |
| while (box && !box->HasOverflowClip() && !box->IsLayoutView() && |
| box->GetNode() != viewport_defining_element) |
| box = box->ContainingBlock(); |
| |
| // If we reach to viewportDefiningElement then we dispatch to viewport |
| if (box && box->GetNode() == viewport_defining_element) |
| return snap_area.GetDocument().GetLayoutView(); |
| |
| return box; |
| } |
| |
| void SnapCoordinator::SnapAreaDidChange(LayoutBox& snap_area, |
| ScrollSnapAlign scroll_snap_align) { |
| LayoutBox* old_container = snap_area.SnapContainer(); |
| if (scroll_snap_align.alignment_inline == SnapAlignment::kNone && |
| scroll_snap_align.alignment_block == SnapAlignment::kNone) { |
| snap_area.SetSnapContainer(nullptr); |
| if (old_container) |
| UpdateSnapContainerData(*old_container); |
| return; |
| } |
| |
| 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. |
| // TODO(sunyunjia): Only update when the localframe doesn't need layout. |
| UpdateSnapContainerData(*new_container); |
| if (old_container && old_container != new_container) |
| UpdateSnapContainerData(*old_container); |
| } else { |
| // TODO(majidvp): keep track of snap areas that do not have any |
| // container so that we check them again when a new container is |
| // added to the page. |
| } |
| } |
| |
| void SnapCoordinator::UpdateAllSnapContainerData() { |
| for (const auto& entry : snap_container_map_) { |
| UpdateSnapContainerData(*entry.key); |
| } |
| } |
| |
| static ScrollableArea* ScrollableAreaForSnapping(const LayoutBox& layout_box) { |
| return layout_box.IsLayoutView() |
| ? layout_box.GetFrameView()->GetScrollableArea() |
| : layout_box.GetScrollableArea(); |
| } |
| |
| static ScrollSnapType GetPhysicalSnapType(const LayoutBox& snap_container) { |
| ScrollSnapType scroll_snap_type = snap_container.Style()->GetScrollSnapType(); |
| if (scroll_snap_type.axis == SnapAxis::kInline) { |
| if (snap_container.Style()->IsHorizontalWritingMode()) |
| scroll_snap_type.axis = SnapAxis::kX; |
| else |
| scroll_snap_type.axis = SnapAxis::kY; |
| } |
| if (scroll_snap_type.axis == SnapAxis::kBlock) { |
| if (snap_container.Style()->IsHorizontalWritingMode()) |
| scroll_snap_type.axis = SnapAxis::kY; |
| else |
| scroll_snap_type.axis = SnapAxis::kX; |
| } |
| // Writing mode does not affect the cases where axis kX, kY or kBoth. |
| return scroll_snap_type; |
| } |
| |
| void SnapCoordinator::UpdateSnapContainerData(const LayoutBox& snap_container) { |
| if (snap_container.Style()->GetScrollSnapType().is_none) |
| return; |
| |
| SnapContainerData snap_container_data(GetPhysicalSnapType(snap_container)); |
| |
| ScrollableArea* scrollable_area = ScrollableAreaForSnapping(snap_container); |
| if (!scrollable_area) |
| return; |
| 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. |
| LayoutRect 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 == |
| SnapStrictness::kProximity) { |
| LayoutSize size = container_rect.Size(); |
| size.Scale(kProximityRatio); |
| gfx::ScrollOffset range(size.Width().ToFloat(), size.Height().ToFloat()); |
| snap_container_data.set_proximity_range(range); |
| } |
| |
| if (SnapAreaSet* snap_areas = snap_container.SnapAreas()) { |
| for (const LayoutBox* snap_area : *snap_areas) { |
| snap_container_data.AddSnapAreaData( |
| CalculateSnapAreaData(*snap_area, snap_container)); |
| } |
| } |
| snap_container_map_.Set(&snap_container, snap_container_data); |
| } |
| |
| static ScrollSnapAlign GetPhysicalAlignment( |
| const ComputedStyle& area_style, |
| const ComputedStyle& container_style) { |
| ScrollSnapAlign align = area_style.GetScrollSnapAlign(); |
| if (container_style.IsHorizontalWritingMode()) |
| return align; |
| |
| SnapAlignment tmp = align.alignment_inline; |
| align.alignment_inline = align.alignment_block; |
| align.alignment_block = tmp; |
| |
| if (container_style.IsFlippedBlocksWritingMode()) { |
| if (align.alignment_inline == SnapAlignment::kStart) { |
| align.alignment_inline = SnapAlignment::kEnd; |
| } else if (align.alignment_inline == SnapAlignment::kEnd) { |
| align.alignment_inline = SnapAlignment::kStart; |
| } |
| } |
| return align; |
| } |
| |
| 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(); |
| 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.LocalToAncestorQuad() returns the snap_area's position relative |
| // to its container's border box. And the |area| below represents the |
| // snap_area rect in respect to the snap_container. |
| LayoutRect area_rect(LayoutPoint(), LayoutSize(snap_area.OffsetWidth(), |
| snap_area.OffsetHeight())); |
| area_rect = EnclosingLayoutRect( |
| snap_area |
| .LocalToAncestorQuad(FloatRect(area_rect), &snap_container, |
| kUseTransforms | kTraverseDocumentBoundaries) |
| .BoundingBox()); |
| ScrollableArea* scrollable_area = ScrollableAreaForSnapping(snap_container); |
| if (scrollable_area) { |
| if (snap_container.IsLayoutView()) |
| area_rect = snap_container.GetFrameView()->FrameToDocument(area_rect); |
| else |
| area_rect.MoveBy(LayoutPoint(scrollable_area->ScrollPosition())); |
| } |
| |
| 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); |
| |
| ScrollSnapAlign align = GetPhysicalAlignment(*area_style, *container_style); |
| snap_area_data.scroll_snap_align = align; |
| |
| snap_area_data.must_snap = |
| (area_style->ScrollSnapStop() == EScrollSnapStop::kAlways); |
| |
| return snap_area_data; |
| } |
| |
| base::Optional<FloatPoint> SnapCoordinator::GetSnapPosition( |
| const LayoutBox& snap_container, |
| const SnapSelectionStrategy& strategy) const { |
| auto iter = snap_container_map_.find(&snap_container); |
| if (iter == snap_container_map_.end()) |
| return base::nullopt; |
| |
| const SnapContainerData& data = iter->value; |
| if (!data.size()) |
| return base::nullopt; |
| |
| gfx::ScrollOffset snap_position; |
| if (data.FindSnapPosition(strategy, &snap_position)) { |
| FloatPoint snap_point(snap_position.x(), snap_position.y()); |
| return snap_point; |
| } |
| |
| return base::nullopt; |
| } |
| |
| bool SnapCoordinator::SnapAtCurrentPosition(const LayoutBox& snap_container, |
| bool scrolled_x, |
| bool scrolled_y) const { |
| ScrollableArea* scrollable_area = ScrollableAreaForSnapping(snap_container); |
| if (!scrollable_area) |
| return false; |
| FloatPoint current_position = scrollable_area->ScrollPosition(); |
| return SnapForEndPosition(snap_container, current_position, scrolled_x, |
| scrolled_y); |
| } |
| |
| bool SnapCoordinator::SnapForEndPosition(const LayoutBox& snap_container, |
| const FloatPoint& end_position, |
| bool scrolled_x, |
| bool scrolled_y) const { |
| std::unique_ptr<SnapSelectionStrategy> strategy = |
| SnapSelectionStrategy::CreateForEndPosition( |
| gfx::ScrollOffset(end_position), scrolled_x, scrolled_y); |
| return PerformSnapping(snap_container, *strategy); |
| } |
| |
| bool SnapCoordinator::SnapForDirection(const LayoutBox& snap_container, |
| const ScrollOffset& delta) const { |
| ScrollableArea* scrollable_area = ScrollableAreaForSnapping(snap_container); |
| if (!scrollable_area) |
| return false; |
| FloatPoint current_position = scrollable_area->ScrollPosition(); |
| std::unique_ptr<SnapSelectionStrategy> strategy = |
| SnapSelectionStrategy::CreateForDirection( |
| gfx::ScrollOffset(current_position), |
| gfx::ScrollOffset(delta.Width(), delta.Height())); |
| return PerformSnapping(snap_container, *strategy); |
| } |
| |
| bool SnapCoordinator::SnapForEndAndDirection(const LayoutBox& snap_container, |
| const ScrollOffset& delta) const { |
| ScrollableArea* scrollable_area = ScrollableAreaForSnapping(snap_container); |
| if (!scrollable_area) |
| return false; |
| FloatPoint current_position = scrollable_area->ScrollPosition(); |
| std::unique_ptr<SnapSelectionStrategy> strategy = |
| SnapSelectionStrategy::CreateForEndAndDirection( |
| gfx::ScrollOffset(current_position), |
| gfx::ScrollOffset(delta.Width(), delta.Height())); |
| return PerformSnapping(snap_container, *strategy); |
| } |
| |
| bool SnapCoordinator::PerformSnapping( |
| const LayoutBox& snap_container, |
| const SnapSelectionStrategy& strategy) const { |
| ScrollableArea* scrollable_area = ScrollableAreaForSnapping(snap_container); |
| if (!scrollable_area) |
| return false; |
| |
| base::Optional<FloatPoint> snap_point = |
| GetSnapPosition(snap_container, strategy); |
| if (!snap_point.has_value()) |
| return false; |
| |
| scrollable_area->CancelScrollAnimation(); |
| scrollable_area->CancelProgrammaticScrollAnimation(); |
| if (snap_point.value() != scrollable_area->ScrollPosition()) { |
| scrollable_area->SetScrollOffset( |
| scrollable_area->ScrollPositionToOffset(snap_point.value()), |
| kProgrammaticScroll, kScrollBehaviorSmooth); |
| } |
| return true; |
| } |
| |
| void SnapCoordinator::SnapContainerDidChange(LayoutBox& snap_container, |
| ScrollSnapType scroll_snap_type) { |
| snap_container.SetNeedsPaintPropertyUpdate(); |
| if (scroll_snap_type.is_none) { |
| snap_container_map_.erase(&snap_container); |
| snap_container.ClearSnapAreas(); |
| return; |
| } |
| |
| // TODO(sunyunjia): Only update when the localframe doesn't need layout. |
| UpdateSnapContainerData(snap_container); |
| |
| // TODO(majidvp): Add logic to correctly handle orphaned snap areas here. |
| // 1. Removing container: find a new snap container for its orphan snap |
| // areas (most likely nearest ancestor of current container) otherwise add |
| // them to an orphan list. |
| // 2. Adding container: may take over snap areas from nearest ancestor snap |
| // container or from existing areas in orphan pool. |
| } |
| |
| base::Optional<SnapContainerData> SnapCoordinator::GetSnapContainerData( |
| const LayoutBox& snap_container) const { |
| auto iter = snap_container_map_.find(&snap_container); |
| if (iter != snap_container_map_.end()) { |
| return iter->value; |
| } |
| return base::nullopt; |
| } |
| |
| #ifndef NDEBUG |
| |
| void SnapCoordinator::ShowSnapAreaMap() { |
| for (auto* const container : snap_container_map_.Keys()) |
| 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) { |
| auto iter = snap_container_map_.find(snap_container); |
| if (iter == snap_container_map_.end()) |
| return; |
| LOG(INFO) << iter->value; |
| } |
| |
| #endif |
| |
| } // namespace blink |