/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* rendering object for textual content of elements */ #include "nsTextFrame.h" #include "gfx2DGlue.h" #include "gfxUtils.h" #include "mozilla/Attributes.h" #include "mozilla/CaretAssociationHint.h" #include "mozilla/ComputedStyle.h" #include "mozilla/DebugOnly.h" #include "mozilla/gfx/2D.h" #include "mozilla/Likely.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/PresShell.h" #include "mozilla/StaticPrefs_layout.h" #include "mozilla/StaticPresData.h" #include "mozilla/SVGTextFrame.h" #include "mozilla/TextEditor.h" #include "mozilla/TextEvents.h" #include "mozilla/BinarySearch.h" #include "mozilla/IntegerRange.h" #include "mozilla/Unused.h" #include "mozilla/PodOperations.h" #include "mozilla/dom/PerformanceMainThread.h" #include "nsCOMPtr.h" #include "nsBlockFrame.h" #include "nsFontMetrics.h" #include "nsSplittableFrame.h" #include "nsLineLayout.h" #include "nsString.h" #include "nsUnicharUtils.h" #include "nsPresContext.h" #include "nsIContent.h" #include "nsStyleConsts.h" #include "nsStyleStruct.h" #include "nsStyleStructInlines.h" #include "nsCoord.h" #include "gfxContext.h" #include "nsTArray.h" #include "nsCSSPseudoElements.h" #include "nsCSSFrameConstructor.h" #include "nsCompatibility.h" #include "nsCSSColorUtils.h" #include "nsLayoutUtils.h" #include "nsDisplayList.h" #include "nsIFrame.h" #include "nsIMathMLFrame.h" #include "nsFirstLetterFrame.h" #include "nsPlaceholderFrame.h" #include "nsTextFrameUtils.h" #include "nsTextPaintStyle.h" #include "nsTextRunTransformations.h" #include "MathMLTextRunFactory.h" #include "nsUnicodeProperties.h" #include "nsStyleUtil.h" #include "nsRubyFrame.h" #include "PresShellInlines.h" #include "TextDrawTarget.h" #include "nsTextFragment.h" #include "nsGkAtoms.h" #include "nsFrameSelection.h" #include "nsRange.h" #include "nsCSSRendering.h" #include "nsContentUtils.h" #include "nsLineBreaker.h" #include "nsIFrameInlines.h" #include "mozilla/intl/Bidi.h" #include "mozilla/intl/Segmenter.h" #include "mozilla/intl/UnicodeProperties.h" #include "mozilla/ServoStyleSet.h" #include #include #include #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 mWidths; }; namespace { struct TabwidthAdaptor { const nsTArray& mWidths; explicit TabwidthAdaptor(const nsTArray& aWidths) : mWidths(aWidths) {} uint32_t operator[](size_t aIdx) const { return mWidths[aIdx].mOffset; } }; } // namespace void nsTextFrame::TabWidthStore::ApplySpacing( gfxTextRun::PropertyProvider::Spacing* aSpacing, uint32_t aOffset, uint32_t aLength) { size_t i = 0; const size_t len = mWidths.Length(); // If aOffset is non-zero, do a binary search to find where to start // processing the tab widths, in case the list is really long. (See bug // 953247.) // We need to start from the first entry where mOffset >= aOffset. if (aOffset > 0) { mozilla::BinarySearch(TabwidthAdaptor(mWidths), 0, len, aOffset, &i); } uint32_t limit = aOffset + aLength; while (i < len) { const TabWidth& tw = mWidths[i]; if (tw.mOffset >= limit) { break; } aSpacing[tw.mOffset - aOffset].mAfter += tw.mWidth; i++; } } NS_DECLARE_FRAME_PROPERTY_DELETABLE(TabWidthProperty, nsTextFrame::TabWidthStore) NS_DECLARE_FRAME_PROPERTY_WITHOUT_DTOR(OffsetToFrameProperty, nsTextFrame) NS_DECLARE_FRAME_PROPERTY_RELEASABLE(UninflatedTextRunProperty, gfxTextRun) NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(FontSizeInflationProperty, float) NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(HangableWhitespaceProperty, nscoord) NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(TrimmableWhitespaceProperty, gfxTextRun::TrimmableWS) struct nsTextFrame::PaintTextSelectionParams : nsTextFrame::PaintTextParams { Point textBaselinePt; PropertyProvider* provider = nullptr; Range contentRange; nsTextPaintStyle* textPaintStyle = nullptr; Range glyphRange; explicit PaintTextSelectionParams(const PaintTextParams& aParams) : PaintTextParams(aParams) {} }; struct nsTextFrame::DrawTextRunParams { gfxContext* context; mozilla::gfx::PaletteCache& paletteCache; PropertyProvider* provider = nullptr; gfxFloat* advanceWidth = nullptr; mozilla::SVGContextPaint* contextPaint = nullptr; DrawPathCallbacks* callbacks = nullptr; nscolor textColor = NS_RGBA(0, 0, 0, 0); nscolor textStrokeColor = NS_RGBA(0, 0, 0, 0); nsAtom* fontPalette = nullptr; float textStrokeWidth = 0.0f; bool drawSoftHyphen = false; bool hasTextShadow = false; bool paintingShadows = false; DrawTextRunParams(gfxContext* aContext, mozilla::gfx::PaletteCache& aPaletteCache) : context(aContext), paletteCache(aPaletteCache) {} }; struct nsTextFrame::ClipEdges { ClipEdges(const nsIFrame* aFrame, const nsPoint& aToReferenceFrame, nscoord aVisIStartEdge, nscoord aVisIEndEdge) { nsRect r = aFrame->ScrollableOverflowRect() + aToReferenceFrame; if (aFrame->GetWritingMode().IsVertical()) { mVisIStart = aVisIStartEdge > 0 ? r.y + aVisIStartEdge : nscoord_MIN; mVisIEnd = aVisIEndEdge > 0 ? std::max(r.YMost() - aVisIEndEdge, mVisIStart) : nscoord_MAX; } else { mVisIStart = aVisIStartEdge > 0 ? r.x + aVisIStartEdge : nscoord_MIN; mVisIEnd = aVisIEndEdge > 0 ? std::max(r.XMost() - aVisIEndEdge, mVisIStart) : nscoord_MAX; } } void Intersect(nscoord* aVisIStart, nscoord* aVisISize) const { nscoord end = *aVisIStart + *aVisISize; *aVisIStart = std::max(*aVisIStart, mVisIStart); *aVisISize = std::max(std::min(end, mVisIEnd) - *aVisIStart, 0); } nscoord mVisIStart; nscoord mVisIEnd; }; struct nsTextFrame::DrawTextParams : nsTextFrame::DrawTextRunParams { Point framePt; LayoutDeviceRect dirtyRect; const nsTextPaintStyle* textStyle = nullptr; const ClipEdges* clipEdges = nullptr; const nscolor* decorationOverrideColor = nullptr; Range glyphRange; DrawTextParams(gfxContext* aContext, mozilla::gfx::PaletteCache& aPaletteCache) : DrawTextRunParams(aContext, aPaletteCache) {} }; struct nsTextFrame::PaintShadowParams { gfxTextRun::Range range; LayoutDeviceRect dirtyRect; Point framePt; Point textBaselinePt; gfxContext* context; DrawPathCallbacks* callbacks = nullptr; nscolor foregroundColor = NS_RGBA(0, 0, 0, 0); const ClipEdges* clipEdges = nullptr; PropertyProvider* provider = nullptr; nscoord leftSideOffset = 0; explicit PaintShadowParams(const PaintTextParams& aParams) : dirtyRect(aParams.dirtyRect), framePt(aParams.framePt), context(aParams.context) {} }; /** * A glyph observer for the change of a font glyph in a text run. * * This is stored in {Simple, Complex}TextRunUserData. */ class GlyphObserver final : public gfxFont::GlyphChangeObserver { public: GlyphObserver(gfxFont* aFont, gfxTextRun* aTextRun) : gfxFont::GlyphChangeObserver(aFont), mTextRun(aTextRun) { MOZ_ASSERT(aTextRun->GetUserData()); } void NotifyGlyphsChanged() override; private: gfxTextRun* mTextRun; }; static const nsFrameState TEXT_REFLOW_FLAGS = TEXT_FIRST_LETTER | TEXT_START_OF_LINE | TEXT_END_OF_LINE | TEXT_HYPHEN_BREAK | TEXT_TRIMMED_TRAILING_WHITESPACE | TEXT_JUSTIFICATION_ENABLED | TEXT_HAS_NONCOLLAPSED_CHARACTERS | TEXT_SELECTION_UNDERLINE_OVERFLOWED | TEXT_NO_RENDERED_GLYPHS; static const nsFrameState TEXT_WHITESPACE_FLAGS = TEXT_IS_ONLY_WHITESPACE | TEXT_ISNOT_ONLY_WHITESPACE; /* * Some general notes * * Text frames delegate work to gfxTextRun objects. The gfxTextRun object * transforms text to positioned glyphs. It can report the geometry of the * glyphs and paint them. Text frames configure gfxTextRuns by providing text, * spacing, language, and other information. * * A gfxTextRun can cover more than one DOM text node. This is necessary to * get kerning, ligatures and shaping for text that spans multiple text nodes * but is all the same font. * * The userdata for a gfxTextRun object can be: * * - A nsTextFrame* in the case a text run maps to only one flow. In this * case, the textrun's user data pointer is a pointer to mStartFrame for that * flow, mDOMOffsetToBeforeTransformOffset is zero, and mContentLength is the * length of the text node. * * - A SimpleTextRunUserData in the case a text run maps to one flow, but we * still have to keep a list of glyph observers. * * - A ComplexTextRunUserData in the case a text run maps to multiple flows, * but we need to keep a list of glyph observers. * * - A TextRunUserData in the case a text run maps multiple flows, but it * doesn't have any glyph observer for changes in SVG fonts. * * You can differentiate between the four different cases with the * IsSimpleFlow and MightHaveGlyphChanges flags. * * We go to considerable effort to make sure things work even if in-flow * siblings have different ComputedStyles (i.e., first-letter and first-line). * * Our convention is that unsigned integer character offsets are offsets into * the transformed string. Signed integer character offsets are offsets into * the DOM string. * * XXX currently we don't handle hyphenated breaks between text frames where the * hyphen occurs at the end of the first text frame, e.g. * Kit­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> 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> mGlyphObservers; }; static TextRunUserData* CreateUserData(uint32_t aMappedFlowCount) { TextRunUserData* data = static_cast(moz_xmalloc( sizeof(TextRunUserData) + aMappedFlowCount * sizeof(TextRunMappedFlow))); #ifdef DEBUG data->mMappedFlows = reinterpret_cast(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( moz_xmalloc(sizeof(ComplexTextRunUserData) + aMappedFlowCount * sizeof(TextRunMappedFlow))); new (data) ComplexTextRunUserData(); #ifdef DEBUG data->mMappedFlows = reinterpret_cast(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(aTextRun->GetUserData()); } } else { if (aTextRun->GetFlags2() & nsTextFrameUtils::Flags::MightHaveGlyphChanges) { DestroyComplexUserData( static_cast(aTextRun->GetUserData())); } else { DestroyUserData(static_cast(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( static_cast(aTextRun->GetUserData()) + 1); } else { flows = reinterpret_cast( static_cast(aTextRun->GetUserData()) + 1); } MOZ_ASSERT( static_cast(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(aTextRun->GetUserData())->mFrame; } return static_cast(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 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(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 , 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( 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(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( mContent->GetProperty(nsGkAtoms::flowlength)) : nullptr; MOZ_ASSERT(mContent->HasFlag(NS_HAS_FLOWLENGTH_PROPERTY) == !!flowLength, "incorrect NS_HAS_FLOWLENGTH_PROPERTY flag"); /** * This frame must start inside the cached flow. If the flow starts at * mContentOffset but this frame is empty, logically it might be before the * start of the cached flow. */ if (flowLength && (flowLength->mStartOffset < mContentOffset || (flowLength->mStartOffset == mContentOffset && GetContentEnd() > mContentOffset)) && flowLength->mEndFlowOffset > mContentOffset) { #ifdef DEBUG NS_ASSERTION(flowLength->mEndFlowOffset >= GetContentEnd(), "frame crosses fixed continuation boundary"); #endif return flowLength->mEndFlowOffset - mContentOffset; } nsTextFrame* nextBidi = LastInFlow()->GetNextContinuation(); int32_t endFlow = nextBidi ? nextBidi->GetContentOffset() : GetContent()->TextLength(); if (!flowLength) { flowLength = new FlowLengthProperty; if (NS_FAILED(mContent->SetProperty( nsGkAtoms::flowlength, flowLength, nsINode::DeleteProperty))) { delete flowLength; flowLength = nullptr; } else { mContent->SetFlags(NS_HAS_FLOWLENGTH_PROPERTY); } } if (flowLength) { flowLength->mStartOffset = mContentOffset; flowLength->mEndFlowOffset = endFlow; } return endFlow - mContentOffset; } // Smarter versions of dom::IsSpaceCharacter. // Unicode is really annoying; sometimes a space character isn't whitespace --- // when it combines with another character // So we have several versions of IsSpace for use in different contexts. static bool IsSpaceCombiningSequenceTail(const nsTextFragment* aFrag, uint32_t aPos) { NS_ASSERTION(aPos <= aFrag->GetLength(), "Bad offset"); if (!aFrag->Is2b()) { return false; } return nsTextFrameUtils::IsSpaceCombiningSequenceTail( aFrag->Get2b() + aPos, aFrag->GetLength() - aPos); } // Check whether aPos is a space for CSS 'word-spacing' purposes static bool IsCSSWordSpacingSpace(const nsTextFragment* aFrag, uint32_t aPos, const nsTextFrame* aFrame, const nsStyleText* aStyleText) { NS_ASSERTION(aPos < aFrag->GetLength(), "No text for IsSpace!"); char16_t ch = aFrag->CharAt(aPos); switch (ch) { case ' ': case CH_NBSP: return !IsSpaceCombiningSequenceTail(aFrag, aPos + 1); case '\r': case '\t': return !aStyleText->WhiteSpaceIsSignificant(); case '\n': return !aStyleText->NewlineIsSignificant(aFrame); default: return false; } } constexpr char16_t kOghamSpaceMark = 0x1680; // Check whether the string aChars/aLength starts with space that's // trimmable according to CSS 'white-space:normal/nowrap'. static bool IsTrimmableSpace(const char16_t* aChars, uint32_t aLength) { NS_ASSERTION(aLength > 0, "No text for IsSpace!"); char16_t ch = *aChars; if (ch == ' ' || ch == kOghamSpaceMark) { return !nsTextFrameUtils::IsSpaceCombiningSequenceTail(aChars + 1, aLength - 1); } return ch == '\t' || ch == '\f' || ch == '\n' || ch == '\r'; } // Check whether the character aCh is trimmable according to CSS // 'white-space:normal/nowrap' static bool IsTrimmableSpace(char aCh) { return aCh == ' ' || aCh == '\t' || aCh == '\f' || aCh == '\n' || aCh == '\r'; } static bool IsTrimmableSpace(const nsTextFragment* aFrag, uint32_t aPos, const nsStyleText* aStyleText, bool aAllowHangingWS = false) { NS_ASSERTION(aPos < aFrag->GetLength(), "No text for IsSpace!"); switch (aFrag->CharAt(aPos)) { case ' ': case kOghamSpaceMark: return (!aStyleText->WhiteSpaceIsSignificant() || aAllowHangingWS) && !IsSpaceCombiningSequenceTail(aFrag, aPos + 1); case '\n': return !aStyleText->NewlineIsSignificantStyle() && aStyleText->mWhiteSpaceCollapse != StyleWhiteSpaceCollapse::PreserveSpaces; case '\t': case '\r': case '\f': return !aStyleText->WhiteSpaceIsSignificant() || aAllowHangingWS; default: return false; } } static bool IsSelectionInlineWhitespace(const nsTextFragment* aFrag, uint32_t aPos) { NS_ASSERTION(aPos < aFrag->GetLength(), "No text for IsSelectionInlineWhitespace!"); char16_t ch = aFrag->CharAt(aPos); if (ch == ' ' || ch == CH_NBSP) { return !IsSpaceCombiningSequenceTail(aFrag, aPos + 1); } return ch == '\t' || ch == '\f'; } static bool IsSelectionNewline(const nsTextFragment* aFrag, uint32_t aPos) { NS_ASSERTION(aPos < aFrag->GetLength(), "No text for IsSelectionNewline!"); char16_t ch = aFrag->CharAt(aPos); return ch == '\n' || ch == '\r'; } // Count the amount of trimmable whitespace (as per CSS // 'white-space:normal/nowrap') in a text fragment. The first // character is at offset aStartOffset; the maximum number of characters // to check is aLength. aDirection is -1 or 1 depending on whether we should // progress backwards or forwards. static uint32_t GetTrimmableWhitespaceCount(const nsTextFragment* aFrag, int32_t aStartOffset, int32_t aLength, int32_t aDirection) { if (!aLength) { return 0; } int32_t count = 0; if (aFrag->Is2b()) { const char16_t* str = aFrag->Get2b() + aStartOffset; int32_t fragLen = aFrag->GetLength() - aStartOffset; for (; count < aLength; ++count) { if (!IsTrimmableSpace(str, fragLen)) { break; } str += aDirection; fragLen -= aDirection; } } else { const char* str = aFrag->Get1b() + aStartOffset; for (; count < aLength; ++count) { if (!IsTrimmableSpace(*str)) { break; } str += aDirection; } } return count; } static bool IsAllWhitespace(const nsTextFragment* aFrag, bool aAllowNewline) { if (aFrag->Is2b()) { return false; } int32_t len = aFrag->GetLength(); const char* str = aFrag->Get1b(); for (int32_t i = 0; i < len; ++i) { char ch = str[i]; if (ch == ' ' || ch == '\t' || ch == '\r' || (ch == '\n' && aAllowNewline)) { continue; } return false; } return true; } static void ClearObserversFromTextRun(gfxTextRun* aTextRun) { if (!(aTextRun->GetFlags2() & nsTextFrameUtils::Flags::MightHaveGlyphChanges)) { return; } if (aTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsSimpleFlow) { static_cast(aTextRun->GetUserData()) ->mGlyphObservers.Clear(); } else { static_cast(aTextRun->GetUserData()) ->mGlyphObservers.Clear(); } } static void CreateObserversForAnimatedGlyphs(gfxTextRun* aTextRun) { if (!aTextRun->GetUserData()) { return; } ClearObserversFromTextRun(aTextRun); nsTArray 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>* 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(aTextRun->GetUserData()); aTextRun->SetUserData(new SimpleTextRunUserData(frame)); } auto data = static_cast(aTextRun->GetUserData()); observers = &data->mGlyphObservers; } else { if (!(aTextRun->GetFlags2() & nsTextFrameUtils::Flags::MightHaveGlyphChanges)) { auto oldData = static_cast(aTextRun->GetUserData()); TextRunMappedFlow* oldMappedFlows = GetMappedFlows(aTextRun); ComplexTextRunUserData* data = CreateComplexUserData(oldData->mMappedFlowCount); TextRunMappedFlow* dataMappedFlows = reinterpret_cast(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(aTextRun->GetUserData()); observers = &data->mGlyphObservers; } aTextRun->SetFlagBits(nsTextFrameUtils::Flags::MightHaveGlyphChanges); observers->SetCapacity(observers->Length() + fontsWithAnimatedGlyphs.Length()); for (auto font : fontsWithAnimatedGlyphs) { observers->AppendElement(MakeUnique(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 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& 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(mTextRun.get()); transformedTextRun->SetCapitalization(aOffset + mOffsetIntoTextRun, aLength, aCapitalize); } } void Finish(gfxMissingFontRecorder* aMFR) { if (mTextRun->GetFlags2() & nsTextFrameUtils::Flags::IsTransformed) { nsTransformedTextRun* transformedTextRun = static_cast(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 mTextRun; DrawTarget* mDrawTarget; uint32_t mOffsetIntoTextRun; }; private: AutoTArray mMappedFlows; AutoTArray mLineBreakBeforeFrames; AutoTArray, 10> mBreakSinks; nsLineBreaker mLineBreaker; RefPtr mCurrentFramesAllSameTextRun; DrawTarget* mDrawTarget; nsIFrame* mLineContainer; nsTextFrame* mLastFrame; // The common ancestor of the current frame and the previous leaf frame // on the line, or null if there was no previous leaf frame. nsIFrame* mCommonAncestorWithLastFrame; gfxMissingFontRecorder* mMissingFonts; // mMaxTextLength is an upper bound on the size of the text in all mapped // frames The value UINT32_MAX represents overflow; text will be discarded uint32_t mMaxTextLength; bool mDoubleByteText; bool mBidiEnabled; bool mStartOfLine; bool mSkipIncompleteTextRuns; bool mCanStopOnThisLine; bool mDoLineBreaking; nsTextFrame::TextRunType mWhichTextRun; uint8_t mNextRunContextInfo; uint8_t mCurrentRunContextInfo; }; static const nsIFrame* FindLineContainer(const nsIFrame* aFrame) { while (aFrame && (aFrame->IsLineParticipant() || aFrame->CanContinueTextRun())) { aFrame = aFrame->GetParent(); } return aFrame; } static nsIFrame* FindLineContainer(nsIFrame* aFrame) { return const_cast( FindLineContainer(const_cast(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(aText); for (uint32_t i = 0; i < aLength; ++i) { if (IsLineBreakingWhiteSpace(chars[i])) { return true; } } return false; } else { const uint8_t* chars = static_cast(aText); for (uint32_t i = 0; i < aLength; ++i) { if (IsLineBreakingWhiteSpace(chars[i])) { return true; } } return false; } } static nsTextFrameUtils::CompressionMode GetCSSWhitespaceToCompressionMode( nsTextFrame* aFrame, const nsStyleText* aStyleText) { switch (aStyleText->mWhiteSpaceCollapse) { case StyleWhiteSpaceCollapse::Collapse: return nsTextFrameUtils::COMPRESS_WHITESPACE_NEWLINE; case StyleWhiteSpaceCollapse::PreserveBreaks: return nsTextFrameUtils::COMPRESS_WHITESPACE; case StyleWhiteSpaceCollapse::Preserve: case StyleWhiteSpaceCollapse::PreserveSpaces: case StyleWhiteSpaceCollapse::BreakSpaces: if (!aStyleText->NewlineIsSignificant(aFrame)) { // If newline is set to be preserved, but then suppressed, // transform newline to space. return nsTextFrameUtils::COMPRESS_NONE_TRANSFORM_TO_SPACE; } return nsTextFrameUtils::COMPRESS_NONE; } MOZ_ASSERT_UNREACHABLE("Unknown white-space-collapse value"); return nsTextFrameUtils::COMPRESS_WHITESPACE_NEWLINE; } struct FrameTextTraversal { FrameTextTraversal() : mFrameToScan(nullptr), mOverflowFrameToScan(nullptr), mScanSiblings(false), mLineBreakerCanCrossFrameBoundary(false), mTextRunCanCrossFrameBoundary(false) {} // These fields identify which frames should be recursively scanned // The first normal frame to scan (or null, if no such frame should be // scanned) nsIFrame* mFrameToScan; // The first overflow frame to scan (or null, if no such frame should be // scanned) nsIFrame* mOverflowFrameToScan; // Whether to scan the siblings of // mFrameToDescendInto/mOverflowFrameToDescendInto bool mScanSiblings; // These identify the boundaries of the context required for // line breaking or textrun construction bool mLineBreakerCanCrossFrameBoundary; bool mTextRunCanCrossFrameBoundary; nsIFrame* NextFrameToScan() { nsIFrame* f; if (mFrameToScan) { f = mFrameToScan; mFrameToScan = mScanSiblings ? f->GetNextSibling() : nullptr; } else if (mOverflowFrameToScan) { f = mOverflowFrameToScan; mOverflowFrameToScan = mScanSiblings ? f->GetNextSibling() : nullptr; } else { f = nullptr; } return f; } }; static FrameTextTraversal CanTextCrossFrameBoundary(nsIFrame* aFrame) { FrameTextTraversal result; bool continuesTextRun = aFrame->CanContinueTextRun(); if (aFrame->IsPlaceholderFrame()) { // placeholders are "invisible", so a text run should be able to span // across one. But don't descend into the out-of-flow. result.mLineBreakerCanCrossFrameBoundary = true; if (continuesTextRun) { // ... Except for first-letter floats, which are really in-flow // from the point of view of capitalization etc, so we'd better // descend into them. But we actually need to break the textrun for // first-letter floats since things look bad if, say, we try to make a // ligature across the float boundary. result.mFrameToScan = (static_cast(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(aFrame) : nullptr; if (textFrame) { if (aState->mLastTextFrame && textFrame != aState->mLastTextFrame->GetNextInFlow() && !ContinueTextRunAcrossFrames(aState->mLastTextFrame, textFrame)) { aState->mSeenTextRunBoundaryOnThisLine = true; if (aState->mSeenSpaceForLineBreakingOnThisLine) { return FB_FOUND_VALID_TEXTRUN_BOUNDARY; } } if (!aState->mFirstTextFrame) { aState->mFirstTextFrame = textFrame; } aState->mLastTextFrame = textFrame; } if (aFrame == aState->mStopAtFrame) { return FB_STOPPED_AT_STOP_FRAME; } if (textFrame) { if (aState->mSeenSpaceForLineBreakingOnThisLine) { return FB_CONTINUE; } const nsTextFragment* frag = textFrame->TextFragment(); uint32_t start = textFrame->GetContentOffset(); uint32_t length = textFrame->GetContentLength(); const void* text; const nsAtom* language = textFrame->StyleFont()->mLanguage; if (frag->Is2b()) { // It is possible that we may end up removing all whitespace in // a piece of text because of The White Space Processing Rules, // so we need to transform it before we can check existence of // such whitespaces. aState->mBuffer.EnsureLengthAtLeast(length); nsTextFrameUtils::CompressionMode compression = GetCSSWhitespaceToCompressionMode(textFrame, textFrame->StyleText()); uint8_t incomingFlags = 0; gfxSkipChars skipChars; nsTextFrameUtils::Flags analysisFlags; char16_t* bufStart = aState->mBuffer.Elements(); char16_t* bufEnd = nsTextFrameUtils::TransformText( frag->Get2b() + start, length, bufStart, compression, &incomingFlags, &skipChars, &analysisFlags, language); text = bufStart; length = bufEnd - bufStart; } else { // If the text only contains ASCII characters, it is currently // impossible that TransformText would remove all whitespaces, // and thus the check below should return the same result for // transformed text and original text. So we don't need to try // transforming it here. text = static_cast(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 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(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 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 buffer; uint32_t bufferSize = mMaxTextLength * (mDoubleByteText ? 2 : 1); if (bufferSize < mMaxTextLength || bufferSize == UINT32_MAX || !buffer.AppendElements(bufferSize, fallible)) { return; } textRun = BuildTextRunForFrames(buffer.Elements()); } } if (aFlushLineBreaks) { FlushLineBreaks(aSuppressTrailingBreak ? nullptr : textRun.get()); if (!mDoLineBreaking && textRun) { CreateObserversForAnimatedGlyphs(textRun.get()); } } mCanStopOnThisLine = true; ResetRunInfo(); } void BuildTextRunsScanner::FlushLineBreaks(gfxTextRun* aTrailingTextRun) { // If the line-breaker is buffering a potentially-unfinished word, // preserve the state of being in-word so that we don't spuriously // capitalize the next letter. bool inWord = mLineBreaker.InWord(); bool trailingLineBreak; nsresult rv = mLineBreaker.Reset(&trailingLineBreak); mLineBreaker.SetWordContinuation(inWord); // textRun may be null for various reasons, including because we constructed // a partial textrun just to get the linebreaker and other state set up // to build the next textrun. if (NS_SUCCEEDED(rv) && trailingLineBreak && aTrailingTextRun) { aTrailingTextRun->SetFlagBits(nsTextFrameUtils::Flags::HasTrailingBreak); } for (uint32_t i = 0; i < mBreakSinks.Length(); ++i) { // TODO cause frames associated with the textrun to be reflowed, if they // aren't being reflowed already! mBreakSinks[i]->Finish(mMissingFonts); } mBreakSinks.Clear(); } void BuildTextRunsScanner::AccumulateRunInfo(nsTextFrame* aFrame) { if (mMaxTextLength != UINT32_MAX) { NS_ASSERTION(mMaxTextLength < UINT32_MAX - aFrame->GetContentLength(), "integer overflow"); if (mMaxTextLength >= UINT32_MAX - aFrame->GetContentLength()) { mMaxTextLength = UINT32_MAX; } else { mMaxTextLength += aFrame->GetContentLength(); } } mDoubleByteText |= aFrame->TextFragment()->Is2b(); mLastFrame = aFrame; mCommonAncestorWithLastFrame = aFrame->GetParent(); MappedFlow* mappedFlow = &mMappedFlows[mMappedFlows.Length() - 1]; NS_ASSERTION(mappedFlow->mStartFrame == aFrame || mappedFlow->GetContentEnd() == aFrame->GetContentOffset(), "Overlapping or discontiguous frames => BAD"); mappedFlow->mEndFrame = aFrame->GetNextContinuation(); if (mCurrentFramesAllSameTextRun != aFrame->GetTextRun(mWhichTextRun)) { mCurrentFramesAllSameTextRun = nullptr; } if (mStartOfLine) { mLineBreakBeforeFrames.AppendElement(aFrame); mStartOfLine = false; } // Default limits used by `hyphenate-limit-chars` for `auto` components, as // suggested by the CSS Text spec. // TODO: consider making these sensitive to the context, e.g. increasing the // values for long line lengths to reduce the tendency to hyphenate too much. const uint32_t kDefaultHyphenateTotalWordLength = 5; const uint32_t kDefaultHyphenatePreBreakLength = 2; const uint32_t kDefaultHyphenatePostBreakLength = 2; const auto& hyphenateLimitChars = aFrame->StyleText()->mHyphenateLimitChars; uint32_t pre = hyphenateLimitChars.pre_hyphen_length.IsAuto() ? kDefaultHyphenatePreBreakLength : std::max(0, hyphenateLimitChars.pre_hyphen_length.AsNumber()); uint32_t post = hyphenateLimitChars.post_hyphen_length.IsAuto() ? kDefaultHyphenatePostBreakLength : std::max(0, hyphenateLimitChars.post_hyphen_length.AsNumber()); uint32_t total = hyphenateLimitChars.total_word_length.IsAuto() ? kDefaultHyphenateTotalWordLength : std::max(0, hyphenateLimitChars.total_word_length.AsNumber()); total = std::max(total, pre + post); mLineBreaker.SetHyphenateLimitChars(total, pre, post); } static bool HasTerminalNewline(const nsTextFrame* aFrame) { if (aFrame->GetContentLength() == 0) { return false; } const nsTextFragment* frag = aFrame->TextFragment(); return frag->CharAt(AssertedCast(aFrame->GetContentEnd()) - 1) == '\n'; } static gfxFont::Metrics GetFirstFontMetrics(gfxFontGroup* aFontGroup, bool aVerticalMetrics) { if (!aFontGroup) { return gfxFont::Metrics(); } RefPtr 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(container)->GetFontSizeScaleFactor(); } static nscoord LetterSpacing(nsIFrame* aFrame, const nsStyleText& aStyleText) { if (aFrame->IsInSVGTextSubtree()) { // SVG text can have a scaling factor applied so that very small or very // large font-sizes don't suffer from poor glyph placement due to app unit // rounding. The used letter-spacing value must be scaled by the same // factor. Unlike word-spacing (below), this applies to both lengths and // percentages, as the percentage basis is 1em, not an already-scaled glyph // dimension. return GetSVGFontSizeScaleFactor(aFrame) * aStyleText.mLetterSpacing.Resolve( [&] { return aFrame->StyleFont()->mSize.ToAppUnits(); }); } return aStyleText.mLetterSpacing.Resolve( [&] { return aFrame->StyleFont()->mSize.ToAppUnits(); }); } // This function converts non-coord values (e.g. percentages) to nscoord. static nscoord WordSpacing(nsIFrame* aFrame, const gfxTextRun* aTextRun, const nsStyleText& aStyleText) { if (aFrame->IsInSVGTextSubtree()) { // SVG text can have a scaling factor applied so that very small or very // large font-sizes don't suffer from poor glyph placement due to app unit // rounding. The used word-spacing value must be scaled by the same // factor, although any percentage basis has already effectively been // scaled, since it's the space glyph width, which is based on the already- // scaled font-size. auto spacing = aStyleText.mWordSpacing; spacing.ScaleLengthsBy(GetSVGFontSizeScaleFactor(aFrame)); return spacing.Resolve([&] { return GetSpaceWidthAppUnits(aTextRun); }); } return aStyleText.mWordSpacing.Resolve( [&] { return GetSpaceWidthAppUnits(aTextRun); }); } // Returns gfxTextRunFactory::TEXT_ENABLE_SPACING if non-standard // letter-spacing or word-spacing is present. static gfx::ShapedTextFlags GetSpacingFlags( nsIFrame* aFrame, const nsStyleText* aStyleText = nullptr) { const nsStyleText* styleText = aFrame->StyleText(); const auto& ls = styleText->mLetterSpacing; const auto& ws = styleText->mWordSpacing; // It's possible to have a calc() value that computes to zero but for which // IsDefinitelyZero() is false, in which case we'll return // TEXT_ENABLE_SPACING unnecessarily. That's ok because such cases are likely // to be rare, and avoiding TEXT_ENABLE_SPACING is just an optimization. bool nonStandardSpacing = !ls.IsDefinitelyZero() || !ws.IsDefinitelyZero(); return nonStandardSpacing ? gfx::ShapedTextFlags::TEXT_ENABLE_SPACING : gfx::ShapedTextFlags(); } bool BuildTextRunsScanner::ContinueTextRunAcrossFrames(nsTextFrame* aFrame1, nsTextFrame* aFrame2) { // We don't need to check font size inflation, since // |FindLineContainer| above (via |nsIFrame::CanContinueTextRun|) // ensures that text runs never cross block boundaries. This means // that the font size inflation on all text frames in the text run is // already guaranteed to be the same as each other (and for the line // container). if (mBidiEnabled) { FrameBidiData data1 = aFrame1->GetBidiData(); FrameBidiData data2 = aFrame2->GetBidiData(); if (data1.embeddingLevel != data2.embeddingLevel || data2.precedingControl != kBidiLevelNone) { return false; } } ComputedStyle* sc1 = aFrame1->Style(); ComputedStyle* sc2 = aFrame2->Style(); // Any difference in writing-mode/directionality inhibits shaping across // the boundary. WritingMode wm(sc1); if (wm != WritingMode(sc2)) { return false; } const nsStyleText* textStyle1 = sc1->StyleText(); // If the first frame ends in a preformatted newline, then we end the textrun // here. This avoids creating giant textruns for an entire plain text file. // Note that we create a single text frame for a preformatted text node, // even if it has newlines in it, so typically we won't see trailing newlines // until after reflow has broken up the frame into one (or more) frames per // line. That's OK though. if (textStyle1->NewlineIsSignificant(aFrame1) && HasTerminalNewline(aFrame1)) { return false; } if (aFrame1->GetParent()->GetContent() != aFrame2->GetParent()->GetContent()) { // Does aFrame, or any ancestor between it and aAncestor, have a property // that should inhibit cross-element-boundary shaping on aSide? auto PreventCrossBoundaryShaping = [](const nsIFrame* aFrame, const nsIFrame* aAncestor, Side aSide) { while (aFrame != aAncestor) { ComputedStyle* ctx = aFrame->Style(); const auto positionProperty = ctx->StyleDisplay()->mPosition; // 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()->GetMargin(aSide, positionProperty); if (!margin->ConvertsToLength() || margin->AsLengthPercentage().ToLength() != 0) { return true; } const auto& padding = ctx->StylePadding()->mPadding.Get(aSide); if (!padding.ConvertsToLength() || padding.ToLength() != 0) { return true; } if (ctx->StyleBorder()->GetComputedBorderWidth(aSide) != 0) { return true; } // 2. vertical-align is not baseline. // // FIXME: Should this use VerticalAlignEnum()? const auto& verticalAlign = ctx->StyleDisplay()->mVerticalAlign; if (!verticalAlign.IsKeyword() || verticalAlign.AsKeyword() != StyleVerticalAlignKeyword::Baseline) { return true; } // 3. The boundary is a bidi isolation boundary. const auto unicodeBidi = ctx->StyleTextReset()->mUnicodeBidi; if (unicodeBidi == StyleUnicodeBidi::Isolate || unicodeBidi == StyleUnicodeBidi::IsolateOverride) { return true; } aFrame = aFrame->GetParent(); } return false; }; const nsIFrame* ancestor = nsLayoutUtils::FindNearestCommonAncestorFrameWithinBlock(aFrame1, aFrame2); if (!ancestor) { // The two frames are within different blocks, e.g. due to block // fragmentation. In theory we shouldn't prevent cross-frame shaping // here, but it's an edge case where we should rarely decide to allow // cross-frame shaping, so we don't try harder here. return false; } // We inhibit cross-element-boundary shaping if we're in SVG content, // as there are too many things SVG might be doing (like applying per- // element positioning) that wouldn't make sense with shaping across // the boundary. if (ancestor->IsInSVGTextSubtree()) { return false; } // Map inline-end and inline-start to physical sides for checking presence // of non-zero margin/border/padding. Side side1 = wm.PhysicalSide(LogicalSide::IEnd); Side side2 = wm.PhysicalSide(LogicalSide::IStart); // If the frames have an embedding level that is opposite to the writing // mode, we need to swap which sides we're checking. if (aFrame1->GetEmbeddingLevel().IsRTL() == wm.IsBidiLTR()) { std::swap(side1, side2); } if (PreventCrossBoundaryShaping(aFrame1, ancestor, side1) || PreventCrossBoundaryShaping(aFrame2, ancestor, side2)) { return false; } } if (aFrame1->GetContent() == aFrame2->GetContent() && aFrame1->GetNextInFlow() != aFrame2) { // aFrame2 must be a non-fluid continuation of aFrame1. This can happen // sometimes when the unicode-bidi property is used; the bidi resolver // breaks text into different frames even though the text has the same // direction. We can't allow these two frames to share the same textrun // because that would violate our invariant that two flows in the same // textrun have different content elements. return false; } if (sc1 == sc2) { return true; } const nsStyleText* textStyle2 = sc2->StyleText(); if (textStyle1->mTextTransform != textStyle2->mTextTransform || textStyle1->EffectiveWordBreak() != textStyle2->EffectiveWordBreak() || textStyle1->mLineBreak != textStyle2->mLineBreak) { return false; } nsPresContext* pc = aFrame1->PresContext(); MOZ_ASSERT(pc == aFrame2->PresContext()); const nsStyleFont* fontStyle1 = sc1->StyleFont(); const nsStyleFont* fontStyle2 = sc2->StyleFont(); nscoord letterSpacing1 = LetterSpacing(aFrame1, *textStyle1); nscoord letterSpacing2 = LetterSpacing(aFrame2, *textStyle2); return fontStyle1->mFont == fontStyle2->mFont && fontStyle1->mLanguage == fontStyle2->mLanguage && nsLayoutUtils::GetTextRunFlagsForStyle(sc1, pc, fontStyle1, textStyle1, letterSpacing1) == nsLayoutUtils::GetTextRunFlagsForStyle(sc2, pc, fontStyle2, textStyle2, letterSpacing2); } void BuildTextRunsScanner::ScanFrame(nsIFrame* aFrame) { LayoutFrameType frameType = aFrame->Type(); if (frameType == LayoutFrameType::RubyTextContainer) { // Don't include any ruby text container into the text run. return; } // First check if we can extend the current mapped frame block. This is // common. if (mMappedFlows.Length() > 0) { MappedFlow* mappedFlow = &mMappedFlows[mMappedFlows.Length() - 1]; if (mappedFlow->mEndFrame == aFrame && aFrame->HasAnyStateBits(NS_FRAME_IS_FLUID_CONTINUATION)) { NS_ASSERTION(frameType == LayoutFrameType::Text, "Flow-sibling of a text frame is not a text frame?"); // Don't do this optimization if mLastFrame has a terminal newline... // it's quite likely preformatted and we might want to end the textrun // here. This is almost always true: if (mLastFrame->Style() == aFrame->Style() && !HasTerminalNewline(mLastFrame)) { AccumulateRunInfo(static_cast(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(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(mLineBreakBeforeFrames.ElementAt(index)); } static gfxFontGroup* GetFontGroupForFrame( const nsIFrame* aFrame, float aFontSizeInflation, nsFontMetrics** aOutFontMetrics = nullptr) { RefPtr 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 CreateReferenceDrawTarget( const nsTextFrame* aTextFrame) { UniquePtr ctx = aTextFrame->PresShell()->CreateReferenceRenderingContext(); RefPtr dt = ctx->GetDrawTarget(); return dt.forget(); } static already_AddRefed GetHyphenTextRun(nsTextFrame* aTextFrame, DrawTarget* aDrawTarget) { RefPtr dt = aDrawTarget; if (!dt) { dt = CreateReferenceDrawTarget(aTextFrame); if (!dt) { return nullptr; } } RefPtr 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 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 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(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 use 'math' script. if (!(parent && parent->GetContent() && parent->GetContent()->IsMathMLElement(nsGkAtoms::mtext))) { flags |= gfx::ShapedTextFlags::TEXT_USE_MATH_SCRIPT; } nsIMathMLFrame* mathFrame = do_QueryFrame(parent); if (mathFrame) { nsPresentationData presData; mathFrame->GetPresentationData(presData); if (NS_MATHML_IS_DTLS_SET(presData.flags)) { mathFlags |= MathMLTextRunFactory::MATH_FONT_FEATURE_DTLS; anyMathMLStyling = true; } } } nsIFrame* child = mLineContainer; uint8_t oldScriptLevel = 0; while (parent && child->HasAnyStateBits(NS_FRAME_MATHML_SCRIPT_DESCENDANT)) { // Reconstruct the script level ignoring any user overrides. It is // calculated this way instead of using scriptlevel to ensure the // correct ssty font feature setting is used even if the user sets a // different (especially negative) scriptlevel. nsIMathMLFrame* mathFrame = do_QueryFrame(parent); if (mathFrame) { sstyScriptLevel += mathFrame->ScriptIncrement(child); } if (sstyScriptLevel < oldScriptLevel) { // overflow sstyScriptLevel = UINT8_MAX; break; } child = parent; parent = parent->GetParent(); oldScriptLevel = sstyScriptLevel; } if (sstyScriptLevel) { anyMathMLStyling = true; } // Figure out what content is included in this flow. nsIContent* content = f->GetContent(); const nsTextFragment* frag = f->TextFragment(); int32_t contentStart = mappedFlow->mStartFrame->GetContentOffset(); int32_t contentEnd = mappedFlow->GetContentEnd(); int32_t contentLength = contentEnd - contentStart; const nsAtom* language = f->StyleFont()->mLanguage; TextRunMappedFlow* newFlow = &userMappedFlows[i]; newFlow->mStartFrame = mappedFlow->mStartFrame; newFlow->mDOMOffsetToBeforeTransformOffset = skipChars.GetOriginalCharCount() - mappedFlow->mStartFrame->GetContentOffset(); newFlow->mContentLength = contentLength; while (nextBreakBeforeFrame && nextBreakBeforeFrame->GetContent() == content) { textBreakPoints.AppendElement(nextBreakBeforeFrame->GetContentOffset() + newFlow->mDOMOffsetToBeforeTransformOffset); nextBreakBeforeFrame = GetNextBreakBeforeFrame(&nextBreakIndex); } nsTextFrameUtils::Flags analysisFlags; if (frag->Is2b()) { NS_ASSERTION(mDoubleByteText, "Wrong buffer char size!"); char16_t* bufStart = static_cast(aTextBuffer); char16_t* bufEnd = nsTextFrameUtils::TransformText( frag->Get2b() + contentStart, contentLength, bufStart, compression, &mNextRunContextInfo, &skipChars, &analysisFlags, language); aTextBuffer = bufEnd; currentTransformedTextOffset = bufEnd - static_cast(textPtr); } else { if (mDoubleByteText) { // Need to expand the text. First transform it into a temporary buffer, // then expand. AutoTArray tempBuf; uint8_t* bufStart = tempBuf.AppendElements(contentLength, fallible); if (!bufStart) { DestroyUserData(userDataToDestroy); return nullptr; } uint8_t* end = nsTextFrameUtils::TransformText( reinterpret_cast(frag->Get1b()) + contentStart, contentLength, bufStart, compression, &mNextRunContextInfo, &skipChars, &analysisFlags, language); aTextBuffer = ExpandBuffer(static_cast(aTextBuffer), tempBuf.Elements(), end - tempBuf.Elements()); currentTransformedTextOffset = static_cast(aTextBuffer) - static_cast(textPtr); } else { uint8_t* bufStart = static_cast(aTextBuffer); uint8_t* end = nsTextFrameUtils::TransformText( reinterpret_cast(frag->Get1b()) + contentStart, contentLength, bufStart, compression, &mNextRunContextInfo, &skipChars, &analysisFlags, language); aTextBuffer = end; currentTransformedTextOffset = end - static_cast(textPtr); } } flags2 |= analysisFlags; } void* finalUserData; if (userData == &dummyData) { flags2 |= nsTextFrameUtils::Flags::IsSimpleFlow; userData = nullptr; finalUserData = mMappedFlows[0].mStartFrame; } else { finalUserData = userData; } uint32_t transformedLength = currentTransformedTextOffset; // Now build the textrun nsTextFrame* firstFrame = mMappedFlows[0].mStartFrame; float fontInflation; gfxFontGroup* fontGroup; if (mWhichTextRun == nsTextFrame::eNotInflated) { fontInflation = 1.0f; fontGroup = GetFontGroupForFrame(firstFrame, fontInflation); } else { fontInflation = nsLayoutUtils::FontSizeInflationFor(firstFrame); fontGroup = GetInflatedFontGroupForFrame(firstFrame); } MOZ_ASSERT(fontGroup); if (flags2 & nsTextFrameUtils::Flags::HasTab) { flags |= gfx::ShapedTextFlags::TEXT_ENABLE_SPACING; } if (flags2 & nsTextFrameUtils::Flags::HasShy) { flags |= gfx::ShapedTextFlags::TEXT_ENABLE_HYPHEN_BREAKS; } if (mBidiEnabled && (firstFrame->GetEmbeddingLevel().IsRTL())) { flags |= gfx::ShapedTextFlags::TEXT_IS_RTL; } if (mNextRunContextInfo & nsTextFrameUtils::INCOMING_WHITESPACE) { flags2 |= nsTextFrameUtils::Flags::TrailingWhitespace; } if (mNextRunContextInfo & nsTextFrameUtils::INCOMING_ARABICCHAR) { flags |= gfx::ShapedTextFlags::TEXT_TRAILING_ARABICCHAR; } // ContinueTextRunAcrossFrames guarantees that it doesn't matter which // frame's style is used, so we use a mixture of the first frame and // last frame's style flags |= nsLayoutUtils::GetTextRunFlagsForStyle( lastComputedStyle, firstFrame->PresContext(), fontStyle, textStyle, LetterSpacing(firstFrame, *textStyle)); // XXX this is a bit of a hack. For performance reasons, if we're favouring // performance over quality, don't try to get accurate glyph extents. if (!(flags & gfx::ShapedTextFlags::TEXT_OPTIMIZE_SPEED)) { flags |= gfx::ShapedTextFlags::TEXT_NEED_BOUNDING_BOX; } // Convert linebreak coordinates to transformed string offsets NS_ASSERTION(nextBreakIndex == mLineBreakBeforeFrames.Length(), "Didn't find all the frames to break-before..."); gfxSkipCharsIterator iter(skipChars); AutoTArray 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 transformingFactory; if (anyTextTransformStyle || needsToMaskPassword) { char16_t maskChar = needsToMaskPassword ? 0 : textStyle->TextSecurityMaskChar(); transformingFactory = MakeUnique( std::move(transformingFactory), false, maskChar); } if (anyMathMLStyling) { transformingFactory = MakeUnique( std::move(transformingFactory), mathFlags, sstyScriptLevel, fontInflation); } nsTArray> styles; if (transformingFactory) { uint32_t unmaskStart = 0, unmaskEnd = UINT32_MAX; if (needsToMaskPassword) { unmaskStart = unmaskEnd = UINT32_MAX; const TextEditor* const passwordEditor = nsContentUtils::GetExtantTextEditorFromAnonymousNode( firstFrame->GetContent()); if (passwordEditor && !passwordEditor->IsAllMasked()) { unmaskStart = passwordEditor->UnmaskedStart(); unmaskEnd = passwordEditor->UnmaskedEnd(); } } iter.SetOriginalOffset(0); for (uint32_t i = 0; i < mMappedFlows.Length(); ++i) { MappedFlow* mappedFlow = &mMappedFlows[i]; nsTextFrame* f; ComputedStyle* sc = nullptr; RefPtr defaultStyle; RefPtr 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 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(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(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 buffer; uint32_t bufferSize = mMaxTextLength * (mDoubleByteText ? 2 : 1); if (bufferSize < mMaxTextLength || bufferSize == UINT32_MAX) { return false; } void* textPtr = buffer.AppendElements(bufferSize, fallible); if (!textPtr) { return false; } gfxSkipChars skipChars; const nsAtom* language = mMappedFlows[0].mStartFrame->StyleFont()->mLanguage; for (uint32_t i = 0; i < mMappedFlows.Length(); ++i) { MappedFlow* mappedFlow = &mMappedFlows[i]; nsTextFrame* f = mappedFlow->mStartFrame; const nsStyleText* textStyle = f->StyleText(); nsTextFrameUtils::CompressionMode compression = GetCSSWhitespaceToCompressionMode(f, textStyle); // Figure out what content is included in this flow. const nsTextFragment* frag = f->TextFragment(); int32_t contentStart = mappedFlow->mStartFrame->GetContentOffset(); int32_t contentEnd = mappedFlow->GetContentEnd(); int32_t contentLength = contentEnd - contentStart; nsTextFrameUtils::Flags analysisFlags; if (frag->Is2b()) { NS_ASSERTION(mDoubleByteText, "Wrong buffer char size!"); char16_t* bufStart = static_cast(textPtr); char16_t* bufEnd = nsTextFrameUtils::TransformText( frag->Get2b() + contentStart, contentLength, bufStart, compression, &mNextRunContextInfo, &skipChars, &analysisFlags, language); textPtr = bufEnd; } else { if (mDoubleByteText) { // Need to expand the text. First transform it into a temporary buffer, // then expand. AutoTArray tempBuf; uint8_t* bufStart = tempBuf.AppendElements(contentLength, fallible); if (!bufStart) { return false; } uint8_t* end = nsTextFrameUtils::TransformText( reinterpret_cast(frag->Get1b()) + contentStart, contentLength, bufStart, compression, &mNextRunContextInfo, &skipChars, &analysisFlags, language); textPtr = ExpandBuffer(static_cast(textPtr), tempBuf.Elements(), end - tempBuf.Elements()); } else { uint8_t* bufStart = static_cast(textPtr); uint8_t* end = nsTextFrameUtils::TransformText( reinterpret_cast(frag->Get1b()) + contentStart, contentLength, bufStart, compression, &mNextRunContextInfo, &skipChars, &analysisFlags, language); textPtr = end; } } } // We have to set these up after we've created the textrun, because // the breaks may be stored in the textrun during this very call. // This is a bit annoying because it requires another loop over the frames // making up the textrun, but I don't see a way to avoid this. SetupBreakSinksForTextRun(aTextRun, buffer.Elements()); return true; } static bool HasCompressedLeadingWhitespace( nsTextFrame* aFrame, const nsStyleText* aStyleText, int32_t aContentEndOffset, const gfxSkipCharsIterator& aIterator) { if (!aIterator.IsOriginalCharSkipped()) { return false; } gfxSkipCharsIterator iter = aIterator; int32_t frameContentOffset = aFrame->GetContentOffset(); const nsTextFragment* frag = aFrame->TextFragment(); while (frameContentOffset < aContentEndOffset && iter.IsOriginalCharSkipped()) { if (IsTrimmableSpace(frag, frameContentOffset, aStyleText)) { return true; } ++frameContentOffset; iter.AdvanceOriginal(1); } return false; } void BuildTextRunsScanner::SetupBreakSinksForTextRun(gfxTextRun* aTextRun, const void* aTextPtr) { using mozilla::intl::LineBreakRule; using mozilla::intl::WordBreakRule; // textruns have uniform language const nsStyleFont* styleFont = mMappedFlows[0].mStartFrame->StyleFont(); // We should only use a language for hyphenation if it was specified // explicitly. nsAtom* hyphenationLanguage = styleFont->mExplicitLanguage ? styleFont->mLanguage.get() : nullptr; // We keep this pointed at the skip-chars data for the current mappedFlow. // This lets us cheaply check whether the flow has compressed initial // whitespace... gfxSkipCharsIterator iter(aTextRun->GetSkipChars()); for (uint32_t i = 0; i < mMappedFlows.Length(); ++i) { MappedFlow* mappedFlow = &mMappedFlows[i]; // The CSS word-break value may change within a word, so we reset it for // each MappedFlow. The line-breaker will flush its text if the property // actually changes. const auto* styleText = mappedFlow->mStartFrame->StyleText(); auto wordBreak = styleText->EffectiveWordBreak(); switch (wordBreak) { case StyleWordBreak::BreakAll: mLineBreaker.SetWordBreak(WordBreakRule::BreakAll); break; case StyleWordBreak::KeepAll: mLineBreaker.SetWordBreak(WordBreakRule::KeepAll); break; case StyleWordBreak::Normal: default: MOZ_ASSERT(wordBreak == StyleWordBreak::Normal); mLineBreaker.SetWordBreak(WordBreakRule::Normal); break; } switch (styleText->mLineBreak) { case StyleLineBreak::Auto: mLineBreaker.SetStrictness(LineBreakRule::Auto); break; case StyleLineBreak::Normal: mLineBreaker.SetStrictness(LineBreakRule::Normal); break; case StyleLineBreak::Loose: mLineBreaker.SetStrictness(LineBreakRule::Loose); break; case StyleLineBreak::Strict: mLineBreaker.SetStrictness(LineBreakRule::Strict); break; case StyleLineBreak::Anywhere: mLineBreaker.SetStrictness(LineBreakRule::Anywhere); break; } uint32_t offset = iter.GetSkippedOffset(); gfxSkipCharsIterator iterNext = iter; iterNext.AdvanceOriginal(mappedFlow->GetContentEnd() - mappedFlow->mStartFrame->GetContentOffset()); UniquePtr* breakSink = mBreakSinks.AppendElement( MakeUnique(aTextRun, mDrawTarget, offset)); uint32_t length = iterNext.GetSkippedOffset() - offset; uint32_t flags = 0; nsIFrame* initialBreakController = mappedFlow->mAncestorControllingInitialBreak; if (!initialBreakController) { initialBreakController = mLineContainer; } if (!initialBreakController->StyleText()->WhiteSpaceCanWrap( initialBreakController)) { flags |= nsLineBreaker::BREAK_SUPPRESS_INITIAL; } nsTextFrame* startFrame = mappedFlow->mStartFrame; const nsStyleText* textStyle = startFrame->StyleText(); if (!textStyle->WhiteSpaceCanWrap(startFrame)) { flags |= nsLineBreaker::BREAK_SUPPRESS_INSIDE; } if (aTextRun->GetFlags2() & nsTextFrameUtils::Flags::NoBreaks) { flags |= nsLineBreaker::BREAK_SKIP_SETTING_NO_BREAKS; } if (textStyle->mTextTransform & StyleTextTransform::CAPITALIZE) { flags |= nsLineBreaker::BREAK_NEED_CAPITALIZATION; } if (textStyle->mHyphens == StyleHyphens::Auto && textStyle->mLineBreak != StyleLineBreak::Anywhere) { flags |= nsLineBreaker::BREAK_USE_AUTO_HYPHENATION; } if (HasCompressedLeadingWhitespace(startFrame, textStyle, mappedFlow->GetContentEnd(), iter)) { mLineBreaker.AppendInvisibleWhitespace(flags); } if (length > 0) { BreakSink* sink = mSkipIncompleteTextRuns ? nullptr : (*breakSink).get(); if (mDoubleByteText) { const char16_t* text = reinterpret_cast(aTextPtr); mLineBreaker.AppendText(hyphenationLanguage, text + offset, length, flags, sink); } else { const uint8_t* text = reinterpret_cast(aTextPtr); mLineBreaker.AppendText(hyphenationLanguage, text + offset, length, flags, sink); } } iter = iterNext; } } static bool MayCharacterHaveEmphasisMark(uint32_t aCh) { // Punctuation characters that *can* take emphasis marks (exceptions to the // rule that characters with GeneralCategory=P* do not take emphasis), as per // https://drafts.csswg.org/css-text-decor/#text-emphasis-style-property // There are no non-BMP codepoints in the punctuation exceptions, so we can // just use a 16-bit string to list & check them. constexpr nsLiteralString kPunctuationAcceptsEmphasis = u"\x0023" // # NUMBER SIGN u"\x0025" // % PERCENT SIGN u"\x0026" // & AMPERSAND u"\x0040" // @ COMMERCIAL AT u"\x00A7" // § SECTION SIGN u"\x00B6" // ¶ PILCROW SIGN u"\x0609" // ؉ ARABIC-INDIC PER MILLE SIGN u"\x060A" // ؊ ARABIC-INDIC PER TEN THOUSAND SIGN u"\x066A" // ٪ ARABIC PERCENT SIGN u"\x2030" // ‰ PER MILLE SIGN u"\x2031" // ‱ PER TEN THOUSAND SIGN u"\x204A" // ⁊ TIRONIAN SIGN ET u"\x204B" // ⁋ REVERSED PILCROW SIGN u"\x2053" // ⁓ SWUNG DASH u"\x303D" // 〽️ PART ALTERNATION MARK // Characters that are NFKD-equivalent to the above, extracted from // UnicodeData.txt. u"\xFE5F" // SMALL NUMBER SIGN;Po;0;ET; 0023;;;;N;;;;; u"\xFE60" // SMALL AMPERSAND;Po;0;ON; 0026;;;;N;;;;; u"\xFE6A" // SMALL PERCENT SIGN;Po;0;ET; 0025;;;;N;;;;; u"\xFE6B" // SMALL COMMERCIAL AT;Po;0;ON; 0040;;;;N;;;;; u"\xFF03" // FULLWIDTH NUMBER SIGN;Po;0;ET; 0023;;;;N;;;;; u"\xFF05" // FULLWIDTH PERCENT SIGN;Po;0;ET; 0025;;;;N;;;;; u"\xFF06" // FULLWIDTH AMPERSAND;Po;0;ON; 0026;;;;N;;;;; u"\xFF20"_ns; // FULLWIDTH COMMERCIAL AT;Po;0;ON; 0040;;;;N;;;;; switch (unicode::GetGenCategory(aCh)) { case nsUGenCategory::kSeparator: // whitespace, line- & para-separators return false; case nsUGenCategory::kOther: // control categories return false; case nsUGenCategory::kPunctuation: return aCh <= 0xffff && kPunctuationAcceptsEmphasis.Contains(char16_t(aCh)); default: return true; } } void BuildTextRunsScanner::SetupTextEmphasisForTextRun(gfxTextRun* aTextRun, const void* aTextPtr) { if (!mDoubleByteText) { auto text = reinterpret_cast(aTextPtr); for (auto i : IntegerRange(aTextRun->GetLength())) { if (!MayCharacterHaveEmphasisMark(text[i])) { aTextRun->SetNoEmphasisMark(i); } } } else { auto text = reinterpret_cast(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(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(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 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(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(aPos)); if (ch == '\n' || ch == '\t' || ch == '\r') { return !aTextStyle->WhiteSpaceIsSignificant(); } if (ch == ' ' || ch == CH_NBSP) { // Don't justify spaces that are combined with diacriticals if (!aFrag->Is2b()) { return true; } return !nsTextFrameUtils::IsSpaceCombiningSequenceTail( aFrag->Get2b() + aPos + 1, aFrag->GetLength() - (aPos + 1)); } if (justifyStyle == StyleTextJustify::InterCharacter) { return true; } else if (justifyStyle == StyleTextJustify::InterWord) { return false; } // text-justify: auto if (ch < 0x2150u) { return false; } if (aLangIsCJ) { if ( // Number Forms, Arrows, Mathematical Operators (0x2150u <= ch && ch <= 0x22ffu) || // Enclosed Alphanumerics (0x2460u <= ch && ch <= 0x24ffu) || // Block Elements, Geometric Shapes, Miscellaneous Symbols, Dingbats (0x2580u <= ch && ch <= 0x27bfu) || // Supplemental Arrows-A, Braille Patterns, Supplemental Arrows-B, // Miscellaneous Mathematical Symbols-B, // Supplemental Mathematical Operators, Miscellaneous Symbols and Arrows (0x27f0u <= ch && ch <= 0x2bffu) || // CJK Radicals Supplement, CJK Radicals Supplement, Ideographic // Description Characters, CJK Symbols and Punctuation, Hiragana, // Katakana, Bopomofo (0x2e80u <= ch && ch <= 0x312fu) || // Kanbun, Bopomofo Extended, Katakana Phonetic Extensions, // Enclosed CJK Letters and Months, CJK Compatibility, // CJK Unified Ideographs Extension A, Yijing Hexagram Symbols, // CJK Unified Ideographs, Yi Syllables, Yi Radicals (0x3190u <= ch && ch <= 0xabffu) || // CJK Compatibility Ideographs (0xf900u <= ch && ch <= 0xfaffu) || // Halfwidth and Fullwidth Forms (a part) (0xff5eu <= ch && ch <= 0xff9fu)) { return true; } if (NS_IS_HIGH_SURROGATE(ch)) { if (char32_t u = aFrag->ScalarValueAt(AssertedCast(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(p) - str) + aOffset; } } } return -1; } static bool IsChineseOrJapanese(const nsTextFrame* aFrame) { if (aFrame->ShouldSuppressLineBreak()) { // Always treat ruby as CJ language so that those characters can // be expanded properly even when surrounded by other language. return true; } nsAtom* language = aFrame->StyleFont()->mLanguage; if (!language) { return false; } return nsStyleUtil::MatchesLanguagePrefix(language, u"ja") || nsStyleUtil::MatchesLanguagePrefix(language, u"zh"); } #ifdef DEBUG static bool IsInBounds(const gfxSkipCharsIterator& aStart, int32_t aContentLength, gfxTextRun::Range aRange) { if (aStart.GetSkippedOffset() > aRange.start) { return false; } if (aContentLength == INT32_MAX) { return true; } gfxSkipCharsIterator iter(aStart); iter.AdvanceOriginal(aContentLength); return iter.GetSkippedOffset() >= aRange.end; } #endif nsTextFrame::PropertyProvider::PropertyProvider( gfxTextRun* aTextRun, const nsStyleText* aTextStyle, const nsTextFragment* aFrag, nsTextFrame* aFrame, const gfxSkipCharsIterator& aStart, int32_t aLength, nsIFrame* aLineContainer, nscoord aOffsetFromBlockOriginForTabs, nsTextFrame::TextRunType aWhichTextRun, bool aAtStartOfLine) : mTextRun(aTextRun), mFontGroup(nullptr), mTextStyle(aTextStyle), mFrag(aFrag), mLineContainer(aLineContainer), mFrame(aFrame), mStart(aStart), mTempIterator(aStart), mTabWidths(nullptr), mTabWidthsAnalyzedLimit(0), mLength(aLength), mWordSpacing(WordSpacing(aFrame, mTextRun, *aTextStyle)), mLetterSpacing(LetterSpacing(aFrame, *aTextStyle)), mMinTabAdvance(-1.0), mHyphenWidth(-1), mOffsetFromBlockOriginForTabs(aOffsetFromBlockOriginForTabs), mJustificationArrayStart(0), mReflowing(true), mWhichTextRun(aWhichTextRun) { NS_ASSERTION(mStart.IsInitialized(), "Start not initialized?"); if (aAtStartOfLine) { mStartOfLineOffset = mStart.GetSkippedOffset(); } } nsTextFrame::PropertyProvider::PropertyProvider( nsTextFrame* aFrame, const gfxSkipCharsIterator& aStart, nsTextFrame::TextRunType aWhichTextRun, nsFontMetrics* aFontMetrics) : mTextRun(aFrame->GetTextRun(aWhichTextRun)), mFontGroup(nullptr), mFontMetrics(aFontMetrics), mTextStyle(aFrame->StyleText()), mFrag(aFrame->TextFragment()), mLineContainer(nullptr), mFrame(aFrame), mStart(aStart), mTempIterator(aStart), mTabWidths(nullptr), mTabWidthsAnalyzedLimit(0), mLength(aFrame->GetContentLength()), mWordSpacing(WordSpacing(aFrame, mTextRun, *mTextStyle)), mLetterSpacing(LetterSpacing(aFrame, *mTextStyle)), mMinTabAdvance(-1.0), mHyphenWidth(-1), mOffsetFromBlockOriginForTabs(0), mJustificationArrayStart(0), mReflowing(false), mWhichTextRun(aWhichTextRun) { NS_ASSERTION(mTextRun, "Textrun not initialized!"); } gfx::ShapedTextFlags nsTextFrame::PropertyProvider::GetShapedTextFlags() const { return nsLayoutUtils::GetTextRunOrientFlagsForStyle(mFrame->Style()); } already_AddRefed nsTextFrame::PropertyProvider::GetDrawTarget() const { return CreateReferenceDrawTarget(GetFrame()); } gfxFloat nsTextFrame::PropertyProvider::MinTabAdvance() const { if (mMinTabAdvance < 0.0) { mMinTabAdvance = GetMinTabAdvanceAppUnits(mTextRun); } return mMinTabAdvance; } /** * Finds the offset of the first character of the cluster containing aPos */ static void FindClusterStart(const gfxTextRun* aTextRun, int32_t aOriginalStart, gfxSkipCharsIterator* aPos) { while (aPos->GetOriginalOffset() > aOriginalStart) { if (aPos->IsOriginalCharSkipped() || aTextRun->IsClusterStart(aPos->GetSkippedOffset())) { break; } aPos->AdvanceOriginal(-1); } } /** * Finds the offset of the last character of the cluster containing aPos. * If aAllowSplitLigature is false, we also check for a ligature-group * start. */ static void FindClusterEnd(const gfxTextRun* aTextRun, int32_t aOriginalEnd, gfxSkipCharsIterator* aPos, bool aAllowSplitLigature = true) { MOZ_ASSERT(aPos->GetOriginalOffset() < aOriginalEnd, "character outside string"); aPos->AdvanceOriginal(1); while (aPos->GetOriginalOffset() < aOriginalEnd) { if (aPos->IsOriginalCharSkipped() || (aTextRun->IsClusterStart(aPos->GetSkippedOffset()) && (aAllowSplitLigature || aTextRun->IsLigatureGroupStart(aPos->GetSkippedOffset())))) { break; } aPos->AdvanceOriginal(1); } aPos->AdvanceOriginal(-1); } // Get the line number of aFrame in the lines referenced by aLineIter, if // known (returning -1 if we don't find it). static int32_t GetFrameLineNum(nsIFrame* aFrame, nsILineIterator* aLineIter) { if (!aLineIter) { return -1; } int32_t n = aLineIter->FindLineContaining(aFrame); if (n >= 0) { return n; } // If we didn't find the frame directly, but its parent is an inline, // we want the line that the inline ancestor is on. nsIFrame* ancestor = aFrame->GetParent(); while (ancestor && ancestor->IsInlineFrame()) { n = aLineIter->FindLineContaining(ancestor); if (n >= 0) { return n; } ancestor = ancestor->GetParent(); } return -1; } // Get the position of the first preserved newline in aFrame, if any, // returning -1 if none. static int32_t FindFirstNewlinePosition(const nsTextFrame* aFrame) { MOZ_ASSERT(aFrame->StyleText()->NewlineIsSignificantStyle(), "how did the HasNewline flag get set?"); const auto* textFragment = aFrame->TextFragment(); for (auto i = aFrame->GetContentOffset(); i < aFrame->GetContentEnd(); ++i) { if (textFragment->CharAt(i) == '\n') { return i; } } return -1; } // Get the position of the last preserved tab in aFrame that is before the // preserved newline at aNewlinePos. // Passing -1 for aNewlinePos means there is no preserved newline, so we look // for the last preserved tab in the whole content. // Returns -1 if no such preserved tab is present. static int32_t FindLastTabPositionBeforeNewline(const nsTextFrame* aFrame, int32_t aNewlinePos) { // We only call this if white-space is not being collapsed. MOZ_ASSERT(aFrame->StyleText()->WhiteSpaceIsSignificant(), "how did the HasTab flag get set?"); const auto* textFragment = aFrame->TextFragment(); // If a non-negative newline position was given, we only need to search the // text before that offset. for (auto i = aNewlinePos < 0 ? aFrame->GetContentEnd() : aNewlinePos; i > aFrame->GetContentOffset(); --i) { if (textFragment->CharAt(i - 1) == '\t') { return i; } } return -1; } // Look for preserved tab or newline in the given frame or its following // siblings on the same line, to determine whether justification should be // suppressed in order to avoid disrupting tab-stop positions. // Returns the first such preserved whitespace char, or 0 if none found. static char NextPreservedWhiteSpaceOnLine(nsIFrame* aSibling, nsILineIterator* aLineIter, int32_t aLineNum) { while (aSibling) { // If we find a
, treat it like a newline. if (aSibling->IsBrFrame()) { return '\n'; } // If we've moved on to a later line, stop searching. if (GetFrameLineNum(aSibling, aLineIter) > aLineNum) { return 0; } // If we encounter an inline frame, recurse into it. if (aSibling->IsInlineFrame()) { auto* child = aSibling->PrincipalChildList().FirstChild(); char result = NextPreservedWhiteSpaceOnLine(child, aLineIter, aLineNum); if (result) { return result; } } // If we have a text frame, and whitespace is not collapsed, we need to // check its contents. if (aSibling->IsTextFrame()) { const auto* textStyle = aSibling->StyleText(); if (textStyle->WhiteSpaceOrNewlineIsSignificant()) { const auto* textFrame = static_cast(aSibling); const auto* textFragment = textFrame->TextFragment(); for (auto i = textFrame->GetContentOffset(); i < textFrame->GetContentEnd(); ++i) { const char16_t ch = textFragment->CharAt(i); if (ch == '\n' && textStyle->NewlineIsSignificantStyle()) { return '\n'; } if (ch == '\t' && textStyle->WhiteSpaceIsSignificant()) { return '\t'; } } } } aSibling = aSibling->GetNextSibling(); } return 0; } static bool HasPreservedTabInFollowingSiblingOnLine(nsTextFrame* aFrame) { bool foundTab = false; nsIFrame* lineContainer = FindLineContainer(aFrame); nsILineIterator* iter = lineContainer->GetLineIterator(); int32_t line = GetFrameLineNum(aFrame, iter); char ws = NextPreservedWhiteSpaceOnLine(aFrame->GetNextSibling(), iter, line); if (ws == '\t') { foundTab = true; } else if (!ws) { // Didn't find a preserved tab or newline in our siblings; if our parent // (and its parent, etc) is an inline, we need to look at their following // siblings, too, as long as they're on the same line. const nsIFrame* maybeInline = aFrame->GetParent(); while (maybeInline && maybeInline->IsInlineFrame()) { ws = NextPreservedWhiteSpaceOnLine(maybeInline->GetNextSibling(), iter, line); if (ws == '\t') { foundTab = true; break; } if (ws == '\n') { break; } maybeInline = maybeInline->GetParent(); } } // We called lineContainer->GetLineIterator() above, but we mustn't // allow a block frame to retain this iterator if we're currently in // reflow, as it will become invalid as the line list is reflowed. if (lineContainer->HasAnyStateBits(NS_FRAME_IN_REFLOW) && lineContainer->IsBlockFrameOrSubclass()) { static_cast(lineContainer)->ClearLineIterator(); } return foundTab; } JustificationInfo nsTextFrame::PropertyProvider::ComputeJustification( Range aRange, nsTArray* aAssignments) { JustificationInfo info; // Horizontal-in-vertical frame is orthogonal to the line, so it // doesn't actually include any justification opportunity inside. // The spec says such frame should be treated as a U+FFFC. Since we // do not insert justification opportunities on the sides of that // character, the sides of this frame are not justifiable either. if (mFrame->Style()->IsTextCombined()) { return info; } int32_t lastTab = -1; if (StaticPrefs::layout_css_text_align_justify_only_after_last_tab()) { // If there is a preserved tab on the line, we don't apply justification // until we're past its position. if (mTextStyle->WhiteSpaceIsSignificant()) { // If there is a preserved newline within the text, we don't need to look // beyond this frame, as following frames will not be on the same line. int32_t newlinePos = (mTextRun->GetFlags2() & nsTextFrameUtils::Flags::HasNewline) ? FindFirstNewlinePosition(mFrame) : -1; if (newlinePos < 0) { // There's no preserved newline within this frame; if there's a tab // in a later sibling frame on the same line, we won't apply any // justification to this one. if (HasPreservedTabInFollowingSiblingOnLine(mFrame)) { return info; } } if (mTextRun->GetFlags2() & nsTextFrameUtils::Flags::HasTab) { // Find last tab character in the content; we won't justify anything // before that position, so that tab alignment remains correct. lastTab = FindLastTabPositionBeforeNewline(mFrame, newlinePos); } } } bool isCJ = IsChineseOrJapanese(mFrame); nsSkipCharsRunIterator run( mStart, nsSkipCharsRunIterator::LENGTH_INCLUDES_SKIPPED, aRange.Length()); run.SetOriginalOffset(aRange.start); mJustificationArrayStart = run.GetSkippedOffset(); nsTArray assignments; assignments.SetCapacity(aRange.Length()); while (run.NextRun()) { uint32_t originalOffset = run.GetOriginalOffset(); uint32_t skippedOffset = run.GetSkippedOffset(); uint32_t length = run.GetRunLength(); assignments.SetLength(skippedOffset + length - mJustificationArrayStart); gfxSkipCharsIterator iter = run.GetPos(); for (uint32_t i = 0; i < length; ++i) { uint32_t offset = originalOffset + i; if (!IsJustifiableCharacter(mTextStyle, mFrag, offset, isCJ) || (lastTab >= 0 && offset <= uint32_t(lastTab))) { continue; } iter.SetOriginalOffset(offset); FindClusterStart(mTextRun, originalOffset, &iter); uint32_t firstCharOffset = iter.GetSkippedOffset(); uint32_t firstChar = firstCharOffset > mJustificationArrayStart ? firstCharOffset - mJustificationArrayStart : 0; if (!firstChar) { info.mIsStartJustifiable = true; } else { auto& assign = assignments[firstChar]; auto& prevAssign = assignments[firstChar - 1]; if (prevAssign.mGapsAtEnd) { prevAssign.mGapsAtEnd = 1; assign.mGapsAtStart = 1; } else { assign.mGapsAtStart = 2; info.mInnerOpportunities++; } } FindClusterEnd(mTextRun, originalOffset + length, &iter); uint32_t lastChar = iter.GetSkippedOffset() - mJustificationArrayStart; // Assign the two gaps temporary to the last char. If the next cluster is // justifiable as well, one of the gaps will be removed by code above. assignments[lastChar].mGapsAtEnd = 2; info.mInnerOpportunities++; // Skip the whole cluster i = iter.GetOriginalOffset() - originalOffset; } } if (!assignments.IsEmpty() && assignments.LastElement().mGapsAtEnd) { // We counted the expansion opportunity after the last character, // but it is not an inner opportunity. MOZ_ASSERT(info.mInnerOpportunities > 0); info.mInnerOpportunities--; info.mIsEndJustifiable = true; } if (aAssignments) { *aAssignments = std::move(assignments); } return info; } // aStart, aLength in transformed string offsets void nsTextFrame::PropertyProvider::GetSpacing(Range aRange, Spacing* aSpacing) const { GetSpacingInternal( aRange, aSpacing, !(mTextRun->GetFlags2() & nsTextFrameUtils::Flags::HasTab)); } static bool CanAddSpacingBefore(const gfxTextRun* aTextRun, uint32_t aOffset, bool aNewlineIsSignificant) { const auto* g = aTextRun->GetCharacterGlyphs(); MOZ_ASSERT(aOffset < aTextRun->GetLength()); if (aNewlineIsSignificant && g[aOffset].CharIsNewline()) { return false; } if (!aOffset) { return true; } return g[aOffset].IsClusterStart() && g[aOffset].IsLigatureGroupStart() && !g[aOffset - 1].CharIsFormattingControl() && !g[aOffset].CharIsTab(); } static bool CanAddSpacingAfter(const gfxTextRun* aTextRun, uint32_t aOffset, bool aNewlineIsSignificant) { const auto* g = aTextRun->GetCharacterGlyphs(); MOZ_ASSERT(aOffset < aTextRun->GetLength()); if (aNewlineIsSignificant && g[aOffset].CharIsNewline()) { return false; } if (aOffset + 1 >= aTextRun->GetLength()) { return true; } return g[aOffset + 1].IsClusterStart() && g[aOffset + 1].IsLigatureGroupStart() && !g[aOffset].CharIsFormattingControl() && !g[aOffset].CharIsTab(); } static gfxFloat ComputeTabWidthAppUnits(const nsIFrame* aFrame) { const auto& tabSize = aFrame->StyleText()->mTabSize; if (tabSize.IsLength()) { nscoord w = tabSize.length._0.ToAppUnits(); MOZ_ASSERT(w >= 0); return w; } MOZ_ASSERT(tabSize.IsNumber()); gfxFloat spaces = tabSize.number._0; MOZ_ASSERT(spaces >= 0); const nsIFrame* cb = aFrame->GetContainingBlock(0, aFrame->StyleDisplay()); const auto* styleText = cb->StyleText(); // Round the space width when converting to appunits the same way textruns do. // We don't use GetFirstFontMetrics here because that may return a font that // does not actually have the character, yet is considered the "first // available font" per CSS Fonts. Here, we want the font that would be used // to render , even if that means looking further down the font-family // list. RefPtr fm = nsLayoutUtils::GetFontMetricsForFrame(cb, 1.0f); bool vertical = cb->GetWritingMode().IsCentralBaseline(); RefPtr font = fm->GetThebesFontGroup()->GetFirstValidFont(' '); auto metrics = font->GetMetrics(vertical ? nsFontMetrics::eVertical : nsFontMetrics::eHorizontal); nscoord spaceWidth = nscoord( NS_round(metrics.spaceWidth * cb->PresContext()->AppUnitsPerDevPixel())); return spaces * (spaceWidth + styleText->mLetterSpacing.Resolve(fm->EmHeight()) + styleText->mWordSpacing.Resolve(spaceWidth)); } void nsTextFrame::PropertyProvider::GetSpacingInternal(Range aRange, Spacing* aSpacing, bool aIgnoreTabs) const { MOZ_ASSERT(IsInBounds(mStart, mLength, aRange), "Range out of bounds"); uint32_t index; for (index = 0; index < aRange.Length(); ++index) { aSpacing[index].mBefore = 0.0; aSpacing[index].mAfter = 0.0; } if (mFrame->Style()->IsTextCombined()) { return; } // Find our offset into the original+transformed string gfxSkipCharsIterator start(mStart); start.SetSkippedOffset(aRange.start); // First, compute the word and letter spacing if (mWordSpacing || mLetterSpacing) { // Iterate over non-skipped characters nsSkipCharsRunIterator run( start, nsSkipCharsRunIterator::LENGTH_UNSKIPPED_ONLY, aRange.Length()); bool newlineIsSignificant = mTextStyle->NewlineIsSignificant(mFrame); // Which letter-spacing model are we using? // 0 - Gecko legacy model, spacing added to trailing side of letter // 1 - WebKit/Blink-compatible, spacing added to right-hand side // 2 - Symmetrical spacing, half added to each side gfxFloat before, after; switch (StaticPrefs::layout_css_letter_spacing_model()) { default: // use Gecko legacy behavior if pref value is unknown case 0: before = 0.0; after = mLetterSpacing; break; case 1: if (mTextRun->IsRightToLeft()) { before = mLetterSpacing; after = 0.0; } else { before = 0.0; after = mLetterSpacing; } break; case 2: before = mLetterSpacing / 2.0; after = mLetterSpacing - before; break; } bool atStart = mStartOfLineOffset == start.GetSkippedOffset() && !mFrame->IsInSVGTextSubtree(); while (run.NextRun()) { uint32_t runOffsetInSubstring = run.GetSkippedOffset() - aRange.start; gfxSkipCharsIterator iter = run.GetPos(); for (int32_t i = 0; i < run.GetRunLength(); ++i) { if (!atStart && before != 0.0 && CanAddSpacingBefore(mTextRun, run.GetSkippedOffset() + i, newlineIsSignificant)) { aSpacing[runOffsetInSubstring + i].mBefore += before; } if (after != 0.0 && CanAddSpacingAfter(mTextRun, run.GetSkippedOffset() + i, newlineIsSignificant)) { // End of a cluster, not in a ligature: put letter-spacing after it aSpacing[runOffsetInSubstring + i].mAfter += after; } if (IsCSSWordSpacingSpace(mFrag, i + run.GetOriginalOffset(), mFrame, mTextStyle)) { // It kinda sucks, but space characters can be part of clusters, // and even still be whitespace (I think!) iter.SetSkippedOffset(run.GetSkippedOffset() + i); FindClusterEnd(mTextRun, run.GetOriginalOffset() + run.GetRunLength(), &iter); uint32_t runOffset = iter.GetSkippedOffset() - aRange.start; aSpacing[runOffset].mAfter += mWordSpacing; } atStart = false; } } } // Now add tab spacing, if there is any if (!aIgnoreTabs) { gfxFloat tabWidth = ComputeTabWidthAppUnits(mFrame); if (tabWidth > 0) { CalcTabWidths(aRange, tabWidth); if (mTabWidths) { mTabWidths->ApplySpacing(aSpacing, aRange.start - mStart.GetSkippedOffset(), aRange.Length()); } } } // Now add in justification spacing if (mJustificationSpacings.Length() > 0) { // If there is any spaces trimmed at the end, aStart + aLength may // be larger than the flags array. When that happens, we can simply // ignore those spaces. auto arrayEnd = mJustificationArrayStart + static_cast(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 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(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(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( run.GetOriginalOffset() + run.GetRunLength() - 1)) == CH_SHY; } else { int32_t runOffsetInSubstring = run.GetSkippedOffset() - aRange.start; memset(aBreakBefore + runOffsetInSubstring, static_cast(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( skipIter.ConvertSkippedToOriginal(aRange.start + i))))) { if (i < aRange.Length() - 1) { aBreakBefore[i + 1] = HyphenType::Explicit; } continue; } if (mTextRun->CanHyphenateBefore(aRange.start + i) && aBreakBefore[i] == HyphenType::None) { aBreakBefore[i] = HyphenType::AutoWithoutManualInSameWord; } } } } void nsTextFrame::PropertyProvider::InitializeForDisplay(bool aTrimAfter) { nsTextFrame::TrimmedOffsets trimmed = mFrame->GetTrimmedOffsets( mFrag, (aTrimAfter ? nsTextFrame::TrimmedOffsetFlags::Default : nsTextFrame::TrimmedOffsetFlags::NoTrimAfter)); mStart.SetOriginalOffset(trimmed.mStart); mLength = trimmed.mLength; if (mFrame->HasAnyStateBits(TEXT_START_OF_LINE)) { mStartOfLineOffset = mStart.GetSkippedOffset(); } SetupJustificationSpacing(true); } void nsTextFrame::PropertyProvider::InitializeForMeasure() { nsTextFrame::TrimmedOffsets trimmed = mFrame->GetTrimmedOffsets( mFrag, nsTextFrame::TrimmedOffsetFlags::NotPostReflow); mStart.SetOriginalOffset(trimmed.mStart); mLength = trimmed.mLength; if (mFrame->HasAnyStateBits(TEXT_START_OF_LINE)) { mStartOfLineOffset = mStart.GetSkippedOffset(); } SetupJustificationSpacing(false); } void nsTextFrame::PropertyProvider::SetupJustificationSpacing( bool aPostReflow) { MOZ_ASSERT(mLength != INT32_MAX, "Can't call this with undefined length"); if (!mFrame->HasAnyStateBits(TEXT_JUSTIFICATION_ENABLED)) { return; } gfxSkipCharsIterator start(mStart), end(mStart); // We can't just use our mLength here; when InitializeForDisplay is // called with false for aTrimAfter, we still shouldn't be assigning // justification space to any trailing whitespace. nsTextFrame::TrimmedOffsets trimmed = mFrame->GetTrimmedOffsets( mFrag, (aPostReflow ? nsTextFrame::TrimmedOffsetFlags::Default : nsTextFrame::TrimmedOffsetFlags::NotPostReflow)); end.AdvanceOriginal(trimmed.mLength); gfxSkipCharsIterator realEnd(end); Range range(uint32_t(start.GetOriginalOffset()), uint32_t(end.GetOriginalOffset())); nsTArray 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::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; if (continuations->SetCapacity(count, fallible)) { for (nsTextFrame* f = this; f; f = static_cast(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(aPrevContinuation); RemoveStateBits(NS_FRAME_IS_FLUID_CONTINUATION); UpdateCachedContinuations(); } nsTextFrame* GetPrevInFlow() const final { return HasAnyStateBits(NS_FRAME_IS_FLUID_CONTINUATION) ? mPrevContinuation : nullptr; } void SetPrevInFlow(nsIFrame* aPrevInFlow) final { NS_ASSERTION(!aPrevInFlow || Type() == aPrevInFlow->Type(), "setting a prev in flow with incorrect type!"); NS_ASSERTION( !nsSplittableFrame::IsInPrevContinuationChain(aPrevInFlow, this), "creating a loop in continuation chain!"); mPrevContinuation = static_cast(aPrevInFlow); AddStateBits(NS_FRAME_IS_FLUID_CONTINUATION); UpdateCachedContinuations(); } // Call this helper to update cache after mPrevContinuation is changed. void UpdateCachedContinuations() { nsTextFrame* prevFirst = mFirstContinuation; if (mPrevContinuation) { mFirstContinuation = mPrevContinuation->FirstContinuation(); if (mFirstContinuation) { mFirstContinuation->ClearCachedContinuations(); } } else { mFirstContinuation = nullptr; } if (mFirstContinuation != prevFirst) { if (prevFirst) { prevFirst->ClearCachedContinuations(); } auto* f = static_cast(mNextContinuation); while (f) { f->mFirstContinuation = mFirstContinuation; f = static_cast(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(mPrevContinuation); MOZ_ASSERT(mFirstContinuation == prev->mFirstContinuation); } else { MOZ_ASSERT(mFirstContinuation == mPrevContinuation->FirstContinuation()); } } else { MOZ_ASSERT(!mFirstContinuation); } #endif return mFirstContinuation; }; void AddInlineMinISize(const IntrinsicSizeInput& aInput, InlineMinISizeData* aData) final { // Do nothing, since the first-in-flow accounts for everything. } void AddInlinePrefISize(const IntrinsicSizeInput& aInput, InlinePrefISizeData* aData) final { // Do nothing, since the first-in-flow accounts for everything. } protected: explicit nsContinuingTextFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) : nsTextFrame(aStyle, aPresContext, kClassID) {} nsTextFrame* mPrevContinuation = nullptr; nsTextFrame* mFirstContinuation = nullptr; }; void nsContinuingTextFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, nsIFrame* aPrevInFlow) { NS_ASSERTION(aPrevInFlow, "Must be a continuation!"); // Hook the frame into the flow nsTextFrame* prev = static_cast(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(static_cast(this)); do { firstInFlow = previous; previous = firstInFlow->GetPrevInFlow(); } while (previous); MOZ_ASSERT(firstInFlow, "post-condition failed"); return firstInFlow; } // XXX Do we want to do all the work for the first-in-flow or do the // work for each part? (Be careful of first-letter / first-line, though, // especially first-line!) Doing all the work on the first-in-flow has // the advantage of avoiding the potential for incremental reflow bugs, // but depends on our maintining the frame tree in reasonable ways even // for edge cases (block-within-inline splits, nextBidi, etc.) // XXX We really need to make :first-letter happen during frame // construction. nscoord nsTextFrame::IntrinsicISize(const IntrinsicSizeInput& aInput, IntrinsicISizeType aType) { return IntrinsicISizeFromInline(aInput, aType); } //---------------------------------------------------------------------- #if defined(DEBUG_rbs) || defined(DEBUG_bzbarsky) static void VerifyNotDirty(nsFrameState state) { bool isZero = state & NS_FRAME_FIRST_REFLOW; bool isDirty = state & NS_FRAME_IS_DIRTY; if (!isZero && isDirty) { NS_WARNING("internal offsets may be out-of-sync"); } } # define DEBUG_VERIFY_NOT_DIRTY(state) VerifyNotDirty(state) #else # define DEBUG_VERIFY_NOT_DIRTY(state) #endif nsIFrame* NS_NewTextFrame(PresShell* aPresShell, ComputedStyle* aStyle) { return new (aPresShell) nsTextFrame(aStyle, aPresShell->GetPresContext()); } NS_IMPL_FRAMEARENA_HELPERS(nsTextFrame) nsIFrame* NS_NewContinuingTextFrame(PresShell* aPresShell, ComputedStyle* aStyle) { return new (aPresShell) nsContinuingTextFrame(aStyle, aPresShell->GetPresContext()); } NS_IMPL_FRAMEARENA_HELPERS(nsContinuingTextFrame) nsTextFrame::~nsTextFrame() = default; nsIFrame::Cursor nsTextFrame::GetCursor(const nsPoint& aPoint) { StyleCursorKind kind = StyleUI()->Cursor().keyword; if (kind == StyleCursorKind::Auto) { if (!IsSelectable(nullptr)) { kind = StyleCursorKind::Default; } else { kind = GetWritingMode().IsVertical() ? StyleCursorKind::VerticalText : StyleCursorKind::Text; } } return Cursor{kind, AllowCustomCursorImage::Yes}; } nsTextFrame* nsTextFrame::LastInFlow() const { nsTextFrame* lastInFlow = const_cast(this); while (lastInFlow->GetNextInFlow()) { lastInFlow = lastInFlow->GetNextInFlow(); } MOZ_ASSERT(lastInFlow, "post-condition failed"); return lastInFlow; } nsTextFrame* nsTextFrame::LastContinuation() const { nsTextFrame* lastContinuation = const_cast(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 textRun = GetTextRun(aWhichTextRun); if (!textRun) { return; } if (aWhichTextRun == nsTextFrame::eInflated) { mFontMetrics = nullptr; } DebugOnly checkmTextrun = textRun == mTextRun; UnhookTextRunFromFrames(textRun, aStartContinuation); MOZ_ASSERT(checkmTextrun ? !mTextRun : !GetProperty(UninflatedTextRunProperty())); } void nsTextFrame::DisconnectTextRuns() { MOZ_ASSERT(!IsInTextRunUserData(), "Textrun mentions this frame in its user data so we can't just " "disconnect"); mTextRun = nullptr; if (HasAnyStateBits(TEXT_HAS_FONT_INFLATION)) { RemoveProperty(UninflatedTextRunProperty()); } } void nsTextFrame::NotifyNativeAnonymousTextnodeChange(uint32_t aOldLength) { MOZ_ASSERT(mContent->IsInNativeAnonymousSubtree()); MarkIntrinsicISizesDirty(); // This is to avoid making a new Reflow request in CharacterDataChanged: for (nsTextFrame* f = this; f; f = f->GetNextContinuation()) { f->MarkSubtreeDirty(); f->mReflowRequestedForCharDataChange = true; } // Pretend that all the text changed. CharacterDataChangeInfo info; info.mAppend = false; info.mChangeStart = 0; info.mChangeEnd = aOldLength; info.mReplaceLength = GetContent()->TextLength(); CharacterDataChanged(info); } nsresult nsTextFrame::CharacterDataChanged( const CharacterDataChangeInfo& aInfo) { if (mContent->HasFlag(NS_HAS_NEWLINE_PROPERTY)) { mContent->RemoveProperty(nsGkAtoms::newline); mContent->UnsetFlags(NS_HAS_NEWLINE_PROPERTY); } if (mContent->HasFlag(NS_HAS_FLOWLENGTH_PROPERTY)) { mContent->RemoveProperty(nsGkAtoms::flowlength); mContent->UnsetFlags(NS_HAS_FLOWLENGTH_PROPERTY); } // Find the first frame whose text has changed. Frames that are entirely // before the text change are completely unaffected. nsTextFrame* next; nsTextFrame* textFrame = this; while (true) { next = textFrame->GetNextContinuation(); if (!next || next->GetContentOffset() > int32_t(aInfo.mChangeStart)) { break; } textFrame = next; } int32_t endOfChangedText = aInfo.mChangeStart + aInfo.mReplaceLength; // Parent of the last frame that we passed to FrameNeedsReflow (or noticed // had already received an earlier FrameNeedsReflow call). // (For subsequent frames with this same parent, we can just set their // dirty bit without bothering to call FrameNeedsReflow again.) nsIFrame* lastDirtiedFrameParent = nullptr; mozilla::PresShell* presShell = PresShell(); do { // textFrame contained deleted text (or the insertion point, // if this was a pure insertion). textFrame->RemoveStateBits(TEXT_WHITESPACE_FLAGS); textFrame->ClearTextRuns(); nsIFrame* parentOfTextFrame = textFrame->GetParent(); bool areAncestorsAwareOfReflowRequest = false; if (lastDirtiedFrameParent == parentOfTextFrame) { // An earlier iteration of this loop already called // FrameNeedsReflow for a sibling of |textFrame|. areAncestorsAwareOfReflowRequest = true; } else { lastDirtiedFrameParent = parentOfTextFrame; } if (textFrame->mReflowRequestedForCharDataChange) { // We already requested a reflow for this frame; nothing to do. MOZ_ASSERT(textFrame->HasAnyStateBits(NS_FRAME_IS_DIRTY), "mReflowRequestedForCharDataChange should only be set " "on dirty frames"); } else { // Make sure textFrame is queued up for a reflow. Also set a flag so we // don't waste time doing this again in repeated calls to this method. textFrame->mReflowRequestedForCharDataChange = true; if (!areAncestorsAwareOfReflowRequest) { // Ask the parent frame to reflow me. presShell->FrameNeedsReflow( textFrame, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); } else { // We already called FrameNeedsReflow on behalf of an earlier sibling, // so we can just mark this frame as dirty and don't need to bother // telling its ancestors. // Note: if the parent is a block, we're cheating here because we should // be marking our line dirty, but we're not. nsTextFrame::SetLength will // do that when it gets called during reflow. textFrame->MarkSubtreeDirty(); } } textFrame->InvalidateFrame(); // Below, frames that start after the deleted text will be adjusted so that // their offsets move with the trailing unchanged text. If this change // deletes more text than it inserts, those frame offsets will decrease. // We need to maintain the invariant that mContentOffset is non-decreasing // along the continuation chain. So we need to ensure that frames that // started in the deleted text are all still starting before the // unchanged text. if (textFrame->mContentOffset > endOfChangedText) { textFrame->mContentOffset = endOfChangedText; } textFrame = textFrame->GetNextContinuation(); } while (textFrame && textFrame->GetContentOffset() < int32_t(aInfo.mChangeEnd)); // This is how much the length of the string changed by --- i.e., // how much the trailing unchanged text moved. int32_t sizeChange = aInfo.mChangeStart + aInfo.mReplaceLength - aInfo.mChangeEnd; if (sizeChange) { // Fix the offsets of the text frames that start in the trailing // unchanged text. while (textFrame) { textFrame->mContentOffset += sizeChange; // XXX we could rescue some text runs by adjusting their user data // to reflect the change in DOM offsets textFrame->ClearTextRuns(); textFrame = textFrame->GetNextContinuation(); } } return NS_OK; } NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(TextCombineScaleFactorProperty, float) float nsTextFrame::GetTextCombineScaleFactor(nsTextFrame* aFrame) { float factor = aFrame->GetProperty(TextCombineScaleFactorProperty()); return factor ? factor : 1.0f; } void nsTextFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { if (!IsVisibleForPainting()) { return; } DO_GLOBAL_REFLOW_COUNT_DSP("nsTextFrame"); const nsStyleText* st = StyleText(); bool isTextTransparent = NS_GET_A(st->mWebkitTextFillColor.CalcColor(this)) == 0 && NS_GET_A(st->mWebkitTextStrokeColor.CalcColor(this)) == 0; if ((HasAnyStateBits(TEXT_NO_RENDERED_GLYPHS) || (isTextTransparent && !StyleText()->HasTextShadow())) && aBuilder->IsForPainting() && !IsInSVGTextSubtree()) { if (!IsSelected()) { TextDecorations textDecs; GetTextDecorations(PresContext(), eResolvedColors, textDecs); if (!textDecs.HasDecorationLines()) { if (auto* currentPresContext = aBuilder->CurrentPresContext()) { currentPresContext->SetBuiltInvisibleText(); } return; } } } aLists.Content()->AppendNewToTop(aBuilder, this); } UniquePtr nsTextFrame::GetSelectionDetails() { const nsFrameSelection* frameSelection = GetConstFrameSelection(); if (frameSelection->IsInTableSelectionMode()) { return nullptr; } UniquePtr details = frameSelection->LookUpSelection( mContent, GetContentOffset(), GetContentLength(), false); for (SelectionDetails* sd = details.get(); sd; sd = sd->mNext.get()) { sd->mStart += mContentOffset; sd->mEnd += mContentOffset; } return details; } static void PaintSelectionBackground( DrawTarget& aDrawTarget, nscolor aColor, const LayoutDeviceRect& aDirtyRect, const LayoutDeviceRect& aRect, nsTextFrame::DrawPathCallbacks* aCallbacks) { Rect rect = aRect.Intersect(aDirtyRect).ToUnknownRect(); MaybeSnapToDevicePixels(rect, aDrawTarget); if (aCallbacks) { aCallbacks->NotifySelectionBackgroundNeedsFill(rect, aColor, aDrawTarget); } else { ColorPattern color(ToDeviceColor(aColor)); aDrawTarget.FillRect(rect, color); } } // Attempt to get the LineBaselineOffset property of aChildFrame // If not set, calculate this value for all child frames of aBlockFrame static nscoord LazyGetLineBaselineOffset(nsIFrame* aChildFrame, nsBlockFrame* aBlockFrame) { bool offsetFound; nscoord offset = aChildFrame->GetProperty(nsIFrame::LineBaselineOffset(), &offsetFound); if (!offsetFound) { for (const auto& line : aBlockFrame->Lines()) { if (line.IsInline()) { int32_t n = line.GetChildCount(); nscoord lineBaseline = line.BStart() + line.GetLogicalAscent(); for (auto* lineFrame = line.mFirstChild; n > 0; lineFrame = lineFrame->GetNextSibling(), --n) { offset = lineBaseline - lineFrame->GetNormalPosition().y; lineFrame->SetProperty(nsIFrame::LineBaselineOffset(), offset); } } } return aChildFrame->GetProperty(nsIFrame::LineBaselineOffset(), &offsetFound); } else { return offset; } } static bool IsUnderlineRight(const ComputedStyle& aStyle) { // Check for 'left' or 'right' explicitly specified in the property; // if neither is there, we use auto positioning based on lang. const auto position = aStyle.StyleText()->mTextUnderlinePosition; if (position.IsLeft()) { return false; } if (position.IsRight()) { return true; } // If neither 'left' nor 'right' was specified, check the language. nsAtom* langAtom = aStyle.StyleFont()->mLanguage; if (!langAtom) { return false; } return nsStyleUtil::MatchesLanguagePrefix(langAtom, u"ja") || nsStyleUtil::MatchesLanguagePrefix(langAtom, u"ko") || nsStyleUtil::MatchesLanguagePrefix(langAtom, u"mn"); } void nsTextFrame::GetTextDecorations( nsPresContext* aPresContext, nsTextFrame::TextDecorationColorResolution aColorResolution, nsTextFrame::TextDecorations& aDecorations) { const nsCompatibility compatMode = aPresContext->CompatibilityMode(); bool useOverride = false; nscolor overrideColor = NS_RGBA(0, 0, 0, 0); bool nearestBlockFound = false; // Use writing mode of parent frame for orthogonal text frame to work. // See comment in nsTextFrame::DrawTextRunAndDecorations. WritingMode wm = GetParent()->GetWritingMode(); bool vertical = wm.IsVertical(); nscoord ascent = GetLogicalBaseline(wm); // physicalBlockStartOffset represents the offset from our baseline // to f's physical block start, which is top in horizontal writing // mode, and left in vertical writing modes, in our coordinate space. // This physical block start is logical block start in most cases, // but for vertical-rl, it is logical block end, and consequently in // that case, it starts from the descent instead of ascent. nscoord physicalBlockStartOffset = wm.IsVerticalRL() ? GetSize().width - ascent : ascent; // baselineOffset represents the offset from our baseline to f's baseline or // the nearest block's baseline, in our coordinate space, whichever is closest // during the particular iteration nscoord baselineOffset = 0; for (nsIFrame *f = this, *fChild = nullptr; f; fChild = f, f = nsLayoutUtils::GetParentOrPlaceholderFor(f)) { ComputedStyle* const context = f->Style(); if (!context->HasTextDecorationLines()) { break; } if (context->GetPseudoType() == PseudoStyleType::marker && (context->StyleList()->mListStylePosition == StyleListStylePosition::Outside || !context->StyleDisplay()->IsInlineOutsideStyle())) { // Outside ::marker pseudos, and inside markers that aren't inlines, don't // have text decorations. break; } const nsStyleTextReset* const styleTextReset = context->StyleTextReset(); StyleTextDecorationLine textDecorations = styleTextReset->mTextDecorationLine; bool ignoreSubproperties = false; auto lineStyle = styleTextReset->mTextDecorationStyle; if (textDecorations == StyleTextDecorationLine::SPELLING_ERROR || textDecorations == StyleTextDecorationLine::GRAMMAR_ERROR) { nscolor lineColor; float relativeSize; useOverride = nsTextPaintStyle::GetSelectionUnderline( this, nsTextPaintStyle::SelectionStyleIndex::SpellChecker, &lineColor, &relativeSize, &lineStyle); if (useOverride) { // We don't currently have a SelectionStyleIndex::GrammarChecker; for // now just use SpellChecker and change its color to green. overrideColor = textDecorations == StyleTextDecorationLine::SPELLING_ERROR ? lineColor : NS_RGBA(0, 128, 0, 255); textDecorations = StyleTextDecorationLine::UNDERLINE; ignoreSubproperties = true; } } if (!useOverride && (StyleTextDecorationLine::COLOR_OVERRIDE & textDecorations)) { // This handles the La // la la case. The link underline should be green. useOverride = true; overrideColor = nsLayoutUtils::GetTextColor( f, &nsStyleTextReset::mTextDecorationColor); } nsBlockFrame* fBlock = do_QueryFrame(f); const bool firstBlock = !nearestBlockFound && fBlock; // Not updating positions once we hit a parent block is equivalent to // the CSS 2.1 spec that blocks should propagate decorations down to their // children (albeit the style should be preserved) // However, if we're vertically aligned within a block, then we need to // recover the correct baseline from the line by querying the FrameProperty // that should be set (see nsLineLayout::VerticalAlignLine). if (firstBlock) { // At this point, fChild can't be null since TextFrames can't be blocks Maybe verticalAlign = fChild->VerticalAlignEnum(); if (verticalAlign != Some(StyleVerticalAlignKeyword::Baseline)) { // Since offset is the offset in the child's coordinate space, we have // to undo the accumulation to bring the transform out of the block's // coordinate space const nscoord lineBaselineOffset = LazyGetLineBaselineOffset(fChild, fBlock); baselineOffset = physicalBlockStartOffset - lineBaselineOffset - (vertical ? fChild->GetNormalPosition().x : fChild->GetNormalPosition().y); } } else if (!nearestBlockFound) { // offset here is the offset from f's baseline to f's top/left // boundary. It's descent for vertical-rl, and ascent otherwise. nscoord offset = wm.IsVerticalRL() ? f->GetSize().width - f->GetLogicalBaseline(wm) : f->GetLogicalBaseline(wm); baselineOffset = physicalBlockStartOffset - offset; } nearestBlockFound = nearestBlockFound || firstBlock; physicalBlockStartOffset += vertical ? f->GetNormalPosition().x : f->GetNormalPosition().y; if (textDecorations) { nscolor color; if (useOverride) { color = overrideColor; } else if (IsInSVGTextSubtree()) { // XXX We might want to do something with text-decoration-color when // painting SVG text, but it's not clear what we should do. We // at least need SVG text decorations to paint with 'fill' if // text-decoration-color has its initial value currentColor. // We could choose to interpret currentColor as "currentFill" // for SVG text, and have e.g. text-decoration-color:red to // override the fill paint of the decoration. color = aColorResolution == eResolvedColors ? nsLayoutUtils::GetTextColor(f, &nsStyleSVG::mFill) : NS_SAME_AS_FOREGROUND_COLOR; } else { color = nsLayoutUtils::GetTextColor( f, &nsStyleTextReset::mTextDecorationColor); } bool swapUnderlineAndOverline = wm.IsCentralBaseline() && IsUnderlineRight(*context); const auto kUnderline = swapUnderlineAndOverline ? StyleTextDecorationLine::OVERLINE : StyleTextDecorationLine::UNDERLINE; const auto kOverline = swapUnderlineAndOverline ? StyleTextDecorationLine::UNDERLINE : StyleTextDecorationLine::OVERLINE; const nsStyleText* const styleText = context->StyleText(); const auto position = ignoreSubproperties ? StyleTextUnderlinePosition::AUTO : styleText->mTextUnderlinePosition; const auto offset = ignoreSubproperties ? LengthPercentageOrAuto::Auto() : styleText->mTextUnderlineOffset; const auto thickness = ignoreSubproperties ? StyleTextDecorationLength::Auto() : styleTextReset->mTextDecorationThickness; if (textDecorations & kUnderline) { aDecorations.mUnderlines.AppendElement(nsTextFrame::LineDecoration( f, baselineOffset, position, offset, thickness, color, lineStyle, !ignoreSubproperties)); } if (textDecorations & kOverline) { aDecorations.mOverlines.AppendElement(nsTextFrame::LineDecoration( f, baselineOffset, position, offset, thickness, color, lineStyle, !ignoreSubproperties)); } if (textDecorations & StyleTextDecorationLine::LINE_THROUGH) { aDecorations.mStrikes.AppendElement(nsTextFrame::LineDecoration( f, baselineOffset, position, offset, thickness, color, lineStyle, !ignoreSubproperties)); } } // In all modes, if we're on an inline-block/table/grid/flex (or // -moz-inline-box), we're done. // If we're on a ruby frame other than ruby text container, we // should continue. mozilla::StyleDisplay display = f->GetDisplay(); if (!display.IsInlineFlow() && (!display.IsRuby() || display == mozilla::StyleDisplay::RubyTextContainer) && display.IsInlineOutside()) { break; } // In quirks mode, if we're on an HTML table element, we're done. if (compatMode == eCompatibility_NavQuirks && f->GetContent()->IsHTMLElement(nsGkAtoms::table)) { break; } // If we're on an absolutely-positioned element or a floating // element, we're done. if (f->IsFloating() || f->IsAbsolutelyPositioned()) { break; } // If we're an outer 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(container)->GetFontSizeScaleFactor(); } return nsLayoutUtils::FontSizeInflationInner(aFrame, aInflationMinFontSize); } struct EmphasisMarkInfo { RefPtr 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 GenerateTextRunForEmphasisMarks( nsTextFrame* aFrame, gfxFontGroup* aFontGroup, ComputedStyle* aComputedStyle, const nsStyleText* aStyleText) { nsAutoString string; ComputeTextEmphasisStyleString(aStyleText->mTextEmphasisStyle, string); RefPtr 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(string.get(), string.Length(), dt, appUnitsPerDevUnit, flags, nsTextFrameUtils::Flags(), nullptr); } static nsRubyFrame* FindFurthestInlineRubyAncestor(nsTextFrame* aFrame) { nsRubyFrame* rubyFrame = nullptr; for (nsIFrame* frame = aFrame->GetParent(); frame && frame->IsLineParticipant(); frame = frame->GetParent()) { if (frame->IsRubyFrame()) { rubyFrame = static_cast(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 fm = nsLayoutUtils::GetFontMetricsOfEmphasisMarks( computedStyle, PresContext(), GetFontSizeInflation()); EmphasisMarkInfo* info = new EmphasisMarkInfo; info->textRun = GenerateTextRunForEmphasisMarks( this, fm->GetThebesFontGroup(), computedStyle, styleText); info->advance = info->textRun->GetAdvanceWidth(); // Calculate the baseline offset LogicalSide side = styleText->TextEmphasisSide(aWM, StyleFont()->mLanguage); LogicalSize frameSize = GetLogicalSize(aWM); // The overflow rect is inflated in the inline direction by half // advance of the emphasis mark on each side, so that even if a mark // is drawn for a zero-width character, it won't be clipped. LogicalRect overflowRect(aWM, -info->advance / 2, /* BStart to be computed below */ 0, frameSize.ISize(aWM) + info->advance, fm->MaxAscent() + fm->MaxDescent()); RefPtr baseFontMetrics = isTextCombined ? nsLayoutUtils::GetInflatedFontMetricsForFrame(GetParent()) : do_AddRef(aProvider.GetFontMetrics()); // When the writing mode is vertical-lr the line is inverted, and thus // the ascent and descent are swapped. nscoord absOffset = (side == LogicalSide::BStart) != aWM.IsLineInverted() ? baseFontMetrics->MaxAscent() + fm->MaxDescent() : baseFontMetrics->MaxDescent() + fm->MaxAscent(); RubyBlockLeadings leadings; if (nsRubyFrame* ruby = FindFurthestInlineRubyAncestor(this)) { leadings = ruby->GetBlockLeadings(); } if (side == LogicalSide::BStart) { info->baselineOffset = -absOffset - leadings.mStart; overflowRect.BStart(aWM) = -overflowRect.BSize(aWM) - leadings.mStart; } else { MOZ_ASSERT(side == LogicalSide::BEnd); info->baselineOffset = absOffset + leadings.mEnd; overflowRect.BStart(aWM) = frameSize.BSize(aWM) + leadings.mEnd; } // If text combined, fix the gap between the text frame and its parent. if (isTextCombined) { nscoord gap = (baseFontMetrics->MaxHeight() - frameSize.BSize(aWM)) / 2; overflowRect.BStart(aWM) += gap * (side == LogicalSide::BStart ? -1 : 1); } SetProperty(EmphasisMarkProperty(), info); return overflowRect.GetPhysicalRect(aWM, frameSize.GetPhysicalSize(aWM)); } // helper function for implementing text-decoration-thickness // 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 font = fontMetrics->GetThebesFontGroup()->GetFirstValidFont(); const gfxFont::Metrics& metrics = font->GetMetrics(useVerticalMetrics ? nsFontMetrics::eVertical : nsFontMetrics::eHorizontal); params.defaultLineThickness = metrics.underlineSize; params.lineSize.height = ComputeDecorationLineThickness( aBlock->Style()->StyleTextReset()->mTextDecorationThickness, params.defaultLineThickness, metrics, appUnitsPerDevUnit, this); const auto* styleText = aBlock->StyleText(); bool swapUnderline = wm.IsCentralBaseline() && IsUnderlineRight(*aBlock->Style()); params.offset = ComputeDecorationLineOffset( StyleTextDecorationLine::UNDERLINE, styleText->mTextUnderlinePosition, styleText->mTextUnderlineOffset, metrics, appUnitsPerDevUnit, this, wm.IsCentralBaseline(), swapUnderline); nscoord maxAscent = inverted ? fontMetrics->MaxDescent() : fontMetrics->MaxAscent(); Float gfxWidth = (verticalRun ? aInkOverflowRect->height : aInkOverflowRect->width) / appUnitsPerDevUnit; params.lineSize.width = gfxWidth; params.ascent = gfxFloat(mAscent) / appUnitsPerDevUnit; params.style = decorationStyle; params.vertical = verticalRun; params.sidewaysLeft = mTextRun->IsSidewaysLeft(); params.decoration = StyleTextDecorationLine::UNDERLINE; nsRect underlineRect = nsCSSRendering::GetTextDecorationRect(aPresContext, params); // TODO(jfkthame): // Should we actually be calling ComputeDecorationLineOffset again here? params.offset = maxAscent / appUnitsPerDevUnit; params.decoration = StyleTextDecorationLine::OVERLINE; nsRect overlineRect = nsCSSRendering::GetTextDecorationRect(aPresContext, params); aInkOverflowRect->UnionRect(*aInkOverflowRect, underlineRect); aInkOverflowRect->UnionRect(*aInkOverflowRect, overlineRect); // XXX If strikeoutSize is much thicker than the underlineSize, it may // cause overflowing from the overflow rect. However, such case // isn't realistic, we don't need to compute it now. } if (aIncludeTextDecorations) { // Use writing mode of parent frame for orthogonal text frame to // work. See comment in nsTextFrame::DrawTextRunAndDecorations. WritingMode parentWM = GetParent()->GetWritingMode(); bool verticalDec = parentWM.IsVertical(); bool useVerticalMetrics = verticalDec != verticalRun ? verticalDec : verticalRun && mTextRun->UseCenterBaseline(); // Since CSS 2.1 requires that text-decoration defined on ancestors maintain // style and position, they can be drawn at virtually any y-offset, so // maxima and minima are required to reliably generate the rectangle for // them TextDecorations textDecs; GetTextDecorations(aPresContext, eResolvedColors, textDecs); if (textDecs.HasDecorationLines()) { nscoord inflationMinFontSize = nsLayoutUtils::InflationMinFontSizeFor(aBlock); const nscoord measure = verticalDec ? GetSize().height : GetSize().width; gfxFloat gfxWidth = measure / appUnitsPerDevUnit; gfxFloat ascent = gfxFloat(GetLogicalBaseline(parentWM)) / appUnitsPerDevUnit; nscoord frameBStart = 0; if (parentWM.IsVerticalRL()) { frameBStart = GetSize().width; ascent = -ascent; } nsCSSRendering::DecorationRectParams params; params.lineSize = Size(gfxWidth, 0); params.ascent = ascent; params.vertical = verticalDec; params.sidewaysLeft = mTextRun->IsSidewaysLeft(); nscoord topOrLeft(nscoord_MAX), bottomOrRight(nscoord_MIN); typedef gfxFont::Metrics Metrics; auto accumulateDecorationRect = [&](const LineDecoration& dec, gfxFloat Metrics::* lineSize, mozilla::StyleTextDecorationLine lineType) { params.style = dec.mStyle; // If the style is solid, let's include decoration line rect of // solid style since changing the style from none to // solid/dotted/dashed doesn't cause reflow. if (params.style == StyleTextDecorationStyle::None) { params.style = StyleTextDecorationStyle::Solid; } float inflation = GetInflationForTextDecorations( dec.mFrame, inflationMinFontSize); const Metrics metrics = GetFirstFontMetrics(GetFontGroupForFrame(dec.mFrame, inflation), useVerticalMetrics); params.defaultLineThickness = metrics.*lineSize; params.lineSize.height = ComputeDecorationLineThickness( dec.mTextDecorationThickness, params.defaultLineThickness, metrics, appUnitsPerDevUnit, this); bool swapUnderline = parentWM.IsCentralBaseline() && IsUnderlineRight(*Style()); params.offset = ComputeDecorationLineOffset( lineType, dec.mTextUnderlinePosition, dec.mTextUnderlineOffset, metrics, appUnitsPerDevUnit, this, parentWM.IsCentralBaseline(), swapUnderline); const nsRect decorationRect = nsCSSRendering::GetTextDecorationRect(aPresContext, params) + (verticalDec ? nsPoint(frameBStart - dec.mBaselineOffset, 0) : nsPoint(0, -dec.mBaselineOffset)); if (verticalDec) { topOrLeft = std::min(decorationRect.x, topOrLeft); bottomOrRight = std::max(decorationRect.XMost(), bottomOrRight); } else { topOrLeft = std::min(decorationRect.y, topOrLeft); bottomOrRight = std::max(decorationRect.YMost(), bottomOrRight); } }; // Below we loop through all text decorations and compute the rectangle // containing all of them, in this frame's coordinate space params.decoration = StyleTextDecorationLine::UNDERLINE; for (const LineDecoration& dec : textDecs.mUnderlines) { accumulateDecorationRect(dec, &Metrics::underlineSize, params.decoration); } params.decoration = StyleTextDecorationLine::OVERLINE; for (const LineDecoration& dec : textDecs.mOverlines) { accumulateDecorationRect(dec, &Metrics::underlineSize, params.decoration); } params.decoration = StyleTextDecorationLine::LINE_THROUGH; for (const LineDecoration& dec : textDecs.mStrikes) { accumulateDecorationRect(dec, &Metrics::strikeoutSize, params.decoration); } aInkOverflowRect->UnionRect( *aInkOverflowRect, verticalDec ? nsRect(topOrLeft, 0, bottomOrRight - topOrLeft, measure) : nsRect(0, topOrLeft, measure, bottomOrRight - topOrLeft)); } aInkOverflowRect->UnionRect(*aInkOverflowRect, UpdateTextEmphasis(parentWM, aProvider)); } // text-stroke overflows: add half of text-stroke-width on all sides nscoord textStrokeWidth = StyleText()->mWebkitTextStrokeWidth; if (textStrokeWidth > 0) { // Inflate rect by stroke-width/2; we add an extra pixel to allow for // antialiasing, rounding errors, etc. nsRect strokeRect = *aInkOverflowRect; strokeRect.Inflate(textStrokeWidth / 2 + appUnitsPerDevUnit); aInkOverflowRect->UnionRect(*aInkOverflowRect, strokeRect); } // Text-shadow overflows if (aIncludeShadows) { *aInkOverflowRect = nsLayoutUtils::GetTextShadowRectsUnion(*aInkOverflowRect, this); } // When this frame is not selected, the text-decoration area must be in // frame bounds. if (!IsSelected() || !CombineSelectionUnderlineRect(aPresContext, *aInkOverflowRect)) { return; } AddStateBits(TEXT_SELECTION_UNDERLINE_OVERFLOWED); } nscoord nsTextFrame::ComputeLineHeight() const { return ReflowInput::CalcLineHeight(*Style(), PresContext(), GetContent(), NS_UNCONSTRAINEDSIZE, GetFontSizeInflation()); } gfxFloat nsTextFrame::ComputeDescentLimitForSelectionUnderline( nsPresContext* aPresContext, const gfxFont::Metrics& aFontMetrics) { const gfxFloat lineHeight = gfxFloat(ComputeLineHeight()) / aPresContext->AppUnitsPerDevPixel(); if (lineHeight <= aFontMetrics.maxHeight) { return aFontMetrics.maxDescent; } return aFontMetrics.maxDescent + (lineHeight - aFontMetrics.maxHeight) / 2; } // Make sure this stays in sync with DrawSelectionDecorations below static constexpr SelectionTypeMask kSelectionTypesWithDecorations = ToSelectionTypeMask(SelectionType::eSpellCheck) | ToSelectionTypeMask(SelectionType::eURLStrikeout) | ToSelectionTypeMask(SelectionType::eIMERawClause) | ToSelectionTypeMask(SelectionType::eIMESelectedRawClause) | ToSelectionTypeMask(SelectionType::eIMEConvertedClause) | ToSelectionTypeMask(SelectionType::eIMESelectedClause); /* static */ gfxFloat nsTextFrame::ComputeSelectionUnderlineHeight( nsPresContext* aPresContext, const gfxFont::Metrics& aFontMetrics, SelectionType aSelectionType) { switch (aSelectionType) { case SelectionType::eIMERawClause: case SelectionType::eIMESelectedRawClause: case SelectionType::eIMEConvertedClause: case SelectionType::eIMESelectedClause: return aFontMetrics.underlineSize; case SelectionType::eSpellCheck: { // The thickness of the spellchecker underline shouldn't honor the font // metrics. It should be constant pixels value which is decided from the // default font size. Note that if the actual font size is smaller than // the default font size, we should use the actual font size because the // computed value from the default font size can be too thick for the // current font size. Length defaultFontSize = aPresContext->Document() ->GetFontPrefsForLang(nullptr) ->GetDefaultFont(StyleGenericFontFamily::None) ->size; int32_t zoomedFontSize = aPresContext->CSSPixelsToDevPixels( nsStyleFont::ZoomText(*aPresContext->Document(), defaultFontSize) .ToCSSPixels()); gfxFloat fontSize = std::min(gfxFloat(zoomedFontSize), aFontMetrics.emHeight); fontSize = std::max(fontSize, 1.0); return ceil(fontSize / 20); } default: NS_WARNING("Requested underline style is not valid"); return aFontMetrics.underlineSize; } } enum class DecorationType { Normal, Selection }; struct nsTextFrame::PaintDecorationLineParams : nsCSSRendering::DecorationRectParams { gfxContext* context = nullptr; LayoutDeviceRect dirtyRect; Point pt; const nscolor* overrideColor = nullptr; nscolor color = NS_RGBA(0, 0, 0, 0); gfxFloat icoordInFrame = 0.0f; gfxFloat baselineOffset = 0.0f; DecorationType decorationType = DecorationType::Normal; DrawPathCallbacks* callbacks = nullptr; bool paintingShadows = false; bool allowInkSkipping = true; }; void nsTextFrame::PaintDecorationLine( const PaintDecorationLineParams& aParams) { nsCSSRendering::PaintDecorationLineParams params; static_cast(params) = aParams; params.dirtyRect = aParams.dirtyRect.ToUnknownRect(); params.pt = aParams.pt; params.color = aParams.overrideColor ? *aParams.overrideColor : aParams.color; params.icoordInFrame = Float(aParams.icoordInFrame); params.baselineOffset = Float(aParams.baselineOffset); params.allowInkSkipping = aParams.allowInkSkipping; if (aParams.callbacks) { Rect path = nsCSSRendering::DecorationLineToPath(params); if (aParams.decorationType == DecorationType::Normal) { aParams.callbacks->PaintDecorationLine(path, aParams.paintingShadows, params.color); } else { aParams.callbacks->PaintSelectionDecorationLine( path, aParams.paintingShadows, params.color); } } else { nsCSSRendering::PaintDecorationLine(this, *aParams.context->GetDrawTarget(), params); } } static StyleTextDecorationStyle ToStyleLineStyle(const TextRangeStyle& aStyle) { switch (aStyle.mLineStyle) { case TextRangeStyle::LineStyle::None: return StyleTextDecorationStyle::None; case TextRangeStyle::LineStyle::Solid: return StyleTextDecorationStyle::Solid; case TextRangeStyle::LineStyle::Dotted: return StyleTextDecorationStyle::Dotted; case TextRangeStyle::LineStyle::Dashed: return StyleTextDecorationStyle::Dashed; case TextRangeStyle::LineStyle::Double: return StyleTextDecorationStyle::Double; case TextRangeStyle::LineStyle::Wavy: return StyleTextDecorationStyle::Wavy; } MOZ_ASSERT_UNREACHABLE("Invalid line style"); return StyleTextDecorationStyle::None; } /** * This, plus kSelectionTypesWithDecorations, encapsulates all knowledge * about drawing text decoration for selections. */ void nsTextFrame::DrawSelectionDecorations( gfxContext* aContext, const LayoutDeviceRect& aDirtyRect, SelectionType aSelectionType, nsTextPaintStyle& aTextPaintStyle, const TextRangeStyle& aRangeStyle, const Point& aPt, gfxFloat aICoordInFrame, gfxFloat aWidth, gfxFloat aAscent, const gfxFont::Metrics& aFontMetrics, DrawPathCallbacks* aCallbacks, bool aVertical, StyleTextDecorationLine aDecoration) { PaintDecorationLineParams params; params.context = aContext; params.dirtyRect = aDirtyRect; params.pt = aPt; params.lineSize.width = aWidth; params.ascent = aAscent; params.decoration = aDecoration; params.decorationType = DecorationType::Selection; params.callbacks = aCallbacks; params.vertical = aVertical; params.sidewaysLeft = mTextRun->IsSidewaysLeft(); params.descentLimit = ComputeDescentLimitForSelectionUnderline( aTextPaintStyle.PresContext(), aFontMetrics); float relativeSize; const auto& decThickness = StyleTextReset()->mTextDecorationThickness; const gfxFloat appUnitsPerDevPixel = aTextPaintStyle.PresContext()->AppUnitsPerDevPixel(); const WritingMode wm = GetWritingMode(); switch (aSelectionType) { case SelectionType::eIMERawClause: case SelectionType::eIMESelectedRawClause: case SelectionType::eIMEConvertedClause: case SelectionType::eIMESelectedClause: case SelectionType::eSpellCheck: case SelectionType::eHighlight: { auto index = nsTextPaintStyle::GetUnderlineStyleIndexForSelectionType( aSelectionType); bool weDefineSelectionUnderline = aTextPaintStyle.GetSelectionUnderlineForPaint( index, ¶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, nsAtom* aHighlightName, nsTextPaintStyle& aTextPaintStyle, const TextRangeStyle& aRangeStyle, nscolor* aForeground, nscolor* aBackground) { switch (aSelectionType) { case SelectionType::eNormal: return aTextPaintStyle.GetSelectionColors(aForeground, aBackground); case SelectionType::eFind: aTextPaintStyle.GetHighlightColors(aForeground, aBackground); return true; case SelectionType::eHighlight: { // Intentionally not short-cutting here because the called methods have // side-effects that affect outparams. bool hasForeground = aTextPaintStyle.GetCustomHighlightTextColor( aHighlightName, aForeground); bool hasBackground = aTextPaintStyle.GetCustomHighlightBackgroundColor( aHighlightName, aBackground); return hasForeground || hasBackground; } case SelectionType::eTargetText: { aTextPaintStyle.GetTargetTextColors(aForeground, aBackground); return true; } case SelectionType::eURLSecondary: aTextPaintStyle.GetURLSecondaryColor(aForeground); *aBackground = NS_RGBA(0, 0, 0, 0); return true; case SelectionType::eIMERawClause: case SelectionType::eIMESelectedRawClause: case SelectionType::eIMEConvertedClause: case SelectionType::eIMESelectedClause: if (aRangeStyle.IsDefined()) { if (!aRangeStyle.IsForegroundColorDefined() && !aRangeStyle.IsBackgroundColorDefined()) { *aForeground = aTextPaintStyle.GetTextColor(); *aBackground = NS_RGBA(0, 0, 0, 0); return false; } if (aRangeStyle.IsForegroundColorDefined()) { *aForeground = aRangeStyle.mForegroundColor; if (aRangeStyle.IsBackgroundColorDefined()) { *aBackground = aRangeStyle.mBackgroundColor; } else { // If foreground color is defined but background color isn't // defined, we can guess that IME must expect that the background // color is system's default field background color. *aBackground = aTextPaintStyle.GetSystemFieldBackgroundColor(); } } else { // aRangeStyle.IsBackgroundColorDefined() is true *aBackground = aRangeStyle.mBackgroundColor; // If background color is defined but foreground color isn't defined, // we can assume that IME must expect that the foreground color is // same as system's field text color. *aForeground = aTextPaintStyle.GetSystemFieldForegroundColor(); } return true; } aTextPaintStyle.GetIMESelectionColors( nsTextPaintStyle::GetUnderlineStyleIndexForSelectionType( aSelectionType), aForeground, aBackground); return true; default: *aForeground = aTextPaintStyle.GetTextColor(); *aBackground = NS_RGBA(0, 0, 0, 0); return false; } } /** * This sets *aShadows to the appropriate shadows, if any, for the given * type of selection. * If text-shadow was not specified, *aShadows is left untouched. */ void nsTextFrame::GetSelectionTextShadow( SelectionType aSelectionType, nsTextPaintStyle& aTextPaintStyle, Span* aShadows) { if (aSelectionType != SelectionType::eNormal) { return; } aTextPaintStyle.GetSelectionShadow(aShadows); } /** * This class lets us iterate over chunks of text recorded in an array of * resolved selection ranges, observing cluster boundaries, in content order, * maintaining the current x-offset as we go, and telling whether the text * chunk has a hyphen after it or not. * In addition to returning the selected chunks, the iterator is responsible * to interpolate unselected chunks in any gaps between them. * The caller is responsible for actually computing the advance width of each * chunk. */ class MOZ_STACK_CLASS SelectionRangeIterator { using PropertyProvider = nsTextFrame::PropertyProvider; using CombinedSelectionRange = nsTextFrame::PriorityOrderedSelectionsForRange; public: // aSelectionRanges and aRange are according to the original string. SelectionRangeIterator( const nsTArray& aSelectionRanges, gfxTextRun::Range aRange, PropertyProvider& aProvider, gfxTextRun* aTextRun, gfxFloat aXOffset); bool GetNextSegment(gfxFloat* aXOffset, gfxTextRun::Range* aRange, gfxFloat* aHyphenWidth, nsTArray& aSelectionType, nsTArray>& aHighlightName, nsTArray& aStyle); void UpdateWithAdvance(gfxFloat aAdvance) { mXOffset += aAdvance * mTextRun->GetDirection(); } private: const nsTArray& mSelectionRanges; PropertyProvider& mProvider; gfxTextRun* mTextRun; gfxSkipCharsIterator mIterator; gfxTextRun::Range mOriginalRange; gfxFloat mXOffset; uint32_t mIndex; }; SelectionRangeIterator::SelectionRangeIterator( const nsTArray& aSelectionRanges, gfxTextRun::Range aRange, PropertyProvider& aProvider, gfxTextRun* aTextRun, gfxFloat aXOffset) : mSelectionRanges(aSelectionRanges), mProvider(aProvider), mTextRun(aTextRun), mIterator(aProvider.GetStart()), mOriginalRange(aRange), mXOffset(aXOffset), mIndex(0) { mIterator.SetOriginalOffset(int32_t(aRange.start)); } bool SelectionRangeIterator::GetNextSegment( gfxFloat* aXOffset, gfxTextRun::Range* aRange, gfxFloat* aHyphenWidth, nsTArray& aSelectionType, nsTArray>& aHighlightName, nsTArray& aStyle) { if (mIterator.GetOriginalOffset() >= int32_t(mOriginalRange.end)) { return false; } uint32_t runOffset = mIterator.GetSkippedOffset(); uint32_t segmentEnd = mOriginalRange.end; aSelectionType.Clear(); aHighlightName.Clear(); aStyle.Clear(); if (mIndex == mSelectionRanges.Length() || mIterator.GetOriginalOffset() < int32_t(mSelectionRanges[mIndex].mRange.start)) { // There's an unselected segment before the next range (or at the end). aSelectionType.AppendElement(SelectionType::eNone); aHighlightName.AppendElement(); aStyle.AppendElement(TextRangeStyle()); if (mIndex < mSelectionRanges.Length()) { segmentEnd = mSelectionRanges[mIndex].mRange.start; } } else { // Get the selection details for the next segment, and increment index. for (const SelectionDetails* sdptr : mSelectionRanges[mIndex].mSelectionRanges) { aSelectionType.AppendElement(sdptr->mSelectionType); aHighlightName.AppendElement(sdptr->mHighlightData.mHighlightName); aStyle.AppendElement(sdptr->mTextRangeStyle); } segmentEnd = mSelectionRanges[mIndex].mRange.end; ++mIndex; } // Advance iterator to the end of the segment. mIterator.SetOriginalOffset(int32_t(segmentEnd)); // Further advance if necessary to a cluster boundary. while (mIterator.GetOriginalOffset() < int32_t(mOriginalRange.end) && !mIterator.IsOriginalCharSkipped() && !mTextRun->IsClusterStart(mIterator.GetSkippedOffset())) { mIterator.AdvanceOriginal(1); } aRange->start = runOffset; aRange->end = mIterator.GetSkippedOffset(); *aXOffset = mXOffset; *aHyphenWidth = 0; if (mIterator.GetOriginalOffset() == int32_t(mOriginalRange.end) && mProvider.GetFrame()->HasAnyStateBits(TEXT_HYPHEN_BREAK)) { *aHyphenWidth = mProvider.GetHyphenWidth(); } return true; } static void AddHyphenToMetrics(nsTextFrame* aTextFrame, bool aIsRightToLeft, gfxTextRun::Metrics* aMetrics, gfxFont::BoundingBoxType aBoundingBoxType, DrawTarget* aDrawTarget) { // Fix up metrics to include hyphen RefPtr hyphenTextRun = GetHyphenTextRun(aTextFrame, aDrawTarget); if (!hyphenTextRun) { return; } gfxTextRun::Metrics hyphenMetrics = hyphenTextRun->MeasureText(aBoundingBoxType, aDrawTarget); if (aTextFrame->GetWritingMode().IsLineInverted()) { hyphenMetrics.mBoundingBox.y = -hyphenMetrics.mBoundingBox.YMost(); } aMetrics->CombineWith(hyphenMetrics, aIsRightToLeft); } void nsTextFrame::PaintOneShadow(const PaintShadowParams& aParams, const StyleSimpleShadow& aShadowDetails, gfxRect& aBoundingBox, uint32_t aBlurFlags) { AUTO_PROFILER_LABEL("nsTextFrame::PaintOneShadow", GRAPHICS); nsPoint shadowOffset(aShadowDetails.horizontal.ToAppUnits(), aShadowDetails.vertical.ToAppUnits()); nscoord blurRadius = std::max(aShadowDetails.blur.ToAppUnits(), 0); nscolor shadowColor = aShadowDetails.color.CalcColor(aParams.foregroundColor); if (auto* textDrawer = aParams.context->GetTextDrawer()) { wr::Shadow wrShadow; wrShadow.offset = {PresContext()->AppUnitsToFloatDevPixels(shadowOffset.x), PresContext()->AppUnitsToFloatDevPixels(shadowOffset.y)}; wrShadow.blur_radius = PresContext()->AppUnitsToFloatDevPixels(blurRadius); wrShadow.color = wr::ToColorF(ToDeviceColor(shadowColor)); bool inflate = true; textDrawer->AppendShadow(wrShadow, inflate); return; } // This rect is the box which is equivalent to where the shadow will be // painted. The origin of aBoundingBox is the text baseline left, so we must // translate it by that much in order to make the origin the top-left corner // of the text bounding box. Note that aLeftSideOffset is line-left, so // actually means top offset in vertical writing modes. gfxRect shadowGfxRect; WritingMode wm = GetWritingMode(); if (wm.IsVertical()) { shadowGfxRect = aBoundingBox; if (wm.IsVerticalRL()) { // for vertical-RL, reverse direction of x-coords of bounding box shadowGfxRect.x = -shadowGfxRect.XMost(); } shadowGfxRect += gfxPoint(aParams.textBaselinePt.x, aParams.framePt.y + aParams.leftSideOffset); } else { shadowGfxRect = aBoundingBox + gfxPoint(aParams.framePt.x + aParams.leftSideOffset, aParams.textBaselinePt.y); } Point shadowGfxOffset(shadowOffset.x, shadowOffset.y); shadowGfxRect += gfxPoint(shadowGfxOffset.x, shadowOffset.y); nsRect shadowRect(NSToCoordRound(shadowGfxRect.X()), NSToCoordRound(shadowGfxRect.Y()), NSToCoordRound(shadowGfxRect.Width()), NSToCoordRound(shadowGfxRect.Height())); nsContextBoxBlur contextBoxBlur; const auto A2D = PresContext()->AppUnitsPerDevPixel(); gfxContext* shadowContext = contextBoxBlur.Init(shadowRect, 0, blurRadius, A2D, aParams.context, LayoutDevicePixel::ToAppUnits(aParams.dirtyRect, A2D), nullptr, aBlurFlags); if (!shadowContext) { return; } aParams.context->Save(); aParams.context->SetColor(sRGBColor::FromABGR(shadowColor)); // Draw the text onto our alpha-only surface to capture the alpha values. // Remember that the box blur context has a device offset on it, so we don't // need to translate any coordinates to fit on the surface. gfxFloat advanceWidth; nsTextPaintStyle textPaintStyle(this); DrawTextParams params(shadowContext, PresContext()->FontPaletteCache()); params.paintingShadows = true; params.advanceWidth = &advanceWidth; params.dirtyRect = aParams.dirtyRect; params.framePt = aParams.framePt + shadowGfxOffset; params.provider = aParams.provider; params.textStyle = &textPaintStyle; params.textColor = aParams.context == shadowContext ? shadowColor : NS_RGB(0, 0, 0); params.callbacks = aParams.callbacks; params.clipEdges = aParams.clipEdges; params.drawSoftHyphen = HasAnyStateBits(TEXT_HYPHEN_BREAK); // Multi-color shadow is not allowed, so we use the same color as the text // color. params.decorationOverrideColor = ¶ms.textColor; params.fontPalette = StyleFont()->GetFontPaletteAtom(); DrawText(aParams.range, aParams.textBaselinePt + shadowGfxOffset, params); contextBoxBlur.DoPaint(); aParams.context->Restore(); } /* static */ SelectionTypeMask nsTextFrame::CreateSelectionRangeList( const SelectionDetails* aDetails, SelectionType aSelectionType, const PaintTextSelectionParams& aParams, nsTArray& aSelectionRanges, bool* aAnyBackgrounds) { SelectionTypeMask allTypes = 0; bool anyBackgrounds = false; uint32_t priorityOfInsertionOrder = 0; for (const SelectionDetails* sd = aDetails; sd; sd = sd->mNext.get()) { MOZ_ASSERT(sd->mStart >= 0 && sd->mEnd >= 0); // XXX make unsigned? uint32_t start = std::max(aParams.contentRange.start, uint32_t(sd->mStart)); uint32_t end = std::min(aParams.contentRange.end, uint32_t(sd->mEnd)); if (start < end) { // The PaintTextWithSelectionColors caller passes SelectionType::eNone, // so we collect all selections that set colors, and prioritize them // according to selection type (lower types take precedence). if (aSelectionType == SelectionType::eNone) { allTypes |= ToSelectionTypeMask(sd->mSelectionType); // Ignore selections that don't set colors. nscolor foreground(0), background(0); if (GetSelectionTextColors(sd->mSelectionType, sd->mHighlightData.mHighlightName, *aParams.textPaintStyle, sd->mTextRangeStyle, &foreground, &background)) { if (NS_GET_A(background) > 0) { anyBackgrounds = true; } aSelectionRanges.AppendElement( SelectionRange{sd, {start, end}, priorityOfInsertionOrder++}); } } else if (sd->mSelectionType == aSelectionType) { // The PaintSelectionTextDecorations caller passes a specific type, // so we include only ranges of that type, and keep them in order // so that later ones take precedence over earlier. aSelectionRanges.AppendElement( SelectionRange{sd, {start, end}, priorityOfInsertionOrder++}); } } } if (aAnyBackgrounds) { *aAnyBackgrounds = anyBackgrounds; } return allTypes; } /* static */ void nsTextFrame::CombineSelectionRanges( const nsTArray& aSelectionRanges, nsTArray& aCombinedSelectionRanges) { struct SelectionRangeEndCmp { bool Equals(const SelectionRange* a, const SelectionRange* b) const { return a->mRange.end == b->mRange.end; } bool LessThan(const SelectionRange* a, const SelectionRange* b) const { return a->mRange.end < b->mRange.end; } }; struct SelectionRangePriorityCmp { bool Equals(const SelectionRange* a, const SelectionRange* b) const { const SelectionDetails* aDetails = a->mDetails; const SelectionDetails* bDetails = b->mDetails; if (aDetails->mSelectionType != bDetails->mSelectionType) { return false; } if (aDetails->mSelectionType != SelectionType::eHighlight) { return a->mPriority == b->mPriority; } if (aDetails->mHighlightData.mHighlight->Priority() != bDetails->mHighlightData.mHighlight->Priority()) { return false; } return a->mPriority == b->mPriority; } bool LessThan(const SelectionRange* a, const SelectionRange* b) const { if (a->mDetails->mSelectionType != b->mDetails->mSelectionType) { // Even though this looks counter-intuitive, // this is intended, as values in `SelectionType` are inverted: // a lower value indicates a higher priority. return a->mDetails->mSelectionType > b->mDetails->mSelectionType; } if (a->mDetails->mSelectionType != SelectionType::eHighlight) { // for non-highlights, the selection which was added later // has a higher priority. return a->mPriority < b->mPriority; } if (a->mDetails->mHighlightData.mHighlight->Priority() != b->mDetails->mHighlightData.mHighlight->Priority()) { // For highlights, first compare the priorities set by the user. return a->mDetails->mHighlightData.mHighlight->Priority() < b->mDetails->mHighlightData.mHighlight->Priority(); } // only if the user priorities are equal, let the highlight that was added // later take precedence. return a->mPriority < b->mPriority; } }; uint32_t currentOffset = 0; AutoTArray activeSelectionsForCurrentSegment; size_t rangeIndex = 0; // Divide the given selection ranges into segments which share the same // set of selections. // The following algorithm iterates `aSelectionRanges`, assuming // that its elements are sorted by their start offset. // Each time a new selection starts, it is pushed into an array of // "currently present" selections, sorted by their *end* offset. // For each iteration the next segment end offset is determined, // which is either the start offset of the next selection or // the next end offset of all "currently present" selections // (which is always the first element of the array because of its order). // Then, a `CombinedSelectionRange` can be constructed, which describes // the text segment until its end offset (as determined above), and contains // all elements of the "currently present" selection list, now sorted // by their priority. // If a range ends at the given offset, it is removed from the array. while (rangeIndex < aSelectionRanges.Length() || !activeSelectionsForCurrentSegment.IsEmpty()) { uint32_t currentSegmentEndOffset = activeSelectionsForCurrentSegment.IsEmpty() ? -1 : activeSelectionsForCurrentSegment[0]->mRange.end; uint32_t nextRangeStartOffset = rangeIndex < aSelectionRanges.Length() ? aSelectionRanges[rangeIndex].mRange.start : -1; uint32_t nextOffset = std::min(currentSegmentEndOffset, nextRangeStartOffset); if (!activeSelectionsForCurrentSegment.IsEmpty() && currentOffset != nextOffset) { auto activeSelectionRangesSortedByPriority = activeSelectionsForCurrentSegment.Clone(); activeSelectionRangesSortedByPriority.Sort(SelectionRangePriorityCmp()); AutoTArray selectionDetails; selectionDetails.SetCapacity( activeSelectionRangesSortedByPriority.Length()); // ensure that overlapping highlights which have the same name // are only added once. If added each time, they would be painted // several times (see wpt // /css/css-highlight-api/painting/custom-highlight-painting-003.html) // Comparing the highlight name with the previous one is // sufficient here because selections are already sorted // in a way that ensures that highlights of the same name are // grouped together. nsAtom* currentHighlightName = nullptr; for (const auto* selectionRange : activeSelectionRangesSortedByPriority) { if (selectionRange->mDetails->mSelectionType == SelectionType::eHighlight) { if (selectionRange->mDetails->mHighlightData.mHighlightName == currentHighlightName) { continue; } currentHighlightName = selectionRange->mDetails->mHighlightData.mHighlightName; } selectionDetails.AppendElement(selectionRange->mDetails); } aCombinedSelectionRanges.AppendElement(PriorityOrderedSelectionsForRange{ std::move(selectionDetails), {currentOffset, nextOffset}}); } currentOffset = nextOffset; if (nextRangeStartOffset < currentSegmentEndOffset) { activeSelectionsForCurrentSegment.InsertElementSorted( &aSelectionRanges[rangeIndex++], SelectionRangeEndCmp()); } else { activeSelectionsForCurrentSegment.RemoveElementAt(0); } } } SelectionTypeMask nsTextFrame::ResolveSelections( const PaintTextSelectionParams& aParams, const SelectionDetails* aDetails, nsTArray& aResult, SelectionType aSelectionType, bool* aAnyBackgrounds) const { AutoTArray selectionRanges; SelectionTypeMask allTypes = CreateSelectionRangeList( aDetails, aSelectionType, aParams, selectionRanges, aAnyBackgrounds); if (selectionRanges.IsEmpty()) { return allTypes; } struct SelectionRangeStartCmp { bool Equals(const SelectionRange& a, const SelectionRange& b) const { return a.mRange.start == b.mRange.start; } bool LessThan(const SelectionRange& a, const SelectionRange& b) const { return a.mRange.start < b.mRange.start; } }; selectionRanges.Sort(SelectionRangeStartCmp()); CombineSelectionRanges(selectionRanges, aResult); return allTypes; } // Paints selection backgrounds and text in the correct colors. Also computes // aAllSelectionTypeMask, the union of all selection types that are applying to // this text. bool nsTextFrame::PaintTextWithSelectionColors( const PaintTextSelectionParams& aParams, const UniquePtr& aDetails, SelectionTypeMask* aAllSelectionTypeMask, const ClipEdges& aClipEdges) { bool anyBackgrounds = false; AutoTArray selectionRanges; *aAllSelectionTypeMask = ResolveSelections(aParams, aDetails.get(), selectionRanges, SelectionType::eNone, &anyBackgrounds); bool vertical = mTextRun->IsVertical(); const gfxFloat startIOffset = vertical ? aParams.textBaselinePt.y - aParams.framePt.y : aParams.textBaselinePt.x - aParams.framePt.x; gfxFloat iOffset, hyphenWidth; Range range; // in transformed string const gfxTextRun::Range& contentRange = aParams.contentRange; auto* textDrawer = aParams.context->GetTextDrawer(); if (anyBackgrounds && !aParams.IsGenerateTextMask()) { int32_t appUnitsPerDevPixel = aParams.textPaintStyle->PresContext()->AppUnitsPerDevPixel(); SelectionRangeIterator iterator(selectionRanges, contentRange, *aParams.provider, mTextRun, startIOffset); AutoTArray selectionTypes; AutoTArray, 1> highlightNames; AutoTArray rangeStyles; while (iterator.GetNextSegment(&iOffset, &range, &hyphenWidth, selectionTypes, highlightNames, rangeStyles)) { nscolor foreground(0), background(0); gfxFloat advance = hyphenWidth + mTextRun->GetAdvanceWidth(range, aParams.provider); nsRect bgRect; gfxFloat offs = iOffset - (mTextRun->IsInlineReversed() ? advance : 0); if (vertical) { bgRect = nsRect(nscoord(aParams.framePt.x), nscoord(aParams.framePt.y + offs), GetSize().width, nscoord(advance)); } else { bgRect = nsRect(nscoord(aParams.framePt.x + offs), nscoord(aParams.framePt.y), nscoord(advance), GetSize().height); } LayoutDeviceRect selectionRect = LayoutDeviceRect::FromAppUnits(bgRect, appUnitsPerDevPixel); // The elements in `selectionTypes` are ordered ascending by their // priority. To account for non-opaque overlapping selections, all // selection backgrounds are painted. for (size_t index = 0; index < selectionTypes.Length(); ++index) { GetSelectionTextColors(selectionTypes[index], highlightNames[index], *aParams.textPaintStyle, rangeStyles[index], &foreground, &background); // Draw background color if (NS_GET_A(background) > 0) { if (textDrawer) { textDrawer->AppendSelectionRect(selectionRect, ToDeviceColor(background)); } else { PaintSelectionBackground(*aParams.context->GetDrawTarget(), background, aParams.dirtyRect, selectionRect, aParams.callbacks); } } } iterator.UpdateWithAdvance(advance); } } gfxFloat advance; DrawTextParams params(aParams.context, PresContext()->FontPaletteCache()); params.dirtyRect = aParams.dirtyRect; params.framePt = aParams.framePt; params.provider = aParams.provider; params.textStyle = aParams.textPaintStyle; params.clipEdges = &aClipEdges; params.advanceWidth = &advance; params.callbacks = aParams.callbacks; params.glyphRange = aParams.glyphRange; params.fontPalette = StyleFont()->GetFontPaletteAtom(); params.hasTextShadow = !StyleText()->mTextShadow.IsEmpty(); PaintShadowParams shadowParams(aParams); shadowParams.provider = aParams.provider; shadowParams.callbacks = aParams.callbacks; shadowParams.clipEdges = &aClipEdges; // Draw text const nsStyleText* textStyle = StyleText(); SelectionRangeIterator iterator(selectionRanges, contentRange, *aParams.provider, mTextRun, startIOffset); AutoTArray selectionTypes; AutoTArray, 1> highlightNames; AutoTArray rangeStyles; while (iterator.GetNextSegment(&iOffset, &range, &hyphenWidth, selectionTypes, highlightNames, rangeStyles)) { nscolor foreground(0), background(0); if (aParams.IsGenerateTextMask()) { foreground = NS_RGBA(0, 0, 0, 255); } else { nscolor tmpForeground(0); bool colorHasBeenSet = false; for (size_t index = 0; index < selectionTypes.Length(); ++index) { if (selectionTypes[index] == SelectionType::eHighlight) { if (aParams.textPaintStyle->GetCustomHighlightTextColor( highlightNames[index], &tmpForeground)) { foreground = tmpForeground; colorHasBeenSet = true; } } else { GetSelectionTextColors(selectionTypes[index], highlightNames[index], *aParams.textPaintStyle, rangeStyles[index], &foreground, &background); colorHasBeenSet = true; } } if (!colorHasBeenSet) { foreground = tmpForeground; } } 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 shadows = textStyle->mTextShadow.AsSpan(); for (auto selectionType : selectionTypes) { 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& aDetails, SelectionType aSelectionType) { // Hide text decorations if we're currently hiding @font-face fallback text if (aParams.provider->GetFontGroup()->ShouldSkipDrawing()) { return; } AutoTArray selectionRanges; ResolveSelections(aParams, aDetails.get(), selectionRanges, aSelectionType); RefPtr 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; } AutoTArray nextSelectionTypes; AutoTArray, 1> highlightNames; AutoTArray selectedStyles; while (iterator.GetNextSegment(&iOffset, &range, &hyphenWidth, nextSelectionTypes, highlightNames, selectedStyles)) { gfxFloat advance = hyphenWidth + mTextRun->GetAdvanceWidth(range, aParams.provider); for (size_t index = 0; index < nextSelectionTypes.Length(); ++index) { if (nextSelectionTypes[index] == 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, selectedStyles[index], 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 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 = std::size(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::GetTextColor(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, PresContext()->FontPaletteCache()); } else { pt.y += (GetSize().height - info->advance) / 2; gfxTextRun::DrawParams params(aContext, PresContext()->FontPaletteCache()); 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 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->mHighlightData.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 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.callbacks = aParams.callbacks; shadowParams.foregroundColor = foregroundColor; shadowParams.clipEdges = &clipEdges; PaintShadows(textStyle->mTextShadow.AsSpan(), shadowParams); } gfxFloat advanceWidth; DrawTextParams params(aParams.context, PresContext()->FontPaletteCache()); 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.hasTextShadow = !StyleText()->mTextShadow.IsEmpty(); 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, aParams.paletteCache); params.provider = aParams.provider; params.advanceWidth = aParams.advanceWidth; params.contextPaint = aParams.contextPaint; params.fontPalette = aParams.fontPalette; params.callbacks = aParams.callbacks; params.hasTextShadow = aParams.hasTextShadow; if (aParams.callbacks) { aParams.callbacks->NotifyBeforeText(aParams.paintingShadows, 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 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; params.paintingShadows = aParams.paintingShadows; // 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); if (GetTextRun(nsTextFrame::eInflated)->IsRightToLeft()) { pt.x += gfxFloat(frameSize.width) / 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; params.allowInkSkipping = dec.mAllowInkSkipping; 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 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 ? CaretAssociationHint::After : CaretAssociationHint::Before; return offsets; } bool nsTextFrame::CombineSelectionUnderlineRect(nsPresContext* aPresContext, nsRect& aRect) { if (aRect.IsEmpty()) { return false; } nsRect givenRect = aRect; gfxFontGroup* fontGroup = GetInflatedFontGroupForFrame(this); RefPtr 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; if (verticalRun) { EnsureTextRun(nsTextFrame::eInflated); params.sidewaysLeft = mTextRun ? mTextRun->IsSidewaysLeft() : false; } else { params.sidewaysLeft = false; } UniquePtr 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; auto 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(), PresShell()->GetSelectionNodeCache()); mIsSelected = isSelected ? nsTextFrame::SelectionState::Selected : nsTextFrame::SelectionState::NotSelected; } else { #ifdef DEBUG // Assert that the selection caching works. const bool isReallySelected = GetContent()->IsSelected(GetContentOffset(), GetContentEnd()); MOZ_ASSERT((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 (Style()->IsTextCombined()) { iSize *= GetTextCombineScaleFactor(this); } if (mTextRun->IsInlineReversed()) { point.x = mRect.width - iSize; } else { point.x = iSize; } } 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& 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); // Initialize iter; this will call FindClusterStart if necessary to align // iter to a cluster boundary. UpdateIteratorFromOffset(properties, aInOffset, iter); nsPoint point = GetPointFromIterator(iter, properties); const int32_t kContentEnd = GetContentEnd(); const int32_t kEndOffset = std::min(aInOffset + aLength, kContentEnd); if (aInOffset >= kEndOffset) { return NS_OK; } if (!aRects.SetCapacity(aRects.Length() + kEndOffset - aInOffset, mozilla::fallible)) { return NS_ERROR_OUT_OF_MEMORY; } do { // We'd like to assert here that |point| matches // |GetPointFromIterator(iter, properties)|, which in principle should be // true; however, testcases with vast dimensions can lead to coordinate // overflow and disrupt the calculations. So we've dropped the assertion // to avoid tripping the fuzzer unnecessarily. // Measure to the end of the cluster. nscoord iSize = 0; gfxSkipCharsIterator nextIter(iter); if (aInOffset < kContentEnd) { 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); } // Compute the cluster rect, depending on directionality, and update // point to the origin we'll need for the next cluster. nsRect rect; rect.x = point.x; rect.y = point.y; 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; point.y -= iSize; } else { point.y += iSize; } } else { if (Style()->IsTextCombined()) { // The scale factor applies to the inline advance of the glyphs, so it // affects both the rect width and the origin point for the next glyph. iSize *= GetTextCombineScaleFactor(this); } rect.width = iSize; rect.height = mRect.height; 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. rect.x -= iSize; point.x -= iSize; } else { point.x += iSize; } } // Set the rect for all characters in the cluster. int32_t end = std::min(kEndOffset, nextIter.GetOriginalOffset()); while (aInOffset < end) { aRects.AppendElement(rect); aInOffset++; } // Advance iter for the next cluster. iter = nextIter; } while (aInOffset < kEndOffset); 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 + . 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 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(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"); const char16_t ch = mFrag->CharAt(AssertedCast(mCharIndex)); return mozilla::IsPunctuationForWordSelect(ch); } int32_t ClusterIterator::GetAfterInternal() const { if (mFrag->IsHighSurrogateFollowedByLowSurrogateAt( AssertedCast(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(); const uint32_t textOffset = AssertedCast(aTextFrame->GetContentOffset()); const uint32_t textLen = AssertedCast(aTextFrame->GetContentLength()); // If we're in a password field, some characters may be masked. In such // case, we need to treat each masked character as 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(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()); // Note that aTextFrame may not cover the whole of mFrag (in cases with // bidi continuations), so we cannot rely on its textrun (and associated // styles) being available for the entire fragment. uint32_t i = 0; // Just copy any text that precedes what aTextFrame covers. while (i < textOffset) { maskedText.Append(mFrag->CharAt(i++)); } // For the range covered by aTextFrame, mask chars if appropriate. while (i < textOffset + textLen) { uint32_t skippedOffset = mIterator.ConvertOriginalToSkipped(i); bool mask = skippedOffset < transformedTextRun->GetLength() ? transformedTextRun->mStyles[skippedOffset]->mMaskPassword : false; if (mFrag->IsHighSurrogateFollowedByLowSurrogateAt(i)) { if (mask) { maskedText.Append(kPasswordMask); maskedText.Append(kPasswordMask); } else { maskedText.Append(mFrag->CharAt(i)); maskedText.Append(mFrag->CharAt(i + 1)); } i += 2; } else { maskedText.Append(mask ? kPasswordMask : mFrag->CharAt(i)); ++i; } } // Copy any trailing text from the fragment. while (i < mFrag->GetLength()) { maskedText.Append(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); // 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 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 nsTextFrame::GetOffsets() const { return std::make_pair(GetContentOffset(), GetContentEnd()); } static bool IsFirstLetterPrefixPunctuation(uint32_t aChar) { switch (mozilla::unicode::GetGeneralCategory(aChar)) { case HB_UNICODE_GENERAL_CATEGORY_CONNECT_PUNCTUATION: /* Pc */ 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 */ return true; default: return false; } } static bool IsFirstLetterSuffixPunctuation(uint32_t aChar) { switch (mozilla::unicode::GetGeneralCategory(aChar)) { case HB_UNICODE_GENERAL_CATEGORY_CONNECT_PUNCTUATION: /* Pc */ 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 */ return true; default: return false; } } static int32_t FindEndOfPrefixPunctuationRun(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 (IsFirstLetterPrefixPunctuation( aFrag->ScalarValueAt(AssertedCast(aOffset + i)))) { aIter->SetOriginalOffset(aOffset + i); FindClusterEnd(aTextRun, aEnd, aIter); i = aIter->GetOriginalOffset() - aOffset; } else { break; } } return i; } static int32_t FindEndOfSuffixPunctuationRun(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 (IsFirstLetterSuffixPunctuation( aFrag->ScalarValueAt(AssertedCast(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 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 any trimmable leading whitespace. int32_t i = GetTrimmableWhitespaceCount(aFrag, aOffset, length, 1); while (true) { // Scan past any leading punctuation. This leaves `j` at the first // non-punctuation character. int32_t j = FindEndOfPrefixPunctuationRun(aFrag, aTextRun, &iter, aOffset, i, endOffset); if (j == length) { return false; } // Scan past any Unicode whitespace characters after punctuation. while (j < length) { char16_t ch = aFrag->CharAt(AssertedCast(aOffset + j)); // The spec says to allow "characters that belong to the `Zs` Unicode // general category _other than_ U+3000" here. if (unicode::GetGeneralCategory(ch) == HB_UNICODE_GENERAL_CATEGORY_SPACE_SEPARATOR && ch != 0x3000) { ++j; } else { break; } } if (j == length) { return false; } if (j == i) { // If no whitespace was found, we've finished the first-letter prefix; // if there was some, then go back to check for more punctuation. break; } i = j; } // 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(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; } // NOTE that FindClusterEnd sets the iterator to the last character that is // part of the cluster, NOT to the first character beyond it. iter.SetOriginalOffset(aOffset + i); FindClusterEnd(aTextRun, endOffset, &iter, allowSplitLigature); // Index of the last character included in the first-letter cluster. 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(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(aOffset + i)); char16_t ch2 = aFrag->CharAt(AssertedCast(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; } } } // When we reach here, `i` points to the last character of the first-letter // cluster, NOT to the first character beyond it. Advance to the next char, // ready to check for following whitespace/punctuation: ++i; while (i < length) { // Skip over whitespace, except for word separator characters, before the // check for following punctuation. But remember the position before the // whitespace, in case we need to reset. const int32_t preWS = i; while (i < length) { char16_t ch = aFrag->CharAt(AssertedCast(aOffset + i)); // The spec says the first-letter suffix includes "any intervening // typographic space -- characters belonging to the Zs Unicode general // category other than U+3000 IDEOGRAPHIC SPACE or a word separator", // where "word separator" includes U+0020 and U+00A0. if (ch == 0x0020 || ch == 0x00A0 || ch == 0x3000 || unicode::GetGeneralCategory(ch) != HB_UNICODE_GENERAL_CATEGORY_SPACE_SEPARATOR) { break; } else { ++i; } } // Consume clusters that start with punctuation. const int32_t prePunct = i; i = FindEndOfSuffixPunctuationRun(aFrag, aTextRun, &iter, aOffset, i, endOffset); // If we didn't find punctuation here, then we also don't want to include // any preceding whitespace, so reset our index. if (i == prePunct) { i = preWS; break; } } 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, 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(GetContentOffset() + GetInFlowContentLength(), tmp.ConvertSkippedToOriginal(flowEndInTextRun)) - iter.GetOriginalOffset(); } PropertyProvider provider(textRun, textStyle, frag, this, iter, len, nullptr, 0, aTextRunType, aData->mAtStartOfLine); 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 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; } provider.SetStartOfLine(iter); } } 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; } void nsTextFrame::MaybeSplitFramesForFirstLetter() { if (!StaticPrefs::layout_css_intrinsic_size_first_letter_enabled()) { return; } if (GetParent()->IsFloating() && GetContentLength() > 0) { // We've already claimed our first-letter content, don't try again. return; } if (GetPrevContinuation()) { // This isn't the first part of the first-letter. return; } // Find the length of the first-letter. We need a textrun for this; just bail // out if we fail to create it. // But in the floating first-letter case, the text is initially all in our // next-in-flow, and the float itself is empty. So we need to look at that // textrun instead of our own during FindFirstLetterRange. nsTextFrame* f = GetParent()->IsFloating() ? GetNextInFlow() : this; gfxSkipCharsIterator iter = f->EnsureTextRun(nsTextFrame::eInflated); const gfxTextRun* textRun = f->GetTextRun(nsTextFrame::eInflated); const nsTextFragment* frag = TextFragment(); const int32_t length = GetInFlowContentLength(); const int32_t offset = GetContentOffset(); int32_t firstLetterLength = length; NewlineProperty* cachedNewlineOffset = nullptr; int32_t newLineOffset = -1; // this will be -1 or a content offset // This will just return -1 if newlines are not significant. int32_t contentNewLineOffset = GetContentNewLineOffset(offset, cachedNewlineOffset); 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) { firstLetterLength = newLineOffset - offset; } } if (contentNewLineOffset >= 0 && contentNewLineOffset < offset) { // 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; } else { // 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; FindFirstLetterRange(frag, lang, textRun, 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 (firstLetterLength) { 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. SetFirstLetterLength(firstLetterLength); } static bool IsUnreflowedLetterFrame(nsIFrame* aFrame) { return aFrame->IsLetterFrame() && aFrame->HasAnyStateBits(NS_FRAME_FIRST_REFLOW); } // XXX Need to do something here to avoid incremental reflow bugs due to // first-line changing min-width /* virtual */ void nsTextFrame::AddInlineMinISize(const IntrinsicSizeInput& aInput, InlineMinISizeData* aData) { // Check if this textframe belongs to a first-letter frame that has not yet // been reflowed; if so, we need to deal with splitting off a continuation // before we can measure the advance correctly. if (IsUnreflowedLetterFrame(GetParent())) { MaybeSplitFramesForFirstLetter(); } 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 = f->FindLineContainer())) { 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(aInput.mContext, 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, InlinePrefISizeData* aData, TextRunType aTextRunType) { if (IsUnreflowedLetterFrame(GetParent())) { MaybeSplitFramesForFirstLetter(); } 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, aData->mLineIsEmpty); // 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); if (aData->mLineIsEmpty) { provider.SetStartOfLine(iter); } // 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(const IntrinsicSizeInput& aInput, 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 = f->FindLineContainer())) { 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(aInput.mContext, 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(this)->EnsureTextRun(nsTextFrame::eInflated); if (!mTextRun) { return nsRect(); } PropertyProvider provider(const_cast(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(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(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 } void nsTextFrame::SetFirstLetterLength(int32_t aLength) { if (aLength == GetContentLength()) { return; } mContentLengthHint = aLength; nsTextFrame* next = static_cast(GetNextInFlow()); if (!aLength && !next) { return; } if (aLength > GetContentLength()) { // Stealing some text from our next-in-flow; this happens with floating // first-letter, which is initially given a zero-length range, with all // the text being in its continuation. if (!next) { MOZ_ASSERT_UNREACHABLE("Expected a next-in-flow; first-letter broken?"); return; } } else if (!next) { // We need to create a continuation for the parent first-letter frame, // and move any kids after this frame to the new one; if there are none, // a new continuing text frame will be created there. MOZ_ASSERT(GetParent()->IsLetterFrame()); auto* letterFrame = static_cast(GetParent()); next = letterFrame->CreateContinuationForFramesAfter(this); } next->mContentOffset = GetContentOffset() + aLength; ClearTextRuns(); } bool nsTextFrame::IsFloatingFirstLetterChild() const { nsIFrame* frame = GetParent(); return frame && frame->IsFloating() && frame->IsLetterFrame(); } bool nsTextFrame::IsInitialLetterChild() const { nsIFrame* frame = GetParent(); return frame && frame->StyleTextReset()->mInitialLetter.size != 0.0f && frame->IsLetterFrame(); } struct nsTextFrame::NewlineProperty { int32_t mStartOffset; // The offset of the first \n after mStartOffset, or -1 if there is none int32_t mNewlineOffset; }; int32_t nsTextFrame::GetContentNewLineOffset( int32_t aOffset, NewlineProperty*& aCachedNewlineOffset) { int32_t contentNewLineOffset = -1; // this will be -1 or a content offset if (StyleText()->NewlineIsSignificant(this)) { // Pointer to the nsGkAtoms::newline set on this frame's element aCachedNewlineOffset = mContent->HasFlag(NS_HAS_NEWLINE_PROPERTY) ? static_cast( mContent->GetProperty(nsGkAtoms::newline)) : nullptr; if (aCachedNewlineOffset && aCachedNewlineOffset->mStartOffset <= aOffset && (aCachedNewlineOffset->mNewlineOffset == -1 || aCachedNewlineOffset->mNewlineOffset >= aOffset)) { contentNewLineOffset = aCachedNewlineOffset->mNewlineOffset; } else { contentNewLineOffset = FindChar( TextFragment(), aOffset, GetContent()->TextLength() - aOffset, '\n'); } } return contentNewLineOffset; } void nsTextFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics, const ReflowInput& aReflowInput, nsReflowStatus& aStatus) { MarkInReflow(); DO_GLOBAL_REFLOW_COUNT("nsTextFrame"); 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 NewlineProperty* cachedNewlineOffset = nullptr; int32_t newLineOffset = -1; // this will be -1 or a content offset // This will just return -1 if newlines are not significant. int32_t contentNewLineOffset = GetContentNewLineOffset(offset, cachedNewlineOffset); 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, HasAnyStateBits(TEXT_START_OF_LINE)); 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::infinity(); } bool canTrimTrailingWhitespace = !textStyle->WhiteSpaceIsSignificant() || HasAnyStateBits(TEXT_IS_IN_TOKEN_MATHML); bool isBreakSpaces = textStyle->mWhiteSpaceCollapse == StyleWhiteSpaceCollapse::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), textStyle->WhiteSpaceCanWrap(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(em) / static_cast(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(); } if (brokeText && breakPriority == gfxBreakPriority::eWordWrapBreak) { aLineLayout.SetUsedOverflowWrap(); } // 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))) { 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, HasAnyStateBits(TEXT_START_OF_LINE)); 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 if appropriate. MOZ_ASSERT(aFragOffset >= 0); for (uint32_t i = 0; i < static_cast(aFragLen); ++i) { char16_t ch = aFrag->CharAt(static_cast(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(aTextRun); nsAutoString convertedString; AutoTArray charsToMergeArray; AutoTArray 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; } } bool nsTextFrame::AppendRenderedText(AppendRenderedTextState& aState, RenderedText& aResult) { if (HasAnyStateBits(NS_FRAME_IS_DIRTY)) { // We don't trust dirty frames, especially when computing rendered text. return false; } // Ensure the text run and grab the gfxSkipCharsIterator for it gfxSkipCharsIterator iter = EnsureTextRun(nsTextFrame::eInflated); if (!mTextRun) { return false; } 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())) { if (thisLc != aState.mLineContainer) { // Setup line cursor when needed. aState.mLineContainer = thisLc; aState.mLineContainer->SetupLineCursorForQuery(); } LineStartsOrEndsAtHardLineBreak(this, aState.mLineContainer, &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 (!IsAtEndOfLine() || aState.mTrimTrailingWhitespace != TrailingWhitespace::Trim || !endsAtHardBreak) { trimFlags |= TrimmedOffsetFlags::NoTrimAfter; } // Whether to trim whitespaces before the text frame. if (!startsAtHardBreak) { trimFlags |= TrimmedOffsetFlags::NoTrimBefore; } TrimmedOffsets trimmedOffsets = GetTrimmedOffsets(aState.mTextFrag, trimFlags); bool trimmedSignificantNewline = (trimmedOffsets.GetEnd() < GetContentEnd() || (aState.mTrimTrailingWhitespace == TrailingWhitespace::Trim && StyleText()->mWhiteSpaceCollapse == StyleWhiteSpaceCollapse::PreserveBreaks)) && HasSignificantTerminalNewline(); uint32_t skippedToRenderedStringOffset = aState.mOffsetInRenderedString - tmpIter.ConvertOriginalToSkipped(trimmedOffsets.mStart); uint32_t nextOffsetInRenderedString = tmpIter.ConvertOriginalToSkipped(trimmedOffsets.GetEnd()) + (trimmedSignificantNewline ? 1 : 0) + skippedToRenderedStringOffset; if (aState.mOffsetType == TextOffsetType::OffsetsInRenderedText) { if (nextOffsetInRenderedString <= aState.mStartOffset) { aState.mOffsetInRenderedString = nextOffsetInRenderedString; return true; } if (!aState.mHaveOffsets) { aResult.mOffsetWithinNodeText = tmpIter.ConvertSkippedToOriginal( aState.mStartOffset - skippedToRenderedStringOffset); aResult.mOffsetWithinNodeRenderedText = aState.mStartOffset; aState.mHaveOffsets = true; } if (aState.mOffsetInRenderedString >= aState.mEndOffset) { return false; } } else { if (uint32_t(GetContentEnd()) <= aState.mStartOffset) { aState.mOffsetInRenderedString = nextOffsetInRenderedString; return true; } if (!aState.mHaveOffsets) { aResult.mOffsetWithinNodeText = aState.mStartOffset; // Skip trimmed space when computed the rendered text offset. int32_t clamped = std::max(aState.mStartOffset, trimmedOffsets.mStart); aResult.mOffsetWithinNodeRenderedText = tmpIter.ConvertOriginalToSkipped(clamped) + skippedToRenderedStringOffset; MOZ_ASSERT(aResult.mOffsetWithinNodeRenderedText >= aState.mOffsetInRenderedString && aResult.mOffsetWithinNodeRenderedText <= INT32_MAX, "Bad offset within rendered text"); aState.mHaveOffsets = true; } if (uint32_t(mContentOffset) >= aState.mEndOffset) { return false; } } int32_t startOffset; int32_t endOffset; if (aState.mOffsetType == TextOffsetType::OffsetsInRenderedText) { startOffset = tmpIter.ConvertSkippedToOriginal( aState.mStartOffset - skippedToRenderedStringOffset); endOffset = tmpIter.ConvertSkippedToOriginal(aState.mEndOffset - skippedToRenderedStringOffset); } else { startOffset = aState.mStartOffset; endOffset = std::min(INT32_MAX, aState.mEndOffset); } // 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(trimmedOffsets.mStart, startOffset); trimmedOffsets.mLength = std::min(origTrimmedOffsetsEnd, endOffset) - trimmedOffsets.mStart; if (trimmedOffsets.mLength > 0) { const nsStyleText* textStyle = 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(runLength); ++i) { const char16_t ch = aState.mTextFrag->CharAt( AssertedCast(iter.GetOriginalOffset() + i)); if (ch == CH_SHY) { // We should preserve soft hyphens. They can't be transformed. aResult.mString.Append(ch); } } } else { TransformChars(this, textStyle, mTextRun, iter.GetSkippedOffset(), aState.mTextFrag, iter.GetOriginalOffset(), runLength, aResult.mString); } iter.AdvanceOriginal(runLength); } } if (trimmedSignificantNewline && GetContentEnd() <= endOffset) { // A significant newline was trimmed off (we must be // white-space:pre-line). Put it back. aResult.mString.Append('\n'); } aState.mOffsetInRenderedString = nextOffsetInRenderedString; return 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; AppendRenderedTextState state{aStartOffset, aEndOffset, aOffsetType, aTrimTrailingWhitespace, TextFragment()}; for (nsTextFrame* textFrame = this; textFrame; textFrame = textFrame->GetNextContinuation()) { if (!textFrame->AppendRenderedText(state, result)) { break; } } if (!state.mHaveOffsets) { result.mOffsetWithinNodeText = state.mTextFrag->GetLength(); result.mOffsetWithinNodeRenderedText = state.mOffsetInRenderedString; } 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). return !GetContentLength(); } if (HasAnyStateBits(TEXT_ISNOT_ONLY_WHITESPACE)) { return false; } if (HasAnyStateBits(TEXT_IS_ONLY_WHITESPACE)) { return true; } bool isEmpty = IsAllWhitespace(TextFragment(), textStyle->mWhiteSpaceCollapse != StyleWhiteSpaceCollapse::PreserveBreaks); 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(GetContentEnd()); uint32_t fragOffset = AssertedCast(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(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); if (!aFlags.contains(ListFlag::OnlyListDeterministicInfo)) { str += nsPrintfCString(" [run=%p]", static_cast(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& 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 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) + ContentBSize(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)); } nscoord nsTextFrame::GetCaretBaseline() const { if (mAscent == 0 && HasAnyStateBits(TEXT_NO_RENDERED_GLYPHS)) { nsBlockFrame* container = do_QueryFrame(FindLineContainer()); // TODO(emilio): Ideally we'd want to find out if only our line is empty, // but that's non-trivial to do, and realistically empty inlines and text // will get placed into a non-empty line unless all lines are empty, I // believe... if (container && container->LinesAreEmpty()) { nscoord blockSize = container->ContentBSize(GetWritingMode()); return GetFontMetricsDerivedCaretBaseline(blockSize); } } return nsIFrame::GetCaretBaseline(); } 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(GetContentOffset()), AssertedCast(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; }