Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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/. */
/* rendering object for textual content of elements */
#include "nsTextFrame.h"
#include "gfx2DGlue.h"
#include "gfxUtils.h"
#include "mozilla/Attributes.h"
#include "mozilla/CaretAssociationHint.h"
#include "mozilla/ComputedStyle.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/Likely.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/StaticPresData.h"
#include "mozilla/SVGTextFrame.h"
#include "mozilla/TextEditor.h"
#include "mozilla/TextEvents.h"
#include "mozilla/BinarySearch.h"
#include "mozilla/IntegerRange.h"
#include "mozilla/Unused.h"
#include "mozilla/PodOperations.h"
#include "mozilla/dom/PerformanceMainThread.h"
#include "nsCOMPtr.h"
#include "nsBlockFrame.h"
#include "nsFontMetrics.h"
#include "nsSplittableFrame.h"
#include "nsLineLayout.h"
#include "nsString.h"
#include "nsUnicharUtils.h"
#include "nsPresContext.h"
#include "nsIContent.h"
#include "nsStyleConsts.h"
#include "nsStyleStruct.h"
#include "nsStyleStructInlines.h"
#include "nsCoord.h"
#include "gfxContext.h"
#include "nsTArray.h"
#include "nsCSSPseudoElements.h"
#include "nsCSSFrameConstructor.h"
#include "nsCompatibility.h"
#include "nsCSSColorUtils.h"
#include "nsLayoutUtils.h"
#include "nsDisplayList.h"
#include "nsIFrame.h"
#include "nsIMathMLFrame.h"
#include "nsFirstLetterFrame.h"
#include "nsPlaceholderFrame.h"
#include "nsTextFrameUtils.h"
#include "nsTextPaintStyle.h"
#include "nsTextRunTransformations.h"
#include "MathMLTextRunFactory.h"
#include "nsUnicodeProperties.h"
#include "nsStyleUtil.h"
#include "nsRubyFrame.h"
#include "PresShellInlines.h"
#include "TextDrawTarget.h"
#include "nsTextFragment.h"
#include "nsGkAtoms.h"
#include "nsFrameSelection.h"
#include "nsRange.h"
#include "nsCSSRendering.h"
#include "nsContentUtils.h"
#include "nsLineBreaker.h"
#include "nsIFrameInlines.h"
#include "mozilla/intl/Bidi.h"
#include "mozilla/intl/Segmenter.h"
#include "mozilla/intl/UnicodeProperties.h"
#include "mozilla/ServoStyleSet.h"
#include <algorithm>
#include <limits>
#include <type_traits>
#ifdef ACCESSIBILITY
# include "nsAccessibilityService.h"
#endif
#include "nsPrintfCString.h"
#include "mozilla/gfx/DrawTargetRecording.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/dom/Element.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/ProfilerLabels.h"
#ifdef DEBUG
# undef NOISY_REFLOW
# undef NOISY_TRIM
#else
# undef NOISY_REFLOW
# undef NOISY_TRIM
#endif
#ifdef DrawText
# undef DrawText
#endif
using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::gfx;
typedef mozilla::layout::TextDrawTarget TextDrawTarget;
static bool NeedsToMaskPassword(nsTextFrame* aFrame) {
MOZ_ASSERT(aFrame);
MOZ_ASSERT(aFrame->GetContent());
if (!aFrame->GetContent()->HasFlag(NS_MAYBE_MASKED)) {
return false;
}
nsIFrame* frame =
nsLayoutUtils::GetClosestFrameOfType(aFrame, LayoutFrameType::TextInput);
MOZ_ASSERT(frame, "How do we have a masked text node without a text input?");
return !frame || !frame->GetContent()->AsElement()->State().HasState(
ElementState::REVEALED);
}
struct TabWidth {
TabWidth(uint32_t aOffset, uint32_t aWidth)
: mOffset(aOffset), mWidth(float(aWidth)) {}
uint32_t mOffset; // DOM offset relative to the current frame's offset.
float mWidth; // extra space to be added at this position (in app units)
};
struct nsTextFrame::TabWidthStore {
explicit TabWidthStore(int32_t aValidForContentOffset)
: mLimit(0), mValidForContentOffset(aValidForContentOffset) {}
// Apply tab widths to the aSpacing array, which corresponds to characters
// beginning at aOffset and has length aLength. (Width records outside this
// range will be ignored.)
void ApplySpacing(gfxTextRun::PropertyProvider::Spacing* aSpacing,
uint32_t aOffset, uint32_t aLength);
// Offset up to which tabs have been measured; positions beyond this have not
// been calculated yet but may be appended if needed later. It's a DOM
// offset relative to the current frame's offset.
uint32_t mLimit;
// Need to recalc tab offsets if frame content offset differs from this.
int32_t mValidForContentOffset;
// A TabWidth record for each tab character measured so far.
nsTArray<TabWidth> mWidths;
};
namespace {
struct TabwidthAdaptor {
const nsTArray<TabWidth>& mWidths;
explicit TabwidthAdaptor(const nsTArray<TabWidth>& aWidths)
: mWidths(aWidths) {}
uint32_t operator[](size_t aIdx) const { return mWidths[aIdx].mOffset; }
};
} // namespace
void nsTextFrame::TabWidthStore::ApplySpacing(
gfxTextRun::PropertyProvider::Spacing* aSpacing, uint32_t aOffset,
uint32_t aLength) {
size_t i = 0;
const size_t len = mWidths.Length();
// If aOffset is non-zero, do a binary search to find where to start
// processing the tab widths, in case the list is really long. (See bug
// 953247.)
// We need to start from the first entry where mOffset >= aOffset.
if (aOffset > 0) {
mozilla::BinarySearch(TabwidthAdaptor(mWidths), 0, len, aOffset, &i);
}
uint32_t limit = aOffset + aLength;
while (i < len) {
const TabWidth& tw = mWidths[i];
if (tw.mOffset >= limit) {
break;
}
aSpacing[tw.mOffset - aOffset].mAfter += tw.mWidth;
i++;
}
}
NS_DECLARE_FRAME_PROPERTY_DELETABLE(TabWidthProperty,
nsTextFrame::TabWidthStore)
NS_DECLARE_FRAME_PROPERTY_WITHOUT_DTOR(OffsetToFrameProperty, nsTextFrame)
NS_DECLARE_FRAME_PROPERTY_RELEASABLE(UninflatedTextRunProperty, gfxTextRun)
NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(FontSizeInflationProperty, float)
NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(HangableWhitespaceProperty, nscoord)
NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(TrimmableWhitespaceProperty,
gfxTextRun::TrimmableWS)
struct nsTextFrame::PaintTextSelectionParams : nsTextFrame::PaintTextParams {
Point textBaselinePt;
PropertyProvider* provider = nullptr;
Range contentRange;
nsTextPaintStyle* textPaintStyle = nullptr;
Range glyphRange;
explicit PaintTextSelectionParams(const PaintTextParams& aParams)
: PaintTextParams(aParams) {}
};
struct nsTextFrame::DrawTextRunParams {
gfxContext* context;
mozilla::gfx::PaletteCache& paletteCache;
PropertyProvider* provider = nullptr;
gfxFloat* advanceWidth = nullptr;
mozilla::SVGContextPaint* contextPaint = nullptr;
DrawPathCallbacks* callbacks = nullptr;
nscolor textColor = NS_RGBA(0, 0, 0, 0);
nscolor textStrokeColor = NS_RGBA(0, 0, 0, 0);
nsAtom* fontPalette = nullptr;
float textStrokeWidth = 0.0f;
bool drawSoftHyphen = false;
bool hasTextShadow = false;
bool paintingShadows = false;
DrawTextRunParams(gfxContext* aContext,
mozilla::gfx::PaletteCache& aPaletteCache)
: context(aContext), paletteCache(aPaletteCache) {}
};
struct nsTextFrame::ClipEdges {
ClipEdges(const nsIFrame* aFrame, const nsPoint& aToReferenceFrame,
nscoord aVisIStartEdge, nscoord aVisIEndEdge) {
nsRect r = aFrame->ScrollableOverflowRect() + aToReferenceFrame;
if (aFrame->GetWritingMode().IsVertical()) {
mVisIStart = aVisIStartEdge > 0 ? r.y + aVisIStartEdge : nscoord_MIN;
mVisIEnd = aVisIEndEdge > 0
? std::max(r.YMost() - aVisIEndEdge, mVisIStart)
: nscoord_MAX;
} else {
mVisIStart = aVisIStartEdge > 0 ? r.x + aVisIStartEdge : nscoord_MIN;
mVisIEnd = aVisIEndEdge > 0
? std::max(r.XMost() - aVisIEndEdge, mVisIStart)
: nscoord_MAX;
}
}
void Intersect(nscoord* aVisIStart, nscoord* aVisISize) const {
nscoord end = *aVisIStart + *aVisISize;
*aVisIStart = std::max(*aVisIStart, mVisIStart);
*aVisISize = std::max(std::min(end, mVisIEnd) - *aVisIStart, 0);
}
nscoord mVisIStart;
nscoord mVisIEnd;
};
struct nsTextFrame::DrawTextParams : nsTextFrame::DrawTextRunParams {
Point framePt;
LayoutDeviceRect dirtyRect;
const nsTextPaintStyle* textStyle = nullptr;
const ClipEdges* clipEdges = nullptr;
const nscolor* decorationOverrideColor = nullptr;
Range glyphRange;
DrawTextParams(gfxContext* aContext,
mozilla::gfx::PaletteCache& aPaletteCache)
: DrawTextRunParams(aContext, aPaletteCache) {}
};
struct nsTextFrame::PaintShadowParams {
gfxTextRun::Range range;
LayoutDeviceRect dirtyRect;
Point framePt;
Point textBaselinePt;
gfxContext* context;
DrawPathCallbacks* callbacks = nullptr;
nscolor foregroundColor = NS_RGBA(0, 0, 0, 0);
const ClipEdges* clipEdges = nullptr;
PropertyProvider* provider = nullptr;
nscoord leftSideOffset = 0;
explicit PaintShadowParams(const PaintTextParams& aParams)
: dirtyRect(aParams.dirtyRect),
framePt(aParams.framePt),
context(aParams.context) {}
};
/**
* A glyph observer for the change of a font glyph in a text run.
*
* This is stored in {Simple, Complex}TextRunUserData.
*/
class GlyphObserver final : public gfxFont::GlyphChangeObserver {
public:
GlyphObserver(gfxFont* aFont, gfxTextRun* aTextRun)
: gfxFont::GlyphChangeObserver(aFont), mTextRun(aTextRun) {
MOZ_ASSERT(aTextRun->GetUserData());
}
void NotifyGlyphsChanged() override;
private:
gfxTextRun* mTextRun;
};
static const nsFrameState TEXT_REFLOW_FLAGS =
TEXT_FIRST_LETTER | TEXT_START_OF_LINE | TEXT_END_OF_LINE |
TEXT_HYPHEN_BREAK | TEXT_TRIMMED_TRAILING_WHITESPACE |
TEXT_JUSTIFICATION_ENABLED | TEXT_HAS_NONCOLLAPSED_CHARACTERS |
TEXT_SELECTION_UNDERLINE_OVERFLOWED | TEXT_NO_RENDERED_GLYPHS;
static const nsFrameState TEXT_WHITESPACE_FLAGS =
TEXT_IS_ONLY_WHITESPACE | TEXT_ISNOT_ONLY_WHITESPACE;
/*
* Some general notes
*
* Text frames delegate work to gfxTextRun objects. The gfxTextRun object
* transforms text to positioned glyphs. It can report the geometry of the
* glyphs and paint them. Text frames configure gfxTextRuns by providing text,
* spacing, language, and other information.
*
* A gfxTextRun can cover more than one DOM text node. This is necessary to
* get kerning, ligatures and shaping for text that spans multiple text nodes
* but is all the same font.
*
* The userdata for a gfxTextRun object can be:
*
* - A nsTextFrame* in the case a text run maps to only one flow. In this
* case, the textrun's user data pointer is a pointer to mStartFrame for that
* flow, mDOMOffsetToBeforeTransformOffset is zero, and mContentLength is the
* length of the text node.
*
* - A SimpleTextRunUserData in the case a text run maps to one flow, but we
* still have to keep a list of glyph observers.
*
* - A ComplexTextRunUserData in the case a text run maps to multiple flows,
* but we need to keep a list of glyph observers.
*
* - A TextRunUserData in the case a text run maps multiple flows, but it
* doesn't have any glyph observer for changes in SVG fonts.
*
* You can differentiate between the four different cases with the
* IsSimpleFlow and MightHaveGlyphChanges flags.
*
* We go to considerable effort to make sure things work even if in-flow
* siblings have different ComputedStyles (i.e., first-letter and first-line).
*
* Our convention is that unsigned integer character offsets are offsets into
* the transformed string. Signed integer character offsets are offsets into
* the DOM string.
*
* XXX currently we don't handle hyphenated breaks between text frames where the
* hyphen occurs at the end of the first text frame, e.g.
* <b>Kit&shy;</b>ty
*/
/**
* This is our user data for the textrun, when textRun->GetFlags2() has
* IsSimpleFlow set, and also MightHaveGlyphChanges.
*
* This allows having an array of observers if there are fonts whose glyphs
* might change, but also avoid allocation in the simple case that there aren't.
*/
struct SimpleTextRunUserData {
nsTArray<UniquePtr<GlyphObserver>> mGlyphObservers;
nsTextFrame* mFrame;
explicit SimpleTextRunUserData(nsTextFrame* aFrame) : mFrame(aFrame) {}
};
/**
* We use an array of these objects to record which text frames
* are associated with the textrun. mStartFrame is the start of a list of
* text frames. Some sequence of its continuations are covered by the textrun.
* A content textnode can have at most one TextRunMappedFlow associated with it
* for a given textrun.
*
* mDOMOffsetToBeforeTransformOffset is added to DOM offsets for those frames to
* obtain the offset into the before-transformation text of the textrun. It can
* be positive (when a text node starts in the middle of a text run) or negative
* (when a text run starts in the middle of a text node). Of course it can also
* be zero.
*/
struct TextRunMappedFlow {
nsTextFrame* mStartFrame;
int32_t mDOMOffsetToBeforeTransformOffset;
// The text mapped starts at mStartFrame->GetContentOffset() and is this long
uint32_t mContentLength;
};
/**
* This is the type in the gfxTextRun's userdata field in the common case that
* the text run maps to multiple flows, but no fonts have been found with
* animatable glyphs.
*
* This way, we avoid allocating and constructing the extra nsTArray.
*/
struct TextRunUserData {
#ifdef DEBUG
TextRunMappedFlow* mMappedFlows;
#endif
uint32_t mMappedFlowCount;
uint32_t mLastFlowIndex;
};
/**
* This is our user data for the textrun, when textRun->GetFlags2() does not
* have IsSimpleFlow set and has the MightHaveGlyphChanges flag.
*/
struct ComplexTextRunUserData : public TextRunUserData {
nsTArray<UniquePtr<GlyphObserver>> mGlyphObservers;
};
static TextRunUserData* CreateUserData(uint32_t aMappedFlowCount) {
TextRunUserData* data = static_cast<TextRunUserData*>(moz_xmalloc(
sizeof(TextRunUserData) + aMappedFlowCount * sizeof(TextRunMappedFlow)));
#ifdef DEBUG
data->mMappedFlows = reinterpret_cast<TextRunMappedFlow*>(data + 1);
#endif
data->mMappedFlowCount = aMappedFlowCount;
data->mLastFlowIndex = 0;
return data;
}
static void DestroyUserData(TextRunUserData* aUserData) {
if (aUserData) {
free(aUserData);
}
}
static ComplexTextRunUserData* CreateComplexUserData(
uint32_t aMappedFlowCount) {
ComplexTextRunUserData* data = static_cast<ComplexTextRunUserData*>(
moz_xmalloc(sizeof(ComplexTextRunUserData) +
aMappedFlowCount * sizeof(TextRunMappedFlow)));
new (data) ComplexTextRunUserData();
#ifdef DEBUG
data->mMappedFlows = reinterpret_cast<TextRunMappedFlow*>(data + 1);
#endif
data->mMappedFlowCount = aMappedFlowCount;
data->mLastFlowIndex = 0;
return data;
}
static void DestroyComplexUserData(ComplexTextRunUserData* aUserData) {
if (aUserData) {
aUserData->~ComplexTextRunUserData();
free(aUserData);
}
}
static void DestroyTextRunUserData(gfxTextRun* aTextRun) {
MOZ_ASSERT(aTextRun->GetUserData());
if (aTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsSimpleFlow) {
if (aTextRun->GetFlags2() &
nsTextFrameUtils::Flags::MightHaveGlyphChanges) {
delete static_cast<SimpleTextRunUserData*>(aTextRun->GetUserData());
}
} else {
if (aTextRun->GetFlags2() &
nsTextFrameUtils::Flags::MightHaveGlyphChanges) {
DestroyComplexUserData(
static_cast<ComplexTextRunUserData*>(aTextRun->GetUserData()));
} else {
DestroyUserData(static_cast<TextRunUserData*>(aTextRun->GetUserData()));
}
}
aTextRun->ClearFlagBits(nsTextFrameUtils::Flags::MightHaveGlyphChanges);
aTextRun->SetUserData(nullptr);
}
static TextRunMappedFlow* GetMappedFlows(const gfxTextRun* aTextRun) {
MOZ_ASSERT(aTextRun->GetUserData(), "UserData must exist.");
MOZ_ASSERT(!(aTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsSimpleFlow),
"The method should not be called for simple flows.");
TextRunMappedFlow* flows;
if (aTextRun->GetFlags2() & nsTextFrameUtils::Flags::MightHaveGlyphChanges) {
flows = reinterpret_cast<TextRunMappedFlow*>(
static_cast<ComplexTextRunUserData*>(aTextRun->GetUserData()) + 1);
} else {
flows = reinterpret_cast<TextRunMappedFlow*>(
static_cast<TextRunUserData*>(aTextRun->GetUserData()) + 1);
}
MOZ_ASSERT(
static_cast<TextRunUserData*>(aTextRun->GetUserData())->mMappedFlows ==
flows,
"GetMappedFlows should return the same pointer as mMappedFlows.");
return flows;
}
/**
* These are utility functions just for helping with the complexity related with
* the text runs user data.
*/
static nsTextFrame* GetFrameForSimpleFlow(const gfxTextRun* aTextRun) {
MOZ_ASSERT(aTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsSimpleFlow,
"Not so simple flow?");
if (aTextRun->GetFlags2() & nsTextFrameUtils::Flags::MightHaveGlyphChanges) {
return static_cast<SimpleTextRunUserData*>(aTextRun->GetUserData())->mFrame;
}
return static_cast<nsTextFrame*>(aTextRun->GetUserData());
}
/**
* Remove |aTextRun| from the frame continuation chain starting at
* |aStartContinuation| if non-null, otherwise starting at |aFrame|.
* Unmark |aFrame| as a text run owner if it's the frame we start at.
* Return true if |aStartContinuation| is non-null and was found
* in the next-continuation chain of |aFrame|.
*/
static bool ClearAllTextRunReferences(nsTextFrame* aFrame, gfxTextRun* aTextRun,
nsTextFrame* aStartContinuation,
nsFrameState aWhichTextRunState) {
MOZ_ASSERT(aFrame, "null frame");
MOZ_ASSERT(!aStartContinuation ||
(!aStartContinuation->GetTextRun(nsTextFrame::eInflated) ||
aStartContinuation->GetTextRun(nsTextFrame::eInflated) ==
aTextRun) ||
(!aStartContinuation->GetTextRun(nsTextFrame::eNotInflated) ||
aStartContinuation->GetTextRun(nsTextFrame::eNotInflated) ==
aTextRun),
"wrong aStartContinuation for this text run");
if (!aStartContinuation || aStartContinuation == aFrame) {
aFrame->RemoveStateBits(aWhichTextRunState);
} else {
do {
NS_ASSERTION(aFrame->IsTextFrame(), "Bad frame");
aFrame = aFrame->GetNextContinuation();
} while (aFrame && aFrame != aStartContinuation);
}
bool found = aStartContinuation == aFrame;
while (aFrame) {
NS_ASSERTION(aFrame->IsTextFrame(), "Bad frame");
if (!aFrame->RemoveTextRun(aTextRun)) {
break;
}
aFrame = aFrame->GetNextContinuation();
}
MOZ_ASSERT(!found || aStartContinuation, "how did we find null?");
return found;
}
/**
* Kill all references to |aTextRun| starting at |aStartContinuation|.
* It could be referenced by any of its owners, and all their in-flows.
* If |aStartContinuation| is null then process all userdata frames
* and their continuations.
* @note the caller is expected to take care of possibly destroying the
* text run if all userdata frames were reset (userdata is deallocated
* by this function though). The caller can detect this has occured by
* checking |aTextRun->GetUserData() == nullptr|.
*/
static void UnhookTextRunFromFrames(gfxTextRun* aTextRun,
nsTextFrame* aStartContinuation) {
if (!aTextRun->GetUserData()) {
return;
}
if (aTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsSimpleFlow) {
nsTextFrame* userDataFrame = GetFrameForSimpleFlow(aTextRun);
nsFrameState whichTextRunState =
userDataFrame->GetTextRun(nsTextFrame::eInflated) == aTextRun
? TEXT_IN_TEXTRUN_USER_DATA
: TEXT_IN_UNINFLATED_TEXTRUN_USER_DATA;
DebugOnly<bool> found = ClearAllTextRunReferences(
userDataFrame, aTextRun, aStartContinuation, whichTextRunState);
NS_ASSERTION(!aStartContinuation || found,
"aStartContinuation wasn't found in simple flow text run");
if (!userDataFrame->HasAnyStateBits(whichTextRunState)) {
DestroyTextRunUserData(aTextRun);
}
} else {
auto userData = static_cast<TextRunUserData*>(aTextRun->GetUserData());
TextRunMappedFlow* userMappedFlows = GetMappedFlows(aTextRun);
int32_t destroyFromIndex = aStartContinuation ? -1 : 0;
for (uint32_t i = 0; i < userData->mMappedFlowCount; ++i) {
nsTextFrame* userDataFrame = userMappedFlows[i].mStartFrame;
nsFrameState whichTextRunState =
userDataFrame->GetTextRun(nsTextFrame::eInflated) == aTextRun
? TEXT_IN_TEXTRUN_USER_DATA
: TEXT_IN_UNINFLATED_TEXTRUN_USER_DATA;
bool found = ClearAllTextRunReferences(
userDataFrame, aTextRun, aStartContinuation, whichTextRunState);
if (found) {
if (userDataFrame->HasAnyStateBits(whichTextRunState)) {
destroyFromIndex = i + 1;
} else {
destroyFromIndex = i;
}
aStartContinuation = nullptr;
}
}
NS_ASSERTION(destroyFromIndex >= 0,
"aStartContinuation wasn't found in multi flow text run");
if (destroyFromIndex == 0) {
DestroyTextRunUserData(aTextRun);
} else {
userData->mMappedFlowCount = uint32_t(destroyFromIndex);
if (userData->mLastFlowIndex >= uint32_t(destroyFromIndex)) {
userData->mLastFlowIndex = uint32_t(destroyFromIndex) - 1;
}
}
}
}
static void InvalidateFrameDueToGlyphsChanged(nsIFrame* aFrame) {
MOZ_ASSERT(aFrame);
PresShell* presShell = aFrame->PresShell();
for (nsIFrame* f = aFrame; f;
f = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(f)) {
f->InvalidateFrame();
// If this is a non-display text frame within SVG <text>, we need
// to reflow the SVGTextFrame. (This is similar to reflowing the
// SVGTextFrame in response to style changes, in
// SVGTextFrame::DidSetComputedStyle.)
if (f->IsInSVGTextSubtree() && f->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) {
auto* svgTextFrame = static_cast<SVGTextFrame*>(
nsLayoutUtils::GetClosestFrameOfType(f, LayoutFrameType::SVGText));
svgTextFrame->ScheduleReflowSVGNonDisplayText(IntrinsicDirty::None);
} else {
// Theoretically we could just update overflow areas, perhaps using
// OverflowChangedTracker, but that would do a bunch of work eagerly that
// we should probably do lazily here since there could be a lot
// of text frames affected and we'd like to coalesce the work. So that's
// not easy to do well.
presShell->FrameNeedsReflow(f, IntrinsicDirty::None, NS_FRAME_IS_DIRTY);
}
}
}
void GlyphObserver::NotifyGlyphsChanged() {
if (mTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsSimpleFlow) {
InvalidateFrameDueToGlyphsChanged(GetFrameForSimpleFlow(mTextRun));
return;
}
auto data = static_cast<TextRunUserData*>(mTextRun->GetUserData());
TextRunMappedFlow* userMappedFlows = GetMappedFlows(mTextRun);
for (uint32_t i = 0; i < data->mMappedFlowCount; ++i) {
InvalidateFrameDueToGlyphsChanged(userMappedFlows[i].mStartFrame);
}
}
int32_t nsTextFrame::GetContentEnd() const {
nsTextFrame* next = GetNextContinuation();
// In case of allocation failure when setting/modifying the textfragment,
// it's possible our text might be missing. So we check the fragment length,
// in addition to the offset of the next continuation (if any).
int32_t fragLen = TextFragment()->GetLength();
return next ? std::min(fragLen, next->GetContentOffset()) : fragLen;
}
struct FlowLengthProperty {
int32_t mStartOffset;
// The offset of the next fixed continuation after mStartOffset, or
// of the end of the text if there is none
int32_t mEndFlowOffset;
};
int32_t nsTextFrame::GetInFlowContentLength() {
if (!HasAnyStateBits(NS_FRAME_IS_BIDI)) {
return mContent->TextLength() - mContentOffset;
}
FlowLengthProperty* flowLength =
mContent->HasFlag(NS_HAS_FLOWLENGTH_PROPERTY)
? static_cast<FlowLengthProperty*>(
mContent->GetProperty(nsGkAtoms::flowlength))
: nullptr;
MOZ_ASSERT(mContent->HasFlag(NS_HAS_FLOWLENGTH_PROPERTY) == !!flowLength,
"incorrect NS_HAS_FLOWLENGTH_PROPERTY flag");
/**
* This frame must start inside the cached flow. If the flow starts at
* mContentOffset but this frame is empty, logically it might be before the
* start of the cached flow.
*/
if (flowLength &&
(flowLength->mStartOffset < mContentOffset ||
(flowLength->mStartOffset == mContentOffset &&
GetContentEnd() > mContentOffset)) &&
flowLength->mEndFlowOffset > mContentOffset) {
#ifdef DEBUG
NS_ASSERTION(flowLength->mEndFlowOffset >= GetContentEnd(),
"frame crosses fixed continuation boundary");
#endif
return flowLength->mEndFlowOffset - mContentOffset;
}
nsTextFrame* nextBidi = LastInFlow()->GetNextContinuation();
int32_t endFlow =
nextBidi ? nextBidi->GetContentOffset() : GetContent()->TextLength();
if (!flowLength) {
flowLength = new FlowLengthProperty;
if (NS_FAILED(mContent->SetProperty(
nsGkAtoms::flowlength, flowLength,
nsINode::DeleteProperty<FlowLengthProperty>))) {
delete flowLength;
flowLength = nullptr;
} else {
mContent->SetFlags(NS_HAS_FLOWLENGTH_PROPERTY);
}
}
if (flowLength) {
flowLength->mStartOffset = mContentOffset;
flowLength->mEndFlowOffset = endFlow;
}
return endFlow - mContentOffset;
}
// Smarter versions of dom::IsSpaceCharacter.
// Unicode is really annoying; sometimes a space character isn't whitespace ---
// when it combines with another character
// So we have several versions of IsSpace for use in different contexts.
static bool IsSpaceCombiningSequenceTail(const nsTextFragment* aFrag,
uint32_t aPos) {
NS_ASSERTION(aPos <= aFrag->GetLength(), "Bad offset");
if (!aFrag->Is2b()) {
return false;
}
return nsTextFrameUtils::IsSpaceCombiningSequenceTail(
aFrag->Get2b() + aPos, aFrag->GetLength() - aPos);
}
// Check whether aPos is a space for CSS 'word-spacing' purposes
static bool IsCSSWordSpacingSpace(const nsTextFragment* aFrag, uint32_t aPos,
const nsTextFrame* aFrame,
const nsStyleText* aStyleText) {
NS_ASSERTION(aPos < aFrag->GetLength(), "No text for IsSpace!");
char16_t ch = aFrag->CharAt(aPos);
switch (ch) {
case ' ':
case CH_NBSP:
return !IsSpaceCombiningSequenceTail(aFrag, aPos + 1);
case '\r':
case '\t':
return !aStyleText->WhiteSpaceIsSignificant();
case '\n':
return !aStyleText->NewlineIsSignificant(aFrame);
default:
return false;
}
}
constexpr char16_t kOghamSpaceMark = 0x1680;
// Check whether the string aChars/aLength starts with space that's
// trimmable according to CSS 'white-space:normal/nowrap'.
static bool IsTrimmableSpace(const char16_t* aChars, uint32_t aLength) {
NS_ASSERTION(aLength > 0, "No text for IsSpace!");
char16_t ch = *aChars;
if (ch == ' ' || ch == kOghamSpaceMark) {
return !nsTextFrameUtils::IsSpaceCombiningSequenceTail(aChars + 1,
aLength - 1);
}
return ch == '\t' || ch == '\f' || ch == '\n' || ch == '\r';
}
// Check whether the character aCh is trimmable according to CSS
// 'white-space:normal/nowrap'
static bool IsTrimmableSpace(char aCh) {
return aCh == ' ' || aCh == '\t' || aCh == '\f' || aCh == '\n' || aCh == '\r';
}
static bool IsTrimmableSpace(const nsTextFragment* aFrag, uint32_t aPos,
const nsStyleText* aStyleText,
bool aAllowHangingWS = false) {
NS_ASSERTION(aPos < aFrag->GetLength(), "No text for IsSpace!");
switch (aFrag->CharAt(aPos)) {
case ' ':
case kOghamSpaceMark:
return (!aStyleText->WhiteSpaceIsSignificant() || aAllowHangingWS) &&
!IsSpaceCombiningSequenceTail(aFrag, aPos + 1);
case '\n':
return !aStyleText->NewlineIsSignificantStyle() &&
aStyleText->mWhiteSpaceCollapse !=
StyleWhiteSpaceCollapse::PreserveSpaces;
case '\t':
case '\r':
case '\f':
return !aStyleText->WhiteSpaceIsSignificant() || aAllowHangingWS;
default:
return false;
}
}
static bool IsSelectionInlineWhitespace(const nsTextFragment* aFrag,
uint32_t aPos) {
NS_ASSERTION(aPos < aFrag->GetLength(),
"No text for IsSelectionInlineWhitespace!");
char16_t ch = aFrag->CharAt(aPos);
if (ch == ' ' || ch == CH_NBSP) {
return !IsSpaceCombiningSequenceTail(aFrag, aPos + 1);
}
return ch == '\t' || ch == '\f';
}
static bool IsSelectionNewline(const nsTextFragment* aFrag, uint32_t aPos) {
NS_ASSERTION(aPos < aFrag->GetLength(), "No text for IsSelectionNewline!");
char16_t ch = aFrag->CharAt(aPos);
return ch == '\n' || ch == '\r';
}
// Count the amount of trimmable whitespace (as per CSS
// 'white-space:normal/nowrap') in a text fragment. The first
// character is at offset aStartOffset; the maximum number of characters
// to check is aLength. aDirection is -1 or 1 depending on whether we should
// progress backwards or forwards.
static uint32_t GetTrimmableWhitespaceCount(const nsTextFragment* aFrag,
int32_t aStartOffset,
int32_t aLength,
int32_t aDirection) {
if (!aLength) {
return 0;
}
int32_t count = 0;
if (aFrag->Is2b()) {
const char16_t* str = aFrag->Get2b() + aStartOffset;
int32_t fragLen = aFrag->GetLength() - aStartOffset;
for (; count < aLength; ++count) {
if (!IsTrimmableSpace(str, fragLen)) {
break;
}
str += aDirection;
fragLen -= aDirection;
}
} else {
const char* str = aFrag->Get1b() + aStartOffset;
for (; count < aLength; ++count) {
if (!IsTrimmableSpace(*str)) {
break;
}
str += aDirection;
}
}
return count;
}
static bool IsAllWhitespace(const nsTextFragment* aFrag, bool aAllowNewline) {
if (aFrag->Is2b()) {
return false;
}
int32_t len = aFrag->GetLength();
const char* str = aFrag->Get1b();
for (int32_t i = 0; i < len; ++i) {
char ch = str[i];
if (ch == ' ' || ch == '\t' || ch == '\r' ||
(ch == '\n' && aAllowNewline)) {
continue;
}
return false;
}
return true;
}
static void ClearObserversFromTextRun(gfxTextRun* aTextRun) {
if (!(aTextRun->GetFlags2() &
nsTextFrameUtils::Flags::MightHaveGlyphChanges)) {
return;
}
if (aTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsSimpleFlow) {
static_cast<SimpleTextRunUserData*>(aTextRun->GetUserData())
->mGlyphObservers.Clear();
} else {
static_cast<ComplexTextRunUserData*>(aTextRun->GetUserData())
->mGlyphObservers.Clear();
}
}
static void CreateObserversForAnimatedGlyphs(gfxTextRun* aTextRun) {
if (!aTextRun->GetUserData()) {
return;
}
ClearObserversFromTextRun(aTextRun);
nsTArray<gfxFont*> fontsWithAnimatedGlyphs;
uint32_t numGlyphRuns;
const gfxTextRun::GlyphRun* glyphRuns = aTextRun->GetGlyphRuns(&numGlyphRuns);
for (uint32_t i = 0; i < numGlyphRuns; ++i) {
gfxFont* font = glyphRuns[i].mFont;
if (font->GlyphsMayChange() && !fontsWithAnimatedGlyphs.Contains(font)) {
fontsWithAnimatedGlyphs.AppendElement(font);
}
}
if (fontsWithAnimatedGlyphs.IsEmpty()) {
// NB: Theoretically, we should clear the MightHaveGlyphChanges
// here. That would involve de-allocating the simple user data struct if
// present too, and resetting the pointer to the frame. In practice, I
// don't think worth doing that work here, given the flag's only purpose is
// to distinguish what kind of user data is there.
return;
}
nsTArray<UniquePtr<GlyphObserver>>* observers;
if (aTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsSimpleFlow) {
// Swap the frame pointer for a just-allocated SimpleTextRunUserData if
// appropriate.
if (!(aTextRun->GetFlags2() &
nsTextFrameUtils::Flags::MightHaveGlyphChanges)) {
auto frame = static_cast<nsTextFrame*>(aTextRun->GetUserData());
aTextRun->SetUserData(new SimpleTextRunUserData(frame));
}
auto data = static_cast<SimpleTextRunUserData*>(aTextRun->GetUserData());
observers = &data->mGlyphObservers;
} else {
if (!(aTextRun->GetFlags2() &
nsTextFrameUtils::Flags::MightHaveGlyphChanges)) {
auto oldData = static_cast<TextRunUserData*>(aTextRun->GetUserData());
TextRunMappedFlow* oldMappedFlows = GetMappedFlows(aTextRun);
ComplexTextRunUserData* data =
CreateComplexUserData(oldData->mMappedFlowCount);
TextRunMappedFlow* dataMappedFlows =
reinterpret_cast<TextRunMappedFlow*>(data + 1);
data->mLastFlowIndex = oldData->mLastFlowIndex;
for (uint32_t i = 0; i < oldData->mMappedFlowCount; ++i) {
dataMappedFlows[i] = oldMappedFlows[i];
}
DestroyUserData(oldData);
aTextRun->SetUserData(data);
}
auto data = static_cast<ComplexTextRunUserData*>(aTextRun->GetUserData());
observers = &data->mGlyphObservers;
}
aTextRun->SetFlagBits(nsTextFrameUtils::Flags::MightHaveGlyphChanges);
observers->SetCapacity(observers->Length() +
fontsWithAnimatedGlyphs.Length());
for (auto font : fontsWithAnimatedGlyphs) {
observers->AppendElement(MakeUnique<GlyphObserver>(font, aTextRun));
}
}
/**
* This class accumulates state as we scan a paragraph of text. It detects
* textrun boundaries (changes from text to non-text, hard
* line breaks, and font changes) and builds a gfxTextRun at each boundary.
* It also detects linebreaker run boundaries (changes from text to non-text,
* and hard line breaks) and at each boundary runs the linebreaker to compute
* potential line breaks. It also records actual line breaks to store them in
* the textruns.
*/
class BuildTextRunsScanner {
public:
BuildTextRunsScanner(nsPresContext* aPresContext, DrawTarget* aDrawTarget,
nsIFrame* aLineContainer,
nsTextFrame::TextRunType aWhichTextRun,
bool aDoLineBreaking)
: mDrawTarget(aDrawTarget),
mLineContainer(aLineContainer),
mCommonAncestorWithLastFrame(nullptr),
mMissingFonts(aPresContext->MissingFontRecorder()),
mBidiEnabled(aPresContext->BidiEnabled()),
mStartOfLine(true),
mSkipIncompleteTextRuns(false),
mCanStopOnThisLine(false),
mDoLineBreaking(aDoLineBreaking),
mWhichTextRun(aWhichTextRun),
mNextRunContextInfo(nsTextFrameUtils::INCOMING_NONE),
mCurrentRunContextInfo(nsTextFrameUtils::INCOMING_NONE) {
ResetRunInfo();
}
~BuildTextRunsScanner() {
NS_ASSERTION(mBreakSinks.IsEmpty(), "Should have been cleared");
NS_ASSERTION(mLineBreakBeforeFrames.IsEmpty(), "Should have been cleared");
NS_ASSERTION(mMappedFlows.IsEmpty(), "Should have been cleared");
}
void SetAtStartOfLine() {
mStartOfLine = true;
mCanStopOnThisLine = false;
}
void SetSkipIncompleteTextRuns(bool aSkip) {
mSkipIncompleteTextRuns = aSkip;
}
void SetCommonAncestorWithLastFrame(nsIFrame* aFrame) {
mCommonAncestorWithLastFrame = aFrame;
}
bool CanStopOnThisLine() { return mCanStopOnThisLine; }
nsIFrame* GetCommonAncestorWithLastFrame() {
return mCommonAncestorWithLastFrame;
}
void LiftCommonAncestorWithLastFrameToParent(nsIFrame* aFrame) {
if (mCommonAncestorWithLastFrame &&
mCommonAncestorWithLastFrame->GetParent() == aFrame) {
mCommonAncestorWithLastFrame = aFrame;
}
}
void ScanFrame(nsIFrame* aFrame);
bool IsTextRunValidForMappedFlows(const gfxTextRun* aTextRun);
void FlushFrames(bool aFlushLineBreaks, bool aSuppressTrailingBreak);
void FlushLineBreaks(gfxTextRun* aTrailingTextRun);
void ResetRunInfo() {
mLastFrame = nullptr;
mMappedFlows.Clear();
mLineBreakBeforeFrames.Clear();
mMaxTextLength = 0;
mDoubleByteText = false;
}
void AccumulateRunInfo(nsTextFrame* aFrame);
/**
* @return null to indicate either textrun construction failed or
* we constructed just a partial textrun to set up linebreaker and other
* state for following textruns.
*/
already_AddRefed<gfxTextRun> BuildTextRunForFrames(void* aTextBuffer);
bool SetupLineBreakerContext(gfxTextRun* aTextRun);
void AssignTextRun(gfxTextRun* aTextRun, float aInflation);
nsTextFrame* GetNextBreakBeforeFrame(uint32_t* aIndex);
void SetupBreakSinksForTextRun(gfxTextRun* aTextRun, const void* aTextPtr);
void SetupTextEmphasisForTextRun(gfxTextRun* aTextRun, const void* aTextPtr);
struct FindBoundaryState {
nsIFrame* mStopAtFrame;
nsTextFrame* mFirstTextFrame;
nsTextFrame* mLastTextFrame;
bool mSeenTextRunBoundaryOnLaterLine;
bool mSeenTextRunBoundaryOnThisLine;
bool mSeenSpaceForLineBreakingOnThisLine;
nsTArray<char16_t>& mBuffer;
};
enum FindBoundaryResult {
FB_CONTINUE,
FB_STOPPED_AT_STOP_FRAME,
FB_FOUND_VALID_TEXTRUN_BOUNDARY
};
FindBoundaryResult FindBoundaries(nsIFrame* aFrame,
FindBoundaryState* aState);
bool ContinueTextRunAcrossFrames(nsTextFrame* aFrame1, nsTextFrame* aFrame2);
// Like TextRunMappedFlow but with some differences. mStartFrame to mEndFrame
// (exclusive) are a sequence of in-flow frames (if mEndFrame is null, then
// continuations starting from mStartFrame are a sequence of in-flow frames).
struct MappedFlow {
nsTextFrame* mStartFrame;
nsTextFrame* mEndFrame;
// When we consider breaking between elements, the nearest common
// ancestor of the elements containing the characters is the one whose
// CSS 'white-space' property governs. So this records the nearest common
// ancestor of mStartFrame and the previous text frame, or null if there
// was no previous text frame on this line.
nsIFrame* mAncestorControllingInitialBreak;
int32_t GetContentEnd() const {
int32_t fragLen = mStartFrame->TextFragment()->GetLength();
return mEndFrame ? std::min(fragLen, mEndFrame->GetContentOffset())
: fragLen;
}
};
class BreakSink final : public nsILineBreakSink {
public:
BreakSink(gfxTextRun* aTextRun, DrawTarget* aDrawTarget,
uint32_t aOffsetIntoTextRun)
: mTextRun(aTextRun),
mDrawTarget(aDrawTarget),
mOffsetIntoTextRun(aOffsetIntoTextRun) {}
void SetBreaks(uint32_t aOffset, uint32_t aLength,
uint8_t* aBreakBefore) final {
gfxTextRun::Range range(aOffset + mOffsetIntoTextRun,
aOffset + mOffsetIntoTextRun + aLength);
if (mTextRun->SetPotentialLineBreaks(range, aBreakBefore)) {
// Be conservative and assume that some breaks have been set
mTextRun->ClearFlagBits(nsTextFrameUtils::Flags::NoBreaks);
}
}
void SetCapitalization(uint32_t aOffset, uint32_t aLength,
bool* aCapitalize) final {
MOZ_ASSERT(mTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsTransformed,
"Text run should be transformed!");
if (mTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsTransformed) {
nsTransformedTextRun* transformedTextRun =
static_cast<nsTransformedTextRun*>(mTextRun.get());
transformedTextRun->SetCapitalization(aOffset + mOffsetIntoTextRun,
aLength, aCapitalize);
}
}
void Finish(gfxMissingFontRecorder* aMFR) {
if (mTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsTransformed) {
nsTransformedTextRun* transformedTextRun =
static_cast<nsTransformedTextRun*>(mTextRun.get());
transformedTextRun->FinishSettingProperties(mDrawTarget, aMFR);
}
// The way nsTransformedTextRun is implemented, its glyph runs aren't
// available until after nsTransformedTextRun::FinishSettingProperties()
// is called. So that's why we defer checking for animated glyphs to here.
CreateObserversForAnimatedGlyphs(mTextRun);
}
RefPtr<gfxTextRun> mTextRun;
DrawTarget* mDrawTarget;
uint32_t mOffsetIntoTextRun;
};
private:
AutoTArray<MappedFlow, 10> mMappedFlows;
AutoTArray<nsTextFrame*, 50> mLineBreakBeforeFrames;
AutoTArray<UniquePtr<BreakSink>, 10> mBreakSinks;
nsLineBreaker mLineBreaker;
RefPtr<gfxTextRun> mCurrentFramesAllSameTextRun;
DrawTarget* mDrawTarget;
nsIFrame* mLineContainer;
nsTextFrame* mLastFrame;
// The common ancestor of the current frame and the previous leaf frame
// on the line, or null if there was no previous leaf frame.
nsIFrame* mCommonAncestorWithLastFrame;
gfxMissingFontRecorder* mMissingFonts;
// mMaxTextLength is an upper bound on the size of the text in all mapped
// frames The value UINT32_MAX represents overflow; text will be discarded
uint32_t mMaxTextLength;
bool mDoubleByteText;
bool mBidiEnabled;
bool mStartOfLine;
bool mSkipIncompleteTextRuns;
bool mCanStopOnThisLine;
bool mDoLineBreaking;
nsTextFrame::TextRunType mWhichTextRun;
uint8_t mNextRunContextInfo;
uint8_t mCurrentRunContextInfo;
};
static const nsIFrame* FindLineContainer(const nsIFrame* aFrame) {
while (aFrame &&
(aFrame->IsLineParticipant() || aFrame->CanContinueTextRun())) {
aFrame = aFrame->GetParent();
}
return aFrame;
}
static nsIFrame* FindLineContainer(nsIFrame* aFrame) {
return const_cast<nsIFrame*>(
FindLineContainer(const_cast<const nsIFrame*>(aFrame)));
}
static bool IsLineBreakingWhiteSpace(char16_t aChar) {
// 0x0A (\n) is not handled as white-space by the line breaker, since
// we break before it, if it isn't transformed to a normal space.
// (If we treat it as normal white-space then we'd only break after it.)
// However, it does induce a line break or is converted to a regular
// space, and either way it can be used to bound the region of text
// that needs to be analyzed for line breaking.
return nsLineBreaker::IsSpace(aChar) || aChar == 0x0A;
}
static bool TextContainsLineBreakerWhiteSpace(const void* aText,
uint32_t aLength,
bool aIsDoubleByte) {
if (aIsDoubleByte) {
const char16_t* chars = static_cast<const char16_t*>(aText);
for (uint32_t i = 0; i < aLength; ++i) {
if (IsLineBreakingWhiteSpace(chars[i])) {
return true;
}
}
return false;
} else {
const uint8_t* chars = static_cast<const uint8_t*>(aText);
for (uint32_t i = 0; i < aLength; ++i) {
if (IsLineBreakingWhiteSpace(chars[i])) {
return true;
}
}
return false;
}
}
static nsTextFrameUtils::CompressionMode GetCSSWhitespaceToCompressionMode(
nsTextFrame* aFrame, const nsStyleText* aStyleText) {
switch (aStyleText->mWhiteSpaceCollapse) {
case StyleWhiteSpaceCollapse::Collapse:
return nsTextFrameUtils::COMPRESS_WHITESPACE_NEWLINE;
case StyleWhiteSpaceCollapse::PreserveBreaks:
return nsTextFrameUtils::COMPRESS_WHITESPACE;
case StyleWhiteSpaceCollapse::Preserve:
case StyleWhiteSpaceCollapse::PreserveSpaces:
case StyleWhiteSpaceCollapse::BreakSpaces:
if (!aStyleText->NewlineIsSignificant(aFrame)) {
// If newline is set to be preserved, but then suppressed,
// transform newline to space.
return nsTextFrameUtils::COMPRESS_NONE_TRANSFORM_TO_SPACE;
}
return nsTextFrameUtils::COMPRESS_NONE;
}
MOZ_ASSERT_UNREACHABLE("Unknown white-space-collapse value");
return nsTextFrameUtils::COMPRESS_WHITESPACE_NEWLINE;
}
struct FrameTextTraversal {
FrameTextTraversal()
: mFrameToScan(nullptr),
mOverflowFrameToScan(nullptr),
mScanSiblings(false),
mLineBreakerCanCrossFrameBoundary(false),
mTextRunCanCrossFrameBoundary(false) {}
// These fields identify which frames should be recursively scanned
// The first normal frame to scan (or null, if no such frame should be
// scanned)
nsIFrame* mFrameToScan;
// The first overflow frame to scan (or null, if no such frame should be
// scanned)
nsIFrame* mOverflowFrameToScan;
// Whether to scan the siblings of
// mFrameToDescendInto/mOverflowFrameToDescendInto
bool mScanSiblings;
// These identify the boundaries of the context required for
// line breaking or textrun construction
bool mLineBreakerCanCrossFrameBoundary;
bool mTextRunCanCrossFrameBoundary;
nsIFrame* NextFrameToScan() {
nsIFrame* f;
if (mFrameToScan) {
f = mFrameToScan;
mFrameToScan = mScanSiblings ? f->GetNextSibling() : nullptr;
} else if (mOverflowFrameToScan) {
f = mOverflowFrameToScan;
mOverflowFrameToScan = mScanSiblings ? f->GetNextSibling() : nullptr;
} else {
f = nullptr;
}
return f;
}
};
static FrameTextTraversal CanTextCrossFrameBoundary(nsIFrame* aFrame) {
FrameTextTraversal result;
bool continuesTextRun = aFrame->CanContinueTextRun();
if (aFrame->IsPlaceholderFrame()) {
// placeholders are "invisible", so a text run should be able to span
// across one. But don't descend into the out-of-flow.
result.mLineBreakerCanCrossFrameBoundary = true;
if (continuesTextRun) {
// ... Except for first-letter floats, which are really in-flow
// from the point of view of capitalization etc, so we'd better
// descend into them. But we actually need to break the textrun for
// first-letter floats since things look bad if, say, we try to make a
// ligature across the float boundary.
result.mFrameToScan =
(static_cast<nsPlaceholderFrame*>(aFrame))->GetOutOfFlowFrame();
} else {
result.mTextRunCanCrossFrameBoundary = true;
}
} else {
if (continuesTextRun) {
result.mFrameToScan = aFrame->PrincipalChildList().FirstChild();
result.mOverflowFrameToScan =
aFrame->GetChildList(FrameChildListID::Overflow).FirstChild();
NS_WARNING_ASSERTION(
!result.mOverflowFrameToScan,
"Scanning overflow inline frames is something we should avoid");
result.mScanSiblings = true;
result.mTextRunCanCrossFrameBoundary = true;
result.mLineBreakerCanCrossFrameBoundary = true;
} else {
MOZ_ASSERT(!aFrame->IsRubyTextContainerFrame(),
"Shouldn't call this method for ruby text container");
}
}
return result;
}
BuildTextRunsScanner::FindBoundaryResult BuildTextRunsScanner::FindBoundaries(
nsIFrame* aFrame, FindBoundaryState* aState) {
LayoutFrameType frameType = aFrame->Type();
if (frameType == LayoutFrameType::RubyTextContainer) {
// Don't stop a text run for ruby text container. We want ruby text
// containers to be skipped, but continue the text run across them.
return FB_CONTINUE;
}
nsTextFrame* textFrame = frameType == LayoutFrameType::Text
? static_cast<nsTextFrame*>(aFrame)
: nullptr;
if (textFrame) {
if (aState->mLastTextFrame &&
textFrame != aState->mLastTextFrame->GetNextInFlow() &&
!ContinueTextRunAcrossFrames(aState->mLastTextFrame, textFrame)) {
aState->mSeenTextRunBoundaryOnThisLine = true;
if (aState->mSeenSpaceForLineBreakingOnThisLine) {
return FB_FOUND_VALID_TEXTRUN_BOUNDARY;
}
}
if (!aState->mFirstTextFrame) {
aState->mFirstTextFrame = textFrame;
}
aState->mLastTextFrame = textFrame;
}
if (aFrame == aState->mStopAtFrame) {
return FB_STOPPED_AT_STOP_FRAME;
}
if (textFrame) {
if (aState->mSeenSpaceForLineBreakingOnThisLine) {
return FB_CONTINUE;
}
const nsTextFragment* frag = textFrame->TextFragment();
uint32_t start = textFrame->GetContentOffset();
uint32_t length = textFrame->GetContentLength();
const void* text;
const nsAtom* language = textFrame->StyleFont()->mLanguage;
if (frag->Is2b()) {
// It is possible that we may end up removing all whitespace in
// a piece of text because of The White Space Processing Rules,
// so we need to transform it before we can check existence of
// such whitespaces.
aState->mBuffer.EnsureLengthAtLeast(length);
nsTextFrameUtils::CompressionMode compression =
GetCSSWhitespaceToCompressionMode(textFrame, textFrame->StyleText());
uint8_t incomingFlags = 0;
gfxSkipChars skipChars;
nsTextFrameUtils::Flags analysisFlags;
char16_t* bufStart = aState->mBuffer.Elements();
char16_t* bufEnd = nsTextFrameUtils::TransformText(
frag->Get2b() + start, length, bufStart, compression, &incomingFlags,
&skipChars, &analysisFlags, language);
text = bufStart;
length = bufEnd - bufStart;
} else {
// If the text only contains ASCII characters, it is currently
// impossible that TransformText would remove all whitespaces,
// and thus the check below should return the same result for
// transformed text and original text. So we don't need to try
// transforming it here.
text = static_cast<const void*>(frag->Get1b() + start);
}
if (TextContainsLineBreakerWhiteSpace(text, length, frag->Is2b())) {
aState->mSeenSpaceForLineBreakingOnThisLine = true;
if (aState->mSeenTextRunBoundaryOnLaterLine) {
return FB_FOUND_VALID_TEXTRUN_BOUNDARY;
}
}
return FB_CONTINUE;
}
FrameTextTraversal traversal = CanTextCrossFrameBoundary(aFrame);
if (!traversal.mTextRunCanCrossFrameBoundary) {
aState->mSeenTextRunBoundaryOnThisLine = true;
if (aState->mSeenSpaceForLineBreakingOnThisLine) {
return FB_FOUND_VALID_TEXTRUN_BOUNDARY;
}
}
for (nsIFrame* f = traversal.NextFrameToScan(); f;
f = traversal.NextFrameToScan()) {
FindBoundaryResult result = FindBoundaries(f, aState);
if (result != FB_CONTINUE) {
return result;
}
}
if (!traversal.mTextRunCanCrossFrameBoundary) {
aState->mSeenTextRunBoundaryOnThisLine = true;
if (aState->mSeenSpaceForLineBreakingOnThisLine) {
return FB_FOUND_VALID_TEXTRUN_BOUNDARY;
}
}
return FB_CONTINUE;
}
// build text runs for the 200 lines following aForFrame, and stop after that
// when we get a chance.
#define NUM_LINES_TO_BUILD_TEXT_RUNS 200
/**
* General routine for building text runs. This is hairy because of the need
* to build text runs that span content nodes.
*
* @param aContext The gfxContext we're using to construct this text run.
* @param aForFrame The nsTextFrame for which we're building this text run.
* @param aLineContainer the line container containing aForFrame; if null,
* we'll walk the ancestors to find it. It's required to be non-null
* when aForFrameLine is non-null.
* @param aForFrameLine the line containing aForFrame; if null, we'll figure
* out the line (slowly)
* @param aWhichTextRun The type of text run we want to build. If font inflation
* is enabled, this will be eInflated, otherwise it's eNotInflated.
*/
static void BuildTextRuns(DrawTarget* aDrawTarget, nsTextFrame* aForFrame,
nsIFrame* aLineContainer,
const nsLineList::iterator* aForFrameLine,
nsTextFrame::TextRunType aWhichTextRun) {
MOZ_ASSERT(aForFrame, "for no frame?");
NS_ASSERTION(!aForFrameLine || aLineContainer, "line but no line container");
nsIFrame* lineContainerChild = aForFrame;
if (!aLineContainer) {
if (aForFrame->IsFloatingFirstLetterChild()) {
lineContainerChild = aForFrame->GetParent()->GetPlaceholderFrame();
}
aLineContainer = FindLineContainer(lineContainerChild);
} else {
NS_ASSERTION(
(aLineContainer == FindLineContainer(aForFrame) ||
(aLineContainer->IsLetterFrame() && aLineContainer->IsFloating())),
"Wrong line container hint");
}
if (aForFrame->HasAnyStateBits(TEXT_IS_IN_TOKEN_MATHML)) {
aLineContainer->AddStateBits(TEXT_IS_IN_TOKEN_MATHML);
if (aForFrame->HasAnyStateBits(NS_FRAME_IS_IN_SINGLE_CHAR_MI)) {
aLineContainer->AddStateBits(NS_FRAME_IS_IN_SINGLE_CHAR_MI);
}
}
if (aForFrame->HasAnyStateBits(NS_FRAME_MATHML_SCRIPT_DESCENDANT)) {
aLineContainer->AddStateBits(NS_FRAME_MATHML_SCRIPT_DESCENDANT);
}
nsPresContext* presContext = aLineContainer->PresContext();
bool doLineBreaking = !aForFrame->IsInSVGTextSubtree();
BuildTextRunsScanner scanner(presContext, aDrawTarget, aLineContainer,
aWhichTextRun, doLineBreaking);
nsBlockFrame* block = do_QueryFrame(aLineContainer);
if (!block) {
nsIFrame* textRunContainer = aLineContainer;
if (aLineContainer->IsRubyTextContainerFrame()) {
textRunContainer = aForFrame;
while (textRunContainer && !textRunContainer->IsRubyTextFrame()) {
textRunContainer = textRunContainer->GetParent();
}
MOZ_ASSERT(textRunContainer &&
textRunContainer->GetParent() == aLineContainer);
} else {
NS_ASSERTION(
!aLineContainer->GetPrevInFlow() && !aLineContainer->GetNextInFlow(),
"Breakable non-block line containers other than "
"ruby text container is not supported");
}
// Just loop through all the children of the linecontainer ... it's really
// just one line
scanner.SetAtStartOfLine();
scanner.SetCommonAncestorWithLastFrame(nullptr);
for (nsIFrame* child : textRunContainer->PrincipalChildList()) {
scanner.ScanFrame(child);
}
// Set mStartOfLine so FlushFrames knows its textrun ends a line
scanner.SetAtStartOfLine();
scanner.FlushFrames(true, false);
return;
}
// Find the line containing 'lineContainerChild'.
bool isValid = true;
nsBlockInFlowLineIterator backIterator(block, &isValid);
if (aForFrameLine) {
backIterator = nsBlockInFlowLineIterator(block, *aForFrameLine);
} else {
backIterator =
nsBlockInFlowLineIterator(block, lineContainerChild, &isValid);
NS_ASSERTION(isValid, "aForFrame not found in block, someone lied to us");
NS_ASSERTION(backIterator.GetContainer() == block,
"Someone lied to us about the block");
}
nsBlockFrame::LineIterator startLine = backIterator.GetLine();
// Find a line where we can start building text runs. We choose the last line
// where:
// -- there is a textrun boundary between the start of the line and the
// start of aForFrame
// -- there is a space between the start of the line and the textrun boundary
// (this is so we can be sure the line breaks will be set properly
// on the textruns we construct).
// The possibly-partial text runs up to and including the first space
// are not reconstructed. We construct partial text runs for that text ---
// for the sake of simplifying the code and feeding the linebreaker ---
// but we discard them instead of assigning them to frames.
// This is a little awkward because we traverse lines in the reverse direction
// but we traverse the frames in each line in the forward direction.
nsBlockInFlowLineIterator forwardIterator = backIterator;
nsIFrame* stopAtFrame = lineContainerChild;
nsTextFrame* nextLineFirstTextFrame = nullptr;
AutoTArray<char16_t, BIG_TEXT_NODE_SIZE> buffer;
bool seenTextRunBoundaryOnLaterLine = false;
bool mayBeginInTextRun = true;
while (true) {
forwardIterator = backIterator;
nsBlockFrame::LineIterator line = backIterator.GetLine();
if (!backIterator.Prev() || backIterator.GetLine()->IsBlock()) {
mayBeginInTextRun = false;
break;
}
BuildTextRunsScanner::FindBoundaryState state = {
stopAtFrame, nullptr, nullptr, bool(seenTextRunBoundaryOnLaterLine),
false, false, buffer};
nsIFrame* child = line->mFirstChild;
bool foundBoundary = false;
for (int32_t i = line->GetChildCount() - 1; i >= 0; --i) {
BuildTextRunsScanner::FindBoundaryResult result =
scanner.FindBoundaries(child, &state);
if (result == BuildTextRunsScanner::FB_FOUND_VALID_TEXTRUN_BOUNDARY) {
foundBoundary = true;
break;
} else if (result == BuildTextRunsScanner::FB_STOPPED_AT_STOP_FRAME) {
break;
}
child = child->GetNextSibling();
}
if (foundBoundary) {
break;
}
if (!stopAtFrame && state.mLastTextFrame && nextLineFirstTextFrame &&
!scanner.ContinueTextRunAcrossFrames(state.mLastTextFrame,
nextLineFirstTextFrame)) {
// Found a usable textrun boundary at the end of the line
if (state.mSeenSpaceForLineBreakingOnThisLine) {
break;
}
seenTextRunBoundaryOnLaterLine = true;
} else if (state.mSeenTextRunBoundaryOnThisLine) {
seenTextRunBoundaryOnLaterLine = true;
}
stopAtFrame = nullptr;
if (state.mFirstTextFrame) {
nextLineFirstTextFrame = state.mFirstTextFrame;
}
}
scanner.SetSkipIncompleteTextRuns(mayBeginInTextRun);
// Now iterate over all text frames starting from the current line.
// First-in-flow text frames will be accumulated into textRunFrames as we go.
// When a text run boundary is required we flush textRunFrames ((re)building
// their gfxTextRuns as necessary).
bool seenStartLine = false;
uint32_t linesAfterStartLine = 0;
do {
nsBlockFrame::LineIterator line = forwardIterator.GetLine();
if (line->IsBlock()) {
break;
}
line->SetInvalidateTextRuns(false);
scanner.SetAtStartOfLine();
scanner.SetCommonAncestorWithLastFrame(nullptr);
nsIFrame* child = line->mFirstChild;
for (int32_t i = line->GetChildCount() - 1; i >= 0; --i) {
scanner.ScanFrame(child);
child = child->GetNextSibling();
}
if (line.get() == startLine.get()) {
seenStartLine = true;
}
if (seenStartLine) {
++linesAfterStartLine;
if (linesAfterStartLine >= NUM_LINES_TO_BUILD_TEXT_RUNS &&
scanner.CanStopOnThisLine()) {
// Don't flush frames; we may be in the middle of a textrun
// that we can't end here. That's OK, we just won't build it.
// Note that we must already have finished the textrun for aForFrame,
// because we've seen the end of a textrun in a line after the line
// containing aForFrame.
scanner.FlushLineBreaks(nullptr);
// This flushes out mMappedFlows and mLineBreakBeforeFrames, which
// silences assertions in the scanner destructor.
scanner.ResetRunInfo();
return;
}
}
} while (forwardIterator.Next());
// Set mStartOfLine so FlushFrames knows its textrun ends a line
scanner.SetAtStartOfLine();
scanner.FlushFrames(true, false);
}
static char16_t* ExpandBuffer(char16_t* aDest, uint8_t* aSrc, uint32_t aCount) {
while (aCount) {
*aDest = *aSrc;
++aDest;
++aSrc;
--aCount;
}
return aDest;
}
bool BuildTextRunsScanner::IsTextRunValidForMappedFlows(
const gfxTextRun* aTextRun) {
if (aTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsSimpleFlow) {
return mMappedFlows.Length() == 1 &&
mMappedFlows[0].mStartFrame == GetFrameForSimpleFlow(aTextRun) &&
mMappedFlows[0].mEndFrame == nullptr;
}
auto userData = static_cast<TextRunUserData*>(aTextRun->GetUserData());
TextRunMappedFlow* userMappedFlows = GetMappedFlows(aTextRun);
if (userData->mMappedFlowCount != mMappedFlows.Length()) {
return false;
}
for (uint32_t i = 0; i < mMappedFlows.Length(); ++i) {
if (userMappedFlows[i].mStartFrame != mMappedFlows[i].mStartFrame ||
int32_t(userMappedFlows[i].mContentLength) !=
mMappedFlows[i].GetContentEnd() -
mMappedFlows[i].mStartFrame->GetContentOffset()) {
return false;
}
}
return true;
}
/**
* This gets called when we need to make a text run for the current list of
* frames.
*/
void BuildTextRunsScanner::FlushFrames(bool aFlushLineBreaks,
bool aSuppressTrailingBreak) {
RefPtr<gfxTextRun> textRun;
if (!mMappedFlows.IsEmpty()) {
if (!mSkipIncompleteTextRuns && mCurrentFramesAllSameTextRun &&
!!(mCurrentFramesAllSameTextRun->GetFlags2() &
nsTextFrameUtils::Flags::IncomingWhitespace) ==
!!(mCurrentRunContextInfo &
nsTextFrameUtils::INCOMING_WHITESPACE) &&
!!(mCurrentFramesAllSameTextRun->GetFlags() &
gfx::ShapedTextFlags::TEXT_INCOMING_ARABICCHAR) ==
!!(mCurrentRunContextInfo &
nsTextFrameUtils::INCOMING_ARABICCHAR) &&
IsTextRunValidForMappedFlows(mCurrentFramesAllSameTextRun)) {
// Optimization: We do not need to (re)build the textrun.
textRun = mCurrentFramesAllSameTextRun;
if (mDoLineBreaking) {
// Feed this run's text into the linebreaker to provide context.
if (!SetupLineBreakerContext(textRun)) {
return;
}
}
// Update mNextRunContextInfo appropriately
mNextRunContextInfo = nsTextFrameUtils::INCOMING_NONE;
if (textRun->GetFlags2() & nsTextFrameUtils::Flags::TrailingWhitespace) {
mNextRunContextInfo |= nsTextFrameUtils::INCOMING_WHITESPACE;
}
if (textRun->GetFlags() &
gfx::ShapedTextFlags::TEXT_TRAILING_ARABICCHAR) {
mNextRunContextInfo |= nsTextFrameUtils::INCOMING_ARABICCHAR;
}
} else {
AutoTArray<uint8_t, BIG_TEXT_NODE_SIZE> buffer;
uint32_t bufferSize = mMaxTextLength * (mDoubleByteText ? 2 : 1);
if (bufferSize < mMaxTextLength || bufferSize == UINT32_MAX ||
!buffer.AppendElements(bufferSize, fallible)) {
return;
}
textRun = BuildTextRunForFrames(buffer.Elements());
}
}
if (aFlushLineBreaks) {
FlushLineBreaks(aSuppressTrailingBreak ? nullptr : textRun.get());
if (!mDoLineBreaking && textRun) {
CreateObserversForAnimatedGlyphs(textRun.get());
}
}
mCanStopOnThisLine = true;
ResetRunInfo();
}
void BuildTextRunsScanner::FlushLineBreaks(gfxTextRun* aTrailingTextRun) {
// If the line-breaker is buffering a potentially-unfinished word,
// preserve the state of being in-word so that we don't spuriously
// capitalize the next letter.
bool inWord = mLineBreaker.InWord();
bool trailingLineBreak;
nsresult rv = mLineBreaker.Reset(&trailingLineBreak);
mLineBreaker.SetWordContinuation(inWord);
// textRun may be null for various reasons, including because we constructed
// a partial textrun just to get the linebreaker and other state set up
// to build the next textrun.
if (NS_SUCCEEDED(rv) && trailingLineBreak && aTrailingTextRun) {
aTrailingTextRun->SetFlagBits(nsTextFrameUtils::Flags::HasTrailingBreak);
}
for (uint32_t i = 0; i < mBreakSinks.Length(); ++i) {
// TODO cause frames associated with the textrun to be reflowed, if they
// aren't being reflowed already!
mBreakSinks[i]->Finish(mMissingFonts);
}
mBreakSinks.Clear();
}
void BuildTextRunsScanner::AccumulateRunInfo(nsTextFrame* aFrame) {
if (mMaxTextLength != UINT32_MAX) {
NS_ASSERTION(mMaxTextLength < UINT32_MAX - aFrame->GetContentLength(),
"integer overflow");
if (mMaxTextLength >= UINT32_MAX - aFrame->GetContentLength()) {
mMaxTextLength = UINT32_MAX;
} else {
mMaxTextLength += aFrame->GetContentLength();
}
}
mDoubleByteText |= aFrame->TextFragment()->Is2b();
mLastFrame = aFrame;
mCommonAncestorWithLastFrame = aFrame->GetParent();
MappedFlow* mappedFlow = &mMappedFlows[mMappedFlows.Length() - 1];
NS_ASSERTION(mappedFlow->mStartFrame == aFrame ||
mappedFlow->GetContentEnd() == aFrame->GetContentOffset(),
"Overlapping or discontiguous frames => BAD");
mappedFlow->mEndFrame = aFrame->GetNextContinuation();
if (mCurrentFramesAllSameTextRun != aFrame->GetTextRun(mWhichTextRun)) {
mCurrentFramesAllSameTextRun = nullptr;
}
if (mStartOfLine) {
mLineBreakBeforeFrames.AppendElement(aFrame);
mStartOfLine = false;
}
// Default limits used by `hyphenate-limit-chars` for `auto` components, as
// suggested by the CSS Text spec.
// TODO: consider making these sensitive to the context, e.g. increasing the
// values for long line lengths to reduce the tendency to hyphenate too much.
const uint32_t kDefaultHyphenateTotalWordLength = 5;
const uint32_t kDefaultHyphenatePreBreakLength = 2;
const uint32_t kDefaultHyphenatePostBreakLength = 2;
const auto& hyphenateLimitChars = aFrame->StyleText()->mHyphenateLimitChars;
uint32_t pre =
hyphenateLimitChars.pre_hyphen_length.IsAuto()
? kDefaultHyphenatePreBreakLength
: std::max(0, hyphenateLimitChars.pre_hyphen_length.AsNumber());
uint32_t post =
hyphenateLimitChars.post_hyphen_length.IsAuto()
? kDefaultHyphenatePostBreakLength
: std::max(0, hyphenateLimitChars.post_hyphen_length.AsNumber());
uint32_t total =
hyphenateLimitChars.total_word_length.IsAuto()
? kDefaultHyphenateTotalWordLength
: std::max(0, hyphenateLimitChars.total_word_length.AsNumber());
total = std::max(total, pre + post);
mLineBreaker.SetHyphenateLimitChars(total, pre, post);
}
static bool HasTerminalNewline(const nsTextFrame* aFrame) {
if (aFrame->GetContentLength() == 0) {
return false;
}
const nsTextFragment* frag = aFrame->TextFragment();
return frag->CharAt(AssertedCast<uint32_t>(aFrame->GetContentEnd()) - 1) ==
'\n';
}
static gfxFont::Metrics GetFirstFontMetrics(gfxFontGroup* aFontGroup,
bool aVerticalMetrics) {
if (!aFontGroup) {
return gfxFont::Metrics();
}
RefPtr<gfxFont> font = aFontGroup->GetFirstValidFont();
return font->GetMetrics(aVerticalMetrics ? nsFontMetrics::eVertical
: nsFontMetrics::eHorizontal);
}
static nscoord GetSpaceWidthAppUnits(const gfxTextRun* aTextRun) {
// Round the space width when converting to appunits the same way textruns
// do.
gfxFloat spaceWidthAppUnits =
NS_round(GetFirstFontMetrics(aTextRun->GetFontGroup(),
aTextRun->UseCenterBaseline())
.spaceWidth *
aTextRun->GetAppUnitsPerDevUnit());
return spaceWidthAppUnits;
}
static gfxFloat GetMinTabAdvanceAppUnits(const gfxTextRun* aTextRun) {
gfxFloat chWidthAppUnits = NS_round(
GetFirstFontMetrics(aTextRun->GetFontGroup(), aTextRun->IsVertical())
.ZeroOrAveCharWidth() *
aTextRun->GetAppUnitsPerDevUnit());
return 0.5 * chWidthAppUnits;
}
static float GetSVGFontSizeScaleFactor(nsIFrame* aFrame) {
if (!aFrame->IsInSVGTextSubtree()) {
return 1.0f;
}
auto* container =
nsLayoutUtils::GetClosestFrameOfType(aFrame, LayoutFrameType::SVGText);
MOZ_ASSERT(container);
return static_cast<SVGTextFrame*>(container)->GetFontSizeScaleFactor();
}
static nscoord LetterSpacing(nsIFrame* aFrame, const nsStyleText& aStyleText) {
if (aFrame->IsInSVGTextSubtree()) {
// SVG text can have a scaling factor applied so that very small or very
// large font-sizes don't suffer from poor glyph placement due to app unit
// rounding. The used letter-spacing value must be scaled by the same
// factor. Unlike word-spacing (below), this applies to both lengths and
// percentages, as the percentage basis is 1em, not an already-scaled glyph
// dimension.
return GetSVGFontSizeScaleFactor(aFrame) *
aStyleText.mLetterSpacing.Resolve(
[&] { return aFrame->StyleFont()->mSize.ToAppUnits(); });
}
return aStyleText.mLetterSpacing.Resolve(
[&] { return aFrame->StyleFont()->mSize.ToAppUnits(); });
}
// This function converts non-coord values (e.g. percentages) to nscoord.
static nscoord WordSpacing(nsIFrame* aFrame, const gfxTextRun* aTextRun,
const nsStyleText& aStyleText) {
if (aFrame->IsInSVGTextSubtree()) {
// SVG text can have a scaling factor applied so that very small or very
// large font-sizes don't suffer from poor glyph placement due to app unit
// rounding. The used word-spacing value must be scaled by the same
// factor, although any percentage basis has already effectively been
// scaled, since it's the space glyph width, which is based on the already-
// scaled font-size.
auto spacing = aStyleText.mWordSpacing;
spacing.ScaleLengthsBy(GetSVGFontSizeScaleFactor(aFrame));
return spacing.Resolve([&] { return GetSpaceWidthAppUnits(aTextRun); });
}
return aStyleText.mWordSpacing.Resolve(
[&] { return GetSpaceWidthAppUnits(aTextRun); });
}
// Returns gfxTextRunFactory::TEXT_ENABLE_SPACING if non-standard
// letter-spacing or word-spacing is present.
static gfx::ShapedTextFlags GetSpacingFlags(
nsIFrame* aFrame, const nsStyleText* aStyleText = nullptr) {
const nsStyleText* styleText = aFrame->StyleText();
const auto& ls = styleText->mLetterSpacing;
const auto& ws = styleText->mWordSpacing;
// It's possible to have a calc() value that computes to zero but for which
// IsDefinitelyZero() is false, in which case we'll return
// TEXT_ENABLE_SPACING unnecessarily. That's ok because such cases are likely
// to be rare, and avoiding TEXT_ENABLE_SPACING is just an optimization.
bool nonStandardSpacing = !ls.IsDefinitelyZero() || !ws.IsDefinitelyZero();
return nonStandardSpacing ? gfx::ShapedTextFlags::TEXT_ENABLE_SPACING
: gfx::ShapedTextFlags();
}
bool BuildTextRunsScanner::ContinueTextRunAcrossFrames(nsTextFrame* aFrame1,
nsTextFrame* aFrame2) {
// We don't need to check font size inflation, since
// |FindLineContainer| above (via |nsIFrame::CanContinueTextRun|)
// ensures that text runs never cross block boundaries. This means
// that the font size inflation on all text frames in the text run is
// already guaranteed to be the same as each other (and for the line
// container).
if (mBidiEnabled) {
FrameBidiData data1 = aFrame1->GetBidiData();
FrameBidiData data2 = aFrame2->GetBidiData();
if (data1.embeddingLevel != data2.embeddingLevel ||
data2.precedingControl != kBidiLevelNone) {
return false;
}
}
ComputedStyle* sc1 = aFrame1->Style();
ComputedStyle* sc2 = aFrame2->Style();
// Any difference in writing-mode/directionality inhibits shaping across
// the boundary.
WritingMode wm(sc1);
if (wm != WritingMode(sc2)) {
return false;
}
const nsStyleText* textStyle1 = sc1->StyleText();
// If the first frame ends in a preformatted newline, then we end the textrun
// here. This avoids creating giant textruns for an entire plain text file.
// Note that we create a single text frame for a preformatted text node,
// even if it has newlines in it, so typically we won't see trailing newlines
// until after reflow has broken up the frame into one (or more) frames per
// line. That's OK though.
if (textStyle1->NewlineIsSignificant(aFrame1) &&
HasTerminalNewline(aFrame1)) {
return false;
}
if (aFrame1->GetParent()->GetContent() !=
aFrame2->GetParent()->GetContent()) {
// Does aFrame, or any ancestor between it and aAncestor, have a property
// that should inhibit cross-element-boundary shaping on aSide?
auto PreventCrossBoundaryShaping = [](const nsIFrame* aFrame,
const nsIFrame* aAncestor,
Side aSide) {
while (aFrame != aAncestor) {
ComputedStyle* ctx = aFrame->Style();
const auto positionProperty = ctx->StyleDisplay()->mPosition;
//
// Text shaping must be broken at inline box boundaries when any of
// the following are true for any box whose boundary separates the
// two typographic character units:
//
// 1. Any of margin/border/padding separating the two typographic
// character units in the inline axis is non-zero.
const auto margin =
ctx->StyleMargin()->GetMargin(aSide, positionProperty);
if (!margin->ConvertsToLength() ||
margin->AsLengthPercentage().ToLength() != 0) {
return true;
}
const auto& padding = ctx->StylePadding()->mPadding.Get(aSide);
if (!padding.ConvertsToLength() || padding.ToLength() != 0) {
return true;
}
if (ctx->StyleBorder()->GetComputedBorderWidth(aSide) != 0) {
return true;
}
// 2. vertical-align is not baseline.
//
// FIXME: Should this use VerticalAlignEnum()?
const auto& verticalAlign = ctx->StyleDisplay()->mVerticalAlign;
if (!verticalAlign.IsKeyword() ||
verticalAlign.AsKeyword() != StyleVerticalAlignKeyword::Baseline) {
return true;
}
// 3. The boundary is a bidi isolation boundary.
const auto unicodeBidi = ctx->StyleTextReset()->mUnicodeBidi;
if (unicodeBidi == StyleUnicodeBidi::Isolate ||
unicodeBidi == StyleUnicodeBidi::IsolateOverride) {
return true;
}
aFrame = aFrame->GetParent();
}
return false;
};
const nsIFrame* ancestor =
nsLayoutUtils::FindNearestCommonAncestorFrameWithinBlock(aFrame1,
aFrame2);
if (!ancestor) {
// The two frames are within different blocks, e.g. due to block
// fragmentation. In theory we shouldn't prevent cross-frame shaping
// here, but it's an edge case where we should rarely decide to allow
// cross-frame shaping, so we don't try harder here.
return false;
}
// We inhibit cross-element-boundary shaping if we're in SVG content,
// as there are too many things SVG might be doing (like applying per-
// element positioning) that wouldn't make sense with shaping across
// the boundary.
if (ancestor->IsInSVGTextSubtree()) {
return false;
}
// Map inline-end and inline-start to physical sides for checking presence
// of non-zero margin/border/padding.
Side side1 = wm.PhysicalSide(LogicalSide::IEnd);
Side side2 = wm.PhysicalSide(LogicalSide::IStart);
// If the frames have an embedding level that is opposite to the writing
// mode, we need to swap which sides we're checking.
if (aFrame1->GetEmbeddingLevel().IsRTL() == wm.IsBidiLTR()) {
std::swap(side1, side2);
}
if (PreventCrossBoundaryShaping(aFrame1, ancestor, side1) ||
PreventCrossBoundaryShaping(aFrame2, ancestor, side2)) {
return false;
}
}
if (aFrame1->GetContent() == aFrame2->GetContent() &&
aFrame1->GetNextInFlow() != aFrame2) {
// aFrame2 must be a non-fluid continuation of aFrame1. This can happen
// sometimes when the unicode-bidi property is used; the bidi resolver
// breaks text into different frames even though the text has the same
// direction. We can't allow these two frames to share the same textrun
// because that would violate our invariant that two flows in the same
// textrun have different content elements.
return false;
}
if (sc1 == sc2) {
return true;
}
const nsStyleText* textStyle2 = sc2->StyleText();
if (textStyle1->mTextTransform != textStyle2->mTextTransform ||
textStyle1->EffectiveWordBreak() != textStyle2->EffectiveWordBreak() ||
textStyle1->mLineBreak != textStyle2->mLineBreak) {
return false;
}
nsPresContext* pc = aFrame1->PresContext();
MOZ_ASSERT(pc == aFrame2->PresContext());
const nsStyleFont* fontStyle1 = sc1->StyleFont();
const nsStyleFont* fontStyle2 = sc2->StyleFont();
nscoord letterSpacing1 = LetterSpacing(aFrame1, *textStyle1);
nscoord letterSpacing2 = LetterSpacing(aFrame2, *textStyle2);
return fontStyle1->mFont == fontStyle2->mFont &&
fontStyle1->mLanguage == fontStyle2->mLanguage &&
nsLayoutUtils::GetTextRunFlagsForStyle(sc1, pc, fontStyle1, textStyle1,
letterSpacing1) ==
nsLayoutUtils::GetTextRunFlagsForStyle(sc2, pc, fontStyle2,
textStyle2, letterSpacing2);
}
void BuildTextRunsScanner::ScanFrame(nsIFrame* aFrame) {
LayoutFrameType frameType = aFrame->Type();
if (frameType == LayoutFrameType::RubyTextContainer) {
// Don't include any ruby text container into the text run.
return;
}
// First check if we can extend the current mapped frame block. This is
// common.
if (mMappedFlows.Length() > 0) {
MappedFlow* mappedFlow = &mMappedFlows[mMappedFlows.Length() - 1];
if (mappedFlow->mEndFrame == aFrame &&
aFrame->HasAnyStateBits(NS_FRAME_IS_FLUID_CONTINUATION)) {
NS_ASSERTION(frameType == LayoutFrameType::Text,
"Flow-sibling of a text frame is not a text frame?");
// Don't do this optimization if mLastFrame has a terminal newline...
// it's quite likely preformatted and we might want to end the textrun
// here. This is almost always true:
if (mLastFrame->Style() == aFrame->Style() &&
!HasTerminalNewline(mLastFrame)) {
AccumulateRunInfo(static_cast<nsTextFrame*>(aFrame));
return;
}
}
}
// Now see if we can add a new set of frames to the current textrun
if (frameType == LayoutFrameType::Text) {
nsTextFrame* frame = static_cast<nsTextFrame*>(aFrame);
if (mLastFrame) {
if (!ContinueTextRunAcrossFrames(mLastFrame, frame)) {
FlushFrames(false, false);
} else {
if (mLastFrame->GetContent() == frame->GetContent()) {
AccumulateRunInfo(frame);
return;
}
}
}
MappedFlow* mappedFlow = mMappedFlows.AppendElement();
mappedFlow->mStartFrame = frame;
mappedFlow->mAncestorControllingInitialBreak = mCommonAncestorWithLastFrame;
AccumulateRunInfo(frame);
if (mMappedFlows.Length() == 1) {
mCurrentFramesAllSameTextRun = frame->GetTextRun(mWhichTextRun);
mCurrentRunContextInfo = mNextRunContextInfo;
}
return;
}
if (frameType == LayoutFrameType::Placeholder &&
aFrame->HasAnyStateBits(PLACEHOLDER_FOR_ABSPOS |
PLACEHOLDER_FOR_FIXEDPOS)) {
// Somewhat hacky fix for bug 1418472:
// If this is a placeholder for an absolute-positioned frame, we need to
// flush the line-breaker to prevent the placeholder becoming separated
// from the immediately-following content.
// XXX This will interrupt text shaping (ligatures, etc) if an abs-pos
// element occurs within a word where shaping should be in effect, but
// that's an edge case, unlikely to occur in real content. A more precise
// fix might require better separation of line-breaking from textrun setup,
// but that's a big invasive change (and potentially expensive for perf, as
// it might introduce an additional pass over all the frames).
FlushFrames(true, false);
}
FrameTextTraversal traversal = CanTextCrossFrameBoundary(aFrame);
bool isBR = frameType == LayoutFrameType::Br;
if (!traversal.mLineBreakerCanCrossFrameBoundary) {
// BR frames are special. We do not need or want to record a break
// opportunity before a BR frame.
FlushFrames(true, isBR);
mCommonAncestorWithLastFrame = aFrame;
mNextRunContextInfo &= ~nsTextFrameUtils::INCOMING_WHITESPACE;
mStartOfLine = false;
} else if (!traversal.mTextRunCanCrossFrameBoundary) {
FlushFrames(false, false);
}
for (nsIFrame* f = traversal.NextFrameToScan(); f;
f = traversal.NextFrameToScan()) {
ScanFrame(f);
}
if (!traversal.mLineBreakerCanCrossFrameBoundary) {
// Really if we're a BR frame this is unnecessary since descendInto will be
// false. In fact this whole "if" statement should move into the
// descendInto.
FlushFrames(true, isBR);
mCommonAncestorWithLastFrame = aFrame;
mNextRunContextInfo &= ~nsTextFrameUtils::INCOMING_WHITESPACE;
} else if (!traversal.mTextRunCanCrossFrameBoundary) {
FlushFrames(false, false);
}
LiftCommonAncestorWithLastFrameToParent(aFrame->GetParent());
}
nsTextFrame* BuildTextRunsScanner::GetNextBreakBeforeFrame(uint32_t* aIndex) {
uint32_t index = *aIndex;
if (index >= mLineBreakBeforeFrames.Length()) {
return nullptr;
}
*aIndex = index + 1;
return static_cast<nsTextFrame*>(mLineBreakBeforeFrames.ElementAt(index));
}
static gfxFontGroup* GetFontGroupForFrame(
const nsIFrame* aFrame, float aFontSizeInflation,
nsFontMetrics** aOutFontMetrics = nullptr) {
RefPtr<nsFontMetrics> metrics =
nsLayoutUtils::GetFontMetricsForFrame(aFrame, aFontSizeInflation);
gfxFontGroup* fontGroup = metrics->GetThebesFontGroup();
// Populate outparam before we return:
if (aOutFontMetrics) {
metrics.forget(aOutFontMetrics);
}
// XXX this is a bit bogus, we're releasing 'metrics' so the
// returned font-group might actually be torn down, although because
// of the way the device context caches font metrics, this seems to
// not actually happen. But we should fix this.
return fontGroup;
}
nsFontMetrics* nsTextFrame::InflatedFontMetrics() const {
if (!mFontMetrics) {
float inflation = nsLayoutUtils::FontSizeInflationFor(this);
mFontMetrics = nsLayoutUtils::GetFontMetricsForFrame(this, inflation);
}
return mFontMetrics;
}
static gfxFontGroup* GetInflatedFontGroupForFrame(nsTextFrame* aFrame) {
gfxTextRun* textRun = aFrame->GetTextRun(nsTextFrame::eInflated);
if (textRun) {
return textRun->GetFontGroup();
}
return aFrame->InflatedFontMetrics()->GetThebesFontGroup();
}
static already_AddRefed<DrawTarget> CreateReferenceDrawTarget(
const nsTextFrame* aTextFrame) {
UniquePtr<gfxContext> ctx =
aTextFrame->PresShell()->CreateReferenceRenderingContext();
RefPtr<DrawTarget> dt = ctx->GetDrawTarget();
return dt.forget();
}
static already_AddRefed<gfxTextRun> GetHyphenTextRun(nsTextFrame* aTextFrame,
DrawTarget* aDrawTarget) {
RefPtr<DrawTarget> dt = aDrawTarget;
if (!dt) {
dt = CreateReferenceDrawTarget(aTextFrame);
if (!dt) {
return nullptr;
}
}
RefPtr<nsFontMetrics> fm =
nsLayoutUtils::GetInflatedFontMetricsForFrame(aTextFrame);
auto* fontGroup = fm->GetThebesFontGroup();
auto appPerDev = aTextFrame->PresContext()->AppUnitsPerDevPixel();
const auto& hyphenateChar = aTextFrame->StyleText()->mHyphenateCharacter;
gfx::ShapedTextFlags flags =
nsLayoutUtils::GetTextRunOrientFlagsForStyle(aTextFrame->Style());
// Make the directionality of the hyphen run (in case it is multi-char) match
// the text frame.
if (aTextFrame->GetWritingMode().IsBidiRTL()) {
flags |= gfx::ShapedTextFlags::TEXT_IS_RTL;
}
if (hyphenateChar.IsAuto()) {
return fontGroup->MakeHyphenTextRun(dt, flags, appPerDev);
}
auto* missingFonts = aTextFrame->PresContext()->MissingFontRecorder();
const NS_ConvertUTF8toUTF16 hyphenStr(hyphenateChar.AsString().AsString());
return fontGroup->MakeTextRun(hyphenStr.BeginReading(), hyphenStr.Length(),
dt, appPerDev, flags, nsTextFrameUtils::Flags(),
missingFonts);
}
already_AddRefed<gfxTextRun> BuildTextRunsScanner::BuildTextRunForFrames(
void* aTextBuffer) {
gfxSkipChars skipChars;
const void* textPtr = aTextBuffer;
bool anyTextTransformStyle = false;
bool anyMathMLStyling = false;
bool anyTextEmphasis = false;
uint8_t sstyScriptLevel = 0;
uint32_t mathFlags = 0;
gfx::ShapedTextFlags flags = gfx::ShapedTextFlags();
nsTextFrameUtils::Flags flags2 = nsTextFrameUtils::Flags::NoBreaks;
if (mCurrentRunContextInfo & nsTextFrameUtils::INCOMING_WHITESPACE) {
flags2 |= nsTextFrameUtils::Flags::IncomingWhitespace;
}
if (mCurrentRunContextInfo & nsTextFrameUtils::INCOMING_ARABICCHAR) {
flags |= gfx::ShapedTextFlags::TEXT_INCOMING_ARABICCHAR;
}
AutoTArray<int32_t, 50> textBreakPoints;
TextRunUserData dummyData;
TextRunMappedFlow dummyMappedFlow;
TextRunMappedFlow* userMappedFlows;
TextRunUserData* userData;
TextRunUserData* userDataToDestroy;
// If the situation is particularly simple (and common) we don't need to
// allocate userData.
if (mMappedFlows.Length() == 1 && !mMappedFlows[0].mEndFrame &&
mMappedFlows[0].mStartFrame->GetContentOffset() == 0) {
userData = &dummyData;
userMappedFlows = &dummyMappedFlow;
userDataToDestroy = nullptr;
dummyData.mMappedFlowCount = mMappedFlows.Length();
dummyData.mLastFlowIndex = 0;
} else {
userData = CreateUserData(mMappedFlows.Length());
userMappedFlows = reinterpret_cast<TextRunMappedFlow*>(userData + 1);
userDataToDestroy = userData;
}
uint32_t currentTransformedTextOffset = 0;
uint32_t nextBreakIndex = 0;
nsTextFrame* nextBreakBeforeFrame = GetNextBreakBeforeFrame(&nextBreakIndex);
bool isSVG = mLineContainer->IsInSVGTextSubtree();
bool enabledJustification =
(mLineContainer->StyleText()->mTextAlign == StyleTextAlign::Justify ||
mLineContainer->StyleText()->mTextAlignLast ==
StyleTextAlignLast::Justify);
const nsStyleText* textStyle = nullptr;
const nsStyleFont* fontStyle = nullptr;
ComputedStyle* lastComputedStyle = nullptr;
for (uint32_t i = 0; i < mMappedFlows.Length(); ++i) {
MappedFlow* mappedFlow = &mMappedFlows[i];
nsTextFrame* f = mappedFlow->mStartFrame;
lastComputedStyle = f->Style();
// Detect use of text-transform or font-variant anywhere in the run
textStyle = f->StyleText();
if (!textStyle->mTextTransform.IsNone() ||
textStyle->mWebkitTextSecurity != StyleTextSecurity::None ||
// text-combine-upright requires converting from full-width
// characters to non-full-width correspendent in some cases.
lastComputedStyle->IsTextCombined()) {
anyTextTransformStyle = true;
}
if (textStyle->HasEffectiveTextEmphasis()) {
anyTextEmphasis = true;
}
flags |= GetSpacingFlags(f);
nsTextFrameUtils::CompressionMode compression =
GetCSSWhitespaceToCompressionMode(f, textStyle);
if ((enabledJustification || f->ShouldSuppressLineBreak()) && !isSVG) {
flags |= gfx::ShapedTextFlags::TEXT_ENABLE_SPACING;
}
fontStyle = f->StyleFont();
nsIFrame* parent = mLineContainer->GetParent();
if (StyleMathVariant::None != fontStyle->mMathVariant) {
if (StyleMathVariant::Normal != fontStyle->mMathVariant) {
anyMathMLStyling = true;
}
} else if (mLineContainer->HasAnyStateBits(NS_FRAME_IS_IN_SINGLE_CHAR_MI)) {
flags2 |= nsTextFrameUtils::Flags::IsSingleCharMi;
anyMathMLStyling = true;
}
if (mLineContainer->HasAnyStateBits(TEXT_IS_IN_TOKEN_MATHML)) {
// All MathML tokens except <mtext> use 'math' script.
if (!(parent && parent->GetContent() &&
parent->GetContent()->IsMathMLElement(nsGkAtoms::mtext))) {
flags |= gfx::ShapedTextFlags::TEXT_USE_MATH_SCRIPT;
}
nsIMathMLFrame* mathFrame = do_QueryFrame(parent);
if (mathFrame) {
nsPresentationData presData;
mathFrame->GetPresentationData(presData);
if (NS_MATHML_IS_DTLS_SET(presData.flags)) {
mathFlags |= MathMLTextRunFactory::MATH_FONT_FEATURE_DTLS;
anyMathMLStyling = true;
}
}
}
nsIFrame* child = mLineContainer;
uint8_t oldScriptLevel = 0;
while (parent &&
child->HasAnyStateBits(NS_FRAME_MATHML_SCRIPT_DESCENDANT)) {
// Reconstruct the script level ignoring any user overrides. It is
// calculated this way instead of using scriptlevel to ensure the
// correct ssty font feature setting is used even if the user sets a
// different (especially negative) scriptlevel.
nsIMathMLFrame* mathFrame = do_QueryFrame(parent);
if (mathFrame) {
sstyScriptLevel += mathFrame->ScriptIncrement(child);
}
if (sstyScriptLevel < oldScriptLevel) {
// overflow
sstyScriptLevel = UINT8_MAX;
break;
}
child = parent;
parent = parent->GetParent();
oldScriptLevel = sstyScriptLevel;
}
if (sstyScriptLevel) {
anyMathMLStyling = true;
}
// Figure out what content is included in this flow.
nsIContent* content = f->GetContent();
const nsTextFragment* frag = f->TextFragment();
int32_t contentStart = mappedFlow->mStartFrame->GetContentOffset();
int32_t contentEnd = mappedFlow->GetContentEnd();
int32_t contentLength = contentEnd - contentStart;
const nsAtom* language = f->StyleFont()->mLanguage;
TextRunMappedFlow* newFlow = &userMappedFlows[i];
newFlow->mStartFrame = mappedFlow->mStartFrame;
newFlow->mDOMOffsetToBeforeTransformOffset =
skipChars.GetOriginalCharCount() -
mappedFlow->mStartFrame->GetContentOffset();
newFlow->mContentLength = contentLength;
while (nextBreakBeforeFrame &&
nextBreakBeforeFrame->GetContent() == content) {
textBreakPoints.AppendElement(nextBreakBeforeFrame->GetContentOffset() +
newFlow->mDOMOffsetToBeforeTransformOffset);
nextBreakBeforeFrame = GetNextBreakBeforeFrame(&nextBreakIndex);
}
nsTextFrameUtils::Flags analysisFlags;
if (frag->Is2b()) {
NS_ASSERTION(mDoubleByteText, "Wrong buffer char size!");
char16_t* bufStart = static_cast<char16_t*>(aTextBuffer);
char16_t* bufEnd = nsTextFrameUtils::TransformText(
frag->Get2b() + contentStart, contentLength, bufStart, compression,
&mNextRunContextInfo, &skipChars, &analysisFlags, language);
aTextBuffer = bufEnd;
currentTransformedTextOffset =
bufEnd - static_cast<const char16_t*>(textPtr);
} else {
if (mDoubleByteText) {
// Need to expand the text. First transform it into a temporary buffer,
// then expand.
AutoTArray<uint8_t, BIG_TEXT_NODE_SIZE> tempBuf;
uint8_t* bufStart = tempBuf.AppendElements(contentLength, fallible);
if (!bufStart) {
DestroyUserData(userDataToDestroy);
return nullptr;
}
uint8_t* end = nsTextFrameUtils::TransformText(
reinterpret_cast<const uint8_t*>(frag->Get1b()) + contentStart,
contentLength, bufStart, compression, &mNextRunContextInfo,
&skipChars, &analysisFlags, language);
aTextBuffer =
ExpandBuffer(static_cast<char16_t*>(aTextBuffer),
tempBuf.Elements(), end - tempBuf.Elements());
currentTransformedTextOffset = static_cast<char16_t*>(aTextBuffer) -
static_cast<const char16_t*>(textPtr);
} else {
uint8_t* bufStart = static_cast<uint8_t*>(aTextBuffer);
uint8_t* end = nsTextFrameUtils::TransformText(
reinterpret_cast<const uint8_t*>(frag->Get1b()) + contentStart,
contentLength, bufStart, compression, &mNextRunContextInfo,
&skipChars, &analysisFlags, language);
aTextBuffer = end;
currentTransformedTextOffset =
end - static_cast<const uint8_t*>(textPtr);
}
}
flags2 |= analysisFlags;
}
void* finalUserData;
if (userData == &dummyData) {
flags2 |= nsTextFrameUtils::Flags::IsSimpleFlow;
userData = nullptr;
finalUserData = mMappedFlows[0].mStartFrame;
} else {
finalUserData = userData;
}
uint32_t transformedLength = currentTransformedTextOffset;
// Now build the textrun
nsTextFrame* firstFrame = mMappedFlows[0].mStartFrame;
float fontInflation;
gfxFontGroup* fontGroup;
if (mWhichTextRun == nsTextFrame::eNotInflated) {
fontInflation = 1.0f;
fontGroup = GetFontGroupForFrame(firstFrame, fontInflation);
} else {
fontInflation = nsLayoutUtils::FontSizeInflationFor(firstFrame);
fontGroup = GetInflatedFontGroupForFrame(firstFrame);
}
MOZ_ASSERT(fontGroup);
if (flags2 & nsTextFrameUtils::Flags::HasTab) {
flags |= gfx::ShapedTextFlags::TEXT_ENABLE_SPACING;
}
if (flags2 & nsTextFrameUtils::Flags::HasShy) {
flags |= gfx::ShapedTextFlags::TEXT_ENABLE_HYPHEN_BREAKS;
}
if (mBidiEnabled && (firstFrame->GetEmbeddingLevel().IsRTL())) {
flags |= gfx::ShapedTextFlags::TEXT_IS_RTL;
}
if (mNextRunContextInfo & nsTextFrameUtils::INCOMING_WHITESPACE) {
flags2 |= nsTextFrameUtils::Flags::TrailingWhitespace;
}
if (mNextRunContextInfo & nsTextFrameUtils::INCOMING_ARABICCHAR) {
flags |= gfx::ShapedTextFlags::TEXT_TRAILING_ARABICCHAR;
}
// ContinueTextRunAcrossFrames guarantees that it doesn't matter which
// frame's style is used, so we use a mixture of the first frame and
// last frame's style
flags |= nsLayoutUtils::GetTextRunFlagsForStyle(
lastComputedStyle, firstFrame->PresContext(), fontStyle, textStyle,
LetterSpacing(firstFrame, *textStyle));
// XXX this is a bit of a hack. For performance reasons, if we're favouring
// performance over quality, don't try to get accurate glyph extents.
if (!(flags & gfx::ShapedTextFlags::TEXT_OPTIMIZE_SPEED)) {
flags |= gfx::ShapedTextFlags::TEXT_NEED_BOUNDING_BOX;
}
// Convert linebreak coordinates to transformed string offsets
NS_ASSERTION(nextBreakIndex == mLineBreakBeforeFrames.Length(),
"Didn't find all the frames to break-before...");
gfxSkipCharsIterator iter(skipChars);
AutoTArray<uint32_t, 50> textBreakPointsAfterTransform;
for (uint32_t i = 0; i < textBreakPoints.Length(); ++i) {
nsTextFrameUtils::AppendLineBreakOffset(
&textBreakPointsAfterTransform,
iter.ConvertOriginalToSkipped(textBreakPoints[i]));
}
if (mStartOfLine) {
nsTextFrameUtils::AppendLineBreakOffset(&textBreakPointsAfterTransform,
transformedLength);
}
// Setup factory chain
bool needsToMaskPassword = NeedsToMaskPassword(firstFrame);
UniquePtr<nsTransformingTextRunFactory> transformingFactory;
if (anyTextTransformStyle || needsToMaskPassword) {
char16_t maskChar =
needsToMaskPassword ? 0 : textStyle->TextSecurityMaskChar();
transformingFactory = MakeUnique<nsCaseTransformTextRunFactory>(
std::move(transformingFactory), false, maskChar);
}
if (anyMathMLStyling) {
transformingFactory = MakeUnique<MathMLTextRunFactory>(
std::move(transformingFactory), mathFlags, sstyScriptLevel,
fontInflation);
}
nsTArray<RefPtr<nsTransformedCharStyle>> styles;
if (transformingFactory) {
uint32_t unmaskStart = 0, unmaskEnd = UINT32_MAX;
if (needsToMaskPassword) {
unmaskStart = unmaskEnd = UINT32_MAX;
const TextEditor* const passwordEditor =
nsContentUtils::GetExtantTextEditorFromAnonymousNode(
firstFrame->GetContent());
if (passwordEditor && !passwordEditor->IsAllMasked()) {
unmaskStart = passwordEditor->UnmaskedStart();
unmaskEnd = passwordEditor->UnmaskedEnd();
}
}
iter.SetOriginalOffset(0);
for (uint32_t i = 0; i < mMappedFlows.Length(); ++i) {
MappedFlow* mappedFlow = &mMappedFlows[i];
nsTextFrame* f;
ComputedStyle* sc = nullptr;
RefPtr<nsTransformedCharStyle> defaultStyle;
RefPtr<nsTransformedCharStyle> unmaskStyle;
for (f = mappedFlow->mStartFrame; f != mappedFlow->mEndFrame;
f = f->GetNextContinuation()) {
uint32_t skippedOffset = iter.GetSkippedOffset();
// Text-combined frames have content-dependent transform, so we
// want to create new nsTransformedCharStyle for them anyway.
if (sc != f->Style() || sc->IsTextCombined()) {
sc = f->Style();
defaultStyle = new nsTransformedCharStyle(sc, f->PresContext());
if (sc->IsTextCombined() && f->CountGraphemeClusters() > 1) {
defaultStyle->mForceNonFullWidth = true;
}
if (needsToMaskPassword) {
defaultStyle->mMaskPassword = true;
if (unmaskStart != unmaskEnd) {
unmaskStyle = new nsTransformedCharStyle(sc, f->PresContext());
unmaskStyle->mForceNonFullWidth =
defaultStyle->mForceNonFullWidth;
}
}
}
iter.AdvanceOriginal(f->GetContentLength());
uint32_t skippedEnd = iter.GetSkippedOffset();
if (unmaskStyle) {
uint32_t skippedUnmaskStart =
iter.ConvertOriginalToSkipped(unmaskStart);
uint32_t skippedUnmaskEnd = iter.ConvertOriginalToSkipped(unmaskEnd);
iter.SetSkippedOffset(skippedEnd);
for (; skippedOffset < std::min(skippedEnd, skippedUnmaskStart);
++skippedOffset) {
styles.AppendElement(defaultStyle);
}
for (; skippedOffset < std::min(skippedEnd, skippedUnmaskEnd);
++skippedOffset) {
styles.AppendElement(unmaskStyle);
}
for (; skippedOffset < skippedEnd; ++skippedOffset) {
styles.AppendElement(defaultStyle);
}
} else {
for (; skippedOffset < skippedEnd; ++skippedOffset) {
styles.AppendElement(defaultStyle);
}
}
}
}
flags2 |= nsTextFrameUtils::Flags::IsTransformed;
NS_ASSERTION(iter.GetSkippedOffset() == transformedLength,
"We didn't cover all the characters in the text run!");
}
RefPtr<gfxTextRun> textRun;
gfxTextRunFactory::Parameters params = {
mDrawTarget,
finalUserData,
&skipChars,
textBreakPointsAfterTransform.Elements(),
uint32_t(textBreakPointsAfterTransform.Length()),
int32_t(firstFrame->PresContext()->AppUnitsPerDevPixel())};
if (mDoubleByteText) {
const char16_t* text = static_cast<const char16_t*>(textPtr);
if (transformingFactory) {
textRun = transformingFactory->MakeTextRun(
text, transformedLength, &params, fontGroup, flags, flags2,
std::move(styles), true);
} else {
textRun = fontGroup->MakeTextRun(text, transformedLength, &params, flags,
flags2, mMissingFonts);
}
} else {
const uint8_t* text = static_cast<const uint8_t*>(textPtr);
flags |= gfx::ShapedTextFlags::TEXT_IS_8BIT;
if (transformingFactory) {
textRun = transformingFactory->MakeTextRun(
text, transformedLength, &params, fontGroup, flags, flags2,
std::move(styles), true);
} else {
textRun = fontGroup->MakeTextRun(text, transformedLength, &params, flags,
flags2, mMissingFonts);
}
}
if (!textRun) {
DestroyUserData(userDataToDestroy);
return nullptr;
}
// We have to set these up after we've created the textrun, because
// the breaks may be stored in the textrun during this very call.
// This is a bit annoying because it requires another loop over the frames
// making up the textrun, but I don't see a way to avoid this.
// We have to do this if line-breaking is required OR if a text-transform
// is in effect, because we depend on the line-breaker's scanner (via
// BreakSink::Finish) to finish building transformed textruns.
if (mDoLineBreaking || transformingFactory) {
SetupBreakSinksForTextRun(textRun.get(), textPtr);
}
// Ownership of the factory has passed to the textrun
// TODO: bug 1285316: clean up ownership transfer from the factory to
// the textrun
Unused << transformingFactory.release();
if (anyTextEmphasis) {
SetupTextEmphasisForTextRun(textRun.get(), textPtr);
}
if (mSkipIncompleteTextRuns) {
mSkipIncompleteTextRuns = !TextContainsLineBreakerWhiteSpace(
textPtr, transformedLength, mDoubleByteText);
// Since we're doing to destroy the user data now, avoid a dangling
// pointer. Strictly speaking we don't need to do this since it should
// not be used (since this textrun will not be used and will be
// itself deleted soon), but it's always better to not have dangling
// pointers around.
textRun->SetUserData(nullptr);
DestroyUserData(userDataToDestroy);
return nullptr;
}
// Actually wipe out the textruns associated with the mapped frames and
// associate those frames with this text run.
AssignTextRun(textRun.get(), fontInflation);
return textRun.forget();
}
// This is a cut-down version of BuildTextRunForFrames used to set up
// context for the line-breaker, when the textrun has already been created.
// So it does the same walk over the mMappedFlows, but doesn't actually
// build a new textrun.
bool BuildTextRunsScanner::SetupLineBreakerContext(gfxTextRun* aTextRun) {
AutoTArray<uint8_t, BIG_TEXT_NODE_SIZE> buffer;
uint32_t bufferSize = mMaxTextLength * (mDoubleByteText ? 2 : 1);
if (bufferSize < mMaxTextLength || bufferSize == UINT32_MAX) {
return false;
}
void* textPtr = buffer.AppendElements(bufferSize, fallible);
if (!textPtr) {
return false;
}
gfxSkipChars skipChars;
const nsAtom* language = mMappedFlows[0].mStartFrame->StyleFont()->mLanguage;
for (uint32_t i = 0; i < mMappedFlows.Length(); ++i) {
MappedFlow* mappedFlow = &mMappedFlows[i];
nsTextFrame* f = mappedFlow->mStartFrame;
const nsStyleText* textStyle = f->StyleText();
nsTextFrameUtils::CompressionMode compression =
GetCSSWhitespaceToCompressionMode(f, textStyle);
// Figure out what content is included in this flow.
const nsTextFragment* frag = f->TextFragment();
int32_t contentStart = mappedFlow->mStartFrame->GetContentOffset();
int32_t contentEnd = mappedFlow->GetContentEnd();
int32_t contentLength = contentEnd - contentStart;
nsTextFrameUtils::Flags analysisFlags;
if (frag->Is2b()) {
NS_ASSERTION(mDoubleByteText, "Wrong buffer char size!");
char16_t* bufStart = static_cast<char16_t*>(textPtr);
char16_t* bufEnd = nsTextFrameUtils::TransformText(
frag->Get2b() + contentStart, contentLength, bufStart, compression,
&mNextRunContextInfo, &skipChars, &analysisFlags, language);
textPtr = bufEnd;
} else {
if (mDoubleByteText) {
// Need to expand the text. First transform it into a temporary buffer,
// then expand.
AutoTArray<uint8_t, BIG_TEXT_NODE_SIZE> tempBuf;
uint8_t* bufStart = tempBuf.AppendElements(contentLength, fallible);
if (!bufStart) {
return false;
}
uint8_t* end = nsTextFrameUtils::TransformText(
reinterpret_cast<const uint8_t*>(frag->Get1b()) + contentStart,
contentLength, bufStart, compression, &mNextRunContextInfo,
&skipChars, &analysisFlags, language);
textPtr = ExpandBuffer(static_cast<char16_t*>(textPtr),
tempBuf.Elements(), end - tempBuf.Elements());
} else {
uint8_t* bufStart = static_cast<uint8_t*>(textPtr);
uint8_t* end = nsTextFrameUtils::TransformText(
reinterpret_cast<const uint8_t*>(frag->Get1b()) + contentStart,
contentLength, bufStart, compression, &mNextRunContextInfo,
&skipChars, &analysisFlags, language);
textPtr = end;
}
}
}
// We have to set these up after we've created the textrun, because
// the breaks may be stored in the textrun during this very call.
// This is a bit annoying because it requires another loop over the frames
// making up the textrun, but I don't see a way to avoid this.
SetupBreakSinksForTextRun(aTextRun, buffer.Elements());
return true;
}
static bool HasCompressedLeadingWhitespace(
nsTextFrame* aFrame, const nsStyleText* aStyleText,
int32_t aContentEndOffset, const gfxSkipCharsIterator& aIterator) {
if (!aIterator.IsOriginalCharSkipped()) {
return false;
}
gfxSkipCharsIterator iter = aIterator;
int32_t frameContentOffset = aFrame->GetContentOffset();
const nsTextFragment* frag = aFrame->TextFragment();
while (frameContentOffset < aContentEndOffset &&
iter.IsOriginalCharSkipped()) {
if (IsTrimmableSpace(frag, frameContentOffset, aStyleText)) {
return true;
}
++frameContentOffset;
iter.AdvanceOriginal(1);
}
return false;
}
void BuildTextRunsScanner::SetupBreakSinksForTextRun(gfxTextRun* aTextRun,
const void* aTextPtr) {
using mozilla::intl::LineBreakRule;
using mozilla::intl::WordBreakRule;
// textruns have uniform language
const nsStyleFont* styleFont = mMappedFlows[0].mStartFrame->StyleFont();
// We should only use a language for hyphenation if it was specified
// explicitly.
nsAtom* hyphenationLanguage =
styleFont->mExplicitLanguage ? styleFont->mLanguage.get() : nullptr;
// We keep this pointed at the skip-chars data for the current mappedFlow.
// This lets us cheaply check whether the flow has compressed initial
// whitespace...
gfxSkipCharsIterator iter(aTextRun->GetSkipChars());
for (uint32_t i = 0; i < mMappedFlows.Length(); ++i) {
MappedFlow* mappedFlow = &mMappedFlows[i];
// The CSS word-break value may change within a word, so we reset it for
// each MappedFlow. The line-breaker will flush its text if the property
// actually changes.
const auto* styleText = mappedFlow->mStartFrame->StyleText();
auto wordBreak = styleText->EffectiveWordBreak();
switch (wordBreak) {
case StyleWordBreak::BreakAll:
mLineBreaker.SetWordBreak(WordBreakRule::BreakAll);
break;
case StyleWordBreak::KeepAll:
mLineBreaker.SetWordBreak(WordBreakRule::KeepAll);
break;
case StyleWordBreak::Normal:
default:
MOZ_ASSERT(wordBreak == StyleWordBreak::Normal);
mLineBreaker.SetWordBreak(WordBreakRule::Normal);
break;
}
switch (styleText->mLineBreak) {
case StyleLineBreak::Auto:
mLineBreaker.SetStrictness(LineBreakRule::Auto);
break;
case StyleLineBreak::Normal:
mLineBreaker.SetStrictness(LineBreakRule::Normal);
break;
case StyleLineBreak::Loose:
mLineBreaker.SetStrictness(LineBreakRule::Loose);
break;
case StyleLineBreak::Strict:
mLineBreaker.SetStrictness(LineBreakRule::Strict);
break;
case StyleLineBreak::Anywhere:
mLineBreaker.SetStrictness(LineBreakRule::Anywhere);
break;
}
uint32_t offset = iter.GetSkippedOffset();
gfxSkipCharsIterator iterNext = iter;
iterNext.AdvanceOriginal(mappedFlow->GetContentEnd() -
mappedFlow->mStartFrame->GetContentOffset());
UniquePtr<BreakSink>* breakSink = mBreakSinks.AppendElement(
MakeUnique<BreakSink>(aTextRun, mDrawTarget, offset));
uint32_t length = iterNext.GetSkippedOffset() - offset;
uint32_t flags = 0;
nsIFrame* initialBreakController =
mappedFlow->mAncestorControllingInitialBreak;
if (!initialBreakController) {
initialBreakController = mLineContainer;
}
if (!initialBreakController->StyleText()->WhiteSpaceCanWrap(
initialBreakController)) {
flags |= nsLineBreaker::BREAK_SUPPRESS_INITIAL;
}
nsTextFrame* startFrame = mappedFlow->mStartFrame;
const nsStyleText* textStyle = startFrame->StyleText();
if (!textStyle->WhiteSpaceCanWrap(startFrame)) {
flags |= nsLineBreaker::BREAK_SUPPRESS_INSIDE;
}
if (aTextRun->GetFlags2() & nsTextFrameUtils::Flags::NoBreaks) {
flags |= nsLineBreaker::BREAK_SKIP_SETTING_NO_BREAKS;
}
if (textStyle->mTextTransform & StyleTextTransform::CAPITALIZE) {
flags |= nsLineBreaker::BREAK_NEED_CAPITALIZATION;
}
if (textStyle->mHyphens == StyleHyphens::Auto &&
textStyle->mLineBreak != StyleLineBreak::Anywhere) {
flags |= nsLineBreaker::BREAK_USE_AUTO_HYPHENATION;
}
if (HasCompressedLeadingWhitespace(startFrame, textStyle,
mappedFlow->GetContentEnd(), iter)) {
mLineBreaker.AppendInvisibleWhitespace(flags);
}
if (length > 0) {
BreakSink* sink = mSkipIncompleteTextRuns ? nullptr : (*breakSink).get();
if (mDoubleByteText) {
const char16_t* text = reinterpret_cast<const char16_t*>(aTextPtr);
mLineBreaker.AppendText(hyphenationLanguage, text + offset, length,
flags, sink);
} else {
const uint8_t* text = reinterpret_cast<const uint8_t*>(aTextPtr);
mLineBreaker.AppendText(hyphenationLanguage, text + offset, length,
flags, sink);
}
}
iter = iterNext;
}
}
static bool MayCharacterHaveEmphasisMark(uint32_t aCh) {
// Punctuation characters that *can* take emphasis marks (exceptions to the
// rule that characters with GeneralCategory=P* do not take emphasis), as per
// There are no non-BMP codepoints in the punctuation exceptions, so we can
// just use a 16-bit string to list & check them.
constexpr nsLiteralString kPunctuationAcceptsEmphasis =
u"\x0023" // # NUMBER SIGN
u"\x0025" // % PERCENT SIGN
u"\x0026" // & AMPERSAND
u"\x0040" // @ COMMERCIAL AT
u"\x00A7" // § SECTION SIGN
u"\x00B6" // ¶ PILCROW SIGN
u"\x0609" // ؉ ARABIC-INDIC PER MILLE SIGN
u"\x060A" // ؊ ARABIC-INDIC PER TEN THOUSAND SIGN
u"\x066A" // ٪ ARABIC PERCENT SIGN
u"\x2030" // ‰ PER MILLE SIGN
u"\x2031" // ‱ PER TEN THOUSAND SIGN
u"\x204A" // ⁊ TIRONIAN SIGN ET
u"\x204B" // ⁋ REVERSED PILCROW SIGN
u"\x2053" // ⁓ SWUNG DASH
u"\x303D" // 〽️ PART ALTERNATION MARK
// Characters that are NFKD-equivalent to the above, extracted from
// UnicodeData.txt.
u"\xFE5F" // SMALL NUMBER SIGN;Po;0;ET;<small> 0023;;;;N;;;;;
u"\xFE60" // SMALL AMPERSAND;Po;0;ON;<small> 0026;;;;N;;;;;
u"\xFE6A" // SMALL PERCENT SIGN;Po;0;ET;<small> 0025;;;;N;;;;;
u"\xFE6B" // SMALL COMMERCIAL AT;Po;0;ON;<small> 0040;;;;N;;;;;
u"\xFF03" // FULLWIDTH NUMBER SIGN;Po;0;ET;<wide> 0023;;;;N;;;;;
u"\xFF05" // FULLWIDTH PERCENT SIGN;Po;0;ET;<wide> 0025;;;;N;;;;;
u"\xFF06" // FULLWIDTH AMPERSAND;Po;0;ON;<wide> 0026;;;;N;;;;;
u"\xFF20"_ns; // FULLWIDTH COMMERCIAL AT;Po;0;ON;<wide> 0040;;;;N;;;;;
switch (unicode::GetGenCategory(aCh)) {
case nsUGenCategory::kSeparator: // whitespace, line- & para-separators
return false;
case nsUGenCategory::kOther: // control categories
return false;
case nsUGenCategory::kPunctuation:
return aCh <= 0xffff &&
kPunctuationAcceptsEmphasis.Contains(char16_t(aCh));
default:
return true;
}
}
void BuildTextRunsScanner::SetupTextEmphasisForTextRun(gfxTextRun* aTextRun,
const void* aTextPtr) {
if (!mDoubleByteText) {
auto text = reinterpret_cast<const uint8_t*>(aTextPtr);
for (auto i : IntegerRange(aTextRun->GetLength())) {
if (!MayCharacterHaveEmphasisMark(text[i])) {
aTextRun->SetNoEmphasisMark(i);
}
}
} else {
auto text = reinterpret_cast<const char16_t*>(aTextPtr);
auto length = aTextRun->GetLength();
for (size_t i = 0; i < length; ++i) {
if (i + 1 < length && NS_IS_SURROGATE_PAIR(text[i], text[i + 1])) {
uint32_t ch = SURROGATE_TO_UCS4(text[i], text[i + 1]);
if (!MayCharacterHaveEmphasisMark(ch)) {
aTextRun->SetNoEmphasisMark(i);
aTextRun->SetNoEmphasisMark(i + 1);
}
++i;
} else {
if (!MayCharacterHaveEmphasisMark(uint32_t(text[i]))) {
aTextRun->SetNoEmphasisMark(i);
}
}
}
}
}
// Find the flow corresponding to aContent in aUserData
static inline TextRunMappedFlow* FindFlowForContent(
TextRunUserData* aUserData, nsIContent* aContent,
TextRunMappedFlow* userMappedFlows) {
// Find the flow that contains us
int32_t i = aUserData->mLastFlowIndex;
int32_t delta = 1;
int32_t sign = 1;
// Search starting at the current position and examine close-by
// positions first, moving further and further away as we go.
while (i >= 0 && uint32_t(i) < aUserData->mMappedFlowCount) {
TextRunMappedFlow* flow = &userMappedFlows[i];
if (flow->mStartFrame->GetContent() == aContent) {
return flow;
}
i += delta;
sign = -sign;
delta = -delta + sign;
}
// We ran into an array edge. Add |delta| to |i| once more to get
// back to the side where we still need to search, then step in
// the |sign| direction.
i += delta;
if (sign > 0) {
for (; i < int32_t(aUserData->mMappedFlowCount); ++i) {
TextRunMappedFlow* flow = &userMappedFlows[i];
if (flow->mStartFrame->GetContent() == aContent) {
return flow;
}
}
} else {
for (; i >= 0; --i) {
TextRunMappedFlow* flow = &userMappedFlows[i];
if (flow->mStartFrame->GetContent() == aContent) {
return flow;
}
}
}
return nullptr;
}
void BuildTextRunsScanner::AssignTextRun(gfxTextRun* aTextRun,
float aInflation) {
for (uint32_t i = 0; i < mMappedFlows.Length(); ++i) {
MappedFlow* mappedFlow = &mMappedFlows[i];
nsTextFrame* startFrame = mappedFlow->mStartFrame;
nsTextFrame* endFrame = mappedFlow->mEndFrame;
nsTextFrame* f;
for (f = startFrame; f != endFrame; f = f->GetNextContinuation()) {
#ifdef DEBUG_roc
if (f->GetTextRun(mWhichTextRun)) {
gfxTextRun* textRun = f->GetTextRun(mWhichTextRun);
if (textRun->GetFlags2() & nsTextFrameUtils::Flags::IsSimpleFlow) {
if (mMappedFlows[0].mStartFrame != GetFrameForSimpleFlow(textRun)) {
NS_WARNING("REASSIGNING SIMPLE FLOW TEXT RUN!");
}
} else {
auto userData =
static_cast<TextRunUserData*>(aTextRun->GetUserData());
TextRunMappedFlow* userMappedFlows = GetMappedFlows(aTextRun);
if (userData->mMappedFlowCount >= mMappedFlows.Length() ||
userMappedFlows[userData->mMappedFlowCount - 1].mStartFrame !=
mMappedFlows[userdata->mMappedFlowCount - 1].mStartFrame) {
NS_WARNING("REASSIGNING MULTIFLOW TEXT RUN (not append)!");
}
}
}
#endif
gfxTextRun* oldTextRun = f->GetTextRun(mWhichTextRun);
if (oldTextRun) {
nsTextFrame* firstFrame = nullptr;
uint32_t startOffset = 0;
if (oldTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsSimpleFlow) {
firstFrame = GetFrameForSimpleFlow(oldTextRun);
} else {
auto userData =
static_cast<TextRunUserData*>(oldTextRun->GetUserData());
TextRunMappedFlow* userMappedFlows = GetMappedFlows(oldTextRun);
firstFrame = userMappedFlows[0].mStartFrame;
if (MOZ_UNLIKELY(f != firstFrame)) {
TextRunMappedFlow* flow =
FindFlowForContent(userData, f->GetContent(), userMappedFlows);
if (flow) {
startOffset = flow->mDOMOffsetToBeforeTransformOffset;
} else {
NS_ERROR("Can't find flow containing frame 'f'");
}
}
}
// Optimization: if |f| is the first frame in the flow then there are no
// prev-continuations that use |oldTextRun|.
nsTextFrame* clearFrom = nullptr;
if (MOZ_UNLIKELY(f != firstFrame)) {
// If all the frames in the mapped flow starting at |f| (inclusive)
// are empty then we let the prev-continuations keep the old text run.
gfxSkipCharsIterator iter(oldTextRun->GetSkipChars(), startOffset,
f->GetContentOffset());
uint32_t textRunOffset =
iter.ConvertOriginalToSkipped(f->GetContentOffset());
clearFrom = textRunOffset == oldTextRun->GetLength() ? f : nullptr;
}
f->ClearTextRun(clearFrom, mWhichTextRun);
#ifdef DEBUG
if (firstFrame && !firstFrame->GetTextRun(mWhichTextRun)) {
// oldTextRun was destroyed - assert that we don't reference it.
for (uint32_t j = 0; j < mBreakSinks.Length(); ++j) {
NS_ASSERTION(oldTextRun != mBreakSinks[j]->mTextRun,
"destroyed text run is still in use");
}
}
#endif
}
f->SetTextRun(aTextRun, mWhichTextRun, aInflation);
}
// Set this bit now; we can't set it any earlier because
// f->ClearTextRun() might clear it out.
nsFrameState whichTextRunState =
startFrame->GetTextRun(nsTextFrame::eInflated) == aTextRun
? TEXT_IN_TEXTRUN_USER_DATA
: TEXT_IN_UNINFLATED_TEXTRUN_USER_DATA;
startFrame->AddStateBits(whichTextRunState);
}
}
NS_QUERYFRAME_HEAD(nsTextFrame)
NS_QUERYFRAME_ENTRY(nsTextFrame)
NS_QUERYFRAME_TAIL_INHERITING(nsIFrame)
gfxSkipCharsIterator nsTextFrame::EnsureTextRun(
TextRunType aWhichTextRun, DrawTarget* aRefDrawTarget,
nsIFrame* aLineContainer, const nsLineList::iterator* aLine,
uint32_t* aFlowEndInTextRun) {
gfxTextRun* textRun = GetTextRun(aWhichTextRun);
if (!textRun || (aLine && (*aLine)->GetInvalidateTextRuns())) {
RefPtr<DrawTarget> refDT = aRefDrawTarget;
if (!refDT) {
refDT = CreateReferenceDrawTarget(this);
}
if (refDT) {
BuildTextRuns(refDT, this, aLineContainer, aLine, aWhichTextRun);
}
textRun = GetTextRun(aWhichTextRun);
if (!textRun) {
// A text run was not constructed for this frame. This is bad. The caller
// will check mTextRun.
return gfxSkipCharsIterator(gfxPlatform::GetPlatform()->EmptySkipChars(),
0);
}
TabWidthStore* tabWidths = GetProperty(TabWidthProperty());
if (tabWidths && tabWidths->mValidForContentOffset != GetContentOffset()) {
RemoveProperty(TabWidthProperty());
}
}
if (textRun->GetFlags2() & nsTextFrameUtils::Flags::IsSimpleFlow) {
if (aFlowEndInTextRun) {
*aFlowEndInTextRun = textRun->GetLength();
}
return gfxSkipCharsIterator(textRun->GetSkipChars(), 0, mContentOffset);
}
auto userData = static_cast<TextRunUserData*>(textRun->GetUserData());
TextRunMappedFlow* userMappedFlows = GetMappedFlows(textRun);
TextRunMappedFlow* flow =
FindFlowForContent(userData, mContent, userMappedFlows);
if (flow) {
// Since textruns can only contain one flow for a given content element,
// this must be our flow.
uint32_t flowIndex = flow - userMappedFlows;
userData->mLastFlowIndex = flowIndex;
gfxSkipCharsIterator iter(textRun->GetSkipChars(),
flow->mDOMOffsetToBeforeTransformOffset,
mContentOffset);
if (aFlowEndInTextRun) {
if (flowIndex + 1 < userData->mMappedFlowCount) {
gfxSkipCharsIterator end(textRun->GetSkipChars());
*aFlowEndInTextRun = end.ConvertOriginalToSkipped(
flow[1].mStartFrame->GetContentOffset() +
flow[1].mDOMOffsetToBeforeTransformOffset);
} else {
*aFlowEndInTextRun = textRun->GetLength();
}
}
return iter;
}
NS_ERROR("Can't find flow containing this frame???");
return gfxSkipCharsIterator(gfxPlatform::GetPlatform()->EmptySkipChars(), 0);
}
static uint32_t GetEndOfTrimmedText(const nsTextFragment* aFrag,
const nsStyleText* aStyleText,
uint32_t aStart, uint32_t aEnd,
gfxSkipCharsIterator* aIterator,
bool aAllowHangingWS = false) {
aIterator->SetSkippedOffset(aEnd);
while (aIterator->GetSkippedOffset() > aStart) {
aIterator->AdvanceSkipped(-1);
if (!IsTrimmableSpace(aFrag, aIterator->GetOriginalOffset(), aStyleText,
aAllowHangingWS)) {
return aIterator->GetSkippedOffset() + 1;
}
}
return aStart;
}
nsTextFrame::TrimmedOffsets nsTextFrame::GetTrimmedOffsets(
const nsTextFragment* aFrag, TrimmedOffsetFlags aFlags) const {
NS_ASSERTION(mTextRun, "Need textrun here");
if (!(aFlags & TrimmedOffsetFlags::NotPostReflow)) {
// This should not be used during reflow. We need our TEXT_REFLOW_FLAGS
// to be set correctly. If our parent wasn't reflowed due to the frame
// tree being too deep then the return value doesn't matter.
NS_ASSERTION(
!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) ||
GetParent()->HasAnyStateBits(NS_FRAME_TOO_DEEP_IN_FRAME_TREE),
"Can only call this on frames that have been reflowed");
NS_ASSERTION(!HasAnyStateBits(NS_FRAME_IN_REFLOW),
"Can only call this on frames that are not being reflowed");
}
TrimmedOffsets offsets = {GetContentOffset(), GetContentLength()};
const nsStyleText* textStyle = StyleText();
// Note that pre-line newlines should still allow us to trim spaces
// for display
if (textStyle->WhiteSpaceIsSignificant()) {
return offsets;
}
if (!(aFlags & TrimmedOffsetFlags::NoTrimBefore) &&
((aFlags & TrimmedOffsetFlags::NotPostReflow) ||
HasAnyStateBits(TEXT_START_OF_LINE))) {
int32_t whitespaceCount =
GetTrimmableWhitespaceCount(aFrag, offsets.mStart, offsets.mLength, 1);
offsets.mStart += whitespaceCount;
offsets.mLength -= whitespaceCount;
}
if (!(aFlags & TrimmedOffsetFlags::NoTrimAfter) &&
((aFlags & TrimmedOffsetFlags::NotPostReflow) ||
HasAnyStateBits(TEXT_END_OF_LINE))) {
// This treats a trailing 'pre-line' newline as trimmable. That's fine,
// it's actually what we want since we want whitespace before it to
// be trimmed.
int32_t whitespaceCount = GetTrimmableWhitespaceCount(
aFrag, offsets.GetEnd() - 1, offsets.mLength, -1);
offsets.mLength -= whitespaceCount;
}
return offsets;
}
static bool IsJustifiableCharacter(const nsStyleText* aTextStyle,
const nsTextFragment* aFrag, int32_t aPos,
bool aLangIsCJ) {
NS_ASSERTION(aPos >= 0, "negative position?!");
StyleTextJustify justifyStyle = aTextStyle->mTextJustify;
if (justifyStyle == StyleTextJustify::None) {
return false;
}
const char16_t ch = aFrag->CharAt(AssertedCast<uint32_t>(aPos));
if (ch == '\n' || ch == '\t' || ch == '\r') {
return !aTextStyle->WhiteSpaceIsSignificant();
}
if (ch == ' ' || ch == CH_NBSP) {
// Don't justify spaces that are combined with diacriticals
if (!aFrag->Is2b()) {
return true;
}
return !nsTextFrameUtils::IsSpaceCombiningSequenceTail(
aFrag->Get2b() + aPos + 1, aFrag->GetLength() - (aPos + 1));
}
if (justifyStyle == StyleTextJustify::InterCharacter) {
return true;
} else if (justifyStyle == StyleTextJustify::InterWord) {
return false;
}
// text-justify: auto
if (ch < 0x2150u) {
return false;
}
if (aLangIsCJ) {
if ( // Number Forms, Arrows, Mathematical Operators
(0x2150u <= ch && ch <= 0x22ffu) ||
// Enclosed Alphanumerics
(0x2460u <= ch && ch <= 0x24ffu) ||
// Block Elements, Geometric Shapes, Miscellaneous Symbols, Dingbats
(0x2580u <= ch && ch <= 0x27bfu) ||
// Supplemental Arrows-A, Braille Patterns, Supplemental Arrows-B,
// Miscellaneous Mathematical Symbols-B,
// Supplemental Mathematical Operators, Miscellaneous Symbols and Arrows
(0x27f0u <= ch && ch <= 0x2bffu) ||
// CJK Radicals Supplement, CJK Radicals Supplement, Ideographic
// Description Characters, CJK Symbols and Punctuation, Hiragana,
// Katakana, Bopomofo
(0x2e80u <= ch && ch <= 0x312fu) ||
// Kanbun, Bopomofo Extended, Katakana Phonetic Extensions,
// Enclosed CJK Letters and Months, CJK Compatibility,
// CJK Unified Ideographs Extension A, Yijing Hexagram Symbols,
// CJK Unified Ideographs, Yi Syllables, Yi Radicals
(0x3190u <= ch && ch <= 0xabffu) ||
// CJK Compatibility Ideographs
(0xf900u <= ch && ch <= 0xfaffu) ||
// Halfwidth and Fullwidth Forms (a part)
(0xff5eu <= ch && ch <= 0xff9fu)) {
return true;
}
if (NS_IS_HIGH_SURROGATE(ch)) {
if (char32_t u = aFrag->ScalarValueAt(AssertedCast<uint32_t>(aPos))) {
// CJK Unified Ideographs Extension B,
// CJK Unified Ideographs Extension C,
// CJK Unified Ideographs Extension D,
// CJK Compatibility Ideographs Supplement
if (0x20000u <= u && u <= 0x2ffffu) {
return true;
}
}
}
}
return false;
}
void nsTextFrame::ClearMetrics(ReflowOutput& aMetrics) {
aMetrics.ClearSize();
aMetrics.SetBlockStartAscent(0);
mAscent = 0;
AddStateBits(TEXT_NO_RENDERED_GLYPHS);
}
static int32_t FindChar(const nsTextFragment* frag, int32_t aOffset,
int32_t aLength, char16_t ch) {
int32_t i = 0;
if (frag->Is2b()) {
const char16_t* str = frag->Get2b() + aOffset;
for (; i < aLength; ++i) {
if (*str == ch) {
return i + aOffset;
}
++str;
}
} else {
if (uint16_t(ch) <= 0xFF) {
const char* str = frag->Get1b() + aOffset;
const void* p = memchr(str, ch, aLength);
if (p) {
return (static_cast<const char*>(p) - str) + aOffset;
}
}
}
return -1;
}
static bool IsChineseOrJapanese(const nsTextFrame* aFrame) {
if (aFrame->ShouldSuppressLineBreak()) {
// Always treat ruby as CJ language so that those characters can
// be expanded properly even when surrounded by other language.
return true;
}
nsAtom* language = aFrame->StyleFont()->mLanguage;
if (!language) {
return false;
}
return nsStyleUtil::MatchesLanguagePrefix(language, u"ja") ||
nsStyleUtil::MatchesLanguagePrefix(language, u"zh");
}
#ifdef DEBUG
static bool IsInBounds(const gfxSkipCharsIterator& aStart,
int32_t aContentLength, gfxTextRun::Range aRange) {
if (aStart.GetSkippedOffset() > aRange.start) {
return false;
}
if (aContentLength == INT32_MAX) {
return true;
}
gfxSkipCharsIterator iter(aStart);
iter.AdvanceOriginal(aContentLength);
return iter.GetSkippedOffset() >= aRange.end;
}
#endif
nsTextFrame::PropertyProvider::PropertyProvider(
gfxTextRun* aTextRun, const nsStyleText* aTextStyle,
const nsTextFragment* aFrag, nsTextFrame* aFrame,
const gfxSkipCharsIterator& aStart, int32_t aLength,
nsIFrame* aLineContainer, nscoord aOffsetFromBlockOriginForTabs,
nsTextFrame::TextRunType aWhichTextRun, bool aAtStartOfLine)
: mTextRun(aTextRun),
mFontGroup(nullptr),
mTextStyle(aTextStyle),
mFrag(aFrag),
mLineContainer(aLineContainer),
mFrame(aFrame),
mStart(aStart),
mTempIterator(aStart),
mTabWidths(nullptr),
mTabWidthsAnalyzedLimit(0),
mLength(aLength),
mWordSpacing(WordSpacing(aFrame, mTextRun, *aTextStyle)),
mLetterSpacing(LetterSpacing(aFrame, *aTextStyle)),
mMinTabAdvance(-1.0),
mHyphenWidth(-1),
mOffsetFromBlockOriginForTabs(aOffsetFromBlockOriginForTabs),
mJustificationArrayStart(0),
mReflowing(true),
mWhichTextRun(aWhichTextRun) {
NS_ASSERTION(mStart.IsInitialized(), "Start not initialized?");
if (aAtStartOfLine) {
mStartOfLineOffset = mStart.GetSkippedOffset();
}
}
nsTextFrame::PropertyProvider::PropertyProvider(
nsTextFrame* aFrame, const gfxSkipCharsIterator& aStart,
nsTextFrame::TextRunType aWhichTextRun, nsFontMetrics* aFontMetrics)
: mTextRun(aFrame->GetTextRun(aWhichTextRun)),
mFontGroup(nullptr),
mFontMetrics(aFontMetrics),
mTextStyle(aFrame->StyleText()),
mFrag(aFrame->TextFragment()),
mLineContainer(nullptr),
mFrame(aFrame),
mStart(aStart),
mTempIterator(aStart),
mTabWidths(nullptr),
mTabWidthsAnalyzedLimit(0),
mLength(aFrame->GetContentLength()),
mWordSpacing(WordSpacing(aFrame, mTextRun, *mTextStyle)),
mLetterSpacing(LetterSpacing(aFrame, *mTextStyle)),
mMinTabAdvance(-1.0),
mHyphenWidth(-1),
mOffsetFromBlockOriginForTabs(0),
mJustificationArrayStart(0),
mReflowing(false),
mWhichTextRun(aWhichTextRun) {
NS_ASSERTION(mTextRun, "Textrun not initialized!");
}
gfx::ShapedTextFlags nsTextFrame::PropertyProvider::GetShapedTextFlags() const {
return nsLayoutUtils::GetTextRunOrientFlagsForStyle(mFrame->Style());
}
already_AddRefed<DrawTarget> nsTextFrame::PropertyProvider::GetDrawTarget()
const {
return CreateReferenceDrawTarget(GetFrame());
}
gfxFloat nsTextFrame::PropertyProvider::MinTabAdvance() const {
if (mMinTabAdvance < 0.0) {
mMinTabAdvance = GetMinTabAdvanceAppUnits(mTextRun);
}
return mMinTabAdvance;
}
/**
* Finds the offset of the first character of the cluster containing aPos
*/
static void FindClusterStart(const gfxTextRun* aTextRun, int32_t aOriginalStart,
gfxSkipCharsIterator* aPos) {
while (aPos->GetOriginalOffset() > aOriginalStart) {
if (aPos->IsOriginalCharSkipped() ||
aTextRun->IsClusterStart(aPos->GetSkippedOffset())) {
break;
}
aPos->AdvanceOriginal(-1);
}
}
/**
* Finds the offset of the last character of the cluster containing aPos.
* If aAllowSplitLigature is false, we also check for a ligature-group
* start.
*/
static void FindClusterEnd(const gfxTextRun* aTextRun, int32_t aOriginalEnd,
gfxSkipCharsIterator* aPos,
bool aAllowSplitLigature = true) {
MOZ_ASSERT(aPos->GetOriginalOffset() < aOriginalEnd,
"character outside string");
aPos->AdvanceOriginal(1);
while (aPos->GetOriginalOffset() < aOriginalEnd) {
if (aPos->IsOriginalCharSkipped() ||
(aTextRun->IsClusterStart(aPos->GetSkippedOffset()) &&
(aAllowSplitLigature ||
aTextRun->IsLigatureGroupStart(aPos->GetSkippedOffset())))) {
break;
}
aPos->AdvanceOriginal(1);
}
aPos->AdvanceOriginal(-1);
}
// Get the line number of aFrame in the lines referenced by aLineIter, if
// known (returning -1 if we don't find it).
static int32_t GetFrameLineNum(nsIFrame* aFrame, nsILineIterator* aLineIter) {
if (!aLineIter) {
return -1;
}
int32_t n = aLineIter->FindLineContaining(aFrame);
if (n >= 0) {
return n;
}
// If we didn't find the frame directly, but its parent is an inline,
// we want the line that the inline ancestor is on.
nsIFrame* ancestor = aFrame->GetParent();
while (ancestor && ancestor->IsInlineFrame()) {
n = aLineIter->FindLineContaining(ancestor);
if (n >= 0) {
return n;
}
ancestor = ancestor->GetParent();
}
return -1;
}
// Get the position of the first preserved newline in aFrame, if any,
// returning -1 if none.
static int32_t FindFirstNewlinePosition(const nsTextFrame* aFrame) {
MOZ_ASSERT(aFrame->StyleText()->NewlineIsSignificantStyle(),
"how did the HasNewline flag get set?");
const auto* textFragment = aFrame->TextFragment();
for (auto i = aFrame->GetContentOffset(); i < aFrame->GetContentEnd(); ++i) {
if (textFragment->CharAt(i) == '\n') {
return i;
}
}
return -1;
}
// Get the position of the last preserved tab in aFrame that is before the
// preserved newline at aNewlinePos.
// Passing -1 for aNewlinePos means there is no preserved newline, so we look
// for the last preserved tab in the whole content.
// Returns -1 if no such preserved tab is present.
static int32_t FindLastTabPositionBeforeNewline(const nsTextFrame* aFrame,
int32_t aNewlinePos) {
// We only call this if white-space is not being collapsed.
MOZ_ASSERT(aFrame->StyleText()->WhiteSpaceIsSignificant(),
"how did the HasTab flag get set?");
const auto* textFragment = aFrame->TextFragment();
// If a non-negative newline position was given, we only need to search the
// text before that offset.
for (auto i = aNewlinePos < 0 ? aFrame->GetContentEnd() : aNewlinePos;
i > aFrame->GetContentOffset(); --i) {
if (textFragment->CharAt(i - 1) == '\t') {
return i;
}
}
return -1;
}
// Look for preserved tab or newline in the given frame or its following
// siblings on the same line, to determine whether justification should be
// suppressed in order to avoid disrupting tab-stop positions.
// Returns the first such preserved whitespace char, or 0 if none found.
static char NextPreservedWhiteSpaceOnLine(nsIFrame* aSibling,
nsILineIterator* aLineIter,
int32_t aLineNum) {
while (aSibling) {
// If we find a <br>, treat it like a newline.
if (aSibling->IsBrFrame()) {
return '\n';
}
// If we've moved on to a later line, stop searching.
if (GetFrameLineNum(aSibling, aLineIter) > aLineNum) {
return 0;
}
// If we encounter an inline frame, recurse into it.
if (aSibling->IsInlineFrame()) {
auto* child = aSibling->PrincipalChildList().FirstChild();
char result = NextPreservedWhiteSpaceOnLine(child, aLineIter, aLineNum);
if (result) {
return result;
}
}
// If we have a text frame, and whitespace is not collapsed, we need to
// check its contents.
if (aSibling->IsTextFrame()) {
const auto* textStyle = aSibling->StyleText();
if (textStyle->WhiteSpaceOrNewlineIsSignificant()) {
const auto* textFrame = static_cast<nsTextFrame*>(aSibling);
const auto* textFragment = textFrame->TextFragment();
for (auto i = textFrame->GetContentOffset();
i < textFrame->GetContentEnd(); ++i) {
const char16_t ch = textFragment->CharAt(i);
if (ch == '\n' && textStyle->NewlineIsSignificantStyle()) {
return '\n';
}
if (ch == '\t' && textStyle->WhiteSpaceIsSignificant()) {
return '\t';
}
}
}
}
aSibling = aSibling->GetNextSibling();
}
return 0;
}
static bool HasPreservedTabInFollowingSiblingOnLine(nsTextFrame* aFrame) {
bool foundTab = false;
nsIFrame* lineContainer = FindLineContainer(aFrame);
nsILineIterator* iter = lineContainer->GetLineIterator();
int32_t line = GetFrameLineNum(aFrame, iter);
char ws = NextPreservedWhiteSpaceOnLine(aFrame->GetNextSibling(), iter, line);
if (ws == '\t') {
foundTab = true;
} else if (!ws) {
// Didn't find a preserved tab or newline in our siblings; if our parent
// (and its parent, etc) is an inline, we need to look at their following
// siblings, too, as long as they're on the same line.
const nsIFrame* maybeInline = aFrame->GetParent();
while (maybeInline && maybeInline->IsInlineFrame()) {
ws = NextPreservedWhiteSpaceOnLine(maybeInline->GetNextSibling(), iter,
line);
if (ws == '\t') {
foundTab = true;
break;
}
if (ws == '\n') {
break;
}
maybeInline = maybeInline->GetParent();
}
}
// We called lineContainer->GetLineIterator() above, but we mustn't
// allow a block frame to retain this iterator if we're currently in
// reflow, as it will become invalid as the line list is reflowed.
if (lineContainer->HasAnyStateBits(NS_FRAME_IN_REFLOW) &&
lineContainer->IsBlockFrameOrSubclass()) {
static_cast<nsBlockFrame*>(lineContainer)->ClearLineIterator();
}
return foundTab;
}
JustificationInfo nsTextFrame::PropertyProvider::ComputeJustification(
Range aRange, nsTArray<JustificationAssignment>* aAssignments) {
JustificationInfo info;
// Horizontal-in-vertical frame is orthogonal to the line, so it
// doesn't actually include any justification opportunity inside.
// The spec says such frame should be treated as a U+FFFC. Since we
// do not insert justification opportunities on the sides of that
// character, the sides of this frame are not justifiable either.
if (mFrame->Style()->IsTextCombined()) {
return info;
}
int32_t lastTab = -1;
if (StaticPrefs::layout_css_text_align_justify_only_after_last_tab()) {
// If there is a preserved tab on the line, we don't apply justification
// until we're past its position.
if (mTextStyle->WhiteSpaceIsSignificant()) {
// If there is a preserved newline within the text, we don't need to look
// beyond this frame, as following frames will not be on the same line.
int32_t newlinePos =
(mTextRun->GetFlags2() & nsTextFrameUtils::Flags::HasNewline)
? FindFirstNewlinePosition(mFrame)
: -1;
if (newlinePos < 0) {
// There's no preserved newline within this frame; if there's a tab
// in a later sibling frame on the same line, we won't apply any
// justification to this one.
if (HasPreservedTabInFollowingSiblingOnLine(mFrame)) {
return info;
}
}
if (mTextRun->GetFlags2() & nsTextFrameUtils::Flags::HasTab) {
// Find last tab character in the content; we won't justify anything
// before that position, so that tab alignment remains correct.
lastTab = FindLastTabPositionBeforeNewline(mFrame, newlinePos);
}
}
}
bool isCJ = IsChineseOrJapanese(mFrame);
nsSkipCharsRunIterator run(
mStart, nsSkipCharsRunIterator::LENGTH_INCLUDES_SKIPPED, aRange.Length());
run.SetOriginalOffset(aRange.start);
mJustificationArrayStart = run.GetSkippedOffset();
nsTArray<JustificationAssignment> assignments;
assignments.SetCapacity(aRange.Length());
while (run.NextRun()) {
uint32_t originalOffset = run.GetOriginalOffset();
uint32_t skippedOffset = run.GetSkippedOffset();
uint32_t length = run.GetRunLength();
assignments.SetLength(skippedOffset + length - mJustificationArrayStart);
gfxSkipCharsIterator iter = run.GetPos();
for (uint32_t i = 0; i < length; ++i) {
uint32_t offset = originalOffset + i;
if (!IsJustifiableCharacter(mTextStyle, mFrag, offset, isCJ) ||
(lastTab >= 0 && offset <= uint32_t(lastTab))) {
continue;
}
iter.SetOriginalOffset(offset);
FindClusterStart(mTextRun, originalOffset, &iter);
uint32_t firstCharOffset = iter.GetSkippedOffset();
uint32_t firstChar = firstCharOffset > mJustificationArrayStart
? firstCharOffset - mJustificationArrayStart
: 0;
if (!firstChar) {
info.mIsStartJustifiable = true;
} else {
auto& assign = assignments[firstChar];
auto& prevAssign = assignments[firstChar - 1];
if (prevAssign.mGapsAtEnd) {
prevAssign.mGapsAtEnd = 1;
assign.mGapsAtStart = 1;
} else {
assign.mGapsAtStart = 2;
info.mInnerOpportunities++;
}
}
FindClusterEnd(mTextRun, originalOffset + length, &iter);
uint32_t lastChar = iter.GetSkippedOffset() - mJustificationArrayStart;
// Assign the two gaps temporary to the last char. If the next cluster is
// justifiable as well, one of the gaps will be removed by code above.
assignments[lastChar].mGapsAtEnd = 2;
info.mInnerOpportunities++;
// Skip the whole cluster
i = iter.GetOriginalOffset() - originalOffset;
}
}
if (!assignments.IsEmpty() && assignments.LastElement().mGapsAtEnd) {
// We counted the expansion opportunity after the last character,
// but it is not an inner opportunity.
MOZ_ASSERT(info.mInnerOpportunities > 0);
info.mInnerOpportunities--;
info.mIsEndJustifiable = true;
}
if (aAssignments) {
*aAssignments = std::move(assignments);
}
return info;
}
// aStart, aLength in transformed string offsets
void nsTextFrame::PropertyProvider::GetSpacing(Range aRange,
Spacing* aSpacing) const {
GetSpacingInternal(
aRange, aSpacing,
!(mTextRun->GetFlags2() & nsTextFrameUtils::Flags::HasTab));
}
static bool CanAddSpacingBefore(const gfxTextRun* aTextRun, uint32_t aOffset,
bool aNewlineIsSignificant) {
const auto* g = aTextRun->GetCharacterGlyphs();
MOZ_ASSERT(aOffset < aTextRun->GetLength());
if (aNewlineIsSignificant && g[aOffset].CharIsNewline()) {
return false;
}
if (!aOffset) {
return true;
}
return g[aOffset].IsClusterStart() && g[aOffset].IsLigatureGroupStart() &&
!g[aOffset - 1].CharIsFormattingControl() && !g[aOffset].CharIsTab();
}
static bool CanAddSpacingAfter(const gfxTextRun* aTextRun, uint32_t aOffset,
bool aNewlineIsSignificant) {
const auto* g = aTextRun->GetCharacterGlyphs();
MOZ_ASSERT(aOffset < aTextRun->GetLength());
if (aNewlineIsSignificant && g[aOffset].CharIsNewline()) {
return false;
}
if (aOffset + 1 >= aTextRun->GetLength()) {
return true;
}
return g[aOffset + 1].IsClusterStart() &&
g[aOffset + 1].IsLigatureGroupStart() &&
!g[aOffset].CharIsFormattingControl() && !g[aOffset].CharIsTab();
}
static gfxFloat ComputeTabWidthAppUnits(const nsIFrame* aFrame) {
const auto& tabSize = aFrame->StyleText()->mTabSize;
if (tabSize.IsLength()) {
nscoord w = tabSize.length._0.ToAppUnits();
MOZ_ASSERT(w >= 0);
return w;
}
MOZ_ASSERT(tabSize.IsNumber());
gfxFloat spaces = tabSize.number._0;
MOZ_ASSERT(spaces >= 0);
const nsIFrame* cb = aFrame->GetContainingBlock(0, aFrame->StyleDisplay());
const auto* styleText = cb->StyleText();
// Round the space width when converting to appunits the same way textruns do.
// We don't use GetFirstFontMetrics here because that may return a font that
// does not actually have the <space> character, yet is considered the "first
// available font" per CSS Fonts. Here, we want the font that would be used
// to render <space>, even if that means looking further down the font-family
// list.
RefPtr fm = nsLayoutUtils::GetFontMetricsForFrame(cb, 1.0f);
bool vertical = cb->GetWritingMode().IsCentralBaseline();
RefPtr font = fm->GetThebesFontGroup()->GetFirstValidFont(' ');
auto metrics = font->GetMetrics(vertical ? nsFontMetrics::eVertical
: nsFontMetrics::eHorizontal);
nscoord spaceWidth = nscoord(
NS_round(metrics.spaceWidth * cb->PresContext()->AppUnitsPerDevPixel()));
return spaces *
(spaceWidth + styleText->mLetterSpacing.Resolve(fm->EmHeight()) +
styleText->mWordSpacing.Resolve(spaceWidth));
}
void nsTextFrame::PropertyProvider::GetSpacingInternal(Range aRange,
Spacing* aSpacing,
bool aIgnoreTabs) const {
MOZ_ASSERT(IsInBounds(mStart, mLength, aRange), "Range out of bounds");
uint32_t index;
for (index = 0; index < aRange.Length(); ++index) {
aSpacing[index].mBefore = 0.0;
aSpacing[index].mAfter = 0.0;
}
if (mFrame->Style()->IsTextCombined()) {
return;
}
// Find our offset into the original+transformed string
gfxSkipCharsIterator start(mStart);
start.SetSkippedOffset(aRange.start);
// First, compute the word and letter spacing
if (mWordSpacing || mLetterSpacing) {
// Iterate over non-skipped characters
nsSkipCharsRunIterator run(
start, nsSkipCharsRunIterator::LENGTH_UNSKIPPED_ONLY, aRange.Length());
bool newlineIsSignificant = mTextStyle->NewlineIsSignificant(mFrame);
// Which letter-spacing model are we using?
// 0 - Gecko legacy model, spacing added to trailing side of letter
// 1 - WebKit/Blink-compatible, spacing added to right-hand side
// 2 - Symmetrical spacing, half added to each side
gfxFloat before, after;
switch (StaticPrefs::layout_css_letter_spacing_model()) {
default: // use Gecko legacy behavior if pref value is unknown
case 0:
before = 0.0;
after = mLetterSpacing;
break;
case 1:
if (mTextRun->IsRightToLeft()) {
before = mLetterSpacing;
after = 0.0;
} else {
before = 0.0;
after = mLetterSpacing;
}
break;
case 2:
before = mLetterSpacing / 2.0;
after = mLetterSpacing - before;
break;
}
bool atStart = mStartOfLineOffset == start.GetSkippedOffset() &&
!mFrame->IsInSVGTextSubtree();
while (run.NextRun()) {
uint32_t runOffsetInSubstring = run.GetSkippedOffset() - aRange.start;
gfxSkipCharsIterator iter = run.GetPos();
for (int32_t i = 0; i < run.GetRunLength(); ++i) {
if (!atStart && before != 0.0 &&
CanAddSpacingBefore(mTextRun, run.GetSkippedOffset() + i,
newlineIsSignificant)) {
aSpacing[runOffsetInSubstring + i].mBefore += before;
}
if (after != 0.0 &&
CanAddSpacingAfter(mTextRun, run.GetSkippedOffset() + i,
newlineIsSignificant)) {
// End of a cluster, not in a ligature: put letter-spacing after it
aSpacing[runOffsetInSubstring + i].mAfter += after;
}
if (IsCSSWordSpacingSpace(mFrag, i + run.GetOriginalOffset(), mFrame,
mTextStyle)) {
// It kinda sucks, but space characters can be part of clusters,
// and even still be whitespace (I think!)
iter.SetSkippedOffset(run.GetSkippedOffset() + i);
FindClusterEnd(mTextRun, run.GetOriginalOffset() + run.GetRunLength(),
&iter);
uint32_t runOffset = iter.GetSkippedOffset() - aRange.start;
aSpacing[runOffset].mAfter += mWordSpacing;
}
atStart = false;
}
}
}
// Now add tab spacing, if there is any
if (!aIgnoreTabs) {
gfxFloat tabWidth = ComputeTabWidthAppUnits(mFrame);
if (tabWidth > 0) {
CalcTabWidths(aRange, tabWidth);
if (mTabWidths) {
mTabWidths->ApplySpacing(aSpacing,
aRange.start - mStart.GetSkippedOffset(),
aRange.Length());
}
}
}
// Now add in justification spacing
if (mJustificationSpacings.Length() > 0) {
// If there is any spaces trimmed at the end, aStart + aLength may
// be larger than the flags array. When that happens, we can simply
// ignore those spaces.
auto arrayEnd = mJustificationArrayStart +
static_cast<uint32_t>(mJustificationSpacings.Length());
auto end = std::min(aRange.end, arrayEnd);
MOZ_ASSERT(aRange.start >= mJustificationArrayStart);
for (auto i = aRange.start; i < end; i++) {
const auto& spacing =
mJustificationSpacings[i - mJustificationArrayStart];
uint32_t offset = i - aRange.start;
aSpacing[offset].mBefore += spacing.mBefore;
aSpacing[offset].mAfter += spacing.mAfter;
}
}
}
// aX and the result are in whole appunits.
static gfxFloat AdvanceToNextTab(gfxFloat aX, gfxFloat aTabWidth,
gfxFloat aMinAdvance) {
// Advance aX to the next multiple of aTabWidth. We must advance
// by at least aMinAdvance.
gfxFloat nextPos = aX + aMinAdvance;
return aTabWidth > 0.0 ? ceil(nextPos / aTabWidth) * aTabWidth : nextPos;
}
void nsTextFrame::PropertyProvider::CalcTabWidths(Range aRange,
gfxFloat aTabWidth) const {
MOZ_ASSERT(aTabWidth > 0);
if (!mTabWidths) {
if (mReflowing && !mLineContainer) {
// Intrinsic width computation does its own tab processing. We
// just don't do anything here.
return;
}
if (!mReflowing) {
mTabWidths = mFrame->GetProperty(TabWidthProperty());
#ifdef DEBUG
// If we're not reflowing, we should have already computed the
// tab widths; check that they're available as far as the last
// tab character present (if any)
for (uint32_t i = aRange.end; i > aRange.start; --i) {
if (mTextRun->CharIsTab(i - 1)) {
uint32_t startOffset = mStart.GetSkippedOffset();
NS_ASSERTION(mTabWidths && mTabWidths->mLimit + startOffset >= i,
"Precomputed tab widths are missing!");
break;
}
}
#endif
return;
}
}
uint32_t startOffset = mStart.GetSkippedOffset();
MOZ_ASSERT(aRange.start >= startOffset, "wrong start offset");
MOZ_ASSERT(aRange.end <= startOffset + mLength, "beyond the end");
uint32_t tabsEnd =
(mTabWidths ? mTabWidths->mLimit : mTabWidthsAnalyzedLimit) + startOffset;
if (tabsEnd < aRange.end) {
NS_ASSERTION(mReflowing,
"We need precomputed tab widths, but don't have enough.");
for (uint32_t i = tabsEnd; i < aRange.end; ++i) {
Spacing spacing;
GetSpacingInternal(Range(i, i + 1), &spacing, true);
mOffsetFromBlockOriginForTabs += spacing.mBefore;
if (!mTextRun->CharIsTab(i)) {
if (mTextRun->IsClusterStart(i)) {
uint32_t clusterEnd = i + 1;
while (clusterEnd < mTextRun->GetLength() &&
!mTextRun->IsClusterStart(clusterEnd)) {
++clusterEnd;
}
mOffsetFromBlockOriginForTabs +=
mTextRun->GetAdvanceWidth(Range(i, clusterEnd), nullptr);
}
} else {
if (!mTabWidths) {
mTabWidths = new TabWidthStore(mFrame->GetContentOffset());
mFrame->SetProperty(TabWidthProperty(), mTabWidths);
}
double nextTab = AdvanceToNextTab(mOffsetFromBlockOriginForTabs,
aTabWidth, MinTabAdvance());
mTabWidths->mWidths.AppendElement(
TabWidth(i - startOffset,
NSToIntRound(nextTab - mOffsetFromBlockOriginForTabs)));
mOffsetFromBlockOriginForTabs = nextTab;
}
mOffsetFromBlockOriginForTabs += spacing.mAfter;
}
if (mTabWidths) {
mTabWidths->mLimit = aRange.end - startOffset;
}
}
if (!mTabWidths) {
// Delete any stale property that may be left on the frame
mFrame->RemoveProperty(TabWidthProperty());
mTabWidthsAnalyzedLimit =
std::max(mTabWidthsAnalyzedLimit, aRange.end - startOffset);
}
}
gfxFloat nsTextFrame::PropertyProvider::GetHyphenWidth() const {
if (mHyphenWidth < 0) {
const auto& hyphenateChar = mTextStyle->mHyphenateCharacter;
if (hyphenateChar.IsAuto()) {
mHyphenWidth = GetFontGroup()->GetHyphenWidth(this);
} else {
RefPtr<gfxTextRun> hyphRun = GetHyphenTextRun(mFrame, nullptr);
mHyphenWidth = hyphRun ? hyphRun->GetAdvanceWidth() : 0;
}
}
return mHyphenWidth + mLetterSpacing;
}
static inline bool IS_HYPHEN(char16_t u) {
return u == char16_t('-') || // HYPHEN-MINUS
u == 0x058A || // ARMENIAN HYPHEN
u == 0x2010 || // HYPHEN
u == 0x2012 || // FIGURE DASH
u == 0x2013; // EN DASH
}
void nsTextFrame::PropertyProvider::GetHyphenationBreaks(
Range aRange, HyphenType* aBreakBefore) const {
MOZ_ASSERT(IsInBounds(mStart, mLength, aRange), "Range out of bounds");
MOZ_ASSERT(mLength != INT32_MAX, "Can't call this with undefined length");
if (!mTextStyle->WhiteSpaceCanWrap(mFrame) ||
mTextStyle->mHyphens == StyleHyphens::None) {
memset(aBreakBefore, static_cast<uint8_t>(HyphenType::None),
aRange.Length() * sizeof(HyphenType));
return;
}
// Iterate through the original-string character runs
nsSkipCharsRunIterator run(
mStart, nsSkipCharsRunIterator::LENGTH_UNSKIPPED_ONLY, aRange.Length());
run.SetSkippedOffset(aRange.start);
// We need to visit skipped characters so that we can detect SHY
run.SetVisitSkipped();
int32_t prevTrailingCharOffset = run.GetPos().GetOriginalOffset() - 1;
bool allowHyphenBreakBeforeNextChar =
prevTrailingCharOffset >= mStart.GetOriginalOffset() &&
prevTrailingCharOffset < mStart.GetOriginalOffset() + mLength &&
mFrag->CharAt(AssertedCast<uint32_t>(prevTrailingCharOffset)) == CH_SHY;
while (run.NextRun()) {
NS_ASSERTION(run.GetRunLength() > 0, "Shouldn't return zero-length runs");
if (run.IsSkipped()) {
// Check if there's a soft hyphen which would let us hyphenate before
// the next non-skipped character. Don't look at soft hyphens followed
// by other skipped characters, we won't use them.
allowHyphenBreakBeforeNextChar =
mFrag->CharAt(AssertedCast<uint32_t>(
run.GetOriginalOffset() + run.GetRunLength() - 1)) == CH_SHY;
} else {
int32_t runOffsetInSubstring = run.GetSkippedOffset() - aRange.start;
memset(aBreakBefore + runOffsetInSubstring,
static_cast<uint8_t>(HyphenType::None),
run.GetRunLength() * sizeof(HyphenType));
// Don't allow hyphen breaks at the start of the line
aBreakBefore[runOffsetInSubstring] =
allowHyphenBreakBeforeNextChar &&
(!mFrame->HasAnyStateBits(TEXT_START_OF_LINE) ||
run.GetSkippedOffset() > mStart.GetSkippedOffset())
? HyphenType::Soft
: HyphenType::None;
allowHyphenBreakBeforeNextChar = false;
}
}
if (mTextStyle->mHyphens == StyleHyphens::Auto) {
gfxSkipCharsIterator skipIter(mStart);
for (uint32_t i = 0; i < aRange.Length(); ++i) {
if (IS_HYPHEN(mFrag->CharAt(AssertedCast<uint32_t>(
skipIter.ConvertSkippedToOriginal(aRange.start + i))))) {
if (i < aRange.Length() - 1) {
aBreakBefore[i + 1] = HyphenType::Explicit;
}
continue;
}
if (mTextRun->CanHyphenateBefore(aRange.start + i) &&
aBreakBefore[i] == HyphenType::None) {
aBreakBefore[i] = HyphenType::AutoWithoutManualInSameWord;
}
}
}
}
void nsTextFrame::PropertyProvider::InitializeForDisplay(bool aTrimAfter) {
nsTextFrame::TrimmedOffsets trimmed = mFrame->GetTrimmedOffsets(
mFrag, (aTrimAfter ? nsTextFrame::TrimmedOffsetFlags::Default
: nsTextFrame::TrimmedOffsetFlags::NoTrimAfter));
mStart.SetOriginalOffset(trimmed.mStart);
mLength = trimmed.mLength;
if (mFrame->HasAnyStateBits(TEXT_START_OF_LINE)) {
mStartOfLineOffset = mStart.GetSkippedOffset();
}
SetupJustificationSpacing(true);
}
void nsTextFrame::PropertyProvider::InitializeForMeasure() {
nsTextFrame::TrimmedOffsets trimmed = mFrame->GetTrimmedOffsets(
mFrag, nsTextFrame::TrimmedOffsetFlags::NotPostReflow);
mStart.SetOriginalOffset(trimmed.mStart);
mLength = trimmed.mLength;
if (mFrame->HasAnyStateBits(TEXT_START_OF_LINE)) {
mStartOfLineOffset = mStart.GetSkippedOffset();
}
SetupJustificationSpacing(false);
}
void nsTextFrame::PropertyProvider::SetupJustificationSpacing(
bool aPostReflow) {
MOZ_ASSERT(mLength != INT32_MAX, "Can't call this with undefined length");
if (!mFrame->HasAnyStateBits(TEXT_JUSTIFICATION_ENABLED)) {
return;
}
gfxSkipCharsIterator start(mStart), end(mStart);
// We can't just use our mLength here; when InitializeForDisplay is
// called with false for aTrimAfter, we still shouldn't be assigning
// justification space to any trailing whitespace.
nsTextFrame::TrimmedOffsets trimmed = mFrame->GetTrimmedOffsets(
mFrag, (aPostReflow ? nsTextFrame::TrimmedOffsetFlags::Default
: nsTextFrame::TrimmedOffsetFlags::NotPostReflow));
end.AdvanceOriginal(trimmed.mLength);
gfxSkipCharsIterator realEnd(end);
Range range(uint32_t(start.GetOriginalOffset()),
uint32_t(end.GetOriginalOffset()));
nsTArray<JustificationAssignment> assignments;
JustificationInfo info = ComputeJustification(range, &assignments);
auto assign = mFrame->GetJustificationAssignment();
auto totalGaps = JustificationUtils::CountGaps(info, assign);
if (!totalGaps || assignments.IsEmpty()) {
// Nothing to do, nothing is justifiable and we shouldn't have any
// justification space assigned
return;
}
// Remember that textrun measurements are in the run's orientation,
// so its advance "width" is actually a height in vertical writing modes,
// corresponding to the inline-direction of the frame.
gfxFloat naturalWidth = mTextRun->GetAdvanceWidth(
Range(mStart.GetSkippedOffset(), realEnd.GetSkippedOffset()), this);
if (mFrame->HasAnyStateBits(TEXT_HYPHEN_BREAK)) {
naturalWidth += GetHyphenWidth();
}
nscoord totalSpacing = mFrame->ISize() - naturalWidth;
if (totalSpacing <= 0) {
// No space available
return;
}
assignments[0].mGapsAtStart = assign.mGapsAtStart;
assignments.LastElement().mGapsAtEnd = assign.mGapsAtEnd;
MOZ_ASSERT(mJustificationSpacings.IsEmpty());
JustificationApplicationState state(totalGaps, totalSpacing);
mJustificationSpacings.SetCapacity(assignments.Length());
for (const JustificationAssignment& assign : assignments) {
Spacing* spacing = mJustificationSpacings.AppendElement();
spacing->mBefore = state.Consume(assign.mGapsAtStart);
spacing->mAfter = state.Consume(assign.mGapsAtEnd);
}
}
void nsTextFrame::PropertyProvider::InitFontGroupAndFontMetrics() const {
if (!mFontMetrics) {
if (mWhichTextRun == nsTextFrame::eInflated) {
mFontMetrics = mFrame->InflatedFontMetrics();
} else {
mFontMetrics = nsLayoutUtils::GetFontMetricsForFrame(mFrame, 1.0f);
}
}
mFontGroup = mFontMetrics->GetThebesFontGroup();
}
#ifdef ACCESSIBILITY
a11y::AccType nsTextFrame::AccessibleType() {
if (IsEmpty()) {
RenderedText text =
GetRenderedText(0, UINT32_MAX, TextOffsetType::OffsetsInContentText,
TrailingWhitespace::DontTrim);
if (text.mString.IsEmpty()) {
return a11y::eNoType;
}
}
return a11y::eTextLeafType;
}
#endif
//-----------------------------------------------------------------------------
void nsTextFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
nsIFrame* aPrevInFlow) {
NS_ASSERTION(!aPrevInFlow, "Can't be a continuation!");
MOZ_ASSERT(aContent->IsText(), "Bogus content!");
// Remove any NewlineOffsetProperty or InFlowContentLengthProperty since they
// might be invalid if the content was modified while there was no frame
if (aContent->HasFlag(NS_HAS_NEWLINE_PROPERTY)) {
aContent->RemoveProperty(nsGkAtoms::newline);
aContent->UnsetFlags(NS_HAS_NEWLINE_PROPERTY);
}
if (aContent->HasFlag(NS_HAS_FLOWLENGTH_PROPERTY)) {
aContent->RemoveProperty(nsGkAtoms::flowlength);
aContent->UnsetFlags(NS_HAS_FLOWLENGTH_PROPERTY);
}
// Since our content has a frame now, this flag is no longer needed.
aContent->UnsetFlags(NS_CREATE_FRAME_IF_NON_WHITESPACE);
// We're not a continuing frame.
// mContentOffset = 0; not necessary since we get zeroed out at init
nsIFrame::Init(aContent, aParent, aPrevInFlow);
}
void nsTextFrame::ClearFrameOffsetCache() {
// See if we need to remove ourselves from the offset cache
if (HasAnyStateBits(TEXT_IN_OFFSET_CACHE)) {
nsIFrame* primaryFrame = mContent->GetPrimaryFrame();
if (primaryFrame) {
// The primary frame might be null here. For example,
// nsLineBox::DeleteLineList just destroys the frames in order, which
// means that the primary frame is already dead if we're a continuing text
// frame, in which case, all of its properties are gone, and we don't need
// to worry about deleting this property here.
primaryFrame->RemoveProperty(OffsetToFrameProperty());
}
RemoveStateBits(TEXT_IN_OFFSET_CACHE);
}
}
void nsTextFrame::Destroy(DestroyContext& aContext) {
ClearFrameOffsetCache();
// We might want to clear NS_CREATE_FRAME_IF_NON_WHITESPACE or
// NS_REFRAME_IF_WHITESPACE on mContent here, since our parent frame
// type might be changing. Not clear whether it's worth it.
ClearTextRuns();
if (mNextContinuation) {
mNextContinuation->SetPrevInFlow(nullptr);
}
// Let the base class destroy the frame
nsIFrame::Destroy(aContext);
}
nsTArray<nsTextFrame*>* nsTextFrame::GetContinuations() {
MOZ_ASSERT(NS_IsMainThread());
// Only for use on the primary frame, which has no prev-continuation.
MOZ_ASSERT(!GetPrevContinuation());
if (!mNextContinuation) {
return nullptr;
}
if (mPropertyFlags & PropertyFlags::Continuations) {
return GetProperty(ContinuationsProperty());
}
size_t count = 0;
for (nsIFrame* f = this; f; f = f->GetNextContinuation()) {
++count;
}
auto* continuations = new nsTArray<nsTextFrame*>;
if (continuations->SetCapacity(count, fallible)) {
for (nsTextFrame* f = this; f;
f = static_cast<nsTextFrame*>(f->GetNextContinuation())) {
continuations->AppendElement(f);
}
} else {
delete continuations;
continuations = nullptr;
}
AddProperty(ContinuationsProperty(), continuations);
mPropertyFlags |= PropertyFlags::Continuations;
return continuations;
}
class nsContinuingTextFrame final : public nsTextFrame {
public:
NS_DECL_FRAMEARENA_HELPERS(nsContinuingTextFrame)
friend nsIFrame* NS_NewContinuingTextFrame(mozilla::PresShell* aPresShell,
ComputedStyle* aStyle);
void Init(nsIContent* aContent, nsContainerFrame* aParent,
nsIFrame* aPrevInFlow) final;
void Destroy(DestroyContext&) override;
nsTextFrame* GetPrevContinuation() const final { return mPrevContinuation; }
void SetPrevContinuation(nsIFrame* aPrevContinuation) final {
NS_ASSERTION(!aPrevContinuation || Type() == aPrevContinuation->Type(),
"setting a prev continuation with incorrect type!");
NS_ASSERTION(
!nsSplittableFrame::IsInPrevContinuationChain(aPrevContinuation, this),
"creating a loop in continuation chain!");
mPrevContinuation = static_cast<nsTextFrame*>(aPrevContinuation);
RemoveStateBits(NS_FRAME_IS_FLUID_CONTINUATION);
UpdateCachedContinuations();
}
nsTextFrame* GetPrevInFlow() const final {
return HasAnyStateBits(NS_FRAME_IS_FLUID_CONTINUATION) ? mPrevContinuation
: nullptr;
}
void SetPrevInFlow(nsIFrame* aPrevInFlow) final {
NS_ASSERTION(!aPrevInFlow || Type() == aPrevInFlow->Type(),
"setting a prev in flow with incorrect type!");
NS_ASSERTION(
!nsSplittableFrame::IsInPrevContinuationChain(aPrevInFlow, this),
"creating a loop in continuation chain!");
mPrevContinuation = static_cast<nsTextFrame*>(aPrevInFlow);
AddStateBits(NS_FRAME_IS_FLUID_CONTINUATION);
UpdateCachedContinuations();
}
// Call this helper to update cache after mPrevContinuation is changed.
void UpdateCachedContinuations() {
nsTextFrame* prevFirst = mFirstContinuation;
if (mPrevContinuation) {
mFirstContinuation = mPrevContinuation->FirstContinuation();
if (mFirstContinuation) {
mFirstContinuation->ClearCachedContinuations();
}
} else {
mFirstContinuation = nullptr;
}
if (mFirstContinuation != prevFirst) {
if (prevFirst) {
prevFirst->ClearCachedContinuations();
}
auto* f = static_cast<nsContinuingTextFrame*>(mNextContinuation);
while (f) {
f->mFirstContinuation = mFirstContinuation;
f = static_cast<nsContinuingTextFrame*>(f->mNextContinuation);
}
}
}
nsIFrame* FirstInFlow() const final;
nsTextFrame* FirstContinuation() const final {
#if DEBUG
// If we have a prev-continuation pointer, then our first-continuation
// must be the same as that frame's.
if (mPrevContinuation) {
// If there's a prev-prev, then we can safely cast mPrevContinuation to
// an nsContinuingTextFrame and access its mFirstContinuation pointer
// directly, to avoid recursively calling FirstContinuation(), leading
// to exponentially-slow behavior in the assertion.
if (mPrevContinuation->GetPrevContinuation()) {
auto* prev = static_cast<nsContinuingTextFrame*>(mPrevContinuation);
MOZ_ASSERT(mFirstContinuation == prev->mFirstContinuation);
} else {
MOZ_ASSERT(mFirstContinuation ==
mPrevContinuation->FirstContinuation());
}
} else {
MOZ_ASSERT(!mFirstContinuation);
}
#endif
return mFirstContinuation;
};
void AddInlineMinISize(const IntrinsicSizeInput& aInput,
InlineMinISizeData* aData) final {
// Do nothing, since the first-in-flow accounts for everything.
}
void AddInlinePrefISize(const IntrinsicSizeInput& aInput,
InlinePrefISizeData* aData) final {
// Do nothing, since the first-in-flow accounts for everything.
}
protected:
explicit nsContinuingTextFrame(ComputedStyle* aStyle,
nsPresContext* aPresContext)
: nsTextFrame(aStyle, aPresContext, kClassID) {}
nsTextFrame* mPrevContinuation = nullptr;
nsTextFrame* mFirstContinuation = nullptr;
};
void nsContinuingTextFrame::Init(nsIContent* aContent,
nsContainerFrame* aParent,
nsIFrame* aPrevInFlow) {
NS_ASSERTION(aPrevInFlow, "Must be a continuation!");
// Hook the frame into the flow
nsTextFrame* prev = static_cast<nsTextFrame*>(aPrevInFlow);
nsTextFrame* nextContinuation = prev->GetNextContinuation();
SetPrevInFlow(aPrevInFlow);
aPrevInFlow->SetNextInFlow(this);
// NOTE: bypassing nsTextFrame::Init!!!
nsIFrame::Init(aContent, aParent, aPrevInFlow);
mContentOffset = prev->GetContentOffset() + prev->GetContentLengthHint();
NS_ASSERTION(mContentOffset < int32_t(aContent->GetText()->GetLength()),
"Creating ContinuingTextFrame, but there is no more content");
if (prev->Style() != Style()) {
// We're taking part of prev's text, and its style may be different
// so clear its textrun which may no longer be valid (and don't set ours)
prev->ClearTextRuns();
} else {
float inflation = prev->GetFontSizeInflation();
SetFontSizeInflation(inflation);
mTextRun = prev->GetTextRun(nsTextFrame::eInflated);
if (inflation != 1.0f) {
gfxTextRun* uninflatedTextRun =
prev->GetTextRun(nsTextFrame::eNotInflated);
if (uninflatedTextRun) {
SetTextRun(uninflatedTextRun, nsTextFrame::eNotInflated, 1.0f);
}
}
}
if (aPrevInFlow->HasAnyStateBits(NS_FRAME_IS_BIDI)) {
FrameBidiData bidiData = aPrevInFlow->GetBidiData();
bidiData.precedingControl = kBidiLevelNone;
SetProperty(BidiDataProperty(), bidiData);
if (nextContinuation) {
SetNextContinuation(nextContinuation);
nextContinuation->SetPrevContinuation(this);
// Adjust next-continuations' content offset as needed.
while (nextContinuation &&
nextContinuation->GetContentOffset() < mContentOffset) {
#ifdef DEBUG
FrameBidiData nextBidiData = nextContinuation->GetBidiData();
NS_ASSERTION(bidiData.embeddingLevel == nextBidiData.embeddingLevel &&
bidiData.baseLevel == nextBidiData.baseLevel,
"stealing text from different type of BIDI continuation");
MOZ_ASSERT(nextBidiData.precedingControl == kBidiLevelNone,
"There shouldn't be any virtual bidi formatting character "
"between continuations");
#endif
nextContinuation->mContentOffset = mContentOffset;
nextContinuation = nextContinuation->GetNextContinuation();
}
}
AddStateBits(NS_FRAME_IS_BIDI);
} // prev frame is bidi
}
void nsContinuingTextFrame::Destroy(DestroyContext& aContext) {
ClearFrameOffsetCache();
// The text associated with this frame will become associated with our
// prev-continuation. If that means the text has changed style, then
// we need to wipe out the text run for the text.
// Note that mPrevContinuation can be null if we're destroying the whole
// frame chain from the start to the end.
// If this frame is mentioned in the userData for a textrun (say
// because there's a direction change at the start of this frame), then
// we have to clear the textrun because we're going away and the
// textrun had better not keep a dangling reference to us.
if (IsInTextRunUserData() ||
(mPrevContinuation && mPrevContinuation->Style() != Style())) {
ClearTextRuns();
// Clear the previous continuation's text run also, so that it can rebuild
// the text run to include our text.
if (mPrevContinuation) {
mPrevContinuation->ClearTextRuns();
}
}
nsSplittableFrame::RemoveFromFlow(this);
// Let the base class destroy the frame
nsIFrame::Destroy(aContext);
}
nsIFrame* nsContinuingTextFrame::FirstInFlow() const {
// Can't cast to |nsContinuingTextFrame*| because the first one isn't.
nsIFrame *firstInFlow,
*previous = const_cast<nsIFrame*>(static_cast<const nsIFrame*>(this));
do {
firstInFlow = previous;
previous = firstInFlow->GetPrevInFlow();
} while (previous);
MOZ_ASSERT(firstInFlow, "post-condition failed");
return firstInFlow;
}
// XXX Do we want to do all the work for the first-in-flow or do the
// work for each part? (Be careful of first-letter / first-line, though,
// especially first-line!) Doing all the work on the first-in-flow has
// the advantage of avoiding the potential for incremental reflow bugs,
// but depends on our maintining the frame tree in reasonable ways even
// for edge cases (block-within-inline splits, nextBidi, etc.)
// XXX We really need to make :first-letter happen during frame
// construction.
nscoord nsTextFrame::IntrinsicISize(const IntrinsicSizeInput& aInput,
IntrinsicISizeType aType) {
return IntrinsicISizeFromInline(aInput, aType);
}
//----------------------------------------------------------------------
#if defined(DEBUG_rbs) || defined(DEBUG_bzbarsky)
static void VerifyNotDirty(nsFrameState state) {
bool isZero = state & NS_FRAME_FIRST_REFLOW;
bool isDirty = state & NS_FRAME_IS_DIRTY;
if (!isZero && isDirty) {
NS_WARNING("internal offsets may be out-of-sync");
}
}
# define DEBUG_VERIFY_NOT_DIRTY(state) VerifyNotDirty(state)
#else
# define DEBUG_VERIFY_NOT_DIRTY(state)
#endif
nsIFrame* NS_NewTextFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
return new (aPresShell) nsTextFrame(aStyle, aPresShell->GetPresContext());
}
NS_IMPL_FRAMEARENA_HELPERS(nsTextFrame)
nsIFrame* NS_NewContinuingTextFrame(PresShell* aPresShell,
ComputedStyle* aStyle) {
return new (aPresShell)
nsContinuingTextFrame(aStyle, aPresShell->GetPresContext());
}
NS_IMPL_FRAMEARENA_HELPERS(nsContinuingTextFrame)
nsTextFrame::~nsTextFrame() = default;
nsIFrame::Cursor nsTextFrame::GetCursor(const nsPoint& aPoint) {
StyleCursorKind kind = StyleUI()->Cursor().keyword;
if (kind == StyleCursorKind::Auto) {
if (!IsSelectable(nullptr)) {
kind = StyleCursorKind::Default;
} else {
kind = GetWritingMode().IsVertical() ? StyleCursorKind::VerticalText
: StyleCursorKind::Text;
}
}
return Cursor{kind, AllowCustomCursorImage::Yes};
}
nsTextFrame* nsTextFrame::LastInFlow() const {
nsTextFrame* lastInFlow = const_cast<nsTextFrame*>(this);
while (lastInFlow->GetNextInFlow()) {
lastInFlow = lastInFlow->GetNextInFlow();
}
MOZ_ASSERT(lastInFlow, "post-condition failed");
return lastInFlow;
}
nsTextFrame* nsTextFrame::LastContinuation() const {
nsTextFrame* lastContinuation = const_cast<nsTextFrame*>(this);
while (lastContinuation->mNextContinuation) {
lastContinuation = lastContinuation->mNextContinuation;
}
MOZ_ASSERT(lastContinuation, "post-condition failed");
return lastContinuation;
}
bool nsTextFrame::ShouldSuppressLineBreak() const {
// If the parent frame of the text frame is ruby content box, it must
// suppress line break inside. This check is necessary, because when
// a whitespace is only contained by pseudo ruby frames, its style
// context won't have SuppressLineBreak bit set.
if (mozilla::RubyUtils::IsRubyContentBox(GetParent()->Type())) {
return true;
}
return Style()->ShouldSuppressLineBreak();
}
void nsTextFrame::InvalidateFrame(uint32_t aDisplayItemKey,
bool aRebuildDisplayItems) {
InvalidateSelectionState();
if (IsInSVGTextSubtree()) {
nsIFrame* svgTextFrame = nsLayoutUtils::GetClosestFrameOfType(
GetParent(), LayoutFrameType::SVGText);
svgTextFrame->InvalidateFrame();
return;
}
nsIFrame::InvalidateFrame(aDisplayItemKey, aRebuildDisplayItems);
}
void nsTextFrame::InvalidateFrameWithRect(const nsRect& aRect,
uint32_t aDisplayItemKey,
bool aRebuildDisplayItems) {
InvalidateSelectionState();
if (IsInSVGTextSubtree()) {
nsIFrame* svgTextFrame = nsLayoutUtils::GetClosestFrameOfType(
GetParent(), LayoutFrameType::SVGText);
svgTextFrame->InvalidateFrame();
return;
}
nsIFrame::InvalidateFrameWithRect(aRect, aDisplayItemKey,
aRebuildDisplayItems);
}
gfxTextRun* nsTextFrame::GetUninflatedTextRun() const {
return GetProperty(UninflatedTextRunProperty());
}
void nsTextFrame::SetTextRun(gfxTextRun* aTextRun, TextRunType aWhichTextRun,
float aInflation) {
NS_ASSERTION(aTextRun, "must have text run");
// Our inflated text run is always stored in mTextRun. In the cases
// where our current inflation is not 1.0, however, we store two text
// runs, and the uninflated one goes in a frame property. We never
// store a single text run in both.
if (aWhichTextRun == eInflated) {
if (HasFontSizeInflation() && aInflation == 1.0f) {
// FIXME: Probably shouldn't do this within each SetTextRun
// method, but it doesn't hurt.
ClearTextRun(nullptr, nsTextFrame::eNotInflated);
}
SetFontSizeInflation(aInflation);
} else {
MOZ_ASSERT(aInflation == 1.0f, "unexpected inflation");
if (HasFontSizeInflation()) {
// Setting the property will not automatically increment the textrun's
// reference count, so we need to do it here.
aTextRun->AddRef();
SetProperty(UninflatedTextRunProperty(), aTextRun);
return;
}
// fall through to setting mTextRun
}
mTextRun = aTextRun;
// FIXME: Add assertions testing the relationship between
// GetFontSizeInflation() and whether we have an uninflated text run
// (but be aware that text runs can go away).
}
bool nsTextFrame::RemoveTextRun(gfxTextRun* aTextRun) {
if (aTextRun == mTextRun) {
mTextRun = nullptr;
mFontMetrics = nullptr;
return true;
}
if (HasAnyStateBits(TEXT_HAS_FONT_INFLATION) &&
GetProperty(UninflatedTextRunProperty()) == aTextRun) {
RemoveProperty(UninflatedTextRunProperty());
return true;
}
return false;
}
void nsTextFrame::ClearTextRun(nsTextFrame* aStartContinuation,
TextRunType aWhichTextRun) {
RefPtr<gfxTextRun> textRun = GetTextRun(aWhichTextRun);
if (!textRun) {
return;
}
if (aWhichTextRun == nsTextFrame::eInflated) {
mFontMetrics = nullptr;
}
DebugOnly<bool> checkmTextrun = textRun == mTextRun;
UnhookTextRunFromFrames(textRun, aStartContinuation);
MOZ_ASSERT(checkmTextrun ? !mTextRun
: !GetProperty(UninflatedTextRunProperty()));
}
void nsTextFrame::DisconnectTextRuns() {
MOZ_ASSERT(!IsInTextRunUserData(),
"Textrun mentions this frame in its user data so we can't just "
"disconnect");
mTextRun = nullptr;
if (HasAnyStateBits(TEXT_HAS_FONT_INFLATION)) {
RemoveProperty(UninflatedTextRunProperty());
}
}
void nsTextFrame::NotifyNativeAnonymousTextnodeChange(uint32_t aOldLength) {
MOZ_ASSERT(mContent->IsInNativeAnonymousSubtree());
MarkIntrinsicISizesDirty();
// This is to avoid making a new Reflow request in CharacterDataChanged:
for (nsTextFrame* f = this; f; f = f->GetNextContinuation()) {
f->MarkSubtreeDirty();
f->mReflowRequestedForCharDataChange = true;
}
// Pretend that all the text changed.
CharacterDataChangeInfo info;
info.mAppend = false;
info.mChangeStart = 0;
info.mChangeEnd = aOldLength;
info.mReplaceLength = GetContent()->TextLength();
CharacterDataChanged(info);
}
nsresult nsTextFrame::CharacterDataChanged(
const CharacterDataChangeInfo& aInfo) {
if (mContent->HasFlag(NS_HAS_NEWLINE_PROPERTY)) {
mContent->RemoveProperty(nsGkAtoms::newline);
mContent->UnsetFlags(NS_HAS_NEWLINE_PROPERTY);
}
if (mContent->HasFlag(NS_HAS_FLOWLENGTH_PROPERTY)) {
mContent->RemoveProperty(nsGkAtoms::flowlength);
mContent->UnsetFlags(NS_HAS_FLOWLENGTH_PROPERTY);
}
// Find the first frame whose text has changed. Frames that are entirely
// before the text change are completely unaffected.
nsTextFrame* next;
nsTextFrame* textFrame = this;
while (true) {
next = textFrame->GetNextContinuation();
if (!next || next->GetContentOffset() > int32_t(aInfo.mChangeStart)) {
break;
}
textFrame = next;
}
int32_t endOfChangedText = aInfo.mChangeStart + aInfo.mReplaceLength;
// Parent of the last frame that we passed to FrameNeedsReflow (or noticed
// had already received an earlier FrameNeedsReflow call).
// (For subsequent frames with this same parent, we can just set their
// dirty bit without bothering to call FrameNeedsReflow again.)
nsIFrame* lastDirtiedFrameParent = nullptr;
mozilla::PresShell* presShell = PresShell();
do {
// textFrame contained deleted text (or the insertion point,
// if this was a pure insertion).
textFrame->RemoveStateBits(TEXT_WHITESPACE_FLAGS);
textFrame->ClearTextRuns();
nsIFrame* parentOfTextFrame = textFrame->GetParent();
bool areAncestorsAwareOfReflowRequest = false;
if (lastDirtiedFrameParent == parentOfTextFrame) {
// An earlier iteration of this loop already called
// FrameNeedsReflow for a sibling of |textFrame|.
areAncestorsAwareOfReflowRequest = true;
} else {
lastDirtiedFrameParent = parentOfTextFrame;
}
if (textFrame->mReflowRequestedForCharDataChange) {
// We already requested a reflow for this frame; nothing to do.
MOZ_ASSERT(textFrame->HasAnyStateBits(NS_FRAME_IS_DIRTY),
"mReflowRequestedForCharDataChange should only be set "
"on dirty frames");
} else {
// Make sure textFrame is queued up for a reflow. Also set a flag so we
// don't waste time doing this again in repeated calls to this method.
textFrame->mReflowRequestedForCharDataChange = true;
if (!areAncestorsAwareOfReflowRequest) {
// Ask the parent frame to reflow me.
presShell->FrameNeedsReflow(
textFrame, IntrinsicDirty::FrameAncestorsAndDescendants,
NS_FRAME_IS_DIRTY);
} else {
// We already called FrameNeedsReflow on behalf of an earlier sibling,
// so we can just mark this frame as dirty and don't need to bother
// telling its ancestors.
// Note: if the parent is a block, we're cheating here because we should
// be marking our line dirty, but we're not. nsTextFrame::SetLength will
// do that when it gets called during reflow.
textFrame->MarkSubtreeDirty();
}
}
textFrame->InvalidateFrame();
// Below, frames that start after the deleted text will be adjusted so that
// their offsets move with the trailing unchanged text. If this change
// deletes more text than it inserts, those frame offsets will decrease.
// We need to maintain the invariant that mContentOffset is non-decreasing
// along the continuation chain. So we need to ensure that frames that
// started in the deleted text are all still starting before the
// unchanged text.
if (textFrame->mContentOffset > endOfChangedText) {
textFrame->mContentOffset = endOfChangedText;
}
textFrame = textFrame->GetNextContinuation();
} while (textFrame &&
textFrame->GetContentOffset() < int32_t(aInfo.mChangeEnd));
// This is how much the length of the string changed by --- i.e.,
// how much the trailing unchanged text moved.
int32_t sizeChange =
aInfo.mChangeStart + aInfo.mReplaceLength - aInfo.mChangeEnd;
if (sizeChange) {
// Fix the offsets of the text frames that start in the trailing
// unchanged text.
while (textFrame) {
textFrame->mContentOffset += sizeChange;
// XXX we could rescue some text runs by adjusting their user data
// to reflect the change in DOM offsets
textFrame->ClearTextRuns();
textFrame = textFrame->GetNextContinuation();
}
}
return NS_OK;
}
NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(TextCombineScaleFactorProperty, float)
float nsTextFrame::GetTextCombineScaleFactor(nsTextFrame* aFrame) {
float factor = aFrame->GetProperty(TextCombineScaleFactorProperty());
return factor ? factor : 1.0f;
}
void nsTextFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
const nsDisplayListSet& aLists) {
if (!IsVisibleForPainting()) {
return;
}
DO_GLOBAL_REFLOW_COUNT_DSP("nsTextFrame");
const nsStyleText* st = StyleText();
bool isTextTransparent =
NS_GET_A(st->mWebkitTextFillColor.CalcColor(this)) == 0 &&
NS_GET_A(st->mWebkitTextStrokeColor.CalcColor(this)) == 0;
if ((HasAnyStateBits(TEXT_NO_RENDERED_GLYPHS) ||
(isTextTransparent && !StyleText()->HasTextShadow())) &&
aBuilder->IsForPainting() && !IsInSVGTextSubtree()) {
if (!IsSelected()) {
TextDecorations textDecs;
GetTextDecorations(PresContext(), eResolvedColors, textDecs);
if (!textDecs.HasDecorationLines()) {
if (auto* currentPresContext = aBuilder->CurrentPresContext()) {
currentPresContext->SetBuiltInvisibleText();
}
return;
}
}
}
aLists.Content()->AppendNewToTop<nsDisplayText>(aBuilder, this);
}
UniquePtr<SelectionDetails> nsTextFrame::GetSelectionDetails() {
const nsFrameSelection* frameSelection = GetConstFrameSelection();
if (frameSelection->IsInTableSelectionMode()) {
return nullptr;
}
UniquePtr<SelectionDetails> details = frameSelection->LookUpSelection(
mContent, GetContentOffset(), GetContentLength(), false);
for (SelectionDetails* sd = details.get(); sd; sd = sd->mNext.get()) {
sd->mStart += mContentOffset;
sd->mEnd += mContentOffset;
}
return details;
}
static void PaintSelectionBackground(
DrawTarget& aDrawTarget, nscolor aColor, const LayoutDeviceRect& aDirtyRect,
const LayoutDeviceRect& aRect, nsTextFrame::DrawPathCallbacks* aCallbacks) {
Rect rect = aRect.Intersect(aDirtyRect).ToUnknownRect();
MaybeSnapToDevicePixels(rect, aDrawTarget);
if (aCallbacks) {
aCallbacks->NotifySelectionBackgroundNeedsFill(rect, aColor, aDrawTarget);
} else {
ColorPattern color(ToDeviceColor(aColor));
aDrawTarget.FillRect(rect, color);
}
}
// Attempt to get the LineBaselineOffset property of aChildFrame
// If not set, calculate this value for all child frames of aBlockFrame
static nscoord LazyGetLineBaselineOffset(nsIFrame* aChildFrame,
nsBlockFrame* aBlockFrame) {
bool offsetFound;
nscoord offset =
aChildFrame->GetProperty(nsIFrame::LineBaselineOffset(), &offsetFound);
if (!offsetFound) {
for (const auto& line : aBlockFrame->Lines()) {
if (line.IsInline()) {
int32_t n = line.GetChildCount();
nscoord lineBaseline = line.BStart() + line.GetLogicalAscent();
for (auto* lineFrame = line.mFirstChild; n > 0;
lineFrame = lineFrame->GetNextSibling(), --n) {
offset = lineBaseline - lineFrame->GetNormalPosition().y;
lineFrame->SetProperty(nsIFrame::LineBaselineOffset(), offset);
}
}
}
return aChildFrame->GetProperty(nsIFrame::LineBaselineOffset(),
&offsetFound);
} else {
return offset;
}
}
static bool IsUnderlineRight(const ComputedStyle& aStyle) {
// Check for 'left' or 'right' explicitly specified in the property;
// if neither is there, we use auto positioning based on lang.
const auto position = aStyle.StyleText()->mTextUnderlinePosition;
if (position.IsLeft()) {
return false;
}
if (position.IsRight()) {
return true;
}
// If neither 'left' nor 'right' was specified, check the language.
nsAtom* langAtom = aStyle.StyleFont()->mLanguage;
if (!langAtom) {
return false;
}
return nsStyleUtil::MatchesLanguagePrefix(langAtom, u"ja") ||
nsStyleUtil::MatchesLanguagePrefix(langAtom, u"ko") ||
nsStyleUtil::MatchesLanguagePrefix(langAtom, u"mn");
}
void nsTextFrame::GetTextDecorations(
nsPresContext* aPresContext,
nsTextFrame::TextDecorationColorResolution aColorResolution,
nsTextFrame::TextDecorations& aDecorations) {
const nsCompatibility compatMode = aPresContext->CompatibilityMode();
bool useOverride = false;
nscolor overrideColor = NS_RGBA(0, 0, 0, 0);
bool nearestBlockFound = false;
// Use writing mode of parent frame for orthogonal text frame to work.
// See comment in nsTextFrame::DrawTextRunAndDecorations.
WritingMode wm = GetParent()->GetWritingMode();
bool vertical = wm.IsVertical();
nscoord ascent = GetLogicalBaseline(wm);
// physicalBlockStartOffset represents the offset from our baseline
// to f's physical block start, which is top in horizontal writing
// mode, and left in vertical writing modes, in our coordinate space.
// This physical block start is logical block start in most cases,
// but for vertical-rl, it is logical block end, and consequently in
// that case, it starts from the descent instead of ascent.
nscoord physicalBlockStartOffset =
wm.IsVerticalRL() ? GetSize().width - ascent : ascent;
// baselineOffset represents the offset from our baseline to f's baseline or
// the nearest block's baseline, in our coordinate space, whichever is closest
// during the particular iteration
nscoord baselineOffset = 0;
for (nsIFrame *f = this, *fChild = nullptr; f;
fChild = f, f = nsLayoutUtils::GetParentOrPlaceholderFor(f)) {
ComputedStyle* const context = f->Style();
if (!context->HasTextDecorationLines()) {
break;
}
if (context->GetPseudoType() == PseudoStyleType::marker &&
(context->StyleList()->mListStylePosition ==
StyleListStylePosition::Outside ||
!context->StyleDisplay()->IsInlineOutsideStyle())) {
// Outside ::marker pseudos, and inside markers that aren't inlines, don't
// have text decorations.
break;
}
const nsStyleTextReset* const styleTextReset = context->StyleTextReset();
StyleTextDecorationLine textDecorations =
styleTextReset->mTextDecorationLine;
bool ignoreSubproperties = false;
auto lineStyle = styleTextReset->mTextDecorationStyle;
if (textDecorations == StyleTextDecorationLine::SPELLING_ERROR ||
textDecorations == StyleTextDecorationLine::GRAMMAR_ERROR) {
nscolor lineColor;
float relativeSize;
useOverride = nsTextPaintStyle::GetSelectionUnderline(
this, nsTextPaintStyle::SelectionStyleIndex::SpellChecker, &lineColor,
&relativeSize, &lineStyle);
if (useOverride) {
// We don't currently have a SelectionStyleIndex::GrammarChecker; for
// now just use SpellChecker and change its color to green.
overrideColor =
textDecorations == StyleTextDecorationLine::SPELLING_ERROR
? lineColor
: NS_RGBA(0, 128, 0, 255);
textDecorations = StyleTextDecorationLine::UNDERLINE;
ignoreSubproperties = true;
}
}
if (!useOverride &&
(StyleTextDecorationLine::COLOR_OVERRIDE & textDecorations)) {
// This handles the <a href="blah.html"><font color="green">La
// la la</font></a> case. The link underline should be green.
useOverride = true;
overrideColor = nsLayoutUtils::GetTextColor(
f, &nsStyleTextReset::mTextDecorationColor);
}
nsBlockFrame* fBlock = do_QueryFrame(f);
const bool firstBlock = !nearestBlockFound && fBlock;
// Not updating positions once we hit a parent block is equivalent to
// the CSS 2.1 spec that blocks should propagate decorations down to their
// children (albeit the style should be preserved)
// However, if we're vertically aligned within a block, then we need to
// recover the correct baseline from the line by querying the FrameProperty
// that should be set (see nsLineLayout::VerticalAlignLine).
if (firstBlock) {
// At this point, fChild can't be null since TextFrames can't be blocks
Maybe<StyleVerticalAlignKeyword> verticalAlign =
fChild->VerticalAlignEnum();
if (verticalAlign != Some(StyleVerticalAlignKeyword::Baseline)) {
// Since offset is the offset in the child's coordinate space, we have
// to undo the accumulation to bring the transform out of the block's
// coordinate space
const nscoord lineBaselineOffset =
LazyGetLineBaselineOffset(fChild, fBlock);
baselineOffset = physicalBlockStartOffset - lineBaselineOffset -
(vertical ? fChild->GetNormalPosition().x
: fChild->GetNormalPosition().y);
}
} else if (!nearestBlockFound) {
// offset here is the offset from f's baseline to f's top/left
// boundary. It's descent for vertical-rl, and ascent otherwise.
nscoord offset = wm.IsVerticalRL()
? f->GetSize().width - f->GetLogicalBaseline(wm)
: f->GetLogicalBaseline(wm);
baselineOffset = physicalBlockStartOffset - offset;
}
nearestBlockFound = nearestBlockFound || firstBlock;
physicalBlockStartOffset +=
vertical ? f->GetNormalPosition().x : f->GetNormalPosition().y;
if (textDecorations) {
nscolor color;
if (useOverride) {
color = overrideColor;
} else if (IsInSVGTextSubtree()) {
// XXX We might want to do something with text-decoration-color when
// painting SVG text, but it's not clear what we should do. We
// at least need SVG text decorations to paint with 'fill' if
// text-decoration-color has its initial value currentColor.
// We could choose to interpret currentColor as "currentFill"
// for SVG text, and have e.g. text-decoration-color:red to
// override the fill paint of the decoration.
color = aColorResolution == eResolvedColors
? nsLayoutUtils::GetTextColor(f, &nsStyleSVG::mFill)
: NS_SAME_AS_FOREGROUND_COLOR;
} else {
color = nsLayoutUtils::GetTextColor(
f, &nsStyleTextReset::mTextDecorationColor);
}
bool swapUnderlineAndOverline =
wm.IsCentralBaseline() && IsUnderlineRight(*context);
const auto kUnderline = swapUnderlineAndOverline
? StyleTextDecorationLine::OVERLINE
: StyleTextDecorationLine::UNDERLINE;
const auto kOverline = swapUnderlineAndOverline
? StyleTextDecorationLine::UNDERLINE
: StyleTextDecorationLine::OVERLINE;
const nsStyleText* const styleText = context->StyleText();
const auto position = ignoreSubproperties
? StyleTextUnderlinePosition::AUTO
: styleText->mTextUnderlinePosition;
const auto offset = ignoreSubproperties ? LengthPercentageOrAuto::Auto()
: styleText->mTextUnderlineOffset;
const auto thickness = ignoreSubproperties
? StyleTextDecorationLength::Auto()
: styleTextReset->mTextDecorationThickness;
if (textDecorations & kUnderline) {
aDecorations.mUnderlines.AppendElement(nsTextFrame::LineDecoration(
f, baselineOffset, position, offset, thickness, color, lineStyle,
!ignoreSubproperties));
}
if (textDecorations & kOverline) {
aDecorations.mOverlines.AppendElement(nsTextFrame::LineDecoration(
f, baselineOffset, position, offset, thickness, color, lineStyle,
!ignoreSubproperties));
}
if (textDecorations & StyleTextDecorationLine::LINE_THROUGH) {
aDecorations.mStrikes.AppendElement(nsTextFrame::LineDecoration(
f, baselineOffset, position, offset, thickness, color, lineStyle,
!ignoreSubproperties));
}
}
// In all modes, if we're on an inline-block/table/grid/flex (or
// -moz-inline-box), we're done.
// If we're on a ruby frame other than ruby text container, we
// should continue.
mozilla::StyleDisplay display = f->GetDisplay();
if (!display.IsInlineFlow() &&
(!display.IsRuby() ||
display == mozilla::StyleDisplay::RubyTextContainer) &&
display.IsInlineOutside()) {
break;
}
// In quirks mode, if we're on an HTML table element, we're done.
if (compatMode == eCompatibility_NavQuirks &&
f->GetContent()->IsHTMLElement(nsGkAtoms::table)) {
break;
}
// If we're on an absolutely-positioned element or a floating
// element, we're done.
if (f->IsFloating() || f->IsAbsolutelyPositioned()) {
break;
}
// If we're an outer <svg> element, which is classified as an atomic
// inline-level element, we're done.
if (f->IsSVGOuterSVGFrame()) {
break;
}
}
}
static float GetInflationForTextDecorations(nsIFrame* aFrame,
nscoord aInflationMinFontSize) {
if (aFrame->IsInSVGTextSubtree()) {
auto* container =
nsLayoutUtils::GetClosestFrameOfType(aFrame, LayoutFrameType::SVGText);
MOZ_ASSERT(container);
return static_cast<SVGTextFrame*>(container)->GetFontSizeScaleFactor();
}
return nsLayoutUtils::FontSizeInflationInner(aFrame, aInflationMinFontSize);
}
struct EmphasisMarkInfo {
RefPtr<gfxTextRun> textRun;
gfxFloat advance;
gfxFloat baselineOffset;
};
NS_DECLARE_FRAME_PROPERTY_DELETABLE(EmphasisMarkProperty, EmphasisMarkInfo)
static void ComputeTextEmphasisStyleString(const StyleTextEmphasisStyle& aStyle,
nsAString& aOut) {
MOZ_ASSERT(!aStyle.IsNone());
if (aStyle.IsString()) {
nsDependentCSubstring string = aStyle.AsString().AsString();
AppendUTF8toUTF16(string, aOut);
return;
}
const auto& keyword = aStyle.AsKeyword();
const bool fill = keyword.fill == StyleTextEmphasisFillMode::Filled;
switch (keyword.shape) {
case StyleTextEmphasisShapeKeyword::Dot:
return aOut.AppendLiteral(fill ? u"\u2022" : u"\u25e6");
case StyleTextEmphasisShapeKeyword::Circle:
return aOut.AppendLiteral(fill ? u"\u25cf" : u"\u25cb");
case StyleTextEmphasisShapeKeyword::DoubleCircle:
return aOut.AppendLiteral(fill ? u"\u25c9" : u"\u25ce");
case StyleTextEmphasisShapeKeyword::Triangle:
return aOut.AppendLiteral(fill ? u"\u25b2" : u"\u25b3");
case StyleTextEmphasisShapeKeyword::Sesame:
return aOut.AppendLiteral(fill ? u"\ufe45" : u"\ufe46");
default:
MOZ_ASSERT_UNREACHABLE("Unknown emphasis style shape");
}
}
static already_AddRefed<gfxTextRun> GenerateTextRunForEmphasisMarks(
nsTextFrame* aFrame, gfxFontGroup* aFontGroup,
ComputedStyle* aComputedStyle, const nsStyleText* aStyleText) {
nsAutoString string;
ComputeTextEmphasisStyleString(aStyleText->mTextEmphasisStyle, string);
RefPtr<DrawTarget> dt = CreateReferenceDrawTarget(aFrame);
auto appUnitsPerDevUnit = aFrame->PresContext()->AppUnitsPerDevPixel();
gfx::ShapedTextFlags flags =
nsLayoutUtils::GetTextRunOrientFlagsForStyle(aComputedStyle);
if (flags == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_MIXED) {
// The emphasis marks should always be rendered upright per spec.
flags = gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT;
}
return aFontGroup->MakeTextRun<char16_t>(string.get(), string.Length(), dt,
appUnitsPerDevUnit, flags,
nsTextFrameUtils::Flags(), nullptr);
}
static nsRubyFrame* FindFurthestInlineRubyAncestor(nsTextFrame* aFrame) {
nsRubyFrame* rubyFrame = nullptr;
for (nsIFrame* frame = aFrame->GetParent();
frame && frame->IsLineParticipant(); frame = frame->GetParent()) {
if (frame->IsRubyFrame()) {
rubyFrame = static_cast<nsRubyFrame*>(frame);
}
}
return rubyFrame;
}
nsRect nsTextFrame::UpdateTextEmphasis(WritingMode aWM,
PropertyProvider& aProvider) {
const nsStyleText* styleText = StyleText();
if (!styleText->HasEffectiveTextEmphasis()) {
RemoveProperty(EmphasisMarkProperty());
return nsRect();
}
ComputedStyle* computedStyle = Style();
bool isTextCombined = computedStyle->IsTextCombined();
if (isTextCombined) {
computedStyle = GetParent()->Style();
}
RefPtr<nsFontMetrics> fm = nsLayoutUtils::GetFontMetricsOfEmphasisMarks(
computedStyle, PresContext(), GetFontSizeInflation());
EmphasisMarkInfo* info = new EmphasisMarkInfo;
info->textRun = GenerateTextRunForEmphasisMarks(
this, fm->GetThebesFontGroup(), computedStyle, styleText);
info->advance = info->textRun->GetAdvanceWidth();
// Calculate the baseline offset
LogicalSide side = styleText->TextEmphasisSide(aWM, StyleFont()->mLanguage);
LogicalSize frameSize = GetLogicalSize(aWM);
// The overflow rect is inflated in the inline direction by half
// advance of the emphasis mark on each side, so that even if a mark
// is drawn for a zero-width character, it won't be clipped.
LogicalRect overflowRect(aWM, -info->advance / 2,
/* BStart to be computed below */ 0,
frameSize.ISize(aWM) + info->advance,
fm->MaxAscent() + fm->MaxDescent());
RefPtr<nsFontMetrics> baseFontMetrics =
isTextCombined
? nsLayoutUtils::GetInflatedFontMetricsForFrame(GetParent())
: do_AddRef(aProvider.GetFontMetrics());
// When the writing mode is vertical-lr the line is inverted, and thus
// the ascent and descent are swapped.
nscoord absOffset = (side == LogicalSide::BStart) != aWM.IsLineInverted()
? baseFontMetrics->MaxAscent() + fm->MaxDescent()
: baseFontMetrics->MaxDescent() + fm->MaxAscent();
RubyBlockLeadings leadings;
if (nsRubyFrame* ruby = FindFurthestInlineRubyAncestor(this)) {
leadings = ruby->GetBlockLeadings();
}
if (side == LogicalSide::BStart) {
info->baselineOffset = -absOffset - leadings.mStart;
overflowRect.BStart(aWM) = -overflowRect.BSize(aWM) - leadings.mStart;
} else {
MOZ_ASSERT(side == LogicalSide::BEnd);
info->baselineOffset = absOffset + leadings.mEnd;
overflowRect.BStart(aWM) = frameSize.BSize(aWM) + leadings.mEnd;
}
// If text combined, fix the gap between the text frame and its parent.
if (isTextCombined) {
nscoord gap = (baseFontMetrics->MaxHeight() - frameSize.BSize(aWM)) / 2;
overflowRect.BStart(aWM) += gap * (side == LogicalSide::BStart ? -1 : 1);
}
SetProperty(EmphasisMarkProperty(), info);
return overflowRect.GetPhysicalRect(aWM, frameSize.GetPhysicalSize(aWM));
}
// helper function for implementing text-decoration-thickness
// Returns the thickness in device pixels.
static gfxFloat ComputeDecorationLineThickness(
const StyleTextDecorationLength& aThickness, const gfxFloat aAutoValue,
const gfxFont::Metrics& aFontMetrics, const gfxFloat aAppUnitsPerDevPixel,
const nsIFrame* aFrame) {
if (aThickness.IsAuto()) {
return aAutoValue;
}
if (aThickness.IsFromFont()) {
return aFontMetrics.underlineSize;
}
auto em = [&] { return aFrame->StyleFont()->mSize.ToAppUnits(); };
return aThickness.AsLengthPercentage().Resolve(em) / aAppUnitsPerDevPixel;
}
// Helper function for implementing text-underline-offset and -position
// Returns the offset in device pixels.
static gfxFloat ComputeDecorationLineOffset(
StyleTextDecorationLine aLineType,
const StyleTextUnderlinePosition& aPosition,
const LengthPercentageOrAuto& aOffset, const gfxFont::Metrics& aFontMetrics,
const gfxFloat aAppUnitsPerDevPixel, const nsIFrame* aFrame,
bool aIsCentralBaseline, bool aSwappedUnderline) {
// Em value to use if we need to resolve a percentage length.
auto em = [&] { return aFrame->StyleFont()->mSize.ToAppUnits(); };
// If we're in vertical-upright typographic mode, we need to compute the
// offset of the decoration line from the default central baseline.
if (aIsCentralBaseline) {
// Line-through simply goes at the (central) baseline.
if (aLineType == StyleTextDecorationLine::LINE_THROUGH) {
return 0;
}
// Compute "zero position" for the under- or overline.
gfxFloat zeroPos = 0.5 * aFontMetrics.emHeight;
// aOffset applies to underline only; for overline (or offset:auto) we use
// a somewhat arbitrary offset of half the font's (horziontal-mode) value
// for underline-offset, to get a little bit of separation between glyph
// edges and the line in typical cases.
// If we have swapped under-/overlines for text-underline-position:right,
// we need to take account of this to determine which decoration lines are
// "real" underlines which should respect the text-underline-* values.
bool isUnderline =
(aLineType == StyleTextDecorationLine::UNDERLINE) != aSwappedUnderline;
gfxFloat offset =
isUnderline && !aOffset.IsAuto()
? aOffset.AsLengthPercentage().Resolve(em) / aAppUnitsPerDevPixel
: aFontMetrics.underlineOffset * -0.5;
// Direction of the decoration line's offset from the central baseline.
gfxFloat dir = aLineType == StyleTextDecorationLine::OVERLINE ? 1.0 : -1.0;
return dir * (zeroPos + offset);
}
// Compute line offset for horizontal typographic mode.
if (aLineType == StyleTextDecorationLine::UNDERLINE) {
if (aPosition.IsFromFont()) {
gfxFloat zeroPos = aFontMetrics.underlineOffset;
gfxFloat offset =
aOffset.IsAuto()
? 0
: aOffset.AsLengthPercentage().Resolve(em) / aAppUnitsPerDevPixel;
return zeroPos - offset;
}
if (aPosition.IsUnder()) {
gfxFloat zeroPos = -aFontMetrics.maxDescent;
gfxFloat offset =
aOffset.IsAuto()
? -0.5 * aFontMetrics.underlineOffset
: aOffset.AsLengthPercentage().Resolve(em) / aAppUnitsPerDevPixel;
return zeroPos - offset;
}
// text-underline-position must be 'auto', so zero position is the
// baseline and 'auto' offset will apply the font's underline-offset.
//
// If offset is `auto`, we clamp the offset (in horizontal typographic mode)
// to a minimum of 1/16 em (equivalent to 1px at font-size 16px) to mitigate
// skip-ink issues with fonts that leave the underlineOffset field as zero.
MOZ_ASSERT(aPosition.IsAuto());
return aOffset.IsAuto() ? std::min(aFontMetrics.underlineOffset,
-aFontMetrics.emHeight / 16.0)
: -aOffset.AsLengthPercentage().Resolve(em) /
aAppUnitsPerDevPixel;
}
if (aLineType == StyleTextDecorationLine::OVERLINE) {
return aFontMetrics.maxAscent;
}
if (aLineType == StyleTextDecorationLine::LINE_THROUGH) {
return aFontMetrics.strikeoutOffset;
}
MOZ_ASSERT_UNREACHABLE("unknown decoration line type");
return 0;
}
void nsTextFrame::UnionAdditionalOverflow(nsPresContext* aPresContext,
nsIFrame* aBlock,
PropertyProvider& aProvider,
nsRect* aInkOverflowRect,
bool aIncludeTextDecorations,
bool aIncludeShadows) {
const WritingMode wm = GetWritingMode();
bool verticalRun = mTextRun->IsVertical();
const gfxFloat appUnitsPerDevUnit = aPresContext->AppUnitsPerDevPixel();
if (IsFloatingFirstLetterChild()) {
bool inverted = wm.IsLineInverted();
// The underline/overline drawable area must be contained in the overflow
// rect when this is in floating first letter frame at *both* modes.
// In this case, aBlock is the ::first-letter frame.
auto decorationStyle =
aBlock->Style()->StyleTextReset()->mTextDecorationStyle;
// If the style is none, let's include decoration line rect as solid style
// since changing the style from none to solid/dotted/dashed doesn't cause
// reflow.
if (decorationStyle == StyleTextDecorationStyle::None) {
decorationStyle = StyleTextDecorationStyle::Solid;
}
nsCSSRendering::DecorationRectParams params;
bool useVerticalMetrics = verticalRun && mTextRun->UseCenterBaseline();
nsFontMetrics* fontMetrics = aProvider.GetFontMetrics();
RefPtr<gfxFont> font =
fontMetrics->GetThebesFontGroup()->GetFirstValidFont();
const gfxFont::Metrics& metrics =
font->GetMetrics(useVerticalMetrics ? nsFontMetrics::eVertical
: nsFontMetrics::eHorizontal);
params.defaultLineThickness = metrics.underlineSize;
params.lineSize.height = ComputeDecorationLineThickness(
aBlock->Style()->StyleTextReset()->mTextDecorationThickness,
params.defaultLineThickness, metrics, appUnitsPerDevUnit, this);
const auto* styleText = aBlock->StyleText();
bool swapUnderline =
wm.IsCentralBaseline() && IsUnderlineRight(*aBlock->Style());
params.offset = ComputeDecorationLineOffset(
StyleTextDecorationLine::UNDERLINE, styleText->mTextUnderlinePosition,
styleText->mTextUnderlineOffset, metrics, appUnitsPerDevUnit, this,
wm.IsCentralBaseline(), swapUnderline);
nscoord maxAscent =
inverted ? fontMetrics->MaxDescent() : fontMetrics->MaxAscent();
Float gfxWidth =
(verticalRun ? aInkOverflowRect->height : aInkOverflowRect->width) /
appUnitsPerDevUnit;
params.lineSize.width = gfxWidth;
params.ascent = gfxFloat(mAscent) / appUnitsPerDevUnit;
params.style = decorationStyle;
params.vertical = verticalRun;
params.sidewaysLeft = mTextRun->IsSidewaysLeft();
params.decoration = StyleTextDecorationLine::UNDERLINE;
nsRect underlineRect =
nsCSSRendering::GetTextDecorationRect(aPresContext, params);
// TODO(jfkthame):
// Should we actually be calling ComputeDecorationLineOffset again here?
params.offset = maxAscent / appUnitsPerDevUnit;
params.decoration = StyleTextDecorationLine::OVERLINE;
nsRect overlineRect =
nsCSSRendering::GetTextDecorationRect(aPresContext, params);
aInkOverflowRect->UnionRect(*aInkOverflowRect, underlineRect);
aInkOverflowRect->UnionRect(*aInkOverflowRect, overlineRect);
// XXX If strikeoutSize is much thicker than the underlineSize, it may
// cause overflowing from the overflow rect. However, such case
// isn't realistic, we don't need to compute it now.
}
if (aIncludeTextDecorations) {
// Use writing mode of parent frame for orthogonal text frame to
// work. See comment in nsTextFrame::DrawTextRunAndDecorations.
WritingMode parentWM = GetParent()->GetWritingMode();
bool verticalDec = parentWM.IsVertical();
bool useVerticalMetrics =
verticalDec != verticalRun
? verticalDec
: verticalRun && mTextRun->UseCenterBaseline();
// Since CSS 2.1 requires that text-decoration defined on ancestors maintain
// style and position, they can be drawn at virtually any y-offset, so
// maxima and minima are required to reliably generate the rectangle for
// them
TextDecorations textDecs;
GetTextDecorations(aPresContext, eResolvedColors, textDecs);
if (textDecs.HasDecorationLines()) {
nscoord inflationMinFontSize =
nsLayoutUtils::InflationMinFontSizeFor(aBlock);
const nscoord measure = verticalDec ? GetSize().height : GetSize().width;
gfxFloat gfxWidth = measure / appUnitsPerDevUnit;
gfxFloat ascent =
gfxFloat(GetLogicalBaseline(parentWM)) / appUnitsPerDevUnit;
nscoord frameBStart = 0;
if (parentWM.IsVerticalRL()) {
frameBStart = GetSize().width;
ascent = -ascent;
}
nsCSSRendering::DecorationRectParams params;
params.lineSize = Size(gfxWidth, 0);
params.ascent = ascent;
params.vertical = verticalDec;
params.sidewaysLeft = mTextRun->IsSidewaysLeft();
nscoord topOrLeft(nscoord_MAX), bottomOrRight(nscoord_MIN);
typedef gfxFont::Metrics Metrics;
auto accumulateDecorationRect =
[&](const LineDecoration& dec, gfxFloat Metrics::* lineSize,
mozilla::StyleTextDecorationLine lineType) {
params.style = dec.mStyle;
// If the style is solid, let's include decoration line rect of
// solid style since changing the style from none to
// solid/dotted/dashed doesn't cause reflow.
if (params.style == StyleTextDecorationStyle::None) {
params.style = StyleTextDecorationStyle::Solid;
}
float inflation = GetInflationForTextDecorations(
dec.mFrame, inflationMinFontSize);
const Metrics metrics =
GetFirstFontMetrics(GetFontGroupForFrame(dec.mFrame, inflation),
useVerticalMetrics);
params.defaultLineThickness = metrics.*lineSize;
params.lineSize.height = ComputeDecorationLineThickness(
dec.mTextDecorationThickness, params.defaultLineThickness,
metrics, appUnitsPerDevUnit, this);
bool swapUnderline =
parentWM.IsCentralBaseline() && IsUnderlineRight(*Style());
params.offset = ComputeDecorationLineOffset(
lineType, dec.mTextUnderlinePosition, dec.mTextUnderlineOffset,
metrics, appUnitsPerDevUnit, this, parentWM.IsCentralBaseline(),
swapUnderline);
const nsRect decorationRect =
nsCSSRendering::GetTextDecorationRect(aPresContext, params) +
(verticalDec ? nsPoint(frameBStart - dec.mBaselineOffset, 0)
: nsPoint(0, -dec.mBaselineOffset));
if (verticalDec) {
topOrLeft = std::min(decorationRect.x, topOrLeft);
bottomOrRight = std::max(decorationRect.XMost(), bottomOrRight);
} else {
topOrLeft = std::min(decorationRect.y, topOrLeft);
bottomOrRight = std::max(decorationRect.YMost(), bottomOrRight);
}
};
// Below we loop through all text decorations and compute the rectangle
// containing all of them, in this frame's coordinate space
params.decoration = StyleTextDecorationLine::UNDERLINE;
for (const LineDecoration& dec : textDecs.mUnderlines) {
accumulateDecorationRect(dec, &Metrics::underlineSize,
params.decoration);
}
params.decoration = StyleTextDecorationLine::OVERLINE;
for (const LineDecoration& dec : textDecs.mOverlines) {
accumulateDecorationRect(dec, &Metrics::underlineSize,
params.decoration);
}
params.decoration = StyleTextDecorationLine::LINE_THROUGH;
for (const LineDecoration& dec : textDecs.mStrikes) {
accumulateDecorationRect(dec, &Metrics::strikeoutSize,
params.decoration);
}
aInkOverflowRect->UnionRect(
*aInkOverflowRect,
verticalDec
? nsRect(topOrLeft, 0, bottomOrRight - topOrLeft, measure)
: nsRect(0, topOrLeft, measure, bottomOrRight - topOrLeft));
}
aInkOverflowRect->UnionRect(*aInkOverflowRect,
UpdateTextEmphasis(parentWM, aProvider));
}
// text-stroke overflows: add half of text-stroke-width on all sides
nscoord textStrokeWidth = StyleText()->mWebkitTextStrokeWidth;
if (textStrokeWidth > 0) {
// Inflate rect by stroke-width/2; we add an extra pixel to allow for
// antialiasing, rounding errors, etc.
nsRect strokeRect = *aInkOverflowRect;
strokeRect.Inflate(textStrokeWidth / 2 + appUnitsPerDevUnit);
aInkOverflowRect->UnionRect(*aInkOverflowRect, strokeRect);
}
// Text-shadow overflows
if (aIncludeShadows) {
*aInkOverflowRect =
nsLayoutUtils::GetTextShadowRectsUnion(*aInkOverflowRect, this);
}
// When this frame is not selected, the text-decoration area must be in
// frame bounds.
if (!IsSelected() ||
!CombineSelectionUnderlineRect(aPresContext, *aInkOverflowRect)) {
return;
}
AddStateBits(TEXT_SELECTION_UNDERLINE_OVERFLOWED);
}
nscoord nsTextFrame::ComputeLineHeight() const {
return ReflowInput::CalcLineHeight(*Style(), PresContext(), GetContent(),
NS_UNCONSTRAINEDSIZE,
GetFontSizeInflation());
}
gfxFloat nsTextFrame::ComputeDescentLimitForSelectionUnderline(
nsPresContext* aPresContext, const gfxFont::Metrics& aFontMetrics) {
const gfxFloat lineHeight =
gfxFloat(ComputeLineHeight()) / aPresContext->AppUnitsPerDevPixel();
if (lineHeight <= aFontMetrics.maxHeight) {
return aFontMetrics.maxDescent;
}
return aFontMetrics.maxDescent + (lineHeight - aFontMetrics.maxHeight) / 2;
}
// Make sure this stays in sync with DrawSelectionDecorations below
static constexpr SelectionTypeMask kSelectionTypesWithDecorations =
ToSelectionTypeMask(SelectionType::eSpellCheck) |
ToSelectionTypeMask(SelectionType::eURLStrikeout) |
ToSelectionTypeMask(SelectionType::eIMERawClause) |
ToSelectionTypeMask(SelectionType::eIMESelectedRawClause) |
ToSelectionTypeMask(SelectionType::eIMEConvertedClause) |
ToSelectionTypeMask(SelectionType::eIMESelectedClause);
/* static */
gfxFloat nsTextFrame::ComputeSelectionUnderlineHeight(
nsPresContext* aPresContext, const gfxFont::Metrics& aFontMetrics,
SelectionType aSelectionType) {
switch (aSelectionType) {
case SelectionType::eIMERawClause:
case SelectionType::eIMESelectedRawClause:
case SelectionType::eIMEConvertedClause:
case SelectionType::eIMESelectedClause:
return aFontMetrics.underlineSize;
case SelectionType::eSpellCheck: {
// The thickness of the spellchecker underline shouldn't honor the font
// metrics. It should be constant pixels value which is decided from the
// default font size. Note that if the actual font size is smaller than
// the default font size, we should use the actual font size because the
// computed value from the default font size can be too thick for the
// current font size.
Length defaultFontSize =
aPresContext->Document()
->GetFontPrefsForLang(nullptr)
->GetDefaultFont(StyleGenericFontFamily::None)
->size;
int32_t zoomedFontSize = aPresContext->CSSPixelsToDevPixels(
nsStyleFont::ZoomText(*aPresContext->Document(), defaultFontSize)
.ToCSSPixels());
gfxFloat fontSize =
std::min(gfxFloat(zoomedFontSize), aFontMetrics.emHeight);
fontSize = std::max(fontSize, 1.0);
return ceil(fontSize / 20);
}
default:
NS_WARNING("Requested underline style is not valid");
return aFontMetrics.underlineSize;
}
}
enum class DecorationType { Normal, Selection };
struct nsTextFrame::PaintDecorationLineParams
: nsCSSRendering::DecorationRectParams {
gfxContext* context = nullptr;
LayoutDeviceRect dirtyRect;
Point pt;
const nscolor* overrideColor = nullptr;
nscolor color = NS_RGBA(0, 0, 0, 0);
gfxFloat icoordInFrame = 0.0f;
gfxFloat baselineOffset = 0.0f;
DecorationType decorationType = DecorationType::Normal;
DrawPathCallbacks* callbacks = nullptr;
bool paintingShadows = false;
bool allowInkSkipping = true;
};
void nsTextFrame::PaintDecorationLine(
const PaintDecorationLineParams& aParams) {
nsCSSRendering::PaintDecorationLineParams params;
static_cast<nsCSSRendering::DecorationRectParams&>(params) = aParams;
params.dirtyRect = aParams.dirtyRect.ToUnknownRect();
params.pt = aParams.pt;
params.color = aParams.overrideColor ? *aParams.overrideColor : aParams.color;
params.icoordInFrame = Float(aParams.icoordInFrame);
params.baselineOffset = Float(aParams.baselineOffset);
params.allowInkSkipping = aParams.allowInkSkipping;
if (aParams.callbacks) {
Rect path = nsCSSRendering::DecorationLineToPath(params);
if (aParams.decorationType == DecorationType::Normal) {
aParams.callbacks->PaintDecorationLine(path, aParams.paintingShadows,
params.color);
} else {
aParams.callbacks->PaintSelectionDecorationLine(
path, aParams.paintingShadows, params.color);
}
} else {
nsCSSRendering::PaintDecorationLine(this, *aParams.context->GetDrawTarget(),
params);
}
}
static StyleTextDecorationStyle ToStyleLineStyle(const TextRangeStyle& aStyle) {
switch (aStyle.mLineStyle) {
case TextRangeStyle::LineStyle::None:
return StyleTextDecorationStyle::None;
case TextRangeStyle::LineStyle::Solid:
return StyleTextDecorationStyle::Solid;
case TextRangeStyle::LineStyle::Dotted:
return StyleTextDecorationStyle::Dotted;
case TextRangeStyle::LineStyle::Dashed:
return StyleTextDecorationStyle::Dashed;
case TextRangeStyle::LineStyle::Double:
return StyleTextDecorationStyle::Double;
case TextRangeStyle::LineStyle::Wavy:
return StyleTextDecorationStyle::Wavy;
}
MOZ_ASSERT_UNREACHABLE("Invalid line style");
return StyleTextDecorationStyle::None;
}
/**
* This, plus kSelectionTypesWithDecorations, encapsulates all knowledge
* about drawing text decoration for selections.
*/
void nsTextFrame::DrawSelectionDecorations(
gfxContext* aContext, const LayoutDeviceRect& aDirtyRect,
SelectionType aSelectionType, nsTextPaintStyle& aTextPaintStyle,
const TextRangeStyle& aRangeStyle, const Point& aPt,
gfxFloat aICoordInFrame, gfxFloat aWidth, gfxFloat aAscent,
const gfxFont::Metrics& aFontMetrics, DrawPathCallbacks* aCallbacks,
bool aVertical, StyleTextDecorationLine aDecoration) {
PaintDecorationLineParams params;
params.context = aContext;
params.dirtyRect = aDirtyRect;
params.pt = aPt;
params.lineSize.width = aWidth;
params.ascent = aAscent;
params.decoration = aDecoration;
params.decorationType = DecorationType::Selection;
params.callbacks = aCallbacks;
params.vertical = aVertical;
params.sidewaysLeft = mTextRun->IsSidewaysLeft();
params.descentLimit = ComputeDescentLimitForSelectionUnderline(
aTextPaintStyle.PresContext(), aFontMetrics);
float relativeSize;
const auto& decThickness = StyleTextReset()->mTextDecorationThickness;
const gfxFloat appUnitsPerDevPixel =
aTextPaintStyle.PresContext()->AppUnitsPerDevPixel();
const WritingMode wm = GetWritingMode();
switch (aSelectionType) {
case SelectionType::eIMERawClause:
case SelectionType::eIMESelectedRawClause:
case SelectionType::eIMEConvertedClause:
case SelectionType::eIMESelectedClause:
case SelectionType::eSpellCheck:
case SelectionType::eHighlight: {
auto index = nsTextPaintStyle::GetUnderlineStyleIndexForSelectionType(
aSelectionType);
bool weDefineSelectionUnderline =
aTextPaintStyle.GetSelectionUnderlineForPaint(
index, &params.color, &relativeSize, &params.style);
params.defaultLineThickness = ComputeSelectionUnderlineHeight(
aTextPaintStyle.PresContext(), aFontMetrics, aSelectionType);
params.lineSize.height = ComputeDecorationLineThickness(
decThickness, params.defaultLineThickness, aFontMetrics,
appUnitsPerDevPixel, this);
bool swapUnderline = wm.IsCentralBaseline() && IsUnderlineRight(*Style());
const auto* styleText = StyleText();
params.offset = ComputeDecorationLineOffset(
aDecoration, styleText->mTextUnderlinePosition,
styleText->mTextUnderlineOffset, aFontMetrics, appUnitsPerDevPixel,
this, wm.IsCentralBaseline(), swapUnderline);
bool isIMEType = aSelectionType != SelectionType::eSpellCheck &&
aSelectionType != SelectionType::eHighlight;
if (isIMEType) {
// IME decoration lines should not be drawn on the both ends, i.e., we
// need to cut both edges of the decoration lines. Because same style
// IME selections can adjoin, but the users need to be able to know
// where are the boundaries of the selections.
//
// X: underline
//
// IME selection #1 IME selection #2 IME selection #3
// | | |
// | XXXXXXXXXXXXXXXXXXX | XXXXXXXXXXXXXXXXXXXX | XXXXXXXXXXXXXXXXXXX
// +---------------------+----------------------+--------------------
// ^ ^ ^ ^ ^
// gap gap gap
params.pt.x += 1.0;
params.lineSize.width -= 2.0;
}
if (isIMEType && aRangeStyle.IsDefined()) {
// If IME defines the style, that should override our definition.
if (aRangeStyle.IsLineStyleDefined()) {
if (aRangeStyle.mLineStyle == TextRangeStyle::LineStyle::None) {
return;
}
params.style = ToStyleLineStyle(aRangeStyle);
relativeSize = aRangeStyle.mIsBoldLine ? 2.0f : 1.0f;
} else if (!weDefineSelectionUnderline) {
// There is no underline style definition.
return;
}
// If underline color is defined and that doesn't depend on the
// foreground color, we should use the color directly.
if (aRangeStyle.IsUnderlineColorDefined() &&
(!aRangeStyle.IsForegroundColorDefined() ||
aRangeStyle.mUnderlineColor != aRangeStyle.mForegroundColor)) {
params.color = aRangeStyle.mUnderlineColor;
}
// If foreground color or background color is defined, the both colors
// are computed by GetSelectionTextColors(). Then, we should use its
// foreground color always. The color should have sufficient contrast
// with the background color.
else if (aRangeStyle.IsForegroundColorDefined() ||
aRangeStyle.IsBackgroundColorDefined()) {
nscolor bg;
GetSelectionTextColors(aSelectionType, nullptr, aTextPaintStyle,
aRangeStyle, &params.color, &bg);
}
// Otherwise, use the foreground color of the frame.
else {
params.color = aTextPaintStyle.GetTextColor();
}
} else if (!weDefineSelectionUnderline) {
// IME doesn't specify the selection style and we don't define selection
// underline.
return;
}
break;
}
case SelectionType::eURLStrikeout: {
nscoord inflationMinFontSize =
nsLayoutUtils::InflationMinFontSizeFor(this);
float inflation =
GetInflationForTextDecorations(this, inflationMinFontSize);
const gfxFont::Metrics metrics =
GetFirstFontMetrics(GetFontGroupForFrame(this, inflation), aVertical);
relativeSize = 2.0f;
aTextPaintStyle.GetURLSecondaryColor(&params.color);
params.style = StyleTextDecorationStyle::Solid;
params.defaultLineThickness = metrics.strikeoutSize;
params.lineSize.height = ComputeDecorationLineThickness(
decThickness, params.defaultLineThickness, metrics,
appUnitsPerDevPixel, this);
// TODO(jfkthame): ComputeDecorationLineOffset? check vertical mode!
params.offset = metrics.strikeoutOffset + 0.5;
params.decoration = StyleTextDecorationLine::LINE_THROUGH;
break;
}
default:
NS_WARNING("Requested selection decorations when there aren't any");
return;
}
params.lineSize.height *= relativeSize;
params.defaultLineThickness *= relativeSize;
params.icoordInFrame =
(aVertical ? params.pt.y - aPt.y : params.pt.x - aPt.x) + aICoordInFrame;
PaintDecorationLine(params);
}
/* static */
bool nsTextFrame::GetSelectionTextColors(SelectionType aSelectionType,
nsAtom* aHighlightName,
nsTextPaintStyle& aTextPaintStyle,
const TextRangeStyle& aRangeStyle,
nscolor* aForeground,
nscolor* aBackground) {
switch (aSelectionType) {
case SelectionType::eNormal:
return aTextPaintStyle.GetSelectionColors(aForeground, aBackground);
case SelectionType::eFind:
aTextPaintStyle.GetHighlightColors(aForeground, aBackground);
return true;
case SelectionType::eHighlight: {
// Intentionally not short-cutting here because the called methods have
// side-effects that affect outparams.
bool hasForeground = aTextPaintStyle.GetCustomHighlightTextColor(
aHighlightName, aForeground);
bool hasBackground = aTextPaintStyle.GetCustomHighlightBackgroundColor(
aHighlightName, aBackground);
return hasForeground || hasBackground;
}
case SelectionType::eTargetText: {
aTextPaintStyle.GetTargetTextColors(aForeground, aBackground);
return true;
}
case SelectionType::eURLSecondary:
aTextPaintStyle.GetURLSecondaryColor(aForeground);
*aBackground = NS_RGBA(0, 0, 0, 0);
return true;
case SelectionType::eIMERawClause:
case SelectionType::eIMESelectedRawClause:
case SelectionType::eIMEConvertedClause:
case SelectionType::eIMESelectedClause:
if (aRangeStyle.IsDefined()) {
if (!aRangeStyle.IsForegroundColorDefined() &&
!aRangeStyle.IsBackgroundColorDefined()) {
*aForeground = aTextPaintStyle.GetTextColor();
*aBackground = NS_RGBA(0, 0, 0, 0);
return false;
}
if (aRangeStyle.IsForegroundColorDefined()) {
*aForeground = aRangeStyle.mForegroundColor;
if (aRangeStyle.IsBackgroundColorDefined()) {
*aBackground = aRangeStyle.mBackgroundColor;
} else {
// If foreground color is defined but background color isn't
// defined, we can guess that IME must expect that the background
// color is system's default field background color.
*aBackground = aTextPaintStyle.GetSystemFieldBackgroundColor();
}
} else { // aRangeStyle.IsBackgroundColorDefined() is true
*aBackground = aRangeStyle.mBackgroundColor;
// If background color is defined but foreground color isn't defined,
// we can assume that IME must expect that the foreground color is
// same as system's field text color.
*aForeground = aTextPaintStyle.GetSystemFieldForegroundColor();
}
return true;
}
aTextPaintStyle.GetIMESelectionColors(
nsTextPaintStyle::GetUnderlineStyleIndexForSelectionType(
aSelectionType),
aForeground, aBackground);
return true;
default:
*aForeground = aTextPaintStyle.GetTextColor();
*aBackground = NS_RGBA(0, 0, 0, 0);
return false;
}
}
/**
* This sets *aShadows to the appropriate shadows, if any, for the given
* type of selection.
* If text-shadow was not specified, *aShadows is left untouched.
*/
void nsTextFrame::GetSelectionTextShadow(
SelectionType aSelectionType, nsTextPaintStyle& aTextPaintStyle,
Span<const StyleSimpleShadow>* aShadows) {
if (aSelectionType != SelectionType::eNormal) {
return;
}
aTextPaintStyle.GetSelectionShadow(aShadows);
}
/**
* This class lets us iterate over chunks of text recorded in an array of
* resolved selection ranges, observing cluster boundaries, in content order,
* maintaining the current x-offset as we go, and telling whether the text
* chunk has a hyphen after it or not.
* In addition to returning the selected chunks, the iterator is responsible
* to interpolate unselected chunks in any gaps between them.
* The caller is responsible for actually computing the advance width of each
* chunk.
*/
class MOZ_STACK_CLASS SelectionRangeIterator {
using PropertyProvider = nsTextFrame::PropertyProvider;
using CombinedSelectionRange = nsTextFrame::PriorityOrderedSelectionsForRange;
public:
// aSelectionRanges and aRange are according to the original string.
SelectionRangeIterator(
const nsTArray<CombinedSelectionRange>& aSelectionRanges,
gfxTextRun::Range aRange, PropertyProvider& aProvider,
gfxTextRun* aTextRun, gfxFloat aXOffset);
bool GetNextSegment(gfxFloat* aXOffset, gfxTextRun::Range* aRange,
gfxFloat* aHyphenWidth,
nsTArray<SelectionType>& aSelectionType,
nsTArray<RefPtr<nsAtom>>& aHighlightName,
nsTArray<TextRangeStyle>& aStyle);
void UpdateWithAdvance(gfxFloat aAdvance) {
mXOffset += aAdvance * mTextRun->GetDirection();
}
private:
const nsTArray<CombinedSelectionRange>& mSelectionRanges;
PropertyProvider& mProvider;
gfxTextRun* mTextRun;
gfxSkipCharsIterator mIterator;
gfxTextRun::Range mOriginalRange;
gfxFloat mXOffset;
uint32_t mIndex;
};
SelectionRangeIterator::SelectionRangeIterator(
const nsTArray<nsTextFrame::PriorityOrderedSelectionsForRange>&
aSelectionRanges,
gfxTextRun::Range aRange, PropertyProvider& aProvider, gfxTextRun* aTextRun,
gfxFloat aXOffset)
: mSelectionRanges(aSelectionRanges),
mProvider(aProvider),
mTextRun(aTextRun),
mIterator(aProvider.GetStart()),
mOriginalRange(aRange),
mXOffset(aXOffset),
mIndex(0) {
mIterator.SetOriginalOffset(int32_t(aRange.start));
}
bool SelectionRangeIterator::GetNextSegment(
gfxFloat* aXOffset, gfxTextRun::Range* aRange, gfxFloat* aHyphenWidth,
nsTArray<SelectionType>& aSelectionType,
nsTArray<RefPtr<nsAtom>>& aHighlightName,
nsTArray<TextRangeStyle>& aStyle) {
if (mIterator.GetOriginalOffset() >= int32_t(mOriginalRange.end)) {
return false;
}
uint32_t runOffset = mIterator.GetSkippedOffset();
uint32_t segmentEnd = mOriginalRange.end;
aSelectionType.Clear();
aHighlightName.Clear();
aStyle.Clear();
if (mIndex == mSelectionRanges.Length() ||
mIterator.GetOriginalOffset() <
int32_t(mSelectionRanges[mIndex].mRange.start)) {
// There's an unselected segment before the next range (or at the end).
aSelectionType.AppendElement(SelectionType::eNone);
aHighlightName.AppendElement();
aStyle.AppendElement(TextRangeStyle());
if (mIndex < mSelectionRanges.Length()) {
segmentEnd = mSelectionRanges[mIndex].mRange.start;
}
} else {
// Get the selection details for the next segment, and increment index.
for (const SelectionDetails* sdptr :
mSelectionRanges[mIndex].mSelectionRanges) {
aSelectionType.AppendElement(sdptr->mSelectionType);
aHighlightName.AppendElement(sdptr->mHighlightData.mHighlightName);
aStyle.AppendElement(sdptr->mTextRangeStyle);
}
segmentEnd = mSelectionRanges[mIndex].mRange.end;
++mIndex;
}
// Advance iterator to the end of the segment.
mIterator.SetOriginalOffset(int32_t(segmentEnd));
// Further advance if necessary to a cluster boundary.
while (mIterator.GetOriginalOffset() < int32_t(mOriginalRange.end) &&
!mIterator.IsOriginalCharSkipped() &&
!mTextRun->IsClusterStart(mIterator.GetSkippedOffset())) {
mIterator.AdvanceOriginal(1);
}
aRange->start = runOffset;
aRange->end = mIterator.GetSkippedOffset();
*aXOffset = mXOffset;
*aHyphenWidth = 0;
if (mIterator.GetOriginalOffset() == int32_t(mOriginalRange.end) &&
mProvider.GetFrame()->HasAnyStateBits(TEXT_HYPHEN_BREAK)) {
*aHyphenWidth = mProvider.GetHyphenWidth();
}
return true;
}
static void AddHyphenToMetrics(nsTextFrame* aTextFrame, bool aIsRightToLeft,
gfxTextRun::Metrics* aMetrics,
gfxFont::BoundingBoxType aBoundingBoxType,
DrawTarget* aDrawTarget) {
// Fix up metrics to include hyphen
RefPtr<gfxTextRun> hyphenTextRun = GetHyphenTextRun(aTextFrame, aDrawTarget);
if (!hyphenTextRun) {
return;
}
gfxTextRun::Metrics hyphenMetrics =
hyphenTextRun->MeasureText(aBoundingBoxType, aDrawTarget);
if (aTextFrame->GetWritingMode().IsLineInverted()) {
hyphenMetrics.mBoundingBox.y = -hyphenMetrics.mBoundingBox.YMost();
}
aMetrics->CombineWith(hyphenMetrics, aIsRightToLeft);
}
void nsTextFrame::PaintOneShadow(const PaintShadowParams& aParams,
const StyleSimpleShadow& aShadowDetails,
gfxRect& aBoundingBox, uint32_t aBlurFlags) {
AUTO_PROFILER_LABEL("nsTextFrame::PaintOneShadow", GRAPHICS);
nsPoint shadowOffset(aShadowDetails.horizontal.ToAppUnits(),
aShadowDetails.vertical.ToAppUnits());
nscoord blurRadius = std::max(aShadowDetails.blur.ToAppUnits(), 0);
nscolor shadowColor = aShadowDetails.color.CalcColor(aParams.foregroundColor);
if (auto* textDrawer = aParams.context->GetTextDrawer()) {
wr::Shadow wrShadow;
wrShadow.offset = {PresContext()->AppUnitsToFloatDevPixels(shadowOffset.x),
PresContext()->AppUnitsToFloatDevPixels(shadowOffset.y)};
wrShadow.blur_radius = PresContext()->AppUnitsToFloatDevPixels(blurRadius);
wrShadow.color = wr::ToColorF(ToDeviceColor(shadowColor));
bool inflate = true;
textDrawer->AppendShadow(wrShadow, inflate);
return;
}
// This rect is the box which is equivalent to where the shadow will be
// painted. The origin of aBoundingBox is the text baseline left, so we must
// translate it by that much in order to make the origin the top-left corner
// of the text bounding box. Note that aLeftSideOffset is line-left, so
// actually means top offset in vertical writing modes.
gfxRect shadowGfxRect;
WritingMode wm = GetWritingMode();
if (wm.IsVertical()) {
shadowGfxRect = aBoundingBox;
if (wm.IsVerticalRL()) {
// for vertical-RL, reverse direction of x-coords of bounding box
shadowGfxRect.x = -shadowGfxRect.XMost();
}
shadowGfxRect += gfxPoint(aParams.textBaselinePt.x,
aParams.framePt.y + aParams.leftSideOffset);
} else {
shadowGfxRect =
aBoundingBox + gfxPoint(aParams.framePt.x + aParams.leftSideOffset,
aParams.textBaselinePt.y);
}
Point shadowGfxOffset(shadowOffset.x, shadowOffset.y);
shadowGfxRect += gfxPoint(shadowGfxOffset.x, shadowOffset.y);
nsRect shadowRect(NSToCoordRound(shadowGfxRect.X()),
NSToCoordRound(shadowGfxRect.Y()),
NSToCoordRound(shadowGfxRect.Width()),
NSToCoordRound(shadowGfxRect.Height()));
nsContextBoxBlur contextBoxBlur;
const auto A2D = PresContext()->AppUnitsPerDevPixel();
gfxContext* shadowContext =
contextBoxBlur.Init(shadowRect, 0, blurRadius, A2D, aParams.context,
LayoutDevicePixel::ToAppUnits(aParams.dirtyRect, A2D),
nullptr, aBlurFlags);
if (!shadowContext) {
return;
}
aParams.context->Save();
aParams.context->SetColor(sRGBColor::FromABGR(shadowColor));
// Draw the text onto our alpha-only surface to capture the alpha values.
// Remember that the box blur context has a device offset on it, so we don't
// need to translate any coordinates to fit on the surface.
gfxFloat advanceWidth;
nsTextPaintStyle textPaintStyle(this);
DrawTextParams params(shadowContext, PresContext()->FontPaletteCache());
params.paintingShadows = true;
params.advanceWidth = &advanceWidth;
params.dirtyRect = aParams.dirtyRect;
params.framePt = aParams.framePt + shadowGfxOffset;
params.provider = aParams.provider;
params.textStyle = &textPaintStyle;
params.textColor =
aParams.context == shadowContext ? shadowColor : NS_RGB(0, 0, 0);
params.callbacks = aParams.callbacks;
params.clipEdges = aParams.clipEdges;
params.drawSoftHyphen = HasAnyStateBits(TEXT_HYPHEN_BREAK);
// Multi-color shadow is not allowed, so we use the same color as the text
// color.
params.decorationOverrideColor = &params.textColor;
params.fontPalette = StyleFont()->GetFontPaletteAtom();
DrawText(aParams.range, aParams.textBaselinePt + shadowGfxOffset, params);
contextBoxBlur.DoPaint();
aParams.context->Restore();
}
/* static */
SelectionTypeMask nsTextFrame::CreateSelectionRangeList(
const SelectionDetails* aDetails, SelectionType aSelectionType,
const PaintTextSelectionParams& aParams,
nsTArray<SelectionRange>& aSelectionRanges, bool* aAnyBackgrounds) {
SelectionTypeMask allTypes = 0;
bool anyBackgrounds = false;
uint32_t priorityOfInsertionOrder = 0;
for (const SelectionDetails* sd = aDetails; sd; sd = sd->mNext.get()) {
MOZ_ASSERT(sd->mStart >= 0 && sd->mEnd >= 0); // XXX make unsigned?
uint32_t start = std::max(aParams.contentRange.start, uint32_t(sd->mStart));
uint32_t end = std::min(aParams.contentRange.end, uint32_t(sd->mEnd));
if (start < end) {
// The PaintTextWithSelectionColors caller passes SelectionType::eNone,
// so we collect all selections that set colors, and prioritize them
// according to selection type (lower types take precedence).
if (aSelectionType == SelectionType::eNone) {
allTypes |= ToSelectionTypeMask(sd->mSelectionType);
// Ignore selections that don't set colors.
nscolor foreground(0), background(0);
if (GetSelectionTextColors(sd->mSelectionType,
sd->mHighlightData.mHighlightName,
*aParams.textPaintStyle, sd->mTextRangeStyle,
&foreground, &background)) {
if (NS_GET_A(background) > 0) {
anyBackgrounds = true;
}
aSelectionRanges.AppendElement(
SelectionRange{sd, {start, end}, priorityOfInsertionOrder++});
}
} else if (sd->mSelectionType == aSelectionType) {
// The PaintSelectionTextDecorations caller passes a specific type,
// so we include only ranges of that type, and keep them in order
// so that later ones take precedence over earlier.
aSelectionRanges.AppendElement(
SelectionRange{sd, {start, end}, priorityOfInsertionOrder++});
}
}
}
if (aAnyBackgrounds) {
*aAnyBackgrounds = anyBackgrounds;
}
return allTypes;
}
/* static */
void nsTextFrame::CombineSelectionRanges(
const nsTArray<SelectionRange>& aSelectionRanges,
nsTArray<PriorityOrderedSelectionsForRange>& aCombinedSelectionRanges) {
struct SelectionRangeEndCmp {
bool Equals(const SelectionRange* a, const SelectionRange* b) const {
return a->mRange.end == b->mRange.end;
}
bool LessThan(const SelectionRange* a, const SelectionRange* b) const {
return a->mRange.end < b->mRange.end;
}
};
struct SelectionRangePriorityCmp {
bool Equals(const SelectionRange* a, const SelectionRange* b) const {
const SelectionDetails* aDetails = a->mDetails;
const SelectionDetails* bDetails = b->mDetails;
if (aDetails->mSelectionType != bDetails->mSelectionType) {
return false;
}
if (aDetails->mSelectionType != SelectionType::eHighlight) {
return a->mPriority == b->mPriority;
}
if (aDetails->mHighlightData.mHighlight->Priority() !=
bDetails->mHighlightData.mHighlight->Priority()) {
return false;
}
return a->mPriority == b->mPriority;
}
bool LessThan(const SelectionRange* a, const SelectionRange* b) const {
if (a->mDetails->mSelectionType != b->mDetails->mSelectionType) {
// Even though this looks counter-intuitive,
// this is intended, as values in `SelectionType` are inverted:
// a lower value indicates a higher priority.
return a->mDetails->mSelectionType > b->mDetails->mSelectionType;
}
if (a->mDetails->mSelectionType != SelectionType::eHighlight) {
// for non-highlights, the selection which was added later
// has a higher priority.
return a->mPriority < b->mPriority;
}
if (a->mDetails->mHighlightData.mHighlight->Priority() !=
b->mDetails->mHighlightData.mHighlight->Priority()) {
// For highlights, first compare the priorities set by the user.
return a->mDetails->mHighlightData.mHighlight->Priority() <
b->mDetails->mHighlightData.mHighlight->Priority();
}
// only if the user priorities are equal, let the highlight that was added
// later take precedence.
return a->mPriority < b->mPriority;
}
};
uint32_t currentOffset = 0;
AutoTArray<const SelectionRange*, 1> activeSelectionsForCurrentSegment;
size_t rangeIndex = 0;
// Divide the given selection ranges into segments which share the same
// set of selections.
// The following algorithm iterates `aSelectionRanges`, assuming
// that its elements are sorted by their start offset.
// Each time a new selection starts, it is pushed into an array of
// "currently present" selections, sorted by their *end* offset.
// For each iteration the next segment end offset is determined,
// which is either the start offset of the next selection or
// the next end offset of all "currently present" selections
// (which is always the first element of the array because of its order).
// Then, a `CombinedSelectionRange` can be constructed, which describes
// the text segment until its end offset (as determined above), and contains
// all elements of the "currently present" selection list, now sorted
// by their priority.
// If a range ends at the given offset, it is removed from the array.
while (rangeIndex < aSelectionRanges.Length() ||
!activeSelectionsForCurrentSegment.IsEmpty()) {
uint32_t currentSegmentEndOffset =
activeSelectionsForCurrentSegment.IsEmpty()
? -1
: activeSelectionsForCurrentSegment[0]->mRange.end;
uint32_t nextRangeStartOffset =
rangeIndex < aSelectionRanges.Length()
? aSelectionRanges[rangeIndex].mRange.start
: -1;
uint32_t nextOffset =
std::min(currentSegmentEndOffset, nextRangeStartOffset);
if (!activeSelectionsForCurrentSegment.IsEmpty() &&
currentOffset != nextOffset) {
auto activeSelectionRangesSortedByPriority =
activeSelectionsForCurrentSegment.Clone();
activeSelectionRangesSortedByPriority.Sort(SelectionRangePriorityCmp());
AutoTArray<const SelectionDetails*, 1> selectionDetails;
selectionDetails.SetCapacity(
activeSelectionRangesSortedByPriority.Length());
// ensure that overlapping highlights which have the same name
// are only added once. If added each time, they would be painted
// several times (see wpt
// /css/css-highlight-api/painting/custom-highlight-painting-003.html)
// Comparing the highlight name with the previous one is
// sufficient here because selections are already sorted
// in a way that ensures that highlights of the same name are
// grouped together.
nsAtom* currentHighlightName = nullptr;
for (const auto* selectionRange : activeSelectionRangesSortedByPriority) {
if (selectionRange->mDetails->mSelectionType ==
SelectionType::eHighlight) {
if (selectionRange->mDetails->mHighlightData.mHighlightName ==
currentHighlightName) {
continue;
}
currentHighlightName =
selectionRange->mDetails->mHighlightData.mHighlightName;
}
selectionDetails.AppendElement(selectionRange->mDetails);
}
aCombinedSelectionRanges.AppendElement(PriorityOrderedSelectionsForRange{
std::move(selectionDetails), {currentOffset, nextOffset}});
}
currentOffset = nextOffset;
if (nextRangeStartOffset < currentSegmentEndOffset) {
activeSelectionsForCurrentSegment.InsertElementSorted(
&aSelectionRanges[rangeIndex++], SelectionRangeEndCmp());
} else {
activeSelectionsForCurrentSegment.RemoveElementAt(0);
}
}
}
SelectionTypeMask nsTextFrame::ResolveSelections(
const PaintTextSelectionParams& aParams, const SelectionDetails* aDetails,
nsTArray<PriorityOrderedSelectionsForRange>& aResult,
SelectionType aSelectionType, bool* aAnyBackgrounds) const {
AutoTArray<SelectionRange, 4> selectionRanges;
SelectionTypeMask allTypes = CreateSelectionRangeList(
aDetails, aSelectionType, aParams, selectionRanges, aAnyBackgrounds);
if (selectionRanges.IsEmpty()) {
return allTypes;
}
struct SelectionRangeStartCmp {
bool Equals(const SelectionRange& a, const SelectionRange& b) const {
return a.mRange.start == b.mRange.start;
}
bool LessThan(const SelectionRange& a, const SelectionRange& b) const {
return a.mRange.start < b.mRange.start;
}
};
selectionRanges.Sort(SelectionRangeStartCmp());
CombineSelectionRanges(selectionRanges, aResult);
return allTypes;
}
// Paints selection backgrounds and text in the correct colors. Also computes
// aAllSelectionTypeMask, the union of all selection types that are applying to
// this text.
bool nsTextFrame::PaintTextWithSelectionColors(
const PaintTextSelectionParams& aParams,
const UniquePtr<SelectionDetails>& aDetails,
SelectionTypeMask* aAllSelectionTypeMask, const ClipEdges& aClipEdges) {
bool anyBackgrounds = false;
AutoTArray<PriorityOrderedSelectionsForRange, 8> selectionRanges;
*aAllSelectionTypeMask =
ResolveSelections(aParams, aDetails.get(), selectionRanges,
SelectionType::eNone, &anyBackgrounds);
bool vertical = mTextRun->IsVertical();
const gfxFloat startIOffset =
vertical ? aParams.textBaselinePt.y - aParams.framePt.y
: aParams.textBaselinePt.x - aParams.framePt.x;
gfxFloat iOffset, hyphenWidth;
Range range; // in transformed string
const gfxTextRun::Range& contentRange = aParams.contentRange;
auto* textDrawer = aParams.context->GetTextDrawer();
if (anyBackgrounds && !aParams.IsGenerateTextMask()) {
int32_t appUnitsPerDevPixel =
aParams.textPaintStyle->PresContext()->AppUnitsPerDevPixel();
SelectionRangeIterator iterator(selectionRanges, contentRange,
*aParams.provider, mTextRun, startIOffset);
AutoTArray<SelectionType, 1> selectionTypes;
AutoTArray<RefPtr<nsAtom>, 1> highlightNames;
AutoTArray<TextRangeStyle, 1> rangeStyles;
while (iterator.GetNextSegment(&iOffset, &range, &hyphenWidth,
selectionTypes, highlightNames,
rangeStyles)) {
nscolor foreground(0), background(0);
gfxFloat advance =
hyphenWidth + mTextRun->GetAdvanceWidth(range, aParams.provider);
nsRect bgRect;
gfxFloat offs = iOffset - (mTextRun->IsInlineReversed() ? advance : 0);
if (vertical) {
bgRect = nsRect(nscoord(aParams.framePt.x),
nscoord(aParams.framePt.y + offs), GetSize().width,
nscoord(advance));
} else {
bgRect = nsRect(nscoord(aParams.framePt.x + offs),
nscoord(aParams.framePt.y), nscoord(advance),
GetSize().height);
}
LayoutDeviceRect selectionRect =
LayoutDeviceRect::FromAppUnits(bgRect, appUnitsPerDevPixel);
// The elements in `selectionTypes` are ordered ascending by their
// priority. To account for non-opaque overlapping selections, all
// selection backgrounds are painted.
for (size_t index = 0; index < selectionTypes.Length(); ++index) {
GetSelectionTextColors(selectionTypes[index], highlightNames[index],
*aParams.textPaintStyle, rangeStyles[index],
&foreground, &background);
// Draw background color
if (NS_GET_A(background) > 0) {
if (textDrawer) {
textDrawer->AppendSelectionRect(selectionRect,
ToDeviceColor(background));
} else {
PaintSelectionBackground(*aParams.context->GetDrawTarget(),
background, aParams.dirtyRect,
selectionRect, aParams.callbacks);
}
}
}
iterator.UpdateWithAdvance(advance);
}
}
gfxFloat advance;
DrawTextParams params(aParams.context, PresContext()->FontPaletteCache());
params.dirtyRect = aParams.dirtyRect;
params.framePt = aParams.framePt;
params.provider = aParams.provider;
params.textStyle = aParams.textPaintStyle;
params.clipEdges = &aClipEdges;
params.advanceWidth = &advance;
params.callbacks = aParams.callbacks;
params.glyphRange = aParams.glyphRange;
params.fontPalette = StyleFont()->GetFontPaletteAtom();
params.hasTextShadow = !StyleText()->mTextShadow.IsEmpty();
PaintShadowParams shadowParams(aParams);
shadowParams.provider = aParams.provider;
shadowParams.callbacks = aParams.callbacks;
shadowParams.clipEdges = &aClipEdges;
// Draw text
const nsStyleText* textStyle = StyleText();
SelectionRangeIterator iterator(selectionRanges, contentRange,
*aParams.provider, mTextRun, startIOffset);
AutoTArray<SelectionType, 1> selectionTypes;
AutoTArray<RefPtr<nsAtom>, 1> highlightNames;
AutoTArray<TextRangeStyle, 1> rangeStyles;
while (iterator.GetNextSegment(&iOffset, &range, &hyphenWidth, selectionTypes,
highlightNames, rangeStyles)) {
nscolor foreground(0), background(0);
if (aParams.IsGenerateTextMask()) {
foreground = NS_RGBA(0, 0, 0, 255);
} else {
nscolor tmpForeground(0);
bool colorHasBeenSet = false;
for (size_t index = 0; index < selectionTypes.Length(); ++index) {
if (selectionTypes[index] == SelectionType::eHighlight) {
if (aParams.textPaintStyle->GetCustomHighlightTextColor(
highlightNames[index], &tmpForeground)) {
foreground = tmpForeground;
colorHasBeenSet = true;
}
} else {
GetSelectionTextColors(selectionTypes[index], highlightNames[index],
*aParams.textPaintStyle, rangeStyles[index],
&foreground, &background);
colorHasBeenSet = true;
}
}
if (!colorHasBeenSet) {
foreground = tmpForeground;
}