/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* the caret is the text cursor used, e.g., when editing */

#include "nsCaret.h"

#include <algorithm>

#include "SelectionMovementUtils.h"
#include "gfxUtils.h"
#include "mozilla/CaretAssociationHint.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/Preferences.h"
#include "mozilla/PresShell.h"
#include "mozilla/ScrollContainerFrame.h"
#include "mozilla/StaticPrefs_bidi.h"
#include "mozilla/dom/CharacterDataBuffer.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/intl/BidiEmbeddingLevel.h"
#include "nsBlockFrame.h"
#include "nsCOMPtr.h"
#include "nsContentUtils.h"
#include "nsFontMetrics.h"
#include "nsFrameSelection.h"
#include "nsIBidiKeyboard.h"
#include "nsIContent.h"
#include "nsIFrame.h"
#include "nsIFrameInlines.h"
#include "nsISelectionController.h"
#include "nsITimer.h"
#include "nsLayoutUtils.h"
#include "nsMenuPopupFrame.h"
#include "nsPresContext.h"
#include "nsTextFrame.h"
#include "nsXULPopupManager.h"

using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::gfx;

using BidiEmbeddingLevel = mozilla::intl::BidiEmbeddingLevel;

// The bidi indicator hangs off the caret to one side, to show which
// direction the typing is in. It needs to be at least 2x2 to avoid looking
// like an insignificant dot
static const int32_t kMinBidiIndicatorPixels = 2;

nsCaret::nsCaret() = default;

nsCaret::~nsCaret() { StopBlinking(); }

nsresult nsCaret::Init(PresShell* aPresShell) {
  NS_ENSURE_ARG(aPresShell);

  RefPtr<Selection> selection =
      aPresShell->GetSelection(nsISelectionController::SELECTION_NORMAL);
  if (!selection) {
    return NS_ERROR_FAILURE;
  }

  selection->AddSelectionListener(this);
  mDomSelectionWeak = selection;
  UpdateHiddenDuringSelection();
  UpdateCaretPositionFromSelectionIfNeeded();

  return NS_OK;
}

static bool DrawCJKCaret(nsIFrame* aFrame, int32_t aOffset) {
  nsIContent* content = aFrame->GetContent();
  const CharacterDataBuffer* characterDataBuffer =
      content->GetCharacterDataBuffer();
  if (!characterDataBuffer) {
    return false;
  }
  if (aOffset < 0 ||
      static_cast<uint32_t>(aOffset) >= characterDataBuffer->GetLength()) {
    return false;
  }
  const char16_t ch =
      characterDataBuffer->CharAt(AssertedCast<uint32_t>(aOffset));
  return 0x2e80 <= ch && ch <= 0xd7ff;
}

nsCaret::Metrics nsCaret::ComputeMetrics(nsIFrame* aFrame, int32_t aOffset,
                                         nscoord aCaretHeight) {
  // Compute nominal sizes in appunits
  nscoord caretWidth =
      (aCaretHeight *
       LookAndFeel::GetFloat(LookAndFeel::FloatID::CaretAspectRatio, 0.0f)) +
      nsPresContext::CSSPixelsToAppUnits(
          LookAndFeel::GetInt(LookAndFeel::IntID::CaretWidth, 1));

  if (DrawCJKCaret(aFrame, aOffset)) {
    caretWidth += nsPresContext::CSSPixelsToAppUnits(1);
  }
  nscoord bidiIndicatorSize =
      nsPresContext::CSSPixelsToAppUnits(kMinBidiIndicatorPixels);
  bidiIndicatorSize = std::max(caretWidth, bidiIndicatorSize);

  // Round them to device pixels. Always round down, except that anything
  // between 0 and 1 goes up to 1 so we don't let the caret disappear.
  int32_t tpp = aFrame->PresContext()->AppUnitsPerDevPixel();
  Metrics result;
  result.mCaretWidth = NS_ROUND_BORDER_TO_PIXELS(caretWidth, tpp);
  result.mBidiIndicatorSize = NS_ROUND_BORDER_TO_PIXELS(bidiIndicatorSize, tpp);
  return result;
}

void nsCaret::Terminate() {
  // this doesn't erase the caret if it's drawn. Should it? We might not have
  // a good drawing environment during teardown.

  StopBlinking();
  mBlinkTimer = nullptr;

  // unregiser ourselves as a selection listener
  if (mDomSelectionWeak) {
    mDomSelectionWeak->RemoveSelectionListener(this);
  }
  mDomSelectionWeak = nullptr;
  mCaretPosition = {};
}

NS_IMPL_ISUPPORTS(nsCaret, nsISelectionListener)

Selection* nsCaret::GetSelection() { return mDomSelectionWeak; }

void nsCaret::SetSelection(Selection* aDOMSel) {
  MOZ_ASSERT(aDOMSel);
  mDomSelectionWeak = aDOMSel;
  UpdateHiddenDuringSelection();
  UpdateCaretPositionFromSelectionIfNeeded();
  ResetBlinking();
  SchedulePaint();
}

void nsCaret::SetVisible(bool aVisible) {
  const bool wasVisible = mVisible;
  mVisible = aVisible;
  if (mVisible != wasVisible) {
    CaretVisibilityMaybeChanged();
  }
}

bool nsCaret::IsVisible() const { return mVisible && !mHideCount; }

void nsCaret::CaretVisibilityMaybeChanged() {
  ResetBlinking();
  SchedulePaint();
  if (IsVisible()) {
    // We ignore caret position updates when the caret is not visible, so we
    // update the caret position here if needed.
    UpdateCaretPositionFromSelectionIfNeeded();
  }
}

void nsCaret::AddForceHide() {
  MOZ_ASSERT(mHideCount < UINT32_MAX);
  if (++mHideCount > 1) {
    return;
  }
  CaretVisibilityMaybeChanged();
}

void nsCaret::RemoveForceHide() {
  if (!mHideCount || --mHideCount) {
    return;
  }
  CaretVisibilityMaybeChanged();
}

void nsCaret::SetCaretReadOnly(bool aReadOnly) {
  mReadOnly = aReadOnly;
  ResetBlinking();
  SchedulePaint();
}

// Clamp the inline-position to be within our closest scroll frame and any
// ancestor clips if any. If we don't, then it clips us, and we don't appear at
// all. See bug 335560 and bug 1539720.
static nsPoint AdjustRectForClipping(const nsRect& aRect, nsIFrame* aFrame,
                                     bool aVertical) {
  nsRect rectRelativeToClip = aRect;
  ScrollContainerFrame* sf = nullptr;
  for (nsIFrame* current = aFrame; current; current = current->GetParent()) {
    if ((sf = do_QueryFrame(current))) {
      break;
    }
    if (current->IsTransformed()) {
      // We don't account for transforms in rectRelativeToCurrent, so stop
      // adjusting here.
      break;
    }
    rectRelativeToClip += current->GetPosition();
  }

  if (!sf) {
    return {};
  }

  nsRect clipRect = sf->GetScrollPortRect();
  nsPoint offset;
  // Now see if the caret extends beyond the view's bounds. If it does, then
  // snap it back, put it as close to the edge as it can.
  if (aVertical) {
    nscoord overflow = rectRelativeToClip.YMost() - clipRect.YMost();
    if (overflow > 0) {
      offset.y -= overflow;
    } else {
      overflow = rectRelativeToClip.y - clipRect.y;
      if (overflow < 0) {
        offset.y -= overflow;
      }
    }
  } else {
    nscoord overflow = rectRelativeToClip.XMost() - clipRect.XMost();
    if (overflow > 0) {
      offset.x -= overflow;
    } else {
      overflow = rectRelativeToClip.x - clipRect.x;
      if (overflow < 0) {
        offset.x -= overflow;
      }
    }
  }
  return offset;
}

/* static */
nsRect nsCaret::GetGeometryForFrame(nsIFrame* aFrame, int32_t aFrameOffset,
                                    nscoord* aBidiIndicatorSize) {
  nsPoint framePos(0, 0);
  nsRect rect;
  nsresult rv = aFrame->GetPointFromOffset(aFrameOffset, &framePos);
  if (NS_FAILED(rv)) {
    if (aBidiIndicatorSize) {
      *aBidiIndicatorSize = 0;
    }
    return rect;
  }

  nsIFrame* frame = aFrame->GetContentInsertionFrame();
  if (!frame) {
    frame = aFrame;
  }
  NS_ASSERTION(!frame->HasAnyStateBits(NS_FRAME_IN_REFLOW),
               "We should not be in the middle of reflow");
  WritingMode wm = aFrame->GetWritingMode();
  RefPtr<nsFontMetrics> fm =
      nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame);
  const auto caretBlockAxisMetrics = frame->GetCaretBlockAxisMetrics(wm, *fm);
  const bool vertical = wm.IsVertical();
  Metrics caretMetrics =
      ComputeMetrics(aFrame, aFrameOffset, caretBlockAxisMetrics.mExtent);

  nscoord inlineOffset = 0;
  if (nsTextFrame* textFrame = do_QueryFrame(aFrame)) {
    if (gfxTextRun* textRun = textFrame->GetTextRun(nsTextFrame::eInflated)) {
      // For "upstream" text where the textrun direction is reversed from the
      // frame's inline-dir we want the caret to be painted before rather than
      // after its nominal inline position, so we offset by its width.
      const bool textRunDirIsReverseOfFrame =
          wm.IsInlineReversed() != textRun->IsInlineReversed();
      // However, in sideways-lr mode we invert this behavior because this is
      // the one writing mode where bidi-LTR corresponds to inline-reversed
      // already, which reverses the desired caret placement behavior.
      // Note that the following condition is equivalent to:
      //   if ( (!textRun->IsSidewaysLeft() && textRunDirIsReverseOfFrame) ||
      //        (textRun->IsSidewaysLeft()  && !textRunDirIsReverseOfFrame) )
      if (textRunDirIsReverseOfFrame != textRun->IsSidewaysLeft()) {
        inlineOffset = wm.IsBidiLTR() ? -caretMetrics.mCaretWidth
                                      : caretMetrics.mCaretWidth;
      }
    }
  }

  // on RTL frames the right edge of mCaretRect must be equal to framePos
  if (aFrame->StyleVisibility()->mDirection == StyleDirection::Rtl) {
    if (vertical) {
      inlineOffset -= caretMetrics.mCaretWidth;
    } else {
      inlineOffset -= caretMetrics.mCaretWidth;
    }
  }

  if (vertical) {
    framePos.x = caretBlockAxisMetrics.mOffset;
    framePos.y += inlineOffset;
  } else {
    framePos.x += inlineOffset;
    framePos.y = caretBlockAxisMetrics.mOffset;
  }

  rect = nsRect(framePos, vertical ? nsSize(caretBlockAxisMetrics.mExtent,
                                            caretMetrics.mCaretWidth)
                                   : nsSize(caretMetrics.mCaretWidth,
                                            caretBlockAxisMetrics.mExtent));

  rect.MoveBy(AdjustRectForClipping(rect, aFrame, vertical));
  if (aBidiIndicatorSize) {
    *aBidiIndicatorSize = caretMetrics.mBidiIndicatorSize;
  }
  return rect;
}

auto nsCaret::CaretPositionFor(const Selection* aSelection) -> CaretPosition {
  if (!aSelection) {
    return {};
  }
  const nsFrameSelection* frameSelection = aSelection->GetFrameSelection();
  if (!frameSelection) {
    return {};
  }
  nsINode* node = aSelection->GetFocusNode();
  if (!node) {
    return {};
  }
  return {
      node,
      int32_t(aSelection->FocusOffset()),
      frameSelection->GetHint(),
      frameSelection->GetCaretBidiLevel(),
  };
}

CaretFrameData nsCaret::GetFrameAndOffset(const CaretPosition& aPosition) {
  nsINode* focusNode = aPosition.mContent;
  int32_t focusOffset = aPosition.mOffset;

  if (!focusNode || !focusNode->IsContent()) {
    return {};
  }

  nsIContent* contentNode = focusNode->AsContent();
  return SelectionMovementUtils::GetCaretFrameForNodeOffset(
      nullptr, contentNode, focusOffset, aPosition.mHint, aPosition.mBidiLevel,
      ForceEditableRegion::No);
}

/* static */
nsIFrame* nsCaret::GetGeometry(const Selection* aSelection, nsRect* aRect) {
  auto data = GetFrameAndOffset(CaretPositionFor(aSelection));
  if (data.mFrame) {
    *aRect =
        GetGeometryForFrame(data.mFrame, data.mOffsetInFrameContent, nullptr);
  }
  return data.mFrame;
}

// Generally we want the caret to paint from the containing block of the frame
// the caret is positioned at. The one exception is the
// ::-moz-text-control-editing-root, in which case we want the caret to paint
// from the input itself, so that it paints atop the placeholder and such
// without having to do magic elsewhere.
[[nodiscard]] static nsIFrame* GetContainingBlockIfNeeded(nsIFrame* aFrame) {
  for (auto* f = aFrame; f; f = f->GetContainingBlock()) {
    if (f->Style()->GetPseudoType() ==
        PseudoStyleType::MozTextControlEditingRoot) {
      continue;
    }
    if (f != aFrame || f->IsBlockOutside() || f->IsBlockFrameOrSubclass()) {
      return f == aFrame ? nullptr : f;
    }
  }
  return nullptr;
}

void nsCaret::SchedulePaint() {
  if (mLastPaintedFrame) {
    mLastPaintedFrame->SchedulePaint();
    mLastPaintedFrame = nullptr;
  }
  auto data = GetFrameAndOffset(mCaretPosition);
  if (!data.mFrame) {
    return;
  }
  nsIFrame* frame = data.mFrame;
  if (nsIFrame* cb = GetContainingBlockIfNeeded(frame)) {
    frame = cb;
  }
  frame->SchedulePaint();
}

void nsCaret::SetVisibilityDuringSelection(bool aVisibility) {
  if (mShowDuringSelection == aVisibility) {
    return;
  }
  mShowDuringSelection = aVisibility;
  UpdateHiddenDuringSelection();
  SchedulePaint();
}

void nsCaret::UpdateCaretPositionFromSelectionIfNeeded() {
  if (mFixedCaretPosition) {
    return;
  }
  CaretPosition newPos = CaretPositionFor(GetSelection());
  if (newPos == mCaretPosition) {
    return;
  }
  mCaretPosition = std::move(newPos);
  SchedulePaint();
}

void nsCaret::SetCaretPosition(nsINode* aNode, int32_t aOffset) {
  // Schedule a paint with the old position to invalidate.
  mFixedCaretPosition = !!aNode;
  if (mFixedCaretPosition) {
    mCaretPosition = {aNode, aOffset};
    SchedulePaint();
  } else {
    UpdateCaretPositionFromSelectionIfNeeded();
  }
  ResetBlinking();
}

void nsCaret::CheckSelectionLanguageChange() {
  if (!StaticPrefs::bidi_browser_ui()) {
    return;
  }

  bool isKeyboardRTL = false;
  nsIBidiKeyboard* bidiKeyboard = nsContentUtils::GetBidiKeyboard();
  if (bidiKeyboard) {
    bidiKeyboard->IsLangRTL(&isKeyboardRTL);
  }
  // Call SelectionLanguageChange on every paint. Mostly it will be a noop
  // but it should be fast anyway. This guarantees we never paint the caret
  // at the wrong place.
  Selection* selection = GetSelection();
  if (selection) {
    selection->SelectionLanguageChange(isKeyboardRTL);
  }
}

// This ensures that the caret is not affected by clips on inlines and so forth.
[[nodiscard]] static nsIFrame* MapToContainingBlock(nsIFrame* aFrame,
                                                    nsRect* aCaretRect,
                                                    nsRect* aHookRect) {
  nsIFrame* containingBlock = GetContainingBlockIfNeeded(aFrame);
  if (!containingBlock) {
    return aFrame;
  }

  if (aCaretRect) {
    *aCaretRect = nsLayoutUtils::TransformFrameRectToAncestor(
        aFrame, *aCaretRect, containingBlock);
  }
  if (aHookRect) {
    *aHookRect = nsLayoutUtils::TransformFrameRectToAncestor(aFrame, *aHookRect,
                                                             containingBlock);
  }
  return containingBlock;
}

nsIFrame* nsCaret::GetPaintGeometry(nsRect* aCaretRect, nsRect* aHookRect,
                                    nscolor* aCaretColor) {
  MOZ_ASSERT(!!aCaretRect == !!aHookRect);

  // Return null if we should not be visible.
  if (!IsVisible() || !mIsBlinkOn) {
    return nullptr;
  }

  // Update selection language direction now so the new direction will be
  // taken into account when computing the caret position below.
  CheckSelectionLanguageChange();

  auto data = GetFrameAndOffset(mCaretPosition);
  MOZ_ASSERT(!!data.mFrame == !!data.mUnadjustedFrame);
  if (!data.mFrame) {
    return nullptr;
  }

  nsIFrame* frame = data.mFrame;
  nsIFrame* unadjustedFrame = data.mUnadjustedFrame;
  int32_t frameOffset(data.mOffsetInFrameContent);
  // Now we have a frame, check whether it's appropriate to show the caret here.
  // Note we need to check the unadjusted frame, otherwise consider the
  // following case:
  //
  //   <div contenteditable><span contenteditable=false>Text   </span><br>
  //
  // Where the selection is targeting the <br>. We want to display the caret,
  // since the <br> we're focused at is editable, but we do want to paint it at
  // the adjusted frame offset, so that we can see the collapsed whitespace.
  if (unadjustedFrame->IsContentDisabled()) {
    return nullptr;
  }

  // If the offset falls outside of the frame, then don't paint the caret.
  if (frame->IsTextFrame()) {
    auto [startOffset, endOffset] = frame->GetOffsets();
    if (startOffset > frameOffset || endOffset < frameOffset) {
      return nullptr;
    }
  }

  if (aCaretColor) {
    *aCaretColor = frame->GetCaretColorAt(frameOffset);
  }

  if (aCaretRect || aHookRect) {
    ComputeCaretRects(frame, frameOffset, aCaretRect, aHookRect);
  }
  return MapToContainingBlock(frame, aCaretRect, aHookRect);
}

nsIFrame* nsCaret::GetPaintGeometry() {
  return GetPaintGeometry(nullptr, nullptr);
}

nsIFrame* nsCaret::GetPaintGeometry(nsRect* aRect) {
  nsRect caretRect;
  nsRect hookRect;
  nsIFrame* frame = GetPaintGeometry(&caretRect, &hookRect);
  aRect->UnionRect(caretRect, hookRect);
  return frame;
}

void nsCaret::PaintCaret(DrawTarget& aDrawTarget, nsIFrame* aForFrame,
                         const nsPoint& aOffset) {
  nsRect caretRect;
  nsRect hookRect;
  nscolor color;
  nsIFrame* frame = GetPaintGeometry(&caretRect, &hookRect, &color);
  MOZ_ASSERT(frame == aForFrame, "We're referring different frame");

  if (!frame) {
    return;
  }

  int32_t appUnitsPerDevPixel = frame->PresContext()->AppUnitsPerDevPixel();
  Rect devPxCaretRect = NSRectToSnappedRect(caretRect + aOffset,
                                            appUnitsPerDevPixel, aDrawTarget);
  Rect devPxHookRect =
      NSRectToSnappedRect(hookRect + aOffset, appUnitsPerDevPixel, aDrawTarget);

  ColorPattern pattern(ToDeviceColor(color));
  aDrawTarget.FillRect(devPxCaretRect, pattern);
  if (!hookRect.IsEmpty()) {
    aDrawTarget.FillRect(devPxHookRect, pattern);
  }
}

NS_IMETHODIMP
nsCaret::NotifySelectionChanged(Document*, Selection* aDomSel, int16_t aReason,
                                int32_t aAmount) {
  // The same caret is shared amongst the document and any text widgets it
  // may contain. This means that the caret could get notifications from
  // multiple selections.
  //
  // If this notification is for a selection that is not the one the
  // the caret is currently interested in (mDomSelectionWeak), or the caret
  // position is fixed, then there is nothing to do!
  if (mDomSelectionWeak != aDomSel) {
    return NS_OK;
  }

  // Check if we need to hide / un-hide the caret due to the selection being
  // collapsed.
  UpdateHiddenDuringSelection();

  // We don't bother computing the caret position when invisible. We'll do it if
  // we become visible in CaretVisibilityMaybeChanged().
  if (IsVisible()) {
    UpdateCaretPositionFromSelectionIfNeeded();
    ResetBlinking();
  }

  return NS_OK;
}

void nsCaret::UpdateHiddenDuringSelection() {
  const bool shouldShowCaret = mShowDuringSelection || !mDomSelectionWeak ||
                               mDomSelectionWeak->IsCollapsed();
  if (!shouldShowCaret == mHiddenDuringSelection) {
    return;
  }
  if (shouldShowCaret) {
    RemoveForceHide();
  } else {
    AddForceHide();
  }
  mHiddenDuringSelection = !shouldShowCaret;
}

void nsCaret::ResetBlinking() {
  // How many milliseconds we allow the timer to get off-sync when resetting
  // blinking too often. 50 is not likely to be user observable in practice,
  // it's ~4 animation frames at 60fps.
  static const auto kBlinkTimerSlack = TimeDuration::FromMilliseconds(50);
  const bool wasBlinkOn = mIsBlinkOn;
  mIsBlinkOn = true;

  if (mReadOnly || !IsVisible()) {
    StopBlinking();
    return;
  }

  const int32_t oldBlinkTime = mBlinkTime;
  mBlinkTime = LookAndFeel::CaretBlinkTime();
  if (mBlinkTime <= 0) {
    StopBlinking();
    return;
  }

  if (!wasBlinkOn) {
    SchedulePaint();
  }

  mBlinkCount = LookAndFeel::CaretBlinkCount();

  const auto now = TimeStamp::NowLoRes();
  const bool mustResetTimer = mBlinkTime != oldBlinkTime ||
                              mLastBlinkTimerReset.IsNull() ||
                              (now - mLastBlinkTimerReset) > kBlinkTimerSlack;
  if (!mustResetTimer) {
    return;
  }

  if (!mBlinkTimer) {
    mBlinkTimer = NS_NewTimer();
  }
  mLastBlinkTimerReset = now;
  mBlinkTimer->InitWithNamedFuncCallback(CaretBlinkCallback, this, mBlinkTime,
                                         nsITimer::TYPE_REPEATING_SLACK,
                                         "CaretBlinkCallback"_ns);
}

void nsCaret::StopBlinking() {
  if (mBlinkTimer) {
    mBlinkTimer->Cancel();
    mLastBlinkTimerReset = TimeStamp();
  }
}

size_t nsCaret::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
  size_t total = aMallocSizeOf(this);
  if (mBlinkTimer) {
    total += mBlinkTimer->SizeOfIncludingThis(aMallocSizeOf);
  }
  return total;
}

void nsCaret::ComputeCaretRects(nsIFrame* aFrame, int32_t aFrameOffset,
                                nsRect* aCaretRect, nsRect* aHookRect) {
  MOZ_ASSERT(aCaretRect && aHookRect);
  NS_ASSERTION(aFrame, "Should have a frame here");

  WritingMode wm = aFrame->GetWritingMode();
  bool isVertical = wm.IsVertical();

  nscoord bidiIndicatorSize;
  *aCaretRect = GetGeometryForFrame(aFrame, aFrameOffset, &bidiIndicatorSize);

  // Simon -- make a hook to draw to the left or right of the caret to show
  // keyboard language direction
  aHookRect->SetEmpty();
  if (!StaticPrefs::bidi_browser_ui()) {
    return;
  }

  bool isCaretRTL;
  nsIBidiKeyboard* bidiKeyboard = nsContentUtils::GetBidiKeyboard();
  // if bidiKeyboard->IsLangRTL() fails, there is no way to tell the
  // keyboard direction, or the user has no right-to-left keyboard
  // installed, so we never draw the hook.
  if (bidiKeyboard && NS_SUCCEEDED(bidiKeyboard->IsLangRTL(&isCaretRTL))) {
    // If keyboard language is RTL, draw the hook on the left; if LTR, to the
    // right The height of the hook rectangle is the same as the width of the
    // caret rectangle.
    if (isVertical) {
      if (wm.IsSidewaysLR()) {
        aHookRect->SetRect(aCaretRect->x + bidiIndicatorSize,
                           aCaretRect->y + (!isCaretRTL ? bidiIndicatorSize * -1
                                                        : aCaretRect->height),
                           aCaretRect->height, bidiIndicatorSize);
      } else {
        aHookRect->SetRect(aCaretRect->XMost() - bidiIndicatorSize,
                           aCaretRect->y + (isCaretRTL ? bidiIndicatorSize * -1
                                                       : aCaretRect->height),
                           aCaretRect->height, bidiIndicatorSize);
      }
    } else {
      aHookRect->SetRect(aCaretRect->x + (isCaretRTL ? bidiIndicatorSize * -1
                                                     : aCaretRect->width),
                         aCaretRect->y + bidiIndicatorSize, bidiIndicatorSize,
                         aCaretRect->width);
    }
  }
}

/* static */
void nsCaret::CaretBlinkCallback(nsITimer* aTimer, void* aClosure) {
  nsCaret* theCaret = static_cast<nsCaret*>(aClosure);
  if (!theCaret) {
    return;
  }
  theCaret->mLastBlinkTimerReset = TimeStamp();
  theCaret->mIsBlinkOn = !theCaret->mIsBlinkOn;
  theCaret->SchedulePaint();

  // mBlinkCount of -1 means blink count is not enabled.
  if (theCaret->mBlinkCount == -1) {
    return;
  }

  // Track the blink count, but only at end of a blink cycle.
  if (theCaret->mIsBlinkOn) {
    // If we exceeded the blink count, stop the timer.
    if (--theCaret->mBlinkCount <= 0) {
      theCaret->StopBlinking();
    }
  }
}
