Source code

Revision control

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/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/StaticPrefs_svg.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 "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 "nsFrame.h"
#include "nsIMathMLFrame.h"
#include "nsPlaceholderFrame.h"
#include "nsTextFrameUtils.h"
#include "nsTextRunTransformations.h"
#include "MathMLTextRunFactory.h"
#include "nsUnicodeProperties.h"
#include "nsStyleUtil.h"
#include "nsRubyFrame.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/WordBreaker.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 "GeckoProfiler.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());
return aFrame->GetContent()->HasFlag(NS_MAYBE_MASKED);
}
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)
/**
* 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;
};
/**
* This helper object computes colors used for painting, and also IME
* underline information. The data is computed lazily and cached as necessary.
* These live for just the duration of one paint operation.
*/
class nsTextPaintStyle {
public:
explicit nsTextPaintStyle(nsTextFrame* aFrame);
void SetResolveColors(bool aResolveColors) {
mResolveColors = aResolveColors;
}
nscolor GetTextColor();
// SVG text has its own painting process, so we should never get its stroke
// property from here.
nscolor GetWebkitTextStrokeColor() {
if (nsSVGUtils::IsInSVGTextSubtree(mFrame)) {
return 0;
}
return mFrame->StyleText()->mWebkitTextStrokeColor.CalcColor(mFrame);
}
float GetWebkitTextStrokeWidth() {
if (nsSVGUtils::IsInSVGTextSubtree(mFrame)) {
return 0.0f;
}
nscoord coord = mFrame->StyleText()->mWebkitTextStrokeWidth;
return mFrame->PresContext()->AppUnitsToFloatDevPixels(coord);
}
/**
* Compute the colors for normally-selected text. Returns false if
* the normal selection is not being displayed.
*/
bool GetSelectionColors(nscolor* aForeColor, nscolor* aBackColor);
void GetHighlightColors(nscolor* aForeColor, nscolor* aBackColor);
void GetURLSecondaryColor(nscolor* aForeColor);
void GetIMESelectionColors(int32_t aIndex, nscolor* aForeColor,
nscolor* aBackColor);
// if this returns false, we don't need to draw underline.
bool GetSelectionUnderlineForPaint(int32_t aIndex, nscolor* aLineColor,
float* aRelativeSize, uint8_t* aStyle);
// if this returns false, we don't need to draw underline.
static bool GetSelectionUnderline(nsPresContext* aPresContext, int32_t aIndex,
nscolor* aLineColor, float* aRelativeSize,
uint8_t* aStyle);
// if this returns false, no text-shadow was specified for the selection
// and the *aShadow parameter was not modified.
bool GetSelectionShadow(Span<const StyleSimpleShadow>* aShadows);
nsPresContext* PresContext() const { return mPresContext; }
enum {
eIndexRawInput = 0,
eIndexSelRawText,
eIndexConvText,
eIndexSelConvText,
eIndexSpellChecker
};
static int32_t GetUnderlineStyleIndexForSelectionType(
SelectionType aSelectionType) {
switch (aSelectionType) {
case SelectionType::eIMERawClause:
return eIndexRawInput;
case SelectionType::eIMESelectedRawClause:
return eIndexSelRawText;
case SelectionType::eIMEConvertedClause:
return eIndexConvText;
case SelectionType::eIMESelectedClause:
return eIndexSelConvText;
case SelectionType::eSpellCheck:
return eIndexSpellChecker;
default:
NS_WARNING("non-IME selection type");
return eIndexRawInput;
}
}
nscolor GetSystemFieldForegroundColor();
nscolor GetSystemFieldBackgroundColor();
protected:
nsTextFrame* mFrame;
nsPresContext* mPresContext;
bool mInitCommonColors;
bool mInitSelectionColorsAndShadow;
bool mResolveColors;
// Selection data
nscolor mSelectionTextColor;
nscolor mSelectionBGColor;
RefPtr<ComputedStyle> mSelectionPseudoStyle;
// Common data
int32_t mSufficientContrast;
nscolor mFrameBackgroundColor;
nscolor mSystemFieldForegroundColor;
nscolor mSystemFieldBackgroundColor;
// selection colors and underline info, the colors are resolved colors if
// mResolveColors is true (which is the default), i.e., the foreground color
// and background color are swapped if it's needed. And also line color will
// be resolved from them.
struct nsSelectionStyle {
bool mInit;
nscolor mTextColor;
nscolor mBGColor;
nscolor mUnderlineColor;
uint8_t mUnderlineStyle;
float mUnderlineRelativeSize;
};
nsSelectionStyle mSelectionStyle[5];
// Color initializations
void InitCommonColors();
bool InitSelectionColorsAndShadow();
nsSelectionStyle* GetSelectionStyle(int32_t aIndex);
void InitSelectionStyle(int32_t aIndex);
// Ensures sufficient contrast between the frame background color and the
// selection background color, and swaps the selection text and background
// colors accordingly.
// Only used on platforms where mSelectionTextColor != NS_DONT_CHANGE_COLOR
bool EnsureSufficientContrast(nscolor* aForeColor, nscolor* aBackColor);
nscolor GetResolvedForeColor(nscolor aColor, nscolor aDefaultForeColor,
nscolor aBackColor);
};
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 (nsSVGUtils::IsInSVGTextSubtree(f) &&
f->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) {
auto svgTextFrame = static_cast<SVGTextFrame*>(
nsLayoutUtils::GetClosestFrameOfType(f, LayoutFrameType::SVGText));
svgTextFrame->ScheduleReflowSVGNonDisplayText(IntrinsicDirty::Resize);
} 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::Resize, 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();
return next ? next->GetContentOffset() : TextFragment()->GetLength();
}
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 (!(mState & NS_FRAME_IS_BIDI)) {
return mContent->TextLength() - mContentOffset;
}
FlowLengthProperty* flowLength =
mContent->HasFlag(NS_HAS_FLOWLENGTH_PROPERTY)
? static_cast<FlowLengthProperty*>(
mContent->GetProperty(nsGkAtoms::flowlength))
: nullptr;
/**
* 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;
}
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;
}
}
// 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 == ' ')
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) {
NS_ASSERTION(aPos < aFrag->GetLength(), "No text for IsSpace!");
switch (aFrag->CharAt(aPos)) {
case ' ':
return !aStyleText->WhiteSpaceIsSignificant() &&
!IsSpaceCombiningSequenceTail(aFrag, aPos + 1);
case '\n':
return !aStyleText->NewlineIsSignificantStyle() &&
aStyleText->mWhiteSpace != mozilla::StyleWhiteSpace::PreSpace;
case '\t':
case '\r':
case '\f':
return !aStyleText->WhiteSpaceIsSignificant();
default:
return false;
}
}
static bool IsSelectionSpace(const nsTextFragment* aFrag, uint32_t aPos) {
NS_ASSERTION(aPos < aFrag->GetLength(), "No text for IsSpace!");
char16_t ch = aFrag->CharAt(aPos);
if (ch == ' ' || ch == CH_NBSP)
return !IsSpaceCombiningSequenceTail(aFrag, aPos + 1);
return ch == '\t' || ch == '\n' || ch == '\f' || 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);
for (auto font : fontsWithAnimatedGlyphs) {
observers->AppendElement(new 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)
: mDrawTarget(aDrawTarget),
mLineContainer(aLineContainer),
mCommonAncestorWithLastFrame(nullptr),
mMissingFonts(aPresContext->MissingFontRecorder()),
mBidiEnabled(aPresContext->BidiEnabled()),
mStartOfLine(true),
mSkipIncompleteTextRuns(false),
mCanStopOnThisLine(false),
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() {
return mEndFrame ? mEndFrame->GetContentOffset()
: mStartFrame->TextFragment()->GetLength();
}
};
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) {
MOZ_ASSERT(
!(mTextRun->GetFlags2() & nsTextFrameUtils::Flags::UnusedFlags),
"Flag set that should never be set! (memory safety error?)");
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;
nsTextFrame::TextRunType mWhichTextRun;
uint8_t mNextRunContextInfo;
uint8_t mCurrentRunContextInfo;
};
static nsIFrame* FindLineContainer(nsIFrame* aFrame) {
while (aFrame && (aFrame->IsFrameOfType(nsIFrame::eLineParticipant) ||
aFrame->CanContinueTextRun())) {
aFrame = aFrame->GetParent();
}
return 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->mWhiteSpace) {
case StyleWhiteSpace::Normal:
case StyleWhiteSpace::Nowrap:
return nsTextFrameUtils::COMPRESS_WHITESPACE_NEWLINE;
case StyleWhiteSpace::Pre:
case StyleWhiteSpace::PreWrap:
case StyleWhiteSpace::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;
case StyleWhiteSpace::PreSpace:
return nsTextFrameUtils::COMPRESS_NONE_TRANSFORM_TO_SPACE;
case StyleWhiteSpace::PreLine:
return nsTextFrameUtils::COMPRESS_WHITESPACE;
default:
MOZ_ASSERT_UNREACHABLE("Unknown white-space 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(nsIFrame::kOverflowList).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;
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);
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();
BuildTextRunsScanner scanner(presContext, aDrawTarget, aLineContainer,
aWhichTextRun);
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;
// 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());
}
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;
}
}
static bool HasTerminalNewline(const nsTextFrame* aFrame) {
if (aFrame->GetContentLength() == 0) return false;
const nsTextFragment* frag = aFrame->TextFragment();
return frag->CharAt(aFrame->GetContentEnd() - 1) == '\n';
}
static gfxFont::Metrics GetFirstFontMetrics(gfxFontGroup* aFontGroup,
bool aVerticalMetrics) {
if (!aFontGroup) return gfxFont::Metrics();
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 (!nsSVGUtils::IsInSVGTextSubtree(aFrame)) {
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 = nullptr) {
if (!aStyleText) {
aStyleText = aFrame->StyleText();
}
if (nsSVGUtils::IsInSVGTextSubtree(aFrame)) {
if (!StaticPrefs::svg_text_spacing_enabled()) {
return 0;
}
// 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.
Length spacing = aStyleText->mLetterSpacing;
spacing.ScaleBy(GetSVGFontSizeScaleFactor(aFrame));
return spacing.ToAppUnits();
}
return aStyleText->mLetterSpacing.ToAppUnits();
}
// This function converts non-coord values (e.g. percentages) to nscoord.
static nscoord WordSpacing(nsIFrame* aFrame, const gfxTextRun* aTextRun,
const nsStyleText* aStyleText = nullptr) {
if (!aStyleText) {
aStyleText = aFrame->StyleText();
}
if (nsSVGUtils::IsInSVGTextSubtree(aFrame)) {
// 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.
if (!StaticPrefs::svg_text_spacing_enabled()) {
return 0;
}
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) {
if (nsSVGUtils::IsInSVGTextSubtree(aFrame) &&
!StaticPrefs::svg_text_spacing_enabled()) {
return gfx::ShapedTextFlags();
}
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.IsZero() || !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;
}