// 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 #include "base/optional.h" namespace cc { namespace { bool IsVisible(const gfx::ScrollOffset& point, const gfx::RectF& visible_region) { return point.x() >= visible_region.x() && point.x() <= visible_region.right() && point.y() >= visible_region.y() && point.y() <= visible_region.bottom(); } bool IsMutualVisible(const SnapAreaData& area_x, const SnapAreaData& area_y) { gfx::ScrollOffset position(area_x.snap_position.x(), area_y.snap_position.y()); return IsVisible(position, area_x.visible_region) && IsVisible(position, area_y.visible_region); } bool SnappableOnAxis(const SnapAreaData& area, SearchAxis search_axis) { return search_axis == SearchAxis::kX ? area.snap_axis == SnapAxis::kX || area.snap_axis == SnapAxis::kBoth : area.snap_axis == SnapAxis::kY || area.snap_axis == SnapAxis::kBoth; } // Finds the best SnapArea candidate that minimizes the distance between current // and candidate positions, while satisfying three invariants: // - |candidate_position| is in |target_region| // - |current_position| is in candidate's visible region // - |target_position| is in candidate's visible region // |current_position| is the scroll position of the container before snapping. // |target_position| is the snap position we have found on the other axis. // |target_region| is the visible region of the target position's area. base::Optional FindClosestValidArea( SearchAxis search_axis, const gfx::ScrollOffset& current_position, const gfx::ScrollOffset& target_position, const gfx::RectF& target_region, const gfx::ScrollOffset& proximity_range, const SnapAreaList& list) { if (list.empty()) return base::nullopt; base::Optional closest_area; float smallest_distance = search_axis == SearchAxis::kX ? proximity_range.x() : proximity_range.y(); for (const SnapAreaData& area : list) { if (!SnappableOnAxis(area, search_axis)) continue; gfx::ScrollOffset candidate_position = search_axis == SearchAxis::kX ? gfx::ScrollOffset(area.snap_position.x(), target_position.y()) : gfx::ScrollOffset(target_position.x(), area.snap_position.y()); if (!IsVisible(candidate_position, target_region) || !IsVisible(current_position, area.visible_region) || !IsVisible(target_position, area.visible_region)) continue; gfx::ScrollOffset offset = current_position - candidate_position; float distance = search_axis == SearchAxis::kX ? std::abs(offset.x()) : std::abs(offset.y()); if (distance < smallest_distance) { smallest_distance = distance; closest_area = area; } } return closest_area; } } // namespace SnapContainerData::SnapContainerData() : proximity_range_(gfx::ScrollOffset(std::numeric_limits::max(), std::numeric_limits::max())) {} SnapContainerData::SnapContainerData(ScrollSnapType type) : scroll_snap_type_(type), proximity_range_(gfx::ScrollOffset(std::numeric_limits::max(), std::numeric_limits::max())) {} SnapContainerData::SnapContainerData(ScrollSnapType type, gfx::ScrollOffset max) : scroll_snap_type_(type), max_position_(max), proximity_range_(gfx::ScrollOffset(std::numeric_limits::max(), std::numeric_limits::max())) {} SnapContainerData::SnapContainerData(const SnapContainerData& other) = default; SnapContainerData::SnapContainerData(SnapContainerData&& other) : scroll_snap_type_(other.scroll_snap_type_), max_position_(other.max_position_), proximity_range_(other.proximity_range_), snap_area_list_(std::move(other.snap_area_list_)) {} SnapContainerData::~SnapContainerData() = default; SnapContainerData& SnapContainerData::operator=( const SnapContainerData& other) = default; SnapContainerData& SnapContainerData::operator=(SnapContainerData&& other) { scroll_snap_type_ = other.scroll_snap_type_; max_position_ = other.max_position_; proximity_range_ = other.proximity_range_; snap_area_list_ = std::move(other.snap_area_list_); return *this; } 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 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) { closest_x = FindClosestValidArea(SearchAxis::kX, current_position, current_position, scrollable_region, proximity_range_, snap_area_list_); } if (should_snap_on_y) { closest_y = FindClosestValidArea(SearchAxis::kY, current_position, current_position, scrollable_region, proximity_range_, snap_area_list_); } 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_position.x() - current_position.x()) <= std::abs(closest_y.value().snap_position.y() - current_position.y()); if (candidate_on_x_axis_is_closer) { gfx::ScrollOffset snapped(closest_x.value().snap_position.x(), current_position.y()); closest_y = FindClosestValidArea( SearchAxis::kY, current_position, snapped, closest_x.value().visible_region, proximity_range_, snap_area_list_); } else { gfx::ScrollOffset snapped(current_position.x(), closest_y.value().snap_position.y()); closest_x = FindClosestValidArea( SearchAxis::kX, current_position, snapped, closest_y.value().visible_region, proximity_range_, snap_area_list_); } } *snap_position = current_position; if (closest_x.has_value()) snap_position->set_x(closest_x.value().snap_position.x()); if (closest_y.has_value()) snap_position->set_y(closest_y.value().snap_position.y()); return true; } std::ostream& operator<<(std::ostream& ostream, const SnapAreaData& area_data) { return ostream << area_data.snap_position.ToString() << "\t" << "visible in: " << area_data.visible_region.ToString(); } std::ostream& operator<<(std::ostream& ostream, const SnapContainerData& container_data) { for (size_t i = 0; i < container_data.size(); ++i) { ostream << container_data.at(i) << "\n"; } return ostream; } } // namespace cc