blob: 9e873aea5237429b213c21124bc0b7e9c5bbf227 [file] [log] [blame]
// Copyright 2017 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 "cc/input/scroll_snap_data.h"
#include <algorithm>
#include <cmath>
namespace cc {
namespace {
bool IsMutualVisible(const SnapSearchResult& a, const SnapSearchResult& b) {
return a.visible_range().Contains(b.snap_offset()) &&
b.visible_range().Contains(a.snap_offset());
}
bool SnappableOnAxis(const SnapAreaData& area, SearchAxis search_axis) {
return search_axis == SearchAxis::kX
? area.scroll_snap_align.alignment_inline != SnapAlignment::kNone
: area.scroll_snap_align.alignment_block != SnapAlignment::kNone;
}
} // namespace
bool SnapVisibleRange::Contains(float value) const {
if (start_ < end_)
return start_ <= value && value <= end_;
return end_ <= value && value <= start_;
}
SnapSearchResult::SnapSearchResult(float offset, const SnapVisibleRange& range)
: snap_offset_(offset) {
set_visible_range(range);
}
void SnapSearchResult::set_visible_range(const SnapVisibleRange& range) {
DCHECK(range.start() <= range.end());
visible_range_ = range;
}
void SnapSearchResult::Clip(float max_snap, float max_visible) {
snap_offset_ = std::max(std::min(snap_offset_, max_snap), 0.0f);
visible_range_ = SnapVisibleRange(
std::max(std::min(visible_range_.start(), max_visible), 0.0f),
std::max(std::min(visible_range_.end(), max_visible), 0.0f));
}
void SnapSearchResult::Union(const SnapSearchResult& other) {
DCHECK(snap_offset_ == other.snap_offset_);
visible_range_ = SnapVisibleRange(
std::min(visible_range_.start(), other.visible_range_.start()),
std::max(visible_range_.end(), other.visible_range_.end()));
}
SnapContainerData::SnapContainerData()
: proximity_range_(gfx::ScrollOffset(std::numeric_limits<float>::max(),
std::numeric_limits<float>::max())) {}
SnapContainerData::SnapContainerData(ScrollSnapType type)
: scroll_snap_type_(type),
proximity_range_(gfx::ScrollOffset(std::numeric_limits<float>::max(),
std::numeric_limits<float>::max())) {}
SnapContainerData::SnapContainerData(ScrollSnapType type,
const gfx::RectF& rect,
const gfx::ScrollOffset& max)
: scroll_snap_type_(type),
rect_(rect),
max_position_(max),
proximity_range_(gfx::ScrollOffset(std::numeric_limits<float>::max(),
std::numeric_limits<float>::max())) {}
SnapContainerData::SnapContainerData(const SnapContainerData& other) = default;
SnapContainerData::SnapContainerData(SnapContainerData&& other) = default;
SnapContainerData::~SnapContainerData() = default;
SnapContainerData& SnapContainerData::operator=(
const SnapContainerData& other) = default;
SnapContainerData& SnapContainerData::operator=(SnapContainerData&& other) =
default;
void SnapContainerData::AddSnapAreaData(SnapAreaData snap_area_data) {
snap_area_list_.push_back(snap_area_data);
}
bool SnapContainerData::FindSnapPosition(
const gfx::ScrollOffset& current_position,
bool should_snap_on_x,
bool should_snap_on_y,
gfx::ScrollOffset* snap_position) const {
SnapAxis axis = scroll_snap_type_.axis;
should_snap_on_x &= (axis == SnapAxis::kX || axis == SnapAxis::kBoth);
should_snap_on_y &= (axis == SnapAxis::kY || axis == SnapAxis::kBoth);
if (!should_snap_on_x && !should_snap_on_y)
return false;
base::Optional<SnapSearchResult> closest_x, closest_y;
// A region that includes every reachable scroll position.
gfx::RectF scrollable_region(0, 0, max_position_.x(), max_position_.y());
if (should_snap_on_x) {
// Start from current offset in the cross axis and assume it's always
// visible.
SnapSearchResult initial_snap_position_y = {
current_position.y(), SnapVisibleRange(0, max_position_.x())};
closest_x = FindClosestValidArea(SearchAxis::kX, current_position.x(),
initial_snap_position_y);
}
if (should_snap_on_y) {
SnapSearchResult initial_snap_position_x = {
current_position.x(), SnapVisibleRange(0, max_position_.y())};
closest_y = FindClosestValidArea(SearchAxis::kY, current_position.y(),
initial_snap_position_x);
}
if (!closest_x.has_value() && !closest_y.has_value())
return false;
// If snapping in one axis pushes off-screen the other snap area, this snap
// position is invalid. https://drafts.csswg.org/css-scroll-snap-1/#snap-scope
// In this case, we choose the axis whose snap area is closer, and find a
// mutual visible snap area on the other axis.
if (closest_x.has_value() && closest_y.has_value() &&
!IsMutualVisible(closest_x.value(), closest_y.value())) {
bool candidate_on_x_axis_is_closer =
std::abs(closest_x.value().snap_offset() - current_position.x()) <=
std::abs(closest_y.value().snap_offset() - current_position.y());
if (candidate_on_x_axis_is_closer)
closest_y = FindClosestValidArea(SearchAxis::kY, current_position.y(),
closest_x.value());
else
closest_x = FindClosestValidArea(SearchAxis::kX, current_position.x(),
closest_y.value());
}
*snap_position = current_position;
if (closest_x.has_value())
snap_position->set_x(closest_x.value().snap_offset());
if (closest_y.has_value())
snap_position->set_y(closest_y.value().snap_offset());
return true;
}
base::Optional<SnapSearchResult> SnapContainerData::FindClosestValidArea(
SearchAxis axis,
float current_offset,
const SnapSearchResult& cros_axis_snap_result) const {
base::Optional<SnapSearchResult> result;
base::Optional<SnapSearchResult> inplace;
// The valid snap offsets immediately before and after the current offset.
float prev = std::numeric_limits<float>::lowest();
float next = std::numeric_limits<float>::max();
float smallest_distance =
axis == SearchAxis::kX ? proximity_range_.x() : proximity_range_.y();
for (const SnapAreaData& area : snap_area_list_) {
if (!SnappableOnAxis(area, axis))
continue;
SnapSearchResult candidate = GetSnapSearchResult(axis, area);
if (IsSnapportCoveredOnAxis(axis, current_offset, area.rect)) {
// Since snap area is currently covering the snapport, we consider the
// current offset as a valid snap position.
SnapSearchResult inplace_candidate = candidate;
inplace_candidate.set_snap_offset(current_offset);
if (IsMutualVisible(inplace_candidate, cros_axis_snap_result)) {
// If we've already found a valid overflowing area before, we enlarge
// the area's visible region.
if (inplace.has_value())
inplace.value().Union(inplace_candidate);
else
inplace = inplace_candidate;
// Even if a snap area covers the snapport, we need to continue this
// search to find previous and next snap positions and also to have
// alternative snap candidates if this inplace candidate is ultimately
// rejected. And this covering snap area has its own alignment that may
// generates a snap position rejecting the current inplace candidate.
}
}
if (!IsMutualVisible(candidate, cros_axis_snap_result))
continue;
float distance = std::abs(candidate.snap_offset() - current_offset);
if (distance < smallest_distance) {
smallest_distance = distance;
result = candidate;
}
if (candidate.snap_offset() < current_offset &&
candidate.snap_offset() > prev)
prev = candidate.snap_offset();
if (candidate.snap_offset() > current_offset &&
candidate.snap_offset() < next)
next = candidate.snap_offset();
}
// According to the spec [1], if the snap area is covering the snapport, the
// scroll offset is a valid snap position only if the distance between the
// geometrically previous and subsequent snap positions in that axis is larger
// than size of the snapport in that axis.
// [1] https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow
float size = axis == SearchAxis::kX ? rect_.width() : rect_.height();
if (inplace.has_value() &&
(prev == std::numeric_limits<float>::lowest() ||
next == std::numeric_limits<float>::max() || next - prev > size)) {
return inplace;
}
return result;
}
SnapSearchResult SnapContainerData::GetSnapSearchResult(
SearchAxis axis,
const SnapAreaData& area) const {
SnapSearchResult result;
if (axis == SearchAxis::kX) {
result.set_visible_range(SnapVisibleRange(area.rect.y() - rect_.bottom(),
area.rect.bottom() - rect_.y()));
// https://www.w3.org/TR/css-scroll-snap-1/#scroll-snap-align
switch (area.scroll_snap_align.alignment_inline) {
case SnapAlignment::kStart:
result.set_snap_offset(area.rect.x() - rect_.x());
break;
case SnapAlignment::kCenter:
result.set_snap_offset(area.rect.CenterPoint().x() -
rect_.CenterPoint().x());
break;
case SnapAlignment::kEnd:
result.set_snap_offset(area.rect.right() - rect_.right());
break;
default:
NOTREACHED();
}
result.Clip(max_position_.x(), max_position_.y());
} else {
result.set_visible_range(SnapVisibleRange(area.rect.x() - rect_.right(),
area.rect.right() - rect_.x()));
switch (area.scroll_snap_align.alignment_block) {
case SnapAlignment::kStart:
result.set_snap_offset(area.rect.y() - rect_.y());
break;
case SnapAlignment::kCenter:
result.set_snap_offset(area.rect.CenterPoint().y() -
rect_.CenterPoint().y());
break;
case SnapAlignment::kEnd:
result.set_snap_offset(area.rect.bottom() - rect_.bottom());
break;
default:
NOTREACHED();
}
result.Clip(max_position_.y(), max_position_.x());
}
return result;
}
bool SnapContainerData::IsSnapportCoveredOnAxis(
SearchAxis axis,
float current_offset,
const gfx::RectF& area_rect) const {
if (axis == SearchAxis::kX) {
if (area_rect.width() < rect_.width())
return false;
float left = area_rect.x() - rect_.x();
float right = area_rect.right() - rect_.right();
return current_offset >= left && current_offset <= right;
} else {
if (area_rect.height() < rect_.height())
return false;
float top = area_rect.y() - rect_.y();
float bottom = area_rect.bottom() - rect_.bottom();
return current_offset >= top && current_offset <= bottom;
}
}
std::ostream& operator<<(std::ostream& ostream, const SnapAreaData& area_data) {
return ostream << area_data.rect.ToString();
}
std::ostream& operator<<(std::ostream& ostream,
const SnapContainerData& container_data) {
ostream << "container_rect: " << container_data.rect().ToString();
ostream << "area_rects: ";
for (size_t i = 0; i < container_data.size(); ++i) {
ostream << container_data.at(i) << "\n";
}
return ostream;
}
} // namespace cc