1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

TextLayout: Use new shaping engine

By utilising ShapedText in the TextLayout implementation.
This commit is contained in:
Oliver James 2024-05-01 12:14:12 +01:00 committed by attila
parent 30daa356ca
commit 03e79f8f12
3 changed files with 164 additions and 275 deletions

View file

@ -35,11 +35,6 @@
namespace juce
{
static String substring (const String& text, Range<int> range)
{
return text.substring (range.getStart(), range.getEnd());
}
TextLayout::Glyph::Glyph (int glyph, Point<float> anch, float w) noexcept
: glyphCode (glyph), anchor (anch), width (w)
{
@ -325,279 +320,141 @@ void TextLayout::createLayoutWithBalancedLineLengths (const AttributedString& te
}
//==============================================================================
namespace TextLayoutHelpers
template <typename T, typename U>
static auto castTo (const Range<U>& r)
{
struct Token
{
Token (const String& t, const Font& f, Colour c, bool whitespace)
: text (t), font (f), colour (c),
area (font.getStringWidthFloat (t), f.getHeight()),
isWhitespace (whitespace),
isNewLine (t.containsChar ('\n') || t.containsChar ('\r'))
{}
const String text;
const Font font;
const Colour colour;
Rectangle<float> area;
int line;
float lineHeight;
const bool isWhitespace, isNewLine;
Token& operator= (const Token&) = delete;
};
struct TokenList
{
TokenList() noexcept {}
void createLayout (const AttributedString& text, TextLayout& layout)
{
layout.ensureStorageAllocated (totalLines);
addTextRuns (text);
layoutRuns (layout.getWidth(), text.getLineSpacing(), text.getWordWrap());
int charPosition = 0;
int lineStartPosition = 0;
int runStartPosition = 0;
std::unique_ptr<TextLayout::Line> currentLine;
std::unique_ptr<TextLayout::Run> currentRun;
bool needToSetLineOrigin = true;
for (int i = 0; i < tokens.size(); ++i)
{
auto& t = *tokens.getUnchecked (i);
Array<int> newGlyphs;
Array<float> xOffsets;
t.font.getGlyphPositions (getTrimmedEndIfNotAllWhitespace (t.text), newGlyphs, xOffsets);
if (currentRun == nullptr) currentRun = std::make_unique<TextLayout::Run>();
if (currentLine == nullptr) currentLine = std::make_unique<TextLayout::Line>();
const auto numGlyphs = newGlyphs.size();
charPosition += numGlyphs;
if (numGlyphs > 0
&& (! (t.isWhitespace || t.isNewLine) || needToSetLineOrigin))
{
currentRun->glyphs.ensureStorageAllocated (currentRun->glyphs.size() + newGlyphs.size());
auto tokenOrigin = t.area.getPosition().translated (0, t.font.getAscent());
if (needToSetLineOrigin)
{
needToSetLineOrigin = false;
currentLine->lineOrigin = tokenOrigin;
}
auto glyphOffset = tokenOrigin - currentLine->lineOrigin;
for (int j = 0; j < newGlyphs.size(); ++j)
{
auto x = xOffsets.getUnchecked (j);
currentRun->glyphs.add (TextLayout::Glyph (newGlyphs.getUnchecked (j),
glyphOffset.translated (x, 0),
xOffsets.getUnchecked (j + 1) - x));
}
}
if (auto* nextToken = tokens[i + 1])
{
if (t.font != nextToken->font || t.colour != nextToken->colour)
{
addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
runStartPosition = charPosition;
}
if (t.line != nextToken->line)
{
if (currentRun == nullptr)
currentRun = std::make_unique<TextLayout::Run>();
addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
currentLine->stringRange = { lineStartPosition, charPosition };
if (! needToSetLineOrigin)
layout.addLine (std::move (currentLine));
runStartPosition = charPosition;
lineStartPosition = charPosition;
needToSetLineOrigin = true;
}
}
else
{
addRun (*currentLine, currentRun.release(), t, runStartPosition, charPosition);
currentLine->stringRange = { lineStartPosition, charPosition };
if (! needToSetLineOrigin)
layout.addLine (std::move (currentLine));
needToSetLineOrigin = true;
}
}
if ((text.getJustification().getFlags() & (Justification::right | Justification::horizontallyCentred)) != 0)
{
auto totalW = layout.getWidth();
bool isCentred = (text.getJustification().getFlags() & Justification::horizontallyCentred) != 0;
for (auto& line : layout)
{
auto dx = totalW - line.getLineBoundsX().getLength();
if (isCentred)
dx /= 2.0f;
line.lineOrigin.x += dx;
}
}
}
private:
static void addRun (TextLayout::Line& glyphLine, TextLayout::Run* glyphRun,
const Token& t, int start, int end)
{
glyphRun->stringRange = { start, end };
glyphRun->font = t.font;
glyphRun->colour = t.colour;
glyphLine.ascent = jmax (glyphLine.ascent, t.font.getAscent());
glyphLine.descent = jmax (glyphLine.descent, t.font.getDescent());
glyphLine.runs.add (glyphRun);
}
static int getCharacterType (juce_wchar c) noexcept
{
if (c == '\r' || c == '\n')
return 0;
return CharacterFunctions::isWhitespace (c) ? 2 : 1;
}
void appendText (const String& stringText, const Font& font, Colour colour)
{
auto t = stringText.getCharPointer();
String currentString;
int lastCharType = 0;
for (;;)
{
auto c = t.getAndAdvance();
if (c == 0)
break;
auto charType = getCharacterType (c);
if (charType == 0 || charType != lastCharType)
{
if (currentString.isNotEmpty())
tokens.add (new Token (currentString, font, colour,
lastCharType == 2 || lastCharType == 0));
currentString = String::charToString (c);
if (c == '\r' && *t == '\n')
currentString += t.getAndAdvance();
}
else
{
currentString += c;
}
lastCharType = charType;
}
if (currentString.isNotEmpty())
tokens.add (new Token (currentString, font, colour, lastCharType == 2));
}
void layoutRuns (float maxWidth, float extraLineSpacing, AttributedString::WordWrap wordWrap)
{
float x = 0, y = 0, h = 0;
int i;
for (i = 0; i < tokens.size(); ++i)
{
auto& t = *tokens.getUnchecked (i);
t.area.setPosition (x, y);
t.line = totalLines;
x += t.area.getWidth();
h = jmax (h, t.area.getHeight() + extraLineSpacing);
auto* nextTok = tokens[i + 1];
if (nextTok == nullptr)
break;
bool tokenTooLarge = (x + nextTok->area.getWidth() > maxWidth);
if (t.isNewLine || ((! nextTok->isWhitespace) && (tokenTooLarge && wordWrap != AttributedString::none)))
{
setLastLineHeight (i + 1, h);
x = 0;
y += h;
h = 0;
++totalLines;
}
}
setLastLineHeight (jmin (i + 1, tokens.size()), h);
++totalLines;
}
void setLastLineHeight (int i, float height) noexcept
{
while (--i >= 0)
{
auto& tok = *tokens.getUnchecked (i);
if (tok.line == totalLines)
tok.lineHeight = height;
else
break;
}
}
void addTextRuns (const AttributedString& text)
{
auto numAttributes = text.getNumAttributes();
tokens.ensureStorageAllocated (jmax (64, numAttributes));
for (int i = 0; i < numAttributes; ++i)
{
auto& attr = text.getAttribute (i);
appendText (substring (text.getText(), attr.range),
attr.font, attr.colour);
}
}
static String getTrimmedEndIfNotAllWhitespace (const String& s)
{
auto trimmed = s.trimEnd();
if (trimmed.isEmpty() && s.isNotEmpty())
trimmed = s.replaceCharacters ("\r\n\t", " ");
return trimmed;
}
OwnedArray<Token> tokens;
int totalLines = 0;
JUCE_DECLARE_NON_COPYABLE (TokenList)
};
return Range<T> (static_cast<T> (r.getStart()), static_cast<T> (r.getEnd()));
}
static auto getFontsForRange (const detail::RangedValues<Font>& fonts)
{
std::vector<FontForRange> result;
result.reserve (fonts.size());
std::transform (fonts.begin(),
fonts.end(),
std::back_inserter (result),
[] (auto entry) {
return FontForRange { entry.range, entry.value };
});
return result;
}
static Range<int64> getInputRange (const ShapedText& st, Range<int64> glyphRange)
{
if (glyphRange.isEmpty())
{
jassertfalse;
return {};
}
const auto startInputRange = st.getTextRange (glyphRange.getStart());
const auto endInputRange = st.getTextRange (glyphRange.getEnd() - 1);
// The glyphRange is always in visual order and could have an opposite direction to the text
return { std::min (startInputRange.getStart(), endInputRange.getStart()),
std::max (startInputRange.getEnd(), endInputRange.getEnd()) };
}
static Range<int64> getLineInputRange (const ShapedText& st, int64 lineNumber)
{
return getInputRange (st, ShapedText::Detail { &st }.getSimpleShapedText()
.getLineNumbers()
.getItem ((size_t) lineNumber).range);
}
struct MaxFontAscentAndDescent
{
float ascent{}, descent{};
};
static MaxFontAscentAndDescent getMaxFontAscentAndDescentInEnclosingLine (const ShapedText& st,
Range<int64> lineChunkRange)
{
const auto sst = ShapedText::Detail { &st }.getSimpleShapedText();
const auto lineRange = sst.getLineNumbers()
.getItemWithEnclosingRange (lineChunkRange.getStart())->range;
const auto fonts = sst.getResolvedFonts().getIntersectionsWith (lineRange);
MaxFontAscentAndDescent result;
for (const auto& [r, font] : fonts)
{
result.ascent = std::max (result.ascent, font.getAscent());
result.descent = std::max (result.descent, font.getDescent());
}
return result;
}
//==============================================================================
void TextLayout::createStandardLayout (const AttributedString& text)
{
TextLayoutHelpers::TokenList l;
l.createLayout (text, *this);
detail::RangedValues<Font> fonts;
detail::RangedValues<Colour> colours;
for (auto i = 0, iMax = text.getNumAttributes(); i < iMax; ++i)
{
const auto& attribute = text.getAttribute (i);
const auto range = castTo<int64> (attribute.range);
fonts.set (range, attribute.font);
colours.set (range, attribute.colour);
}
ShapedText shapedText { text.getText(), ShapedTextOptions{}.withFontsForRange (getFontsForRange (fonts))
.withMaxWidth (width)
.withLanguage (SystemStats::getUserLanguage())
.withTrailingWhitespacesShouldFit (false)
.withJustification (justification) };
std::optional<int64> lastLineNumber;
std::unique_ptr<Line> line;
auto& jt = ShapedText::Detail { &shapedText }.getJustifiedText();
jt.accessTogetherWith ([&] (Span<const ShapedGlyph> glyphs,
Span<Point<float>> positions,
Font font,
Range<int64> glyphRange,
int64 lineNumber,
Colour colour)
{
if (std::exchange (lastLineNumber, lineNumber) != lineNumber)
{
if (line != nullptr)
addLine (std::move (line));
const auto ascentAndDescent = getMaxFontAscentAndDescentInEnclosingLine (shapedText,
glyphRange);
line = std::make_unique<Line> (castTo<int> (getLineInputRange (shapedText, lineNumber)),
positions[0],
ascentAndDescent.ascent,
ascentAndDescent.descent,
0.0f,
0);
}
auto run = std::make_unique<Run> (castTo<int> (getInputRange (shapedText, glyphRange)), 0);
run->font = font;
run->colour = colour;
for (decltype (glyphs.size()) i = 0, iMax = glyphs.size(); i < iMax; ++i)
{
if (glyphs[i].whitespace)
continue;
run->glyphs.add ({ (int) glyphs[i].glyphId, positions[i] - line->lineOrigin, glyphs[i].advance.x });
}
line->runs.add (std::move (run));
},
colours);
if (line != nullptr)
addLine (std::move (line));
}
void TextLayout::recalculateSize()