Bug 1851952 - Allow line-wrapping after hyphen if necessary to avoid overflow, even if the hyphen would not normally be a break opportunity (e.g. between numerals). r=layout-reviewers,emilio

We don't line-break after a hyphen when it occurs between numerals, like in page or date ranges,
preferring to keep the range together as a unit when wrapping text.

However, if the available space is very narrow, e.g. in a small table cell, this may lead to
undesirable overflow. So to try and avoid this, this patch allows an "emergency" line-break
opportunity (similar to what `overflow-wrap: break-word` would do) after the hyphen in such
a case.

This affects a number of existing reftests, but the changes in behavior make us more like
WebKit/Blink (which generally allow a break after hyphen between numerals) in these cases,
so it seems unlikely to lead to webcompat issues; rather, it will help with existing issues
where people assume the content can wrap.

Differential Revision: https://phabricator.services.mozilla.com/D197936
This commit is contained in:
Jonathan Kew 2024-01-09 21:59:56 +00:00
parent 6468af16a1
commit 3977755320
5 changed files with 63 additions and 15 deletions

View File

@ -32,6 +32,7 @@
#include "gfxHarfBuzzShaper.h"
#include "gfxUserFontSet.h"
#include "nsCRT.h"
#include "nsContentUtils.h"
#include "nsSpecialCasingData.h"
#include "nsTextRunTransformations.h"
#include "nsUGenCategory.h"
@ -728,10 +729,30 @@ void gfxShapedText::SetupClusterBoundaries(uint32_t aOffset,
// preceding letter by any letter-spacing or justification.
const char16_t kBengaliVirama = 0x09CD;
const char16_t kBengaliYa = 0x09AF;
// Characters treated as hyphens for the purpose of "emergency" breaking
// when the content would otherwise overflow.
auto isHyphen = [](char16_t c) {
return c == char16_t('-') || // HYPHEN-MINUS
c == 0x2010 || // HYPHEN
c == 0x2012 || // FIGURE DASH
c == 0x2013 || // EN DASH
c == 0x058A; // ARMENIAN HYPHEN
};
bool prevWasHyphen = false;
while (pos < aLength) {
const char16_t ch = aString[pos];
if (prevWasHyphen) {
if (nsContentUtils::IsAlphanumeric(ch)) {
glyphs[pos].SetCanBreakBefore(
CompressedGlyph::FLAG_BREAK_TYPE_EMERGENCY_WRAP);
}
prevWasHyphen = false;
}
if (ch == char16_t(' ') || ch == kIdeographicSpace) {
glyphs[pos].SetIsSpace();
} else if (isHyphen(ch) && pos &&
nsContentUtils::IsAlphanumeric(aString[pos - 1])) {
prevWasHyphen = true;
} else if (ch == kBengaliYa) {
// Unless we're at the start, check for a preceding virama.
if (pos > 0 && aString[pos - 1] == kBengaliVirama) {
@ -753,14 +774,25 @@ void gfxShapedText::SetupClusterBoundaries(uint32_t aOffset,
const uint8_t* aString,
uint32_t aLength) {
CompressedGlyph* glyphs = GetCharacterGlyphs() + aOffset;
const uint8_t* limit = aString + aLength;
while (aString < limit) {
if (*aString == uint8_t(' ')) {
glyphs->SetIsSpace();
uint32_t pos = 0;
bool prevWasHyphen = false;
while (pos < aLength) {
uint8_t ch = aString[pos];
if (prevWasHyphen) {
if (nsContentUtils::IsAlphanumeric(ch)) {
glyphs->SetCanBreakBefore(
CompressedGlyph::FLAG_BREAK_TYPE_EMERGENCY_WRAP);
}
prevWasHyphen = false;
}
aString++;
glyphs++;
if (ch == uint8_t(' ')) {
glyphs->SetIsSpace();
} else if (ch == uint8_t('-') && pos &&
nsContentUtils::IsAlphanumeric(aString[pos - 1])) {
prevWasHyphen = true;
}
++pos;
++glyphs;
}
}

View File

@ -762,6 +762,8 @@ class gfxShapedText {
FLAG_BREAK_TYPE_NONE = 0,
FLAG_BREAK_TYPE_NORMAL = 1,
FLAG_BREAK_TYPE_HYPHEN = 2,
// Allow break before this position if needed to avoid overflow:
FLAG_BREAK_TYPE_EMERGENCY_WRAP = 3,
FLAG_CHAR_IS_SPACE = 0x10000000U,
@ -886,7 +888,7 @@ class gfxShapedText {
}
// Returns FLAGS_CAN_BREAK_BEFORE if the setting changed, 0 otherwise
uint32_t SetCanBreakBefore(uint8_t aCanBreakBefore) {
MOZ_ASSERT(aCanBreakBefore <= 2, "Bogus break-before value!");
MOZ_ASSERT(aCanBreakBefore <= 3, "Bogus break-flags value!");
uint32_t breakMask = (uint32_t(aCanBreakBefore) << FLAGS_CAN_BREAK_SHIFT);
uint32_t toggle = breakMask ^ (mValue & FLAGS_CAN_BREAK_BEFORE);
mValue ^= toggle;
@ -1369,6 +1371,7 @@ class gfxShapedWord final : public gfxShapedText {
memset(mCharGlyphsStorage, 0, aLength * sizeof(CompressedGlyph));
uint8_t* text = reinterpret_cast<uint8_t*>(&mCharGlyphsStorage[aLength]);
memcpy(text, aText, aLength * sizeof(uint8_t));
SetupClusterBoundaries(0, aText, aLength);
}
gfxShapedWord(const char16_t* aText, uint32_t aLength, Script aRunScript,

View File

@ -249,7 +249,11 @@ bool gfxTextRun::SetPotentialLineBreaks(Range aRange,
canBreak = CompressedGlyph::FLAG_BREAK_TYPE_NONE;
}
}
changed |= cg->SetCanBreakBefore(canBreak);
// If a break is allowed here, set the break flag, but don't clear a
// possible pre-existing emergency-break flag already in the run.
if (canBreak) {
changed |= cg->SetCanBreakBefore(canBreak);
}
++cg;
}
return changed != 0;
@ -927,6 +931,8 @@ uint32_t gfxTextRun::BreakAndMeasureText(
gfxFloat aWidth, const PropertyProvider& aProvider,
SuppressBreak aSuppressBreak, gfxFont::BoundingBoxType aBoundingBoxType,
DrawTarget* aRefDrawTarget, bool aCanWordWrap, bool aCanWhitespaceWrap,
bool aIsBreakSpaces,
// output params:
TrimmableWS* aOutTrimmableWhitespace, Metrics& aOutMetrics,
bool& aOutUsedHyphenation, uint32_t& aOutLastBreak,
gfxBreakPriority& aBreakPriority) {
@ -1033,7 +1039,8 @@ uint32_t gfxTextRun::BreakAndMeasureText(
// would trigger an infinite loop.
if (aSuppressBreak != eSuppressAllBreaks &&
(aSuppressBreak != eSuppressInitialBreak || i > aStart)) {
bool atNaturalBreak = mCharacterGlyphs[i].CanBreakBefore() == 1;
bool atNaturalBreak = mCharacterGlyphs[i].CanBreakBefore() ==
CompressedGlyph::FLAG_BREAK_TYPE_NORMAL;
// atHyphenationBreak indicates we're at a "soft" hyphen, where an extra
// hyphen glyph will need to be painted. It is NOT set for breaks at an
// explicit hyphen present in the text.
@ -1046,16 +1053,20 @@ uint32_t gfxTextRun::BreakAndMeasureText(
atHyphenationBreak &&
hyphenBuffer[i - aStart] == HyphenType::AutoWithManualInSameWord;
bool atBreak = atNaturalBreak || atHyphenationBreak;
bool wordWrapping = aCanWordWrap &&
mCharacterGlyphs[i].IsClusterStart() &&
aBreakPriority <= gfxBreakPriority::eWordWrapBreak;
bool wordWrapping =
(aCanWordWrap ||
(aCanWhitespaceWrap &&
mCharacterGlyphs[i].CanBreakBefore() ==
CompressedGlyph::FLAG_BREAK_TYPE_EMERGENCY_WRAP)) &&
mCharacterGlyphs[i].IsClusterStart() &&
aBreakPriority <= gfxBreakPriority::eWordWrapBreak;
bool whitespaceWrapping = false;
if (i > aStart) {
// The spec says the breaking opportunity is *after* whitespace.
auto const& g = mCharacterGlyphs[i - 1];
whitespaceWrapping =
aCanWhitespaceWrap &&
aIsBreakSpaces &&
(g.CharIsSpace() || g.CharIsTab() || g.CharIsNewline());
}

View File

@ -464,6 +464,7 @@ class gfxTextRun : public gfxShapedText {
gfxFloat aWidth, const PropertyProvider& aProvider,
SuppressBreak aSuppressBreak, gfxFont::BoundingBoxType aBoundingBoxType,
DrawTarget* aRefDrawTarget, bool aCanWordWrap, bool aCanWhitespaceWrap,
bool aIsBreakSpaces,
// Output parameters:
TrimmableWS* aOutTrimmableWhitespace, // may be null
Metrics& aOutMetrics, bool& aOutUsedHyphenation, uint32_t& aOutLastBreak,

View File

@ -9566,7 +9566,8 @@ void nsTextFrame::ReflowText(nsLineLayout& aLineLayout, nscoord aAvailableWidth,
uint32_t transformedCharsFit = mTextRun->BreakAndMeasureText(
transformedOffset, transformedLength, HasAnyStateBits(TEXT_START_OF_LINE),
availWidth, provider, suppressBreak, boundingBoxType, aDrawTarget,
textStyle->WordCanWrap(this), isBreakSpaces,
textStyle->WordCanWrap(this), textStyle->WhiteSpaceCanWrap(this),
isBreakSpaces,
// The following are output parameters:
canTrimTrailingWhitespace || whitespaceCanHang ? &trimmableWS : nullptr,
textMetrics, usedHyphenation, transformedLastBreak,