tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

nsCaret.cpp (22456B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 /* the caret is the text cursor used, e.g., when editing */
      8 
      9 #include "nsCaret.h"
     10 
     11 #include <algorithm>
     12 
     13 #include "SelectionMovementUtils.h"
     14 #include "gfxUtils.h"
     15 #include "mozilla/CaretAssociationHint.h"
     16 #include "mozilla/LookAndFeel.h"
     17 #include "mozilla/Preferences.h"
     18 #include "mozilla/PresShell.h"
     19 #include "mozilla/ScrollContainerFrame.h"
     20 #include "mozilla/StaticPrefs_bidi.h"
     21 #include "mozilla/dom/CharacterDataBuffer.h"
     22 #include "mozilla/dom/Selection.h"
     23 #include "mozilla/gfx/2D.h"
     24 #include "mozilla/intl/BidiEmbeddingLevel.h"
     25 #include "nsBlockFrame.h"
     26 #include "nsCOMPtr.h"
     27 #include "nsContentUtils.h"
     28 #include "nsFontMetrics.h"
     29 #include "nsFrameSelection.h"
     30 #include "nsIBidiKeyboard.h"
     31 #include "nsIContent.h"
     32 #include "nsIFrame.h"
     33 #include "nsIFrameInlines.h"
     34 #include "nsISelectionController.h"
     35 #include "nsITimer.h"
     36 #include "nsLayoutUtils.h"
     37 #include "nsMenuPopupFrame.h"
     38 #include "nsPresContext.h"
     39 #include "nsTextFrame.h"
     40 #include "nsXULPopupManager.h"
     41 
     42 using namespace mozilla;
     43 using namespace mozilla::dom;
     44 using namespace mozilla::gfx;
     45 
     46 using BidiEmbeddingLevel = mozilla::intl::BidiEmbeddingLevel;
     47 
     48 // The bidi indicator hangs off the caret to one side, to show which
     49 // direction the typing is in. It needs to be at least 2x2 to avoid looking
     50 // like an insignificant dot
     51 static const int32_t kMinBidiIndicatorPixels = 2;
     52 
     53 nsCaret::nsCaret() = default;
     54 
     55 nsCaret::~nsCaret() { StopBlinking(); }
     56 
     57 nsresult nsCaret::Init(PresShell* aPresShell) {
     58  NS_ENSURE_ARG(aPresShell);
     59 
     60  RefPtr<Selection> selection =
     61      aPresShell->GetSelection(nsISelectionController::SELECTION_NORMAL);
     62  if (!selection) {
     63    return NS_ERROR_FAILURE;
     64  }
     65 
     66  selection->AddSelectionListener(this);
     67  mDomSelectionWeak = selection;
     68  UpdateCaretPositionFromSelectionIfNeeded();
     69 
     70  return NS_OK;
     71 }
     72 
     73 static bool DrawCJKCaret(nsIFrame* aFrame, int32_t aOffset) {
     74  nsIContent* content = aFrame->GetContent();
     75  const CharacterDataBuffer* characterDataBuffer =
     76      content->GetCharacterDataBuffer();
     77  if (!characterDataBuffer) {
     78    return false;
     79  }
     80  if (aOffset < 0 ||
     81      static_cast<uint32_t>(aOffset) >= characterDataBuffer->GetLength()) {
     82    return false;
     83  }
     84  const char16_t ch =
     85      characterDataBuffer->CharAt(AssertedCast<uint32_t>(aOffset));
     86  return 0x2e80 <= ch && ch <= 0xd7ff;
     87 }
     88 
     89 nsCaret::Metrics nsCaret::ComputeMetrics(nsIFrame* aFrame, int32_t aOffset,
     90                                         nscoord aCaretHeight) {
     91  // Compute nominal sizes in appunits
     92  nscoord caretWidth =
     93      (aCaretHeight *
     94       LookAndFeel::GetFloat(LookAndFeel::FloatID::CaretAspectRatio, 0.0f)) +
     95      nsPresContext::CSSPixelsToAppUnits(
     96          LookAndFeel::GetInt(LookAndFeel::IntID::CaretWidth, 1));
     97 
     98  if (DrawCJKCaret(aFrame, aOffset)) {
     99    caretWidth += nsPresContext::CSSPixelsToAppUnits(1);
    100  }
    101  nscoord bidiIndicatorSize =
    102      nsPresContext::CSSPixelsToAppUnits(kMinBidiIndicatorPixels);
    103  bidiIndicatorSize = std::max(caretWidth, bidiIndicatorSize);
    104 
    105  // Round them to device pixels. Always round down, except that anything
    106  // between 0 and 1 goes up to 1 so we don't let the caret disappear.
    107  int32_t tpp = aFrame->PresContext()->AppUnitsPerDevPixel();
    108  Metrics result;
    109  result.mCaretWidth = NS_ROUND_BORDER_TO_PIXELS(caretWidth, tpp);
    110  result.mBidiIndicatorSize = NS_ROUND_BORDER_TO_PIXELS(bidiIndicatorSize, tpp);
    111  return result;
    112 }
    113 
    114 void nsCaret::Terminate() {
    115  // this doesn't erase the caret if it's drawn. Should it? We might not have
    116  // a good drawing environment during teardown.
    117 
    118  StopBlinking();
    119  mBlinkTimer = nullptr;
    120 
    121  // unregiser ourselves as a selection listener
    122  if (mDomSelectionWeak) {
    123    mDomSelectionWeak->RemoveSelectionListener(this);
    124  }
    125  mDomSelectionWeak = nullptr;
    126  mCaretPosition = {};
    127 }
    128 
    129 NS_IMPL_ISUPPORTS(nsCaret, nsISelectionListener)
    130 
    131 Selection* nsCaret::GetSelection() { return mDomSelectionWeak; }
    132 
    133 void nsCaret::SetSelection(Selection* aDOMSel) {
    134  MOZ_ASSERT(aDOMSel);
    135  mDomSelectionWeak = aDOMSel;
    136  UpdateCaretPositionFromSelectionIfNeeded();
    137  ResetBlinking();
    138  SchedulePaint();
    139 }
    140 
    141 void nsCaret::SetVisible(bool aVisible) {
    142  const bool wasVisible = mVisible;
    143  mVisible = aVisible;
    144  if (mVisible != wasVisible) {
    145    CaretVisibilityMaybeChanged();
    146  }
    147 }
    148 
    149 bool nsCaret::IsVisible() const { return mVisible && !mHideCount; }
    150 
    151 void nsCaret::CaretVisibilityMaybeChanged() {
    152  ResetBlinking();
    153  SchedulePaint();
    154  if (IsVisible()) {
    155    // We ignore caret position updates when the caret is not visible, so we
    156    // update the caret position here if needed.
    157    UpdateCaretPositionFromSelectionIfNeeded();
    158  }
    159 }
    160 
    161 void nsCaret::AddForceHide() {
    162  MOZ_ASSERT(mHideCount < UINT32_MAX);
    163  if (++mHideCount > 1) {
    164    return;
    165  }
    166  CaretVisibilityMaybeChanged();
    167 }
    168 
    169 void nsCaret::RemoveForceHide() {
    170  if (!mHideCount || --mHideCount) {
    171    return;
    172  }
    173  CaretVisibilityMaybeChanged();
    174 }
    175 
    176 void nsCaret::SetCaretReadOnly(bool aReadOnly) {
    177  mReadOnly = aReadOnly;
    178  ResetBlinking();
    179  SchedulePaint();
    180 }
    181 
    182 // Clamp the inline-position to be within our closest scroll frame and any
    183 // ancestor clips if any. If we don't, then it clips us, and we don't appear at
    184 // all. See bug 335560 and bug 1539720.
    185 static nsPoint AdjustRectForClipping(const nsRect& aRect, nsIFrame* aFrame,
    186                                     bool aVertical) {
    187  nsRect rectRelativeToClip = aRect;
    188  ScrollContainerFrame* sf = nullptr;
    189  for (nsIFrame* current = aFrame; current; current = current->GetParent()) {
    190    if ((sf = do_QueryFrame(current))) {
    191      break;
    192    }
    193    if (current->IsTransformed()) {
    194      // We don't account for transforms in rectRelativeToCurrent, so stop
    195      // adjusting here.
    196      break;
    197    }
    198    rectRelativeToClip += current->GetPosition();
    199  }
    200 
    201  if (!sf) {
    202    return {};
    203  }
    204 
    205  nsRect clipRect = sf->GetScrollPortRect();
    206  nsPoint offset;
    207  // Now see if the caret extends beyond the view's bounds. If it does, then
    208  // snap it back, put it as close to the edge as it can.
    209  if (aVertical) {
    210    nscoord overflow = rectRelativeToClip.YMost() - clipRect.YMost();
    211    if (overflow > 0) {
    212      offset.y -= overflow;
    213    } else {
    214      overflow = rectRelativeToClip.y - clipRect.y;
    215      if (overflow < 0) {
    216        offset.y -= overflow;
    217      }
    218    }
    219  } else {
    220    nscoord overflow = rectRelativeToClip.XMost() - clipRect.XMost();
    221    if (overflow > 0) {
    222      offset.x -= overflow;
    223    } else {
    224      overflow = rectRelativeToClip.x - clipRect.x;
    225      if (overflow < 0) {
    226        offset.x -= overflow;
    227      }
    228    }
    229  }
    230  return offset;
    231 }
    232 
    233 /* static */
    234 nsRect nsCaret::GetGeometryForFrame(nsIFrame* aFrame, int32_t aFrameOffset,
    235                                    nscoord* aBidiIndicatorSize) {
    236  nsPoint framePos(0, 0);
    237  nsRect rect;
    238  nsresult rv = aFrame->GetPointFromOffset(aFrameOffset, &framePos);
    239  if (NS_FAILED(rv)) {
    240    if (aBidiIndicatorSize) {
    241      *aBidiIndicatorSize = 0;
    242    }
    243    return rect;
    244  }
    245 
    246  nsIFrame* frame = aFrame->GetContentInsertionFrame();
    247  if (!frame) {
    248    frame = aFrame;
    249  }
    250  NS_ASSERTION(!frame->HasAnyStateBits(NS_FRAME_IN_REFLOW),
    251               "We should not be in the middle of reflow");
    252  WritingMode wm = aFrame->GetWritingMode();
    253  RefPtr<nsFontMetrics> fm =
    254      nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame);
    255  const auto caretBlockAxisMetrics = frame->GetCaretBlockAxisMetrics(wm, *fm);
    256  const bool vertical = wm.IsVertical();
    257  Metrics caretMetrics =
    258      ComputeMetrics(aFrame, aFrameOffset, caretBlockAxisMetrics.mExtent);
    259 
    260  nscoord inlineOffset = 0;
    261  if (nsTextFrame* textFrame = do_QueryFrame(aFrame)) {
    262    if (gfxTextRun* textRun = textFrame->GetTextRun(nsTextFrame::eInflated)) {
    263      // For "upstream" text where the textrun direction is reversed from the
    264      // frame's inline-dir we want the caret to be painted before rather than
    265      // after its nominal inline position, so we offset by its width.
    266      const bool textRunDirIsReverseOfFrame =
    267          wm.IsInlineReversed() != textRun->IsInlineReversed();
    268      // However, in sideways-lr mode we invert this behavior because this is
    269      // the one writing mode where bidi-LTR corresponds to inline-reversed
    270      // already, which reverses the desired caret placement behavior.
    271      // Note that the following condition is equivalent to:
    272      //   if ( (!textRun->IsSidewaysLeft() && textRunDirIsReverseOfFrame) ||
    273      //        (textRun->IsSidewaysLeft()  && !textRunDirIsReverseOfFrame) )
    274      if (textRunDirIsReverseOfFrame != textRun->IsSidewaysLeft()) {
    275        inlineOffset = wm.IsBidiLTR() ? -caretMetrics.mCaretWidth
    276                                      : caretMetrics.mCaretWidth;
    277      }
    278    }
    279  }
    280 
    281  // on RTL frames the right edge of mCaretRect must be equal to framePos
    282  if (aFrame->StyleVisibility()->mDirection == StyleDirection::Rtl) {
    283    if (vertical) {
    284      inlineOffset -= caretMetrics.mCaretWidth;
    285    } else {
    286      inlineOffset -= caretMetrics.mCaretWidth;
    287    }
    288  }
    289 
    290  if (vertical) {
    291    framePos.x = caretBlockAxisMetrics.mOffset;
    292    framePos.y += inlineOffset;
    293  } else {
    294    framePos.x += inlineOffset;
    295    framePos.y = caretBlockAxisMetrics.mOffset;
    296  }
    297 
    298  rect = nsRect(framePos, vertical ? nsSize(caretBlockAxisMetrics.mExtent,
    299                                            caretMetrics.mCaretWidth)
    300                                   : nsSize(caretMetrics.mCaretWidth,
    301                                            caretBlockAxisMetrics.mExtent));
    302 
    303  rect.MoveBy(AdjustRectForClipping(rect, aFrame, vertical));
    304  if (aBidiIndicatorSize) {
    305    *aBidiIndicatorSize = caretMetrics.mBidiIndicatorSize;
    306  }
    307  return rect;
    308 }
    309 
    310 auto nsCaret::CaretPositionFor(const Selection* aSelection) -> CaretPosition {
    311  if (!aSelection) {
    312    return {};
    313  }
    314  const nsFrameSelection* frameSelection = aSelection->GetFrameSelection();
    315  if (!frameSelection) {
    316    return {};
    317  }
    318  nsINode* node = aSelection->GetFocusNode();
    319  if (!node) {
    320    return {};
    321  }
    322  return {
    323      node,
    324      int32_t(aSelection->FocusOffset()),
    325      frameSelection->GetHint(),
    326      frameSelection->GetCaretBidiLevel(),
    327  };
    328 }
    329 
    330 CaretFrameData nsCaret::GetFrameAndOffset(const CaretPosition& aPosition) {
    331  nsINode* focusNode = aPosition.mContent;
    332  int32_t focusOffset = aPosition.mOffset;
    333 
    334  if (!focusNode || !focusNode->IsContent()) {
    335    return {};
    336  }
    337 
    338  nsIContent* contentNode = focusNode->AsContent();
    339  return SelectionMovementUtils::GetCaretFrameForNodeOffset(
    340      nullptr, contentNode, focusOffset, aPosition.mHint, aPosition.mBidiLevel,
    341      ForceEditableRegion::No);
    342 }
    343 
    344 /* static */
    345 nsIFrame* nsCaret::GetGeometry(const Selection* aSelection, nsRect* aRect) {
    346  auto data = GetFrameAndOffset(CaretPositionFor(aSelection));
    347  if (data.mFrame) {
    348    *aRect =
    349        GetGeometryForFrame(data.mFrame, data.mOffsetInFrameContent, nullptr);
    350  }
    351  return data.mFrame;
    352 }
    353 
    354 [[nodiscard]] static nsIFrame* GetContainingBlockIfNeeded(nsIFrame* aFrame) {
    355  if (aFrame->IsBlockOutside() || aFrame->IsBlockFrameOrSubclass()) {
    356    return nullptr;
    357  }
    358  return aFrame->GetContainingBlock();
    359 }
    360 
    361 void nsCaret::SchedulePaint() {
    362  if (mLastPaintedFrame) {
    363    mLastPaintedFrame->SchedulePaint();
    364    mLastPaintedFrame = nullptr;
    365  }
    366  auto data = GetFrameAndOffset(mCaretPosition);
    367  if (!data.mFrame) {
    368    return;
    369  }
    370  nsIFrame* frame = data.mFrame;
    371  if (nsIFrame* cb = GetContainingBlockIfNeeded(frame)) {
    372    frame = cb;
    373  }
    374  frame->SchedulePaint();
    375 }
    376 
    377 void nsCaret::SetVisibilityDuringSelection(bool aVisibility) {
    378  if (mShowDuringSelection == aVisibility) {
    379    return;
    380  }
    381  mShowDuringSelection = aVisibility;
    382  if (mHiddenDuringSelection && aVisibility) {
    383    RemoveForceHide();
    384    mHiddenDuringSelection = false;
    385  }
    386  SchedulePaint();
    387 }
    388 
    389 void nsCaret::UpdateCaretPositionFromSelectionIfNeeded() {
    390  if (mFixedCaretPosition) {
    391    return;
    392  }
    393  CaretPosition newPos = CaretPositionFor(GetSelection());
    394  if (newPos == mCaretPosition) {
    395    return;
    396  }
    397  mCaretPosition = newPos;
    398  SchedulePaint();
    399 }
    400 
    401 void nsCaret::SetCaretPosition(nsINode* aNode, int32_t aOffset) {
    402  // Schedule a paint with the old position to invalidate.
    403  mFixedCaretPosition = !!aNode;
    404  if (mFixedCaretPosition) {
    405    mCaretPosition = {aNode, aOffset};
    406    SchedulePaint();
    407  } else {
    408    UpdateCaretPositionFromSelectionIfNeeded();
    409  }
    410  ResetBlinking();
    411 }
    412 
    413 void nsCaret::CheckSelectionLanguageChange() {
    414  if (!StaticPrefs::bidi_browser_ui()) {
    415    return;
    416  }
    417 
    418  bool isKeyboardRTL = false;
    419  nsIBidiKeyboard* bidiKeyboard = nsContentUtils::GetBidiKeyboard();
    420  if (bidiKeyboard) {
    421    bidiKeyboard->IsLangRTL(&isKeyboardRTL);
    422  }
    423  // Call SelectionLanguageChange on every paint. Mostly it will be a noop
    424  // but it should be fast anyway. This guarantees we never paint the caret
    425  // at the wrong place.
    426  Selection* selection = GetSelection();
    427  if (selection) {
    428    selection->SelectionLanguageChange(isKeyboardRTL);
    429  }
    430 }
    431 
    432 // This ensures that the caret is not affected by clips on inlines and so forth.
    433 [[nodiscard]] static nsIFrame* MapToContainingBlock(nsIFrame* aFrame,
    434                                                    nsRect* aCaretRect,
    435                                                    nsRect* aHookRect) {
    436  nsIFrame* containingBlock = GetContainingBlockIfNeeded(aFrame);
    437  if (!containingBlock) {
    438    return aFrame;
    439  }
    440 
    441  if (aCaretRect) {
    442    *aCaretRect = nsLayoutUtils::TransformFrameRectToAncestor(
    443        aFrame, *aCaretRect, containingBlock);
    444  }
    445  if (aHookRect) {
    446    *aHookRect = nsLayoutUtils::TransformFrameRectToAncestor(aFrame, *aHookRect,
    447                                                             containingBlock);
    448  }
    449  return containingBlock;
    450 }
    451 
    452 nsIFrame* nsCaret::GetPaintGeometry(nsRect* aCaretRect, nsRect* aHookRect,
    453                                    nscolor* aCaretColor) {
    454  MOZ_ASSERT(!!aCaretRect == !!aHookRect);
    455 
    456  // Return null if we should not be visible.
    457  if (!IsVisible() || !mIsBlinkOn) {
    458    return nullptr;
    459  }
    460 
    461  // Update selection language direction now so the new direction will be
    462  // taken into account when computing the caret position below.
    463  CheckSelectionLanguageChange();
    464 
    465  auto data = GetFrameAndOffset(mCaretPosition);
    466  MOZ_ASSERT(!!data.mFrame == !!data.mUnadjustedFrame);
    467  if (!data.mFrame) {
    468    return nullptr;
    469  }
    470 
    471  nsIFrame* frame = data.mFrame;
    472  nsIFrame* unadjustedFrame = data.mUnadjustedFrame;
    473  int32_t frameOffset(data.mOffsetInFrameContent);
    474  // Now we have a frame, check whether it's appropriate to show the caret here.
    475  // Note we need to check the unadjusted frame, otherwise consider the
    476  // following case:
    477  //
    478  //   <div contenteditable><span contenteditable=false>Text   </span><br>
    479  //
    480  // Where the selection is targeting the <br>. We want to display the caret,
    481  // since the <br> we're focused at is editable, but we do want to paint it at
    482  // the adjusted frame offset, so that we can see the collapsed whitespace.
    483  if (unadjustedFrame->IsContentDisabled()) {
    484    return nullptr;
    485  }
    486 
    487  // If the offset falls outside of the frame, then don't paint the caret.
    488  if (frame->IsTextFrame()) {
    489    auto [startOffset, endOffset] = frame->GetOffsets();
    490    if (startOffset > frameOffset || endOffset < frameOffset) {
    491      return nullptr;
    492    }
    493  }
    494 
    495  if (aCaretColor) {
    496    *aCaretColor = frame->GetCaretColorAt(frameOffset);
    497  }
    498 
    499  if (aCaretRect || aHookRect) {
    500    ComputeCaretRects(frame, frameOffset, aCaretRect, aHookRect);
    501  }
    502  return MapToContainingBlock(frame, aCaretRect, aHookRect);
    503 }
    504 
    505 nsIFrame* nsCaret::GetPaintGeometry() {
    506  return GetPaintGeometry(nullptr, nullptr);
    507 }
    508 
    509 nsIFrame* nsCaret::GetPaintGeometry(nsRect* aRect) {
    510  nsRect caretRect;
    511  nsRect hookRect;
    512  nsIFrame* frame = GetPaintGeometry(&caretRect, &hookRect);
    513  aRect->UnionRect(caretRect, hookRect);
    514  return frame;
    515 }
    516 
    517 void nsCaret::PaintCaret(DrawTarget& aDrawTarget, nsIFrame* aForFrame,
    518                         const nsPoint& aOffset) {
    519  nsRect caretRect;
    520  nsRect hookRect;
    521  nscolor color;
    522  nsIFrame* frame = GetPaintGeometry(&caretRect, &hookRect, &color);
    523  MOZ_ASSERT(frame == aForFrame, "We're referring different frame");
    524 
    525  if (!frame) {
    526    return;
    527  }
    528 
    529  int32_t appUnitsPerDevPixel = frame->PresContext()->AppUnitsPerDevPixel();
    530  Rect devPxCaretRect = NSRectToSnappedRect(caretRect + aOffset,
    531                                            appUnitsPerDevPixel, aDrawTarget);
    532  Rect devPxHookRect =
    533      NSRectToSnappedRect(hookRect + aOffset, appUnitsPerDevPixel, aDrawTarget);
    534 
    535  ColorPattern pattern(ToDeviceColor(color));
    536  aDrawTarget.FillRect(devPxCaretRect, pattern);
    537  if (!hookRect.IsEmpty()) {
    538    aDrawTarget.FillRect(devPxHookRect, pattern);
    539  }
    540 }
    541 
    542 NS_IMETHODIMP
    543 nsCaret::NotifySelectionChanged(Document*, Selection* aDomSel, int16_t aReason,
    544                                int32_t aAmount) {
    545  // The same caret is shared amongst the document and any text widgets it
    546  // may contain. This means that the caret could get notifications from
    547  // multiple selections.
    548  //
    549  // If this notification is for a selection that is not the one the
    550  // the caret is currently interested in (mDomSelectionWeak), or the caret
    551  // position is fixed, then there is nothing to do!
    552  if (mDomSelectionWeak != aDomSel) {
    553    return NS_OK;
    554  }
    555 
    556  // Check if we need to hide / un-hide the caret due to the selection being
    557  // collapsed.
    558  if (!mShowDuringSelection &&
    559      !aDomSel->IsCollapsed() != mHiddenDuringSelection) {
    560    if (mHiddenDuringSelection) {
    561      RemoveForceHide();
    562    } else {
    563      AddForceHide();
    564    }
    565    mHiddenDuringSelection = !mHiddenDuringSelection;
    566  }
    567 
    568  // We don't bother computing the caret position when invisible. We'll do it if
    569  // we become visible in CaretVisibilityMaybeChanged().
    570  if (IsVisible()) {
    571    UpdateCaretPositionFromSelectionIfNeeded();
    572    ResetBlinking();
    573  }
    574 
    575  return NS_OK;
    576 }
    577 
    578 void nsCaret::ResetBlinking() {
    579  // How many milliseconds we allow the timer to get off-sync when resetting
    580  // blinking too often. 50 is not likely to be user observable in practice,
    581  // it's ~4 animation frames at 60fps.
    582  static const auto kBlinkTimerSlack = TimeDuration::FromMilliseconds(50);
    583  const bool wasBlinkOn = mIsBlinkOn;
    584  mIsBlinkOn = true;
    585 
    586  if (mReadOnly || !IsVisible()) {
    587    StopBlinking();
    588    return;
    589  }
    590 
    591  const int32_t oldBlinkTime = mBlinkTime;
    592  mBlinkTime = LookAndFeel::CaretBlinkTime();
    593  if (mBlinkTime <= 0) {
    594    StopBlinking();
    595    return;
    596  }
    597 
    598  if (!wasBlinkOn) {
    599    SchedulePaint();
    600  }
    601 
    602  mBlinkCount = LookAndFeel::CaretBlinkCount();
    603 
    604  const auto now = TimeStamp::NowLoRes();
    605  const bool mustResetTimer = mBlinkTime != oldBlinkTime ||
    606                              mLastBlinkTimerReset.IsNull() ||
    607                              (now - mLastBlinkTimerReset) > kBlinkTimerSlack;
    608  if (!mustResetTimer) {
    609    return;
    610  }
    611 
    612  if (!mBlinkTimer) {
    613    mBlinkTimer = NS_NewTimer();
    614  }
    615  mLastBlinkTimerReset = now;
    616  mBlinkTimer->InitWithNamedFuncCallback(CaretBlinkCallback, this, mBlinkTime,
    617                                         nsITimer::TYPE_REPEATING_SLACK,
    618                                         "CaretBlinkCallback"_ns);
    619 }
    620 
    621 void nsCaret::StopBlinking() {
    622  if (mBlinkTimer) {
    623    mBlinkTimer->Cancel();
    624    mLastBlinkTimerReset = TimeStamp();
    625  }
    626 }
    627 
    628 size_t nsCaret::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
    629  size_t total = aMallocSizeOf(this);
    630  if (mBlinkTimer) {
    631    total += mBlinkTimer->SizeOfIncludingThis(aMallocSizeOf);
    632  }
    633  return total;
    634 }
    635 
    636 void nsCaret::ComputeCaretRects(nsIFrame* aFrame, int32_t aFrameOffset,
    637                                nsRect* aCaretRect, nsRect* aHookRect) {
    638  MOZ_ASSERT(aCaretRect && aHookRect);
    639  NS_ASSERTION(aFrame, "Should have a frame here");
    640 
    641  WritingMode wm = aFrame->GetWritingMode();
    642  bool isVertical = wm.IsVertical();
    643 
    644  nscoord bidiIndicatorSize;
    645  *aCaretRect = GetGeometryForFrame(aFrame, aFrameOffset, &bidiIndicatorSize);
    646 
    647  // Simon -- make a hook to draw to the left or right of the caret to show
    648  // keyboard language direction
    649  aHookRect->SetEmpty();
    650  if (!StaticPrefs::bidi_browser_ui()) {
    651    return;
    652  }
    653 
    654  bool isCaretRTL;
    655  nsIBidiKeyboard* bidiKeyboard = nsContentUtils::GetBidiKeyboard();
    656  // if bidiKeyboard->IsLangRTL() fails, there is no way to tell the
    657  // keyboard direction, or the user has no right-to-left keyboard
    658  // installed, so we never draw the hook.
    659  if (bidiKeyboard && NS_SUCCEEDED(bidiKeyboard->IsLangRTL(&isCaretRTL))) {
    660    // If keyboard language is RTL, draw the hook on the left; if LTR, to the
    661    // right The height of the hook rectangle is the same as the width of the
    662    // caret rectangle.
    663    if (isVertical) {
    664      if (wm.IsSidewaysLR()) {
    665        aHookRect->SetRect(aCaretRect->x + bidiIndicatorSize,
    666                           aCaretRect->y + (!isCaretRTL ? bidiIndicatorSize * -1
    667                                                        : aCaretRect->height),
    668                           aCaretRect->height, bidiIndicatorSize);
    669      } else {
    670        aHookRect->SetRect(aCaretRect->XMost() - bidiIndicatorSize,
    671                           aCaretRect->y + (isCaretRTL ? bidiIndicatorSize * -1
    672                                                       : aCaretRect->height),
    673                           aCaretRect->height, bidiIndicatorSize);
    674      }
    675    } else {
    676      aHookRect->SetRect(aCaretRect->x + (isCaretRTL ? bidiIndicatorSize * -1
    677                                                     : aCaretRect->width),
    678                         aCaretRect->y + bidiIndicatorSize, bidiIndicatorSize,
    679                         aCaretRect->width);
    680    }
    681  }
    682 }
    683 
    684 /* static */
    685 void nsCaret::CaretBlinkCallback(nsITimer* aTimer, void* aClosure) {
    686  nsCaret* theCaret = static_cast<nsCaret*>(aClosure);
    687  if (!theCaret) {
    688    return;
    689  }
    690  theCaret->mLastBlinkTimerReset = TimeStamp();
    691  theCaret->mIsBlinkOn = !theCaret->mIsBlinkOn;
    692  theCaret->SchedulePaint();
    693 
    694  // mBlinkCount of -1 means blink count is not enabled.
    695  if (theCaret->mBlinkCount == -1) {
    696    return;
    697  }
    698 
    699  // Track the blink count, but only at end of a blink cycle.
    700  if (theCaret->mIsBlinkOn) {
    701    // If we exceeded the blink count, stop the timer.
    702    if (--theCaret->mBlinkCount <= 0) {
    703      theCaret->StopBlinking();
    704    }
    705  }
    706 }