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/. */
7 /* the caret is the text cursor used, e.g., when editing */
14 #include "mozilla/CaretAssociationHint.h"
15 #include "mozilla/gfx/2D.h"
16 #include "mozilla/intl/BidiEmbeddingLevel.h"
17 #include "mozilla/ScrollContainerFrame.h"
18 #include "mozilla/StaticPrefs_bidi.h"
20 #include "nsFontMetrics.h"
22 #include "nsFrameSelection.h"
24 #include "nsIContent.h"
25 #include "nsIFrameInlines.h"
26 #include "nsLayoutUtils.h"
27 #include "nsPresContext.h"
28 #include "nsBlockFrame.h"
29 #include "nsISelectionController.h"
30 #include "nsTextFrame.h"
31 #include "nsXULPopupManager.h"
32 #include "nsMenuPopupFrame.h"
33 #include "nsTextFragment.h"
34 #include "mozilla/Preferences.h"
35 #include "mozilla/PresShell.h"
36 #include "mozilla/LookAndFeel.h"
37 #include "mozilla/dom/Selection.h"
38 #include "nsIBidiKeyboard.h"
39 #include "nsContentUtils.h"
40 #include "SelectionMovementUtils.h"
42 using namespace mozilla
;
43 using namespace mozilla::dom
;
44 using namespace mozilla::gfx
;
46 using BidiEmbeddingLevel
= mozilla::intl::BidiEmbeddingLevel
;
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;
53 nsCaret::nsCaret() = default;
55 nsCaret::~nsCaret() { StopBlinking(); }
57 nsresult
nsCaret::Init(PresShell
* aPresShell
) {
58 NS_ENSURE_ARG(aPresShell
);
61 do_GetWeakReference(aPresShell
); // the presshell owns us, so no addref
62 NS_ASSERTION(mPresShell
, "Hey, pres shell should support weak refs");
64 RefPtr
<Selection
> selection
=
65 aPresShell
->GetSelection(nsISelectionController::SELECTION_NORMAL
);
67 return NS_ERROR_FAILURE
;
70 selection
->AddSelectionListener(this);
71 mDomSelectionWeak
= selection
;
72 UpdateCaretPositionFromSelectionIfNeeded();
77 static bool DrawCJKCaret(nsIFrame
* aFrame
, int32_t aOffset
) {
78 nsIContent
* content
= aFrame
->GetContent();
79 const nsTextFragment
* frag
= content
->GetText();
83 if (aOffset
< 0 || static_cast<uint32_t>(aOffset
) >= frag
->GetLength()) {
86 const char16_t ch
= frag
->CharAt(AssertedCast
<uint32_t>(aOffset
));
87 return 0x2e80 <= ch
&& ch
<= 0xd7ff;
90 nsCaret::Metrics
nsCaret::ComputeMetrics(nsIFrame
* aFrame
, int32_t aOffset
,
91 nscoord aCaretHeight
) {
92 // Compute nominal sizes in appunits
95 LookAndFeel::GetFloat(LookAndFeel::FloatID::CaretAspectRatio
, 0.0f
)) +
96 nsPresContext::CSSPixelsToAppUnits(
97 LookAndFeel::GetInt(LookAndFeel::IntID::CaretWidth
, 1));
99 if (DrawCJKCaret(aFrame
, aOffset
)) {
100 caretWidth
+= nsPresContext::CSSPixelsToAppUnits(1);
102 nscoord bidiIndicatorSize
=
103 nsPresContext::CSSPixelsToAppUnits(kMinBidiIndicatorPixels
);
104 bidiIndicatorSize
= std::max(caretWidth
, bidiIndicatorSize
);
106 // Round them to device pixels. Always round down, except that anything
107 // between 0 and 1 goes up to 1 so we don't let the caret disappear.
108 int32_t tpp
= aFrame
->PresContext()->AppUnitsPerDevPixel();
110 result
.mCaretWidth
= NS_ROUND_BORDER_TO_PIXELS(caretWidth
, tpp
);
111 result
.mBidiIndicatorSize
= NS_ROUND_BORDER_TO_PIXELS(bidiIndicatorSize
, tpp
);
115 void nsCaret::Terminate() {
116 // this doesn't erase the caret if it's drawn. Should it? We might not have
117 // a good drawing environment during teardown.
120 mBlinkTimer
= nullptr;
122 // unregiser ourselves as a selection listener
123 if (mDomSelectionWeak
) {
124 mDomSelectionWeak
->RemoveSelectionListener(this);
126 mDomSelectionWeak
= nullptr;
127 mPresShell
= nullptr;
131 NS_IMPL_ISUPPORTS(nsCaret
, nsISelectionListener
)
133 Selection
* nsCaret::GetSelection() { return mDomSelectionWeak
; }
135 void nsCaret::SetSelection(Selection
* aDOMSel
) {
137 mDomSelectionWeak
= aDOMSel
;
138 UpdateCaretPositionFromSelectionIfNeeded();
143 void nsCaret::SetVisible(bool aVisible
) {
144 const bool wasVisible
= mVisible
;
146 if (mVisible
!= wasVisible
) {
147 CaretVisibilityMaybeChanged();
151 bool nsCaret::IsVisible() const { return mVisible
&& !mHideCount
; }
153 void nsCaret::CaretVisibilityMaybeChanged() {
157 // We ignore caret position updates when the caret is not visible, so we
158 // update the caret position here if needed.
159 UpdateCaretPositionFromSelectionIfNeeded();
163 void nsCaret::AddForceHide() {
164 MOZ_ASSERT(mHideCount
< UINT32_MAX
);
165 if (++mHideCount
> 1) {
168 CaretVisibilityMaybeChanged();
171 void nsCaret::RemoveForceHide() {
172 if (!mHideCount
|| --mHideCount
) {
175 CaretVisibilityMaybeChanged();
178 void nsCaret::SetCaretReadOnly(bool aReadOnly
) {
179 mReadOnly
= aReadOnly
;
184 // Clamp the inline-position to be within our closest scroll frame and any
185 // ancestor clips if any. If we don't, then it clips us, and we don't appear at
186 // all. See bug 335560 and bug 1539720.
187 static nsPoint
AdjustRectForClipping(const nsRect
& aRect
, nsIFrame
* aFrame
,
189 nsRect rectRelativeToClip
= aRect
;
190 ScrollContainerFrame
* sf
= nullptr;
191 nsIFrame
* scrollFrame
= nullptr;
192 for (nsIFrame
* current
= aFrame
; current
; current
= current
->GetParent()) {
193 if ((sf
= do_QueryFrame(current
))) {
194 scrollFrame
= current
;
197 if (current
->IsTransformed()) {
198 // We don't account for transforms in rectRelativeToCurrent, so stop
202 rectRelativeToClip
+= current
->GetPosition();
209 nsRect clipRect
= sf
->GetScrollPortRect();
211 const auto& disp
= *scrollFrame
->StyleDisplay();
212 if (disp
.mOverflowClipBoxBlock
== StyleOverflowClipBox::ContentBox
||
213 disp
.mOverflowClipBoxInline
== StyleOverflowClipBox::ContentBox
) {
214 const WritingMode wm
= scrollFrame
->GetWritingMode();
215 const bool cbH
= (wm
.IsVertical() ? disp
.mOverflowClipBoxBlock
216 : disp
.mOverflowClipBoxInline
) ==
217 StyleOverflowClipBox::ContentBox
;
218 const bool cbV
= (wm
.IsVertical() ? disp
.mOverflowClipBoxInline
219 : disp
.mOverflowClipBoxBlock
) ==
220 StyleOverflowClipBox::ContentBox
;
221 nsMargin padding
= scrollFrame
->GetUsedPadding();
223 padding
.left
= padding
.right
= 0;
226 padding
.top
= padding
.bottom
= 0;
228 clipRect
.Deflate(padding
);
232 // Now see if the caret extends beyond the view's bounds. If it does, then
233 // snap it back, put it as close to the edge as it can.
235 nscoord overflow
= rectRelativeToClip
.YMost() - clipRect
.YMost();
237 offset
.y
-= overflow
;
239 overflow
= rectRelativeToClip
.y
- clipRect
.y
;
241 offset
.y
-= overflow
;
245 nscoord overflow
= rectRelativeToClip
.XMost() - clipRect
.XMost();
247 offset
.x
-= overflow
;
249 overflow
= rectRelativeToClip
.x
- clipRect
.x
;
251 offset
.x
-= overflow
;
259 nsRect
nsCaret::GetGeometryForFrame(nsIFrame
* aFrame
, int32_t aFrameOffset
,
260 nscoord
* aBidiIndicatorSize
) {
261 nsPoint
framePos(0, 0);
263 nsresult rv
= aFrame
->GetPointFromOffset(aFrameOffset
, &framePos
);
265 if (aBidiIndicatorSize
) {
266 *aBidiIndicatorSize
= 0;
271 nsIFrame
* frame
= aFrame
->GetContentInsertionFrame();
275 NS_ASSERTION(!frame
->HasAnyStateBits(NS_FRAME_IN_REFLOW
),
276 "We should not be in the middle of reflow");
277 WritingMode wm
= aFrame
->GetWritingMode();
278 RefPtr
<nsFontMetrics
> fm
=
279 nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame
);
280 const auto caretBlockAxisMetrics
= frame
->GetCaretBlockAxisMetrics(wm
, *fm
);
281 const bool vertical
= wm
.IsVertical();
282 Metrics caretMetrics
=
283 ComputeMetrics(aFrame
, aFrameOffset
, caretBlockAxisMetrics
.mExtent
);
285 nscoord inlineOffset
= 0;
286 if (nsTextFrame
* textFrame
= do_QueryFrame(aFrame
)) {
287 if (gfxTextRun
* textRun
= textFrame
->GetTextRun(nsTextFrame::eInflated
)) {
288 // For "upstream" text where the textrun direction is reversed from the
289 // frame's inline-dir we want the caret to be painted before rather than
290 // after its nominal inline position, so we offset by its width.
291 const bool textRunDirIsReverseOfFrame
=
292 wm
.IsInlineReversed() != textRun
->IsInlineReversed();
293 // However, in sideways-lr mode we invert this behavior because this is
294 // the one writing mode where bidi-LTR corresponds to inline-reversed
295 // already, which reverses the desired caret placement behavior.
296 // Note that the following condition is equivalent to:
297 // if ( (!textRun->IsSidewaysLeft() && textRunDirIsReverseOfFrame) ||
298 // (textRun->IsSidewaysLeft() && !textRunDirIsReverseOfFrame) )
299 if (textRunDirIsReverseOfFrame
!= textRun
->IsSidewaysLeft()) {
300 inlineOffset
= wm
.IsBidiLTR() ? -caretMetrics
.mCaretWidth
301 : caretMetrics
.mCaretWidth
;
306 // on RTL frames the right edge of mCaretRect must be equal to framePos
307 if (aFrame
->StyleVisibility()->mDirection
== StyleDirection::Rtl
) {
309 inlineOffset
-= caretMetrics
.mCaretWidth
;
311 inlineOffset
-= caretMetrics
.mCaretWidth
;
316 framePos
.x
= caretBlockAxisMetrics
.mOffset
;
317 framePos
.y
+= inlineOffset
;
319 framePos
.x
+= inlineOffset
;
320 framePos
.y
= caretBlockAxisMetrics
.mOffset
;
323 rect
= nsRect(framePos
, vertical
? nsSize(caretBlockAxisMetrics
.mExtent
,
324 caretMetrics
.mCaretWidth
)
325 : nsSize(caretMetrics
.mCaretWidth
,
326 caretBlockAxisMetrics
.mExtent
));
328 rect
.MoveBy(AdjustRectForClipping(rect
, aFrame
, vertical
));
329 if (aBidiIndicatorSize
) {
330 *aBidiIndicatorSize
= caretMetrics
.mBidiIndicatorSize
;
335 auto nsCaret::CaretPositionFor(const Selection
* aSelection
) -> CaretPosition
{
339 const nsFrameSelection
* frameSelection
= aSelection
->GetFrameSelection();
340 if (!frameSelection
) {
343 nsINode
* node
= aSelection
->GetFocusNode();
349 int32_t(aSelection
->FocusOffset()),
350 frameSelection
->GetHint(),
351 frameSelection
->GetCaretBidiLevel(),
355 CaretFrameData
nsCaret::GetFrameAndOffset(const CaretPosition
& aPosition
) {
356 nsINode
* focusNode
= aPosition
.mContent
;
357 int32_t focusOffset
= aPosition
.mOffset
;
359 if (!focusNode
|| !focusNode
->IsContent()) {
363 nsIContent
* contentNode
= focusNode
->AsContent();
364 return SelectionMovementUtils::GetCaretFrameForNodeOffset(
365 nullptr, contentNode
, focusOffset
, aPosition
.mHint
, aPosition
.mBidiLevel
,
366 ForceEditableRegion::No
);
370 nsIFrame
* nsCaret::GetGeometry(const Selection
* aSelection
, nsRect
* aRect
) {
371 auto data
= GetFrameAndOffset(CaretPositionFor(aSelection
));
374 GetGeometryForFrame(data
.mFrame
, data
.mOffsetInFrameContent
, nullptr);
379 [[nodiscard
]] static nsIFrame
* GetContainingBlockIfNeeded(nsIFrame
* aFrame
) {
380 if (aFrame
->IsBlockOutside() || aFrame
->IsBlockFrameOrSubclass()) {
383 return aFrame
->GetContainingBlock();
386 void nsCaret::SchedulePaint() {
387 if (mLastPaintedFrame
) {
388 mLastPaintedFrame
->SchedulePaint();
389 mLastPaintedFrame
= nullptr;
391 auto data
= GetFrameAndOffset(mCaretPosition
);
395 nsIFrame
* frame
= data
.mFrame
;
396 if (nsIFrame
* cb
= GetContainingBlockIfNeeded(frame
)) {
399 frame
->SchedulePaint();
402 void nsCaret::SetVisibilityDuringSelection(bool aVisibility
) {
403 if (mShowDuringSelection
== aVisibility
) {
406 mShowDuringSelection
= aVisibility
;
407 if (mHiddenDuringSelection
&& aVisibility
) {
409 mHiddenDuringSelection
= false;
414 void nsCaret::UpdateCaretPositionFromSelectionIfNeeded() {
415 if (mFixedCaretPosition
) {
418 CaretPosition newPos
= CaretPositionFor(GetSelection());
419 if (newPos
== mCaretPosition
) {
422 mCaretPosition
= newPos
;
426 void nsCaret::SetCaretPosition(nsINode
* aNode
, int32_t aOffset
) {
427 // Schedule a paint with the old position to invalidate.
428 mFixedCaretPosition
= !!aNode
;
429 if (mFixedCaretPosition
) {
430 mCaretPosition
= {aNode
, aOffset
};
433 UpdateCaretPositionFromSelectionIfNeeded();
438 void nsCaret::CheckSelectionLanguageChange() {
439 if (!StaticPrefs::bidi_browser_ui()) {
443 bool isKeyboardRTL
= false;
444 nsIBidiKeyboard
* bidiKeyboard
= nsContentUtils::GetBidiKeyboard();
446 bidiKeyboard
->IsLangRTL(&isKeyboardRTL
);
448 // Call SelectionLanguageChange on every paint. Mostly it will be a noop
449 // but it should be fast anyway. This guarantees we never paint the caret
450 // at the wrong place.
451 Selection
* selection
= GetSelection();
453 selection
->SelectionLanguageChange(isKeyboardRTL
);
457 // This ensures that the caret is not affected by clips on inlines and so forth.
458 [[nodiscard
]] static nsIFrame
* MapToContainingBlock(nsIFrame
* aFrame
,
461 nsIFrame
* containingBlock
= GetContainingBlockIfNeeded(aFrame
);
462 if (!containingBlock
) {
467 *aCaretRect
= nsLayoutUtils::TransformFrameRectToAncestor(
468 aFrame
, *aCaretRect
, containingBlock
);
471 *aHookRect
= nsLayoutUtils::TransformFrameRectToAncestor(aFrame
, *aHookRect
,
474 return containingBlock
;
477 nsIFrame
* nsCaret::GetPaintGeometry(nsRect
* aCaretRect
, nsRect
* aHookRect
,
478 nscolor
* aCaretColor
) {
479 MOZ_ASSERT(!!aCaretRect
== !!aHookRect
);
481 // Return null if we should not be visible.
482 if (!IsVisible() || !mIsBlinkOn
) {
486 // Update selection language direction now so the new direction will be
487 // taken into account when computing the caret position below.
488 CheckSelectionLanguageChange();
490 auto data
= GetFrameAndOffset(mCaretPosition
);
491 MOZ_ASSERT(!!data
.mFrame
== !!data
.mUnadjustedFrame
);
496 nsIFrame
* frame
= data
.mFrame
;
497 nsIFrame
* unadjustedFrame
= data
.mUnadjustedFrame
;
498 int32_t frameOffset(data
.mOffsetInFrameContent
);
499 // Now we have a frame, check whether it's appropriate to show the caret here.
500 // Note we need to check the unadjusted frame, otherwise consider the
503 // <div contenteditable><span contenteditable=false>Text </span><br>
505 // Where the selection is targeting the <br>. We want to display the caret,
506 // since the <br> we're focused at is editable, but we do want to paint it at
507 // the adjusted frame offset, so that we can see the collapsed whitespace.
508 if (unadjustedFrame
->IsContentDisabled()) {
512 // If the offset falls outside of the frame, then don't paint the caret.
513 if (frame
->IsTextFrame()) {
514 auto [startOffset
, endOffset
] = frame
->GetOffsets();
515 if (startOffset
> frameOffset
|| endOffset
< frameOffset
) {
521 *aCaretColor
= frame
->GetCaretColorAt(frameOffset
);
524 if (aCaretRect
|| aHookRect
) {
525 ComputeCaretRects(frame
, frameOffset
, aCaretRect
, aHookRect
);
527 return MapToContainingBlock(frame
, aCaretRect
, aHookRect
);
530 nsIFrame
* nsCaret::GetPaintGeometry() {
531 return GetPaintGeometry(nullptr, nullptr);
534 nsIFrame
* nsCaret::GetPaintGeometry(nsRect
* aRect
) {
537 nsIFrame
* frame
= GetPaintGeometry(&caretRect
, &hookRect
);
538 aRect
->UnionRect(caretRect
, hookRect
);
542 void nsCaret::PaintCaret(DrawTarget
& aDrawTarget
, nsIFrame
* aForFrame
,
543 const nsPoint
& aOffset
) {
547 nsIFrame
* frame
= GetPaintGeometry(&caretRect
, &hookRect
, &color
);
548 MOZ_ASSERT(frame
== aForFrame
, "We're referring different frame");
554 int32_t appUnitsPerDevPixel
= frame
->PresContext()->AppUnitsPerDevPixel();
555 Rect devPxCaretRect
= NSRectToSnappedRect(caretRect
+ aOffset
,
556 appUnitsPerDevPixel
, aDrawTarget
);
558 NSRectToSnappedRect(hookRect
+ aOffset
, appUnitsPerDevPixel
, aDrawTarget
);
560 ColorPattern
pattern(ToDeviceColor(color
));
561 aDrawTarget
.FillRect(devPxCaretRect
, pattern
);
562 if (!hookRect
.IsEmpty()) {
563 aDrawTarget
.FillRect(devPxHookRect
, pattern
);
568 nsCaret::NotifySelectionChanged(Document
*, Selection
* aDomSel
, int16_t aReason
,
570 // The same caret is shared amongst the document and any text widgets it
571 // may contain. This means that the caret could get notifications from
572 // multiple selections.
574 // If this notification is for a selection that is not the one the
575 // the caret is currently interested in (mDomSelectionWeak), or the caret
576 // position is fixed, then there is nothing to do!
577 if (mDomSelectionWeak
!= aDomSel
) {
581 // Check if we need to hide / un-hide the caret due to the selection being
583 if (!mShowDuringSelection
&&
584 !aDomSel
->IsCollapsed() != mHiddenDuringSelection
) {
585 if (mHiddenDuringSelection
) {
590 mHiddenDuringSelection
= !mHiddenDuringSelection
;
593 // We don't bother computing the caret position when invisible. We'll do it if
594 // we become visible in CaretVisibilityMaybeChanged().
596 UpdateCaretPositionFromSelectionIfNeeded();
603 void nsCaret::ResetBlinking() {
606 if (mReadOnly
|| !IsVisible()) {
611 const auto blinkTime
= LookAndFeel::CaretBlinkTime();
612 if (blinkTime
<= 0) {
617 mBlinkCount
= LookAndFeel::CaretBlinkCount();
619 mBlinkTimer
= NS_NewTimer();
621 mBlinkTimer
->InitWithNamedFuncCallback(CaretBlinkCallback
, this, blinkTime
,
622 nsITimer::TYPE_REPEATING_SLACK
,
623 "CaretBlinkCallback");
626 void nsCaret::StopBlinking() {
628 mBlinkTimer
->Cancel();
632 size_t nsCaret::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf
) const {
633 size_t total
= aMallocSizeOf(this);
635 // We only want the size of the nsWeakReference object, not the PresShell
636 // (since we don't own the PresShell).
637 total
+= mPresShell
->SizeOfOnlyThis(aMallocSizeOf
);
640 total
+= mBlinkTimer
->SizeOfIncludingThis(aMallocSizeOf
);
645 void nsCaret::ComputeCaretRects(nsIFrame
* aFrame
, int32_t aFrameOffset
,
646 nsRect
* aCaretRect
, nsRect
* aHookRect
) {
647 MOZ_ASSERT(aCaretRect
&& aHookRect
);
648 NS_ASSERTION(aFrame
, "Should have a frame here");
650 WritingMode wm
= aFrame
->GetWritingMode();
651 bool isVertical
= wm
.IsVertical();
653 nscoord bidiIndicatorSize
;
654 *aCaretRect
= GetGeometryForFrame(aFrame
, aFrameOffset
, &bidiIndicatorSize
);
656 // Simon -- make a hook to draw to the left or right of the caret to show
657 // keyboard language direction
658 aHookRect
->SetEmpty();
659 if (!StaticPrefs::bidi_browser_ui()) {
664 nsIBidiKeyboard
* bidiKeyboard
= nsContentUtils::GetBidiKeyboard();
665 // if bidiKeyboard->IsLangRTL() fails, there is no way to tell the
666 // keyboard direction, or the user has no right-to-left keyboard
667 // installed, so we never draw the hook.
668 if (bidiKeyboard
&& NS_SUCCEEDED(bidiKeyboard
->IsLangRTL(&isCaretRTL
))) {
669 // If keyboard language is RTL, draw the hook on the left; if LTR, to the
670 // right The height of the hook rectangle is the same as the width of the
673 if (wm
.IsSidewaysLR()) {
674 aHookRect
->SetRect(aCaretRect
->x
+ bidiIndicatorSize
,
675 aCaretRect
->y
+ (!isCaretRTL
? bidiIndicatorSize
* -1
676 : aCaretRect
->height
),
677 aCaretRect
->height
, bidiIndicatorSize
);
679 aHookRect
->SetRect(aCaretRect
->XMost() - bidiIndicatorSize
,
680 aCaretRect
->y
+ (isCaretRTL
? bidiIndicatorSize
* -1
681 : aCaretRect
->height
),
682 aCaretRect
->height
, bidiIndicatorSize
);
685 aHookRect
->SetRect(aCaretRect
->x
+ (isCaretRTL
? bidiIndicatorSize
* -1
686 : aCaretRect
->width
),
687 aCaretRect
->y
+ bidiIndicatorSize
, bidiIndicatorSize
,
694 void nsCaret::CaretBlinkCallback(nsITimer
* aTimer
, void* aClosure
) {
695 nsCaret
* theCaret
= static_cast<nsCaret
*>(aClosure
);
699 theCaret
->mIsBlinkOn
= !theCaret
->mIsBlinkOn
;
700 theCaret
->SchedulePaint();
702 // mBlinkCount of -1 means blink count is not enabled.
703 if (theCaret
->mBlinkCount
== -1) {
707 // Track the blink count, but only at end of a blink cycle.
708 if (theCaret
->mIsBlinkOn
) {
709 // If we exceeded the blink count, stop the timer.
710 if (--theCaret
->mBlinkCount
<= 0) {
711 theCaret
->StopBlinking();