tdesktop/Telegram/SourceFiles/ui/text/text_block.cpp
John Preston 5195b4d3ef RTL strings with trailing spaces render fix.
Commit 8d354382a4 introduced a regression in RTL phrases display.
When an RTL line had trailing spaces we started displaying them in
front of the text still assuming counted line width value that did
not include those trailing spaces.

Line width is not including trailing spaces width because it is
allowed to fit in the line any number of spaces.

Also text block "left padding" entity was eliminated. If we have
some spaces in the start of the text block (for example a text block
after a link) we just add an empty word and mark those spaces as its
right padding.
2017-02-26 21:02:45 +03:00

365 lines
11 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop version of Telegram messaging app, see https://telegram.org
Telegram Desktop is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
It is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
In addition, as a special exception, the copyright holders give permission
to link the code of portions of this program with the OpenSSL library.
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
*/
#include "stdafx.h"
#include "ui/text/text_block.h"
// COPIED FROM qtextlayout.cpp AND MODIFIED
namespace {
struct ScriptLine {
ScriptLine() : length(0), textWidth(0) {
}
int32 length;
QFixed textWidth;
};
// All members finished with "_" are internal.
struct LineBreakHelper
{
LineBreakHelper()
: glyphCount(0), maxGlyphs(INT_MAX), currentPosition(0), fontEngine(0), logClusters(0)
{
}
ScriptLine tmpData;
ScriptLine spaceData;
QGlyphLayout glyphs;
int glyphCount;
int maxGlyphs;
int currentPosition;
glyph_t previousGlyph_ = 0;
QFontEngine *previousFontEngine_ = nullptr;
QFixed rightBearing;
QFontEngine *fontEngine;
const unsigned short *logClusters;
inline glyph_t currentGlyph() const
{
Q_ASSERT(currentPosition > 0);
Q_ASSERT(logClusters[currentPosition - 1] < glyphs.numGlyphs);
return glyphs.glyphs[logClusters[currentPosition - 1]];
}
inline void saveCurrentGlyph()
{
if (currentPosition > 0 &&
logClusters[currentPosition - 1] < glyphs.numGlyphs) {
previousGlyph_ = currentGlyph(); // needed to calculate right bearing later
previousFontEngine_ = fontEngine;
} else {
previousGlyph_ = 0;
previousFontEngine_ = nullptr;
}
}
inline void calculateRightBearing(QFontEngine *engine, glyph_t glyph)
{
qreal rb;
engine->getGlyphBearings(glyph, 0, &rb);
// We only care about negative right bearings, so we limit the range
// of the bearing here so that we can assume it's negative in the rest
// of the code, as well ase use QFixed(1) as a sentinel to represent
// the state where we have yet to compute the right bearing.
rightBearing = qMin(QFixed::fromReal(rb), QFixed(0));
}
inline void calculateRightBearing()
{
if (currentPosition > 0 &&
logClusters[currentPosition - 1] < glyphs.numGlyphs) {
calculateRightBearing(fontEngine, currentGlyph());
} else {
rightBearing = 0;
}
}
inline void calculateRightBearingForPreviousGlyph()
{
if (previousGlyph_ > 0) {
calculateRightBearing(previousFontEngine_, previousGlyph_);
} else {
rightBearing = 0;
}
}
// We always calculate the right bearing right before it is needed.
// So we don't need caching / optimizations referred to delayed right bearing calculations.
//static const QFixed RightBearingNotCalculated;
//inline void resetRightBearing()
//{
// rightBearing = RightBearingNotCalculated;
//}
// We express the negative right bearing as an absolute number
// so that it can be applied to the width using addition.
inline QFixed negativeRightBearing() const
{
//if (rightBearing == RightBearingNotCalculated)
// return QFixed(0);
return qAbs(rightBearing);
}
};
//const QFixed LineBreakHelper::RightBearingNotCalculated = QFixed(1);
static inline void addNextCluster(int &pos, int end, ScriptLine &line, int &glyphCount,
const QScriptItem &current, const unsigned short *logClusters,
const QGlyphLayout &glyphs)
{
int glyphPosition = logClusters[pos];
do { // got to the first next cluster
++pos;
++line.length;
} while (pos < end && logClusters[pos] == glyphPosition);
do { // calculate the textWidth for the rest of the current cluster.
if (!glyphs.attributes[glyphPosition].dontPrint)
line.textWidth += glyphs.advances[glyphPosition];
++glyphPosition;
} while (glyphPosition < current.num_glyphs && !glyphs.attributes[glyphPosition].clusterStart);
Q_ASSERT((pos == end && glyphPosition == current.num_glyphs) || logClusters[pos] == glyphPosition);
++glyphCount;
}
} // anonymous namespace
class BlockParser {
public:
BlockParser(QTextEngine *e, TextBlock *b, QFixed minResizeWidth, int32 blockFrom, const QString &str)
: block(b), eng(e), str(str) {
parseWords(minResizeWidth, blockFrom);
}
void parseWords(QFixed minResizeWidth, int32 blockFrom) {
LineBreakHelper lbh;
int item = -1;
int newItem = eng->findItem(0);
style::align alignment = eng->option.alignment();
const QCharAttributes *attributes = eng->attributes();
if (!attributes)
return;
int end = 0;
lbh.logClusters = eng->layoutData->logClustersPtr;
block->_words.clear();
int wordStart = lbh.currentPosition;
bool addingEachGrapheme = false;
int lastGraphemeBoundaryPosition = -1;
ScriptLine lastGraphemeBoundaryLine;
while (newItem < eng->layoutData->items.size()) {
if (newItem != item) {
item = newItem;
const QScriptItem &current = eng->layoutData->items[item];
if (!current.num_glyphs) {
eng->shape(item);
attributes = eng->attributes();
if (!attributes)
return;
lbh.logClusters = eng->layoutData->logClustersPtr;
}
lbh.currentPosition = current.position;
end = current.position + eng->length(item);
lbh.glyphs = eng->shapedGlyphs(&current);
QFontEngine *fontEngine = eng->fontEngine(current);
if (lbh.fontEngine != fontEngine) {
lbh.fontEngine = fontEngine;
}
}
const QScriptItem &current = eng->layoutData->items[item];
if (attributes[lbh.currentPosition].whiteSpace) {
while (lbh.currentPosition < end && attributes[lbh.currentPosition].whiteSpace)
addNextCluster(lbh.currentPosition, end, lbh.spaceData, lbh.glyphCount,
current, lbh.logClusters, lbh.glyphs);
if (block->_words.isEmpty()) {
block->_words.push_back(TextWord(wordStart + blockFrom, lbh.tmpData.textWidth, -lbh.negativeRightBearing()));
}
block->_words.back().add_rpadding(lbh.spaceData.textWidth);
block->_width += lbh.spaceData.textWidth;
lbh.spaceData.length = 0;
lbh.spaceData.textWidth = 0;
wordStart = lbh.currentPosition;
addingEachGrapheme = false;
lastGraphemeBoundaryPosition = -1;
lastGraphemeBoundaryLine = ScriptLine();
} else {
do {
addNextCluster(lbh.currentPosition, end, lbh.tmpData, lbh.glyphCount,
current, lbh.logClusters, lbh.glyphs);
if (lbh.currentPosition >= eng->layoutData->string.length()
|| attributes[lbh.currentPosition].whiteSpace
|| isLineBreak(attributes, lbh.currentPosition)) {
lbh.calculateRightBearing();
block->_words.push_back(TextWord(wordStart + blockFrom, lbh.tmpData.textWidth, -lbh.negativeRightBearing()));
block->_width += lbh.tmpData.textWidth;
lbh.tmpData.textWidth = 0;
lbh.tmpData.length = 0;
wordStart = lbh.currentPosition;
break;
} else if (attributes[lbh.currentPosition].graphemeBoundary) {
if (!addingEachGrapheme && lbh.tmpData.textWidth > minResizeWidth) {
if (lastGraphemeBoundaryPosition >= 0) {
lbh.calculateRightBearingForPreviousGlyph();
block->_words.push_back(TextWord(wordStart + blockFrom, -lastGraphemeBoundaryLine.textWidth, -lbh.negativeRightBearing()));
block->_width += lastGraphemeBoundaryLine.textWidth;
lbh.tmpData.textWidth -= lastGraphemeBoundaryLine.textWidth;
lbh.tmpData.length -= lastGraphemeBoundaryLine.length;
wordStart = lastGraphemeBoundaryPosition;
}
addingEachGrapheme = true;
}
if (addingEachGrapheme) {
lbh.calculateRightBearing();
block->_words.push_back(TextWord(wordStart + blockFrom, -lbh.tmpData.textWidth, -lbh.negativeRightBearing()));
block->_width += lbh.tmpData.textWidth;
lbh.tmpData.textWidth = 0;
lbh.tmpData.length = 0;
wordStart = lbh.currentPosition;
} else {
lastGraphemeBoundaryPosition = lbh.currentPosition;
lastGraphemeBoundaryLine = lbh.tmpData;
lbh.saveCurrentGlyph();
}
}
} while (lbh.currentPosition < end);
}
if (lbh.currentPosition == end)
newItem = item + 1;
}
if (!block->_words.isEmpty()) {
block->_rpadding = block->_words.back().f_rpadding();
block->_width -= block->_rpadding;
block->_words.squeeze();
}
}
bool isLineBreak(const QCharAttributes *attributes, int32 index) {
bool lineBreak = attributes[index].lineBreak;
if (lineBreak && block->lnkIndex() > 0 && index > 0 && str.at(index - 1) == '/') {
return false; // don't break after / in links
}
return lineBreak;
}
private:
TextBlock *block;
QTextEngine *eng;
const QString &str;
};
QFixed ITextBlock::f_rbearing() const {
return (type() == TextBlockTText) ? static_cast<const TextBlock*>(this)->real_f_rbearing() : 0;
}
TextBlock::TextBlock(const style::font &font, const QString &str, QFixed minResizeWidth, uint16 from, uint16 length, uchar flags, uint16 lnkIndex) : ITextBlock(font, str, from, length, flags, lnkIndex) {
_flags |= ((TextBlockTText & 0x0F) << 8);
if (length) {
style::font blockFont = font;
if (!flags && lnkIndex) {
// should use TextStyle lnkFlags somehow... not supported
}
if ((flags & TextBlockFPre) || (flags & TextBlockFCode)) {
blockFont = App::monofont();
if (blockFont->size() != font->size() || blockFont->flags() != font->flags()) {
blockFont = style::font(font->size(), font->flags(), blockFont->family());
}
} else {
if (flags & TextBlockFBold) {
blockFont = blockFont->bold();
} else if (flags & TextBlockFSemibold) {
blockFont = st::semiboldFont;
if (blockFont->size() != font->size() || blockFont->flags() != font->flags()) {
blockFont = style::font(font->size(), font->flags(), blockFont->family());
}
}
if (flags & TextBlockFItalic) blockFont = blockFont->italic();
if (flags & TextBlockFUnderline) blockFont = blockFont->underline();
if (flags & TextBlockFTilde) { // tilde fix in OpenSans
blockFont = st::semiboldFont;
}
}
QString part = str.mid(_from, length);
// Attempt to catch a crash in text processing
SignalHandlers::setCrashAnnotationRef("CrashString", &part);
QStackTextEngine engine(part, blockFont->f);
QTextLayout layout(&engine);
layout.beginLayout();
layout.createLine();
BlockParser parser(&engine, this, minResizeWidth, _from, part);
layout.endLayout();
SignalHandlers::clearCrashAnnotationRef("CrashString");
}
}
EmojiBlock::EmojiBlock(const style::font &font, const QString &str, uint16 from, uint16 length, uchar flags, uint16 lnkIndex, EmojiPtr emoji) : ITextBlock(font, str, from, length, flags, lnkIndex)
, emoji(emoji) {
_flags |= ((TextBlockTEmoji & 0x0F) << 8);
_width = int(st::emojiSize + 2 * st::emojiPadding);
_rpadding = 0;
for (auto i = length; i != 0;) {
auto ch = str[_from + (--i)];
if (ch.unicode() == QChar::Space) {
_rpadding += font->spacew;
} else {
break;
}
}
}
SkipBlock::SkipBlock(const style::font &font, const QString &str, uint16 from, int32 w, int32 h, uint16 lnkIndex) : ITextBlock(font, str, from, 1, 0, lnkIndex), _height(h) {
_flags |= ((TextBlockTSkip & 0x0F) << 8);
_width = w;
}