blob: acc3d5d82626f91a16ec72a9d57fdea93b092b42 [file] [log] [blame]
// 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