732 lines
23 KiB
C++
732 lines
23 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* 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 "gfxUtils.h"
|
|
#include "mozilla/CaretAssociationHint.h"
|
|
#include "mozilla/gfx/2D.h"
|
|
#include "mozilla/intl/BidiEmbeddingLevel.h"
|
|
#include "mozilla/ScrollContainerFrame.h"
|
|
#include "mozilla/StaticPrefs_bidi.h"
|
|
#include "nsCOMPtr.h"
|
|
#include "nsFontMetrics.h"
|
|
#include "nsITimer.h"
|
|
#include "nsFrameSelection.h"
|
|
#include "nsIFrame.h"
|
|
#include "nsIContent.h"
|
|
#include "nsIFrameInlines.h"
|
|
#include "nsLayoutUtils.h"
|
|
#include "nsPresContext.h"
|
|
#include "nsBlockFrame.h"
|
|
#include "nsISelectionController.h"
|
|
#include "nsTextFrame.h"
|
|
#include "nsXULPopupManager.h"
|
|
#include "nsMenuPopupFrame.h"
|
|
#include "nsTextFragment.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "mozilla/PresShell.h"
|
|
#include "mozilla/LookAndFeel.h"
|
|
#include "mozilla/dom/Selection.h"
|
|
#include "nsIBidiKeyboard.h"
|
|
#include "nsContentUtils.h"
|
|
#include "SelectionMovementUtils.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);
|
|
|
|
mPresShell =
|
|
do_GetWeakReference(aPresShell); // the presshell owns us, so no addref
|
|
NS_ASSERTION(mPresShell, "Hey, pres shell should support weak refs");
|
|
|
|
RefPtr<Selection> selection =
|
|
aPresShell->GetSelection(nsISelectionController::SELECTION_NORMAL);
|
|
if (!selection) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
selection->AddSelectionListener(this);
|
|
mDomSelectionWeak = selection;
|
|
UpdateCaretPositionFromSelectionIfNeeded();
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
static bool DrawCJKCaret(nsIFrame* aFrame, int32_t aOffset) {
|
|
nsIContent* content = aFrame->GetContent();
|
|
const nsTextFragment* frag = content->GetText();
|
|
if (!frag) {
|
|
return false;
|
|
}
|
|
if (aOffset < 0 || static_cast<uint32_t>(aOffset) >= frag->GetLength()) {
|
|
return false;
|
|
}
|
|
const char16_t ch = frag->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;
|
|
mPresShell = nullptr;
|
|
mCaretPosition = {};
|
|
}
|
|
|
|
NS_IMPL_ISUPPORTS(nsCaret, nsISelectionListener)
|
|
|
|
Selection* nsCaret::GetSelection() { return mDomSelectionWeak; }
|
|
|
|
void nsCaret::SetSelection(Selection* aDOMSel) {
|
|
MOZ_ASSERT(aDOMSel);
|
|
mDomSelectionWeak = aDOMSel;
|
|
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;
|
|
nsIFrame* scrollFrame = nullptr;
|
|
for (nsIFrame* current = aFrame; current; current = current->GetParent()) {
|
|
if ((sf = do_QueryFrame(current))) {
|
|
scrollFrame = 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();
|
|
{
|
|
const auto& disp = *scrollFrame->StyleDisplay();
|
|
if (disp.mOverflowClipBoxBlock == StyleOverflowClipBox::ContentBox ||
|
|
disp.mOverflowClipBoxInline == StyleOverflowClipBox::ContentBox) {
|
|
const WritingMode wm = scrollFrame->GetWritingMode();
|
|
const bool cbH = (wm.IsVertical() ? disp.mOverflowClipBoxBlock
|
|
: disp.mOverflowClipBoxInline) ==
|
|
StyleOverflowClipBox::ContentBox;
|
|
const bool cbV = (wm.IsVertical() ? disp.mOverflowClipBoxInline
|
|
: disp.mOverflowClipBoxBlock) ==
|
|
StyleOverflowClipBox::ContentBox;
|
|
nsMargin padding = scrollFrame->GetUsedPadding();
|
|
if (!cbH) {
|
|
padding.left = padding.right = 0;
|
|
}
|
|
if (!cbV) {
|
|
padding.top = padding.bottom = 0;
|
|
}
|
|
clipRect.Deflate(padding);
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
[[nodiscard]] static nsIFrame* GetContainingBlockIfNeeded(nsIFrame* aFrame) {
|
|
if (aFrame->IsBlockOutside() || aFrame->IsBlockFrameOrSubclass()) {
|
|
return nullptr;
|
|
}
|
|
return aFrame->GetContainingBlock();
|
|
}
|
|
|
|
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;
|
|
if (mHiddenDuringSelection && aVisibility) {
|
|
RemoveForceHide();
|
|
mHiddenDuringSelection = false;
|
|
}
|
|
SchedulePaint();
|
|
}
|
|
|
|
void nsCaret::UpdateCaretPositionFromSelectionIfNeeded() {
|
|
if (mFixedCaretPosition) {
|
|
return;
|
|
}
|
|
CaretPosition newPos = CaretPositionFor(GetSelection());
|
|
if (newPos == mCaretPosition) {
|
|
return;
|
|
}
|
|
mCaretPosition = 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.
|
|
if (!mShowDuringSelection &&
|
|
!aDomSel->IsCollapsed() != mHiddenDuringSelection) {
|
|
if (mHiddenDuringSelection) {
|
|
RemoveForceHide();
|
|
} else {
|
|
AddForceHide();
|
|
}
|
|
mHiddenDuringSelection = !mHiddenDuringSelection;
|
|
}
|
|
|
|
// 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::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);
|
|
|
|
mIsBlinkOn = true;
|
|
|
|
if (mReadOnly || !IsVisible()) {
|
|
StopBlinking();
|
|
return;
|
|
}
|
|
|
|
const int32_t oldBlinkTime = mBlinkTime;
|
|
mBlinkTime = LookAndFeel::CaretBlinkTime();
|
|
if (mBlinkTime <= 0) {
|
|
StopBlinking();
|
|
return;
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
void nsCaret::StopBlinking() {
|
|
if (mBlinkTimer) {
|
|
mBlinkTimer->Cancel();
|
|
mLastBlinkTimerReset = TimeStamp();
|
|
}
|
|
}
|
|
|
|
size_t nsCaret::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
|
|
size_t total = aMallocSizeOf(this);
|
|
if (mPresShell) {
|
|
// We only want the size of the nsWeakReference object, not the PresShell
|
|
// (since we don't own the PresShell).
|
|
total += mPresShell->SizeOfOnlyThis(aMallocSizeOf);
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
}
|