blob: 796374e5db4d00a14bbf8697466de66043115db8 [file]
// 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 "pdf/pdf_caret.h"
#include <stdint.h>
#include <optional>
#include <vector>
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/flat_map.h"
#include "base/containers/span.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "pdf/accessibility_structs.h"
#include "pdf/page_character_index.h"
#include "pdf/page_orientation.h"
#include "pdf/pdf_caret_client.h"
#include "pdf/pdf_utils/text_util.h"
#include "pdf/region_data.h"
#include "third_party/blink/public/common/input/web_keyboard_event.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/vector2d.h"
namespace chrome_pdf {
namespace {
constexpr SkColor4f kCaretColor = SkColors::kBlack;
// `is_same_char` is true when the actual char's screen rect is being used,
// otherwise false if a different char's screen rect is. A different char's
// screen rect can be used if the actual char does not have a screen rect.
void TransformCaretScreenRectWithRotatedTextDirection(
gfx::Rect& screen_rect,
AccessibilityTextDirection rotated_direction,
bool is_same_char) {
CHECK_NE(rotated_direction, AccessibilityTextDirection::kNone);
// Apply an offset if the caret should be at the end of a char. For forward
// directions, this is necessary when using the previous char's screen rect.
// For backward directions, this is necessary when using the current char's
// screen rect.
const bool is_forward_direction =
rotated_direction == AccessibilityTextDirection::kLeftToRight ||
rotated_direction == AccessibilityTextDirection::kTopToBottom;
const bool needs_offset = is_forward_direction != is_same_char;
if (rotated_direction == AccessibilityTextDirection::kLeftToRight ||
rotated_direction == AccessibilityTextDirection::kRightToLeft) {
if (needs_offset) {
screen_rect.Offset(screen_rect.width(), 0);
}
screen_rect.set_width(PdfCaret::kCaretWidth);
} else {
if (needs_offset) {
screen_rect.Offset(0, screen_rect.height());
}
screen_rect.set_height(PdfCaret::kCaretWidth);
}
}
// Helper for `PdfCaret::GetLogicalKeyAfterTextDirection()` to return the
// converted key.
ui::KeyboardCode GetLogicalKey(int key,
ui::KeyboardCode left_key,
ui::KeyboardCode right_key,
ui::KeyboardCode up_key,
ui::KeyboardCode down_key) {
switch (key) {
case ui::KeyboardCode::VKEY_LEFT:
return left_key;
case ui::KeyboardCode::VKEY_RIGHT:
return right_key;
case ui::KeyboardCode::VKEY_UP:
return up_key;
case ui::KeyboardCode::VKEY_DOWN:
return down_key;
default:
NOTREACHED();
}
}
bool ContainsMoveByWordBoundaryModifier(int modifiers) {
#if BUILDFLAG(IS_MAC)
return !!(modifiers & blink::WebInputEvent::Modifiers::kAltKey);
#else
return !!(modifiers & blink::WebInputEvent::Modifiers::kControlKey);
#endif // BUILDFLAG(IS_MAC)
}
} // namespace
PdfCaret::PdfCaret(PdfCaretClient* client) : client_(client) {}
PdfCaret::~PdfCaret() = default;
void PdfCaret::SetEnabled(bool enabled) {
if (enabled_ == enabled) {
return;
}
enabled_ = enabled;
if (ShouldDrawCaret()) {
SetScreenRectForCurrentCaret();
}
RefreshDisplayState();
}
void PdfCaret::SetVisible(bool visible) {
if (is_visible_ == visible) {
return;
}
is_visible_ = visible;
if (ShouldDrawCaret()) {
SetScreenRectForCurrentCaret();
}
RefreshDisplayState();
}
void PdfCaret::SetBlinkInterval(base::TimeDelta interval) {
if (interval.is_negative() || blink_interval_ == interval) {
return;
}
blink_interval_ = interval;
RefreshDisplayState();
}
void PdfCaret::SetChar(const PageCharacterIndex& next_char) {
uint32_t char_count = client_->GetCharCount(next_char.page_index);
CHECK_LE(next_char.char_index, char_count);
index_ = next_char;
const gfx::Rect old_screen_rect = caret_screen_rect_;
SetScreenRectForCurrentCaret();
if (is_blink_visible_ && !old_screen_rect.IsEmpty() &&
old_screen_rect != caret_screen_rect_) {
client_->InvalidateRect(old_screen_rect);
}
}
void PdfCaret::SetCharAndDraw(const PageCharacterIndex& next_char) {
SetChar(next_char);
if (ShouldDrawCaret()) {
RefreshDisplayState();
}
}
bool PdfCaret::MaybeDrawCaret(const RegionData& region,
const gfx::Rect& dirty_in_screen) const {
if (!is_blink_visible_) {
return false;
}
gfx::Rect visible_caret =
gfx::IntersectRects(caret_screen_rect_, dirty_in_screen);
if (visible_caret.IsEmpty()) {
return false;
}
visible_caret.Offset(-dirty_in_screen.OffsetFromOrigin());
Draw(region, visible_caret);
return true;
}
void PdfCaret::OnGeometryChanged() {
if (!ShouldDrawCaret()) {
return;
}
SetScreenRectForCurrentCaret();
if (!caret_screen_rect_.IsEmpty()) {
client_->InvalidateRect(caret_screen_rect_);
}
}
bool PdfCaret::WillHandleKeyDownEvent(const blink::WebKeyboardEvent& event) {
// The caret is not visible during text selection, so key events should still
// be handled when not visible.
return enabled_ && (event.windows_key_code == ui::KeyboardCode::VKEY_LEFT ||
event.windows_key_code == ui::KeyboardCode::VKEY_RIGHT ||
event.windows_key_code == ui::KeyboardCode::VKEY_UP ||
event.windows_key_code == ui::KeyboardCode::VKEY_DOWN);
}
bool PdfCaret::OnKeyDown(const blink::WebKeyboardEvent& event) {
if (!WillHandleKeyDownEvent(event)) {
return false;
}
ui::KeyboardCode key = GetLogicalKeyAfterTextDirection(
static_cast<ui::KeyboardCode>(event.windows_key_code));
bool should_select =
!!(event.GetModifiers() & blink::WebInputEvent::Modifiers::kShiftKey);
bool move_right = key == ui::KeyboardCode::VKEY_RIGHT;
if (move_right || key == ui::KeyboardCode::VKEY_LEFT) {
if (ContainsMoveByWordBoundaryModifier(event.GetModifiers())) {
MoveHorizontallyToNextWordBoundary(move_right, should_select);
} else {
MoveHorizontallyToNextChar(move_right, should_select);
}
} else {
bool move_down = key == ui::KeyboardCode::VKEY_DOWN;
CHECK(move_down || key == ui::KeyboardCode::VKEY_UP);
MoveVerticallyToNextChar(move_down, should_select);
}
return true;
}
bool PdfCaret::ShouldDrawCaret() const {
return enabled_ && is_visible_;
}
void PdfCaret::RefreshDisplayState() {
blink_timer_.Stop();
is_blink_visible_ = ShouldDrawCaret();
if (is_blink_visible_ && blink_interval_.is_positive()) {
blink_timer_.Start(FROM_HERE, blink_interval_, this,
&PdfCaret::OnBlinkTimerFired);
}
if (!caret_screen_rect_.IsEmpty()) {
client_->InvalidateRect(caret_screen_rect_);
}
}
void PdfCaret::OnBlinkTimerFired() {
CHECK(ShouldDrawCaret());
CHECK(blink_interval_.is_positive());
is_blink_visible_ = !is_blink_visible_;
if (!caret_screen_rect_.IsEmpty()) {
client_->InvalidateRect(caret_screen_rect_);
}
}
void PdfCaret::SetScreenRectForCurrentCaret() {
CaretScreenRectData data = GetScreenRectForCaret(index_);
caret_screen_rect_ = data.screen_rect;
cached_screen_rect_index_ = data.actual_index;
}
PdfCaret::CaretScreenRectData PdfCaret::GetScreenRectForCaret(
const PageCharacterIndex& index) const {
gfx::Rect screen_rect;
PageCharacterIndex curr_index = index;
do {
screen_rect = GetScreenRectForChar(curr_index);
if (!screen_rect.IsEmpty()) {
break;
}
// Failed to find a screen rect at the start of the page.
if (curr_index.char_index == 0) {
return {screen_rect, curr_index};
}
// Synthetic whitespaces and newlines generated by PDFium may not have a
// screen rect. Find the nearest previous char that has a screen rect.
--curr_index.char_index;
} while (true);
CHECK(!screen_rect.IsEmpty());
TransformCaretScreenRectWithRotatedTextDirection(
screen_rect, GetTextDirectionAfterRotationAt(curr_index),
/*is_same_char=*/index.char_index == curr_index.char_index);
return {screen_rect, curr_index};
}
gfx::Rect PdfCaret::GetScreenRectForChar(
const PageCharacterIndex& index) const {
uint32_t char_count = client_->GetCharCount(index.page_index);
CHECK_LE(index.char_index, char_count);
if (char_count > 0 && index.char_index == char_count) {
return gfx::Rect();
}
const std::vector<gfx::Rect> screen_rects =
client_->GetScreenRectsForCaret(index);
return !screen_rects.empty() ? screen_rects[0] : gfx::Rect();
}
AccessibilityTextDirection PdfCaret::GetTextDirectionAt(
const PageCharacterIndex& index) const {
std::optional<AccessibilityTextRunInfo> text_run =
client_->GetTextRunInfoAt(index);
auto direction = AccessibilityTextDirection::kLeftToRight;
if (text_run.has_value()) {
direction = text_run.value().direction;
}
// Default to LTR.
return direction == AccessibilityTextDirection::kNone
? AccessibilityTextDirection::kLeftToRight
: direction;
}
AccessibilityTextDirection PdfCaret::GetTextDirectionAfterRotationAt(
const PageCharacterIndex& index) const {
AccessibilityTextDirection direction = GetTextDirectionAt(index);
int rotation_steps =
GetClockwiseRotationSteps(client_->GetCurrentOrientation());
if (rotation_steps == 0) {
return direction;
}
// Order of text directions when rotating clockwise.
static constexpr std::array<AccessibilityTextDirection, 4> rotation_cycle = {
AccessibilityTextDirection::kLeftToRight,
AccessibilityTextDirection::kTopToBottom,
AccessibilityTextDirection::kRightToLeft,
AccessibilityTextDirection::kBottomToTop};
// Find the position of the current direction in the cycle.
auto it = std::ranges::find(rotation_cycle, direction);
CHECK(it != rotation_cycle.end());
size_t current_index = std::distance(rotation_cycle.begin(), it);
// Calculate the new direction after rotation.
size_t new_index = (current_index + rotation_steps) % rotation_cycle.size();
return rotation_cycle[new_index];
}
void PdfCaret::Draw(const RegionData& region, const gfx::Rect& rect) const {
gfx::Rect addressable_rect = rect;
// See crbug.com/483907177. `rect` is occasionally seen to be outside
// `region`, so clip it. Taking `row_pixels == region.stride/4` works because
// the bitmap format is always some form of BGRx.
addressable_rect.Intersect(
gfx::Rect(region.stride / 4, region.buffer.size() / region.stride));
int l = addressable_rect.x();
int t = addressable_rect.y();
int w = addressable_rect.width();
int h = addressable_rect.height();
for (int y = t; y < t + h; ++y) {
base::span<uint8_t> row =
region.buffer.subspan(y * region.stride, region.stride);
for (int x = l; x < l + w; ++x) {
size_t pixel_index = x * 4;
if (pixel_index + 2 < row.size()) {
row[pixel_index] =
static_cast<uint8_t>(row[pixel_index] * kCaretColor.fB);
row[pixel_index + 1] =
static_cast<uint8_t>(row[pixel_index + 1] * kCaretColor.fG);
row[pixel_index + 2] =
static_cast<uint8_t>(row[pixel_index + 2] * kCaretColor.fR);
}
}
}
if (!first_visible_) {
base::UmaHistogramBoolean("PDF.Caret.FirstVisible", true);
first_visible_ = true;
}
}
void PdfCaret::MoveToChar(const PageCharacterIndex& new_index,
bool should_select) {
if (!should_select) {
client_->ClearTextSelection();
SetVisible(true);
}
if (index_ == new_index) {
return;
}
if (!should_select || (!client_->IsSelecting() &&
!StartSelection(/*move_right=*/index_ < new_index))) {
SetCharAndDraw(new_index);
} else {
ExtendSelection(new_index);
SetChar(new_index);
}
if (!caret_screen_rect_.IsEmpty()) {
client_->ScrollToChar(cached_screen_rect_index_);
}
}
ui::KeyboardCode PdfCaret::GetLogicalKeyAfterTextDirection(
ui::KeyboardCode key) const {
switch (GetTextDirectionAt(index_)) {
case AccessibilityTextDirection::kLeftToRight:
return GetLogicalKey(key,
/*left_key=*/ui::KeyboardCode::VKEY_LEFT,
/*right_key=*/ui::KeyboardCode::VKEY_RIGHT,
/*up_key=*/ui::KeyboardCode::VKEY_UP,
/*down_key=*/ui::KeyboardCode::VKEY_DOWN);
case AccessibilityTextDirection::kRightToLeft:
return GetLogicalKey(key,
/*left_key=*/ui::KeyboardCode::VKEY_RIGHT,
/*right_key=*/ui::KeyboardCode::VKEY_LEFT,
/*up_key=*/ui::KeyboardCode::VKEY_UP,
/*down_key=*/ui::KeyboardCode::VKEY_DOWN);
case AccessibilityTextDirection::kTopToBottom:
return GetLogicalKey(key,
/*left_key=*/ui::KeyboardCode::VKEY_DOWN,
/*right_key=*/ui::KeyboardCode::VKEY_UP,
/*up_key=*/ui::KeyboardCode::VKEY_LEFT,
/*down_key=*/ui::KeyboardCode::VKEY_RIGHT);
case AccessibilityTextDirection::kBottomToTop:
return GetLogicalKey(key,
/*left_key=*/ui::KeyboardCode::VKEY_DOWN,
/*right_key=*/ui::KeyboardCode::VKEY_UP,
/*up_key=*/ui::KeyboardCode::VKEY_RIGHT,
/*down_key=*/ui::KeyboardCode::VKEY_LEFT);
default:
NOTREACHED();
}
}
void PdfCaret::MoveHorizontallyToNextChar(bool move_right, bool should_select) {
std::optional<PageCharacterIndex> next_char =
GetAdjacentCaretPos(index_, move_right);
MoveToChar(next_char.value_or(index_), should_select);
}
void PdfCaret::MoveVerticallyToNextChar(bool move_down, bool should_select) {
// Find the next text line by getting the next two sets of newlines that
// border the text line.
// Newlines are considered part of the current line. When moving up and on a
// newline, skip it so that `GetNextNewlineOnPage()` does not use the current
// newline.
PageCharacterIndex start_index = index_;
if (!move_down) {
start_index = GetNextNonNewlineOnPage(index_, /*move_right=*/false)
.value_or(start_index);
}
// Search for the first newline.
std::optional<PageCharacterIndex> first_newline =
GetNextNewlineOnPage(start_index, move_down);
uint32_t page_index = index_.page_index;
const int delta = move_down ? 1 : -1;
if (!first_newline.has_value()) {
// If there is no newline, then there is no next line on the current page.
page_index += delta;
if (!client_->PageIndexInBounds(page_index)) {
// There is no page in the `move_down` direction. Stay at the end of the
// page.
const PageCharacterIndex end_index = {
index_.page_index,
move_down ? client_->GetCharCount(index_.page_index) : 0};
MoveToChar(end_index, should_select);
return;
}
// Start at the beginning or end of the adjacent page.
first_newline = {page_index,
move_down ? 0 : client_->GetCharCount(page_index)};
} else {
// Check for consecutive newlines and skip one of them. There cannot be more
// than two consecutive newlines.
//
// Synthetic newlines cannot be the first or last char on a page.
CHECK(!WillCaretExitPage(first_newline.value(), /*move_right=*/false));
PageCharacterIndex adjacent_char = first_newline.value();
adjacent_char.char_index += delta;
if (client_->IsSynthesizedNewline(adjacent_char)) {
first_newline = adjacent_char;
}
}
// Search for the second newline. Start on the adjacent non-newline char.
start_index = GetNextNonNewlineOnPage(first_newline.value(), move_down)
.value_or(first_newline.value());
std::optional<PageCharacterIndex> second_newline =
GetNextNewlineOnPage(start_index, move_down);
if (!second_newline.has_value()) {
// If there is no newline, then the line starts or ends at one end of the
// page.
second_newline = {start_index.page_index,
move_down ? client_->GetCharCount(page_index) : 0};
}
// When moving up, `first_newline` is after the line and `second_newline` is
// before the line. Swap them.
if (first_newline.value().char_index > second_newline.value().char_index) {
std::swap(first_newline, second_newline);
}
MoveToChar(
GetClosestCharInTextLine(first_newline.value(), second_newline.value()),
should_select);
}
void PdfCaret::MoveHorizontallyToNextWordBoundary(bool move_right,
bool should_select) {
if (WillCaretExitPage(index_, move_right)) {
std::optional<PageCharacterIndex> next_char =
GetCaretPosOnAdjacentPage(index_, move_right);
MoveToChar(next_char.value_or(index_), should_select);
return;
}
MoveToChar(move_right ? GetCaretPosAtRightWordBoundary()
: GetCaretPosAtLeftWordBoundary(),
should_select);
}
bool PdfCaret::StartSelection(bool move_right) const {
if (client_->GetCharCount(index_.page_index) != 0) {
client_->StartSelection(index_);
return true;
}
// Avoid starting a selection on a no-text page by starting on the adjacent
// caret position.
// `GetAdjacentCaretPos()` will never return std::nullopt because the caret
// should always be moving.
PageCharacterIndex adjacent_caret_pos =
GetAdjacentCaretPos(index_, move_right).value();
if (client_->GetCharCount(adjacent_caret_pos.page_index) != 0) {
client_->StartSelection(adjacent_caret_pos);
return true;
}
return false;
}
void PdfCaret::ExtendSelection(const PageCharacterIndex& new_index) const {
if (client_->GetCharCount(new_index.page_index) != 0) {
client_->ExtendAndInvalidateSelectionByChar(new_index);
return;
}
uint32_t char_count = client_->GetCharCount(index_.page_index);
if (char_count == 0) {
// Moving from a no-text page to another no-text page. Do nothing.
return;
}
// When moving to a no-text page, select the remaining text on the original
// page.
const bool move_right = index_ < new_index;
PageCharacterIndex end_index = {index_.page_index,
move_right ? char_count : 0};
if (end_index == index_) {
// `end_index` is already part of the selection.
return;
}
client_->ExtendAndInvalidateSelectionByChar(end_index);
}
bool PdfCaret::WillCaretExitPage(const PageCharacterIndex& index,
bool move_right) const {
if (move_right) {
return index.char_index == client_->GetCharCount(index.page_index);
}
return index.char_index == 0;
}
bool PdfCaret::IndexHasChar(const PageCharacterIndex& index) const {
return index.char_index < client_->GetCharCount(index.page_index);
}
bool PdfCaret::IsIndexWordBoundary(const PageCharacterIndex& index) const {
return IndexHasChar(index) && IsWordBoundary(client_->GetCharUnicode(index));
}
bool PdfCaret::IsSynthesizedNewline(const PageCharacterIndex& index) const {
return IndexHasChar(index) && client_->IsSynthesizedNewline(index);
}
std::optional<PageCharacterIndex> PdfCaret::GetCaretPosOnAdjacentPage(
const PageCharacterIndex& index,
bool move_right) const {
uint32_t page_index = index.page_index;
// If `move_right` is true, move one page to the right if possible.
if (move_right) {
++page_index;
if (!client_->PageIndexInBounds(page_index)) {
// There is no next page. Stay at current position.
return std::nullopt;
}
return PageCharacterIndex(page_index, 0);
}
// Otherwise, move one page to the left if possible.
if (page_index == 0) {
// There is no previous page. Stay at current position.
return std::nullopt;
}
--page_index;
return PageCharacterIndex(page_index, client_->GetCharCount(page_index));
}
std::optional<PageCharacterIndex> PdfCaret::GetAdjacentCaretPos(
const PageCharacterIndex& index,
bool move_right) const {
if (WillCaretExitPage(index, move_right)) {
return GetCaretPosOnAdjacentPage(index, move_right);
}
const int delta = move_right ? 1 : -1;
PageCharacterIndex next_char = {index.page_index, index.char_index + delta};
// Newlines synthetically created by PDFium have empty screen rects.
// Skip consecutive newlines.
if (IsSynthesizedNewline(index) && IsSynthesizedNewline(next_char)) {
// Synthetic newlines cannot be the first or last char on a page.
CHECK(!WillCaretExitPage(next_char, move_right));
// There cannot be more than two consecutive synthetic newlines.
next_char.char_index += delta;
}
return next_char;
}
PageCharacterIndex PdfCaret::GetCaretPosAtLeftWordBoundary() const {
PageCharacterIndex index = index_;
// Blink's behavior with word boundaries is complex, so just use a simpler
// algorithm that almost matches Blink's.
//
// Move the caret left once to check for word boundaries or newlines.
CHECK(!WillCaretExitPage(index, /*move_right=*/false));
--index.char_index;
if (IsSynthesizedNewline(index)) {
// When skipping newlines, moving to the left should place the caret at the
// end of the word.
while (!WillCaretExitPage(index, /*move_right=*/false) &&
IsSynthesizedNewline(index)) {
--index.char_index;
}
// Move the caret once to the right to the start of the word.
++index.char_index;
return index;
}
SetIndexToAdjacentWordBoundary(index, /*move_right=*/false);
// Move the caret once to the right to the start of the word, except for the
// edge case where the word boundary is at the start of the page.
if (index.char_index > 0) {
++index.char_index;
}
return index;
}
PageCharacterIndex PdfCaret::GetCaretPosAtRightWordBoundary() const {
PageCharacterIndex index = index_;
// Blink's behavior with word boundaries is complex, so just use a simpler
// algorithm that almost matches Blink's.
if (IsSynthesizedNewline(index)) {
// When skipping newlines, moving right should place the caret at the start
// of the word.
while (!WillCaretExitPage(index, /*move_right=*/true) &&
IsSynthesizedNewline(index)) {
++index.char_index;
}
// Then skip word boundaries.
while (!WillCaretExitPage(index, /*move_right=*/true) &&
IsIndexWordBoundary(index)) {
++index.char_index;
}
return index;
}
if (IsIndexWordBoundary(index)) {
// Handle an edge case when moving right where the current char is a word
// boundary adjacent to more word boundaries. Skip any of the same word
// boundaries (e.g. " "), but stop at any different word boundaries (e.g.
// ", ").
uint32_t prev_char = client_->GetCharUnicode(index);
while (!WillCaretExitPage(index, /*move_right=*/true) &&
prev_char == client_->GetCharUnicode(index)) {
++index.char_index;
}
if (IsSynthesizedNewline(index) || IsIndexWordBoundary(index)) {
return index;
}
}
SetIndexToAdjacentWordBoundary(index, /*move_right=*/true);
return index;
}
void PdfCaret::SetIndexToAdjacentWordBoundary(PageCharacterIndex& index,
bool move_right) const {
const int delta = move_right ? 1 : -1;
// Skip consecutive word boundaries.
while (!WillCaretExitPage(index, move_right) && IsIndexWordBoundary(index)) {
index.char_index += delta;
}
// Find the word boundary or newline adjacent to the word.
while (!WillCaretExitPage(index, move_right) &&
!IsSynthesizedNewline(index) && !IsIndexWordBoundary(index)) {
index.char_index += delta;
}
}
std::optional<PageCharacterIndex> PdfCaret::GetNextNonNewlineOnPage(
const PageCharacterIndex& index,
bool move_right) const {
PageCharacterIndex curr_index = index;
const int delta = move_right ? 1 : -1;
while (!WillCaretExitPage(curr_index, move_right) &&
IsSynthesizedNewline(curr_index)) {
curr_index.char_index += delta;
}
if (curr_index == index) {
return std::nullopt;
}
return curr_index;
}
std::optional<PageCharacterIndex> PdfCaret::GetNextNewlineOnPage(
const PageCharacterIndex& index,
bool move_right) const {
PageCharacterIndex curr_index = index;
const int delta = move_right ? 1 : -1;
while (!WillCaretExitPage(curr_index, move_right) &&
!IsSynthesizedNewline(curr_index)) {
curr_index.char_index += delta;
}
if (WillCaretExitPage(curr_index, move_right)) {
// Could not find a newline.
return std::nullopt;
}
return curr_index;
}
PageCharacterIndex PdfCaret::GetClosestCharInTextLine(
const PageCharacterIndex& start_newline,
const PageCharacterIndex& end_newline) const {
const uint32_t page_index = start_newline.page_index;
CHECK_EQ(page_index, end_newline.page_index);
// The start newline is not part of the text line, so skip to the right.
PageCharacterIndex line_start =
GetNextNonNewlineOnPage(start_newline, /*move_right=*/true)
.value_or(start_newline);
const gfx::Point caret_center = caret_screen_rect_.CenterPoint();
// The property of distances of characters in a line from a point is unimodal
// (it decreases to a minimum, then increases). A binary search that compares
// mid and mid+1 can find this minimum efficiently.
// Cache the results of `GetScreenRectForCaret()` to avoid redundant calls.
base::flat_map<uint32_t, uint64_t> distances;
auto get_cached_distance = [&](uint32_t char_index) {
if (!distances.contains(char_index)) {
gfx::Rect screen_rect =
GetScreenRectForCaret({page_index, char_index}).screen_rect;
gfx::Vector2d distance_vector = caret_center - screen_rect.CenterPoint();
// Just need to compare relative lengths. Length squared is cheaper to
// compute.
distances[char_index] = distance_vector.LengthSquared();
}
return distances[char_index];
};
uint32_t low_char_index = line_start.char_index;
uint32_t high_char_index = end_newline.char_index;
while (low_char_index < high_char_index) {
uint32_t mid_char_index =
low_char_index + (high_char_index - low_char_index) / 2;
int64_t mid_distance = get_cached_distance(mid_char_index);
int64_t mid_right_distance = get_cached_distance(mid_char_index + 1);
if (mid_distance < mid_right_distance) {
// The closest character is in the left half, including mid.
high_char_index = mid_char_index;
} else {
// The closest character is in the right half, not including mid.
low_char_index = mid_char_index + 1;
}
}
return {page_index, low_char_index};
}
} // namespace chrome_pdf