mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-11 16:32:59 +00:00
25c0d10932
Sorry this is not a particularly easy patch to review. But it should be mostly straight-forward. I kept Document::Dispatch mostly for convenience, but could be cleaned-up too / changed by SchedulerGroup::Dispatch. Similarly maybe that can just be NS_DispatchToMainThread if we add an NS_IsMainThread check there or something (to preserve shutdown semantics). Differential Revision: https://phabricator.services.mozilla.com/D190450
1000 lines
34 KiB
C++
1000 lines
34 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/gfx/2D.h"
|
|
#include "mozilla/intl/BidiEmbeddingLevel.h"
|
|
#include "mozilla/StaticPrefs_bidi.h"
|
|
#include "nsCOMPtr.h"
|
|
#include "nsFontMetrics.h"
|
|
#include "nsITimer.h"
|
|
#include "nsFrameSelection.h"
|
|
#include "nsIFrame.h"
|
|
#include "nsIScrollableFrame.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"
|
|
|
|
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;
|
|
|
|
/**
|
|
* Find the first frame in an in-order traversal of the frame subtree rooted
|
|
* at aFrame which is either a text frame logically at the end of a line,
|
|
* or which is aStopAtFrame. Return null if no such frame is found. We don't
|
|
* descend into the children of non-eLineParticipant frames.
|
|
*/
|
|
static nsIFrame* CheckForTrailingTextFrameRecursive(nsIFrame* aFrame,
|
|
nsIFrame* aStopAtFrame) {
|
|
if (aFrame == aStopAtFrame ||
|
|
((aFrame->IsTextFrame() &&
|
|
(static_cast<nsTextFrame*>(aFrame))->IsAtEndOfLine())))
|
|
return aFrame;
|
|
if (!aFrame->IsFrameOfType(nsIFrame::eLineParticipant)) return nullptr;
|
|
|
|
for (nsIFrame* f : aFrame->PrincipalChildList()) {
|
|
nsIFrame* r = CheckForTrailingTextFrameRecursive(f, aStopAtFrame);
|
|
if (r) return r;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static nsLineBox* FindContainingLine(nsIFrame* aFrame) {
|
|
while (aFrame && aFrame->IsFrameOfType(nsIFrame::eLineParticipant)) {
|
|
nsIFrame* parent = aFrame->GetParent();
|
|
nsBlockFrame* blockParent = do_QueryFrame(parent);
|
|
if (blockParent) {
|
|
bool isValid;
|
|
nsBlockInFlowLineIterator iter(blockParent, aFrame, &isValid);
|
|
return isValid ? iter.GetLine().get() : nullptr;
|
|
}
|
|
aFrame = parent;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static void AdjustCaretFrameForLineEnd(nsIFrame** aFrame, int32_t* aOffset) {
|
|
nsLineBox* line = FindContainingLine(*aFrame);
|
|
if (!line) {
|
|
return;
|
|
}
|
|
int32_t count = line->GetChildCount();
|
|
for (nsIFrame* f = line->mFirstChild; count > 0;
|
|
--count, f = f->GetNextSibling()) {
|
|
nsIFrame* r = CheckForTrailingTextFrameRecursive(f, *aFrame);
|
|
if (r == *aFrame) {
|
|
return;
|
|
}
|
|
if (r) {
|
|
// We found our frame, but we may not be able to properly paint the caret
|
|
// if -moz-user-modify differs from our actual frame.
|
|
MOZ_ASSERT(r->IsTextFrame(), "Expected text frame");
|
|
*aFrame = r;
|
|
*aOffset = (static_cast<nsTextFrame*>(r))->GetContentEnd();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
nsCaret::nsCaret()
|
|
: mOverrideOffset(0),
|
|
mBlinkCount(-1),
|
|
mBlinkRate(0),
|
|
mHideCount(0),
|
|
mIsBlinkOn(false),
|
|
mVisible(false),
|
|
mReadOnly(false),
|
|
mShowDuringSelection(false),
|
|
mIgnoreUserModify(true) {}
|
|
|
|
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");
|
|
|
|
mShowDuringSelection =
|
|
LookAndFeel::GetInt(LookAndFeel::IntID::ShowCaretDuringSelection,
|
|
mShowDuringSelection ? 1 : 0) != 0;
|
|
|
|
RefPtr<Selection> selection =
|
|
aPresShell->GetSelection(nsISelectionController::SELECTION_NORMAL);
|
|
if (!selection) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
selection->AddSelectionListener(this);
|
|
mDomSelectionWeak = selection;
|
|
|
|
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;
|
|
|
|
mOverrideContent = nullptr;
|
|
}
|
|
|
|
NS_IMPL_ISUPPORTS(nsCaret, nsISelectionListener)
|
|
|
|
Selection* nsCaret::GetSelection() { return mDomSelectionWeak; }
|
|
|
|
void nsCaret::SetSelection(Selection* aDOMSel) {
|
|
MOZ_ASSERT(aDOMSel);
|
|
mDomSelectionWeak = aDOMSel;
|
|
ResetBlinking();
|
|
SchedulePaint(aDOMSel);
|
|
}
|
|
|
|
void nsCaret::SetVisible(bool inMakeVisible) {
|
|
mVisible = inMakeVisible;
|
|
mIgnoreUserModify = mVisible;
|
|
ResetBlinking();
|
|
SchedulePaint();
|
|
}
|
|
|
|
void nsCaret::AddForceHide() {
|
|
MOZ_ASSERT(mHideCount < UINT32_MAX);
|
|
if (++mHideCount > 1) {
|
|
return;
|
|
}
|
|
ResetBlinking();
|
|
SchedulePaint();
|
|
}
|
|
|
|
void nsCaret::RemoveForceHide() {
|
|
if (!mHideCount || --mHideCount) {
|
|
return;
|
|
}
|
|
ResetBlinking();
|
|
SchedulePaint();
|
|
}
|
|
|
|
void nsCaret::SetCaretReadOnly(bool inMakeReadonly) {
|
|
mReadOnly = inMakeReadonly;
|
|
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;
|
|
nsIScrollableFrame* 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;
|
|
}
|
|
|
|
nsIFrame* nsCaret::GetFrameAndOffset(const Selection* aSelection,
|
|
nsINode* aOverrideNode,
|
|
int32_t aOverrideOffset,
|
|
int32_t* aFrameOffset,
|
|
nsIFrame** aUnadjustedFrame) {
|
|
if (aUnadjustedFrame) {
|
|
*aUnadjustedFrame = nullptr;
|
|
}
|
|
|
|
nsINode* focusNode;
|
|
int32_t focusOffset;
|
|
|
|
if (aOverrideNode) {
|
|
focusNode = aOverrideNode;
|
|
focusOffset = aOverrideOffset;
|
|
} else if (aSelection) {
|
|
focusNode = aSelection->GetFocusNode();
|
|
focusOffset = aSelection->FocusOffset();
|
|
} else {
|
|
return nullptr;
|
|
}
|
|
|
|
if (!focusNode || !focusNode->IsContent() || !aSelection) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsIContent* contentNode = focusNode->AsContent();
|
|
nsFrameSelection* frameSelection = aSelection->GetFrameSelection();
|
|
BidiEmbeddingLevel bidiLevel = frameSelection->GetCaretBidiLevel();
|
|
|
|
return nsCaret::GetCaretFrameForNodeOffset(
|
|
frameSelection, contentNode, focusOffset, frameSelection->GetHint(),
|
|
bidiLevel, aUnadjustedFrame, aFrameOffset);
|
|
}
|
|
|
|
/* static */
|
|
nsIFrame* nsCaret::GetGeometry(const Selection* aSelection, nsRect* aRect) {
|
|
int32_t frameOffset;
|
|
nsIFrame* frame = GetFrameAndOffset(aSelection, nullptr, 0, &frameOffset);
|
|
if (frame) {
|
|
*aRect = GetGeometryForFrame(frame, frameOffset, nullptr);
|
|
}
|
|
return frame;
|
|
}
|
|
|
|
[[nodiscard]] static nsIFrame* GetContainingBlockIfNeeded(nsIFrame* aFrame) {
|
|
if (aFrame->IsBlockOutside() || aFrame->IsBlockFrameOrSubclass()) {
|
|
return nullptr;
|
|
}
|
|
return aFrame->GetContainingBlock();
|
|
}
|
|
|
|
void nsCaret::SchedulePaint(Selection* aSelection) {
|
|
Selection* selection;
|
|
if (aSelection) {
|
|
selection = aSelection;
|
|
} else {
|
|
selection = GetSelection();
|
|
}
|
|
|
|
int32_t frameOffset;
|
|
nsIFrame* frame = GetFrameAndOffset(selection, mOverrideContent,
|
|
mOverrideOffset, &frameOffset);
|
|
if (!frame) {
|
|
return;
|
|
}
|
|
|
|
if (nsIFrame* cb = GetContainingBlockIfNeeded(frame)) {
|
|
cb->SchedulePaint();
|
|
} else {
|
|
frame->SchedulePaint();
|
|
}
|
|
}
|
|
|
|
void nsCaret::SetVisibilityDuringSelection(bool aVisibility) {
|
|
mShowDuringSelection = aVisibility;
|
|
SchedulePaint();
|
|
}
|
|
|
|
void nsCaret::SetCaretPosition(nsINode* aNode, int32_t aOffset) {
|
|
mOverrideContent = aNode;
|
|
mOverrideOffset = aOffset;
|
|
|
|
ResetBlinking();
|
|
SchedulePaint();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
*aCaretRect = nsLayoutUtils::TransformFrameRectToAncestor(aFrame, *aCaretRect,
|
|
containingBlock);
|
|
*aHookRect = nsLayoutUtils::TransformFrameRectToAncestor(aFrame, *aHookRect,
|
|
containingBlock);
|
|
return containingBlock;
|
|
}
|
|
|
|
nsIFrame* nsCaret::GetPaintGeometry(nsRect* aCaretRect, nsRect* aHookRect,
|
|
nscolor* aCaretColor) {
|
|
// 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();
|
|
|
|
int32_t frameOffset;
|
|
nsIFrame* unadjustedFrame = nullptr;
|
|
nsIFrame* frame =
|
|
GetFrameAndOffset(GetSelection(), mOverrideContent, mOverrideOffset,
|
|
&frameOffset, &unadjustedFrame);
|
|
MOZ_ASSERT(!!frame == !!unadjustedFrame);
|
|
if (!frame) {
|
|
return nullptr;
|
|
}
|
|
|
|
// 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.
|
|
const nsStyleUI* ui = unadjustedFrame->StyleUI();
|
|
if ((!mIgnoreUserModify && ui->UserModify() == StyleUserModify::ReadOnly) ||
|
|
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);
|
|
}
|
|
|
|
ComputeCaretRects(frame, frameOffset, aCaretRect, aHookRect);
|
|
return MapToContainingBlock(frame, aCaretRect, aHookRect);
|
|
}
|
|
|
|
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) {
|
|
// Note that aDomSel, per the comment below may not be the same as our
|
|
// selection, but that's OK since if that is the case, it wouldn't have
|
|
// mattered what IsVisible() returns here, so we just opt for checking
|
|
// the selection later down below.
|
|
if ((aReason & nsISelectionListener::MOUSEUP_REASON) ||
|
|
!IsVisible(aDomSel)) // this wont do
|
|
return NS_OK;
|
|
|
|
// 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), then there
|
|
// is nothing to do!
|
|
|
|
if (mDomSelectionWeak != aDomSel) return NS_OK;
|
|
|
|
ResetBlinking();
|
|
SchedulePaint(aDomSel);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void nsCaret::ResetBlinking() {
|
|
using IntID = LookAndFeel::IntID;
|
|
|
|
// The default caret blinking rate (in ms of blinking interval)
|
|
constexpr uint32_t kDefaultBlinkRate = 500;
|
|
// The default caret blinking count (-1 for "never stop blinking")
|
|
constexpr int32_t kDefaultBlinkCount = -1;
|
|
|
|
mIsBlinkOn = true;
|
|
|
|
if (mReadOnly || !mVisible || mHideCount) {
|
|
StopBlinking();
|
|
return;
|
|
}
|
|
|
|
auto blinkRate =
|
|
LookAndFeel::GetInt(IntID::CaretBlinkTime, kDefaultBlinkRate);
|
|
|
|
if (blinkRate > 0) {
|
|
// Make sure to reset the remaining blink count even if the blink rate
|
|
// hasn't changed.
|
|
mBlinkCount =
|
|
LookAndFeel::GetInt(IntID::CaretBlinkCount, kDefaultBlinkCount);
|
|
}
|
|
|
|
if (mBlinkRate == blinkRate) {
|
|
// If the rate hasn't changed, then there is nothing else to do.
|
|
return;
|
|
}
|
|
|
|
mBlinkRate = blinkRate;
|
|
|
|
if (mBlinkTimer) {
|
|
mBlinkTimer->Cancel();
|
|
} else {
|
|
mBlinkTimer = NS_NewTimer(GetMainThreadSerialEventTarget());
|
|
if (!mBlinkTimer) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (blinkRate > 0) {
|
|
mBlinkTimer->InitWithNamedFuncCallback(CaretBlinkCallback, this, blinkRate,
|
|
nsITimer::TYPE_REPEATING_SLACK,
|
|
"nsCaret::CaretBlinkCallback_timer");
|
|
}
|
|
}
|
|
|
|
void nsCaret::StopBlinking() {
|
|
if (mBlinkTimer) {
|
|
mBlinkTimer->Cancel();
|
|
mBlinkRate = 0;
|
|
}
|
|
}
|
|
|
|
nsIFrame* nsCaret::GetCaretFrameForNodeOffset(nsFrameSelection* aFrameSelection,
|
|
nsIContent* aContentNode,
|
|
int32_t aOffset,
|
|
CaretAssociationHint aFrameHint,
|
|
BidiEmbeddingLevel aBidiLevel,
|
|
nsIFrame** aReturnUnadjustedFrame,
|
|
int32_t* aReturnOffset) {
|
|
if (!aFrameSelection) {
|
|
return nullptr;
|
|
}
|
|
|
|
PresShell* presShell = aFrameSelection->GetPresShell();
|
|
if (!presShell) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (!aContentNode || !aContentNode->IsInComposedDoc() ||
|
|
presShell->GetDocument() != aContentNode->GetComposedDoc()) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsIFrame* theFrame = nullptr;
|
|
int32_t theFrameOffset = 0;
|
|
|
|
theFrame = nsFrameSelection::GetFrameForNodeOffset(
|
|
aContentNode, aOffset, aFrameHint, &theFrameOffset);
|
|
if (!theFrame) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (aReturnUnadjustedFrame) {
|
|
*aReturnUnadjustedFrame = theFrame;
|
|
}
|
|
|
|
if (nsFrameSelection::AdjustFrameForLineStart(theFrame, theFrameOffset)) {
|
|
aFrameSelection->SetHint(CARET_ASSOCIATE_AFTER);
|
|
} else {
|
|
// if theFrame is after a text frame that's logically at the end of the line
|
|
// (e.g. if theFrame is a <br> frame), then put the caret at the end of
|
|
// that text frame instead. This way, the caret will be positioned as if
|
|
// trailing whitespace was not trimmed.
|
|
AdjustCaretFrameForLineEnd(&theFrame, &theFrameOffset);
|
|
}
|
|
|
|
// Mamdouh : modification of the caret to work at rtl and ltr with Bidi
|
|
//
|
|
// Direction Style from visibility->mDirection
|
|
// ------------------
|
|
if (theFrame->PresContext()->BidiEnabled()) {
|
|
// If there has been a reflow, take the caret Bidi level to be the level of
|
|
// the current frame
|
|
if (aBidiLevel & BIDI_LEVEL_UNDEFINED) {
|
|
aBidiLevel = theFrame->GetEmbeddingLevel();
|
|
}
|
|
|
|
nsIFrame* frameBefore;
|
|
nsIFrame* frameAfter;
|
|
BidiEmbeddingLevel
|
|
levelBefore; // Bidi level of the character before the caret
|
|
BidiEmbeddingLevel
|
|
levelAfter; // Bidi level of the character after the caret
|
|
|
|
auto [start, end] = theFrame->GetOffsets();
|
|
if (start == 0 || end == 0 || start == theFrameOffset ||
|
|
end == theFrameOffset) {
|
|
nsPrevNextBidiLevels levels =
|
|
aFrameSelection->GetPrevNextBidiLevels(aContentNode, aOffset, false);
|
|
|
|
/* Boundary condition, we need to know the Bidi levels of the characters
|
|
* before and after the caret */
|
|
if (levels.mFrameBefore || levels.mFrameAfter) {
|
|
frameBefore = levels.mFrameBefore;
|
|
frameAfter = levels.mFrameAfter;
|
|
levelBefore = levels.mLevelBefore;
|
|
levelAfter = levels.mLevelAfter;
|
|
|
|
if ((levelBefore != levelAfter) || (aBidiLevel != levelBefore)) {
|
|
aBidiLevel = std::max(aBidiLevel,
|
|
std::min(levelBefore, levelAfter)); // rule c3
|
|
aBidiLevel = std::min(aBidiLevel,
|
|
std::max(levelBefore, levelAfter)); // rule c4
|
|
if (aBidiLevel == levelBefore || // rule c1
|
|
(aBidiLevel > levelBefore && aBidiLevel < levelAfter &&
|
|
aBidiLevel.IsSameDirection(levelBefore)) || // rule c5
|
|
(aBidiLevel < levelBefore && aBidiLevel > levelAfter &&
|
|
aBidiLevel.IsSameDirection(levelBefore))) // rule c9
|
|
{
|
|
if (theFrame != frameBefore) {
|
|
if (frameBefore) { // if there is a frameBefore, move into it
|
|
theFrame = frameBefore;
|
|
std::tie(start, end) = theFrame->GetOffsets();
|
|
theFrameOffset = end;
|
|
} else {
|
|
// if there is no frameBefore, we must be at the beginning of
|
|
// the line so we stay with the current frame. Exception: when
|
|
// the first frame on the line has a different Bidi level from
|
|
// the paragraph level, there is no real frame for the caret to
|
|
// be in. We have to find the visually first frame on the line.
|
|
BidiEmbeddingLevel baseLevel = frameAfter->GetBaseLevel();
|
|
if (baseLevel != levelAfter) {
|
|
PeekOffsetStruct pos(eSelectBeginLine, eDirPrevious, 0,
|
|
nsPoint(0, 0),
|
|
{PeekOffsetOption::ScrollViewStop,
|
|
PeekOffsetOption::Visual});
|
|
if (NS_SUCCEEDED(frameAfter->PeekOffset(&pos))) {
|
|
theFrame = pos.mResultFrame;
|
|
theFrameOffset = pos.mContentOffset;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (aBidiLevel == levelAfter || // rule c2
|
|
(aBidiLevel > levelBefore && aBidiLevel < levelAfter &&
|
|
aBidiLevel.IsSameDirection(levelAfter)) || // rule c6
|
|
(aBidiLevel < levelBefore && aBidiLevel > levelAfter &&
|
|
aBidiLevel.IsSameDirection(levelAfter))) // rule c10
|
|
{
|
|
if (theFrame != frameAfter) {
|
|
if (frameAfter) {
|
|
// if there is a frameAfter, move into it
|
|
theFrame = frameAfter;
|
|
std::tie(start, end) = theFrame->GetOffsets();
|
|
theFrameOffset = start;
|
|
} else {
|
|
// if there is no frameAfter, we must be at the end of the line
|
|
// so we stay with the current frame.
|
|
// Exception: when the last frame on the line has a different
|
|
// Bidi level from the paragraph level, there is no real frame
|
|
// for the caret to be in. We have to find the visually last
|
|
// frame on the line.
|
|
BidiEmbeddingLevel baseLevel = frameBefore->GetBaseLevel();
|
|
if (baseLevel != levelBefore) {
|
|
PeekOffsetStruct pos(eSelectEndLine, eDirNext, 0,
|
|
nsPoint(0, 0),
|
|
{PeekOffsetOption::ScrollViewStop,
|
|
PeekOffsetOption::Visual});
|
|
if (NS_SUCCEEDED(frameBefore->PeekOffset(&pos))) {
|
|
theFrame = pos.mResultFrame;
|
|
theFrameOffset = pos.mContentOffset;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (aBidiLevel > levelBefore &&
|
|
aBidiLevel < levelAfter && // rule c7/8
|
|
// before and after have the same parity
|
|
levelBefore.IsSameDirection(levelAfter) &&
|
|
// caret has different parity
|
|
!aBidiLevel.IsSameDirection(levelAfter)) {
|
|
if (NS_SUCCEEDED(aFrameSelection->GetFrameFromLevel(
|
|
frameAfter, eDirNext, aBidiLevel, &theFrame))) {
|
|
std::tie(start, end) = theFrame->GetOffsets();
|
|
levelAfter = theFrame->GetEmbeddingLevel();
|
|
if (aBidiLevel.IsRTL()) {
|
|
// c8: caret to the right of the rightmost character
|
|
theFrameOffset = levelAfter.IsRTL() ? start : end;
|
|
} else {
|
|
// c7: caret to the left of the leftmost character
|
|
theFrameOffset = levelAfter.IsRTL() ? end : start;
|
|
}
|
|
}
|
|
} else if (aBidiLevel < levelBefore &&
|
|
aBidiLevel > levelAfter && // rule c11/12
|
|
// before and after have the same parity
|
|
levelBefore.IsSameDirection(levelAfter) &&
|
|
// caret has different parity
|
|
!aBidiLevel.IsSameDirection(levelAfter)) {
|
|
if (NS_SUCCEEDED(aFrameSelection->GetFrameFromLevel(
|
|
frameBefore, eDirPrevious, aBidiLevel, &theFrame))) {
|
|
std::tie(start, end) = theFrame->GetOffsets();
|
|
levelBefore = theFrame->GetEmbeddingLevel();
|
|
if (aBidiLevel.IsRTL()) {
|
|
// c12: caret to the left of the leftmost character
|
|
theFrameOffset = levelBefore.IsRTL() ? end : start;
|
|
} else {
|
|
// c11: caret to the right of the rightmost character
|
|
theFrameOffset = levelBefore.IsRTL() ? start : end;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
*aReturnOffset = theFrameOffset;
|
|
return theFrame;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool nsCaret::IsMenuPopupHidingCaret() {
|
|
// Check if there are open popups.
|
|
nsXULPopupManager* popMgr = nsXULPopupManager::GetInstance();
|
|
nsTArray<nsIFrame*> popups;
|
|
popMgr->GetVisiblePopups(popups);
|
|
|
|
if (popups.Length() == 0)
|
|
return false; // No popups, so caret can't be hidden by them.
|
|
|
|
// Get the selection focus content, that's where the caret would
|
|
// go if it was drawn.
|
|
if (!mDomSelectionWeak) {
|
|
return true; // No selection/caret to draw.
|
|
}
|
|
nsCOMPtr<nsIContent> caretContent =
|
|
nsIContent::FromNodeOrNull(mDomSelectionWeak->GetFocusNode());
|
|
if (!caretContent) return true; // No selection/caret to draw.
|
|
|
|
// If there's a menu popup open before the popup with
|
|
// the caret, don't show the caret.
|
|
for (uint32_t i = 0; i < popups.Length(); i++) {
|
|
nsMenuPopupFrame* popupFrame = static_cast<nsMenuPopupFrame*>(popups[i]);
|
|
nsIContent* popupContent = popupFrame->GetContent();
|
|
|
|
if (caretContent->IsInclusiveDescendantOf(popupContent)) {
|
|
// The caret is in this popup. There were no menu popups before this
|
|
// popup, so don't hide the caret.
|
|
return false;
|
|
}
|
|
|
|
if (popupFrame->GetPopupType() == widget::PopupType::Menu &&
|
|
!popupFrame->IsContextMenu()) {
|
|
// This is an open menu popup. It does not contain the caret (else we'd
|
|
// have returned above). Even if the caret is in a subsequent popup,
|
|
// or another document/frame, it should be hidden.
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// There are no open menu popups, no need to hide the caret.
|
|
return false;
|
|
}
|
|
|
|
void nsCaret::ComputeCaretRects(nsIFrame* aFrame, int32_t aFrameOffset,
|
|
nsRect* aCaretRect, nsRect* 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 = reinterpret_cast<nsCaret*>(aClosure);
|
|
if (!theCaret) {
|
|
return;
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
void nsCaret::SetIgnoreUserModify(bool aIgnoreUserModify) {
|
|
mIgnoreUserModify = aIgnoreUserModify;
|
|
SchedulePaint();
|
|
}
|