[go: up one dir, main page]

blob: 984bd8077da3bcc08f32d1b140688841bc70dece [file] [log] [blame]
// 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"
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;
// 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::SnapContainerDidChange(LayoutBox& snap_container,
bool is_removed) {
if (is_removed) {
snap_container_map_.erase(&snap_container);
return;
}
// Scroll snap properties have no effect on the viewport defining element
// instead they are propagated to (See Document::PropagateStyleToViewport) and
// handled by the LayoutView.
if (snap_container.GetNode() ==
snap_container.GetDocument().ViewportDefiningElement())
return;
bool is_scroll_container =
snap_container.IsLayoutView() || snap_container.HasOverflowClip();
if (!is_scroll_container) {
snap_container_map_.erase(&snap_container);
snap_container.ClearSnapAreas();
snap_container.SetNeedsPaintPropertyUpdate();
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
// 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.
}
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)
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 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 kX, kY or kBoth.
return scroll_snap_type;
}
void SnapCoordinator::UpdateSnapContainerData(LayoutBox& snap_container) {
cc::SnapContainerData snap_container_data(
GetPhysicalSnapType(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) {
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.
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);
}
if (SnapAreaSet* snap_areas = snap_container.SnapAreas()) {
for (const LayoutBox* snap_area : *snap_areas) {
snap_container_data.AddSnapAreaData(
CalculateSnapAreaData(*snap_area, snap_container));
}
}
}
auto old_snap_container_data = GetSnapContainerData(snap_container);
if (old_snap_container_data != snap_container_data)
snap_container.SetNeedsPaintPropertyUpdate();
snap_container_map_.Set(&snap_container, snap_container_data);
}
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 its container's border box. And the |area| below represents the
// snap_area rect in respect to the snap_container.
PhysicalRect area_rect = snap_area.PhysicalBorderBoxRect();
area_rect = snap_area.LocalToAncestorRect(area_rect, &snap_container,
kTraverseDocumentBoundaries);
ScrollableArea* scrollable_area = ScrollableAreaForSnapping(snap_container);
if (scrollable_area) {
if (snap_container.IsLayoutView()) {
area_rect = snap_container.GetFrameView()->FrameToDocument(area_rect);
} else {
area_rect.Move(PhysicalOffset::FromFloatPointRound(
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);
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);
return snap_area_data;
}
base::Optional<FloatPoint> SnapCoordinator::GetSnapPosition(
const LayoutBox& snap_container,
const cc::SnapSelectionStrategy& strategy) const {
// const_cast is safe here because we only need to modify the type to match
// the key type, not actually mutating the object.
auto iter = snap_container_map_.find(&const_cast<LayoutBox&>(snap_container));
if (iter == snap_container_map_.end())
return base::nullopt;
const cc::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<cc::SnapSelectionStrategy> strategy =
cc::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<cc::SnapSelectionStrategy> strategy =
cc::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<cc::SnapSelectionStrategy> strategy =
cc::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 cc::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;
}
base::Optional<cc::SnapContainerData> SnapCoordinator::GetSnapContainerData(
const LayoutBox& snap_container) const {
// const_cast is safe here because we only need to modify the type to match
// the key type, not actually mutating the object.
auto iter = snap_container_map_.find(&const_cast<LayoutBox&>(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) {
// const_cast is safe here because we only need to modify the type to match
// the key type, not actually mutating the object.
auto iter = snap_container_map_.find(const_cast<LayoutBox*>(snap_container));
if (iter == snap_container_map_.end())
return;
LOG(INFO) << iter->value;
}
#endif
} // namespace blink