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:
parent
30daa356ca
commit
03e79f8f12
3 changed files with 164 additions and 275 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue