910e62b5创建于 1月15日历史提交
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/renderer/actor/scroll_tool.h"

#include <optional>

#include "base/feature_list.h"
#include "base/notimplemented.h"
#include "base/strings/to_string.h"
#include "chrome/common/actor/action_result.h"
#include "chrome/common/actor/actor_constants.h"
#include "chrome/common/actor/actor_logging.h"
#include "chrome/common/actor/journal_details_builder.h"
#include "chrome/common/chrome_features.h"
#include "chrome/renderer/actor/tool_utils.h"
#include "content/public/renderer/render_frame.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_frame_widget.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_node.h"
#include "ui/display/screen_info.h"
#include "ui/events/base_event_utils.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/vector2d_f.h"

namespace actor {

using ::blink::WebElement;
using ::blink::WebLocalFrame;
using ::blink::WebNode;

bool IsTargetUserScrollable(const blink::WebNode& node,
                            const gfx::Vector2dF& offset) {
  if (!node.IsElementNode()) {
    return false;
  }

  WebElement element = node.To<WebElement>();
  return (!offset.x() || element.IsUserScrollableX()) &&
         (!offset.y() || element.IsUserScrollableY());
}

ScrollTool::ScrollTool(content::RenderFrame& frame,
                       TaskId task_id,
                       Journal& journal,
                       mojom::ScrollActionPtr action,
                       mojom::ToolTargetPtr target,
                       mojom::ObservedToolTargetPtr observed_target)
    : ToolBase(frame,
               task_id,
               journal,
               std::move(target),
               std::move(observed_target)),
      action_(std::move(action)) {}

ScrollTool::~ScrollTool() = default;

void ScrollTool::Execute(ToolFinishedCallback callback) {
  ValidatedResult validated_result = Validate();
  if (!validated_result.has_value()) {
    std::move(callback).Run(std::move(validated_result.error()));
    return;
  }

  WebElement scrolling_element = validated_result->scroller;
  gfx::Vector2dF offset_physical = validated_result->scroll_by_offset;

  float physical_to_css = 1 / scrolling_element.GetEffectiveZoom();
  gfx::Vector2dF offset_css =
      gfx::ScaleVector2d(offset_physical, physical_to_css, physical_to_css);

  gfx::Vector2dF start_offset_css = scrolling_element.GetScrollOffset();
  bool did_scroll =
      scrolling_element.SetScrollOffset(start_offset_css + offset_css);

  journal_->Log(task_id_, "ScrollTool::Execute",
                JournalDetailsBuilder()
                    .Add("element", scrolling_element)
                    .Add("start_offset", start_offset_css)
                    .Add("offset", offset_css)
                    .Build());

  std::move(callback).Run(
      did_scroll
          ? MakeOkResult()
          : MakeResult(mojom::ActionResultCode::kScrollOffsetDidNotChange));
}

std::string ScrollTool::DebugString() const {
  return absl::StrFormat("ScrollTool[%s;direction(%s);distance(%f)]",
                         ToDebugString(target_),
                         base::ToString(action_->direction), action_->distance);
}

ScrollTool::ValidatedResult ScrollTool::Validate() const {
  WebLocalFrame* web_frame = frame_->GetWebFrame();
  CHECK(web_frame);
  CHECK(web_frame->FrameWidget());

  // The scroll distance should always be positive.
  if (action_->distance <= 0.0) {
    return base::unexpected(
        MakeResult(mojom::ActionResultCode::kArgumentsInvalid,
                   /*requires_page_stabilization=*/false, "Negative Distance"));
  }

  gfx::Vector2dF offset_dips;
  switch (action_->direction) {
    case mojom::ScrollAction::ScrollDirection::kLeft: {
      offset_dips.set_x(-action_->distance);
      break;
    }
    case mojom::ScrollAction::ScrollDirection::kRight: {
      offset_dips.set_x(action_->distance);
      break;
    }
    case mojom::ScrollAction::ScrollDirection::kUp: {
      offset_dips.set_y(-action_->distance);
      break;
    }
    case mojom::ScrollAction::ScrollDirection::kDown: {
      offset_dips.set_y(action_->distance);
      break;
    }
  }

  // No matter what frame the scroll element is on, it must be on the same
  // physical screen, so the device scale factor will be the same.
  const float dsf =
      web_frame->FrameWidget()->GetScreenInfo().device_scale_factor;
  gfx::Vector2dF offset_physical = gfx::ScaleVector2d(offset_dips, dsf);

  WebElement scrolling_element;
  if (target_->is_coordinate_dip()) {
    // We don't do a TimeOfUse check for coordinate based targets,
    // because the intended target is determined by bubbling up to
    // ancestors and only the `observed_target_` node is provided from
    // the browser.
    ToolBase::ResolveResult resolved_target = ResolveTarget(*target_);
    if (!resolved_target.has_value()) {
      return base::unexpected(std::move(resolved_target.error()));
    }

    blink::WebNode& node = resolved_target->node;
    WebElement root_scroller = web_frame->GetDocument().ScrollingElement();

    while (!node.IsNull()) {
      // If we reach the root scroller, we check if it is a scrollable target.
      if (node.IsDocumentNode() && !root_scroller.IsNull() &&
          IsTargetUserScrollable(root_scroller, offset_physical)) {
        node = root_scroller;
        break;
      }

      if (IsTargetUserScrollable(node, offset_physical)) {
        break;
      }

      node = node.ParentOrShadowHostNode();
    }

    if (node.IsNull()) {
      return base::unexpected(
          MakeResult(mojom::ActionResultCode::kScrollTargetNotUserScrollable,
                     /*requires_page_stabilization=*/false,
                     "No scrollable ancestor found by coordinate"));
    }

    scrolling_element = node.To<WebElement>();

    // At this point, `scrolling_element` is guaranteed to be a scrollable
    // element. Otherwise, we would have returned the error above.
    CHECK(IsTargetUserScrollable(scrolling_element, offset_physical));
  } else {
    int32_t dom_node_id = target_->get_dom_node_id();
    if (dom_node_id == kRootElementDomNodeId) {
      scrolling_element = web_frame->GetDocument().ScrollingElement();
      if (scrolling_element.IsNull()) {
        return base::unexpected(
            MakeResult(mojom::ActionResultCode::kScrollNoScrollingElement));
      }
    } else {
      ToolBase::ResolveResult resolved_target = ValidateAndResolveTarget();
      if (!resolved_target.has_value()) {
        return base::unexpected(std::move(resolved_target.error()));
      }
      scrolling_element = resolved_target->node.DynamicTo<WebElement>();
    }

    if (!IsTargetUserScrollable(scrolling_element, offset_physical)) {
      return base::unexpected(
          MakeResult(mojom::ActionResultCode::kScrollTargetNotUserScrollable,
                     /*requires_page_stabilization=*/false,
                     absl::StrFormat("ScrollingElement [%s]",
                                     base::ToString(scrolling_element))));
    }
  }

  return ScrollerAndDistance{scrolling_element, offset_physical};
}

}  // namespace actor