/* 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-2016 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; }; struct LineBreakHelper { LineBreakHelper() : glyphCount(0), maxGlyphs(0), currentPosition(0), fontEngine(0), logClusters(0) { } ScriptLine tmpData; ScriptLine spaceData; QGlyphLayout glyphs; int glyphCount; int maxGlyphs; int currentPosition; glyph_t previousGlyph; 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() { previousGlyph = 0; if (currentPosition > 0 && logClusters[currentPosition - 1] < glyphs.numGlyphs) { previousGlyph = currentGlyph(); // needed to calculate right bearing later } } inline void adjustRightBearing(glyph_t glyph) { qreal rb; fontEngine->getGlyphBearings(glyph, 0, &rb); rightBearing = qMin(QFixed(), QFixed::fromReal(rb)); } inline void adjustRightBearing() { if (currentPosition <= 0) return; adjustRightBearing(currentGlyph()); } inline void adjustPreviousRightBearing() { if (previousGlyph > 0) adjustRightBearing(previousGlyph); } }; static inline void addNextCluster(int &pos, int end, ScriptLine &line, int &glyphCount, const QScriptItem ¤t, 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; lbh.maxGlyphs = INT_MAX; int item = -1; int newItem = eng->findItem(0); style::align alignment = eng->option.alignment(); const QCharAttributes *attributes = eng->attributes(); if (!attributes) return; lbh.currentPosition = 0; int end = 0; lbh.logClusters = eng->layoutData->logClustersPtr; lbh.previousGlyph = 0; block->_lpadding = 0; 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 ¤t = 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(¤t); QFontEngine *fontEngine = eng->fontEngine(current); if (lbh.fontEngine != fontEngine) { lbh.fontEngine = fontEngine; } } const QScriptItem ¤t = 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->_lpadding = lbh.spaceData.textWidth; } else { block->_words.back().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.adjustRightBearing(); block->_words.push_back(TextWord(wordStart + blockFrom, lbh.tmpData.textWidth, qMin(QFixed(), lbh.rightBearing))); 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.adjustPreviousRightBearing(); block->_words.push_back(TextWord(wordStart + blockFrom, -lastGraphemeBoundaryLine.textWidth, qMin(QFixed(), lbh.rightBearing))); block->_width += lastGraphemeBoundaryLine.textWidth; lbh.tmpData.textWidth -= lastGraphemeBoundaryLine.textWidth; lbh.tmpData.length -= lastGraphemeBoundaryLine.length; wordStart = lastGraphemeBoundaryPosition; } addingEachGrapheme = true; } if (addingEachGrapheme) { lbh.adjustRightBearing(); block->_words.push_back(TextWord(wordStart + blockFrom, -lbh.tmpData.textWidth, qMin(QFixed(), lbh.rightBearing))); 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 = 0; } else { block->_rpadding = block->_words.back().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; }; TextBlock::TextBlock(const style::font &font, const QString &str, QFixed minResizeWidth, uint16 from, uint16 length, uchar flags, const style::color &color, uint16 lnkIndex) : ITextBlock(font, str, from, length, flags, color, 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); QStackTextEngine engine(part, blockFont->f); engine.itemize(); QTextLayout layout(&engine); layout.beginLayout(); layout.createLine(); bool logCrashString = (rand_value() % 4 == 1); if (logCrashString) { SignalHandlers::setCrashAnnotationRef("CrashString", &str); } BlockParser parser(&engine, this, minResizeWidth, _from, part); if (logCrashString) { SignalHandlers::clearCrashAnnotationRef("CrashString"); } layout.endLayout(); } } EmojiBlock::EmojiBlock(const style::font &font, const QString &str, uint16 from, uint16 length, uchar flags, const style::color &color, uint16 lnkIndex, const EmojiData *emoji) : ITextBlock(font, str, from, length, flags, color, lnkIndex), emoji(emoji) { _flags |= ((TextBlockTEmoji & 0x0F) << 8); _width = int(st::emojiSize + 2 * st::emojiPadding); } SkipBlock::SkipBlock(const style::font &font, const QString &str, uint16 from, int32 w, int32 h, uint16 lnkIndex) : ITextBlock(font, str, from, 1, 0, style::color(), lnkIndex), _height(h) { _flags |= ((TextBlockTSkip & 0x0F) << 8); _width = w; }