/* 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; }; // 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 ¤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; 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->_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().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 = 0; } else { 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(this)->real_f_rbearing() : 0; } 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(); BlockParser parser(&engine, this, minResizeWidth, _from, part); 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; }