diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /layout/generic/nsTextFrame.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'layout/generic/nsTextFrame.cpp')
-rw-r--r-- | layout/generic/nsTextFrame.cpp | 10215 |
1 files changed, 10215 insertions, 0 deletions
diff --git a/layout/generic/nsTextFrame.cpp b/layout/generic/nsTextFrame.cpp new file mode 100644 index 0000000000..14b2198f49 --- /dev/null +++ b/layout/generic/nsTextFrame.cpp @@ -0,0 +1,10215 @@ +/* -*- 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/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 "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 "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; + 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; + gfx::FontPaletteValueSet* paletteValueSet = nullptr; + float textStrokeWidth = 0.0f; + bool drawSoftHyphen = false; + explicit DrawTextRunParams(gfxContext* aContext) : context(aContext) {} +}; + +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; + explicit DrawTextParams(gfxContext* aContext) : DrawTextRunParams(aContext) {} +}; + +struct nsTextFrame::PaintShadowParams { + gfxTextRun::Range range; + LayoutDeviceRect dirtyRect; + Point framePt; + Point textBaselinePt; + gfxContext* context; + 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­</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; + + /** + * 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; + } +} + +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->mWhiteSpace != mozilla::StyleWhiteSpace::PreSpace; + 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); + + 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) { + 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; + bool mDoLineBreaking; + 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(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; + 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(); + 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; + } +} + +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. + 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) { + 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.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; + } + + 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(); + // According to https://drafts.csswg.org/css-text/#boundary-shaping: + // + // 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()->mMargin.Get(aSide); + 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(eLogicalSideIEnd); + Side side2 = wm.PhysicalSide(eLogicalSideIStart); + // 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; + + 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); + 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); + 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); + 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); + } + + if (fontGroup) { + // Refresh fontgroup if necessary, before trying to build textruns. + fontGroup->CheckForUpdatedPlatformList(); + } else { + DestroyUserData(userDataToDestroy); + return nullptr; + } + + 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; + TextEditor* passwordEditor = + nsContentUtils::GetTextEditorFromAnonymousNodeWithoutCreation( + 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, ¶ms, fontGroup, flags, flags2, + std::move(styles), true); + } else { + textRun = fontGroup->MakeTextRun(text, transformedLength, ¶ms, 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, ¶ms, fontGroup, flags, flags2, + std::move(styles), true); + } else { + textRun = fontGroup->MakeTextRun(text, transformedLength, ¶ms, 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; + + 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); + 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); + 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); + 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.case_ == StyleTextTransformCase::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) { + auto category = unicode::GetGeneralCategory(aCh); + // Comparing an unsigned variable against zero is a compile error, + // so we use static assert here to ensure we really don't need to + // compare it with the given constant. + static_assert(std::is_unsigned_v<decltype(category)> && + HB_UNICODE_GENERAL_CATEGORY_CONTROL == 0, + "if this constant is not zero, or category is signed, " + "we need to explicitly do the comparison below"); + return !(category <= HB_UNICODE_GENERAL_CATEGORY_UNASSIGNED || + (category >= HB_UNICODE_GENERAL_CATEGORY_LINE_SEPARATOR && + category <= HB_UNICODE_GENERAL_CATEGORY_SPACE_SEPARATOR)); +} + +static bool MayCharacterHaveEmphasisMark(uint8_t aCh) { + // 0x00~0x1f and 0x7f~0x9f are in category Cc + // 0x20 and 0xa0 are in category Zs + bool result = !(aCh <= 0x20 || (aCh >= 0x7f && aCh <= 0xa0)); + MOZ_ASSERT(result == MayCharacterHaveEmphasisMark(uint32_t(aCh)), + "result for uint8_t should match result for uint32_t"); + return result; +} + +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 true; + } + 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) + : 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?"); +} + +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); +} + +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; + } + + 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)) { + 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 CanAddSpacingAfter(const gfxTextRun* aTextRun, uint32_t aOffset, + bool aNewlineIsSignificant) { + if (aOffset + 1 >= aTextRun->GetLength()) { + return true; + } + const auto* g = aTextRun->GetCharacterGlyphs(); + return g[aOffset + 1].IsClusterStart() && + g[aOffset + 1].IsLigatureGroupStart() && + !g[aOffset].CharIsFormattingControl() && !g[aOffset].CharIsTab() && + !(aNewlineIsSignificant && g[aOffset].CharIsNewline()); +} + +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. + RefPtr<nsFontMetrics> fm = nsLayoutUtils::GetFontMetricsForFrame(cb, 1.0f); + bool vertical = cb->GetWritingMode().IsCentralBaseline(); + nscoord spaceWidth = nscoord(NS_round( + GetFirstFontMetrics(fm->GetThebesFontGroup(), vertical).spaceWidth * + cb->PresContext()->AppUnitsPerDevPixel())); + return spaces * (spaceWidth + styleText->mLetterSpacing.ToAppUnits() + + 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); + while (run.NextRun()) { + uint32_t runOffsetInSubstring = run.GetSkippedOffset() - aRange.start; + gfxSkipCharsIterator iter = run.GetPos(); + for (int32_t i = 0; i < run.GetRunLength(); ++i) { + if (CanAddSpacingAfter(mTextRun, run.GetSkippedOffset() + i, + newlineIsSignificant)) { + // End of a cluster, not in a ligature: put letter-spacing after it + aSpacing[runOffsetInSubstring + i].mAfter += mLetterSpacing; + } + 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; + } + } + } + } + + // 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; + SetupJustificationSpacing(true); +} + +void nsTextFrame::PropertyProvider::InitializeForMeasure() { + nsTextFrame::TrimmedOffsets trimmed = mFrame->GetTrimmedOffsets( + mFrag, nsTextFrame::TrimmedOffsetFlags::NotPostReflow); + mStart.SetOriginalOffset(trimmed.mStart); + mLength = trimmed.mLength; + 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); + 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); + } + } + } + + 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); + 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(gfxContext* aRenderingContext, + InlineMinISizeData* aData) final; + void AddInlinePrefISize(gfxContext* aRenderingContext, + InlinePrefISizeData* aData) final; + + protected: + explicit nsContinuingTextFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsTextFrame(aStyle, aPresContext, kClassID) {} + + nsTextFrame* mPrevContinuation; + 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. + +// Needed for text frames in XUL. +/* virtual */ +nscoord nsTextFrame::GetMinISize(gfxContext* aRenderingContext) { + return nsLayoutUtils::MinISizeFromInline(this, aRenderingContext); +} + +// Needed for text frames in XUL. +/* virtual */ +nscoord nsTextFrame::GetPrefISize(gfxContext* aRenderingContext) { + return nsLayoutUtils::PrefISizeFromInline(this, aRenderingContext); +} + +/* virtual */ +void nsContinuingTextFrame::AddInlineMinISize(gfxContext* aRenderingContext, + InlineMinISizeData* aData) { + // Do nothing, since the first-in-flow accounts for everything. +} + +/* virtual */ +void nsContinuingTextFrame::AddInlinePrefISize(gfxContext* aRenderingContext, + InlinePrefISizeData* aData) { + // Do nothing, since the first-in-flow accounts for everything. +} + +//---------------------------------------------------------------------- + +#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; + +Maybe<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 Some(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 = PresContext()->GetPresShell(); + 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; + } + nsDependentAtomString langStr(langAtom); + return (StringBeginsWith(langStr, u"ja"_ns) || + StringBeginsWith(langStr, u"ko"_ns)) && + (langStr.Length() == 2 || langStr[2] == '-'); +} + +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(); + const StyleTextDecorationLine textDecorations = + styleTextReset->mTextDecorationLine; + + 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::GetColor(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; + + const auto style = styleTextReset->mTextDecorationStyle; + 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::GetColor(f, &nsStyleSVG::mFill) + : NS_SAME_AS_FOREGROUND_COLOR; + } else { + color = + nsLayoutUtils::GetColor(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(); + if (textDecorations & kUnderline) { + aDecorations.mUnderlines.AppendElement(nsTextFrame::LineDecoration( + f, baselineOffset, styleText->mTextUnderlinePosition, + styleText->mTextUnderlineOffset, + styleTextReset->mTextDecorationThickness, color, style)); + } + if (textDecorations & kOverline) { + aDecorations.mOverlines.AppendElement(nsTextFrame::LineDecoration( + f, baselineOffset, styleText->mTextUnderlinePosition, + styleText->mTextUnderlineOffset, + styleTextReset->mTextDecorationThickness, color, style)); + } + if (textDecorations & StyleTextDecorationLine::LINE_THROUGH) { + aDecorations.mStrikes.AppendElement(nsTextFrame::LineDecoration( + f, baselineOffset, styleText->mTextUnderlinePosition, + styleText->mTextUnderlineOffset, + styleTextReset->mTextDecorationThickness, color, style)); + } + } + + // 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 (!nsStyleDisplay::IsInlineFlow(display) && + (!nsStyleDisplay::IsRubyDisplayType(display) || + display == mozilla::StyleDisplay::RubyTextContainer) && + nsStyleDisplay::IsDisplayTypeInlineOutside(display)) { + 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->IsFrameOfType(nsIFrame::eLineParticipant); + 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); + 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 == eLogicalSideBStart) != aWM.IsLineInverted() + ? baseFontMetrics->MaxAscent() + fm->MaxDescent() + : baseFontMetrics->MaxDescent() + fm->MaxAscent(); + RubyBlockLeadings leadings; + if (nsRubyFrame* ruby = FindFurthestInlineRubyAncestor(this)) { + leadings = ruby->GetBlockLeadings(); + } + if (side == eLogicalSideBStart) { + info->baselineOffset = -absOffset - leadings.mStart; + overflowRect.BStart(aWM) = -overflowRect.BSize(aWM) - leadings.mStart; + } else { + MOZ_ASSERT(side == eLogicalSideBEnd); + 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 == eLogicalSideBStart ? -1 : 1); + } + + SetProperty(EmphasisMarkProperty(), info); + return overflowRect.GetPhysicalRect(aWM, frameSize.GetPhysicalSize(aWM)); +} + +// helper function for implementing text-decoration-thickness +// https://drafts.csswg.org/css-text-decor-4/#text-decoration-width-property +// 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 +// https://drafts.csswg.org/css-text-decor-4/#underline-offset +// 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) { + nsRect shadowRect = + nsLayoutUtils::GetTextShadowRectsUnion(*aInkOverflowRect, this); + aInkOverflowRect->UnionRect(*aInkOverflowRect, shadowRect); + } + + // 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 const 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; +}; + +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); + if (aParams.callbacks) { + Rect path = nsCSSRendering::DecorationLineToPath(params); + if (aParams.decorationType == DecorationType::Normal) { + aParams.callbacks->PaintDecorationLine(path, params.color); + } else { + aParams.callbacks->PaintSelectionDecorationLine(path, 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: { + int32_t index = nsTextPaintStyle::GetUnderlineStyleIndexForSelectionType( + aSelectionType); + bool weDefineSelectionUnderline = + aTextPaintStyle.GetSelectionUnderlineForPaint( + index, ¶ms.color, &relativeSize, ¶ms.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, ¶ms.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(¶ms.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, + const 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: + return aTextPaintStyle.GetCustomHighlightColors(aHighlightName, + aForeground, aBackground); + 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 SelectionRange = nsTextFrame::SelectionRange; + + public: + // aSelectionRanges and aRange are according to the original string. + SelectionRangeIterator(const nsTArray<SelectionRange>& aSelectionRanges, + gfxTextRun::Range aRange, PropertyProvider& aProvider, + gfxTextRun* aTextRun, gfxFloat aXOffset); + + bool GetNextSegment(gfxFloat* aXOffset, gfxTextRun::Range* aRange, + gfxFloat* aHyphenWidth, SelectionType* aSelectionType, + RefPtr<const nsAtom>* aHighlightName, + TextRangeStyle* aStyle); + + void UpdateWithAdvance(gfxFloat aAdvance) { + mXOffset += aAdvance * mTextRun->GetDirection(); + } + + private: + const nsTArray<SelectionRange>& mSelectionRanges; + PropertyProvider& mProvider; + gfxTextRun* mTextRun; + gfxSkipCharsIterator mIterator; + gfxTextRun::Range mOriginalRange; + gfxFloat mXOffset; + uint32_t mIndex; +}; + +SelectionRangeIterator::SelectionRangeIterator( + const nsTArray<nsTextFrame::SelectionRange>& 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, + SelectionType* aSelectionType, RefPtr<const nsAtom>* aHighlightName, + TextRangeStyle* aStyle) { + if (mIterator.GetOriginalOffset() >= int32_t(mOriginalRange.end)) { + return false; + } + + uint32_t runOffset = mIterator.GetSkippedOffset(); + uint32_t segmentEnd = mOriginalRange.end; + + 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 = SelectionType::eNone; + *aHighlightName = nullptr; + *aStyle = TextRangeStyle(); + if (mIndex < mSelectionRanges.Length()) { + segmentEnd = mSelectionRanges[mIndex].mRange.start; + } + } else { + // Get the selection details for the next segment, and increment index. + const SelectionDetails* sdptr = mSelectionRanges[mIndex].mDetails; + *aSelectionType = sdptr->mSelectionType; + *aHighlightName = sdptr->mHighlightName; + *aStyle = 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); + 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.clipEdges = aParams.clipEdges; + params.drawSoftHyphen = HasAnyStateBits(TEXT_HYPHEN_BREAK); + // Multi-color shadow is not allowed, so we use the same color of the text + // color. + params.decorationOverrideColor = ¶ms.textColor; + params.fontPalette = StyleFont()->GetFontPaletteAtom(); + params.paletteValueSet = PresContext()->GetFontPaletteValueSet(); + + DrawText(aParams.range, aParams.textBaselinePt + shadowGfxOffset, params); + + contextBoxBlur.DoPaint(); + aParams.context->Restore(); +} + +SelectionTypeMask nsTextFrame::ResolveSelections( + const PaintTextSelectionParams& aParams, const SelectionDetails* aDetails, + nsTArray<SelectionRange>& aResult, SelectionType aSelectionType, + bool* aAnyBackgrounds) const { + const gfxTextRun::Range& contentRange = aParams.contentRange; + + SelectionTypeMask allTypes = 0; + bool anyBackgrounds = false; + + uint32_t i = 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(contentRange.start, uint32_t(sd->mStart)); + uint32_t end = std::min(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, background; + if (GetSelectionTextColors(sd->mSelectionType, sd->mHighlightName, + *aParams.textPaintStyle, sd->mTextRangeStyle, + &foreground, &background)) { + if (NS_GET_A(background) > 0) { + anyBackgrounds = true; + } + uint32_t prio = kSelectionTypeCount - uint32_t(sd->mSelectionType); + aResult.AppendElement(SelectionRange{sd, {start, end}, prio}); + } + } 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. + aResult.AppendElement(SelectionRange{sd, {start, end}, i++}); + } + } + } + if (aAnyBackgrounds) { + *aAnyBackgrounds = anyBackgrounds; + } + + if (aResult.Length() < 1) { + return allTypes; + } + + // Sort by starting offset. + 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; + } + }; + aResult.Sort(SelectionRangeStartCmp()); + + // Resolve overlapping selections according to priority. + uint32_t currentIndex = 0; + while (currentIndex < aResult.Length() - 1) { + auto& current = aResult[currentIndex]; + uint32_t probeIndex = currentIndex + 1; + while (probeIndex < aResult.Length()) { + auto& probe = aResult[probeIndex]; + if (probe.mRange.start >= current.mRange.end) { + break; + } + // TODO: ordering of highlight selections + if (current.mDetails->mSelectionType <= probe.mDetails->mSelectionType) { + // current overwrites (beginning or all of) probe + if (current.mRange.end >= probe.mRange.end) { + aResult.RemoveElementAt(probeIndex); + } else { + probe.mRange.start = current.mRange.end; + ++probeIndex; + } + } else { + // probe overwrites current (possibly splitting it) + if (probe.mRange.start > current.mRange.start) { + uint32_t prevEnd = current.mRange.end; + current.mRange.end = probe.mRange.start; + if (probe.mRange.end < prevEnd) { + aResult.InsertElementSorted( + SelectionRange{current.mDetails, + {probe.mRange.end, prevEnd}, + current.mPriority}, + SelectionRangeStartCmp()); + } + break; + } + if (probe.mRange.end < current.mRange.end) { + aResult.InsertElementSorted( + SelectionRange{current.mDetails, + {probe.mRange.end, current.mRange.end}, + current.mPriority}, + SelectionRangeStartCmp()); + } + aResult.RemoveElementAt(currentIndex); + --currentIndex; // This is OK even when currentIndex is zero, because + // unsigned under/overflow is defined as wrapping. + } + } + ++currentIndex; + } + + 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<SelectionRange, 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 + TextRangeStyle rangeStyle; + + 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); + SelectionType selectionType; + RefPtr<const nsAtom> highlightName; + while (iterator.GetNextSegment(&iOffset, &range, &hyphenWidth, + &selectionType, &highlightName, + &rangeStyle)) { + nscolor foreground, background; + GetSelectionTextColors(selectionType, highlightName, + *aParams.textPaintStyle, rangeStyle, &foreground, + &background); + // Draw background color + gfxFloat advance = + hyphenWidth + mTextRun->GetAdvanceWidth(range, aParams.provider); + if (NS_GET_A(background) > 0) { + nsRect bgRect; + gfxFloat offs = iOffset - (mTextRun->IsInlineReversed() ? advance : 0); + if (vertical) { + bgRect = nsRect(aParams.framePt.x, aParams.framePt.y + offs, + GetSize().width, advance); + } else { + bgRect = nsRect(aParams.framePt.x + offs, aParams.framePt.y, advance, + GetSize().height); + } + + LayoutDeviceRect selectionRect = + LayoutDeviceRect::FromAppUnits(bgRect, appUnitsPerDevPixel); + + 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); + 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.paletteValueSet = PresContext()->GetFontPaletteValueSet(); + + PaintShadowParams shadowParams(aParams); + shadowParams.provider = aParams.provider; + shadowParams.clipEdges = &aClipEdges; + + // Draw text + const nsStyleText* textStyle = StyleText(); + SelectionRangeIterator iterator(selectionRanges, contentRange, + *aParams.provider, mTextRun, startIOffset); + SelectionType selectionType; + RefPtr<const nsAtom> highlightName; + while (iterator.GetNextSegment(&iOffset, &range, &hyphenWidth, &selectionType, + &highlightName, &rangeStyle)) { + nscolor foreground, background; + if (aParams.IsGenerateTextMask()) { + foreground = NS_RGBA(0, 0, 0, 255); + } else { + GetSelectionTextColors(selectionType, highlightName, + *aParams.textPaintStyle, rangeStyle, &foreground, + &background); + } + + gfx::Point textBaselinePt = + vertical + ? gfx::Point(aParams.textBaselinePt.x, aParams.framePt.y + iOffset) + : gfx::Point(aParams.framePt.x + iOffset, aParams.textBaselinePt.y); + + // Determine what shadow, if any, to draw - either from textStyle + // or from the ::-moz-selection pseudo-class if specified there + Span<const StyleSimpleShadow> shadows = textStyle->mTextShadow.AsSpan(); + GetSelectionTextShadow(selectionType, *aParams.textPaintStyle, &shadows); + if (!shadows.IsEmpty()) { + nscoord startEdge = iOffset; + if (mTextRun->IsInlineReversed()) { + startEdge -= + hyphenWidth + mTextRun->GetAdvanceWidth(range, aParams.provider); + } + shadowParams.range = range; + shadowParams.textBaselinePt = textBaselinePt; + shadowParams.foregroundColor = foreground; + shadowParams.leftSideOffset = startEdge; + PaintShadows(shadows, shadowParams); + } + + // Draw text segment + params.textColor = foreground; + params.textStrokeColor = aParams.textPaintStyle->GetWebkitTextStrokeColor(); + params.textStrokeWidth = aParams.textPaintStyle->GetWebkitTextStrokeWidth(); + params.drawSoftHyphen = hyphenWidth > 0; + DrawText(range, textBaselinePt, params); + advance += hyphenWidth; + iterator.UpdateWithAdvance(advance); + } + return true; +} + +void nsTextFrame::PaintTextSelectionDecorations( + const PaintTextSelectionParams& aParams, + const UniquePtr<SelectionDetails>& aDetails, SelectionType aSelectionType) { + // Hide text decorations if we're currently hiding @font-face fallback text + if (aParams.provider->GetFontGroup()->ShouldSkipDrawing()) { + return; + } + + AutoTArray<SelectionRange, 8> selectionRanges; + ResolveSelections(aParams, aDetails.get(), selectionRanges, aSelectionType); + + RefPtr<gfxFont> firstFont = + aParams.provider->GetFontGroup()->GetFirstValidFont(); + bool verticalRun = mTextRun->IsVertical(); + bool useVerticalMetrics = verticalRun && mTextRun->UseCenterBaseline(); + bool rightUnderline = useVerticalMetrics && IsUnderlineRight(*Style()); + const auto kDecoration = rightUnderline ? StyleTextDecorationLine::OVERLINE + : StyleTextDecorationLine::UNDERLINE; + gfxFont::Metrics decorationMetrics( + firstFont->GetMetrics(useVerticalMetrics ? nsFontMetrics::eVertical + : nsFontMetrics::eHorizontal)); + decorationMetrics.underlineOffset = + aParams.provider->GetFontGroup()->GetUnderlineOffset(); + + const gfxTextRun::Range& contentRange = aParams.contentRange; + gfxFloat startIOffset = verticalRun + ? aParams.textBaselinePt.y - aParams.framePt.y + : aParams.textBaselinePt.x - aParams.framePt.x; + SelectionRangeIterator iterator(selectionRanges, contentRange, + *aParams.provider, mTextRun, startIOffset); + gfxFloat iOffset, hyphenWidth; + Range range; + int32_t app = aParams.textPaintStyle->PresContext()->AppUnitsPerDevPixel(); + // XXX aTextBaselinePt is in AppUnits, shouldn't it be nsFloatPoint? + Point pt; + if (verticalRun) { + pt.x = (aParams.textBaselinePt.x - mAscent) / app; + } else { + pt.y = (aParams.textBaselinePt.y - mAscent) / app; + } + SelectionType nextSelectionType; + RefPtr<const nsAtom> highlightName; + TextRangeStyle selectedStyle; + + while (iterator.GetNextSegment(&iOffset, &range, &hyphenWidth, + &nextSelectionType, &highlightName, + &selectedStyle)) { + gfxFloat advance = + hyphenWidth + mTextRun->GetAdvanceWidth(range, aParams.provider); + if (nextSelectionType == aSelectionType) { + if (verticalRun) { + pt.y = (aParams.framePt.y + iOffset - + (mTextRun->IsInlineReversed() ? advance : 0)) / + app; + } else { + pt.x = (aParams.framePt.x + iOffset - + (mTextRun->IsInlineReversed() ? advance : 0)) / + app; + } + gfxFloat width = Abs(advance) / app; + gfxFloat xInFrame = pt.x - (aParams.framePt.x / app); + DrawSelectionDecorations(aParams.context, aParams.dirtyRect, + aSelectionType, *aParams.textPaintStyle, + selectedStyle, pt, xInFrame, width, + mAscent / app, decorationMetrics, + aParams.callbacks, verticalRun, kDecoration); + } + iterator.UpdateWithAdvance(advance); + } +} + +bool nsTextFrame::PaintTextWithSelection( + const PaintTextSelectionParams& aParams, const ClipEdges& aClipEdges) { + NS_ASSERTION(GetContent()->IsMaybeSelected(), "wrong paint path"); + + UniquePtr<SelectionDetails> details = GetSelectionDetails(); + if (!details) { + return false; + } + + SelectionTypeMask allSelectionTypeMask; + if (!PaintTextWithSelectionColors(aParams, details, &allSelectionTypeMask, + aClipEdges)) { + return false; + } + // Iterate through just the selection rawSelectionTypes that paint decorations + // and paint decorations for any that actually occur in this frame. Paint + // higher-numbered selection rawSelectionTypes below lower-numered ones on the + // general principal that lower-numbered selections are higher priority. + allSelectionTypeMask &= kSelectionTypesWithDecorations; + MOZ_ASSERT(kPresentSelectionTypes[0] == SelectionType::eNormal, + "The following for loop assumes that the first item of " + "kPresentSelectionTypes is SelectionType::eNormal"); + for (size_t i = ArrayLength(kPresentSelectionTypes) - 1; i >= 1; --i) { + SelectionType selectionType = kPresentSelectionTypes[i]; + if (ToSelectionTypeMask(selectionType) & allSelectionTypeMask) { + // There is some selection of this selectionType. Try to paint its + // decorations (there might not be any for this type but that's OK, + // PaintTextSelectionDecorations will exit early). + PaintTextSelectionDecorations(aParams, details, selectionType); + } + } + + return true; +} + +void nsTextFrame::DrawEmphasisMarks(gfxContext* aContext, WritingMode aWM, + const gfx::Point& aTextBaselinePt, + const gfx::Point& aFramePt, Range aRange, + const nscolor* aDecorationOverrideColor, + PropertyProvider* aProvider) { + const EmphasisMarkInfo* info = GetProperty(EmphasisMarkProperty()); + if (!info) { + return; + } + + bool isTextCombined = Style()->IsTextCombined(); + if (isTextCombined && !aWM.IsVertical()) { + // XXX This only happens when the parent is display:contents with an + // orthogonal writing mode. This should be rare, and don't have use + // cases, so we don't care. It is non-trivial to implement a sane + // behavior for that case: if you treat the text as not combined, + // the marks would spread wider than the text (which is rendered as + // combined); if you try to draw a single mark, selecting part of + // the text could dynamically create multiple new marks. + NS_WARNING("Give up on combined text with horizontal wm"); + return; + } + nscolor color = + aDecorationOverrideColor + ? *aDecorationOverrideColor + : nsLayoutUtils::GetColor(this, &nsStyleText::mTextEmphasisColor); + aContext->SetColor(sRGBColor::FromABGR(color)); + gfx::Point pt; + if (!isTextCombined) { + pt = aTextBaselinePt; + } else { + MOZ_ASSERT(aWM.IsVertical()); + pt = aFramePt; + if (aWM.IsVerticalRL()) { + pt.x += GetSize().width - GetLogicalBaseline(aWM); + } else { + pt.x += GetLogicalBaseline(aWM); + } + } + if (!aWM.IsVertical()) { + pt.y += info->baselineOffset; + } else { + if (aWM.IsVerticalRL()) { + pt.x -= info->baselineOffset; + } else { + pt.x += info->baselineOffset; + } + } + if (!isTextCombined) { + mTextRun->DrawEmphasisMarks(aContext, info->textRun.get(), info->advance, + pt, aRange, aProvider); + } else { + pt.y += (GetSize().height - info->advance) / 2; + gfxTextRun::DrawParams params(aContext); + info->textRun->Draw(Range(info->textRun.get()), pt, params); + } +} + +nscolor nsTextFrame::GetCaretColorAt(int32_t aOffset) { + MOZ_ASSERT(aOffset >= 0, "aOffset must be positive"); + + nscolor result = nsIFrame::GetCaretColorAt(aOffset); + gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); + PropertyProvider provider(this, iter, nsTextFrame::eInflated, mFontMetrics); + int32_t contentOffset = provider.GetStart().GetOriginalOffset(); + int32_t contentLength = provider.GetOriginalLength(); + MOZ_ASSERT( + aOffset >= contentOffset && aOffset <= contentOffset + contentLength, + "aOffset must be in the frame's range"); + + int32_t offsetInFrame = aOffset - contentOffset; + if (offsetInFrame < 0 || offsetInFrame >= contentLength) { + return result; + } + + bool isSolidTextColor = true; + if (IsInSVGTextSubtree()) { + const nsStyleSVG* style = StyleSVG(); + if (!style->mFill.kind.IsNone() && !style->mFill.kind.IsColor()) { + isSolidTextColor = false; + } + } + + nsTextPaintStyle textPaintStyle(this); + textPaintStyle.SetResolveColors(isSolidTextColor); + UniquePtr<SelectionDetails> details = GetSelectionDetails(); + SelectionType selectionType = SelectionType::eNone; + for (SelectionDetails* sdptr = details.get(); sdptr; + sdptr = sdptr->mNext.get()) { + int32_t start = std::max(0, sdptr->mStart - contentOffset); + int32_t end = std::min(contentLength, sdptr->mEnd - contentOffset); + if (start <= offsetInFrame && offsetInFrame < end && + (selectionType == SelectionType::eNone || + sdptr->mSelectionType < selectionType)) { + nscolor foreground, background; + if (GetSelectionTextColors(sdptr->mSelectionType, sdptr->mHighlightName, + textPaintStyle, sdptr->mTextRangeStyle, + &foreground, &background)) { + if (!isSolidTextColor && NS_IS_SELECTION_SPECIAL_COLOR(foreground)) { + result = NS_RGBA(0, 0, 0, 255); + } else { + result = foreground; + } + selectionType = sdptr->mSelectionType; + } + } + } + + return result; +} + +static gfxTextRun::Range ComputeTransformedRange( + nsTextFrame::PropertyProvider& aProvider) { + gfxSkipCharsIterator iter(aProvider.GetStart()); + uint32_t start = iter.GetSkippedOffset(); + iter.AdvanceOriginal(aProvider.GetOriginalLength()); + return gfxTextRun::Range(start, iter.GetSkippedOffset()); +} + +bool nsTextFrame::MeasureCharClippedText(nscoord aVisIStartEdge, + nscoord aVisIEndEdge, + nscoord* aSnappedStartEdge, + nscoord* aSnappedEndEdge) { + // We need a *reference* rendering context (not one that might have a + // transform), so we don't have a rendering context argument. + // XXX get the block and line passed to us somehow! This is slow! + gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); + if (!mTextRun) { + return false; + } + + PropertyProvider provider(this, iter, nsTextFrame::eInflated, mFontMetrics); + // Trim trailing whitespace + provider.InitializeForDisplay(true); + + Range range = ComputeTransformedRange(provider); + uint32_t startOffset = range.start; + uint32_t maxLength = range.Length(); + return MeasureCharClippedText(provider, aVisIStartEdge, aVisIEndEdge, + &startOffset, &maxLength, aSnappedStartEdge, + aSnappedEndEdge); +} + +static uint32_t GetClusterLength(const gfxTextRun* aTextRun, + uint32_t aStartOffset, uint32_t aMaxLength) { + uint32_t clusterLength = 0; + while (++clusterLength < aMaxLength) { + if (aTextRun->IsClusterStart(aStartOffset + clusterLength)) { + return clusterLength; + } + } + return aMaxLength; +} + +bool nsTextFrame::MeasureCharClippedText( + PropertyProvider& aProvider, nscoord aVisIStartEdge, nscoord aVisIEndEdge, + uint32_t* aStartOffset, uint32_t* aMaxLength, nscoord* aSnappedStartEdge, + nscoord* aSnappedEndEdge) { + *aSnappedStartEdge = 0; + *aSnappedEndEdge = 0; + if (aVisIStartEdge <= 0 && aVisIEndEdge <= 0) { + return true; + } + + uint32_t offset = *aStartOffset; + uint32_t maxLength = *aMaxLength; + const nscoord frameISize = ISize(); + const bool rtl = mTextRun->IsRightToLeft(); + gfxFloat advanceWidth = 0; + const nscoord startEdge = rtl ? aVisIEndEdge : aVisIStartEdge; + if (startEdge > 0) { + const gfxFloat maxAdvance = gfxFloat(startEdge); + while (maxLength > 0) { + uint32_t clusterLength = GetClusterLength(mTextRun, offset, maxLength); + advanceWidth += mTextRun->GetAdvanceWidth( + Range(offset, offset + clusterLength), &aProvider); + maxLength -= clusterLength; + offset += clusterLength; + if (advanceWidth >= maxAdvance) { + break; + } + } + nscoord* snappedStartEdge = rtl ? aSnappedEndEdge : aSnappedStartEdge; + *snappedStartEdge = NSToCoordFloor(advanceWidth); + *aStartOffset = offset; + } + + const nscoord endEdge = rtl ? aVisIStartEdge : aVisIEndEdge; + if (endEdge > 0) { + const gfxFloat maxAdvance = gfxFloat(frameISize - endEdge); + while (maxLength > 0) { + uint32_t clusterLength = GetClusterLength(mTextRun, offset, maxLength); + gfxFloat nextAdvance = + advanceWidth + mTextRun->GetAdvanceWidth( + Range(offset, offset + clusterLength), &aProvider); + if (nextAdvance > maxAdvance) { + break; + } + // This cluster fits, include it. + advanceWidth = nextAdvance; + maxLength -= clusterLength; + offset += clusterLength; + } + maxLength = offset - *aStartOffset; + nscoord* snappedEndEdge = rtl ? aSnappedStartEdge : aSnappedEndEdge; + *snappedEndEdge = NSToCoordFloor(gfxFloat(frameISize) - advanceWidth); + } + *aMaxLength = maxLength; + return maxLength != 0; +} + +void nsTextFrame::PaintShadows(Span<const StyleSimpleShadow> aShadows, + const PaintShadowParams& aParams) { + if (aShadows.IsEmpty()) { + return; + } + + gfxTextRun::Metrics shadowMetrics = mTextRun->MeasureText( + aParams.range, gfxFont::LOOSE_INK_EXTENTS, nullptr, aParams.provider); + if (GetWritingMode().IsLineInverted()) { + std::swap(shadowMetrics.mAscent, shadowMetrics.mDescent); + shadowMetrics.mBoundingBox.y = -shadowMetrics.mBoundingBox.YMost(); + } + if (HasAnyStateBits(TEXT_HYPHEN_BREAK)) { + AddHyphenToMetrics(this, mTextRun->IsRightToLeft(), &shadowMetrics, + gfxFont::LOOSE_INK_EXTENTS, + aParams.context->GetDrawTarget()); + } + // Add bounds of text decorations + gfxRect decorationRect(0, -shadowMetrics.mAscent, shadowMetrics.mAdvanceWidth, + shadowMetrics.mAscent + shadowMetrics.mDescent); + shadowMetrics.mBoundingBox.UnionRect(shadowMetrics.mBoundingBox, + decorationRect); + + // If the textrun uses any color or SVG fonts, we need to force use of a mask + // for shadow rendering even if blur radius is zero. + // Force disable hardware acceleration for text shadows since it's usually + // more expensive than just doing it on the CPU. + uint32_t blurFlags = nsContextBoxBlur::DISABLE_HARDWARE_ACCELERATION_BLUR; + uint32_t numGlyphRuns; + const gfxTextRun::GlyphRun* run = mTextRun->GetGlyphRuns(&numGlyphRuns); + while (numGlyphRuns-- > 0) { + if (run->mFont->AlwaysNeedsMaskForShadow()) { + blurFlags |= nsContextBoxBlur::FORCE_MASK; + break; + } + run++; + } + + if (mTextRun->IsVertical()) { + std::swap(shadowMetrics.mBoundingBox.x, shadowMetrics.mBoundingBox.y); + std::swap(shadowMetrics.mBoundingBox.width, + shadowMetrics.mBoundingBox.height); + } + + for (const auto& shadow : Reversed(aShadows)) { + PaintOneShadow(aParams, shadow, shadowMetrics.mBoundingBox, blurFlags); + } +} + +void nsTextFrame::PaintText(const PaintTextParams& aParams, + const nscoord aVisIStartEdge, + const nscoord aVisIEndEdge, + const nsPoint& aToReferenceFrame, + const bool aIsSelected, + float aOpacity /* = 1.0f */) { +#ifdef DEBUG + if (IsInSVGTextSubtree()) { + auto* container = + nsLayoutUtils::GetClosestFrameOfType(this, LayoutFrameType::SVGText); + MOZ_ASSERT(container); + MOZ_ASSERT(!container->HasAnyStateBits(NS_STATE_SVG_CLIPPATH_CHILD) || + !aParams.IsPaintText(), + "Expecting IsPaintText to be false for a clipPath"); + } +#endif + + // Don't pass in the rendering context here, because we need a + // *reference* context and rendering context might have some transform + // in it + // XXX get the block and line passed to us somehow! This is slow! + gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); + if (!mTextRun) { + return; + } + + PropertyProvider provider(this, iter, nsTextFrame::eInflated, mFontMetrics); + + // Trim trailing whitespace, unless we're painting a selection highlight, + // which should include trailing spaces if present (bug 1146754). + provider.InitializeForDisplay(!aIsSelected); + + const bool reversed = mTextRun->IsInlineReversed(); + const bool verticalRun = mTextRun->IsVertical(); + WritingMode wm = GetWritingMode(); + const float frameWidth = GetSize().width; + const float frameHeight = GetSize().height; + gfx::Point textBaselinePt; + if (verticalRun) { + if (wm.IsVerticalLR()) { + textBaselinePt.x = nsLayoutUtils::GetSnappedBaselineX( + this, aParams.context, nscoord(aParams.framePt.x), mAscent); + } else { + textBaselinePt.x = nsLayoutUtils::GetSnappedBaselineX( + this, aParams.context, nscoord(aParams.framePt.x) + frameWidth, + -mAscent); + } + textBaselinePt.y = reversed ? aParams.framePt.y.value + frameHeight + : aParams.framePt.y.value; + } else { + textBaselinePt = + gfx::Point(reversed ? aParams.framePt.x.value + frameWidth + : aParams.framePt.x.value, + nsLayoutUtils::GetSnappedBaselineY( + this, aParams.context, aParams.framePt.y, mAscent)); + } + Range range = ComputeTransformedRange(provider); + uint32_t startOffset = range.start; + uint32_t maxLength = range.Length(); + nscoord snappedStartEdge, snappedEndEdge; + if (!MeasureCharClippedText(provider, aVisIStartEdge, aVisIEndEdge, + &startOffset, &maxLength, &snappedStartEdge, + &snappedEndEdge)) { + return; + } + if (verticalRun) { + textBaselinePt.y += reversed ? -snappedEndEdge : snappedStartEdge; + } else { + textBaselinePt.x += reversed ? -snappedEndEdge : snappedStartEdge; + } + const ClipEdges clipEdges(this, aToReferenceFrame, snappedStartEdge, + snappedEndEdge); + nsTextPaintStyle textPaintStyle(this); + textPaintStyle.SetResolveColors(!aParams.callbacks); + + // Fork off to the (slower) paint-with-selection path if necessary. + if (aIsSelected) { + MOZ_ASSERT(aOpacity == 1.0f, "We don't support opacity with selections!"); + gfxSkipCharsIterator tmp(provider.GetStart()); + Range contentRange( + uint32_t(tmp.ConvertSkippedToOriginal(startOffset)), + uint32_t(tmp.ConvertSkippedToOriginal(startOffset + maxLength))); + PaintTextSelectionParams params(aParams); + params.textBaselinePt = textBaselinePt; + params.provider = &provider; + params.contentRange = contentRange; + params.textPaintStyle = &textPaintStyle; + params.glyphRange = range; + if (PaintTextWithSelection(params, clipEdges)) { + return; + } + } + + nscolor foregroundColor = aParams.IsGenerateTextMask() + ? NS_RGBA(0, 0, 0, 255) + : textPaintStyle.GetTextColor(); + if (aOpacity != 1.0f) { + gfx::sRGBColor gfxColor = gfx::sRGBColor::FromABGR(foregroundColor); + gfxColor.a *= aOpacity; + foregroundColor = gfxColor.ToABGR(); + } + + nscolor textStrokeColor = aParams.IsGenerateTextMask() + ? NS_RGBA(0, 0, 0, 255) + : textPaintStyle.GetWebkitTextStrokeColor(); + if (aOpacity != 1.0f) { + gfx::sRGBColor gfxColor = gfx::sRGBColor::FromABGR(textStrokeColor); + gfxColor.a *= aOpacity; + textStrokeColor = gfxColor.ToABGR(); + } + + range = Range(startOffset, startOffset + maxLength); + if (aParams.IsPaintText()) { + const nsStyleText* textStyle = StyleText(); + PaintShadowParams shadowParams(aParams); + shadowParams.range = range; + shadowParams.textBaselinePt = textBaselinePt; + shadowParams.leftSideOffset = snappedStartEdge; + shadowParams.provider = &provider; + shadowParams.foregroundColor = foregroundColor; + shadowParams.clipEdges = &clipEdges; + PaintShadows(textStyle->mTextShadow.AsSpan(), shadowParams); + } + + gfxFloat advanceWidth; + DrawTextParams params(aParams.context); + params.dirtyRect = aParams.dirtyRect; + params.framePt = aParams.framePt; + params.provider = &provider; + params.advanceWidth = &advanceWidth; + params.textStyle = &textPaintStyle; + params.textColor = foregroundColor; + params.textStrokeColor = textStrokeColor; + params.textStrokeWidth = textPaintStyle.GetWebkitTextStrokeWidth(); + params.clipEdges = &clipEdges; + params.drawSoftHyphen = HasAnyStateBits(TEXT_HYPHEN_BREAK); + params.contextPaint = aParams.contextPaint; + params.callbacks = aParams.callbacks; + params.glyphRange = range; + params.fontPalette = StyleFont()->GetFontPaletteAtom(); + params.paletteValueSet = PresContext()->GetFontPaletteValueSet(); + + DrawText(range, textBaselinePt, params); +} + +static void DrawTextRun(const gfxTextRun* aTextRun, + const gfx::Point& aTextBaselinePt, + gfxTextRun::Range aRange, + const nsTextFrame::DrawTextRunParams& aParams, + nsTextFrame* aFrame) { + gfxTextRun::DrawParams params(aParams.context); + params.provider = aParams.provider; + params.advanceWidth = aParams.advanceWidth; + params.contextPaint = aParams.contextPaint; + params.fontPalette = aParams.fontPalette; + params.paletteValueSet = aParams.paletteValueSet; + params.callbacks = aParams.callbacks; + if (aParams.callbacks) { + aParams.callbacks->NotifyBeforeText(aParams.textColor); + params.drawMode = DrawMode::GLYPH_PATH; + aTextRun->Draw(aRange, aTextBaselinePt, params); + aParams.callbacks->NotifyAfterText(); + } else { + auto* textDrawer = aParams.context->GetTextDrawer(); + if (NS_GET_A(aParams.textColor) != 0 || textDrawer || + aParams.textStrokeWidth == 0.0f) { + aParams.context->SetColor(sRGBColor::FromABGR(aParams.textColor)); + } else { + params.drawMode = DrawMode::GLYPH_STROKE; + } + + if ((NS_GET_A(aParams.textStrokeColor) != 0 || textDrawer) && + aParams.textStrokeWidth != 0.0f) { + if (textDrawer) { + textDrawer->FoundUnsupportedFeature(); + return; + } + params.drawMode |= DrawMode::GLYPH_STROKE; + + // Check the paint-order property; if we find stroke before fill, + // then change mode to GLYPH_STROKE_UNDERNEATH. + uint32_t paintOrder = aFrame->StyleSVG()->mPaintOrder; + while (paintOrder) { + auto component = StylePaintOrder(paintOrder & kPaintOrderMask); + switch (component) { + case StylePaintOrder::Fill: + // Just break the loop, no need to check further + paintOrder = 0; + break; + case StylePaintOrder::Stroke: + params.drawMode |= DrawMode::GLYPH_STROKE_UNDERNEATH; + paintOrder = 0; + break; + default: + MOZ_FALLTHROUGH_ASSERT("Unknown paint-order variant, how?"); + case StylePaintOrder::Markers: + case StylePaintOrder::Normal: + break; + } + paintOrder >>= kPaintOrderShift; + } + + // Use ROUND joins as they are less likely to produce ugly artifacts + // when stroking glyphs with sharp angles (see bug 1546985). + StrokeOptions strokeOpts(aParams.textStrokeWidth, JoinStyle::ROUND); + params.textStrokeColor = aParams.textStrokeColor; + params.strokeOpts = &strokeOpts; + aTextRun->Draw(aRange, aTextBaselinePt, params); + } else { + aTextRun->Draw(aRange, aTextBaselinePt, params); + } + } +} + +void nsTextFrame::DrawTextRun(Range aRange, const gfx::Point& aTextBaselinePt, + const DrawTextRunParams& aParams) { + MOZ_ASSERT(aParams.advanceWidth, "Must provide advanceWidth"); + + ::DrawTextRun(mTextRun, aTextBaselinePt, aRange, aParams, this); + + if (aParams.drawSoftHyphen) { + // Don't use ctx as the context, because we need a reference context here, + // ctx may be transformed. + DrawTextRunParams params = aParams; + params.provider = nullptr; + params.advanceWidth = nullptr; + RefPtr<gfxTextRun> hyphenTextRun = GetHyphenTextRun(this, nullptr); + if (hyphenTextRun) { + gfx::Point p(aTextBaselinePt); + bool vertical = GetWritingMode().IsVertical(); + // For right-to-left text runs, the soft-hyphen is positioned at the left + // of the text. + float shift = mTextRun->GetDirection() * (*aParams.advanceWidth); + if (vertical) { + p.y += shift; + } else { + p.x += shift; + } + ::DrawTextRun(hyphenTextRun.get(), p, Range(hyphenTextRun.get()), params, + this); + } + } +} + +void nsTextFrame::DrawTextRunAndDecorations( + Range aRange, const gfx::Point& aTextBaselinePt, + const DrawTextParams& aParams, const TextDecorations& aDecorations) { + const gfxFloat app = aParams.textStyle->PresContext()->AppUnitsPerDevPixel(); + // Writing mode of parent frame is used because the text frame may + // be orthogonal to its parent when text-combine-upright is used or + // its parent has "display: contents", and in those cases, we want + // to draw the decoration lines according to parents' direction + // rather than ours. + const WritingMode wm = GetParent()->GetWritingMode(); + bool verticalDec = wm.IsVertical(); + bool verticalRun = mTextRun->IsVertical(); + // If the text run and the decoration is orthogonal, we choose the + // metrics for decoration so that decoration line won't be broken. + bool useVerticalMetrics = verticalDec != verticalRun + ? verticalDec + : verticalRun && mTextRun->UseCenterBaseline(); + + // XXX aFramePt is in AppUnits, shouldn't it be nsFloatPoint? + nscoord x = NSToCoordRound(aParams.framePt.x); + nscoord y = NSToCoordRound(aParams.framePt.y); + + // 'measure' here is textrun-relative, so for a horizontal run it's the + // width, while for a vertical run it's the height of the decoration + const nsSize frameSize = GetSize(); + nscoord measure = verticalDec ? frameSize.height : frameSize.width; + + if (verticalDec) { + aParams.clipEdges->Intersect(&y, &measure); + } else { + aParams.clipEdges->Intersect(&x, &measure); + } + + // decSize is a textrun-relative size, so its 'width' field is actually + // the run-relative measure, and 'height' will be the line thickness + gfxFloat ascent = gfxFloat(GetLogicalBaseline(wm)) / app; + // The starting edge of the frame in block direction + gfxFloat frameBStart = verticalDec ? aParams.framePt.x : aParams.framePt.y; + + // In vertical-rl mode, block coordinates are measured from the + // right, so we need to adjust here. + if (wm.IsVerticalRL()) { + frameBStart += frameSize.width; + ascent = -ascent; + } + + nscoord inflationMinFontSize = nsLayoutUtils::InflationMinFontSizeFor(this); + + PaintDecorationLineParams params; + params.context = aParams.context; + params.dirtyRect = aParams.dirtyRect; + params.overrideColor = aParams.decorationOverrideColor; + params.callbacks = aParams.callbacks; + params.glyphRange = aParams.glyphRange; + params.provider = aParams.provider; + // pt is the physical point where the decoration is to be drawn, + // relative to the frame; one of its coordinates will be updated below. + params.pt = Point(x / app, y / app); + Float& bCoord = verticalDec ? params.pt.x.value : params.pt.y.value; + params.lineSize = Size(measure / app, 0); + params.ascent = ascent; + params.vertical = verticalDec; + params.sidewaysLeft = mTextRun->IsSidewaysLeft(); + + // The matrix of the context may have been altered for text-combine- + // upright. However, we want to draw decoration lines unscaled, thus + // we need to revert the scaling here. + gfxContextMatrixAutoSaveRestore scaledRestorer; + if (Style()->IsTextCombined()) { + float scaleFactor = GetTextCombineScaleFactor(this); + if (scaleFactor != 1.0f) { + scaledRestorer.SetContext(aParams.context); + gfxMatrix unscaled = aParams.context->CurrentMatrixDouble(); + gfxPoint pt(x / app, y / app); + unscaled.PreTranslate(pt) + .PreScale(1.0f / scaleFactor, 1.0f) + .PreTranslate(-pt); + aParams.context->SetMatrixDouble(unscaled); + } + } + + typedef gfxFont::Metrics Metrics; + auto paintDecorationLine = [&](const LineDecoration& dec, + gfxFloat Metrics::*lineSize, + StyleTextDecorationLine lineType) { + if (dec.mStyle == StyleTextDecorationStyle::None) { + return; + } + + float inflation = + GetInflationForTextDecorations(dec.mFrame, inflationMinFontSize); + const Metrics metrics = GetFirstFontMetrics( + GetFontGroupForFrame(dec.mFrame, inflation), useVerticalMetrics); + + bCoord = (frameBStart - dec.mBaselineOffset) / app; + + params.color = dec.mColor; + params.baselineOffset = dec.mBaselineOffset / app; + params.defaultLineThickness = metrics.*lineSize; + params.lineSize.height = ComputeDecorationLineThickness( + dec.mTextDecorationThickness, params.defaultLineThickness, metrics, app, + dec.mFrame); + + bool swapUnderline = wm.IsCentralBaseline() && IsUnderlineRight(*Style()); + params.offset = ComputeDecorationLineOffset( + lineType, dec.mTextUnderlinePosition, dec.mTextUnderlineOffset, metrics, + app, dec.mFrame, wm.IsCentralBaseline(), swapUnderline); + + params.style = dec.mStyle; + PaintDecorationLine(params); + }; + + // We create a clip region in order to draw the decoration lines only in the + // range of the text. Restricting the draw area prevents the decoration lines + // to be drawn multiple times when a part of the text is selected. + + // We skip clipping for the following cases: + // - drawing the whole text + // - having different orientation of the text and the writing-mode, such as + // "text-combine-upright" (Bug 1408825) + bool skipClipping = + aRange.Length() == mTextRun->GetLength() || verticalDec != verticalRun; + + gfxRect clipRect; + if (!skipClipping) { + // Get the inline-size according to the specified range. + gfxFloat clipLength = mTextRun->GetAdvanceWidth(aRange, aParams.provider); + nsRect visualRect = InkOverflowRect(); + + const bool isInlineReversed = mTextRun->IsInlineReversed(); + if (verticalDec) { + clipRect.x = aParams.framePt.x + visualRect.x; + clipRect.y = isInlineReversed ? aTextBaselinePt.y.value - clipLength + : aTextBaselinePt.y.value; + clipRect.width = visualRect.width; + clipRect.height = clipLength; + } else { + clipRect.x = isInlineReversed ? aTextBaselinePt.x.value - clipLength + : aTextBaselinePt.x.value; + clipRect.y = aParams.framePt.y + visualRect.y; + clipRect.width = clipLength; + clipRect.height = visualRect.height; + } + + clipRect.Scale(1 / app); + clipRect.Round(); + params.context->Clip(clipRect); + } + + // Underlines + params.decoration = StyleTextDecorationLine::UNDERLINE; + for (const LineDecoration& dec : Reversed(aDecorations.mUnderlines)) { + paintDecorationLine(dec, &Metrics::underlineSize, params.decoration); + } + + // Overlines + params.decoration = StyleTextDecorationLine::OVERLINE; + for (const LineDecoration& dec : Reversed(aDecorations.mOverlines)) { + paintDecorationLine(dec, &Metrics::underlineSize, params.decoration); + } + + // Some glyphs and emphasis marks may extend outside the region, so we reset + // the clip region here. For an example, italic glyphs. + if (!skipClipping) { + params.context->PopClip(); + } + + { + gfxContextMatrixAutoSaveRestore unscaledRestorer; + if (scaledRestorer.HasMatrix()) { + unscaledRestorer.SetContext(aParams.context); + aParams.context->SetMatrix(scaledRestorer.Matrix()); + } + + // CSS 2.1 mandates that text be painted after over/underlines, + // and *then* line-throughs + DrawTextRun(aRange, aTextBaselinePt, aParams); + } + + // Emphasis marks + DrawEmphasisMarks(aParams.context, wm, aTextBaselinePt, aParams.framePt, + aRange, aParams.decorationOverrideColor, aParams.provider); + + // Re-apply the clip region when the line-through is being drawn. + if (!skipClipping) { + params.context->Clip(clipRect); + } + + // Line-throughs + params.decoration = StyleTextDecorationLine::LINE_THROUGH; + for (const LineDecoration& dec : Reversed(aDecorations.mStrikes)) { + paintDecorationLine(dec, &Metrics::strikeoutSize, params.decoration); + } + + if (!skipClipping) { + params.context->PopClip(); + } +} + +void nsTextFrame::DrawText(Range aRange, const gfx::Point& aTextBaselinePt, + const DrawTextParams& aParams) { + TextDecorations decorations; + GetTextDecorations(aParams.textStyle->PresContext(), + aParams.callbacks ? eUnresolvedColors : eResolvedColors, + decorations); + + // Hide text decorations if we're currently hiding @font-face fallback text + const bool drawDecorations = + !aParams.provider->GetFontGroup()->ShouldSkipDrawing() && + (decorations.HasDecorationLines() || + StyleText()->HasEffectiveTextEmphasis()); + if (drawDecorations) { + DrawTextRunAndDecorations(aRange, aTextBaselinePt, aParams, decorations); + } else { + DrawTextRun(aRange, aTextBaselinePt, aParams); + } + + if (auto* textDrawer = aParams.context->GetTextDrawer()) { + textDrawer->TerminateShadows(); + } +} + +NS_DECLARE_FRAME_PROPERTY_DELETABLE(WebRenderTextBounds, nsRect) + +nsRect nsTextFrame::WebRenderBounds() { + // WR text bounds is just our ink overflow rect but without shadows. So if we + // have no shadows, just use the layout bounds. + if (!StyleText()->HasTextShadow()) { + return InkOverflowRect(); + } + nsRect* cachedBounds = GetProperty(WebRenderTextBounds()); + if (!cachedBounds) { + OverflowAreas overflowAreas; + ComputeCustomOverflowInternal(overflowAreas, false); + cachedBounds = new nsRect(overflowAreas.InkOverflow()); + SetProperty(WebRenderTextBounds(), cachedBounds); + } + return *cachedBounds; +} + +int16_t nsTextFrame::GetSelectionStatus(int16_t* aSelectionFlags) { + // get the selection controller + nsCOMPtr<nsISelectionController> selectionController; + nsresult rv = GetSelectionController(PresContext(), + getter_AddRefs(selectionController)); + if (NS_FAILED(rv) || !selectionController) + return nsISelectionController::SELECTION_OFF; + + selectionController->GetSelectionFlags(aSelectionFlags); + + int16_t selectionValue; + selectionController->GetDisplaySelection(&selectionValue); + + return selectionValue; +} + +bool nsTextFrame::IsEntirelyWhitespace() const { + const nsTextFragment& text = mContent->AsText()->TextFragment(); + for (uint32_t index = 0; index < text.GetLength(); ++index) { + const char16_t ch = text.CharAt(index); + if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == 0xa0) { + continue; + } + return false; + } + return true; +} + +/** + * Compute the longest prefix of text whose width is <= aWidth. Return + * the length of the prefix. Also returns the width of the prefix in aFitWidth. + */ +static uint32_t CountCharsFit(const gfxTextRun* aTextRun, + gfxTextRun::Range aRange, gfxFloat aWidth, + nsTextFrame::PropertyProvider* aProvider, + gfxFloat* aFitWidth) { + uint32_t last = 0; + gfxFloat width = 0; + for (uint32_t i = 1; i <= aRange.Length(); ++i) { + if (i == aRange.Length() || aTextRun->IsClusterStart(aRange.start + i)) { + gfxTextRun::Range range(aRange.start + last, aRange.start + i); + gfxFloat nextWidth = width + aTextRun->GetAdvanceWidth(range, aProvider); + if (nextWidth > aWidth) { + break; + } + last = i; + width = nextWidth; + } + } + *aFitWidth = width; + return last; +} + +nsIFrame::ContentOffsets nsTextFrame::CalcContentOffsetsFromFramePoint( + const nsPoint& aPoint) { + return GetCharacterOffsetAtFramePointInternal(aPoint, true); +} + +nsIFrame::ContentOffsets nsTextFrame::GetCharacterOffsetAtFramePoint( + const nsPoint& aPoint) { + return GetCharacterOffsetAtFramePointInternal(aPoint, false); +} + +nsIFrame::ContentOffsets nsTextFrame::GetCharacterOffsetAtFramePointInternal( + const nsPoint& aPoint, bool aForInsertionPoint) { + ContentOffsets offsets; + + gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); + if (!mTextRun) { + return offsets; + } + + PropertyProvider provider(this, iter, nsTextFrame::eInflated, mFontMetrics); + // Trim leading but not trailing whitespace if possible + provider.InitializeForDisplay(false); + gfxFloat width = + mTextRun->IsVertical() + ? (mTextRun->IsInlineReversed() ? mRect.height - aPoint.y : aPoint.y) + : (mTextRun->IsInlineReversed() ? mRect.width - aPoint.x : aPoint.x); + if (Style()->IsTextCombined()) { + width /= GetTextCombineScaleFactor(this); + } + gfxFloat fitWidth; + Range skippedRange = ComputeTransformedRange(provider); + + uint32_t charsFit = + CountCharsFit(mTextRun, skippedRange, width, &provider, &fitWidth); + + int32_t selectedOffset; + if (charsFit < skippedRange.Length()) { + // charsFit characters fitted, but no more could fit. See if we're + // more than halfway through the cluster.. If we are, choose the next + // cluster. + gfxSkipCharsIterator extraCluster(provider.GetStart()); + extraCluster.AdvanceSkipped(charsFit); + + bool allowSplitLigature = true; // Allow selection of partial ligature... + + // ...but don't let selection/insertion-point split two Regional Indicator + // chars that are ligated in the textrun to form a single flag symbol. + uint32_t offs = extraCluster.GetOriginalOffset(); + const nsTextFragment* frag = TextFragment(); + if (frag->IsHighSurrogateFollowedByLowSurrogateAt(offs) && + gfxFontUtils::IsRegionalIndicator(frag->ScalarValueAt(offs))) { + allowSplitLigature = false; + if (extraCluster.GetSkippedOffset() > 1 && + !mTextRun->IsLigatureGroupStart(extraCluster.GetSkippedOffset())) { + // CountCharsFit() left us in the middle of the flag; back up over the + // first character of the ligature, and adjust fitWidth accordingly. + extraCluster.AdvanceSkipped(-2); // it's a surrogate pair: 2 code units + fitWidth -= mTextRun->GetAdvanceWidth( + Range(extraCluster.GetSkippedOffset(), + extraCluster.GetSkippedOffset() + 2), + &provider); + } + } + + gfxSkipCharsIterator extraClusterLastChar(extraCluster); + FindClusterEnd( + mTextRun, + provider.GetStart().GetOriginalOffset() + provider.GetOriginalLength(), + &extraClusterLastChar, allowSplitLigature); + PropertyProvider::Spacing spacing; + Range extraClusterRange(extraCluster.GetSkippedOffset(), + extraClusterLastChar.GetSkippedOffset() + 1); + gfxFloat charWidth = + mTextRun->GetAdvanceWidth(extraClusterRange, &provider, &spacing); + charWidth -= spacing.mBefore + spacing.mAfter; + selectedOffset = !aForInsertionPoint || + width <= fitWidth + spacing.mBefore + charWidth / 2 + ? extraCluster.GetOriginalOffset() + : extraClusterLastChar.GetOriginalOffset() + 1; + } else { + // All characters fitted, we're at (or beyond) the end of the text. + // XXX This could be some pathological situation where negative spacing + // caused characters to move backwards. We can't really handle that + // in the current frame system because frames can't have negative + // intrinsic widths. + selectedOffset = + provider.GetStart().GetOriginalOffset() + provider.GetOriginalLength(); + // If we're at the end of a preformatted line which has a terminating + // linefeed, we want to reduce the offset by one to make sure that the + // selection is placed before the linefeed character. + if (HasSignificantTerminalNewline()) { + --selectedOffset; + } + } + + offsets.content = GetContent(); + offsets.offset = offsets.secondaryOffset = selectedOffset; + offsets.associate = mContentOffset == offsets.offset ? CARET_ASSOCIATE_AFTER + : CARET_ASSOCIATE_BEFORE; + return offsets; +} + +bool nsTextFrame::CombineSelectionUnderlineRect(nsPresContext* aPresContext, + nsRect& aRect) { + if (aRect.IsEmpty()) { + return false; + } + + nsRect givenRect = aRect; + + gfxFontGroup* fontGroup = GetInflatedFontGroupForFrame(this); + RefPtr<gfxFont> firstFont = fontGroup->GetFirstValidFont(); + WritingMode wm = GetWritingMode(); + bool verticalRun = wm.IsVertical(); + bool useVerticalMetrics = verticalRun && !wm.IsSideways(); + const gfxFont::Metrics& metrics = + firstFont->GetMetrics(useVerticalMetrics ? nsFontMetrics::eVertical + : nsFontMetrics::eHorizontal); + + nsCSSRendering::DecorationRectParams params; + params.ascent = aPresContext->AppUnitsToGfxUnits(mAscent); + + params.offset = fontGroup->GetUnderlineOffset(); + + TextDecorations textDecs; + GetTextDecorations(aPresContext, eResolvedColors, textDecs); + + params.descentLimit = + ComputeDescentLimitForSelectionUnderline(aPresContext, metrics); + params.vertical = verticalRun; + + EnsureTextRun(nsTextFrame::eInflated); + params.sidewaysLeft = mTextRun ? mTextRun->IsSidewaysLeft() : false; + + UniquePtr<SelectionDetails> details = GetSelectionDetails(); + for (SelectionDetails* sd = details.get(); sd; sd = sd->mNext.get()) { + if (sd->mStart == sd->mEnd || + sd->mSelectionType == SelectionType::eInvalid || + !(ToSelectionTypeMask(sd->mSelectionType) & + kSelectionTypesWithDecorations) || + // URL strikeout does not use underline. + sd->mSelectionType == SelectionType::eURLStrikeout) { + continue; + } + + float relativeSize; + int32_t index = nsTextPaintStyle::GetUnderlineStyleIndexForSelectionType( + sd->mSelectionType); + if (sd->mSelectionType == SelectionType::eSpellCheck) { + if (!nsTextPaintStyle::GetSelectionUnderline( + this, index, nullptr, &relativeSize, ¶ms.style)) { + continue; + } + } else { + // IME selections + TextRangeStyle& rangeStyle = sd->mTextRangeStyle; + if (rangeStyle.IsDefined()) { + if (!rangeStyle.IsLineStyleDefined() || + rangeStyle.mLineStyle == TextRangeStyle::LineStyle::None) { + continue; + } + params.style = ToStyleLineStyle(rangeStyle); + relativeSize = rangeStyle.mIsBoldLine ? 2.0f : 1.0f; + } else if (!nsTextPaintStyle::GetSelectionUnderline( + this, index, nullptr, &relativeSize, ¶ms.style)) { + continue; + } + } + nsRect decorationArea; + + const auto& decThickness = StyleTextReset()->mTextDecorationThickness; + params.lineSize.width = aPresContext->AppUnitsToGfxUnits(aRect.width); + params.defaultLineThickness = ComputeSelectionUnderlineHeight( + aPresContext, metrics, sd->mSelectionType); + + params.lineSize.height = ComputeDecorationLineThickness( + decThickness, params.defaultLineThickness, metrics, + aPresContext->AppUnitsPerDevPixel(), this); + + bool swapUnderline = wm.IsCentralBaseline() && IsUnderlineRight(*Style()); + const auto* styleText = StyleText(); + params.offset = ComputeDecorationLineOffset( + textDecs.HasUnderline() ? StyleTextDecorationLine::UNDERLINE + : StyleTextDecorationLine::OVERLINE, + styleText->mTextUnderlinePosition, styleText->mTextUnderlineOffset, + metrics, aPresContext->AppUnitsPerDevPixel(), this, + wm.IsCentralBaseline(), swapUnderline); + + relativeSize = std::max(relativeSize, 1.0f); + params.lineSize.height *= relativeSize; + params.defaultLineThickness *= relativeSize; + decorationArea = + nsCSSRendering::GetTextDecorationRect(aPresContext, params); + aRect.UnionRect(aRect, decorationArea); + } + + return !aRect.IsEmpty() && !givenRect.Contains(aRect); +} + +bool nsTextFrame::IsFrameSelected() const { + NS_ASSERTION(!GetContent() || GetContent()->IsMaybeSelected(), + "use the public IsSelected() instead"); + if (mIsSelected == nsTextFrame::SelectionState::Unknown) { + const bool isSelected = + GetContent()->IsSelected(GetContentOffset(), GetContentEnd()); + mIsSelected = isSelected ? nsTextFrame::SelectionState::Selected + : nsTextFrame::SelectionState::NotSelected; + } else { +#ifdef DEBUG + // Assert that the selection caching works. + const bool isReallySelected = + GetContent()->IsSelected(GetContentOffset(), GetContentEnd()); + NS_ASSERTION((mIsSelected == nsTextFrame::SelectionState::Selected) == + isReallySelected, + "Should have called InvalidateSelectionState()"); +#endif + } + + return mIsSelected == nsTextFrame::SelectionState::Selected; +} + +nsTextFrame* nsTextFrame::FindContinuationForOffset(int32_t aOffset) { + // Use a continuations array to accelerate finding the first continuation + // of interest, if possible. + MOZ_ASSERT(!GetPrevContinuation(), "should be called on the primary frame"); + auto* continuations = GetContinuations(); + nsTextFrame* f = this; + if (continuations) { + size_t index; + if (BinarySearchIf( + *continuations, 0, continuations->Length(), + [=](nsTextFrame* aFrame) -> int { + return aOffset - aFrame->GetContentOffset(); + }, + &index)) { + f = (*continuations)[index]; + } else { + f = (*continuations)[index ? index - 1 : 0]; + } + } + + while (f && f->GetContentEnd() <= aOffset) { + f = f->GetNextContinuation(); + } + + return f; +} + +void nsTextFrame::SelectionStateChanged(uint32_t aStart, uint32_t aEnd, + bool aSelected, + SelectionType aSelectionType) { + NS_ASSERTION(!GetPrevContinuation(), + "Should only be called for primary frame"); + DEBUG_VERIFY_NOT_DIRTY(GetStateBits()); + + InvalidateSelectionState(); + + // Selection is collapsed, which can't affect text frame rendering + if (aStart == aEnd) { + return; + } + + nsTextFrame* f = FindContinuationForOffset(aStart); + + nsPresContext* presContext = PresContext(); + while (f && f->GetContentOffset() < int32_t(aEnd)) { + // We may need to reflow to recompute the overflow area for + // spellchecking or IME underline if their underline is thicker than + // the normal decoration line. + if (ToSelectionTypeMask(aSelectionType) & kSelectionTypesWithDecorations) { + bool didHaveOverflowingSelection = + f->HasAnyStateBits(TEXT_SELECTION_UNDERLINE_OVERFLOWED); + nsRect r(nsPoint(0, 0), GetSize()); + if (didHaveOverflowingSelection || + (aSelected && f->CombineSelectionUnderlineRect(presContext, r))) { + presContext->PresShell()->FrameNeedsReflow( + f, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); + } + } + // Selection might change anything. Invalidate the overflow area. + f->InvalidateFrame(); + + f = f->GetNextContinuation(); + } +} + +void nsTextFrame::UpdateIteratorFromOffset(const PropertyProvider& aProperties, + int32_t& aInOffset, + gfxSkipCharsIterator& aIter) { + if (aInOffset < GetContentOffset()) { + NS_WARNING("offset before this frame's content"); + aInOffset = GetContentOffset(); + } else if (aInOffset > GetContentEnd()) { + NS_WARNING("offset after this frame's content"); + aInOffset = GetContentEnd(); + } + + int32_t trimmedOffset = aProperties.GetStart().GetOriginalOffset(); + int32_t trimmedEnd = trimmedOffset + aProperties.GetOriginalLength(); + aInOffset = std::max(aInOffset, trimmedOffset); + aInOffset = std::min(aInOffset, trimmedEnd); + + aIter.SetOriginalOffset(aInOffset); + + if (aInOffset < trimmedEnd && !aIter.IsOriginalCharSkipped() && + !mTextRun->IsClusterStart(aIter.GetSkippedOffset())) { + // Called for non-cluster boundary + FindClusterStart(mTextRun, trimmedOffset, &aIter); + } +} + +nsPoint nsTextFrame::GetPointFromIterator(const gfxSkipCharsIterator& aIter, + PropertyProvider& aProperties) { + Range range(aProperties.GetStart().GetSkippedOffset(), + aIter.GetSkippedOffset()); + gfxFloat advance = mTextRun->GetAdvanceWidth(range, &aProperties); + nscoord iSize = NSToCoordCeilClamped(advance); + nsPoint point; + + if (mTextRun->IsVertical()) { + point.x = 0; + if (mTextRun->IsInlineReversed()) { + point.y = mRect.height - iSize; + } else { + point.y = iSize; + } + } else { + point.y = 0; + if (mTextRun->IsInlineReversed()) { + point.x = mRect.width - iSize; + } else { + point.x = iSize; + } + if (Style()->IsTextCombined()) { + point.x *= GetTextCombineScaleFactor(this); + } + } + return point; +} + +nsresult nsTextFrame::GetPointFromOffset(int32_t inOffset, nsPoint* outPoint) { + if (!outPoint) { + return NS_ERROR_NULL_POINTER; + } + + DEBUG_VERIFY_NOT_DIRTY(GetStateBits()); + if (HasAnyStateBits(NS_FRAME_IS_DIRTY)) { + return NS_ERROR_UNEXPECTED; + } + + if (GetContentLength() <= 0) { + outPoint->x = 0; + outPoint->y = 0; + return NS_OK; + } + + gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); + if (!mTextRun) { + return NS_ERROR_FAILURE; + } + + PropertyProvider properties(this, iter, nsTextFrame::eInflated, mFontMetrics); + // Don't trim trailing whitespace, we want the caret to appear in the right + // place if it's positioned there + properties.InitializeForDisplay(false); + + UpdateIteratorFromOffset(properties, inOffset, iter); + + *outPoint = GetPointFromIterator(iter, properties); + + return NS_OK; +} + +nsresult nsTextFrame::GetCharacterRectsInRange(int32_t aInOffset, + int32_t aLength, + nsTArray<nsRect>& aRects) { + DEBUG_VERIFY_NOT_DIRTY(GetStateBits()); + if (HasAnyStateBits(NS_FRAME_IS_DIRTY)) { + return NS_ERROR_UNEXPECTED; + } + + if (GetContentLength() <= 0) { + return NS_OK; + } + + if (!mTextRun) { + return NS_ERROR_FAILURE; + } + + gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); + PropertyProvider properties(this, iter, nsTextFrame::eInflated, mFontMetrics); + // Don't trim trailing whitespace, we want the caret to appear in the right + // place if it's positioned there + properties.InitializeForDisplay(false); + + UpdateIteratorFromOffset(properties, aInOffset, iter); + + const int32_t kContentEnd = GetContentEnd(); + const int32_t kEndOffset = std::min(aInOffset + aLength, kContentEnd); + while (aInOffset < kEndOffset) { + if (!iter.IsOriginalCharSkipped() && + !mTextRun->IsClusterStart(iter.GetSkippedOffset())) { + FindClusterStart(mTextRun, + properties.GetStart().GetOriginalOffset() + + properties.GetOriginalLength(), + &iter); + } + + nsPoint point = GetPointFromIterator(iter, properties); + nsRect rect; + rect.x = point.x; + rect.y = point.y; + + nscoord iSize = 0; + if (aInOffset < kContentEnd) { + gfxSkipCharsIterator nextIter(iter); + nextIter.AdvanceOriginal(1); + if (!nextIter.IsOriginalCharSkipped() && + !mTextRun->IsClusterStart(nextIter.GetSkippedOffset()) && + nextIter.GetOriginalOffset() < kContentEnd) { + FindClusterEnd(mTextRun, kContentEnd, &nextIter); + } + + gfxFloat advance = mTextRun->GetAdvanceWidth( + Range(iter.GetSkippedOffset(), nextIter.GetSkippedOffset()), + &properties); + iSize = NSToCoordCeilClamped(advance); + } + + if (mTextRun->IsVertical()) { + rect.width = mRect.width; + rect.height = iSize; + if (mTextRun->IsInlineReversed()) { + // The iterator above returns a point with the origin at the + // bottom left instead of the top left. Move the origin to the top left + // by subtracting the character's height. + rect.y -= rect.height; + } + } else { + rect.width = iSize; + rect.height = mRect.height; + if (Style()->IsTextCombined()) { + rect.width *= GetTextCombineScaleFactor(this); + } + if (mTextRun->IsInlineReversed()) { + // The iterator above returns a point with the origin at the + // top right instead of the top left. Move the origin to the top left by + // subtracting the character's width. This is intentionally done after + // GetTextCombineScaleFactor() so we use the final, scaled width. + rect.x -= rect.width; + } + } + aRects.AppendElement(rect); + aInOffset++; + // Don't advance iter if we've reached the end + if (aInOffset < kEndOffset) { + iter.AdvanceOriginal(1); + } + } + + return NS_OK; +} + +nsresult nsTextFrame::GetChildFrameContainingOffset(int32_t aContentOffset, + bool aHint, + int32_t* aOutOffset, + nsIFrame** aOutFrame) { + DEBUG_VERIFY_NOT_DIRTY(GetStateBits()); +#if 0 // XXXrbs disable due to bug 310227 + if (HasAnyStateBits(NS_FRAME_IS_DIRTY)) + return NS_ERROR_UNEXPECTED; +#endif + + NS_ASSERTION(aOutOffset && aOutFrame, "Bad out parameters"); + NS_ASSERTION(aContentOffset >= 0, + "Negative content offset, existing code was very broken!"); + nsIFrame* primaryFrame = mContent->GetPrimaryFrame(); + if (this != primaryFrame) { + // This call needs to happen on the primary frame + return primaryFrame->GetChildFrameContainingOffset(aContentOffset, aHint, + aOutOffset, aOutFrame); + } + + nsTextFrame* f = this; + int32_t offset = mContentOffset; + + // Try to look up the offset to frame property + nsTextFrame* cachedFrame = GetProperty(OffsetToFrameProperty()); + + if (cachedFrame) { + f = cachedFrame; + offset = f->GetContentOffset(); + + f->RemoveStateBits(TEXT_IN_OFFSET_CACHE); + } + + if ((aContentOffset >= offset) && (aHint || aContentOffset != offset)) { + while (true) { + nsTextFrame* next = f->GetNextContinuation(); + if (!next || aContentOffset < next->GetContentOffset()) { + break; + } + if (aContentOffset == next->GetContentOffset()) { + if (aHint) { + f = next; + if (f->GetContentLength() == 0) { + continue; // use the last of the empty frames with this offset + } + } + break; + } + f = next; + } + } else { + while (true) { + nsTextFrame* prev = f->GetPrevContinuation(); + if (!prev || aContentOffset > f->GetContentOffset()) { + break; + } + if (aContentOffset == f->GetContentOffset()) { + if (!aHint) { + f = prev; + if (f->GetContentLength() == 0) { + continue; // use the first of the empty frames with this offset + } + } + break; + } + f = prev; + } + } + + *aOutOffset = aContentOffset - f->GetContentOffset(); + *aOutFrame = f; + + // cache the frame we found + SetProperty(OffsetToFrameProperty(), f); + f->AddStateBits(TEXT_IN_OFFSET_CACHE); + + return NS_OK; +} + +nsIFrame::FrameSearchResult nsTextFrame::PeekOffsetNoAmount(bool aForward, + int32_t* aOffset) { + NS_ASSERTION(aOffset && *aOffset <= GetContentLength(), + "aOffset out of range"); + + gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); + if (!mTextRun) { + return CONTINUE_EMPTY; + } + + TrimmedOffsets trimmed = GetTrimmedOffsets(TextFragment()); + // Check whether there are nonskipped characters in the trimmmed range + return (iter.ConvertOriginalToSkipped(trimmed.GetEnd()) > + iter.ConvertOriginalToSkipped(trimmed.mStart)) + ? FOUND + : CONTINUE; +} + +/** + * This class iterates through the clusters before or after the given + * aPosition (which is a content offset). You can test each cluster + * to see if it's whitespace (as far as selection/caret movement is concerned), + * or punctuation, or if there is a word break before the cluster. ("Before" + * is interpreted according to aDirection, so if aDirection is -1, "before" + * means actually *after* the cluster content.) + */ +class MOZ_STACK_CLASS ClusterIterator { + public: + ClusterIterator(nsTextFrame* aTextFrame, int32_t aPosition, + int32_t aDirection, nsString& aContext, + bool aTrimSpaces = true); + + bool NextCluster(); + bool IsInlineWhitespace() const; + bool IsNewline() const; + bool IsPunctuation() const; + bool HaveWordBreakBefore() const { return mHaveWordBreak; } + + // Get the charIndex that corresponds to the "before" side of the current + // character, according to the direction of iteration: so for a forward + // iterator, this is simply mCharIndex, while for a reverse iterator it will + // be mCharIndex + <number of code units in the character>. + int32_t GetBeforeOffset() const { + MOZ_ASSERT(mCharIndex >= 0); + return mDirection < 0 ? GetAfterInternal() : mCharIndex; + } + // Get the charIndex that corresponds to the "before" side of the current + // character, according to the direction of iteration: the opposite side + // to what GetBeforeOffset returns. + int32_t GetAfterOffset() const { + MOZ_ASSERT(mCharIndex >= 0); + return mDirection > 0 ? GetAfterInternal() : mCharIndex; + } + + private: + // Helper for Get{After,Before}Offset; returns the charIndex after the + // current position in the text, accounting for surrogate pairs. + int32_t GetAfterInternal() const; + + gfxSkipCharsIterator mIterator; + // Usually, mFrag is pointer to `dom::CharacterData::mText`. However, if + // we're in a password field, this points `mMaskedFrag`. + const nsTextFragment* mFrag; + // If we're in a password field, this is initialized with mask characters. + nsTextFragment mMaskedFrag; + nsTextFrame* mTextFrame; + int32_t mDirection; // +1 or -1, or 0 to indicate failure + int32_t mCharIndex; + nsTextFrame::TrimmedOffsets mTrimmed; + nsTArray<bool> mWordBreaks; + bool mHaveWordBreak; +}; + +static bool IsAcceptableCaretPosition(const gfxSkipCharsIterator& aIter, + bool aRespectClusters, + const gfxTextRun* aTextRun, + nsTextFrame* aFrame) { + if (aIter.IsOriginalCharSkipped()) { + return false; + } + uint32_t index = aIter.GetSkippedOffset(); + if (aRespectClusters && !aTextRun->IsClusterStart(index)) { + return false; + } + if (index > 0) { + // Check whether the proposed position is in between the two halves of a + // surrogate pair, before a Variation Selector character, or within a + // ligated emoji sequence; if so, this is not a valid character boundary. + // (In the case where we are respecting clusters, we won't actually get + // this far because the low surrogate is also marked as non-clusterStart + // so we'll return FALSE above.) + const uint32_t offs = AssertedCast<uint32_t>(aIter.GetOriginalOffset()); + const nsTextFragment* frag = aFrame->TextFragment(); + const char16_t ch = frag->CharAt(offs); + + if (gfxFontUtils::IsVarSelector(ch) || + frag->IsLowSurrogateFollowingHighSurrogateAt(offs) || + (!aTextRun->IsLigatureGroupStart(index) && + (unicode::GetEmojiPresentation(ch) == unicode::EmojiDefault || + (unicode::GetEmojiPresentation(ch) == unicode::TextDefault && + offs + 1 < frag->GetLength() && + frag->CharAt(offs + 1) == gfxFontUtils::kUnicodeVS16)))) { + return false; + } + + // If the proposed position is before a high surrogate, we need to decode + // the surrogate pair (if valid) and check the resulting character. + if (NS_IS_HIGH_SURROGATE(ch)) { + if (const char32_t ucs4 = frag->ScalarValueAt(offs)) { + // If the character is a (Plane-14) variation selector, + // or an emoji character that is ligated with the previous + // character (i.e. part of a Regional-Indicator flag pair, + // or an emoji-ZWJ sequence), this is not a valid boundary. + if (gfxFontUtils::IsVarSelector(ucs4) || + (!aTextRun->IsLigatureGroupStart(index) && + unicode::GetEmojiPresentation(ucs4) == unicode::EmojiDefault)) { + return false; + } + } + } + } + return true; +} + +nsIFrame::FrameSearchResult nsTextFrame::PeekOffsetCharacter( + bool aForward, int32_t* aOffset, PeekOffsetCharacterOptions aOptions) { + int32_t contentLength = GetContentLength(); + NS_ASSERTION(aOffset && *aOffset <= contentLength, "aOffset out of range"); + + if (!aOptions.mIgnoreUserStyleAll) { + StyleUserSelect selectStyle; + Unused << IsSelectable(&selectStyle); + if (selectStyle == StyleUserSelect::All) { + return CONTINUE_UNSELECTABLE; + } + } + + gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); + if (!mTextRun) { + return CONTINUE_EMPTY; + } + + TrimmedOffsets trimmed = + GetTrimmedOffsets(TextFragment(), TrimmedOffsetFlags::NoTrimAfter); + + // A negative offset means "end of frame". + int32_t startOffset = + GetContentOffset() + (*aOffset < 0 ? contentLength : *aOffset); + + if (!aForward) { + // If at the beginning of the line, look at the previous continuation + for (int32_t i = std::min(trimmed.GetEnd(), startOffset) - 1; + i >= trimmed.mStart; --i) { + iter.SetOriginalOffset(i); + if (IsAcceptableCaretPosition(iter, aOptions.mRespectClusters, mTextRun, + this)) { + *aOffset = i - mContentOffset; + return FOUND; + } + } + *aOffset = 0; + } else { + // If we're at the end of a line, look at the next continuation + iter.SetOriginalOffset(startOffset); + if (startOffset <= trimmed.GetEnd() && + !(startOffset < trimmed.GetEnd() && + StyleText()->NewlineIsSignificant(this) && + iter.GetSkippedOffset() < mTextRun->GetLength() && + mTextRun->CharIsNewline(iter.GetSkippedOffset()))) { + for (int32_t i = startOffset + 1; i <= trimmed.GetEnd(); ++i) { + iter.SetOriginalOffset(i); + if (i == trimmed.GetEnd() || + IsAcceptableCaretPosition(iter, aOptions.mRespectClusters, mTextRun, + this)) { + *aOffset = i - mContentOffset; + return FOUND; + } + } + } + *aOffset = contentLength; + } + + return CONTINUE; +} + +bool ClusterIterator::IsInlineWhitespace() const { + NS_ASSERTION(mCharIndex >= 0, "No cluster selected"); + return IsSelectionInlineWhitespace(mFrag, mCharIndex); +} + +bool ClusterIterator::IsNewline() const { + NS_ASSERTION(mCharIndex >= 0, "No cluster selected"); + return IsSelectionNewline(mFrag, mCharIndex); +} + +bool ClusterIterator::IsPunctuation() const { + NS_ASSERTION(mCharIndex >= 0, "No cluster selected"); + // Return true for all Punctuation categories (Unicode general category P?), + // and also for Symbol categories (S?) except for Modifier Symbol, which is + // kept together with any adjacent letter/number. (Bug 1066756) + const char16_t ch = mFrag->CharAt(AssertedCast<uint32_t>(mCharIndex)); + const uint8_t cat = unicode::GetGeneralCategory(ch); + switch (cat) { + case HB_UNICODE_GENERAL_CATEGORY_CONNECT_PUNCTUATION: /* Pc */ + if (ch == '_' && !StaticPrefs::layout_word_select_stop_at_underscore()) { + return false; + } + [[fallthrough]]; + case HB_UNICODE_GENERAL_CATEGORY_DASH_PUNCTUATION: /* Pd */ + case HB_UNICODE_GENERAL_CATEGORY_CLOSE_PUNCTUATION: /* Pe */ + case HB_UNICODE_GENERAL_CATEGORY_FINAL_PUNCTUATION: /* Pf */ + case HB_UNICODE_GENERAL_CATEGORY_INITIAL_PUNCTUATION: /* Pi */ + case HB_UNICODE_GENERAL_CATEGORY_OTHER_PUNCTUATION: /* Po */ + case HB_UNICODE_GENERAL_CATEGORY_OPEN_PUNCTUATION: /* Ps */ + case HB_UNICODE_GENERAL_CATEGORY_CURRENCY_SYMBOL: /* Sc */ + // Deliberately omitted: + // case HB_UNICODE_GENERAL_CATEGORY_MODIFIER_SYMBOL: /* Sk */ + case HB_UNICODE_GENERAL_CATEGORY_MATH_SYMBOL: /* Sm */ + case HB_UNICODE_GENERAL_CATEGORY_OTHER_SYMBOL: /* So */ + return true; + default: + return false; + } +} + +int32_t ClusterIterator::GetAfterInternal() const { + if (mFrag->IsHighSurrogateFollowedByLowSurrogateAt( + AssertedCast<uint32_t>(mCharIndex))) { + return mCharIndex + 2; + } + return mCharIndex + 1; +} + +bool ClusterIterator::NextCluster() { + if (!mDirection) { + return false; + } + const gfxTextRun* textRun = mTextFrame->GetTextRun(nsTextFrame::eInflated); + + mHaveWordBreak = false; + while (true) { + bool keepGoing = false; + if (mDirection > 0) { + if (mIterator.GetOriginalOffset() >= mTrimmed.GetEnd()) { + return false; + } + keepGoing = mIterator.IsOriginalCharSkipped() || + mIterator.GetOriginalOffset() < mTrimmed.mStart || + !textRun->IsClusterStart(mIterator.GetSkippedOffset()); + mCharIndex = mIterator.GetOriginalOffset(); + mIterator.AdvanceOriginal(1); + } else { + if (mIterator.GetOriginalOffset() <= mTrimmed.mStart) { + // Trimming can skip backward word breakers, see bug 1667138 + return mHaveWordBreak; + } + mIterator.AdvanceOriginal(-1); + keepGoing = mIterator.IsOriginalCharSkipped() || + mIterator.GetOriginalOffset() >= mTrimmed.GetEnd() || + !textRun->IsClusterStart(mIterator.GetSkippedOffset()); + mCharIndex = mIterator.GetOriginalOffset(); + } + + if (mWordBreaks[GetBeforeOffset() - mTextFrame->GetContentOffset()]) { + mHaveWordBreak = true; + } + if (!keepGoing) { + return true; + } + } +} + +ClusterIterator::ClusterIterator(nsTextFrame* aTextFrame, int32_t aPosition, + int32_t aDirection, nsString& aContext, + bool aTrimSpaces) + : mIterator(aTextFrame->EnsureTextRun(nsTextFrame::eInflated)), + mTextFrame(aTextFrame), + mDirection(aDirection), + mCharIndex(-1), + mHaveWordBreak(false) { + gfxTextRun* textRun = aTextFrame->GetTextRun(nsTextFrame::eInflated); + if (!textRun) { + mDirection = 0; // signal failure + return; + } + + mFrag = aTextFrame->TextFragment(); + // If we're in a password field, some characters may be masked. In such + // case, we need to treat each masked character is a mask character since + // we shouldn't expose word boundary which is hidden by the masking. + if (aTextFrame->GetContent() && mFrag->GetLength() > 0 && + aTextFrame->GetContent()->HasFlag(NS_MAYBE_MASKED) && + (textRun->GetFlags2() & nsTextFrameUtils::Flags::IsTransformed)) { + const char16_t kPasswordMask = TextEditor::PasswordMask(); + const nsTransformedTextRun* transformedTextRun = + static_cast<const nsTransformedTextRun*>(textRun); + // Use nsString and not nsAutoString so that we get a nsStringBuffer which + // can be just AddRefed in `mMaskedFrag`. + nsString maskedText; + maskedText.SetCapacity(mFrag->GetLength()); + for (uint32_t i = 0; i < mFrag->GetLength(); ++i) { + mIterator.SetOriginalOffset(i); + uint32_t skippedOffset = mIterator.GetSkippedOffset(); + if (mFrag->IsHighSurrogateFollowedByLowSurrogateAt(i)) { + if (transformedTextRun->mStyles[skippedOffset]->mMaskPassword) { + maskedText.Append(kPasswordMask); + maskedText.Append(kPasswordMask); + } else { + maskedText.Append(mFrag->CharAt(i)); + maskedText.Append(mFrag->CharAt(i + 1)); + } + ++i; + } else { + maskedText.Append( + transformedTextRun->mStyles[skippedOffset]->mMaskPassword + ? kPasswordMask + : mFrag->CharAt(i)); + } + } + mMaskedFrag.SetTo(maskedText, mFrag->IsBidi(), true); + mFrag = &mMaskedFrag; + } + + mIterator.SetOriginalOffset(aPosition); + mTrimmed = aTextFrame->GetTrimmedOffsets( + mFrag, aTrimSpaces ? nsTextFrame::TrimmedOffsetFlags::Default + : nsTextFrame::TrimmedOffsetFlags::NoTrimAfter | + nsTextFrame::TrimmedOffsetFlags::NoTrimBefore); + + const uint32_t textOffset = + AssertedCast<uint32_t>(aTextFrame->GetContentOffset()); + const uint32_t textLen = + AssertedCast<uint32_t>(aTextFrame->GetContentLength()); + + // Allocate an extra element to record the word break at the end of the line + // or text run in mWordBreak[textLen]. + mWordBreaks.AppendElements(textLen + 1); + PodZero(mWordBreaks.Elements(), textLen + 1); + uint32_t textStart; + if (aDirection > 0) { + if (aContext.IsEmpty()) { + // No previous context, so it must be the start of a line or text run + mWordBreaks[0] = true; + } + textStart = aContext.Length(); + mFrag->AppendTo(aContext, textOffset, textLen); + } else { + if (aContext.IsEmpty()) { + // No following context, so it must be the end of a line or text run + mWordBreaks[textLen] = true; + } + textStart = 0; + nsAutoString str; + mFrag->AppendTo(str, textOffset, textLen); + aContext.Insert(str, 0); + } + + const uint32_t textEnd = textStart + textLen; + intl::WordBreakIteratorUtf16 wordBreakIter(aContext); + Maybe<uint32_t> nextBreak = + wordBreakIter.Seek(textStart > 0 ? textStart - 1 : textStart); + while (nextBreak && *nextBreak <= textEnd) { + mWordBreaks[*nextBreak - textStart] = true; + nextBreak = wordBreakIter.Next(); + } + + MOZ_ASSERT(textEnd != aContext.Length() || mWordBreaks[textLen], + "There should be a word break at the end of a line or text run!"); +} + +nsIFrame::FrameSearchResult nsTextFrame::PeekOffsetWord( + bool aForward, bool aWordSelectEatSpace, bool aIsKeyboardSelect, + int32_t* aOffset, PeekWordState* aState, bool aTrimSpaces) { + int32_t contentLength = GetContentLength(); + NS_ASSERTION(aOffset && *aOffset <= contentLength, "aOffset out of range"); + + StyleUserSelect selectStyle; + Unused << IsSelectable(&selectStyle); + if (selectStyle == StyleUserSelect::All) { + return CONTINUE_UNSELECTABLE; + } + + int32_t offset = + GetContentOffset() + (*aOffset < 0 ? contentLength : *aOffset); + ClusterIterator cIter(this, offset, aForward ? 1 : -1, aState->mContext, + aTrimSpaces); + + if (!cIter.NextCluster()) { + return CONTINUE_EMPTY; + } + + do { + bool isPunctuation = cIter.IsPunctuation(); + bool isInlineWhitespace = cIter.IsInlineWhitespace(); + bool isWhitespace = isInlineWhitespace || cIter.IsNewline(); + bool isWordBreakBefore = cIter.HaveWordBreakBefore(); + if (!isWhitespace || isInlineWhitespace) { + aState->SetSawInlineCharacter(); + } + if (aWordSelectEatSpace == isWhitespace && !aState->mSawBeforeType) { + aState->SetSawBeforeType(); + aState->Update(isPunctuation, isWhitespace); + continue; + } + // See if we can break before the current cluster + if (!aState->mAtStart) { + bool canBreak; + if (isPunctuation != aState->mLastCharWasPunctuation) { + canBreak = BreakWordBetweenPunctuation(aState, aForward, isPunctuation, + isWhitespace, aIsKeyboardSelect); + } else if (!aState->mLastCharWasWhitespace && !isWhitespace && + !isPunctuation && isWordBreakBefore) { + // if both the previous and the current character are not white + // space but this can be word break before, we don't need to eat + // a white space in this case. This case happens in some languages + // that their words are not separated by white spaces. E.g., + // Japanese and Chinese. + canBreak = true; + } else { + canBreak = isWordBreakBefore && aState->mSawBeforeType && + (aWordSelectEatSpace != isWhitespace); + } + if (canBreak) { + *aOffset = cIter.GetBeforeOffset() - mContentOffset; + return FOUND; + } + } + aState->Update(isPunctuation, isWhitespace); + } while (cIter.NextCluster()); + + *aOffset = cIter.GetAfterOffset() - mContentOffset; + return CONTINUE; +} + +bool nsTextFrame::HasVisibleText() { + // Text in the range is visible if there is at least one character in the + // range that is not skipped and is mapped by this frame (which is the primary + // frame) or one of its continuations. + for (nsTextFrame* f = this; f; f = f->GetNextContinuation()) { + int32_t dummyOffset = 0; + if (f->PeekOffsetNoAmount(true, &dummyOffset) == FOUND) { + return true; + } + } + return false; +} + +std::pair<int32_t, int32_t> nsTextFrame::GetOffsets() const { + return std::make_pair(GetContentOffset(), GetContentEnd()); +} + +static int32_t FindEndOfPunctuationRun(const nsTextFragment* aFrag, + const gfxTextRun* aTextRun, + gfxSkipCharsIterator* aIter, + int32_t aOffset, int32_t aStart, + int32_t aEnd) { + int32_t i; + + for (i = aStart; i < aEnd - aOffset; ++i) { + if (nsContentUtils::IsFirstLetterPunctuation( + aFrag->ScalarValueAt(AssertedCast<uint32_t>(aOffset + i)))) { + aIter->SetOriginalOffset(aOffset + i); + FindClusterEnd(aTextRun, aEnd, aIter); + i = aIter->GetOriginalOffset() - aOffset; + } else { + break; + } + } + return i; +} + +/** + * Returns true if this text frame completes the first-letter, false + * if it does not contain a true "letter". + * If returns true, then it also updates aLength to cover just the first-letter + * text. + * + * XXX :first-letter should be handled during frame construction + * (and it has a good bit in common with nextBidi) + * + * @param aLength an in/out parameter: on entry contains the maximum length to + * return, on exit returns length of the first-letter fragment (which may + * include leading and trailing punctuation, for example) + */ +static bool FindFirstLetterRange(const nsTextFragment* aFrag, + const nsAtom* aLang, + const gfxTextRun* aTextRun, int32_t aOffset, + const gfxSkipCharsIterator& aIter, + int32_t* aLength) { + int32_t i; + int32_t length = *aLength; + int32_t endOffset = aOffset + length; + gfxSkipCharsIterator iter(aIter); + + // Currently the only language-specific special case we handle here is the + // Dutch "IJ" digraph. + auto LangTagIsDutch = [](const nsAtom* aLang) -> bool { + if (!aLang) { + return false; + } + if (aLang == nsGkAtoms::nl) { + return true; + } + // We don't need to fully parse as a Locale; just check the initial subtag. + nsDependentAtomString langStr(aLang); + int32_t index = langStr.FindChar('-'); + if (index > 0) { + langStr.Truncate(index); + return langStr.EqualsLiteral("nl"); + } + return false; + }; + + // skip leading whitespace, then consume clusters that start with punctuation + i = FindEndOfPunctuationRun( + aFrag, aTextRun, &iter, aOffset, + GetTrimmableWhitespaceCount(aFrag, aOffset, length, 1), endOffset); + if (i == length) { + return false; + } + + // If the next character is not a letter, number or symbol, there is no + // first-letter. + // Return true so that we don't go on looking, but set aLength to 0. + const char32_t usv = + aFrag->ScalarValueAt(AssertedCast<uint32_t>(aOffset + i)); + if (!nsContentUtils::IsAlphanumericOrSymbol(usv)) { + *aLength = 0; + return true; + } + + // consume another cluster (the actual first letter) + + // For complex scripts such as Indic and SEAsian, where first-letter + // should extend to entire orthographic "syllable" clusters, we don't + // want to allow this to split a ligature. + bool allowSplitLigature; + bool usesIndicHalfForms = false; + + typedef intl::Script Script; + Script script = intl::UnicodeProperties::GetScriptCode(usv); + switch (script) { + default: + allowSplitLigature = true; + break; + + // Don't break regional-indicator ligatures. + case Script::COMMON: + allowSplitLigature = !gfxFontUtils::IsRegionalIndicator(usv); + break; + + // For now, lacking any definitive specification of when to apply this + // behavior, we'll base the decision on the HarfBuzz shaping engine + // used for each script: those that are handled by the Indic, Tibetan, + // Myanmar and SEAsian shapers will apply the "don't split ligatures" + // rule. + + // Indic + case Script::BENGALI: + case Script::DEVANAGARI: + case Script::GUJARATI: + usesIndicHalfForms = true; + [[fallthrough]]; + + case Script::GURMUKHI: + case Script::KANNADA: + case Script::MALAYALAM: + case Script::ORIYA: + case Script::TAMIL: + case Script::TELUGU: + case Script::SINHALA: + case Script::BALINESE: + case Script::LEPCHA: + case Script::REJANG: + case Script::SUNDANESE: + case Script::JAVANESE: + case Script::KAITHI: + case Script::MEETEI_MAYEK: + case Script::CHAKMA: + case Script::SHARADA: + case Script::TAKRI: + case Script::KHMER: + + // Tibetan + case Script::TIBETAN: + + // Myanmar + case Script::MYANMAR: + + // Other SEAsian + case Script::BUGINESE: + case Script::NEW_TAI_LUE: + case Script::CHAM: + case Script::TAI_THAM: + + // What about Thai/Lao - any special handling needed? + // Should we special-case Arabic lam-alef? + + allowSplitLigature = false; + break; + } + + iter.SetOriginalOffset(aOffset + i); + FindClusterEnd(aTextRun, endOffset, &iter, allowSplitLigature); + + i = iter.GetOriginalOffset() - aOffset; + + // Heuristic for Indic scripts that like to form conjuncts: + // If we ended at a virama that is ligated with the preceding character + // (e.g. creating a half-form), then don't stop here; include the next + // cluster as well so that we don't break a conjunct. + // + // Unfortunately this cannot distinguish between a letter+virama that ligate + // to create a half-form (in which case we have a conjunct that should not + // be broken) and a letter+virama that ligate purely for presentational + // reasons to position the (visible) virama component (in which case breaking + // after the virama would be acceptable). So results may be imperfect, + // depending how the font has chosen to implement visible viramas. + if (usesIndicHalfForms) { + while (i + 1 < length && + !aTextRun->IsLigatureGroupStart(iter.GetSkippedOffset())) { + char32_t c = aFrag->ScalarValueAt(AssertedCast<uint32_t>(aOffset + i)); + if (intl::UnicodeProperties::GetCombiningClass(c) == + HB_UNICODE_COMBINING_CLASS_VIRAMA) { + iter.AdvanceOriginal(1); + FindClusterEnd(aTextRun, endOffset, &iter, allowSplitLigature); + i = iter.GetOriginalOffset() - aOffset; + } else { + break; + } + } + } + + if (i + 1 == length) { + return true; + } + + // Check for Dutch "ij" digraph special case, but only if both letters have + // the same case. + if (script == Script::LATIN && LangTagIsDutch(aLang)) { + char16_t ch1 = aFrag->CharAt(AssertedCast<uint32_t>(aOffset + i)); + char16_t ch2 = aFrag->CharAt(AssertedCast<uint32_t>(aOffset + i + 1)); + if ((ch1 == 'i' && ch2 == 'j') || (ch1 == 'I' && ch2 == 'J')) { + iter.SetOriginalOffset(aOffset + i + 1); + FindClusterEnd(aTextRun, endOffset, &iter, allowSplitLigature); + i = iter.GetOriginalOffset() - aOffset; + if (i + 1 == length) { + return true; + } + } + } + + // consume clusters that start with punctuation + i = FindEndOfPunctuationRun(aFrag, aTextRun, &iter, aOffset, i + 1, + endOffset); + if (i < length) { + *aLength = i; + } + return true; +} + +static uint32_t FindStartAfterSkippingWhitespace( + nsTextFrame::PropertyProvider* aProvider, + nsIFrame::InlineIntrinsicISizeData* aData, const nsStyleText* aTextStyle, + gfxSkipCharsIterator* aIterator, uint32_t aFlowEndInTextRun) { + if (aData->mSkipWhitespace) { + while (aIterator->GetSkippedOffset() < aFlowEndInTextRun && + IsTrimmableSpace(aProvider->GetFragment(), + aIterator->GetOriginalOffset(), aTextStyle)) { + aIterator->AdvanceOriginal(1); + } + } + return aIterator->GetSkippedOffset(); +} + +float nsTextFrame::GetFontSizeInflation() const { + if (!HasFontSizeInflation()) { + return 1.0f; + } + return GetProperty(FontSizeInflationProperty()); +} + +void nsTextFrame::SetFontSizeInflation(float aInflation) { + if (aInflation == 1.0f) { + if (HasFontSizeInflation()) { + RemoveStateBits(TEXT_HAS_FONT_INFLATION); + RemoveProperty(FontSizeInflationProperty()); + } + return; + } + + AddStateBits(TEXT_HAS_FONT_INFLATION); + SetProperty(FontSizeInflationProperty(), aInflation); +} + +void nsTextFrame::SetHangableISize(nscoord aISize) { + MOZ_ASSERT(aISize >= 0, "unexpected negative hangable advance"); + if (aISize <= 0) { + ClearHangableISize(); + return; + } + SetProperty(HangableWhitespaceProperty(), aISize); + mPropertyFlags |= PropertyFlags::HangableWS; +} + +nscoord nsTextFrame::GetHangableISize() const { + MOZ_ASSERT(!!(mPropertyFlags & PropertyFlags::HangableWS) == + HasProperty(HangableWhitespaceProperty()), + "flag/property mismatch!"); + return (mPropertyFlags & PropertyFlags::HangableWS) + ? GetProperty(HangableWhitespaceProperty()) + : 0; +} + +void nsTextFrame::ClearHangableISize() { + if (mPropertyFlags & PropertyFlags::HangableWS) { + RemoveProperty(HangableWhitespaceProperty()); + mPropertyFlags &= ~PropertyFlags::HangableWS; + } +} + +void nsTextFrame::SetTrimmableWS(gfxTextRun::TrimmableWS aTrimmableWS) { + MOZ_ASSERT(aTrimmableWS.mAdvance >= 0, "negative trimmable size"); + if (aTrimmableWS.mAdvance <= 0) { + ClearTrimmableWS(); + return; + } + SetProperty(TrimmableWhitespaceProperty(), aTrimmableWS); + mPropertyFlags |= PropertyFlags::TrimmableWS; +} + +gfxTextRun::TrimmableWS nsTextFrame::GetTrimmableWS() const { + MOZ_ASSERT(!!(mPropertyFlags & PropertyFlags::TrimmableWS) == + HasProperty(TrimmableWhitespaceProperty()), + "flag/property mismatch!"); + return (mPropertyFlags & PropertyFlags::TrimmableWS) + ? GetProperty(TrimmableWhitespaceProperty()) + : gfxTextRun::TrimmableWS{}; +} + +void nsTextFrame::ClearTrimmableWS() { + if (mPropertyFlags & PropertyFlags::TrimmableWS) { + RemoveProperty(TrimmableWhitespaceProperty()); + mPropertyFlags &= ~PropertyFlags::TrimmableWS; + } +} + +/* virtual */ +void nsTextFrame::MarkIntrinsicISizesDirty() { + ClearTextRuns(); + nsIFrame::MarkIntrinsicISizesDirty(); +} + +// XXX this doesn't handle characters shaped by line endings. We need to +// temporarily override the "current line ending" settings. +void nsTextFrame::AddInlineMinISizeForFlow(gfxContext* aRenderingContext, + nsIFrame::InlineMinISizeData* aData, + TextRunType aTextRunType) { + uint32_t flowEndInTextRun; + gfxSkipCharsIterator iter = + EnsureTextRun(aTextRunType, aRenderingContext->GetDrawTarget(), + aData->LineContainer(), aData->mLine, &flowEndInTextRun); + gfxTextRun* textRun = GetTextRun(aTextRunType); + if (!textRun) { + return; + } + + // Pass null for the line container. This will disable tab spacing, but that's + // OK since we can't really handle tabs for intrinsic sizing anyway. + const nsStyleText* textStyle = StyleText(); + const nsTextFragment* frag = TextFragment(); + + // If we're hyphenating, the PropertyProvider needs the actual length; + // otherwise we can just pass INT32_MAX to mean "all the text" + int32_t len = INT32_MAX; + bool hyphenating = frag->GetLength() > 0 && + (textStyle->mHyphens == StyleHyphens::Auto || + (textStyle->mHyphens == StyleHyphens::Manual && + !!(textRun->GetFlags() & + gfx::ShapedTextFlags::TEXT_ENABLE_HYPHEN_BREAKS))); + if (hyphenating) { + gfxSkipCharsIterator tmp(iter); + len = std::min<int32_t>(GetContentOffset() + GetInFlowContentLength(), + tmp.ConvertSkippedToOriginal(flowEndInTextRun)) - + iter.GetOriginalOffset(); + } + PropertyProvider provider(textRun, textStyle, frag, this, iter, len, nullptr, + 0, aTextRunType); + + bool collapseWhitespace = !textStyle->WhiteSpaceIsSignificant(); + bool preformatNewlines = textStyle->NewlineIsSignificant(this); + bool preformatTabs = textStyle->WhiteSpaceIsSignificant(); + bool whitespaceCanHang = textStyle->WhiteSpaceCanHangOrVisuallyCollapse(); + gfxFloat tabWidth = -1; + uint32_t start = FindStartAfterSkippingWhitespace(&provider, aData, textStyle, + &iter, flowEndInTextRun); + + // text-combine-upright frame is constantly 1em on inline-axis. + if (Style()->IsTextCombined()) { + if (start < flowEndInTextRun && textRun->CanBreakLineBefore(start)) { + aData->OptionallyBreak(); + } + aData->mCurrentLine += provider.GetFontMetrics()->EmHeight(); + aData->mTrailingWhitespace = 0; + return; + } + + if (textStyle->EffectiveOverflowWrap() == StyleOverflowWrap::Anywhere && + textStyle->WordCanWrap(this)) { + aData->OptionallyBreak(); + aData->mCurrentLine += + textRun->GetMinAdvanceWidth(Range(start, flowEndInTextRun)); + aData->mTrailingWhitespace = 0; + aData->mAtStartOfLine = false; + aData->OptionallyBreak(); + return; + } + + AutoTArray<gfxTextRun::HyphenType, BIG_TEXT_NODE_SIZE> hyphBuffer; + if (hyphenating) { + if (hyphBuffer.AppendElements(flowEndInTextRun - start, fallible)) { + provider.GetHyphenationBreaks(Range(start, flowEndInTextRun), + hyphBuffer.Elements()); + } else { + hyphenating = false; + } + } + + for (uint32_t i = start, wordStart = start; i <= flowEndInTextRun; ++i) { + bool preformattedNewline = false; + bool preformattedTab = false; + if (i < flowEndInTextRun) { + // XXXldb Shouldn't we be including the newline as part of the + // segment that it ends rather than part of the segment that it + // starts? + preformattedNewline = preformatNewlines && textRun->CharIsNewline(i); + preformattedTab = preformatTabs && textRun->CharIsTab(i); + if (!textRun->CanBreakLineBefore(i) && !preformattedNewline && + !preformattedTab && + (!hyphenating || + !gfxTextRun::IsOptionalHyphenBreak(hyphBuffer[i - start]))) { + // we can't break here (and it's not the end of the flow) + continue; + } + } + + if (i > wordStart) { + nscoord width = NSToCoordCeilClamped( + textRun->GetAdvanceWidth(Range(wordStart, i), &provider)); + width = std::max(0, width); + aData->mCurrentLine = NSCoordSaturatingAdd(aData->mCurrentLine, width); + aData->mAtStartOfLine = false; + + if (collapseWhitespace || whitespaceCanHang) { + uint32_t trimStart = GetEndOfTrimmedText(frag, textStyle, wordStart, i, + &iter, whitespaceCanHang); + if (trimStart == start) { + // This is *all* trimmable whitespace, so whatever trailingWhitespace + // we saw previously is still trailing... + aData->mTrailingWhitespace += width; + } else { + // Some non-whitespace so the old trailingWhitespace is no longer + // trailing + nscoord wsWidth = NSToCoordCeilClamped( + textRun->GetAdvanceWidth(Range(trimStart, i), &provider)); + aData->mTrailingWhitespace = std::max(0, wsWidth); + } + } else { + aData->mTrailingWhitespace = 0; + } + } + + if (preformattedTab) { + PropertyProvider::Spacing spacing; + provider.GetSpacing(Range(i, i + 1), &spacing); + aData->mCurrentLine += nscoord(spacing.mBefore); + if (tabWidth < 0) { + tabWidth = ComputeTabWidthAppUnits(this); + } + gfxFloat afterTab = AdvanceToNextTab(aData->mCurrentLine, tabWidth, + provider.MinTabAdvance()); + aData->mCurrentLine = nscoord(afterTab + spacing.mAfter); + wordStart = i + 1; + } else if (i < flowEndInTextRun || + (i == textRun->GetLength() && + (textRun->GetFlags2() & + nsTextFrameUtils::Flags::HasTrailingBreak))) { + if (preformattedNewline) { + aData->ForceBreak(); + } else if (i < flowEndInTextRun && hyphenating && + gfxTextRun::IsOptionalHyphenBreak(hyphBuffer[i - start])) { + aData->OptionallyBreak(NSToCoordRound(provider.GetHyphenWidth())); + } else { + aData->OptionallyBreak(); + } + if (aData->mSkipWhitespace) { + iter.SetSkippedOffset(i); + wordStart = FindStartAfterSkippingWhitespace( + &provider, aData, textStyle, &iter, flowEndInTextRun); + } else { + wordStart = i; + } + } + } + + if (start < flowEndInTextRun) { + // Check if we have collapsible whitespace at the end + aData->mSkipWhitespace = IsTrimmableSpace( + provider.GetFragment(), + iter.ConvertSkippedToOriginal(flowEndInTextRun - 1), textStyle); + } +} + +bool nsTextFrame::IsCurrentFontInflation(float aInflation) const { + return fabsf(aInflation - GetFontSizeInflation()) < 1e-6; +} + +// XXX Need to do something here to avoid incremental reflow bugs due to +// first-line and first-letter changing min-width +/* virtual */ +void nsTextFrame::AddInlineMinISize(gfxContext* aRenderingContext, + nsIFrame::InlineMinISizeData* aData) { + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + TextRunType trtype = (inflation == 1.0f) ? eNotInflated : eInflated; + + if (trtype == eInflated && !IsCurrentFontInflation(inflation)) { + // FIXME: Ideally, if we already have a text run, we'd move it to be + // the uninflated text run. + ClearTextRun(nullptr, nsTextFrame::eInflated); + mFontMetrics = nullptr; + } + + nsTextFrame* f; + const gfxTextRun* lastTextRun = nullptr; + // nsContinuingTextFrame does nothing for AddInlineMinISize; all text frames + // in the flow are handled right here. + for (f = this; f; f = f->GetNextContinuation()) { + // f->GetTextRun(nsTextFrame::eNotInflated) could be null if we + // haven't set up textruns yet for f. Except in OOM situations, + // lastTextRun will only be null for the first text frame. + if (f == this || f->GetTextRun(trtype) != lastTextRun) { + nsIFrame* lc; + if (aData->LineContainer() && + aData->LineContainer() != (lc = FindLineContainer(f))) { + NS_ASSERTION(f != this, + "wrong InlineMinISizeData container" + " for first continuation"); + aData->mLine = nullptr; + aData->SetLineContainer(lc); + } + + // This will process all the text frames that share the same textrun as f. + f->AddInlineMinISizeForFlow(aRenderingContext, aData, trtype); + lastTextRun = f->GetTextRun(trtype); + } + } +} + +// XXX this doesn't handle characters shaped by line endings. We need to +// temporarily override the "current line ending" settings. +void nsTextFrame::AddInlinePrefISizeForFlow( + gfxContext* aRenderingContext, nsIFrame::InlinePrefISizeData* aData, + TextRunType aTextRunType) { + uint32_t flowEndInTextRun; + gfxSkipCharsIterator iter = + EnsureTextRun(aTextRunType, aRenderingContext->GetDrawTarget(), + aData->LineContainer(), aData->mLine, &flowEndInTextRun); + gfxTextRun* textRun = GetTextRun(aTextRunType); + if (!textRun) { + return; + } + + // Pass null for the line container. This will disable tab spacing, but that's + // OK since we can't really handle tabs for intrinsic sizing anyway. + + const nsStyleText* textStyle = StyleText(); + const nsTextFragment* frag = TextFragment(); + PropertyProvider provider(textRun, textStyle, frag, this, iter, INT32_MAX, + nullptr, 0, aTextRunType); + + // text-combine-upright frame is constantly 1em on inline-axis. + if (Style()->IsTextCombined()) { + aData->mCurrentLine += provider.GetFontMetrics()->EmHeight(); + aData->mTrailingWhitespace = 0; + aData->mLineIsEmpty = false; + return; + } + + bool collapseWhitespace = !textStyle->WhiteSpaceIsSignificant(); + bool preformatNewlines = textStyle->NewlineIsSignificant(this); + bool preformatTabs = textStyle->TabIsSignificant(); + gfxFloat tabWidth = -1; + uint32_t start = FindStartAfterSkippingWhitespace(&provider, aData, textStyle, + &iter, flowEndInTextRun); + + // XXX Should we consider hyphenation here? + // If newlines and tabs aren't preformatted, nothing to do inside + // the loop so make i skip to the end + uint32_t loopStart = + (preformatNewlines || preformatTabs) ? start : flowEndInTextRun; + for (uint32_t i = loopStart, lineStart = start; i <= flowEndInTextRun; ++i) { + bool preformattedNewline = false; + bool preformattedTab = false; + if (i < flowEndInTextRun) { + // XXXldb Shouldn't we be including the newline as part of the + // segment that it ends rather than part of the segment that it + // starts? + NS_ASSERTION(preformatNewlines || preformatTabs, + "We can't be here unless newlines are " + "hard breaks or there are tabs"); + preformattedNewline = preformatNewlines && textRun->CharIsNewline(i); + preformattedTab = preformatTabs && textRun->CharIsTab(i); + if (!preformattedNewline && !preformattedTab) { + // we needn't break here (and it's not the end of the flow) + continue; + } + } + + if (i > lineStart) { + nscoord width = NSToCoordCeilClamped( + textRun->GetAdvanceWidth(Range(lineStart, i), &provider)); + width = std::max(0, width); + aData->mCurrentLine = NSCoordSaturatingAdd(aData->mCurrentLine, width); + aData->mLineIsEmpty = false; + + if (collapseWhitespace) { + uint32_t trimStart = + GetEndOfTrimmedText(frag, textStyle, lineStart, i, &iter); + if (trimStart == start) { + // This is *all* trimmable whitespace, so whatever trailingWhitespace + // we saw previously is still trailing... + aData->mTrailingWhitespace += width; + } else { + // Some non-whitespace so the old trailingWhitespace is no longer + // trailing + nscoord wsWidth = NSToCoordCeilClamped( + textRun->GetAdvanceWidth(Range(trimStart, i), &provider)); + aData->mTrailingWhitespace = std::max(0, wsWidth); + } + } else { + aData->mTrailingWhitespace = 0; + } + } + + if (preformattedTab) { + PropertyProvider::Spacing spacing; + provider.GetSpacing(Range(i, i + 1), &spacing); + aData->mCurrentLine += nscoord(spacing.mBefore); + if (tabWidth < 0) { + tabWidth = ComputeTabWidthAppUnits(this); + } + gfxFloat afterTab = AdvanceToNextTab(aData->mCurrentLine, tabWidth, + provider.MinTabAdvance()); + aData->mCurrentLine = nscoord(afterTab + spacing.mAfter); + aData->mLineIsEmpty = false; + lineStart = i + 1; + } else if (preformattedNewline) { + aData->ForceBreak(); + lineStart = i; + } + } + + // Check if we have collapsible whitespace at the end + if (start < flowEndInTextRun) { + aData->mSkipWhitespace = IsTrimmableSpace( + provider.GetFragment(), + iter.ConvertSkippedToOriginal(flowEndInTextRun - 1), textStyle); + } +} + +// XXX Need to do something here to avoid incremental reflow bugs due to +// first-line and first-letter changing pref-width +/* virtual */ +void nsTextFrame::AddInlinePrefISize(gfxContext* aRenderingContext, + nsIFrame::InlinePrefISizeData* aData) { + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + TextRunType trtype = (inflation == 1.0f) ? eNotInflated : eInflated; + + if (trtype == eInflated && !IsCurrentFontInflation(inflation)) { + // FIXME: Ideally, if we already have a text run, we'd move it to be + // the uninflated text run. + ClearTextRun(nullptr, nsTextFrame::eInflated); + mFontMetrics = nullptr; + } + + nsTextFrame* f; + const gfxTextRun* lastTextRun = nullptr; + // nsContinuingTextFrame does nothing for AddInlineMinISize; all text frames + // in the flow are handled right here. + for (f = this; f; f = f->GetNextContinuation()) { + // f->GetTextRun(nsTextFrame::eNotInflated) could be null if we + // haven't set up textruns yet for f. Except in OOM situations, + // lastTextRun will only be null for the first text frame. + if (f == this || f->GetTextRun(trtype) != lastTextRun) { + nsIFrame* lc; + if (aData->LineContainer() && + aData->LineContainer() != (lc = FindLineContainer(f))) { + NS_ASSERTION(f != this, + "wrong InlinePrefISizeData container" + " for first continuation"); + aData->mLine = nullptr; + aData->SetLineContainer(lc); + } + + // This will process all the text frames that share the same textrun as f. + f->AddInlinePrefISizeForFlow(aRenderingContext, aData, trtype); + lastTextRun = f->GetTextRun(trtype); + } + } +} + +/* virtual */ +nsIFrame::SizeComputationResult nsTextFrame::ComputeSize( + gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize, + nscoord aAvailableISize, const LogicalSize& aMargin, + const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides, + ComputeSizeFlags aFlags) { + // Inlines and text don't compute size before reflow. + return {LogicalSize(aWM, NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE), + AspectRatioUsage::None}; +} + +static nsRect RoundOut(const gfxRect& aRect) { + nsRect r; + r.x = NSToCoordFloor(aRect.X()); + r.y = NSToCoordFloor(aRect.Y()); + r.width = NSToCoordCeil(aRect.XMost()) - r.x; + r.height = NSToCoordCeil(aRect.YMost()) - r.y; + return r; +} + +nsRect nsTextFrame::ComputeTightBounds(DrawTarget* aDrawTarget) const { + if (Style()->HasTextDecorationLines() || HasAnyStateBits(TEXT_HYPHEN_BREAK)) { + // This is conservative, but OK. + return InkOverflowRect(); + } + + gfxSkipCharsIterator iter = + const_cast<nsTextFrame*>(this)->EnsureTextRun(nsTextFrame::eInflated); + if (!mTextRun) { + return nsRect(); + } + + PropertyProvider provider(const_cast<nsTextFrame*>(this), iter, + nsTextFrame::eInflated, mFontMetrics); + // Trim trailing whitespace + provider.InitializeForDisplay(true); + + gfxTextRun::Metrics metrics = mTextRun->MeasureText( + ComputeTransformedRange(provider), gfxFont::TIGHT_HINTED_OUTLINE_EXTENTS, + aDrawTarget, &provider); + if (GetWritingMode().IsLineInverted()) { + metrics.mBoundingBox.y = -metrics.mBoundingBox.YMost(); + } + // mAscent should be the same as metrics.mAscent, but it's what we use to + // paint so that's the one we'll use. + nsRect boundingBox = RoundOut(metrics.mBoundingBox); + boundingBox += nsPoint(0, mAscent); + if (mTextRun->IsVertical()) { + // Swap line-relative textMetrics dimensions to physical coordinates. + std::swap(boundingBox.x, boundingBox.y); + std::swap(boundingBox.width, boundingBox.height); + } + return boundingBox; +} + +/* virtual */ +nsresult nsTextFrame::GetPrefWidthTightBounds(gfxContext* aContext, nscoord* aX, + nscoord* aXMost) { + gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); + if (!mTextRun) { + return NS_ERROR_FAILURE; + } + + PropertyProvider provider(this, iter, nsTextFrame::eInflated, mFontMetrics); + provider.InitializeForMeasure(); + + gfxTextRun::Metrics metrics = mTextRun->MeasureText( + ComputeTransformedRange(provider), gfxFont::TIGHT_HINTED_OUTLINE_EXTENTS, + aContext->GetDrawTarget(), &provider); + // Round it like nsTextFrame::ComputeTightBounds() to ensure consistency. + *aX = NSToCoordFloor(metrics.mBoundingBox.x); + *aXMost = NSToCoordCeil(metrics.mBoundingBox.XMost()); + + return NS_OK; +} + +static bool HasSoftHyphenBefore(const nsTextFragment* aFrag, + const gfxTextRun* aTextRun, + int32_t aStartOffset, + const gfxSkipCharsIterator& aIter) { + if (aIter.GetSkippedOffset() < aTextRun->GetLength() && + aTextRun->CanHyphenateBefore(aIter.GetSkippedOffset())) { + return true; + } + if (!(aTextRun->GetFlags2() & nsTextFrameUtils::Flags::HasShy)) { + return false; + } + gfxSkipCharsIterator iter = aIter; + while (iter.GetOriginalOffset() > aStartOffset) { + iter.AdvanceOriginal(-1); + if (!iter.IsOriginalCharSkipped()) { + break; + } + if (aFrag->CharAt(AssertedCast<uint32_t>(iter.GetOriginalOffset())) == + CH_SHY) { + return true; + } + } + return false; +} + +/** + * Removes all frames from aFrame up to (but not including) aFirstToNotRemove, + * because their text has all been taken and reflowed by earlier frames. + */ +static void RemoveEmptyInFlows(nsTextFrame* aFrame, + nsTextFrame* aFirstToNotRemove) { + MOZ_ASSERT(aFrame != aFirstToNotRemove, "This will go very badly"); + // We have to be careful here, because some RemoveFrame implementations + // remove and destroy not only the passed-in frame but also all its following + // in-flows (and sometimes all its following continuations in general). So + // we remove |f| and everything up to but not including firstToNotRemove from + // the flow first, to make sure that only the things we want destroyed are + // destroyed. + + // This sadly duplicates some of the logic from + // nsSplittableFrame::RemoveFromFlow. We can get away with not duplicating + // all of it, because we know that the prev-continuation links of + // firstToNotRemove and f are fluid, and non-null. + NS_ASSERTION(aFirstToNotRemove->GetPrevContinuation() == + aFirstToNotRemove->GetPrevInFlow() && + aFirstToNotRemove->GetPrevInFlow() != nullptr, + "aFirstToNotRemove should have a fluid prev continuation"); + NS_ASSERTION(aFrame->GetPrevContinuation() == aFrame->GetPrevInFlow() && + aFrame->GetPrevInFlow() != nullptr, + "aFrame should have a fluid prev continuation"); + + nsTextFrame* prevContinuation = aFrame->GetPrevContinuation(); + nsTextFrame* lastRemoved = aFirstToNotRemove->GetPrevContinuation(); + + for (nsTextFrame* f = aFrame; f != aFirstToNotRemove; + f = f->GetNextContinuation()) { + // f is going to be destroyed soon, after it is unlinked from the + // continuation chain. If its textrun is going to be destroyed we need to + // do it now, before we unlink the frames to remove from the flow, + // because Destroy calls ClearTextRuns() and that will start at the + // first frame with the text run and walk the continuations. + if (f->IsInTextRunUserData()) { + f->ClearTextRuns(); + } else { + f->DisconnectTextRuns(); + } + } + + prevContinuation->SetNextInFlow(aFirstToNotRemove); + aFirstToNotRemove->SetPrevInFlow(prevContinuation); + + // **Note: it is important here that we clear the Next link from lastRemoved + // BEFORE clearing the Prev link from aFrame, because SetPrevInFlow() will + // follow the Next pointers, wiping out the cached mFirstContinuation field + // from each following frame in the list. We need this to stop when it + // reaches lastRemoved! + lastRemoved->SetNextInFlow(nullptr); + aFrame->SetPrevInFlow(nullptr); + + nsContainerFrame* parent = aFrame->GetParent(); + nsIFrame::DestroyContext context(aFrame->PresShell()); + nsBlockFrame* parentBlock = do_QueryFrame(parent); + if (parentBlock) { + // Manually call DoRemoveFrame so we can tell it that we're + // removing empty frames; this will keep it from blowing away + // text runs. + parentBlock->DoRemoveFrame(context, aFrame, nsBlockFrame::FRAMES_ARE_EMPTY); + } else { + // Just remove it normally; use FrameChildListID::NoReflowPrincipal to avoid + // posting new reflows. + parent->RemoveFrame(context, FrameChildListID::NoReflowPrincipal, aFrame); + } +} + +void nsTextFrame::SetLength(int32_t aLength, nsLineLayout* aLineLayout, + uint32_t aSetLengthFlags) { + mContentLengthHint = aLength; + int32_t end = GetContentOffset() + aLength; + nsTextFrame* f = GetNextInFlow(); + if (!f) { + return; + } + + // If our end offset is moving, then even if frames are not being pushed or + // pulled, content is moving to or from the next line and the next line + // must be reflowed. + // If the next-continuation is dirty, then we should dirty the next line now + // because we may have skipped doing it if we dirtied it in + // CharacterDataChanged. This is ugly but teaching FrameNeedsReflow + // and ChildIsDirty to handle a range of frames would be worse. + if (aLineLayout && + (end != f->mContentOffset || f->HasAnyStateBits(NS_FRAME_IS_DIRTY))) { + aLineLayout->SetDirtyNextLine(); + } + + if (end < f->mContentOffset) { + // Our frame is shrinking. Give the text to our next in flow. + if (aLineLayout && HasSignificantTerminalNewline() && + !GetParent()->IsLetterFrame() && + (aSetLengthFlags & ALLOW_FRAME_CREATION_AND_DESTRUCTION)) { + // Whatever text we hand to our next-in-flow will end up in a frame all of + // its own, since it ends in a forced linebreak. Might as well just put + // it in a separate frame now. This is important to prevent text run + // churn; if we did not do that, then we'd likely end up rebuilding + // textruns for all our following continuations. + // We skip this optimization when the parent is a first-letter frame + // because it doesn't deal well with more than one child frame. + // We also skip this optimization if we were called during bidi + // resolution, so as not to create a new frame which doesn't appear in + // the bidi resolver's list of frames + nsIFrame* newFrame = + PresShell()->FrameConstructor()->CreateContinuingFrame(this, + GetParent()); + nsTextFrame* next = static_cast<nsTextFrame*>(newFrame); + GetParent()->InsertFrames(FrameChildListID::NoReflowPrincipal, this, + aLineLayout->GetLine(), + nsFrameList(next, next)); + f = next; + } + + f->mContentOffset = end; + if (f->GetTextRun(nsTextFrame::eInflated) != mTextRun) { + ClearTextRuns(); + f->ClearTextRuns(); + } + return; + } + // Our frame is growing. Take text from our in-flow(s). + // We can take text from frames in lines beyond just the next line. + // We don't dirty those lines. That's OK, because when we reflow + // our empty next-in-flow, it will take text from its next-in-flow and + // dirty that line. + + // Note that in the process we may end up removing some frames from + // the flow if they end up empty. + nsTextFrame* framesToRemove = nullptr; + while (f && f->mContentOffset < end) { + f->mContentOffset = end; + if (f->GetTextRun(nsTextFrame::eInflated) != mTextRun) { + ClearTextRuns(); + f->ClearTextRuns(); + } + nsTextFrame* next = f->GetNextInFlow(); + // Note: the "f->GetNextSibling() == next" check below is to restrict + // this optimization to the case where they are on the same child list. + // Otherwise we might remove the only child of a nsFirstLetterFrame + // for example and it can't handle that. See bug 597627 for details. + if (next && next->mContentOffset <= end && f->GetNextSibling() == next && + (aSetLengthFlags & ALLOW_FRAME_CREATION_AND_DESTRUCTION)) { + // |f| is now empty. We may as well remove it, instead of copying all + // the text from |next| into it instead; the latter leads to use + // rebuilding textruns for all following continuations. + // We skip this optimization if we were called during bidi resolution, + // since the bidi resolver may try to handle the destroyed frame later + // and crash + if (!framesToRemove) { + // Remember that we have to remove this frame. + framesToRemove = f; + } + } else if (framesToRemove) { + RemoveEmptyInFlows(framesToRemove, f); + framesToRemove = nullptr; + } + f = next; + } + + MOZ_ASSERT(!framesToRemove || (f && f->mContentOffset == end), + "How did we exit the loop if we null out framesToRemove if " + "!next || next->mContentOffset > end ?"); + + if (framesToRemove) { + // We are guaranteed that we exited the loop with f not null, per the + // postcondition above + RemoveEmptyInFlows(framesToRemove, f); + } + +#ifdef DEBUG + f = this; + int32_t iterations = 0; + while (f && iterations < 10) { + f->GetContentLength(); // Assert if negative length + f = f->GetNextContinuation(); + ++iterations; + } + f = this; + iterations = 0; + while (f && iterations < 10) { + f->GetContentLength(); // Assert if negative length + f = f->GetPrevContinuation(); + ++iterations; + } +#endif +} + +bool nsTextFrame::IsFloatingFirstLetterChild() const { + nsIFrame* frame = GetParent(); + return frame && frame->IsFloating() && frame->IsLetterFrame(); +} + +bool nsTextFrame::IsInitialLetterChild() const { + nsIFrame* frame = GetParent(); + return frame && frame->StyleTextReset()->mInitialLetterSize != 0.0f && + frame->IsLetterFrame(); +} + +struct NewlineProperty { + int32_t mStartOffset; + // The offset of the first \n after mStartOffset, or -1 if there is none + int32_t mNewlineOffset; +}; + +void nsTextFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsTextFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aMetrics, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + InvalidateSelectionState(); + + // XXX If there's no line layout, we shouldn't even have created this + // frame. This may happen if, for example, this is text inside a table + // but not inside a cell. For now, just don't reflow. + if (!aReflowInput.mLineLayout) { + ClearMetrics(aMetrics); + return; + } + + ReflowText(*aReflowInput.mLineLayout, aReflowInput.AvailableWidth(), + aReflowInput.mRenderingContext->GetDrawTarget(), aMetrics, + aStatus); +} + +#ifdef ACCESSIBILITY +/** + * Notifies accessibility about text reflow. Used by nsTextFrame::ReflowText. + */ +class MOZ_STACK_CLASS ReflowTextA11yNotifier { + public: + ReflowTextA11yNotifier(nsPresContext* aPresContext, nsIContent* aContent) + : mContent(aContent), mPresContext(aPresContext) {} + ~ReflowTextA11yNotifier() { + if (nsAccessibilityService* accService = GetAccService()) { + accService->UpdateText(mPresContext->PresShell(), mContent); + } + } + + private: + ReflowTextA11yNotifier(); + ReflowTextA11yNotifier(const ReflowTextA11yNotifier&); + ReflowTextA11yNotifier& operator=(const ReflowTextA11yNotifier&); + + nsIContent* mContent; + nsPresContext* mPresContext; +}; +#endif + +void nsTextFrame::ReflowText(nsLineLayout& aLineLayout, nscoord aAvailableWidth, + DrawTarget* aDrawTarget, ReflowOutput& aMetrics, + nsReflowStatus& aStatus) { + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + +#ifdef NOISY_REFLOW + ListTag(stdout); + printf(": BeginReflow: availableWidth=%d\n", aAvailableWidth); +#endif + + nsPresContext* presContext = PresContext(); + +#ifdef ACCESSIBILITY + // Schedule the update of accessible tree since rendered text might be + // changed. + if (StyleVisibility()->IsVisible()) { + ReflowTextA11yNotifier(presContext, mContent); + } +#endif + + ///////////////////////////////////////////////////////////////////// + // Set up flags and clear out state + ///////////////////////////////////////////////////////////////////// + + // Clear out the reflow input flags in mState. We also clear the whitespace + // flags because this can change whether the frame maps whitespace-only text + // or not. We also clear the flag that tracks whether we had a pending + // reflow request from CharacterDataChanged (since we're reflowing now). + RemoveStateBits(TEXT_REFLOW_FLAGS | TEXT_WHITESPACE_FLAGS); + mReflowRequestedForCharDataChange = false; + RemoveProperty(WebRenderTextBounds()); + + // Discard cached continuations array that will be invalidated by the reflow. + if (nsTextFrame* first = FirstContinuation()) { + first->ClearCachedContinuations(); + } + + // Temporarily map all possible content while we construct our new textrun. + // so that when doing reflow our styles prevail over any part of the + // textrun we look at. Note that next-in-flows may be mapping the same + // content; gfxTextRun construction logic will ensure that we take priority. + int32_t maxContentLength = GetInFlowContentLength(); + + InvalidateSelectionState(); + + // We don't need to reflow if there is no content. + if (!maxContentLength) { + ClearMetrics(aMetrics); + return; + } + +#ifdef NOISY_BIDI + printf("Reflowed textframe\n"); +#endif + + const nsStyleText* textStyle = StyleText(); + + bool atStartOfLine = aLineLayout.LineAtStart(); + if (atStartOfLine) { + AddStateBits(TEXT_START_OF_LINE); + } + + uint32_t flowEndInTextRun; + nsIFrame* lineContainer = aLineLayout.LineContainerFrame(); + const nsTextFragment* frag = TextFragment(); + + // DOM offsets of the text range we need to measure, after trimming + // whitespace, restricting to first-letter, and restricting preformatted text + // to nearest newline + int32_t length = maxContentLength; + int32_t offset = GetContentOffset(); + + // Restrict preformatted text to the nearest newline + int32_t newLineOffset = -1; // this will be -1 or a content offset + int32_t contentNewLineOffset = -1; + // Pointer to the nsGkAtoms::newline set on this frame's element + NewlineProperty* cachedNewlineOffset = nullptr; + if (textStyle->NewlineIsSignificant(this)) { + cachedNewlineOffset = mContent->HasFlag(NS_HAS_NEWLINE_PROPERTY) + ? static_cast<NewlineProperty*>( + mContent->GetProperty(nsGkAtoms::newline)) + : nullptr; + if (cachedNewlineOffset && cachedNewlineOffset->mStartOffset <= offset && + (cachedNewlineOffset->mNewlineOffset == -1 || + cachedNewlineOffset->mNewlineOffset >= offset)) { + contentNewLineOffset = cachedNewlineOffset->mNewlineOffset; + } else { + contentNewLineOffset = + FindChar(frag, offset, GetContent()->TextLength() - offset, '\n'); + } + if (contentNewLineOffset < offset + length) { + /* + The new line offset could be outside this frame if the frame has been + split by bidi resolution. In that case we won't use it in this reflow + (newLineOffset will remain -1), but we will still cache it in mContent + */ + newLineOffset = contentNewLineOffset; + } + if (newLineOffset >= 0) { + length = newLineOffset + 1 - offset; + } + } + if ((atStartOfLine && !textStyle->WhiteSpaceIsSignificant()) || + HasAnyStateBits(TEXT_IS_IN_TOKEN_MATHML)) { + // Skip leading whitespace. Make sure we don't skip a 'pre-line' + // newline if there is one. + int32_t skipLength = newLineOffset >= 0 ? length - 1 : length; + int32_t whitespaceCount = + GetTrimmableWhitespaceCount(frag, offset, skipLength, 1); + if (whitespaceCount) { + offset += whitespaceCount; + length -= whitespaceCount; + // Make sure this frame maps the trimmable whitespace. + if (MOZ_UNLIKELY(offset > GetContentEnd())) { + SetLength(offset - GetContentOffset(), &aLineLayout, + ALLOW_FRAME_CREATION_AND_DESTRUCTION); + } + } + } + + // If trimming whitespace left us with nothing to do, return early. + if (length == 0) { + ClearMetrics(aMetrics); + return; + } + + bool completedFirstLetter = false; + // Layout dependent styles are a problem because we need to reconstruct + // the gfxTextRun based on our layout. + if (aLineLayout.GetInFirstLetter() || aLineLayout.GetInFirstLine()) { + SetLength(maxContentLength, &aLineLayout, + ALLOW_FRAME_CREATION_AND_DESTRUCTION); + + if (aLineLayout.GetInFirstLetter()) { + // floating first-letter boundaries are significant in textrun + // construction, so clear the textrun out every time we hit a first-letter + // and have changed our length (which controls the first-letter boundary) + ClearTextRuns(); + // Find the length of the first-letter. We need a textrun for this. + // REVIEW: maybe-bogus inflation should be ok (fixed below) + gfxSkipCharsIterator iter = + EnsureTextRun(nsTextFrame::eInflated, aDrawTarget, lineContainer, + aLineLayout.GetLine(), &flowEndInTextRun); + + if (mTextRun) { + int32_t firstLetterLength = length; + if (aLineLayout.GetFirstLetterStyleOK()) { + // We only pass a language code to FindFirstLetterRange if it was + // explicit in the content. + const nsStyleFont* styleFont = StyleFont(); + const nsAtom* lang = styleFont->mExplicitLanguage + ? styleFont->mLanguage.get() + : nullptr; + completedFirstLetter = FindFirstLetterRange( + frag, lang, mTextRun, offset, iter, &firstLetterLength); + if (newLineOffset >= 0) { + // Don't allow a preformatted newline to be part of a first-letter. + firstLetterLength = std::min(firstLetterLength, length - 1); + if (length == 1) { + // There is no text to be consumed by the first-letter before the + // preformatted newline. Note that the first letter is therefore + // complete (FindFirstLetterRange will have returned false). + completedFirstLetter = true; + } + } + } else { + // We're in a first-letter frame's first in flow, so if there + // was a first-letter, we'd be it. However, for one reason + // or another (e.g., preformatted line break before this text), + // we're not actually supposed to have first-letter style. So + // just make a zero-length first-letter. + firstLetterLength = 0; + completedFirstLetter = true; + } + length = firstLetterLength; + if (length) { + AddStateBits(TEXT_FIRST_LETTER); + } + // Change this frame's length to the first-letter length right now + // so that when we rebuild the textrun it will be built with the + // right first-letter boundary + SetLength(offset + length - GetContentOffset(), &aLineLayout, + ALLOW_FRAME_CREATION_AND_DESTRUCTION); + // Ensure that the textrun will be rebuilt + ClearTextRuns(); + } + } + } + + float fontSizeInflation = nsLayoutUtils::FontSizeInflationFor(this); + + if (!IsCurrentFontInflation(fontSizeInflation)) { + // FIXME: Ideally, if we already have a text run, we'd move it to be + // the uninflated text run. + ClearTextRun(nullptr, nsTextFrame::eInflated); + mFontMetrics = nullptr; + } + + gfxSkipCharsIterator iter = + EnsureTextRun(nsTextFrame::eInflated, aDrawTarget, lineContainer, + aLineLayout.GetLine(), &flowEndInTextRun); + + NS_ASSERTION(IsCurrentFontInflation(fontSizeInflation), + "EnsureTextRun should have set font size inflation"); + + if (mTextRun && iter.GetOriginalEnd() < offset + length) { + // The textrun does not map enough text for this frame. This can happen + // when the textrun was ended in the middle of a text node because a + // preformatted newline was encountered, and prev-in-flow frames have + // consumed all the text of the textrun. We need a new textrun. + ClearTextRuns(); + iter = EnsureTextRun(nsTextFrame::eInflated, aDrawTarget, lineContainer, + aLineLayout.GetLine(), &flowEndInTextRun); + } + + if (!mTextRun) { + ClearMetrics(aMetrics); + return; + } + + NS_ASSERTION(gfxSkipCharsIterator(iter).ConvertOriginalToSkipped( + offset + length) <= mTextRun->GetLength(), + "Text run does not map enough text for our reflow"); + + ///////////////////////////////////////////////////////////////////// + // See how much text should belong to this text frame, and measure it + ///////////////////////////////////////////////////////////////////// + + iter.SetOriginalOffset(offset); + nscoord xOffsetForTabs = + (mTextRun->GetFlags2() & nsTextFrameUtils::Flags::HasTab) + ? (aLineLayout.GetCurrentFrameInlineDistanceFromBlock() - + lineContainer->GetUsedBorderAndPadding().left) + : -1; + PropertyProvider provider(mTextRun, textStyle, frag, this, iter, length, + lineContainer, xOffsetForTabs, + nsTextFrame::eInflated); + + uint32_t transformedOffset = provider.GetStart().GetSkippedOffset(); + + gfxFont::BoundingBoxType boundingBoxType = gfxFont::LOOSE_INK_EXTENTS; + if (IsFloatingFirstLetterChild() || IsInitialLetterChild()) { + if (nsFirstLetterFrame* firstLetter = do_QueryFrame(GetParent())) { + if (firstLetter->UseTightBounds()) { + boundingBoxType = gfxFont::TIGHT_HINTED_OUTLINE_EXTENTS; + } + } + } + + int32_t limitLength = length; + int32_t forceBreak = aLineLayout.GetForcedBreakPosition(this); + bool forceBreakAfter = false; + if (forceBreak >= length) { + forceBreakAfter = forceBreak == length; + // The break is not within the text considered for this textframe. + forceBreak = -1; + } + if (forceBreak >= 0) { + limitLength = forceBreak; + } + // This is the heart of text reflow right here! We don't know where + // to break, so we need to see how much text fits in the available width. + uint32_t transformedLength; + if (offset + limitLength >= int32_t(frag->GetLength())) { + NS_ASSERTION(offset + limitLength == int32_t(frag->GetLength()), + "Content offset/length out of bounds"); + NS_ASSERTION(flowEndInTextRun >= transformedOffset, + "Negative flow length?"); + transformedLength = flowEndInTextRun - transformedOffset; + } else { + // we're not looking at all the content, so we need to compute the + // length of the transformed substring we're looking at + gfxSkipCharsIterator iter(provider.GetStart()); + iter.SetOriginalOffset(offset + limitLength); + transformedLength = iter.GetSkippedOffset() - transformedOffset; + } + gfxTextRun::Metrics textMetrics; + uint32_t transformedLastBreak = 0; + bool usedHyphenation = false; + gfxTextRun::TrimmableWS trimmableWS; + gfxFloat availWidth = aAvailableWidth; + if (Style()->IsTextCombined()) { + // If text-combine-upright is 'all', we would compress whatever long + // text into ~1em width, so there is no limited on the avail width. + availWidth = std::numeric_limits<gfxFloat>::infinity(); + } + bool canTrimTrailingWhitespace = !textStyle->WhiteSpaceIsSignificant() || + HasAnyStateBits(TEXT_IS_IN_TOKEN_MATHML); + bool isBreakSpaces = textStyle->mWhiteSpace == StyleWhiteSpace::BreakSpaces; + // allow whitespace to overflow the container + bool whitespaceCanHang = textStyle->WhiteSpaceCanHangOrVisuallyCollapse(); + gfxBreakPriority breakPriority = aLineLayout.LastOptionalBreakPriority(); + gfxTextRun::SuppressBreak suppressBreak = gfxTextRun::eNoSuppressBreak; + bool shouldSuppressLineBreak = ShouldSuppressLineBreak(); + if (shouldSuppressLineBreak) { + suppressBreak = gfxTextRun::eSuppressAllBreaks; + } else if (!aLineLayout.LineIsBreakable()) { + suppressBreak = gfxTextRun::eSuppressInitialBreak; + } + uint32_t transformedCharsFit = mTextRun->BreakAndMeasureText( + transformedOffset, transformedLength, HasAnyStateBits(TEXT_START_OF_LINE), + availWidth, provider, suppressBreak, boundingBoxType, aDrawTarget, + textStyle->WordCanWrap(this), isBreakSpaces, + // The following are output parameters: + canTrimTrailingWhitespace || whitespaceCanHang ? &trimmableWS : nullptr, + textMetrics, usedHyphenation, transformedLastBreak, + // In/out + breakPriority); + if (!length && !textMetrics.mAscent && !textMetrics.mDescent) { + // If we're measuring a zero-length piece of text, update + // the height manually. + nsFontMetrics* fm = provider.GetFontMetrics(); + if (fm) { + textMetrics.mAscent = gfxFloat(fm->MaxAscent()); + textMetrics.mDescent = gfxFloat(fm->MaxDescent()); + } + } + if (GetWritingMode().IsLineInverted()) { + std::swap(textMetrics.mAscent, textMetrics.mDescent); + textMetrics.mBoundingBox.y = -textMetrics.mBoundingBox.YMost(); + } + // The "end" iterator points to the first character after the string mapped + // by this frame. Basically, its original-string offset is offset+charsFit + // after we've computed charsFit. + gfxSkipCharsIterator end(provider.GetEndHint()); + end.SetSkippedOffset(transformedOffset + transformedCharsFit); + int32_t charsFit = end.GetOriginalOffset() - offset; + if (offset + charsFit == newLineOffset) { + // We broke before a trailing preformatted '\n'. The newline should + // be assigned to this frame. Note that newLineOffset will be -1 if + // there was no preformatted newline, so we wouldn't get here in that + // case. + ++charsFit; + } + // That might have taken us beyond our assigned content range (because + // we might have advanced over some skipped chars that extend outside + // this frame), so get back in. + int32_t lastBreak = -1; + if (charsFit >= limitLength) { + charsFit = limitLength; + if (transformedLastBreak != UINT32_MAX) { + // lastBreak is needed. + // This may set lastBreak greater than 'length', but that's OK + lastBreak = end.ConvertSkippedToOriginal(transformedOffset + + transformedLastBreak); + } + end.SetOriginalOffset(offset + charsFit); + // If we were forced to fit, and the break position is after a soft hyphen, + // note that this is a hyphenation break. + if ((forceBreak >= 0 || forceBreakAfter) && + HasSoftHyphenBefore(frag, mTextRun, offset, end)) { + usedHyphenation = true; + } + } + if (usedHyphenation) { + // Fix up metrics to include hyphen + AddHyphenToMetrics(this, mTextRun->IsRightToLeft(), &textMetrics, + boundingBoxType, aDrawTarget); + AddStateBits(TEXT_HYPHEN_BREAK | TEXT_HAS_NONCOLLAPSED_CHARACTERS); + } + if (textMetrics.mBoundingBox.IsEmpty()) { + AddStateBits(TEXT_NO_RENDERED_GLYPHS); + } + + bool brokeText = forceBreak >= 0 || transformedCharsFit < transformedLength; + if (trimmableWS.mAdvance > 0.0) { + if (canTrimTrailingWhitespace) { + // Optimization: if we we can be sure this frame will be at end of line, + // then trim the whitespace now. + if (brokeText || HasAnyStateBits(TEXT_IS_IN_TOKEN_MATHML)) { + // We're definitely going to break so our trailing whitespace should + // definitely be trimmed. Record that we've already done it. + AddStateBits(TEXT_TRIMMED_TRAILING_WHITESPACE); + textMetrics.mAdvanceWidth -= trimmableWS.mAdvance; + trimmableWS.mAdvance = 0.0; + } + ClearHangableISize(); + ClearTrimmableWS(); + } else if (whitespaceCanHang) { + // Figure out how much whitespace will hang if at end-of-line. + gfxFloat hang = + std::min(std::max(0.0, textMetrics.mAdvanceWidth - availWidth), + gfxFloat(trimmableWS.mAdvance)); + SetHangableISize(NSToCoordRound(trimmableWS.mAdvance - hang)); + // nsLineLayout only needs the TrimmableWS property if justifying, so + // check whether this is relevant. + if (textStyle->mTextAlign == StyleTextAlign::Justify || + textStyle->mTextAlignLast == StyleTextAlignLast::Justify) { + SetTrimmableWS(trimmableWS); + } + textMetrics.mAdvanceWidth -= hang; + trimmableWS.mAdvance = 0.0; + } else { + MOZ_ASSERT_UNREACHABLE("How did trimmableWS get set?!"); + ClearHangableISize(); + ClearTrimmableWS(); + trimmableWS.mAdvance = 0.0; + } + } else { + // Remove any stale frame properties. + ClearHangableISize(); + ClearTrimmableWS(); + } + + if (!brokeText && lastBreak >= 0) { + // Since everything fit and no break was forced, + // record the last break opportunity + NS_ASSERTION(textMetrics.mAdvanceWidth - trimmableWS.mAdvance <= availWidth, + "If the text doesn't fit, and we have a break opportunity, " + "why didn't MeasureText use it?"); + MOZ_ASSERT(lastBreak >= offset, "Strange break position"); + aLineLayout.NotifyOptionalBreakPosition(this, lastBreak - offset, true, + breakPriority); + } + + int32_t contentLength = offset + charsFit - GetContentOffset(); + + ///////////////////////////////////////////////////////////////////// + // Compute output metrics + ///////////////////////////////////////////////////////////////////// + + // first-letter frames should use the tight bounding box metrics for + // ascent/descent for good drop-cap effects + if (HasAnyStateBits(TEXT_FIRST_LETTER)) { + textMetrics.mAscent = + std::max(gfxFloat(0.0), -textMetrics.mBoundingBox.Y()); + textMetrics.mDescent = + std::max(gfxFloat(0.0), textMetrics.mBoundingBox.YMost()); + } + + // Setup metrics for caller + // Disallow negative widths + WritingMode wm = GetWritingMode(); + LogicalSize finalSize(wm); + finalSize.ISize(wm) = + NSToCoordCeilClamped(std::max(gfxFloat(0.0), textMetrics.mAdvanceWidth)); + + nscoord fontBaseline; + // Note(dshin): Baseline should tecnhically be halfway through the em box for + // a central baseline. It is simply half of the text run block size so that it + // can be easily calculated in `GetNaturalBaselineBOffset`. + if (transformedCharsFit == 0 && !usedHyphenation) { + aMetrics.SetBlockStartAscent(0); + finalSize.BSize(wm) = 0; + fontBaseline = 0; + } else if (boundingBoxType != gfxFont::LOOSE_INK_EXTENTS) { + fontBaseline = NSToCoordCeil(textMetrics.mAscent); + const auto size = fontBaseline + NSToCoordCeil(textMetrics.mDescent); + // Use actual text metrics for floating first letter frame. + aMetrics.SetBlockStartAscent(wm.IsAlphabeticalBaseline() ? fontBaseline + : size / 2); + finalSize.BSize(wm) = size; + } else { + // Otherwise, ascent should contain the overline drawable area. + // And also descent should contain the underline drawable area. + // nsFontMetrics::GetMaxAscent/GetMaxDescent contains them. + nsFontMetrics* fm = provider.GetFontMetrics(); + nscoord fontAscent = + wm.IsLineInverted() ? fm->MaxDescent() : fm->MaxAscent(); + nscoord fontDescent = + wm.IsLineInverted() ? fm->MaxAscent() : fm->MaxDescent(); + fontBaseline = std::max(NSToCoordCeil(textMetrics.mAscent), fontAscent); + const auto size = + fontBaseline + + std::max(NSToCoordCeil(textMetrics.mDescent), fontDescent); + aMetrics.SetBlockStartAscent(wm.IsAlphabeticalBaseline() ? fontBaseline + : size / 2); + finalSize.BSize(wm) = size; + } + if (Style()->IsTextCombined()) { + nsFontMetrics* fm = provider.GetFontMetrics(); + nscoord width = finalSize.ISize(wm); + nscoord em = fm->EmHeight(); + // Compress the characters in horizontal axis if necessary. + if (width <= em) { + RemoveProperty(TextCombineScaleFactorProperty()); + } else { + SetProperty(TextCombineScaleFactorProperty(), + static_cast<float>(em) / static_cast<float>(width)); + finalSize.ISize(wm) = em; + } + // Make the characters be in an 1em square. + if (finalSize.BSize(wm) != em) { + fontBaseline = + aMetrics.BlockStartAscent() + (em - finalSize.BSize(wm)) / 2; + aMetrics.SetBlockStartAscent(fontBaseline); + finalSize.BSize(wm) = em; + } + } + aMetrics.SetSize(wm, finalSize); + + NS_ASSERTION(aMetrics.BlockStartAscent() >= 0, "Negative ascent???"); + NS_ASSERTION( + (Style()->IsTextCombined() ? aMetrics.ISize(aMetrics.GetWritingMode()) + : aMetrics.BSize(aMetrics.GetWritingMode())) - + aMetrics.BlockStartAscent() >= + 0, + "Negative descent???"); + + mAscent = fontBaseline; + + // Handle text that runs outside its normal bounds. + nsRect boundingBox = RoundOut(textMetrics.mBoundingBox); + if (mTextRun->IsVertical()) { + // Swap line-relative textMetrics dimensions to physical coordinates. + std::swap(boundingBox.x, boundingBox.y); + std::swap(boundingBox.width, boundingBox.height); + if (GetWritingMode().IsVerticalRL()) { + boundingBox.x = -boundingBox.XMost(); + boundingBox.x += aMetrics.Width() - mAscent; + } else { + boundingBox.x += mAscent; + } + } else { + boundingBox.y += mAscent; + } + aMetrics.SetOverflowAreasToDesiredBounds(); + aMetrics.InkOverflow().UnionRect(aMetrics.InkOverflow(), boundingBox); + + // When we have text decorations, we don't need to compute their overflow now + // because we're guaranteed to do it later + // (see nsLineLayout::RelativePositionFrames) + UnionAdditionalOverflow(presContext, aLineLayout.LineContainerFrame(), + provider, &aMetrics.InkOverflow(), false, true); + + ///////////////////////////////////////////////////////////////////// + // Clean up, update state + ///////////////////////////////////////////////////////////////////// + + // If all our characters are discarded or collapsed, then trimmable width + // from the last textframe should be preserved. Otherwise the trimmable width + // from this textframe overrides. (Currently in CSS trimmable width can be + // at most one space so there's no way for trimmable width from a previous + // frame to accumulate with trimmable width from this frame.) + if (transformedCharsFit > 0) { + aLineLayout.SetTrimmableISize(NSToCoordFloor(trimmableWS.mAdvance)); + AddStateBits(TEXT_HAS_NONCOLLAPSED_CHARACTERS); + } + bool breakAfter = forceBreakAfter; + if (!shouldSuppressLineBreak) { + if (charsFit > 0 && charsFit == length && + textStyle->mHyphens != StyleHyphens::None && + HasSoftHyphenBefore(frag, mTextRun, offset, end)) { + bool fits = + textMetrics.mAdvanceWidth + provider.GetHyphenWidth() <= availWidth; + // Record a potential break after final soft hyphen + aLineLayout.NotifyOptionalBreakPosition(this, length, fits, + gfxBreakPriority::eNormalBreak); + } + // length == 0 means either the text is empty or it's all collapsed away + bool emptyTextAtStartOfLine = atStartOfLine && length == 0; + if (!breakAfter && charsFit == length && !emptyTextAtStartOfLine && + transformedOffset + transformedLength == mTextRun->GetLength() && + (mTextRun->GetFlags2() & nsTextFrameUtils::Flags::HasTrailingBreak)) { + // We placed all the text in the textrun and we have a break opportunity + // at the end of the textrun. We need to record it because the following + // content may not care about nsLineBreaker. + + // Note that because we didn't break, we can be sure that (thanks to the + // code up above) textMetrics.mAdvanceWidth includes the width of any + // trailing whitespace. So we need to subtract trimmableWidth here + // because if we did break at this point, that much width would be + // trimmed. + if (textMetrics.mAdvanceWidth - trimmableWS.mAdvance > availWidth) { + breakAfter = true; + } else { + aLineLayout.NotifyOptionalBreakPosition(this, length, true, + gfxBreakPriority::eNormalBreak); + } + } + } + + // Compute reflow status + if (contentLength != maxContentLength) { + aStatus.SetIncomplete(); + } + + if (charsFit == 0 && length > 0 && !usedHyphenation) { + // Couldn't place any text + aStatus.SetInlineLineBreakBeforeAndReset(); + } else if (contentLength > 0 && + mContentOffset + contentLength - 1 == newLineOffset) { + // Ends in \n + aStatus.SetInlineLineBreakAfter(); + aLineLayout.SetLineEndsInBR(true); + } else if (breakAfter) { + aStatus.SetInlineLineBreakAfter(); + } + if (completedFirstLetter) { + aLineLayout.SetFirstLetterStyleOK(false); + aStatus.SetFirstLetterComplete(); + } + + // Updated the cached NewlineProperty, or delete it. + if (contentLength < maxContentLength && + textStyle->NewlineIsSignificant(this) && + (contentNewLineOffset < 0 || + mContentOffset + contentLength <= contentNewLineOffset)) { + if (!cachedNewlineOffset) { + cachedNewlineOffset = new NewlineProperty; + if (NS_FAILED(mContent->SetProperty( + nsGkAtoms::newline, cachedNewlineOffset, + nsINode::DeleteProperty<NewlineProperty>))) { + delete cachedNewlineOffset; + cachedNewlineOffset = nullptr; + } + mContent->SetFlags(NS_HAS_NEWLINE_PROPERTY); + } + if (cachedNewlineOffset) { + cachedNewlineOffset->mStartOffset = offset; + cachedNewlineOffset->mNewlineOffset = contentNewLineOffset; + } + } else if (cachedNewlineOffset) { + mContent->RemoveProperty(nsGkAtoms::newline); + mContent->UnsetFlags(NS_HAS_NEWLINE_PROPERTY); + } + + // Compute space and letter counts for justification, if required + if ((lineContainer->StyleText()->mTextAlign == StyleTextAlign::Justify || + lineContainer->StyleText()->mTextAlignLast == + StyleTextAlignLast::Justify || + shouldSuppressLineBreak) && + !lineContainer->IsInSVGTextSubtree()) { + AddStateBits(TEXT_JUSTIFICATION_ENABLED); + Range range(uint32_t(offset), uint32_t(offset + charsFit)); + aLineLayout.SetJustificationInfo(provider.ComputeJustification(range)); + } + + SetLength(contentLength, &aLineLayout, ALLOW_FRAME_CREATION_AND_DESTRUCTION); + + InvalidateFrame(); + +#ifdef NOISY_REFLOW + ListTag(stdout); + printf(": desiredSize=%d,%d(b=%d) status=%x\n", aMetrics.Width(), + aMetrics.Height(), aMetrics.BlockStartAscent(), aStatus); +#endif +} + +/* virtual */ +bool nsTextFrame::CanContinueTextRun() const { + // We can continue a text run through a text frame + return true; +} + +nsTextFrame::TrimOutput nsTextFrame::TrimTrailingWhiteSpace( + DrawTarget* aDrawTarget) { + MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IS_DIRTY | NS_FRAME_FIRST_REFLOW), + "frame should have been reflowed"); + + TrimOutput result; + result.mChanged = false; + result.mDeltaWidth = 0; + + AddStateBits(TEXT_END_OF_LINE); + + if (!GetTextRun(nsTextFrame::eInflated)) { + // If reflow didn't create a textrun, there must have been no content once + // leading whitespace was trimmed, so nothing more to do here. + return result; + } + + int32_t contentLength = GetContentLength(); + if (!contentLength) { + return result; + } + + gfxSkipCharsIterator start = + EnsureTextRun(nsTextFrame::eInflated, aDrawTarget); + NS_ENSURE_TRUE(mTextRun, result); + + uint32_t trimmedStart = start.GetSkippedOffset(); + + const nsTextFragment* frag = TextFragment(); + TrimmedOffsets trimmed = GetTrimmedOffsets(frag); + gfxSkipCharsIterator trimmedEndIter = start; + const nsStyleText* textStyle = StyleText(); + gfxFloat delta = 0; + uint32_t trimmedEnd = + trimmedEndIter.ConvertOriginalToSkipped(trimmed.GetEnd()); + + if (!HasAnyStateBits(TEXT_TRIMMED_TRAILING_WHITESPACE) && + trimmed.GetEnd() < GetContentEnd()) { + gfxSkipCharsIterator end = trimmedEndIter; + uint32_t endOffset = + end.ConvertOriginalToSkipped(GetContentOffset() + contentLength); + if (trimmedEnd < endOffset) { + // We can't be dealing with tabs here ... they wouldn't be trimmed. So + // it's OK to pass null for the line container. + PropertyProvider provider(mTextRun, textStyle, frag, this, start, + contentLength, nullptr, 0, + nsTextFrame::eInflated); + delta = + mTextRun->GetAdvanceWidth(Range(trimmedEnd, endOffset), &provider); + result.mChanged = true; + } + } + + gfxFloat advanceDelta; + mTextRun->SetLineBreaks(Range(trimmedStart, trimmedEnd), + HasAnyStateBits(TEXT_START_OF_LINE), true, + &advanceDelta); + if (advanceDelta != 0) { + result.mChanged = true; + } + + // aDeltaWidth is *subtracted* from our width. + // If advanceDelta is positive then setting the line break made us longer, + // so aDeltaWidth could go negative. + result.mDeltaWidth = NSToCoordFloor(delta - advanceDelta); + // If aDeltaWidth goes negative, that means this frame might not actually fit + // anymore!!! We need higher level line layout to recover somehow. + // If it's because the frame has a soft hyphen that is now being displayed, + // this should actually be OK, because our reflow recorded the break + // opportunity that allowed the soft hyphen to be used, and we wouldn't + // have recorded the opportunity unless the hyphen fit (or was the first + // opportunity on the line). + // Otherwise this can/ really only happen when we have glyphs with special + // shapes at the end of lines, I think. Breaking inside a kerning pair won't + // do it because that would mean we broke inside this textrun, and + // BreakAndMeasureText should make sure the resulting shaped substring fits. + // Maybe if we passed a maxTextLength? But that only happens at direction + // changes (so we wouldn't kern across the boundary) or for first-letter + // (which always fits because it starts the line!). + NS_WARNING_ASSERTION(result.mDeltaWidth >= 0, + "Negative deltawidth, something odd is happening"); + +#ifdef NOISY_TRIM + ListTag(stdout); + printf(": trim => %d\n", result.mDeltaWidth); +#endif + return result; +} + +OverflowAreas nsTextFrame::RecomputeOverflow(nsIFrame* aBlockFrame, + bool aIncludeShadows) { + RemoveProperty(WebRenderTextBounds()); + + nsRect bounds(nsPoint(0, 0), GetSize()); + OverflowAreas result(bounds, bounds); + + gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); + if (!mTextRun) { + return result; + } + + PropertyProvider provider(this, iter, nsTextFrame::eInflated, mFontMetrics); + // Don't trim trailing space, in case we need to paint it as selected. + provider.InitializeForDisplay(false); + + gfxTextRun::Metrics textMetrics = + mTextRun->MeasureText(ComputeTransformedRange(provider), + gfxFont::LOOSE_INK_EXTENTS, nullptr, &provider); + if (GetWritingMode().IsLineInverted()) { + textMetrics.mBoundingBox.y = -textMetrics.mBoundingBox.YMost(); + } + nsRect boundingBox = RoundOut(textMetrics.mBoundingBox); + boundingBox += nsPoint(0, mAscent); + if (mTextRun->IsVertical()) { + // Swap line-relative textMetrics dimensions to physical coordinates. + std::swap(boundingBox.x, boundingBox.y); + std::swap(boundingBox.width, boundingBox.height); + } + nsRect& vis = result.InkOverflow(); + vis.UnionRect(vis, boundingBox); + UnionAdditionalOverflow(PresContext(), aBlockFrame, provider, &vis, true, + aIncludeShadows); + return result; +} + +static void TransformChars(nsTextFrame* aFrame, const nsStyleText* aStyle, + const gfxTextRun* aTextRun, uint32_t aSkippedOffset, + const nsTextFragment* aFrag, int32_t aFragOffset, + int32_t aFragLen, nsAString& aOut) { + nsAutoString fragString; + char16_t* out; + bool needsToMaskPassword = NeedsToMaskPassword(aFrame); + if (aStyle->mTextTransform.IsNone() && !needsToMaskPassword && + aStyle->mWebkitTextSecurity == StyleTextSecurity::None) { + // No text-transform, so we can copy directly to the output string. + aOut.SetLength(aOut.Length() + aFragLen); + out = aOut.EndWriting() - aFragLen; + } else { + // Use a temporary string as source for the transform. + fragString.SetLength(aFragLen); + out = fragString.BeginWriting(); + } + + // Copy the text, with \n and \t replaced by <space> if appropriate. + MOZ_ASSERT(aFragOffset >= 0); + for (uint32_t i = 0; i < static_cast<uint32_t>(aFragLen); ++i) { + char16_t ch = aFrag->CharAt(static_cast<uint32_t>(aFragOffset) + i); + if ((ch == '\n' && !aStyle->NewlineIsSignificant(aFrame)) || + (ch == '\t' && !aStyle->TabIsSignificant())) { + ch = ' '; + } + out[i] = ch; + } + + if (!aStyle->mTextTransform.IsNone() || needsToMaskPassword || + aStyle->mWebkitTextSecurity != StyleTextSecurity::None) { + MOZ_ASSERT(aTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsTransformed); + if (aTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsTransformed) { + // Apply text-transform according to style in the transformed run. + char16_t maskChar = + needsToMaskPassword ? 0 : aStyle->TextSecurityMaskChar(); + auto transformedTextRun = + static_cast<const nsTransformedTextRun*>(aTextRun); + nsAutoString convertedString; + AutoTArray<bool, 50> charsToMergeArray; + AutoTArray<bool, 50> deletedCharsArray; + nsCaseTransformTextRunFactory::TransformString( + fragString, convertedString, /* aGlobalTransform = */ Nothing(), + maskChar, /* aCaseTransformsOnly = */ true, nullptr, + charsToMergeArray, deletedCharsArray, transformedTextRun, + aSkippedOffset); + aOut.Append(convertedString); + } else { + // Should not happen (see assertion above), but as a fallback... + aOut.Append(fragString); + } + } +} + +static void LineStartsOrEndsAtHardLineBreak(nsTextFrame* aFrame, + nsBlockFrame* aLineContainer, + bool* aStartsAtHardBreak, + bool* aEndsAtHardBreak) { + bool foundValidLine; + nsBlockInFlowLineIterator iter(aLineContainer, aFrame, &foundValidLine); + if (!foundValidLine) { + NS_ERROR("Invalid line!"); + *aStartsAtHardBreak = *aEndsAtHardBreak = true; + return; + } + + *aEndsAtHardBreak = !iter.GetLine()->IsLineWrapped(); + if (iter.Prev()) { + *aStartsAtHardBreak = !iter.GetLine()->IsLineWrapped(); + } else { + // Hit block boundary + *aStartsAtHardBreak = true; + } +} + +nsIFrame::RenderedText nsTextFrame::GetRenderedText( + uint32_t aStartOffset, uint32_t aEndOffset, TextOffsetType aOffsetType, + TrailingWhitespace aTrimTrailingWhitespace) { + MOZ_ASSERT(aStartOffset <= aEndOffset, "bogus offsets"); + MOZ_ASSERT(!GetPrevContinuation() || + (aOffsetType == TextOffsetType::OffsetsInContentText && + aStartOffset >= (uint32_t)GetContentOffset() && + aEndOffset <= (uint32_t)GetContentEnd()), + "Must be called on first-in-flow, or content offsets must be " + "given and be within this frame."); + + // The handling of offsets could be more efficient... + RenderedText result; + nsBlockFrame* lineContainer = nullptr; + nsTextFrame* textFrame; + const nsTextFragment* textFrag = TextFragment(); + uint32_t offsetInRenderedString = 0; + bool haveOffsets = false; + + for (textFrame = this; textFrame; + textFrame = textFrame->GetNextContinuation()) { + if (textFrame->HasAnyStateBits(NS_FRAME_IS_DIRTY)) { + // We don't trust dirty frames, especially when computing rendered text. + break; + } + + // Ensure the text run and grab the gfxSkipCharsIterator for it + gfxSkipCharsIterator iter = + textFrame->EnsureTextRun(nsTextFrame::eInflated); + if (!textFrame->mTextRun) { + break; + } + gfxSkipCharsIterator tmpIter = iter; + + // Check if the frame starts/ends at a hard line break, to determine + // whether whitespace should be trimmed. + bool startsAtHardBreak, endsAtHardBreak; + if (!HasAnyStateBits(TEXT_START_OF_LINE | TEXT_END_OF_LINE)) { + startsAtHardBreak = endsAtHardBreak = false; + } else if (nsBlockFrame* thisLc = + do_QueryFrame(FindLineContainer(textFrame))) { + if (thisLc != lineContainer) { + // Setup line cursor when needed. + lineContainer = thisLc; + lineContainer->SetupLineCursorForQuery(); + } + LineStartsOrEndsAtHardLineBreak(textFrame, lineContainer, + &startsAtHardBreak, &endsAtHardBreak); + } else { + // Weird situation where we have a line layout without a block. + // No soft breaks occur in this situation. + startsAtHardBreak = endsAtHardBreak = true; + } + + // Whether we need to trim whitespaces after the text frame. + // TrimmedOffsetFlags::Default will allow trimming; we set NoTrim* flags + // in the cases where this should not occur. + TrimmedOffsetFlags trimFlags = TrimmedOffsetFlags::Default; + if (!textFrame->IsAtEndOfLine() || + aTrimTrailingWhitespace != TrailingWhitespace::Trim || + !endsAtHardBreak) { + trimFlags |= TrimmedOffsetFlags::NoTrimAfter; + } + + // Whether to trim whitespaces before the text frame. + if (!startsAtHardBreak) { + trimFlags |= TrimmedOffsetFlags::NoTrimBefore; + } + + TrimmedOffsets trimmedOffsets = + textFrame->GetTrimmedOffsets(textFrag, trimFlags); + bool trimmedSignificantNewline = + trimmedOffsets.GetEnd() < GetContentEnd() && + HasSignificantTerminalNewline(); + uint32_t skippedToRenderedStringOffset = + offsetInRenderedString - + tmpIter.ConvertOriginalToSkipped(trimmedOffsets.mStart); + uint32_t nextOffsetInRenderedString = + tmpIter.ConvertOriginalToSkipped(trimmedOffsets.GetEnd()) + + (trimmedSignificantNewline ? 1 : 0) + skippedToRenderedStringOffset; + + if (aOffsetType == TextOffsetType::OffsetsInRenderedText) { + if (nextOffsetInRenderedString <= aStartOffset) { + offsetInRenderedString = nextOffsetInRenderedString; + continue; + } + if (!haveOffsets) { + result.mOffsetWithinNodeText = tmpIter.ConvertSkippedToOriginal( + aStartOffset - skippedToRenderedStringOffset); + result.mOffsetWithinNodeRenderedText = aStartOffset; + haveOffsets = true; + } + if (offsetInRenderedString >= aEndOffset) { + break; + } + } else { + if (uint32_t(textFrame->GetContentEnd()) <= aStartOffset) { + offsetInRenderedString = nextOffsetInRenderedString; + continue; + } + if (!haveOffsets) { + result.mOffsetWithinNodeText = aStartOffset; + // Skip trimmed space when computed the rendered text offset. + int32_t clamped = + std::max<int32_t>(aStartOffset, trimmedOffsets.mStart); + result.mOffsetWithinNodeRenderedText = + tmpIter.ConvertOriginalToSkipped(clamped) + + skippedToRenderedStringOffset; + MOZ_ASSERT( + result.mOffsetWithinNodeRenderedText >= offsetInRenderedString && + result.mOffsetWithinNodeRenderedText <= INT32_MAX, + "Bad offset within rendered text"); + haveOffsets = true; + } + if (uint32_t(textFrame->mContentOffset) >= aEndOffset) { + break; + } + } + + int32_t startOffset; + int32_t endOffset; + if (aOffsetType == TextOffsetType::OffsetsInRenderedText) { + startOffset = tmpIter.ConvertSkippedToOriginal( + aStartOffset - skippedToRenderedStringOffset); + endOffset = tmpIter.ConvertSkippedToOriginal( + aEndOffset - skippedToRenderedStringOffset); + } else { + startOffset = aStartOffset; + endOffset = std::min<uint32_t>(INT32_MAX, aEndOffset); + } + + // If startOffset and/or endOffset are inside of trimmedOffsets' range, + // then clamp the edges of trimmedOffsets accordingly. + int32_t origTrimmedOffsetsEnd = trimmedOffsets.GetEnd(); + trimmedOffsets.mStart = + std::max<uint32_t>(trimmedOffsets.mStart, startOffset); + trimmedOffsets.mLength = + std::min<uint32_t>(origTrimmedOffsetsEnd, endOffset) - + trimmedOffsets.mStart; + if (trimmedOffsets.mLength <= 0) { + offsetInRenderedString = nextOffsetInRenderedString; + continue; + } + + const nsStyleText* textStyle = textFrame->StyleText(); + iter.SetOriginalOffset(trimmedOffsets.mStart); + while (iter.GetOriginalOffset() < trimmedOffsets.GetEnd()) { + int32_t runLength; + bool isSkipped = iter.IsOriginalCharSkipped(&runLength); + runLength = std::min(runLength, + trimmedOffsets.GetEnd() - iter.GetOriginalOffset()); + if (isSkipped) { + MOZ_ASSERT(runLength >= 0); + for (uint32_t i = 0; i < static_cast<uint32_t>(runLength); ++i) { + const char16_t ch = textFrag->CharAt( + AssertedCast<uint32_t>(iter.GetOriginalOffset() + i)); + if (ch == CH_SHY) { + // We should preserve soft hyphens. They can't be transformed. + result.mString.Append(ch); + } + } + } else { + TransformChars(textFrame, textStyle, textFrame->mTextRun, + iter.GetSkippedOffset(), textFrag, + iter.GetOriginalOffset(), runLength, result.mString); + } + iter.AdvanceOriginal(runLength); + } + + if (trimmedSignificantNewline && GetContentEnd() <= endOffset) { + // A significant newline was trimmed off (we must be + // white-space:pre-line). Put it back. + result.mString.Append('\n'); + } + offsetInRenderedString = nextOffsetInRenderedString; + } + + if (!haveOffsets) { + result.mOffsetWithinNodeText = textFrag->GetLength(); + result.mOffsetWithinNodeRenderedText = offsetInRenderedString; + } + return result; +} + +/* virtual */ +bool nsTextFrame::IsEmpty() { + NS_ASSERTION( + !HasAllStateBits(TEXT_IS_ONLY_WHITESPACE | TEXT_ISNOT_ONLY_WHITESPACE), + "Invalid state"); + + // XXXldb Should this check compatibility mode as well??? + const nsStyleText* textStyle = StyleText(); + if (textStyle->WhiteSpaceIsSignificant()) { + // When WhiteSpaceIsSignificant styles are in effect, we only treat the + // frame as empty if its content really is entirely *empty* (not just + // whitespace), AND it is NOT editable or within an <input> element. + // In these cases we consider that the whitespace-preserving style makes + // the frame behave as non-empty so that its height doesn't become zero. + return GetContentLength() == 0 && !GetContent()->IsEditable() && + !GetContent()->GetParent()->IsHTMLElement(nsGkAtoms::input); + } + + if (HasAnyStateBits(TEXT_ISNOT_ONLY_WHITESPACE)) { + return false; + } + + if (HasAnyStateBits(TEXT_IS_ONLY_WHITESPACE)) { + return true; + } + + bool isEmpty = + IsAllWhitespace(TextFragment(), textStyle->mWhiteSpace != + mozilla::StyleWhiteSpace::PreLine); + AddStateBits(isEmpty ? TEXT_IS_ONLY_WHITESPACE : TEXT_ISNOT_ONLY_WHITESPACE); + return isEmpty; +} + +#ifdef DEBUG_FRAME_DUMP +// Translate the mapped content into a string that's printable +void nsTextFrame::ToCString(nsCString& aBuf) const { + // Get the frames text content + const nsTextFragment* frag = TextFragment(); + if (!frag) { + return; + } + + const int32_t length = GetContentEnd() - mContentOffset; + if (length <= 0) { + // Negative lengths are possible during invalidation. + return; + } + + const uint32_t fragLength = AssertedCast<uint32_t>(GetContentEnd()); + uint32_t fragOffset = AssertedCast<uint32_t>(GetContentOffset()); + + while (fragOffset < fragLength) { + char16_t ch = frag->CharAt(fragOffset++); + if (ch == '\r') { + aBuf.AppendLiteral("\\r"); + } else if (ch == '\n') { + aBuf.AppendLiteral("\\n"); + } else if (ch == '\t') { + aBuf.AppendLiteral("\\t"); + } else if ((ch < ' ') || (ch >= 127)) { + aBuf.Append(nsPrintfCString("\\u%04x", ch)); + } else { + aBuf.Append(ch); + } + } +} + +nsresult nsTextFrame::GetFrameName(nsAString& aResult) const { + MakeFrameName(u"Text"_ns, aResult); + nsAutoCString tmp; + ToCString(tmp); + tmp.SetLength(std::min<size_t>(tmp.Length(), 50u)); + aResult += u"\""_ns + NS_ConvertASCIItoUTF16(tmp) + u"\""_ns; + return NS_OK; +} + +void nsTextFrame::List(FILE* out, const char* aPrefix, ListFlags aFlags) const { + nsCString str; + ListGeneric(str, aPrefix, aFlags); + + str += nsPrintfCString(" [run=%p]", static_cast<void*>(mTextRun)); + + // Output the first/last content offset and prev/next in flow info + bool isComplete = uint32_t(GetContentEnd()) == GetContent()->TextLength(); + str += nsPrintfCString("[%d,%d,%c] ", GetContentOffset(), GetContentLength(), + isComplete ? 'T' : 'F'); + + if (IsSelected()) { + str += " SELECTED"; + } + fprintf_stderr(out, "%s\n", str.get()); +} + +void nsTextFrame::ListTextRuns(FILE* out, + nsTHashSet<const void*>& aSeen) const { + if (!mTextRun || aSeen.Contains(mTextRun)) { + return; + } + aSeen.Insert(mTextRun); + mTextRun->Dump(out); +} +#endif + +void nsTextFrame::AdjustOffsetsForBidi(int32_t aStart, int32_t aEnd) { + AddStateBits(NS_FRAME_IS_BIDI); + if (mContent->HasFlag(NS_HAS_FLOWLENGTH_PROPERTY)) { + mContent->RemoveProperty(nsGkAtoms::flowlength); + mContent->UnsetFlags(NS_HAS_FLOWLENGTH_PROPERTY); + } + + /* + * After Bidi resolution we may need to reassign text runs. + * This is called during bidi resolution from the block container, so we + * shouldn't be holding a local reference to a textrun anywhere. + */ + ClearTextRuns(); + + nsTextFrame* prev = GetPrevContinuation(); + if (prev) { + // the bidi resolver can be very evil when columns/pages are involved. Don't + // let it violate our invariants. + int32_t prevOffset = prev->GetContentOffset(); + aStart = std::max(aStart, prevOffset); + aEnd = std::max(aEnd, prevOffset); + prev->ClearTextRuns(); + } + + mContentOffset = aStart; + SetLength(aEnd - aStart, nullptr, 0); +} + +/** + * @return true if this text frame ends with a newline character. It should + * return false if it is not a text frame. + */ +bool nsTextFrame::HasSignificantTerminalNewline() const { + return ::HasTerminalNewline(this) && StyleText()->NewlineIsSignificant(this); +} + +bool nsTextFrame::IsAtEndOfLine() const { + return HasAnyStateBits(TEXT_END_OF_LINE); +} + +Maybe<nscoord> nsTextFrame::GetNaturalBaselineBOffset( + WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const { + if (aBaselineGroup == BaselineSharingGroup::Last) { + return Nothing{}; + } + + if (!aWM.IsOrthogonalTo(GetWritingMode())) { + if (aWM.IsCentralBaseline()) { + return Some(GetLogicalUsedBorderAndPadding(aWM).BStart(aWM) + + ContentSize(aWM).BSize(aWM) / 2); + } + return Some(mAscent); + } + + // When the text frame has a writing mode orthogonal to the desired + // writing mode, return a baseline coincides its parent frame. + nsIFrame* parent = GetParent(); + nsPoint position = GetNormalPosition(); + nscoord parentAscent = parent->GetLogicalBaseline(aWM); + if (aWM.IsVerticalRL()) { + nscoord parentDescent = parent->GetSize().width - parentAscent; + nscoord descent = parentDescent - position.x; + return Some(GetSize().width - descent); + } + return Some(parentAscent - (aWM.IsVertical() ? position.x : position.y)); +} + +bool nsTextFrame::HasAnyNoncollapsedCharacters() { + gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); + int32_t offset = GetContentOffset(), offsetEnd = GetContentEnd(); + int32_t skippedOffset = iter.ConvertOriginalToSkipped(offset); + int32_t skippedOffsetEnd = iter.ConvertOriginalToSkipped(offsetEnd); + return skippedOffset != skippedOffsetEnd; +} + +bool nsTextFrame::ComputeCustomOverflow(OverflowAreas& aOverflowAreas) { + return ComputeCustomOverflowInternal(aOverflowAreas, true); +} + +bool nsTextFrame::ComputeCustomOverflowInternal(OverflowAreas& aOverflowAreas, + bool aIncludeShadows) { + if (HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) { + return true; + } + + nsIFrame* decorationsBlock; + if (IsFloatingFirstLetterChild()) { + decorationsBlock = GetParent(); + } else { + nsIFrame* f = this; + for (;;) { + nsBlockFrame* fBlock = do_QueryFrame(f); + if (fBlock) { + decorationsBlock = fBlock; + break; + } + + f = f->GetParent(); + if (!f) { + NS_ERROR("Couldn't find any block ancestor (for text decorations)"); + return nsIFrame::ComputeCustomOverflow(aOverflowAreas); + } + } + } + + aOverflowAreas = RecomputeOverflow(decorationsBlock, aIncludeShadows); + return nsIFrame::ComputeCustomOverflow(aOverflowAreas); +} + +NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(JustificationAssignmentProperty, int32_t) + +void nsTextFrame::AssignJustificationGaps( + const mozilla::JustificationAssignment& aAssign) { + int32_t encoded = (aAssign.mGapsAtStart << 8) | aAssign.mGapsAtEnd; + static_assert(sizeof(aAssign) == 1, + "The encoding might be broken if JustificationAssignment " + "is larger than 1 byte"); + SetProperty(JustificationAssignmentProperty(), encoded); +} + +mozilla::JustificationAssignment nsTextFrame::GetJustificationAssignment() + const { + int32_t encoded = GetProperty(JustificationAssignmentProperty()); + mozilla::JustificationAssignment result; + result.mGapsAtStart = encoded >> 8; + result.mGapsAtEnd = encoded & 0xFF; + return result; +} + +uint32_t nsTextFrame::CountGraphemeClusters() const { + const nsTextFragment* frag = TextFragment(); + MOZ_ASSERT(frag, "Text frame must have text fragment"); + nsAutoString content; + frag->AppendTo(content, AssertedCast<uint32_t>(GetContentOffset()), + AssertedCast<uint32_t>(GetContentLength())); + return unicode::CountGraphemeClusters(content); +} + +bool nsTextFrame::HasNonSuppressedText() const { + if (HasAnyStateBits(TEXT_ISNOT_ONLY_WHITESPACE | + // If we haven't reflowed yet, or are currently doing so, + // just return true because we can't be sure. + NS_FRAME_FIRST_REFLOW | NS_FRAME_IN_REFLOW)) { + return true; + } + + if (!GetTextRun(nsTextFrame::eInflated)) { + return false; + } + + TrimmedOffsets offsets = + GetTrimmedOffsets(TextFragment(), TrimmedOffsetFlags::NoTrimAfter); + return offsets.mLength != 0; +} |