| // Copyright 2022 The Chromium Authors |
| // 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/annotation/annotation_agent_impl.h" |
| |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/notreached.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/trace_event/typed_macros.h" |
| #include "cc/base/features.h" |
| #include "third_party/blink/public/mojom/annotation/annotation.mojom-blink-forward.h" |
| #include "third_party/blink/public/mojom/input/focus_type.mojom-blink-forward.h" |
| #include "third_party/blink/public/mojom/scroll/scroll_into_view_params.mojom-blink.h" |
| #include "third_party/blink/renderer/core/annotation/annotation_agent_container_impl.h" |
| #include "third_party/blink/renderer/core/annotation/annotation_selector.h" |
| #include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/element.h" |
| #include "third_party/blink/renderer/core/dom/focus_params.h" |
| #include "third_party/blink/renderer/core/dom/node.h" |
| #include "third_party/blink/renderer/core/editing/ephemeral_range.h" |
| #include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h" |
| #include "third_party/blink/renderer/core/editing/markers/text_fragment_marker.h" |
| #include "third_party/blink/renderer/core/editing/position_with_affinity.h" |
| #include "third_party/blink/renderer/core/editing/range_in_flat_tree.h" |
| #include "third_party/blink/renderer/core/editing/visible_units.h" |
| #include "third_party/blink/renderer/core/execution_context/execution_context.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/settings.h" |
| #include "third_party/blink/renderer/core/frame/web_feature.h" |
| #include "third_party/blink/renderer/core/highlight/highlight_style_utils.h" |
| #include "third_party/blink/renderer/core/html/html_details_element.h" |
| #include "third_party/blink/renderer/core/layout/geometry/box_strut.h" |
| #include "third_party/blink/renderer/core/layout/geometry/physical_rect.h" |
| #include "third_party/blink/renderer/core/layout/hit_test_result.h" |
| #include "third_party/blink/renderer/core/layout/layout_object.h" |
| #include "third_party/blink/renderer/core/layout/layout_text.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/page/chrome_client.h" |
| #include "third_party/blink/renderer/core/scroll/scroll_alignment.h" |
| #include "third_party/blink/renderer/core/scroll/scroll_into_view_util.h" |
| #include "third_party/blink/renderer/core/scroll/scrollable_area.h" |
| #include "third_party/blink/renderer/platform/heap/persistent.h" |
| #include "third_party/blink/renderer/platform/heap/visitor.h" |
| #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" |
| #include "third_party/blink/renderer/platform/wtf/casting.h" |
| #include "ui/display/screen_info.h" |
| #include "ui/gfx/geometry/vector2d_f.h" |
| |
| namespace blink { |
| |
| namespace { |
| bool IsValidRange(const RangeInFlatTree* range) { |
| // An attached range may have !IsCollapsed but converting to EphemeralRange |
| // results in IsCollapsed. For an example, see |
| // AnnotationAgentImplTest.ScrollIntoViewCollapsedRange. |
| bool is_valid = range && range->IsConnected() && !range->IsCollapsed() && |
| !range->ToEphemeralRange().IsCollapsed(); |
| |
| if (is_valid) { |
| // TODO(crbug.com/410033683): Temporary to work around a crash. |
| // DocumentMarkers work on EphemeralRange (i.e. not FlatTree) so when we try |
| // to add a marker in ProcessAttachmentFinished, a well-ordered range in a |
| // flat tree may become invalid due to slotted elements. DocumentMarkers |
| // should maybe work on FlatTree types but for now just invalidate this |
| // case. |
| Position start = ToPositionInDomTree(range->StartPosition()); |
| Position end = ToPositionInDomTree(range->EndPosition()); |
| if (start > end) { |
| return false; |
| } |
| } |
| |
| return is_valid; |
| } |
| |
| // There are several cases where text isn't visible/presented to the user but |
| // does appear findable to FindBuffer. Some use cases (see |
| // `ShouldUseIsValidRangeAndMarkable` below) want to prevent offering scrolls to |
| // these sections as its confusing (in fact, document Markers will avoid |
| // creating a highlight for these, despite the fact we can scroll to it). We |
| // probably want to do this for general SharedHighlights as well but that will |
| // require some more thought and spec changes but we can experiment with this to |
| // see how it works. |
| bool IsValidRangeAndMarkable(const RangeInFlatTree* range) { |
| if (!IsValidRange(range)) { |
| return false; |
| } |
| |
| EphemeralRangeInFlatTree ephemeral_range = range->ToEphemeralRange(); |
| |
| // Technically, the text could span multiple Elements, each of which could |
| // hide overflow. However, that doesn't seem to be common so do the more |
| // performant thing and check the common ancestor. |
| Node* common_node = ephemeral_range.CommonAncestorContainer(); |
| |
| LayoutObject* object = common_node->GetLayoutObject(); |
| while (!object) { |
| common_node = FlatTreeTraversal::Parent(*common_node); |
| // We should exit this loop before this is false (i.e. there must be at |
| // least one ancestor node with a LayoutObject). |
| CHECK(common_node); |
| object = common_node->GetLayoutObject(); |
| } |
| |
| PhysicalRect absolute_bounding_box = |
| object->AbsoluteBoundingBoxRectForScrollIntoView(); |
| |
| if (LocalFrameView* view = ephemeral_range.GetDocument().View()) { |
| PhysicalRect bounding_box_in_document = |
| view->FrameToDocument(absolute_bounding_box); |
| // If the box is positioned out of the document bounds (eg: something like |
| // position: absolute; top: -9999em), it cannot be highlighted. |
| if (bounding_box_in_document.Bottom() <= 0 || |
| bounding_box_in_document.Right() <= 0) { |
| return false; |
| } |
| } |
| |
| for (; !object->IsLayoutView(); object = object->Parent()) { |
| LayoutBox* box = DynamicTo<LayoutBox>(object); |
| if (!box) { |
| continue; |
| } |
| |
| // It's common for collapsible sections to be implemented by hiding |
| // collapsed text within a `height:0; overflow: hidden` box. However, |
| // FindBuffer does find this text (as typically overflow: hidden can still |
| // be programmatically scrolled). |
| if (box->HasNonVisibleOverflow()) { |
| PhysicalSize size = box->StitchedSize(); |
| if (box->StyleRef().OverflowX() != EOverflow::kVisible && |
| size.width.RawValue() <= 0) { |
| return false; |
| } |
| |
| if (box->StyleRef().OverflowY() != EOverflow::kVisible && |
| size.height.RawValue() <= 0) { |
| return false; |
| } |
| } |
| |
| // If an ancestor is set to opacity 0, consider the target invisible. |
| if (box->StyleRef().Opacity() == 0) { |
| return false; |
| } |
| |
| // If the range is in a fixed subtree, scrolling the view won't change its |
| // viewport-relative location so report the range as unfindable if its |
| // currently offscreen. |
| if (box->StyleRef().GetPosition() == EPosition::kFixed) { |
| PhysicalRect view_rect = |
| PhysicalRect::EnclosingRect(box->View()->AbsoluteBoundingBoxRectF()); |
| if (!view_rect.Intersects(absolute_bounding_box)) { |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| bool ShouldUseIsValidRangeAndMarkable(mojom::blink::AnnotationType type) { |
| switch (type) { |
| case mojom::blink::AnnotationType::kTextFinder: |
| case mojom::blink::AnnotationType::kGlic: |
| return true; |
| case mojom::blink::AnnotationType::kSharedHighlight: |
| case mojom::blink::AnnotationType::kScrollOnly: |
| return false; |
| } |
| } |
| |
| // The maximum scroll distance for which an AnnotationAgent of type kGlic should |
| // use a smooth (animated) scroll. For longer distances, the scroll will be |
| // instant. |
| int GetGlicSmoothScrollThresholdInDIPs() { |
| const base::FeatureParam<int> glic_smooth_scroll_threshold_in_dips{ |
| &features::kProgrammaticScrollAnimationOverride, |
| "glic_smooth_scroll_threshold_in_dips", 15000}; |
| return glic_smooth_scroll_threshold_in_dips.Get(); |
| } |
| |
| std::optional<DocumentMarker::MarkerTypes> GetMarkerTypesForAnnotationType( |
| mojom::blink::AnnotationType annotation_type) { |
| switch (annotation_type) { |
| case mojom::blink::AnnotationType::kSharedHighlight: |
| return DocumentMarker::MarkerTypes::TextFragment(); |
| case mojom::blink::AnnotationType::kGlic: |
| return DocumentMarker::MarkerTypes::Glic(); |
| case mojom::blink::AnnotationType::kTextFinder: |
| case mojom::blink::AnnotationType::kScrollOnly: |
| return std::nullopt; |
| } |
| } |
| |
| bool HasMarkerAroundPosition(const HitTestResult& result, |
| DocumentMarker::MarkerType marker_type) { |
| // Tree should be clean before accessing the position. |
| // |HitTestResult::GetPosition| calls |PositionForPoint()| which requires |
| // |kPrePaintClean|. |
| DCHECK_GE(result.InnerNodeFrame()->GetDocument()->Lifecycle().GetState(), |
| DocumentLifecycle::kPrePaintClean); |
| |
| DocumentMarkerController& marker_controller = |
| result.InnerNodeFrame()->GetDocument()->Markers(); |
| PositionWithAffinity pos_with_affinity = result.GetPosition(); |
| const Position marker_position = pos_with_affinity.GetPosition(); |
| |
| auto markers = marker_controller.MarkersAroundPosition( |
| ToPositionInFlatTree(marker_position), |
| DocumentMarker::MarkerTypes(marker_type)); |
| return !markers.empty(); |
| } |
| |
| float CalculateMaxScrollOffsetPx( |
| LocalFrameView* view, |
| const PhysicalRect& bounding_box, |
| const mojom::blink::ScrollIntoViewParams& params) { |
| CHECK(view); |
| CHECK(view->GetScrollableArea()); |
| ScrollOffset scroll_offset_px = |
| scroll_into_view_util::GetScrollOffsetToExpose( |
| *view->GetScrollableArea(), bounding_box, PhysicalBoxStrut(), |
| *params.align_x, *params.align_y); |
| // Removes any potential negative offset from the |
| // `ScrollAlignment::CenterAlways()`. |
| scroll_offset_px = |
| view->GetScrollableArea()->ClampScrollOffset(scroll_offset_px); |
| ScrollOffset scroll_distance_px = |
| scroll_offset_px - view->GetScrollableArea()->GetScrollOffset(); |
| |
| return std::max(std::abs(scroll_distance_px.x()), |
| std::abs(scroll_distance_px.y())); |
| } |
| |
| } // namespace |
| |
| AnnotationAgentImpl::AnnotationAgentImpl( |
| AnnotationAgentContainerImpl& owning_container, |
| mojom::blink::AnnotationType annotation_type, |
| AnnotationSelector& selector, |
| std::optional<DOMNodeId> search_range_start_node_id, |
| AnnotationAgentContainerImpl::PassKey) |
| : agent_host_(owning_container.GetSupplementable()->GetExecutionContext()), |
| receiver_(this, |
| owning_container.GetSupplementable()->GetExecutionContext()), |
| owning_container_(&owning_container), |
| selector_(&selector), |
| search_range_start_node_id_(search_range_start_node_id), |
| type_(annotation_type) { |
| DCHECK(!IsAttached()); |
| DCHECK(!IsRemoved()); |
| } |
| |
| void AnnotationAgentImpl::Trace(Visitor* visitor) const { |
| visitor->Trace(agent_host_); |
| visitor->Trace(receiver_); |
| visitor->Trace(owning_container_); |
| visitor->Trace(selector_); |
| visitor->Trace(attached_range_); |
| visitor->Trace(pending_range_); |
| } |
| |
| void AnnotationAgentImpl::Bind( |
| mojo::PendingRemote<mojom::blink::AnnotationAgentHost> host_remote, |
| mojo::PendingReceiver<mojom::blink::AnnotationAgent> agent_receiver) { |
| DCHECK(!IsRemoved()); |
| |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner = |
| owning_container_->GetSupplementable()->GetTaskRunner( |
| TaskType::kInternalDefault); |
| |
| agent_host_.Bind(std::move(host_remote), task_runner); |
| receiver_.Bind(std::move(agent_receiver), task_runner); |
| |
| // Breaking the mojo connection will cause this agent to remove itself from |
| // the container. |
| receiver_.set_disconnect_handler(BindOnce( |
| [](WeakPersistent<AnnotationAgentImpl> agent) { |
| if (!agent || !agent->OwningContainer()) { |
| return; |
| } |
| agent->OwningContainer()->RemoveAgent(*agent); |
| }, |
| WrapWeakPersistent(this))); |
| } |
| |
| void AnnotationAgentImpl::Attach(AnnotationAgentContainerImpl::PassKey) { |
| TRACE_EVENT("blink", "AnnotationAgentImpl::Attach"); |
| CHECK(!IsRemoved()); |
| CHECK(!IsAttached()); |
| CHECK(!pending_range_); |
| CHECK(owning_container_->IsLifecycleCleanForAttachment()); |
| |
| // We may still have an old range despite the CHECK above if the range become |
| // collapsed due to DOM changes. |
| attached_range_.Clear(); |
| |
| // Create the search range within which the agent will attempt to match the |
| // selector in. |
| Document* document = owning_container_->GetSupplementable(); |
| Range* search_range = document->createRange(); |
| if (document->body()) { |
| search_range->selectNode(document->body()); |
| } |
| |
| if (search_range_start_node_id_.has_value()) { |
| Node* search_range_start_node = |
| Node::FromDomNodeId(search_range_start_node_id_.value()); |
| // Check if the search range start node is null, which is not in the |
| // document. |
| if (!search_range_start_node || search_range->collapsed()) { |
| needs_attachment_ = false; |
| agent_host_->DidFinishAttachment( |
| gfx::Rect(), mojom::blink::AttachmentResult::kRangeInvalid); |
| return; |
| } |
| search_range->setStart(search_range_start_node, 0); |
| } |
| |
| needs_attachment_ = false; |
| selector_->FindRange(*search_range, AnnotationSelector::kSynchronous, |
| BindOnce(&AnnotationAgentImpl::DidFinishFindRange, |
| WrapWeakPersistent(this))); |
| } |
| |
| bool AnnotationAgentImpl::IsAttached() const { |
| return IsValidRange(attached_range_); |
| } |
| |
| bool AnnotationAgentImpl::IsAttachmentPending() const { |
| // This can be an invalid range but still returns true because the attachment |
| // is still in progress until the DomMutation task runs in the next rAF. |
| return pending_range_ != nullptr; |
| } |
| |
| bool AnnotationAgentImpl::IsBoundForTesting() const { |
| DCHECK_EQ(agent_host_.is_bound(), receiver_.is_bound()); |
| return receiver_.is_bound(); |
| } |
| |
| void AnnotationAgentImpl::Reset(base::PassKey<AnnotationAgentContainerImpl>) { |
| DCHECK(!IsRemoved()); |
| |
| if (IsAttached()) { |
| EphemeralRange dom_range = |
| EphemeralRange(ToPositionInDomTree(attached_range_->StartPosition()), |
| ToPositionInDomTree(attached_range_->EndPosition())); |
| Document* document = attached_range_->StartPosition().GetDocument(); |
| DCHECK(document); |
| |
| if (LocalFrame* frame = document->GetFrame()) { |
| // Markers require that layout is up to date if we're making any |
| // modifications. |
| frame->GetDocument()->UpdateStyleAndLayout( |
| DocumentUpdateReason::kFindInPage); |
| |
| std::optional<DocumentMarker::MarkerTypes> marker_types = |
| GetMarkerTypesForAnnotationType(type_); |
| if (marker_types.has_value()) { |
| document->Markers().RemoveMarkersInRange(dom_range, *marker_types); |
| } |
| } |
| } |
| |
| attached_range_.Clear(); |
| pending_range_.Clear(); |
| |
| agent_host_.reset(); |
| receiver_.reset(); |
| |
| selector_.Clear(); |
| owning_container_.Clear(); |
| } |
| |
| void AnnotationAgentImpl::ScrollIntoView(bool applies_focus) const { |
| DCHECK(!IsRemoved()); |
| |
| if (!IsAttached()) { |
| return; |
| } |
| |
| EphemeralRangeInFlatTree range = attached_range_->ToEphemeralRange(); |
| CHECK(range.Nodes().begin() != range.Nodes().end()); |
| Node& first_node = *range.Nodes().begin(); |
| |
| Document& document = *owning_container_->GetSupplementable(); |
| document.EnsurePaintLocationDataValidForNode( |
| &first_node, DocumentUpdateReason::kFindInPage); |
| |
| Node* first_node_with_layout_object = nullptr; |
| for (Node& node : range.Nodes()) { |
| if (node.GetLayoutObject()) { |
| first_node_with_layout_object = &node; |
| } |
| } |
| |
| // TODO(bokan): Text can be attached without having a LayoutObject since it |
| // may be inside an unexpanded <details> element or inside a |
| // `content-visibility: auto` subtree. In those cases we should make sure we |
| // expand/make-visible the node. This is implemented in TextFragmentAnchor |
| // but that doesn't cover all cases we can get here so we should migrate that |
| // code here. |
| if (!first_node_with_layout_object) { |
| return; |
| } |
| |
| // Set the bounding box height to zero because we want to center the top of |
| // the text range. |
| PhysicalRect bounding_box(ComputeTextRect(range)); |
| bounding_box.SetHeight(LayoutUnit()); |
| |
| mojom::blink::ScrollIntoViewParamsPtr params = |
| scroll_into_view_util::CreateScrollIntoViewParams( |
| ScrollAlignment::CenterAlways(), ScrollAlignment::CenterAlways(), |
| mojom::blink::ScrollType::kProgrammatic); |
| params->cross_origin_boundaries = false; |
| params->behavior = ComputeScrollIntoViewBehavior(bounding_box, *params); |
| |
| if (applies_focus) { |
| // If the first node accepts keyboard focus, move focus there to aid users |
| // relying on keyboard navigation. If the node is not focusable, clear focus |
| // so the next "Tab" press will start the search to find the next focusable |
| // element from this element. |
| auto* element = first_node_with_layout_object->parentElement(); |
| if (element && element->IsFocusable()) { |
| document.SetFocusedElement( |
| element, FocusParams(SelectionBehaviorOnFocus::kNone, |
| mojom::blink::FocusType::kNone, nullptr)); |
| } else { |
| document.ClearFocusedElement(); |
| } |
| } |
| |
| // Set the sequential focus navigation to the start of selection. |
| // Even if this element isn't focusable, "Tab" press will |
| // start the search to find the next focusable element from this element. |
| document.SetSequentialFocusNavigationStartingPoint( |
| first_node_with_layout_object); |
| |
| if (type_ == mojom::blink::AnnotationType::kGlic) { |
| float max_distance_px = CalculateMaxScrollOffsetPx( |
| first_node_with_layout_object->GetLayoutObject()->GetFrameView(), |
| bounding_box, *params); |
| if (max_distance_px <= 1.f) { |
| document.Markers().StartGlicMarkerAnimationIfNeeded(); |
| } else { |
| // Scroll is guaranteed to happen. `ScrollableArea::OnScrollFinished()` |
| // will call `StartGlicMarkerAnimation()`. This is a near-term solution |
| // due to the re-arch work in crbug.com/41406914. It means in the nested |
| // multiple scollers case, the first ever `OnScrollFinished()` starts the |
| // animation, regardless if the actual scroll has finished or not. |
| // |
| // TODO(https://crbug.com/41406914): Migrate from `OnScrollFinished()` to |
| // the scroll-promises. |
| } |
| } |
| |
| scroll_into_view_util::ScrollRectToVisible( |
| *first_node_with_layout_object->GetLayoutObject(), bounding_box, |
| std::move(params)); |
| } |
| |
| std::optional<mojom::blink::AnnotationType> |
| AnnotationAgentImpl::IsOverAnnotation(const HitTestResult& result) { |
| if (!result.InnerNode() || !result.InnerNodeFrame()) { |
| return std::nullopt; |
| } |
| |
| if (HasMarkerAroundPosition(result, DocumentMarker::MarkerType::kGlic)) { |
| // Note: We could also have a marker of type kTextFragment around the |
| // position as well, but we treat kGlic as topmost. |
| return mojom::blink::AnnotationType::kGlic; |
| } |
| |
| if (HasMarkerAroundPosition(result, |
| DocumentMarker::MarkerType::kTextFragment)) { |
| return mojom::blink::AnnotationType::kSharedHighlight; |
| } |
| |
| return std::nullopt; |
| } |
| |
| void AnnotationAgentImpl::DidFinishFindRange(const RangeInFlatTree* range) { |
| TRACE_EVENT("blink", "AnnotationAgentImpl::DidFinishFindRange", |
| "bound_to_host", agent_host_.is_bound()); |
| if (IsRemoved()) { |
| TRACE_EVENT_INSTANT("blink", "Removed"); |
| return; |
| } |
| |
| pending_range_ = range; |
| |
| // In some cases, attaching to text can lead to DOM mutation. For example, |
| // expanding <details> elements or unhiding an hidden=until-found element. |
| // That needs to be done before processing the attachment (i.e. adding a |
| // highlight). However, DOM/layout may not be safe to do here so we'll post a |
| // task in that case. However, if we don't need to perform those actions we |
| // can avoid the extra post and just process the attachment now. |
| if (!NeedsDOMMutationToAttach()) { |
| ProcessAttachmentFinished(); |
| } else { |
| // TODO(bokan): We may need to force an animation frame e.g. if we're in a |
| // throttled iframe. |
| Document& document = *owning_container_->GetSupplementable(); |
| document.EnqueueAnimationFrameTask( |
| BindOnce(&AnnotationAgentImpl::PerformPreAttachDOMMutation, |
| WrapPersistent(this))); |
| } |
| } |
| |
| bool AnnotationAgentImpl::NeedsDOMMutationToAttach() const { |
| if (!IsValidRange(pending_range_)) { |
| return false; |
| } |
| |
| // TextFinder type is used only to determine whether a given text can be |
| // found in the page, it should have no side-effects. |
| if (type_ == mojom::blink::AnnotationType::kTextFinder) { |
| return false; |
| } |
| |
| EphemeralRangeInFlatTree range = pending_range_->ToEphemeralRange(); |
| |
| // TODO(crbug.com/1252872): Only |first_node| is considered in the range, but |
| // we should be considering the entire range of selected text for ancestor |
| // unlocking as well. |
| if (DisplayLockUtilities::NeedsActivationForFindInPage(range)) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void AnnotationAgentImpl::PerformPreAttachDOMMutation() { |
| if (IsValidRange(pending_range_)) { |
| // TODO(crbug.com/1252872): Only |first_node| is considered for the below |
| // ancestor expanding code, but we should be considering the entire range |
| // of selected text for ancestor unlocking as well. |
| Node& first_node = *pending_range_->ToEphemeralRange().Nodes().begin(); |
| |
| // Activate content-visibility:auto subtrees if needed. |
| DisplayLockUtilities::ActivateFindInPageMatchRangeIfNeeded( |
| pending_range_->ToEphemeralRange()); |
| |
| // If the active match is hidden inside a <details> element or a |
| // hidden=until-found element, then we should expand it so we can scroll to |
| // it. |
| if (DisplayLockUtilities::RevealAutoExpandableAncestors(first_node) |
| .revealed_details) { |
| UseCounter::Count( |
| first_node.GetDocument(), |
| WebFeature::kAutoExpandedDetailsForScrollToTextFragment); |
| } |
| |
| // Ensure we leave clean layout since we'll be applying markers after this. |
| first_node.GetDocument().UpdateStyleAndLayout( |
| DocumentUpdateReason::kFindInPage); |
| } |
| |
| ProcessAttachmentFinished(); |
| } |
| |
| void AnnotationAgentImpl::ProcessAttachmentFinished() { |
| CHECK(!attached_range_); |
| |
| // See IsValidRangeAndMarkable for why we treat some types differently here. |
| bool pending_range_valid = ShouldUseIsValidRangeAndMarkable(type_) |
| ? IsValidRangeAndMarkable(pending_range_) |
| : IsValidRange(pending_range_); |
| |
| if (pending_range_valid) { |
| attached_range_ = pending_range_; |
| |
| TRACE_EVENT_INSTANT("blink", "IsAttached"); |
| |
| EphemeralRange dom_range = |
| EphemeralRange(ToPositionInDomTree(attached_range_->StartPosition()), |
| ToPositionInDomTree(attached_range_->EndPosition())); |
| Document* document = attached_range_->StartPosition().GetDocument(); |
| DCHECK(document); |
| |
| switch (type_) { |
| case mojom::blink::AnnotationType::kSharedHighlight: { |
| document->Markers().AddTextFragmentMarker(dom_range); |
| document->Markers().MergeOverlappingMarkers( |
| DocumentMarker::kTextFragment); |
| break; |
| } |
| case mojom::blink::AnnotationType::kGlic: { |
| document->Markers().AddGlicMarker(dom_range); |
| break; |
| } |
| case mojom::blink::AnnotationType::kTextFinder: |
| case mojom::blink::AnnotationType::kScrollOnly: { |
| // TextFinder and ScrollOnly types are used only to determine whether a |
| // given text can be found in the page (and scrolled to), they should |
| // have no marker side-effects. |
| break; |
| } |
| } |
| |
| Node* anchor_node = attached_range_->StartPosition().AnchorNode(); |
| CHECK(anchor_node); |
| if (anchor_node->IsInShadowTree()) { |
| UseCounter::Count(document, WebFeature::kTextDirectiveInShadowDOM); |
| } |
| } else { |
| TRACE_EVENT_INSTANT("blink", "NotAttached"); |
| } |
| |
| pending_range_.Clear(); |
| |
| // If we're bound to one, let the host know we've finished attempting to |
| // attach. |
| // TODO(bokan): Perhaps we should keep track of whether we've called |
| // DidFinishFindRange and, if set, call the host method when binding. |
| if (agent_host_.is_bound()) { |
| gfx::Rect range_rect_in_document; |
| if (IsAttached()) { |
| gfx::Rect rect_in_frame = |
| ComputeTextRect(attached_range_->ToEphemeralRange()); |
| |
| Document* document = attached_range_->StartPosition().GetDocument(); |
| DCHECK(document); |
| |
| LocalFrameView* view = document->View(); |
| DCHECK(view); |
| |
| range_rect_in_document = view->FrameToDocument(rect_in_frame); |
| } |
| |
| mojom::blink::AttachmentResult attachment_result = |
| IsAttached() ? mojom::blink::AttachmentResult::kSuccess |
| : mojom::blink::AttachmentResult::kSelectorNotMatched; |
| agent_host_->DidFinishAttachment(range_rect_in_document, attachment_result); |
| } |
| } |
| |
| bool AnnotationAgentImpl::IsRemoved() const { |
| // selector_ and owning_container_ should only ever be null if the agent was |
| // removed. |
| DCHECK_EQ(!owning_container_, !selector_); |
| |
| // If the agent is removed, all its state should be cleared. |
| DCHECK(owning_container_ || !attached_range_); |
| DCHECK(owning_container_ || !agent_host_.is_bound()); |
| DCHECK(owning_container_ || !receiver_.is_bound()); |
| return !owning_container_; |
| } |
| |
| mojom::blink::ScrollBehavior AnnotationAgentImpl::ComputeScrollIntoViewBehavior( |
| const PhysicalRect& bounding_box, |
| const mojom::blink::ScrollIntoViewParams& params) const { |
| using mojom::blink::AnnotationType; |
| using mojom::blink::ScrollBehavior; |
| |
| CHECK(owning_container_->GetSupplementable()); |
| Document* document = owning_container_->GetSupplementable(); |
| if (document->GetSettings() && |
| document->GetSettings()->GetPrefersReducedMotion()) { |
| return ScrollBehavior::kInstant; |
| } |
| |
| switch (type_) { |
| case AnnotationType::kSharedHighlight: |
| case AnnotationType::kTextFinder: |
| return ScrollBehavior::kAuto; |
| case AnnotationType::kScrollOnly: |
| // Use kInstant to match the behavior of browser-initiated scroll |
| // restoration. |
| return ScrollBehavior::kInstant; |
| case AnnotationType::kGlic: |
| // Use kInstant for long scroll distances, kSmooth otherwise. |
| if (LocalFrameView* view = document->GetFrame()->View()) { |
| float max_distance_in_dips = |
| CalculateMaxScrollOffsetPx(view, bounding_box, params); |
| if (ChromeClient* client = view->GetChromeClient()) { |
| // Note: We explicitly don't use `LocalFrame::DevicePixelRatio` or |
| // `LocalFrame::LayoutZoomFactor` as both are affected by browser |
| // zoom, and we don't want to allow longer scrolls (in physical |
| // pixels) when content is zoomed. |
| const float device_scale_factor = |
| client->GetScreenInfo(view->GetFrame()).device_scale_factor; |
| max_distance_in_dips = max_distance_in_dips / device_scale_factor; |
| } |
| base::UmaHistogramCustomCounts("Glic.ScrollTo.ScrollDistance", |
| max_distance_in_dips, 1, 500000, 50); |
| if (max_distance_in_dips < GetGlicSmoothScrollThresholdInDIPs()) { |
| return ScrollBehavior::kSmooth; |
| } |
| } |
| return ScrollBehavior::kInstant; |
| } |
| } |
| |
| } // namespace blink |