| // 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); |
| |
| // Moves the caret horizontally to the next word boundary, handling different |
| // pages and newlines when necessary. Moves to its original position if there |
| // is no valid word boundary. |
| void MoveHorizontallyToNextWordBoundary(bool move_right, 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 word boundary or not. |
| bool IsIndexWordBoundary(const PageCharacterIndex& index) const; |
| |
| // Returns whether `index` is a synthesized newline or not. |
| bool IsSynthesizedNewline(const PageCharacterIndex& index) const; |
| |
| // Returns the nearest caret position of the page adjacent to index, in the |
| // `move_right` direction. Returns `std::nullopt` if there is no adjacent |
| // page. If there is an adjacent page, then if `move_right` is true, this will |
| // return the first char on the next page, otherwise it will return the last |
| // char on the previous page. |
| std::optional<PageCharacterIndex> GetCaretPosOnAdjacentPage( |
| const PageCharacterIndex& index, |
| bool move_right) 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; |
| |
| // Returns the caret position of the nearest word boundary on the left. |
| // Handles different pages when necessary. May return the original position if |
| // there are no other left word boundaries. |
| PageCharacterIndex GetCaretPosAtLeftWordBoundary() const; |
| |
| // Returns the caret position of the nearest word boundary on the right. |
| // Handles different pages when necessary. May return the original position if |
| // there are no other right word boundaries. |
| PageCharacterIndex GetCaretPosAtRightWordBoundary() const; |
| |
| // Sets `index` to the nearest adjacent word boundary in the `move_right` |
| // direction. |
| void SetIndexToAdjacentWordBoundary(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_ |