1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +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

@ -2,6 +2,38 @@
# Version 8.0.0
## Change
As part of the Unicode upgrades the vertical alignment logic of TextLayout has
been altered. Lines containing text written in multiple different fonts will
now have their baselines aligned. Additionally, using the
Justification::verticallyCentred or Justification::bottom flags may now result
in the text being positioned slightly differently.
**Possible Issues**
User interfaces using TextLayout with texts drawn using multiple fonts will now
have their look changed.
**Workaround**
There is no workaround.
**Rationale**
The old implementation had incosistent vertical alignment behaviour. Depending
on what exact fonts the first line of text happened to use, the bottom alignment
would sometimes produce unnecessary padding on the bottom. With certain text and
Font combinations the text would be drawn beyond the bottom boundary even though
there was free space above the text.
The same amount of incorrect vertical offset, that was calculated for bottom
alignment, was also present when using centred, it just wasn't as apparent.
Not having the baselines aligned between different fonts resulted in generally
displeasing visuals.
## Change
The virtual functions LowLevelGraphicsContext::drawGlyph() and drawTextLayout()

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()

View file

@ -53,7 +53,7 @@ private:
class DereferencingIterator
{
public:
using value_type = std::remove_reference_t<decltype(**std::declval<Iterator>())>;
using value_type = std::remove_reference_t<decltype (**std::declval<Iterator>())>;
using difference_type = typename std::iterator_traits<Iterator>::difference_type;
using pointer = value_type*;
using reference = value_type&;
@ -94,7 +94,7 @@ private:
DereferencingIterator operator++ (int) const { DereferencingIterator copy (*this); ++(*this); return copy; }
DereferencingIterator operator-- (int) const { DereferencingIterator copy (*this); --(*this); return copy; }
reference operator* () const { return **iterator; }
reference operator*() const { return **iterator; }
pointer operator->() const { return *iterator; }
private: