Bug 1253840 - patch 2 - When justifying, the full advance of any trimmable end-of-line whitespace needs to be hung into the margin. r=emilio

Depends on D178210

Differential Revision: https://phabricator.services.mozilla.com/D178211
This commit is contained in:
Jonathan Kew 2023-05-21 13:17:42 +00:00
Родитель d76893ccc8
Коммит 8109de8531
6 изменённых файлов: 178 добавлений и 51 удалений

Просмотреть файл

@ -928,7 +928,7 @@ uint32_t gfxTextRun::BreakAndMeasureText(
gfxFloat aWidth, const PropertyProvider& aProvider,
SuppressBreak aSuppressBreak, gfxFont::BoundingBoxType aBoundingBoxType,
DrawTarget* aRefDrawTarget, bool aCanWordWrap, bool aCanWhitespaceWrap,
gfxFloat* aOutTrimmableWhitespace, Metrics& aOutMetrics,
TrimmableWS* aOutTrimmableWhitespace, Metrics& aOutMetrics,
bool& aOutUsedHyphenation, uint32_t& aOutLastBreak,
gfxBreakPriority& aBreakPriority) {
aMaxLength = std::min(aMaxLength, GetLength() - aStart);
@ -1174,7 +1174,8 @@ uint32_t gfxTextRun::BreakAndMeasureText(
aRefDrawTarget, &aProvider);
if (aOutTrimmableWhitespace) {
*aOutTrimmableWhitespace = trimmableAdvance;
aOutTrimmableWhitespace->mAdvance = trimmableAdvance;
aOutTrimmableWhitespace->mCount = trimmableChars;
}
if (charsFit == aMaxLength) {

Просмотреть файл

@ -384,6 +384,13 @@ class gfxTextRun : public gfxShapedText {
nsTArray<HyphenType>& aHyphenBuffer,
HyphenationState* aWordState);
// Struct used by BreakAndMeasureText to return the amount of trimmable
// trailing whitespace included in the run.
struct TrimmableWS {
mozilla::gfx::Float mAdvance = 0;
uint32_t mCount = 0;
};
/**
* Finds the longest substring that will fit into the given width.
* Uses GetHyphenationBreaks and GetSpacing from aProvider.
@ -455,7 +462,7 @@ class gfxTextRun : public gfxShapedText {
SuppressBreak aSuppressBreak, gfxFont::BoundingBoxType aBoundingBoxType,
DrawTarget* aRefDrawTarget, bool aCanWordWrap, bool aCanWhitespaceWrap,
// Output parameters:
gfxFloat* aOutTrimmableWhitespace, // may be null
TrimmableWS* aOutTrimmableWhitespace, // may be null
Metrics& aOutMetrics, bool& aOutUsedHyphenation, uint32_t& aOutLastBreak,
// In/out:
gfxBreakPriority& aBreakPriority);

Просмотреть файл

@ -3046,7 +3046,8 @@ void nsLineLayout::ExpandInlineRubyBoxes(PerSpanData* aSpan) {
}
}
nscoord nsLineLayout::GetHangFrom(const PerSpanData* aSpan, bool aLineIsRTL) {
nscoord nsLineLayout::GetHangFrom(const PerSpanData* aSpan,
bool aLineIsRTL) const {
const PerFrameData* pfd = aSpan->mLastFrame;
nscoord result = 0;
while (pfd) {
@ -3079,6 +3080,36 @@ nscoord nsLineLayout::GetHangFrom(const PerSpanData* aSpan, bool aLineIsRTL) {
return result;
}
gfxTextRun::TrimmableWS nsLineLayout::GetTrimFrom(const PerSpanData* aSpan,
bool aLineIsRTL) const {
const PerFrameData* pfd = aSpan->mLastFrame;
while (pfd) {
if (const PerSpanData* childSpan = pfd->mSpan) {
return GetTrimFrom(childSpan, aLineIsRTL);
}
if (pfd->mIsTextFrame) {
auto* lastText = static_cast<nsTextFrame*>(pfd->mFrame);
auto result = lastText->GetTrimmableWS();
if (result.mAdvance) {
lastText->EnsureTextRun(nsTextFrame::eInflated);
auto* textRun = lastText->GetTextRun(nsTextFrame::eInflated);
if (textRun && textRun->IsRightToLeft() != aLineIsRTL) {
result.mAdvance = -result.mAdvance;
}
}
return result;
}
if (!pfd->mSkipWhenTrimmingWhitespace) {
// If we hit a frame on the end that's not text and not a placeholder or
// <br>, then there is no trailing whitespace to trim. Stop the search.
return gfxTextRun::TrimmableWS{};
}
// Scan back for a preceding frame whose whitespace we can trim.
pfd = pfd->mPrev;
}
return gfxTextRun::TrimmableWS{};
}
// Align inline frames within the line according to the CSS text-align
// property.
void nsLineLayout::TextAlignLine(nsLineBox* aLine, bool aIsLastLine) {
@ -3106,9 +3137,16 @@ void nsLineLayout::TextAlignLine(nsLineBox* aLine, bool aIsLastLine) {
// Check if there's trailing whitespace we need to "hang" at line-wrap.
nscoord hang = 0;
uint32_t trimCount = 0;
if (aLine->IsLineWrapped()) {
if (textAlign == StyleTextAlign::Justify) {
auto trim = GetTrimFrom(mRootSpan, lineWM.IsBidiRTL());
hang = NSToCoordRound(trim.mAdvance);
trimCount = trim.mCount;
} else {
hang = GetHangFrom(mRootSpan, lineWM.IsBidiRTL());
}
}
bool isSVG = LineContainerFrame()->IsInSVGTextSubtree();
bool doTextAlign = remainingISize > 0 || hang != 0;
@ -3144,9 +3182,11 @@ void nsLineLayout::TextAlignLine(nsLineBox* aLine, bool aIsLastLine) {
switch (textAlign) {
case StyleTextAlign::Justify: {
int32_t opportunities =
psd->mFrame->mJustificationInfo.mInnerOpportunities;
psd->mFrame->mJustificationInfo.mInnerOpportunities -
(hang ? trimCount : 0);
if (opportunities > 0) {
int32_t gaps = opportunities * 2 + additionalGaps;
remainingISize += std::abs(hang);
JustificationApplicationState applyState(gaps, remainingISize);
// Apply the justification, and make sure to update our linebox
@ -3154,10 +3194,28 @@ void nsLineLayout::TextAlignLine(nsLineBox* aLine, bool aIsLastLine) {
aLine->ExpandBy(ApplyFrameJustification(psd, applyState),
ContainerSizeForSpan(psd));
MOZ_ASSERT(applyState.mGaps.mHandled == applyState.mGaps.mCount,
// If the trimmable trailing whitespace that we want to hang had
// reverse-inline directionality, adjust line position to account for
// it being at the inline-start side.
// On top of the original "hang" amount, justification will have
// modified its width, so we include that adjustment here.
if (hang < 0) {
dx = hang - trimCount * remainingISize / opportunities;
}
// Gaps that belong to trimmed whitespace were not included in the
// applyState count, so we need to add them here for the assert.
DebugOnly<int32_t> trimmedGaps = hang ? trimCount * 2 : 0;
MOZ_ASSERT(applyState.mGaps.mHandled ==
applyState.mGaps.mCount + trimmedGaps,
"Unprocessed justification gaps");
NS_ASSERTION(
applyState.mWidth.mConsumed == applyState.mWidth.mAvailable,
// Similarly, account for the adjustment applied to the trimmed
// whitespace, which is in addition to the adjustment that applies
// within the actual width of the line.
DebugOnly<int32_t> trimmedAdjustment =
trimCount * remainingISize / opportunities;
NS_ASSERTION(applyState.mWidth.mConsumed ==
applyState.mWidth.mAvailable + trimmedAdjustment,
"Unprocessed justification width");
break;
}

Просмотреть файл

@ -10,6 +10,7 @@
#define nsLineLayout_h___
#include "gfxTypes.h"
#include "gfxTextRun.h"
#include "JustificationUtils.h"
#include "mozilla/ArenaAllocator.h"
#include "mozilla/WritingModes.h"
@ -526,7 +527,9 @@ class nsLineLayout {
// Get the advance of any trailing hangable whitespace. If the whitespace
// has directionality opposite to the line, the result is negated.
nscoord GetHangFrom(const PerSpanData* aSpan, bool aLineIsRTL);
nscoord GetHangFrom(const PerSpanData* aSpan, bool aLineIsRTL) const;
gfxTextRun::TrimmableWS GetTrimFrom(const PerSpanData* aSpan,
bool aLineIsRTL) const;
gfxBreakPriority mLastOptionalBreakPriority;
int32_t mLastOptionalBreakFrameOffset;

Просмотреть файл

@ -198,6 +198,8 @@ 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;
@ -3888,7 +3890,7 @@ nsTArray<nsTextFrame*>* nsTextFrame::GetContinuations() {
if (!mNextContinuation) {
return nullptr;
}
if (mHasContinuationsProperty) {
if (mPropertyFlags & PropertyFlags::Continuations) {
return GetProperty(ContinuationsProperty());
}
size_t count = 0;
@ -3906,7 +3908,7 @@ nsTArray<nsTextFrame*>* nsTextFrame::GetContinuations() {
continuations = nullptr;
}
AddProperty(ContinuationsProperty(), continuations);
mHasContinuationsProperty = true;
mPropertyFlags |= PropertyFlags::Continuations;
return continuations;
}
@ -8155,20 +8157,53 @@ void nsTextFrame::SetFontSizeInflation(float aInflation) {
void nsTextFrame::SetHangableISize(nscoord aISize) {
MOZ_ASSERT(aISize >= 0, "unexpected negative hangable advance");
if (aISize <= 0) {
if (mHasHangableWS) {
RemoveProperty(HangableWhitespaceProperty());
}
mHasHangableWS = false;
ClearHangableISize();
return;
}
SetProperty(HangableWhitespaceProperty(), aISize);
mHasHangableWS = true;
mPropertyFlags |= PropertyFlags::HangableWS;
}
nscoord nsTextFrame::GetHangableISize() const {
MOZ_ASSERT(mHasHangableWS == HasProperty(HangableWhitespaceProperty()),
MOZ_ASSERT(!!(mPropertyFlags & PropertyFlags::HangableWS) ==
HasProperty(HangableWhitespaceProperty()),
"flag/property mismatch!");
return mHasHangableWS ? GetProperty(HangableWhitespaceProperty()) : 0;
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 */
@ -9167,7 +9202,7 @@ void nsTextFrame::ReflowText(nsLineLayout& aLineLayout, nscoord aAvailableWidth,
gfxTextRun::Metrics textMetrics;
uint32_t transformedLastBreak = 0;
bool usedHyphenation = false;
gfxFloat trimmableWidth = 0;
gfxTextRun::TrimmableWS trimmableWS;
gfxFloat availWidth = aAvailableWidth;
if (Style()->IsTextCombined()) {
// If text-combine-upright is 'all', we would compress whatever long
@ -9192,8 +9227,7 @@ void nsTextFrame::ReflowText(nsLineLayout& aLineLayout, nscoord aAvailableWidth,
availWidth, provider, suppressBreak, boundingBoxType, aDrawTarget,
textStyle->WordCanWrap(this), isBreakSpaces,
// The following are output parameters:
canTrimTrailingWhitespace || whitespaceCanHang ? &trimmableWidth
: nullptr,
canTrimTrailingWhitespace || whitespaceCanHang ? &trimmableWS : nullptr,
textMetrics, usedHyphenation, transformedLastBreak,
// In/out
breakPriority);
@ -9254,7 +9288,7 @@ void nsTextFrame::ReflowText(nsLineLayout& aLineLayout, nscoord aAvailableWidth,
}
bool brokeText = forceBreak >= 0 || transformedCharsFit < transformedLength;
if (trimmableWidth > 0.0) {
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.
@ -9262,32 +9296,41 @@ void nsTextFrame::ReflowText(nsLineLayout& aLineLayout, nscoord aAvailableWidth,
// 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 -= trimmableWidth;
trimmableWidth = 0.0;
textMetrics.mAdvanceWidth -= trimmableWS.mAdvance;
trimmableWS.mAdvance = 0.0;
}
SetHangableISize(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),
trimmableWidth);
SetHangableISize(NSToCoordRound(trimmableWidth - hang));
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;
trimmableWidth = 0.0;
trimmableWS.mAdvance = 0.0;
} else {
MOZ_ASSERT_UNREACHABLE("How did trimmableWidth get set?!");
SetHangableISize(0);
trimmableWidth = 0.0;
MOZ_ASSERT_UNREACHABLE("How did trimmableWS get set?!");
ClearHangableISize();
ClearTrimmableWS();
trimmableWS.mAdvance = 0.0;
}
} else {
// Remove any stale frame property.
SetHangableISize(0);
// 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 - trimmableWidth <= availWidth,
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");
@ -9415,7 +9458,7 @@ void nsTextFrame::ReflowText(nsLineLayout& aLineLayout, nscoord aAvailableWidth,
// 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(trimmableWidth));
aLineLayout.SetTrimmableISize(NSToCoordFloor(trimmableWS.mAdvance));
AddStateBits(TEXT_HAS_NONCOLLAPSED_CHARACTERS);
}
bool breakAfter = forceBreakAfter;
@ -9443,7 +9486,7 @@ void nsTextFrame::ReflowText(nsLineLayout& aLineLayout, nscoord aAvailableWidth,
// 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 - trimmableWidth > availWidth) {
if (textMetrics.mAdvanceWidth - trimmableWS.mAdvance > availWidth) {
breakAfter = true;
} else {
aLineLayout.NotifyOptionalBreakPosition(this, length, true,

Просмотреть файл

@ -773,6 +773,11 @@ class nsTextFrame : public nsIFrame {
void SetHangableISize(nscoord aISize);
nscoord GetHangableISize() const;
void ClearHangableISize();
void SetTrimmableWS(gfxTextRun::TrimmableWS aTrimmableWS);
gfxTextRun::TrimmableWS GetTrimmableWS() const;
void ClearTrimmableWS();
protected:
virtual ~nsTextFrame();
@ -808,13 +813,20 @@ class nsTextFrame : public nsIFrame {
};
mutable SelectionState mIsSelected;
// Flags used to track whether certain properties are present.
// (Public to keep MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS happy.)
public:
enum class PropertyFlags : uint8_t {
// Whether a cached continuations array is present.
bool mHasContinuationsProperty = false;
Continuations = 1 << 0,
// Whether a HangableWhitespace property is present.
HangableWS = 1 << 1,
// Whether a TrimmableWhitespace property is present.
TrimmableWS = 2 << 1,
};
// Whether a HangableWhitespace property is present. This could have been a
// frame state bit, but they are currently full. Because we have a uint8_t
// and a bool just above, there's a hole here that we can use.
bool mHasHangableWS = false;
protected:
PropertyFlags mPropertyFlags = PropertyFlags(0);
/**
* Return true if the frame is part of a Selection.
@ -1000,13 +1012,7 @@ class nsTextFrame : public nsIFrame {
// Clear any cached continuations array; this should be called whenever the
// chain is modified.
void ClearCachedContinuations() {
MOZ_ASSERT(NS_IsMainThread());
if (mHasContinuationsProperty) {
RemoveProperty(ContinuationsProperty());
mHasContinuationsProperty = false;
}
}
inline void ClearCachedContinuations();
/**
* UpdateIteratorFromOffset() updates the iterator from a given offset.
@ -1022,5 +1028,14 @@ class nsTextFrame : public nsIFrame {
};
MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(nsTextFrame::TrimmedOffsetFlags)
MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(nsTextFrame::PropertyFlags)
inline void nsTextFrame::ClearCachedContinuations() {
MOZ_ASSERT(NS_IsMainThread());
if (mPropertyFlags & PropertyFlags::Continuations) {
RemoveProperty(ContinuationsProperty());
mPropertyFlags &= ~PropertyFlags::Continuations;
}
}
#endif