Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=4 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
#include "gfxTextRun.h"
#include "gfxGlyphExtents.h"
#include "gfxHarfBuzzShaper.h"
#include "gfxPlatformFontList.h"
#include "gfxUserFontSet.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/gfx/PathHelpers.h"
#include "mozilla/ServoStyleSet.h"
#include "mozilla/Sprintf.h"
#include "mozilla/StaticPresData.h"
#include "gfxContext.h"
#include "gfxFontConstants.h"
#include "gfxFontMissingGlyphs.h"
#include "gfxScriptItemizer.h"
#include "nsUnicodeProperties.h"
#include "nsStyleConsts.h"
#include "nsStyleUtil.h"
#include "mozilla/Likely.h"
#include "gfx2DGlue.h"
#include "mozilla/gfx/Logging.h" // for gfxCriticalError
#include "mozilla/intl/String.h"
#include "mozilla/intl/UnicodeProperties.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "SharedFontList-impl.h"
#include "TextDrawTarget.h"
#ifdef XP_WIN
# include "gfxWindowsPlatform.h"
#endif
using namespace mozilla;
using namespace mozilla::gfx;
using namespace mozilla::intl;
using namespace mozilla::unicode;
using mozilla::services::GetObserverService;
static const char16_t kEllipsisChar[] = {0x2026, 0x0};
static const char16_t kASCIIPeriodsChar[] = {'.', '.', '.', 0x0};
#ifdef DEBUG_roc
# define DEBUG_TEXT_RUN_STORAGE_METRICS
#endif
#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS
extern uint32_t gTextRunStorageHighWaterMark;
extern uint32_t gTextRunStorage;
extern uint32_t gFontCount;
extern uint32_t gGlyphExtentsCount;
extern uint32_t gGlyphExtentsWidthsTotalSize;
extern uint32_t gGlyphExtentsSetupEagerSimple;
extern uint32_t gGlyphExtentsSetupEagerTight;
extern uint32_t gGlyphExtentsSetupLazyTight;
extern uint32_t gGlyphExtentsSetupFallBackToTight;
#endif
void gfxTextRun::GlyphRunIterator::NextRun() {
if (mReverse) {
if (mGlyphRun == mTextRun->mGlyphRuns.begin()) {
mGlyphRun = nullptr;
return;
}
--mGlyphRun;
} else {
MOZ_DIAGNOSTIC_ASSERT(mGlyphRun != mTextRun->mGlyphRuns.end());
++mGlyphRun;
if (mGlyphRun == mTextRun->mGlyphRuns.end()) {
mGlyphRun = nullptr;
return;
}
}
if (mGlyphRun->mCharacterOffset >= mEndOffset) {
mGlyphRun = nullptr;
return;
}
uint32_t glyphRunEndOffset = mGlyphRun == mTextRun->mGlyphRuns.end() - 1
? mTextRun->GetLength()
: (mGlyphRun + 1)->mCharacterOffset;
if (glyphRunEndOffset < mStartOffset) {
mGlyphRun = nullptr;
return;
}
mStringEnd = std::min(mEndOffset, glyphRunEndOffset);
mStringStart = std::max(mStartOffset, mGlyphRun->mCharacterOffset);
}
#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS
static void AccountStorageForTextRun(gfxTextRun* aTextRun, int32_t aSign) {
// Ignores detailed glyphs... we don't know when those have been constructed
// Also ignores gfxSkipChars dynamic storage (which won't be anything
// for preformatted text)
// Also ignores GlyphRun array, again because it hasn't been constructed
// by the time this gets called. If there's only one glyphrun that's stored
// directly in the textrun anyway so no additional overhead.
uint32_t length = aTextRun->GetLength();
int32_t bytes = length * sizeof(gfxTextRun::CompressedGlyph);
bytes += sizeof(gfxTextRun);
gTextRunStorage += bytes * aSign;
gTextRunStorageHighWaterMark =
std::max(gTextRunStorageHighWaterMark, gTextRunStorage);
}
#endif
bool gfxTextRun::NeedsGlyphExtents() const {
if (GetFlags() & gfx::ShapedTextFlags::TEXT_NEED_BOUNDING_BOX) {
return true;
}
for (const auto& run : mGlyphRuns) {
if (run.mFont->GetFontEntry()->IsUserFont()) {
return true;
}
}
return false;
}
// Helper for textRun creation to preallocate storage for glyph records;
// this function returns a pointer to the newly-allocated glyph storage.
// Returns nullptr if allocation fails.
void* gfxTextRun::AllocateStorageForTextRun(size_t aSize, uint32_t aLength) {
// Allocate the storage we need, returning nullptr on failure rather than
// throwing an exception (because web content can create huge runs).
void* storage = malloc(aSize + aLength * sizeof(CompressedGlyph));
if (!storage) {
NS_WARNING("failed to allocate storage for text run!");
return nullptr;
}
// Initialize the glyph storage (beyond aSize) to zero
memset(reinterpret_cast<char*>(storage) + aSize, 0,
aLength * sizeof(CompressedGlyph));
return storage;
}
already_AddRefed<gfxTextRun> gfxTextRun::Create(
const gfxTextRunFactory::Parameters* aParams, uint32_t aLength,
gfxFontGroup* aFontGroup, gfx::ShapedTextFlags aFlags,
nsTextFrameUtils::Flags aFlags2) {
void* storage = AllocateStorageForTextRun(sizeof(gfxTextRun), aLength);
if (!storage) {
return nullptr;
}
RefPtr<gfxTextRun> result =
new (storage) gfxTextRun(aParams, aLength, aFontGroup, aFlags, aFlags2);
return result.forget();
}
gfxTextRun::gfxTextRun(const gfxTextRunFactory::Parameters* aParams,
uint32_t aLength, gfxFontGroup* aFontGroup,
gfx::ShapedTextFlags aFlags,
nsTextFrameUtils::Flags aFlags2)
: gfxShapedText(aLength, aFlags, aParams->mAppUnitsPerDevUnit),
mUserData(aParams->mUserData),
mFontGroup(aFontGroup),
mFlags2(aFlags2),
mReleasedFontGroup(false),
mReleasedFontGroupSkippedDrawing(false),
mShapingState(eShapingState_Normal) {
NS_ASSERTION(mAppUnitsPerDevUnit > 0, "Invalid app unit scale");
NS_ADDREF(mFontGroup);
#ifndef RELEASE_OR_BETA
gfxTextPerfMetrics* tp = aFontGroup->GetTextPerfMetrics();
if (tp) {
tp->current.textrunConst++;
}
#endif
mCharacterGlyphs = reinterpret_cast<CompressedGlyph*>(this + 1);
if (aParams->mSkipChars) {
mSkipChars.TakeFrom(aParams->mSkipChars);
}
#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS
AccountStorageForTextRun(this, 1);
#endif
mDontSkipDrawing =
!!(aFlags2 & nsTextFrameUtils::Flags::DontSkipDrawingForPendingUserFonts);
}
gfxTextRun::~gfxTextRun() {
#ifdef DEBUG_TEXT_RUN_STORAGE_METRICS
AccountStorageForTextRun(this, -1);
#endif
#ifdef DEBUG
// Make it easy to detect a dead text run
mFlags = ~gfx::ShapedTextFlags();
mFlags2 = ~nsTextFrameUtils::Flags();
#endif
// The cached ellipsis textrun (if any) in a fontgroup will have already
// been told to release its reference to the group, so we mustn't do that
// again here.
if (!mReleasedFontGroup) {
#ifndef RELEASE_OR_BETA
gfxTextPerfMetrics* tp = mFontGroup->GetTextPerfMetrics();
if (tp) {
tp->current.textrunDestr++;
}
#endif
NS_RELEASE(mFontGroup);
}
}
void gfxTextRun::ReleaseFontGroup() {
NS_ASSERTION(!mReleasedFontGroup, "doubly released!");
// After dropping our reference to the font group, we'll no longer be able
// to get up-to-date results for ShouldSkipDrawing(). Store the current
// value in mReleasedFontGroupSkippedDrawing.
//
// (It doesn't actually matter that we can't get up-to-date results for
// ShouldSkipDrawing(), since the only text runs that we call
// ReleaseFontGroup() for are ellipsis text runs, and we ask the font
// group for a new ellipsis text run each time we want to draw one,
// and ensure that the cached one is cleared in ClearCachedData() when
// font loading status changes.)
mReleasedFontGroupSkippedDrawing = mFontGroup->ShouldSkipDrawing();
NS_RELEASE(mFontGroup);
mReleasedFontGroup = true;
}
bool gfxTextRun::SetPotentialLineBreaks(Range aRange,
const uint8_t* aBreakBefore) {
NS_ASSERTION(aRange.end <= GetLength(), "Overflow");
uint32_t changed = 0;
CompressedGlyph* cg = mCharacterGlyphs + aRange.start;
const CompressedGlyph* const end = cg + aRange.Length();
while (cg < end) {
uint8_t canBreak = *aBreakBefore++;
if (canBreak && !cg->IsClusterStart()) {
// XXX If we replace the line-breaker with one based more closely
// on UAX#14 (e.g. using ICU), this may not be needed any more.
// Avoid possible breaks inside a cluster, EXCEPT when the previous
// character was a space (compare UAX#14 rules LB9, LB10).
if (cg == mCharacterGlyphs || !(cg - 1)->CharIsSpace()) {
canBreak = CompressedGlyph::FLAG_BREAK_TYPE_NONE;
}
}
changed |= cg->SetCanBreakBefore(canBreak);
++cg;
}
return changed != 0;
}
gfxTextRun::LigatureData gfxTextRun::ComputeLigatureData(
Range aPartRange, PropertyProvider* aProvider) const {
NS_ASSERTION(aPartRange.start < aPartRange.end,
"Computing ligature data for empty range");
NS_ASSERTION(aPartRange.end <= GetLength(), "Character length overflow");
LigatureData result;
const CompressedGlyph* charGlyphs = mCharacterGlyphs;
uint32_t i;
for (i = aPartRange.start; !charGlyphs[i].IsLigatureGroupStart(); --i) {
NS_ASSERTION(i > 0, "Ligature at the start of the run??");
}
result.mRange.start = i;
for (i = aPartRange.start + 1;
i < GetLength() && !charGlyphs[i].IsLigatureGroupStart(); ++i) {
}
result.mRange.end = i;
int32_t ligatureWidth = GetAdvanceForGlyphs(result.mRange);
// Count the number of started clusters we have seen
uint32_t totalClusterCount = 0;
uint32_t partClusterIndex = 0;
uint32_t partClusterCount = 0;
for (i = result.mRange.start; i < result.mRange.end; ++i) {
// Treat the first character of the ligature as the start of a
// cluster for our purposes of allocating ligature width to its
// characters.
if (i == result.mRange.start || charGlyphs[i].IsClusterStart()) {
++totalClusterCount;
if (i < aPartRange.start) {
++partClusterIndex;
} else if (i < aPartRange.end) {
++partClusterCount;
}
}
}
NS_ASSERTION(totalClusterCount > 0, "Ligature involving no clusters??");
result.mPartAdvance = partClusterIndex * (ligatureWidth / totalClusterCount);
result.mPartWidth = partClusterCount * (ligatureWidth / totalClusterCount);
// Any rounding errors are apportioned to the final part of the ligature,
// so that measuring all parts of a ligature and summing them is equal to
// the ligature width.
if (aPartRange.end == result.mRange.end) {
gfxFloat allParts = totalClusterCount * (ligatureWidth / totalClusterCount);
result.mPartWidth += ligatureWidth - allParts;
}
if (partClusterCount == 0) {
// nothing to draw
result.mClipBeforePart = result.mClipAfterPart = true;
} else {
// Determine whether we should clip before or after this part when
// drawing its slice of the ligature.
// We need to clip before the part if any cluster is drawn before
// this part.
result.mClipBeforePart = partClusterIndex > 0;
// We need to clip after the part if any cluster is drawn after
// this part.
result.mClipAfterPart =
partClusterIndex + partClusterCount < totalClusterCount;
}
if (aProvider && (mFlags & gfx::ShapedTextFlags::TEXT_ENABLE_SPACING)) {
gfxFont::Spacing spacing;
if (aPartRange.start == result.mRange.start) {
aProvider->GetSpacing(Range(aPartRange.start, aPartRange.start + 1),
&spacing);
result.mPartWidth += spacing.mBefore;
}
if (aPartRange.end == result.mRange.end) {
aProvider->GetSpacing(Range(aPartRange.end - 1, aPartRange.end),
&spacing);
result.mPartWidth += spacing.mAfter;
}
}
return result;
}
gfxFloat gfxTextRun::ComputePartialLigatureWidth(
Range aPartRange, PropertyProvider* aProvider) const {
if (aPartRange.start >= aPartRange.end) return 0;
LigatureData data = ComputeLigatureData(aPartRange, aProvider);
return data.mPartWidth;
}
int32_t gfxTextRun::GetAdvanceForGlyphs(Range aRange) const {
int32_t advance = 0;
for (auto i = aRange.start; i < aRange.end; ++i) {
advance += GetAdvanceForGlyph(i);
}
return advance;
}
static void GetAdjustedSpacing(
const gfxTextRun* aTextRun, gfxTextRun::Range aRange,
gfxTextRun::PropertyProvider* aProvider,
gfxTextRun::PropertyProvider::Spacing* aSpacing) {
if (aRange.start >= aRange.end) return;
aProvider->GetSpacing(aRange, aSpacing);
#ifdef DEBUG
// Check to see if we have spacing inside ligatures
const gfxTextRun::CompressedGlyph* charGlyphs =
aTextRun->GetCharacterGlyphs();
uint32_t i;
for (i = aRange.start; i < aRange.end; ++i) {
if (!charGlyphs[i].IsLigatureGroupStart()) {
NS_ASSERTION(i == aRange.start || aSpacing[i - aRange.start].mBefore == 0,
"Before-spacing inside a ligature!");
NS_ASSERTION(
i - 1 <= aRange.start || aSpacing[i - 1 - aRange.start].mAfter == 0,
"After-spacing inside a ligature!");
}
}
#endif
}
bool gfxTextRun::GetAdjustedSpacingArray(
Range aRange, PropertyProvider* aProvider, Range aSpacingRange,
nsTArray<PropertyProvider::Spacing>* aSpacing) const {
if (!aProvider || !(mFlags & gfx::ShapedTextFlags::TEXT_ENABLE_SPACING)) {
return false;
}
if (!aSpacing->AppendElements(aRange.Length(), fallible)) {
return false;
}
auto spacingOffset = aSpacingRange.start - aRange.start;
memset(aSpacing->Elements(), 0, sizeof(gfxFont::Spacing) * spacingOffset);
GetAdjustedSpacing(this, aSpacingRange, aProvider,
aSpacing->Elements() + spacingOffset);
memset(aSpacing->Elements() + spacingOffset + aSpacingRange.Length(), 0,
sizeof(gfxFont::Spacing) * (aRange.end - aSpacingRange.end));
return true;
}
bool gfxTextRun::ShrinkToLigatureBoundaries(Range* aRange) const {
if (aRange->start >= aRange->end) {
return false;
}
const CompressedGlyph* charGlyphs = mCharacterGlyphs;
bool adjusted = false;
while (aRange->start < aRange->end &&
!charGlyphs[aRange->start].IsLigatureGroupStart()) {
++aRange->start;
adjusted = true;
}
if (aRange->end < GetLength()) {
while (aRange->end > aRange->start &&
!charGlyphs[aRange->end].IsLigatureGroupStart()) {
--aRange->end;
adjusted = true;
}
}
return adjusted;
}
void gfxTextRun::DrawGlyphs(gfxFont* aFont, Range aRange, gfx::Point* aPt,
PropertyProvider* aProvider, Range aSpacingRange,
TextRunDrawParams& aParams,
gfx::ShapedTextFlags aOrientation) const {
AutoTArray<PropertyProvider::Spacing, 200> spacingBuffer;
bool haveSpacing =
GetAdjustedSpacingArray(aRange, aProvider, aSpacingRange, &spacingBuffer);
aParams.spacing = haveSpacing ? spacingBuffer.Elements() : nullptr;
aFont->Draw(this, aRange.start, aRange.end, aPt, aParams, aOrientation);
}
static void ClipPartialLigature(const gfxTextRun* aTextRun, gfxFloat* aStart,
gfxFloat* aEnd, gfxFloat aOrigin,
gfxTextRun::LigatureData* aLigature) {
if (aLigature->mClipBeforePart) {
if (aTextRun->IsRightToLeft()) {
*aEnd = std::min(*aEnd, aOrigin);
} else {
*aStart = std::max(*aStart, aOrigin);
}
}
if (aLigature->mClipAfterPart) {
gfxFloat endEdge =
aOrigin + aTextRun->GetDirection() * aLigature->mPartWidth;
if (aTextRun->IsRightToLeft()) {
*aStart = std::max(*aStart, endEdge);
} else {
*aEnd = std::min(*aEnd, endEdge);
}
}
}
void gfxTextRun::DrawPartialLigature(gfxFont* aFont, Range aRange,
gfx::Point* aPt,
PropertyProvider* aProvider,
TextRunDrawParams& aParams,
gfx::ShapedTextFlags aOrientation) const {
if (aRange.start >= aRange.end) {
return;
}
// Draw partial ligature. We hack this by clipping the ligature.
LigatureData data = ComputeLigatureData(aRange, aProvider);
gfxRect clipExtents = aParams.context->GetClipExtents();
gfxFloat start, end;
if (aParams.isVerticalRun) {
start = clipExtents.Y() * mAppUnitsPerDevUnit;
end = clipExtents.YMost() * mAppUnitsPerDevUnit;
ClipPartialLigature(this, &start, &end, aPt->y, &data);
} else {
start = clipExtents.X() * mAppUnitsPerDevUnit;
end = clipExtents.XMost() * mAppUnitsPerDevUnit;
ClipPartialLigature(this, &start, &end, aPt->x, &data);
}
{
// use division here to ensure that when the rect is aligned on multiples
// of mAppUnitsPerDevUnit, we clip to true device unit boundaries.
// Also, make sure we snap the rectangle to device pixels.
Rect clipRect =
aParams.isVerticalRun
? Rect(clipExtents.X(), start / mAppUnitsPerDevUnit,
clipExtents.Width(), (end - start) / mAppUnitsPerDevUnit)
: Rect(start / mAppUnitsPerDevUnit, clipExtents.Y(),
(end - start) / mAppUnitsPerDevUnit, clipExtents.Height());
MaybeSnapToDevicePixels(clipRect, *aParams.dt, true);
aParams.context->Clip(clipRect);
}
gfx::Point pt;
if (aParams.isVerticalRun) {
pt = Point(aPt->x, aPt->y - aParams.direction * data.mPartAdvance);
} else {
pt = Point(aPt->x - aParams.direction * data.mPartAdvance, aPt->y);
}
DrawGlyphs(aFont, data.mRange, &pt, aProvider, aRange, aParams, aOrientation);
aParams.context->PopClip();
if (aParams.isVerticalRun) {
aPt->y += aParams.direction * data.mPartWidth;
} else {
aPt->x += aParams.direction * data.mPartWidth;
}
}
// Returns true if the font has synthetic bolding enabled,
// or is a color font (COLR/SVG/sbix/CBDT), false otherwise. This is used to
// check whether the text run needs to be explicitly composited in order to
// support opacity.
static bool HasSyntheticBoldOrColor(gfxFont* aFont) {
if (aFont->ApplySyntheticBold()) {
return true;
}
gfxFontEntry* fe = aFont->GetFontEntry();
if (fe->TryGetSVGData(aFont) || fe->TryGetColorGlyphs()) {
return true;
}
#if defined(XP_MACOSX) // sbix fonts only supported via Core Text
if (fe->HasFontTable(TRUETYPE_TAG('s', 'b', 'i', 'x'))) {
return true;
}
#endif
return false;
}
// helper class for double-buffering drawing with non-opaque color
struct MOZ_STACK_CLASS BufferAlphaColor {
explicit BufferAlphaColor(gfxContext* aContext) : mContext(aContext) {}
~BufferAlphaColor() = default;
void PushSolidColor(const gfxRect& aBounds, const DeviceColor& aAlphaColor,
uint32_t appsPerDevUnit) {
mContext->Save();
mContext->SnappedClip(gfxRect(
aBounds.X() / appsPerDevUnit, aBounds.Y() / appsPerDevUnit,
aBounds.Width() / appsPerDevUnit, aBounds.Height() / appsPerDevUnit));
mContext->SetDeviceColor(
DeviceColor(aAlphaColor.r, aAlphaColor.g, aAlphaColor.b));
mContext->PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, aAlphaColor.a);
}
void PopAlpha() {
// pop the text, using the color alpha as the opacity
mContext->PopGroupAndBlend();
mContext->Restore();
}
gfxContext* mContext;
};
void gfxTextRun::Draw(const Range aRange, const gfx::Point aPt,
const DrawParams& aParams) const {
NS_ASSERTION(aRange.end <= GetLength(), "Substring out of range");
NS_ASSERTION(aParams.drawMode == DrawMode::GLYPH_PATH ||
!(aParams.drawMode & DrawMode::GLYPH_PATH),
"GLYPH_PATH cannot be used with GLYPH_FILL, GLYPH_STROKE or "
"GLYPH_STROKE_UNDERNEATH");
NS_ASSERTION(aParams.drawMode == DrawMode::GLYPH_PATH || !aParams.callbacks,
"callback must not be specified unless using GLYPH_PATH");
bool skipDrawing =
!mDontSkipDrawing && (mFontGroup ? mFontGroup->ShouldSkipDrawing()
: mReleasedFontGroupSkippedDrawing);
auto* textDrawer = aParams.context->GetTextDrawer();
if (aParams.drawMode & DrawMode::GLYPH_FILL) {
DeviceColor currentColor;
if (aParams.context->GetDeviceColor(currentColor) && currentColor.a == 0 &&
!textDrawer) {
skipDrawing = true;
}
}
gfxFloat direction = GetDirection();
if (skipDrawing) {
// We don't need to draw anything;
// but if the caller wants advance width, we need to compute it here
if (aParams.advanceWidth) {
gfxTextRun::Metrics metrics =
MeasureText(aRange, gfxFont::LOOSE_INK_EXTENTS,
aParams.context->GetDrawTarget(), aParams.provider);
*aParams.advanceWidth = metrics.mAdvanceWidth * direction;
}
// return without drawing
return;
}
// synthetic bolding draws glyphs twice ==> colors with opacity won't draw
// correctly unless first drawn without alpha
BufferAlphaColor syntheticBoldBuffer(aParams.context);
DeviceColor currentColor;
bool mayNeedBuffering =
aParams.drawMode & DrawMode::GLYPH_FILL &&
aParams.context->HasNonOpaqueNonTransparentColor(currentColor) &&
!textDrawer;
// If we need to double-buffer, we'll need to measure the text first to
// get the bounds of the area of interest. Ideally we'd do that just for
// the specific glyph run(s) that need buffering, but because of bug
// 1612610 we currently use the extent of the entire range even when
// just buffering a subrange. So we'll measure the full range once and
// keep the metrics on hand for any subsequent subranges.
gfxTextRun::Metrics metrics;
bool gotMetrics = false;
// Set up parameters that will be constant across all glyph runs we need
// to draw, regardless of the font used.
TextRunDrawParams params;
params.context = aParams.context;
params.devPerApp = 1.0 / double(GetAppUnitsPerDevUnit());
params.isVerticalRun = IsVertical();
params.isRTL = IsRightToLeft();
params.direction = direction;
params.strokeOpts = aParams.strokeOpts;
params.textStrokeColor = aParams.textStrokeColor;
params.fontPalette = aParams.fontPalette;
params.paletteValueSet = aParams.paletteValueSet;
params.textStrokePattern = aParams.textStrokePattern;
params.drawOpts = aParams.drawOpts;
params.drawMode = aParams.drawMode;
params.callbacks = aParams.callbacks;
params.runContextPaint = aParams.contextPaint;
params.paintSVGGlyphs =
!aParams.callbacks || aParams.callbacks->mShouldPaintSVGGlyphs;
params.dt = aParams.context->GetDrawTarget();
params.textDrawer = textDrawer;
if (textDrawer) {
params.clipRect = textDrawer->GeckoClipRect();
}
params.allowGDI = aParams.allowGDI;
gfxFloat advance = 0.0;
gfx::Point pt = aPt;
for (GlyphRunIterator iter(this, aRange); !iter.AtEnd(); iter.NextRun()) {
gfxFont* font = iter.GlyphRun()->mFont;
Range runRange(iter.StringStart(), iter.StringEnd());
bool needToRestore = false;
if (mayNeedBuffering && HasSyntheticBoldOrColor(font)) {
needToRestore = true;
if (!gotMetrics) {
// Measure text; use the bounding box to determine the area we need
// to buffer. We measure the entire range, rather than just the glyph
// bounding box passed to PushSolidColor does not intersect the
// drawTarget's current clip, the skia backend fails to clip properly.
// This means we may use a larger buffer than actually needed, but is
// otherwise harmless.
metrics = MeasureText(aRange, gfxFont::LOOSE_INK_EXTENTS, params.dt,
aParams.provider);
if (IsRightToLeft()) {
metrics.mBoundingBox.MoveBy(
gfxPoint(aPt.x - metrics.mAdvanceWidth, aPt.y));
} else {
metrics.mBoundingBox.MoveBy(gfxPoint(aPt.x, aPt.y));
}
gotMetrics = true;
}
syntheticBoldBuffer.PushSolidColor(metrics.mBoundingBox, currentColor,
GetAppUnitsPerDevUnit());
}
Range ligatureRange(runRange);
bool adjusted = ShrinkToLigatureBoundaries(&ligatureRange);
bool drawPartial =
adjusted &&
((aParams.drawMode & (DrawMode::GLYPH_FILL | DrawMode::GLYPH_STROKE)) ||
(aParams.drawMode == DrawMode::GLYPH_PATH && aParams.callbacks));
gfx::Point origPt = pt;
if (drawPartial) {
DrawPartialLigature(font, Range(runRange.start, ligatureRange.start), &pt,
aParams.provider, params,
iter.GlyphRun()->mOrientation);
}
DrawGlyphs(font, ligatureRange, &pt, aParams.provider, ligatureRange,
params, iter.GlyphRun()->mOrientation);
if (drawPartial) {
DrawPartialLigature(font, Range(ligatureRange.end, runRange.end), &pt,
aParams.provider, params,
iter.GlyphRun()->mOrientation);
}
if (params.isVerticalRun) {
advance += (pt.y - origPt.y) * params.direction;
} else {
advance += (pt.x - origPt.x) * params.direction;
}
// composite result when synthetic bolding used
if (needToRestore) {
syntheticBoldBuffer.PopAlpha();
}
}
if (aParams.advanceWidth) {
*aParams.advanceWidth = advance;
}
}
// This method is mostly parallel to Draw().
void gfxTextRun::DrawEmphasisMarks(gfxContext* aContext, gfxTextRun* aMark,
gfxFloat aMarkAdvance, gfx::Point aPt,
Range aRange,
PropertyProvider* aProvider) const {
MOZ_ASSERT(aRange.end <= GetLength());
EmphasisMarkDrawParams params;
params.context = aContext;
params.mark = aMark;
params.advance = aMarkAdvance;
params.direction = GetDirection();
params.isVertical = IsVertical();
float& inlineCoord = params.isVertical ? aPt.y.value : aPt.x.value;
float direction = params.direction;
for (GlyphRunIterator iter(this, aRange); !iter.AtEnd(); iter.NextRun()) {
gfxFont* font = iter.GlyphRun()->mFont;
uint32_t start = iter.StringStart();
uint32_t end = iter.StringEnd();
Range ligatureRange(start, end);
bool adjusted = ShrinkToLigatureBoundaries(&ligatureRange);
if (adjusted) {
inlineCoord +=
direction * ComputePartialLigatureWidth(
Range(start, ligatureRange.start), aProvider);
}
AutoTArray<PropertyProvider::Spacing, 200> spacingBuffer;
bool haveSpacing = GetAdjustedSpacingArray(ligatureRange, aProvider,
ligatureRange, &spacingBuffer);
params.spacing = haveSpacing ? spacingBuffer.Elements() : nullptr;
font->DrawEmphasisMarks(this, &aPt, ligatureRange.start,
ligatureRange.Length(), params);
if (adjusted) {
inlineCoord += direction * ComputePartialLigatureWidth(
Range(ligatureRange.end, end), aProvider);
}
}
}
void gfxTextRun::AccumulateMetricsForRun(
gfxFont* aFont, Range aRange, gfxFont::BoundingBoxType aBoundingBoxType,
DrawTarget* aRefDrawTarget, PropertyProvider* aProvider,
Range aSpacingRange, gfx::ShapedTextFlags aOrientation,
Metrics* aMetrics) const {
AutoTArray<PropertyProvider::Spacing, 200> spacingBuffer;
bool haveSpacing =
GetAdjustedSpacingArray(aRange, aProvider, aSpacingRange, &spacingBuffer);
Metrics metrics = aFont->Measure(
this, aRange.start, aRange.end, aBoundingBoxType, aRefDrawTarget,
haveSpacing ? spacingBuffer.Elements() : nullptr, aOrientation);
aMetrics->CombineWith(metrics, IsRightToLeft());
}
void gfxTextRun::AccumulatePartialLigatureMetrics(
gfxFont* aFont, Range aRange, gfxFont::BoundingBoxType aBoundingBoxType,
DrawTarget* aRefDrawTarget, PropertyProvider* aProvider,
gfx::ShapedTextFlags aOrientation, Metrics* aMetrics) const {
if (aRange.start >= aRange.end) return;
// Measure partial ligature. We hack this by clipping the metrics in the
// same way we clip the drawing.
LigatureData data = ComputeLigatureData(aRange, aProvider);
// First measure the complete ligature
Metrics metrics;
AccumulateMetricsForRun(aFont, data.mRange, aBoundingBoxType, aRefDrawTarget,
aProvider, aRange, aOrientation, &metrics);
// Clip the bounding box to the ligature part
gfxFloat bboxLeft = metrics.mBoundingBox.X();
gfxFloat bboxRight = metrics.mBoundingBox.XMost();
// Where we are going to start "drawing" relative to our left baseline origin
gfxFloat origin =
IsRightToLeft() ? metrics.mAdvanceWidth - data.mPartAdvance : 0;
ClipPartialLigature(this, &bboxLeft, &bboxRight, origin, &data);
metrics.mBoundingBox.SetBoxX(bboxLeft, bboxRight);
// mBoundingBox is now relative to the left baseline origin for the entire
// ligature. Shift it left.
metrics.mBoundingBox.MoveByX(
-(IsRightToLeft()
? metrics.mAdvanceWidth - (data.mPartAdvance + data.mPartWidth)
: data.mPartAdvance));
metrics.mAdvanceWidth = data.mPartWidth;
aMetrics->CombineWith(metrics, IsRightToLeft());
}
gfxTextRun::Metrics gfxTextRun::MeasureText(
Range aRange, gfxFont::BoundingBoxType aBoundingBoxType,
DrawTarget* aRefDrawTarget, PropertyProvider* aProvider) const {
NS_ASSERTION(aRange.end <= GetLength(), "Substring out of range");
Metrics accumulatedMetrics;
for (GlyphRunIterator iter(this, aRange); !iter.AtEnd(); iter.NextRun()) {
gfxFont* font = iter.GlyphRun()->mFont;
uint32_t start = iter.StringStart();
uint32_t end = iter.StringEnd();
Range ligatureRange(start, end);
bool adjusted = ShrinkToLigatureBoundaries(&ligatureRange);
if (adjusted) {
AccumulatePartialLigatureMetrics(font, Range(start, ligatureRange.start),
aBoundingBoxType, aRefDrawTarget,
aProvider, iter.GlyphRun()->mOrientation,
&accumulatedMetrics);
}
// XXX This sucks. We have to get glyph extents just so we can detect
// glyphs outside the font box, even when aBoundingBoxType is LOOSE,
// even though in almost all cases we could get correct results just
// by getting some ascent/descent from the font and using our stored
// advance widths.
AccumulateMetricsForRun(font, ligatureRange, aBoundingBoxType,
aRefDrawTarget, aProvider, ligatureRange,
iter.GlyphRun()->mOrientation, &accumulatedMetrics);
if (adjusted) {
AccumulatePartialLigatureMetrics(
font, Range(ligatureRange.end, end), aBoundingBoxType, aRefDrawTarget,
aProvider, iter.GlyphRun()->mOrientation, &accumulatedMetrics);
}
}
return accumulatedMetrics;
}
void gfxTextRun::GetLineHeightMetrics(Range aRange, gfxFloat& aAscent,
gfxFloat& aDescent) const {
Metrics accumulatedMetrics;
for (GlyphRunIterator iter(this, aRange); !iter.AtEnd(); iter.NextRun()) {
gfxFont* font = iter.GlyphRun()->mFont;
auto metrics =
font->Measure(this, 0, 0, gfxFont::LOOSE_INK_EXTENTS, nullptr, nullptr,
iter.GlyphRun()->mOrientation);
accumulatedMetrics.CombineWith(metrics, false);
}
aAscent = accumulatedMetrics.mAscent;
aDescent = accumulatedMetrics.mDescent;
}
#define MEASUREMENT_BUFFER_SIZE 100
void gfxTextRun::ClassifyAutoHyphenations(uint32_t aStart, Range aRange,
nsTArray<HyphenType>& aHyphenBuffer,
HyphenationState* aWordState) {
MOZ_ASSERT(
aRange.end - aStart <= aHyphenBuffer.Length() && aRange.start >= aStart,
"Range out of bounds");
MOZ_ASSERT(aWordState->mostRecentBoundary >= aStart,
"Unexpected aMostRecentWordBoundary!!");
uint32_t start =
std::min<uint32_t>(aRange.start, aWordState->mostRecentBoundary);
for (uint32_t i = start; i < aRange.end; ++i) {
if (aHyphenBuffer[i - aStart] == HyphenType::Explicit &&
!aWordState->hasExplicitHyphen) {
aWordState->hasExplicitHyphen = true;
}
if (!aWordState->hasManualHyphen &&
(aHyphenBuffer[i - aStart] == HyphenType::Soft ||
aHyphenBuffer[i - aStart] == HyphenType::Explicit)) {
aWordState->hasManualHyphen = true;
// This is the first manual hyphen in the current word. We can only
// know if the current word has a manual hyphen until now. So, we need
// to run a sub loop to update the auto hyphens between the start of
// the current word and this manual hyphen.
if (aWordState->hasAutoHyphen) {
for (uint32_t j = aWordState->mostRecentBoundary; j < i; j++) {
if (aHyphenBuffer[j - aStart] ==
HyphenType::AutoWithoutManualInSameWord) {
aHyphenBuffer[j - aStart] = HyphenType::AutoWithManualInSameWord;
}
}
}
}
if (aHyphenBuffer[i - aStart] == HyphenType::AutoWithoutManualInSameWord) {
if (!aWordState->hasAutoHyphen) {
aWordState->hasAutoHyphen = true;
}
if (aWordState->hasManualHyphen) {
aHyphenBuffer[i - aStart] = HyphenType::AutoWithManualInSameWord;
}
}
// If we're at the word boundary, clear/reset couple states.
if (mCharacterGlyphs[i].CharIsSpace() || mCharacterGlyphs[i].CharIsTab() ||
mCharacterGlyphs[i].CharIsNewline() ||
// Since we will not have a boundary in the end of the string, let's
// call the end of the string a special case for word boundary.
i == GetLength() - 1) {
// We can only get to know whether we should raise/clear an explicit
// manual hyphen until we get to the end of a word, because this depends
// on whether there exists at least one auto hyphen in the same word.
if (!aWordState->hasAutoHyphen && aWordState->hasExplicitHyphen) {
for (uint32_t j = aWordState->mostRecentBoundary; j <= i; j++) {
if (aHyphenBuffer[j - aStart] == HyphenType::Explicit) {
aHyphenBuffer[j - aStart] = HyphenType::None;
}
}
}
aWordState->mostRecentBoundary = i;
aWordState->hasManualHyphen = false;
aWordState->hasAutoHyphen = false;
aWordState->hasExplicitHyphen = false;
}
}
}
uint32_t gfxTextRun::BreakAndMeasureText(
uint32_t aStart, uint32_t aMaxLength, bool aLineBreakBefore,
gfxFloat aWidth, PropertyProvider* aProvider, SuppressBreak aSuppressBreak,
gfxFloat* aTrimWhitespace, bool aWhitespaceCanHang, Metrics* aMetrics,
gfxFont::BoundingBoxType aBoundingBoxType, DrawTarget* aRefDrawTarget,
bool* aUsedHyphenation, uint32_t* aLastBreak, bool aCanWordWrap,
bool aCanWhitespaceWrap, gfxBreakPriority* aBreakPriority) {
aMaxLength = std::min(aMaxLength, GetLength() - aStart);
NS_ASSERTION(aStart + aMaxLength <= GetLength(), "Substring out of range");
Range bufferRange(
aStart, aStart + std::min<uint32_t>(aMaxLength, MEASUREMENT_BUFFER_SIZE));
PropertyProvider::Spacing spacingBuffer[MEASUREMENT_BUFFER_SIZE];
bool haveSpacing =
aProvider && !!(mFlags & gfx::ShapedTextFlags::TEXT_ENABLE_SPACING);
if (haveSpacing) {
GetAdjustedSpacing(this, bufferRange, aProvider, spacingBuffer);
}
AutoTArray<HyphenType, 4096> hyphenBuffer;
HyphenationState wordState;
wordState.mostRecentBoundary = aStart;
bool haveHyphenation =
aProvider &&
(aProvider->GetHyphensOption() == StyleHyphens::Auto ||
(aProvider->GetHyphensOption() == StyleHyphens::Manual &&
!!(mFlags & gfx::ShapedTextFlags::TEXT_ENABLE_HYPHEN_BREAKS)));
if (haveHyphenation) {
if (hyphenBuffer.AppendElements(bufferRange.Length(), fallible)) {
aProvider->GetHyphenationBreaks(bufferRange, hyphenBuffer.Elements());
if (aProvider->GetHyphensOption() == StyleHyphens::Auto) {
ClassifyAutoHyphenations(aStart, bufferRange, hyphenBuffer, &wordState);
}
} else {
haveHyphenation = false;
}
}
gfxFloat width = 0;
gfxFloat advance = 0;
// The number of space characters that can be trimmed or hang at a soft-wrap
uint32_t trimmableChars = 0;
// The amount of space removed by ignoring trimmableChars
gfxFloat trimmableAdvance = 0;
int32_t lastBreak = -1;
int32_t lastBreakTrimmableChars = -1;
gfxFloat lastBreakTrimmableAdvance = -1;
// Cache the last candidate break
int32_t lastCandidateBreak = -1;
int32_t lastCandidateBreakTrimmableChars = -1;
gfxFloat lastCandidateBreakTrimmableAdvance = -1;
bool lastCandidateBreakUsedHyphenation = false;
gfxBreakPriority lastCandidateBreakPriority = gfxBreakPriority::eNoBreak;
bool aborted = false;
uint32_t end = aStart + aMaxLength;
bool lastBreakUsedHyphenation = false;
Range ligatureRange(aStart, end);
ShrinkToLigatureBoundaries(&ligatureRange);
// We may need to move `i` backwards in the following loop, and re-scan
// part of the textrun; we'll use `rescanLimit` so we can tell when that
// is happening: if `i < rescanLimit` then we're rescanning.
uint32_t rescanLimit = aStart;
for (uint32_t i = aStart; i < end; ++i) {
if (i >= bufferRange.end) {
// Fetch more spacing and hyphenation data
uint32_t oldHyphenBufferLength = hyphenBuffer.Length();
bufferRange.start = i;
bufferRange.end =
std::min(aStart + aMaxLength, i + MEASUREMENT_BUFFER_SIZE);
// For spacing, we always overwrite the old data with the newly
// fetched one. However, for hyphenation, hyphenation data sometimes
// depends on the context in every word (if "hyphens: auto" is set).
// To ensure we get enough information between neighboring buffers,
// we grow the hyphenBuffer instead of overwrite it.
// NOTE that this means bufferRange does not correspond to the
// entire hyphenBuffer, but only to the most recently added portion.
// Therefore, we need to add the old length to hyphenBuffer.Elements()
// when getting more data.
if (haveSpacing) {
GetAdjustedSpacing(this, bufferRange, aProvider, spacingBuffer);
}
if (haveHyphenation) {
if (hyphenBuffer.AppendElements(bufferRange.Length(), fallible)) {
aProvider->GetHyphenationBreaks(
bufferRange, hyphenBuffer.Elements() + oldHyphenBufferLength);
if (aProvider->GetHyphensOption() == StyleHyphens::Auto) {
uint32_t prevMostRecentWordBoundary = wordState.mostRecentBoundary;
ClassifyAutoHyphenations(aStart, bufferRange, hyphenBuffer,
&wordState);
// If the buffer boundary is in the middle of a word,
// we need to go back to the start of the current word.
// So, we can correct the wrong candidates that we set
// in the previous runs of the loop.
if (prevMostRecentWordBoundary < oldHyphenBufferLength) {
rescanLimit = i;
i = prevMostRecentWordBoundary - 1;
continue;
}
}
} else {
haveHyphenation = false;
}
}
}
// There can't be a word-wrap break opportunity at the beginning of the
// line: if the width is too small for even one character to fit, it
// could be the first and last break opportunity on the line, and that
// would trigger an infinite loop.
if (aSuppressBreak != eSuppressAllBreaks &&
(aSuppressBreak != eSuppressInitialBreak || i > aStart)) {
bool atNaturalBreak = mCharacterGlyphs[i].CanBreakBefore() == 1;
// 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.
//
// NOTE(emilio): If you change this condition you also need to change
// nsTextFrame::AddInlineMinISizeForFlow to match.
bool atHyphenationBreak = !atNaturalBreak && haveHyphenation &&
IsOptionalHyphenBreak(hyphenBuffer[i - aStart]);
bool atAutoHyphenWithManualHyphenInSameWord =
atHyphenationBreak &&
hyphenBuffer[i - aStart] == HyphenType::AutoWithManualInSameWord;
bool atBreak = atNaturalBreak || atHyphenationBreak;
bool wordWrapping = aCanWordWrap &&
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 &&
(g.CharIsSpace() || g.CharIsTab() || g.CharIsNewline());
}
if (atBreak || wordWrapping || whitespaceWrapping) {
gfxFloat hyphenatedAdvance = advance;
if (atHyphenationBreak) {
hyphenatedAdvance += aProvider->GetHyphenWidth();
}
if (lastBreak < 0 ||
width + hyphenatedAdvance - trimmableAdvance <= aWidth) {
// We can break here.
lastBreak = i;
lastBreakTrimmableChars = trimmableChars;
lastBreakTrimmableAdvance = trimmableAdvance;
lastBreakUsedHyphenation = atHyphenationBreak;
*aBreakPriority = (atBreak || whitespaceWrapping)
? gfxBreakPriority::eNormalBreak
: gfxBreakPriority::eWordWrapBreak;
}
width += advance;
advance = 0;
if (width - trimmableAdvance > aWidth) {
// No more text fits. Abort
aborted = true;
break;
}
// There are various kinds of break opportunities:
// 1. word wrap break,
// 2. natural break,
// 3. manual hyphenation break,
// 4. auto hyphenation break without any manual hyphenation
// in the same word,
// 5. auto hyphenation break with another manual hyphenation
// in the same word.
// Allow all of them except the last one to be a candidate.
// So, we can ensure that we don't use an automatic
// hyphenation opportunity within a word that contains another
// manual hyphenation, unless it is the only choice.
if (wordWrapping || !atAutoHyphenWithManualHyphenInSameWord) {
lastCandidateBreak = lastBreak;
lastCandidateBreakTrimmableChars = lastBreakTrimmableChars;
lastCandidateBreakTrimmableAdvance = lastBreakTrimmableAdvance;
lastCandidateBreakUsedHyphenation = lastBreakUsedHyphenation;
lastCandidateBreakPriority = *aBreakPriority;
}
}
}
// If we're re-scanning part of a word (to re-process potential
// hyphenation types) then we don't want to accumulate widths again
// for the characters that were already added to `advance`.
if (i < rescanLimit) {
continue;
}
gfxFloat charAdvance;
if (i >= ligatureRange.start && i < ligatureRange.end) {
charAdvance = GetAdvanceForGlyphs(Range(i, i + 1));
if (haveSpacing) {
PropertyProvider::Spacing* space =
&spacingBuffer[i - bufferRange.start];
charAdvance += space->mBefore + space->mAfter;
}
} else {
charAdvance = ComputePartialLigatureWidth(Range(i, i + 1), aProvider);
}
advance += charAdvance;
if (aTrimWhitespace || aWhitespaceCanHang) {
if (mCharacterGlyphs[i].CharIsSpace()) {
++trimmableChars;
trimmableAdvance += charAdvance;
} else {
trimmableAdvance = 0;
trimmableChars = 0;
}
}
}
if (!aborted) {
width += advance;
}
// There are three possibilities:
// 1) all the text fit (width <= aWidth)
// 2) some of the text fit up to a break opportunity (width > aWidth &&
// lastBreak >= 0)
// 3) none of the text fits before a break opportunity (width > aWidth &&
// lastBreak < 0)
uint32_t charsFit;
bool usedHyphenation = false;
if (width - trimmableAdvance <= aWidth) {
charsFit = aMaxLength;
} else if (lastBreak >= 0) {
if (lastCandidateBreak >= 0 && lastCandidateBreak != lastBreak) {
lastBreak = lastCandidateBreak;
lastBreakTrimmableChars = lastCandidateBreakTrimmableChars;
lastBreakTrimmableAdvance = lastCandidateBreakTrimmableAdvance;
lastBreakUsedHyphenation = lastCandidateBreakUsedHyphenation;
*aBreakPriority = lastCandidateBreakPriority;
}
charsFit = lastBreak - aStart;
trimmableChars = lastBreakTrimmableChars;
trimmableAdvance = lastBreakTrimmableAdvance;
usedHyphenation = lastBreakUsedHyphenation;
} else {
charsFit = aMaxLength;
}
if (aMetrics) {
auto fitEnd = aStart + charsFit;
// Initially, measure everything, so that our bounding box includes
// any trimmable or hanging whitespace.
*aMetrics = MeasureText(Range(aStart, fitEnd), aBoundingBoxType,
aRefDrawTarget, aProvider);
if (aTrimWhitespace || aWhitespaceCanHang) {
// Measure trailing whitespace that is to be trimmed/hung.
Metrics trimOrHangMetrics =
MeasureText(Range(fitEnd - trimmableChars, fitEnd), aBoundingBoxType,
aRefDrawTarget, aProvider);
if (aTrimWhitespace) {
aMetrics->mAdvanceWidth -= trimOrHangMetrics.mAdvanceWidth;
} else if (aMetrics->mAdvanceWidth > aWidth) {
// Restrict width of hanging whitespace so it doesn't overflow.
aMetrics->mAdvanceWidth = std::max(
aWidth, aMetrics->mAdvanceWidth - trimOrHangMetrics.mAdvanceWidth);
}
}
}
if (aTrimWhitespace) {
*aTrimWhitespace = trimmableAdvance;
}
if (aUsedHyphenation) {
*aUsedHyphenation = usedHyphenation;
}
if (aLastBreak && charsFit == aMaxLength) {
if (lastBreak < 0) {
*aLastBreak = UINT32_MAX;
} else {
*aLastBreak = lastBreak - aStart;
}
}
return charsFit;
}
gfxFloat gfxTextRun::GetAdvanceWidth(
Range aRange, PropertyProvider* aProvider,
PropertyProvider::Spacing* aSpacing) const {
NS_ASSERTION(aRange.end <= GetLength(), "Substring out of range");
Range ligatureRange = aRange;
bool adjusted = ShrinkToLigatureBoundaries(&ligatureRange);
gfxFloat result =
adjusted ? ComputePartialLigatureWidth(
Range(aRange.start, ligatureRange.start), aProvider) +
ComputePartialLigatureWidth(
Range(ligatureRange.end, aRange.end), aProvider)
: 0.0;
if (aSpacing) {
aSpacing->mBefore = aSpacing->mAfter = 0;
}
// Account for all remaining spacing here. This is more efficient than
// processing it along with the glyphs.
if (aProvider && (mFlags & gfx::ShapedTextFlags::TEXT_ENABLE_SPACING)) {
uint32_t i;
AutoTArray<PropertyProvider::Spacing, 200> spacingBuffer;
if (spacingBuffer.AppendElements(aRange.Length(), fallible)) {
GetAdjustedSpacing(this, ligatureRange, aProvider,
spacingBuffer.Elements());
for (i = 0; i < ligatureRange.Length(); ++i) {
PropertyProvider::Spacing* space = &spacingBuffer[i];
result += space->mBefore + space->mAfter;
}
if (aSpacing) {
aSpacing->mBefore = spacingBuffer[0].mBefore;
aSpacing->mAfter = spacingBuffer.LastElement().mAfter;
}
}
}
return result + GetAdvanceForGlyphs(ligatureRange);
}
gfxFloat gfxTextRun::GetMinAdvanceWidth(Range aRange) {
MOZ_ASSERT(aRange.end <= GetLength(), "Substring out of range");
Range ligatureRange = aRange;
bool adjusted = ShrinkToLigatureBoundaries(&ligatureRange);
gfxFloat result =
adjusted
? std::max(ComputePartialLigatureWidth(
Range(aRange.start, ligatureRange.start), nullptr),
ComputePartialLigatureWidth(
Range(ligatureRange.end, aRange.end), nullptr))
: 0.0;
// Compute min advance width by assuming each grapheme cluster takes its own
// line.
gfxFloat clusterAdvance = 0;
for (uint32_t i = ligatureRange.start; i < ligatureRange.end; ++i) {
if (mCharacterGlyphs[i].CharIsSpace()) {
// Skip space char to prevent its advance width contributing to the
// result. That is, don't consider a space can be in its own line.
continue;
}
clusterAdvance += GetAdvanceForGlyph(i);
if (i + 1 == ligatureRange.end || IsClusterStart(i + 1)) {
result = std::max(result, clusterAdvance);
clusterAdvance = 0;
}
}
return result;
}
bool gfxTextRun::SetLineBreaks(Range aRange, bool aLineBreakBefore,
bool aLineBreakAfter,
gfxFloat* aAdvanceWidthDelta) {
// Do nothing because our shaping does not currently take linebreaks into
// account. There is no change in advance width.
if (aAdvanceWidthDelta) {
*aAdvanceWidthDelta = 0;
}
return false;
}
const gfxTextRun::GlyphRun* gfxTextRun::FindFirstGlyphRunContaining(
uint32_t aOffset) const {
MOZ_ASSERT(aOffset <= GetLength(), "Bad offset looking for glyphrun");
MOZ_ASSERT(GetLength() == 0 || !mGlyphRuns.IsEmpty(),
"non-empty text but no glyph runs present!");
if (mGlyphRuns.Length() <= 1) {
return mGlyphRuns.begin();
}
if (aOffset == GetLength()) {
return mGlyphRuns.end() - 1;
}
const auto* start = mGlyphRuns.begin();
const auto* limit = mGlyphRuns.end();
while (limit - start > 1) {
const auto* mid = start + (limit - start) / 2;
if (mid->mCharacterOffset <= aOffset) {
start = mid;
} else {
limit = mid;
}
}
MOZ_ASSERT(start->mCharacterOffset <= aOffset,
"Hmm, something went wrong, aOffset should have been found");
return start;
}
void gfxTextRun::AddGlyphRun(gfxFont* aFont, FontMatchType aMatchType,
uint32_t aUTF16Offset, bool aForceNewRun,
gfx::ShapedTextFlags aOrientation, bool aIsCJK) {
MOZ_ASSERT(aFont, "adding glyph run for null font!");
MOZ_ASSERT(aOrientation != gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_MIXED,
"mixed orientation should have been resolved");
if (!aFont) {
return;
}
if (mGlyphRuns.IsEmpty()) {
mGlyphRuns.AppendElement(
GlyphRun{aFont, aUTF16Offset, aOrientation, aMatchType, aIsCJK});
return;
}
uint32_t numGlyphRuns = mGlyphRuns.Length();
if (!aForceNewRun) {
GlyphRun* lastGlyphRun = &mGlyphRuns.LastElement();
MOZ_ASSERT(lastGlyphRun->mCharacterOffset <= aUTF16Offset,
"Glyph runs out of order (and run not forced)");
// Don't append a run if the font is already the one we want
if (lastGlyphRun->Matches(aFont, aOrientation, aIsCJK, aMatchType)) {
return;
}
// If the offset has not changed, avoid leaving a zero-length run
// by overwriting the last entry instead of appending...
if (lastGlyphRun->mCharacterOffset == aUTF16Offset) {
// ...except that if the run before the last entry had the same
// font as the new one wants, merge with it instead of creating
// adjacent runs with the same font
if (numGlyphRuns > 1 && mGlyphRuns[numGlyphRuns - 2].Matches(
aFont, aOrientation, aIsCJK, aMatchType)) {
mGlyphRuns.TruncateLength(numGlyphRuns - 1);
return;
}
lastGlyphRun->SetProperties(aFont, aOrientation, aIsCJK, aMatchType);
return;
}
}
MOZ_ASSERT(
aForceNewRun || numGlyphRuns > 0 || aUTF16Offset == 0,
"First run doesn't cover the first character (and run not forced)?");
mGlyphRuns.AppendElement(
GlyphRun{aFont, aUTF16Offset, aOrientation, aMatchType, aIsCJK});
}
void gfxTextRun::SanitizeGlyphRuns() {
if (mGlyphRuns.Length() < 2) {
return;
}
auto& runs = mGlyphRuns.Array();
// The runs are almost certain to be already sorted, so it's worth avoiding
// the Sort() call if possible.
bool isSorted = true;
uint32_t prevOffset = 0;
for (const auto& r : runs) {
if (r.mCharacterOffset < prevOffset) {
isSorted = false;
break;
}
prevOffset = r.mCharacterOffset;
}
if (!isSorted) {
runs.Sort(GlyphRunOffsetComparator());
}
// Coalesce adjacent glyph runs that have the same properties, and eliminate
// any empty runs.
GlyphRun* prevRun = nullptr;
const CompressedGlyph* charGlyphs = mCharacterGlyphs;
runs.RemoveElementsBy([&](GlyphRun& aRun) -> bool {
// First run is always retained.
if (!prevRun) {
prevRun = &aRun;
return false;
}
// Merge any run whose properties match its predecessor.
if (prevRun->Matches(aRun.mFont, aRun.mOrientation, aRun.mIsCJK,
aRun.mMatchType)) {
return true;
}
if (prevRun->mCharacterOffset >= aRun.mCharacterOffset) {
// Preceding run is empty (or has become so due to the adjusting for
// ligature boundaries), so we will overwrite it with this one, which
// will then be discarded.
*prevRun = aRun;
return true;
}
// If any glyph run starts with ligature-continuation characters, we need to
// advance it to the first "real" character to avoid drawing partial
// ligature glyphs from wrong font (seen with U+FEFF in reftest 474417-1, as
// Core Text eliminates the glyph, which makes it appear as if a ligature
// has been formed)
while (charGlyphs[aRun.mCharacterOffset].IsLigatureContinuation() &&
aRun.mCharacterOffset < GetLength()) {
aRun.mCharacterOffset++;
}
// We're keeping another run, so update prevRun pointer to refer to it (in
// its new position).
++prevRun;
return false;
});
MOZ_ASSERT(prevRun == &runs.LastElement(), "lost track of prevRun!");
// Drop any trailing empty run.
if (runs.Length() > 1 && prevRun->mCharacterOffset == GetLength()) {
runs.RemoveLastElement();
}
MOZ_ASSERT(!runs.IsEmpty());
if (runs.Length() == 1) {
mGlyphRuns.ConvertToElement();
}
}
void gfxTextRun::CopyGlyphDataFrom(gfxShapedWord* aShapedWord,
uint32_t aOffset) {
uint32_t wordLen = aShapedWord->GetLength();
MOZ_ASSERT(aOffset + wordLen <= GetLength(), "word overruns end of textrun");
CompressedGlyph* charGlyphs = GetCharacterGlyphs();
const CompressedGlyph* wordGlyphs = aShapedWord->GetCharacterGlyphs();
if (aShapedWord->HasDetailedGlyphs()) {
for (uint32_t i = 0; i < wordLen; ++i, ++aOffset) {
const CompressedGlyph& g = wordGlyphs[i];
if (!g.IsSimpleGlyph()) {
const DetailedGlyph* details =
g.GetGlyphCount() > 0 ? aShapedWord->GetDetailedGlyphs(i) : nullptr;
SetDetailedGlyphs(aOffset, g.GetGlyphCount(), details);
}
charGlyphs[aOffset] = g;
}
} else {
memcpy(charGlyphs + aOffset, wordGlyphs, wordLen * sizeof(CompressedGlyph));
}
}
void gfxTextRun::CopyGlyphDataFrom(gfxTextRun* aSource, Range aRange,
uint32_t aDest) {
MOZ_ASSERT(aRange.end <= aSource->GetLength(),
"Source substring out of range");
MOZ_ASSERT(aDest + aRange.Length() <= GetLength(),
"Destination substring out of range");
if (aSource->mDontSkipDrawing) {
mDontSkipDrawing = true;
}
// Copy base glyph data, and DetailedGlyph data where present
const CompressedGlyph* srcGlyphs = aSource->mCharacterGlyphs + aRange.start;
CompressedGlyph* dstGlyphs = mCharacterGlyphs + aDest;
for (uint32_t i = 0; i < aRange.Length(); ++i) {
CompressedGlyph g = srcGlyphs[i];
g.SetCanBreakBefore(!g.IsClusterStart()
? CompressedGlyph::FLAG_BREAK_TYPE_NONE
: dstGlyphs[i].CanBreakBefore());
if (!g.IsSimpleGlyph()) {
uint32_t count = g.GetGlyphCount();
if (count > 0) {
// DetailedGlyphs allocation is infallible, so this should never be
// null unless the source textrun is somehow broken.
DetailedGlyph* src = aSource->GetDetailedGlyphs(i + aRange.start);
MOZ_ASSERT(src, "missing DetailedGlyphs?");
if (src) {
DetailedGlyph* dst = AllocateDetailedGlyphs(i + aDest, count);
::memcpy(dst, src, count * sizeof(DetailedGlyph));
} else {
g.SetMissing();
}
}
}
dstGlyphs[i] = g;
}
// Copy glyph runs
#ifdef DEBUG
GlyphRun* prevRun = nullptr;
#endif
for (GlyphRunIterator iter(aSource, aRange); !iter.AtEnd(); iter.NextRun()) {
gfxFont* font = iter.GlyphRun()->mFont;
MOZ_ASSERT(!prevRun || !prevRun->Matches(iter.GlyphRun()->mFont,
iter.GlyphRun()->mOrientation,
iter.GlyphRun()->mIsCJK,
FontMatchType::Kind::kUnspecified),
"Glyphruns not coalesced?");
#ifdef DEBUG
prevRun = const_cast<GlyphRun*>(iter.GlyphRun());
uint32_t end = iter.StringEnd();
#endif