| // Copyright 2020 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/animation/scroll_timeline_offset.h" |
| |
| #include "base/optional.h" |
| #include "third_party/blink/renderer/bindings/core/v8/css_numeric_value_or_string_or_css_keyword_value_or_scroll_timeline_element_based_offset.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_scroll_timeline_element_based_offset.h" |
| #include "third_party/blink/renderer/core/css/css_to_length_conversion_data.h" |
| #include "third_party/blink/renderer/core/css/cssom/css_keyword_value.h" |
| #include "third_party/blink/renderer/core/css/cssom/css_numeric_value.h" |
| #include "third_party/blink/renderer/core/dom/node_computed_style.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/style/data_equivalency.h" |
| #include "third_party/blink/renderer/platform/geometry/layout_unit.h" |
| #include "third_party/blink/renderer/platform/geometry/length_functions.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| bool ValidateElementBasedOffset(ScrollTimelineElementBasedOffset* offset) { |
| if (!offset->hasTarget()) |
| return false; |
| |
| if (offset->hasThreshold()) { |
| if (offset->threshold() < 0 || offset->threshold() > 1) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // TODO(majidvp): Dedup. This is a copy of the function in |
| // third_party/blink/renderer/core/intersection_observer/intersection_geometry.cc |
| // http://crbug.com/1023375 |
| |
| // Return true if ancestor is in the containing block chain above descendant. |
| bool IsContainingBlockChainDescendant(const LayoutObject* descendant, |
| const LayoutObject* ancestor) { |
| if (!ancestor || !descendant) |
| return false; |
| LocalFrame* ancestor_frame = ancestor->GetDocument().GetFrame(); |
| LocalFrame* descendant_frame = descendant->GetDocument().GetFrame(); |
| if (ancestor_frame != descendant_frame) |
| return false; |
| |
| while (descendant && descendant != ancestor) |
| descendant = descendant->ContainingBlock(); |
| return descendant; |
| } |
| |
| bool ElementBasedOffsetsEqual(ScrollTimelineElementBasedOffset* o1, |
| ScrollTimelineElementBasedOffset* o2) { |
| if (o1 == o2) |
| return true; |
| if (!o1 || !o2) |
| return false; |
| // TODO(crbug.com/1070871): Use targetOr(nullptr) after migration is done. |
| Element* target_or_null1 = o1->hasTarget() ? o1->target() : nullptr; |
| Element* target_or_null2 = o2->hasTarget() ? o2->target() : nullptr; |
| return target_or_null1 == target_or_null2 && o1->edge() == o2->edge() && |
| o1->threshold() == o2->threshold(); |
| } |
| |
| CSSKeywordValue* GetCSSKeywordValue(const ScrollTimelineOffsetValue& offset) { |
| if (offset.IsCSSKeywordValue()) |
| return offset.GetAsCSSKeywordValue(); |
| // CSSKeywordish: |
| if (offset.IsString() && !offset.GetAsString().IsEmpty()) |
| return CSSKeywordValue::Create(offset.GetAsString()); |
| return nullptr; |
| } |
| |
| } // namespace |
| |
| // static |
| ScrollTimelineOffset* ScrollTimelineOffset::Create( |
| const ScrollTimelineOffsetValue& input_offset) { |
| if (input_offset.IsCSSNumericValue()) { |
| auto* numeric = input_offset.GetAsCSSNumericValue(); |
| const auto& offset = To<CSSPrimitiveValue>(*numeric->ToCSSValue()); |
| bool matches_length_percentage = offset.IsLength() || |
| offset.IsPercentage() || |
| offset.IsCalculatedPercentageWithLength(); |
| if (!matches_length_percentage) |
| return nullptr; |
| return MakeGarbageCollected<ScrollTimelineOffset>(&offset); |
| } |
| |
| if (input_offset.IsScrollTimelineElementBasedOffset()) { |
| auto* offset = input_offset.GetAsScrollTimelineElementBasedOffset(); |
| if (!ValidateElementBasedOffset(offset)) |
| return nullptr; |
| |
| return MakeGarbageCollected<ScrollTimelineOffset>(offset); |
| } |
| |
| if (auto* keyword = GetCSSKeywordValue(input_offset)) { |
| if (keyword->KeywordValueID() != CSSValueID::kAuto) |
| return nullptr; |
| return MakeGarbageCollected<ScrollTimelineOffset>(); |
| } |
| |
| return nullptr; |
| } |
| |
| base::Optional<double> ScrollTimelineOffset::ResolveOffset( |
| Node* scroll_source, |
| ScrollOrientation orientation, |
| double max_offset, |
| double default_offset) { |
| const LayoutBox* root_box = scroll_source->GetLayoutBox(); |
| DCHECK(root_box); |
| Document& document = root_box->GetDocument(); |
| |
| if (length_based_offset_) { |
| // Resolve scroll based offset. |
| const ComputedStyle& computed_style = root_box->StyleRef(); |
| const ComputedStyle* root_style = |
| document.documentElement() |
| ? document.documentElement()->GetComputedStyle() |
| : document.GetComputedStyle(); |
| |
| CSSToLengthConversionData conversion_data = CSSToLengthConversionData( |
| &computed_style, root_style, document.GetLayoutView(), |
| computed_style.EffectiveZoom()); |
| double resolved = FloatValueForLength( |
| length_based_offset_->ConvertToLength(conversion_data), max_offset); |
| |
| return resolved; |
| } else if (element_based_offset_) { |
| if (!element_based_offset_->hasTarget()) |
| return base::nullopt; |
| Element* target = element_based_offset_->target(); |
| const LayoutBox* target_box = target->GetLayoutBox(); |
| |
| // It is possible for target to not have a layout box e.g., if it is an |
| // unattached element. In which case we return the default offset for now. |
| // |
| // See the spec discussion here: |
| // https://github.com/w3c/csswg-drafts/issues/4337#issuecomment-610997231 |
| if (!target_box) |
| return base::nullopt; |
| |
| if (!IsContainingBlockChainDescendant(target_box, root_box)) |
| return base::nullopt; |
| |
| PhysicalRect target_rect = target_box->PhysicalBorderBoxRect(); |
| target_rect = target_box->LocalToAncestorRect( |
| target_rect, root_box, |
| kTraverseDocumentBoundaries | kIgnoreScrollOffset); |
| |
| PhysicalRect root_rect(root_box->PhysicalBorderBoxRect()); |
| |
| LayoutUnit root_edge; |
| LayoutUnit target_edge; |
| |
| // Here is the simple diagram that shows the computation. |
| // |
| // +-----+ |
| // | | +------+ |
| // | | | | |
| // edge:start +----+-----+-------------------+-----+-------+ |
| // | |xxxxxx| |xxxxx| | |
| // | +------+ |xxxxx| | |
| // | +-----+ | |
| // | | |
| // threshold: | A) 0 B) 0.5 C) 1 | |
| // | | |
| // | +-----+ | |
| // | +------+ |xxxxx| | |
| // | |xxxxxx| |xxxxx| | |
| // edge: end +----+-----+-------------------+-----+-------+ |
| // | | | | |
| // | | +------+ |
| // +-----+ |
| // |
| // We always take the target top edge and compute the distance to the |
| // root's selected edge. This give us (C) in start edge case and (A) in |
| // end edge case. |
| // |
| // To take threshold into account we simply add (1-threshold) or threshold |
| // in start and end edge cases respectively. |
| bool is_start = element_based_offset_->edge() == "start"; |
| float threshold_adjustment = is_start |
| ? (1 - element_based_offset_->threshold()) |
| : element_based_offset_->threshold(); |
| |
| if (orientation == kVerticalScroll) { |
| root_edge = is_start ? root_rect.Y() : root_rect.Bottom(); |
| target_edge = target_rect.Y(); |
| // Note that threshold is considered as a portion of target and not as a |
| // portion of root. IntersectionObserver has option to allow both. |
| target_edge += (threshold_adjustment * target_rect.Height()); |
| } else { // kHorizontalScroll |
| root_edge = is_start ? root_rect.X() : root_rect.Right(); |
| target_edge = target_rect.X(); |
| target_edge += (threshold_adjustment * target_rect.Width()); |
| } |
| |
| LayoutUnit offset = target_edge - root_edge; |
| return std::min(std::max(offset.ToDouble(), 0.0), max_offset); |
| } else { |
| // Resolve the default case (i.e., 'auto' value) |
| return default_offset; |
| } |
| } |
| |
| ScrollTimelineOffsetValue ScrollTimelineOffset::ToScrollTimelineOffsetValue() |
| const { |
| ScrollTimelineOffsetValue result; |
| if (length_based_offset_) { |
| result.SetCSSNumericValue( |
| CSSNumericValue::FromCSSValue(*length_based_offset_.Get())); |
| } else if (element_based_offset_) { |
| result.SetScrollTimelineElementBasedOffset(element_based_offset_); |
| } else { |
| // This is the default value (i.e., 'auto' value) |
| result.SetCSSKeywordValue(CSSKeywordValue::Create("auto")); |
| } |
| |
| return result; |
| } |
| |
| bool ScrollTimelineOffset::operator==(const ScrollTimelineOffset& o) const { |
| return DataEquivalent(length_based_offset_, o.length_based_offset_) && |
| ElementBasedOffsetsEqual(element_based_offset_, |
| o.element_based_offset_); |
| } |
| |
| ScrollTimelineOffset::ScrollTimelineOffset(const CSSPrimitiveValue* offset) |
| : length_based_offset_(offset), element_based_offset_(nullptr) {} |
| |
| ScrollTimelineOffset::ScrollTimelineOffset( |
| ScrollTimelineElementBasedOffset* offset) |
| : length_based_offset_(nullptr), element_based_offset_(offset) {} |
| |
| void ScrollTimelineOffset::Trace(blink::Visitor* visitor) const { |
| visitor->Trace(length_based_offset_); |
| visitor->Trace(element_based_offset_); |
| } |
| |
| } // namespace blink |