354 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			354 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-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<const TextBlock*>(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;
 | |
| }
 | 
