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.

#ifndef PDF_PDF_CARET_H_
#define PDF_PDF_CARET_H_

#include <optional>

#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "pdf/page_character_index.h"
#include "pdf/pdf_caret_client.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/geometry/rect.h"

namespace blink {
class WebKeyboardEvent;
}

namespace chrome_pdf {

struct RegionData;

// Manages the text caret for text selection and navigation within a PDF. This
// class handles caret drawing, blinking, position updates, and keyboard-driven
// movement. For now, only used if Ink2 text highlighting is enabled.
//
// When moving by lines, caret movement is based on the geometric proximity of
// characters. This works well for standard text layouts, but has limitations.
// The implementation currently assumes a top-to-bottom text flow, and may not
// behave as expected with multi-column layouts or unconventional page designs
// (e.g. text lines that are not ordered from top to bottom).
class PdfCaret {
 public:
  // The pixel width of the caret.
  static constexpr int kCaretWidth = 1;

  // The default interval the caret should blink if not set by
  // `SetBlinkInterval()`. Exposed for testing.
  static constexpr base::TimeDelta kDefaultBlinkInterval =
      base::Milliseconds(500);

  explicit PdfCaret(PdfCaretClient* client);
  PdfCaret(const PdfCaret&) = delete;
  PdfCaret& operator=(const PdfCaret&) = delete;
  virtual ~PdfCaret();

  bool enabled() const { return enabled_; }

  // Sets whether the caret is enabled. No-op if state does not change. Draws
  // the caret if it should be visible, hides it otherwise. See
  // `ShouldDrawCaret()` for when the caret will be visible.
  void SetEnabled(bool enabled);

  // Sets whether the caret should be visible. No-op if state does not change.
  // Draws the caret if it should be visible, hides it otherwise. Note that even
  // if `visible` is true, the caret may still be hidden if the caret is
  // disabled. This state can be desired if the caller wants the caret to be
  // visible again on re-enable. See `ShouldDrawCaret()` for when the caret will
  // be visible.
  void SetVisible(bool visible);

  // Sets how often the caret should blink. If the interval is set to 0, the
  // caret will not blink. No-op if `interval` is negative.
  void SetBlinkInterval(base::TimeDelta interval);

  // Sets the caret's char position and updates its screen rect. Invalidates the
  // old caret rect if visible but not the new caret rect. Requires a page with
  // at least one char and a valid char index (from 0 up to the page's char
  // count, inclusive), otherwise crashes. Use this over `SetCharAndDraw()` when
  // the new caret should not appear on screen (e.g. during text selection).
  void SetChar(const PageCharacterIndex& next_char);

  // Same as `SetChar()`, but also draws the caret at the new position if
  // visible. Use this over `SetChar()` when the caret should appear on screen
  // immediately.
  void SetCharAndDraw(const PageCharacterIndex& next_char);

  // Draws the caret on the canvas if it is visible within any paint updates in
  // `dirty_in_screen`. Returns true if the caret was drawn, false otherwise.
  bool MaybeDrawCaret(const RegionData& region,
                      const gfx::Rect& dirty_in_screen) const;

  // Recalculates the caret's screen position and invalidates its area when the
  // viewport geometry changes.
  void OnGeometryChanged();

  // Returns whether `OnKeyDown()` will handle `event`. Only arrow key events
  // are handled. Events are not handled if the caret is disabled.
  bool WillHandleKeyDownEvent(const blink::WebKeyboardEvent& event);

  // Handles key events that move the caret. See `WillHandleKeyDownEvent()` for
  // what key events are handled. Returns true when the key event is handled,
  // false otherwise. Virtual to support testing.
  virtual bool OnKeyDown(const blink::WebKeyboardEvent& event);

 private:
  // Return result of `GetScreenRectForCaret()`.
  struct CaretScreenRectData {
    gfx::Rect screen_rect;
    PageCharacterIndex actual_index;
  };

  // Returns whether the caret should be drawn. It should only be drawn when the
  // caret is enabled and set as visible.
  bool ShouldDrawCaret() const;

  // Refreshes the caret's display state, drawing or hiding the caret depending
  // on the value of `ShouldDrawCaret()` and resetting the blink timer depending
  // on the value of `is_blinking_`.
  void RefreshDisplayState();

  // Called by `blink_timer_` to toggle caret visibility.
  void OnBlinkTimerFired();

  // Calculates and sets `caret_screen_rect_` and `cached_screen_rect_index_`
  // using the current `index_`.
  void SetScreenRectForCurrentCaret();

  // Returns the screen rect and index for the current caret if it were placed
  // at `index`. For chars without a defined rect (like synthetic newlines), it
  // calculates a position based on the preceding char.
  CaretScreenRectData GetScreenRectForCaret(
      const PageCharacterIndex& index) const;

  // Returns the screen rect for a char, which may be empty.
  gfx::Rect GetScreenRectForChar(const PageCharacterIndex& index) const;

  // Returns the text direction of `index`.
  AccessibilityTextDirection GetTextDirectionAt(
      const PageCharacterIndex& index) const;

  // Same as `GetTextDirectionAt()`, but takes page rotations into account.
  AccessibilityTextDirection GetTextDirectionAfterRotationAt(
      const PageCharacterIndex& index) const;

  // Draws `rect` as the caret on `region`.
  void Draw(const RegionData& region, const gfx::Rect& rect) const;

  // Moves the caret to `new_index`. If `should_select` is true, then the text
  // selection will be extended to `new_index`, starting from the original caret
  // position if not yet text selecting. If `should_select` is false, text
  // selection will be cleared, and the caret will be set visible.
  void MoveToChar(const PageCharacterIndex& new_index, bool should_select);

  // Returns the arrow key converted from the `key` input after taking text
  // direction into account. E.g. if the text direction is RTL and `key` is
  // `ui::KeyboardCode::VKEY_LEFT`, the return result will be
  // `ui::KeyboardCode::VKEY_RIGHT`. `key` must be an arrow key, otherwise
  // crashes.
  ui::KeyboardCode GetLogicalKeyAfterTextDirection(ui::KeyboardCode key) const;

  // Determines the next valid char, handling moving horizontally to a char on a
  // different page and ignoring newlines. Does nothing if the current char
  // cannot move to a valid page or char.
  void MoveHorizontallyToNextChar(bool move_right, bool should_select);

  // Same as `MoveHorizontallyToNextChar()`, but moves in the vertical
  // direction.
  void MoveVerticallyToNextChar(bool move_down, bool should_select);

  // This should only be called when the caret is moving. Starts a new text
  // selection at the current caret position, adjusting the exact index
  // depending on the direction specified by `move_right`.
  bool StartSelection(bool move_right) const;

  // Extends the text selection to `new_index`. Must already be selecting text,
  // otherwise does nothing. Never extends to a non-text page. Instead, the text
  // selection will be extended to the end of the page of the original caret
  // position.
  void ExtendSelection(const PageCharacterIndex& new_index) const;

  // Returns whether moving the caret from `index` will cause it to exit the
  // page or not. Does not consider whether there are any adjacent pages.
  bool WillCaretExitPage(const PageCharacterIndex& index,
                         bool move_right) const;

  // Returns whether `index` is a valid char or not. False when `index` is the
  // last caret position of a page.
  bool IndexHasChar(const PageCharacterIndex& index) const;

  // Returns whether `index` is a synthesized newline or not.
  bool IsSynthesizedNewline(const PageCharacterIndex& index) const;

  // Returns the adjacent caret position to `index`, moving in the direction
  // indicated by `move_right`. Moves across pages if necessary. This can return
  // caret positions on no-text pages. Returns `std::nullopt` if no adjacent
  // position is available.
  std::optional<PageCharacterIndex> GetAdjacentCaretPos(
      const PageCharacterIndex& index,
      bool move_right) const;

  // Gets the `PageCharacterIndex` of the next non-newline char. Starts from
  // `index` and skips past consecutive newlines on a page, moving in the
  // direction specified by `move_right`. Returns `std::nullopt` if `index` is
  // already a non-newline char or no non-newline char is found.
  std::optional<PageCharacterIndex> GetNextNonNewlineOnPage(
      const PageCharacterIndex& index,
      bool move_right) const;

  // Gets the `PageCharacterIndex` of the next newline on a page. Starts from
  // `index` and moves in the direction specified by `move_right`. Returns
  // `std::nullopt` if no more newlines are found.
  std::optional<PageCharacterIndex> GetNextNewlineOnPage(
      const PageCharacterIndex& index,
      bool move_right) const;

  // Gets the `PageCharacterIndex` of the char within a single line of text,
  // bounded by `start_newline` exclusive and `end_newline` inclusive, that is
  // closest to the current caret from center to center.
  PageCharacterIndex GetClosestCharInTextLine(
      const PageCharacterIndex& start_newline,
      const PageCharacterIndex& end_newline) const;

  // Client must outlive `this`.
  const raw_ptr<PdfCaretClient> client_;

  // The current caret position.
  // The char index can be max char count on the page, since the cursor can be
  // to the right of the last char.
  PageCharacterIndex index_;

  // The actual char index used to determine the caret's screen rect. This can
  // differ from `index_` if `index_` points to a char without a screen rect.
  PageCharacterIndex cached_screen_rect_index_;

  // Whether the caret is enabled.
  bool enabled_ = false;

  // Whether the caret is visible.
  bool is_visible_ = false;

  // Whether the caret is visible on screen, taking into account blinking.
  bool is_blink_visible_ = false;

  // Whether the caret has been drawn on screen at least once. Only used to
  // report metrics.
  mutable bool first_visible_ = false;

  // How often the caret should blink. 0 if the caret should not blink. Never
  // negative.
  base::TimeDelta blink_interval_ = kDefaultBlinkInterval;

  gfx::Rect caret_screen_rect_;

  base::RepeatingTimer blink_timer_;
};

}  // namespace chrome_pdf

#endif  // PDF_PDF_CARET_H_