| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtGui module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qtextlayout.h" |
| #include "qtextengine_p.h" |
| |
| #include <qthread.h> |
| #include <qfont.h> |
| #include <qmath.h> |
| #include <qpainter.h> |
| #include <qvarlengtharray.h> |
| #include <qtextformat.h> |
| #include <qabstracttextdocumentlayout.h> |
| #include "qtextdocument_p.h" |
| #include "qtextformat_p.h" |
| #include "qpainterpath.h" |
| #include "qglyphrun.h" |
| #include "qglyphrun_p.h" |
| #include "qrawfont.h" |
| #include "qrawfont_p.h" |
| #include <limits.h> |
| |
| #include <qdebug.h> |
| |
| #include "qfontengine_p.h" |
| #include <private/qpainter_p.h> |
| |
| QT_BEGIN_NAMESPACE |
| |
| #define ObjectSelectionBrush (QTextFormat::ForegroundBrush + 1) |
| #define SuppressText 0x5012 |
| #define SuppressBackground 0x513 |
| |
| /*! |
| \class QTextLayout::FormatRange |
| \reentrant |
| |
| \brief The QTextLayout::FormatRange structure is used to apply extra formatting information |
| for a specified area in the text layout's content. |
| \inmodule QtGui |
| |
| \sa QTextLayout::setFormats(), QTextLayout::draw() |
| */ |
| |
| /*! |
| \variable QTextLayout::FormatRange::start |
| Specifies the beginning of the format range within the text layout's text. |
| */ |
| |
| /*! |
| \variable QTextLayout::FormatRange::length |
| Specifies the numer of characters the format range spans. |
| */ |
| |
| /*! |
| \variable QTextLayout::FormatRange::format |
| Specifies the format to apply. |
| */ |
| |
| /*! \fn bool operator==(const QTextLayout::FormatRange &lhs, const QTextLayout::FormatRange &rhs) |
| \relates QTextLayout::FormatRange |
| |
| Returns true if the \c {start}, \c {length}, and \c {format} fields |
| in \a lhs and \a rhs contain the same values respectively. |
| */ |
| |
| /*! \fn bool operator!=(const QTextLayout::FormatRange &lhs, const QTextLayout::FormatRange &rhs) |
| \relates QTextLayout::FormatRange |
| |
| Returns true if any of the \c {start}, \c {length}, or \c {format} fields |
| in \a lhs and \a rhs contain different values respectively. |
| */ |
| |
| /*! |
| \class QTextInlineObject |
| \reentrant |
| |
| \brief The QTextInlineObject class represents an inline object in |
| a QAbstractTextDocumentLayout and its implementations. |
| \inmodule QtGui |
| |
| \ingroup richtext-processing |
| |
| Normally, you do not need to create a QTextInlineObject. It is |
| used by QAbstractTextDocumentLayout to handle inline objects when |
| implementing a custom layout. |
| |
| The inline object has various attributes that can be set, for |
| example using, setWidth(), setAscent(), and setDescent(). The |
| rectangle it occupies is given by rect(), and its direction by |
| textDirection(). Its position in the text layout is given by |
| textPosition(), and its format is given by format(). |
| */ |
| |
| /*! |
| \fn QTextInlineObject::QTextInlineObject(int i, QTextEngine *e) |
| \internal |
| |
| Creates a new inline object for the item at position \a i in the |
| text engine \a e. |
| */ |
| |
| /*! |
| \fn QTextInlineObject::QTextInlineObject() |
| |
| \internal |
| */ |
| |
| /*! |
| \fn bool QTextInlineObject::isValid() const |
| |
| Returns \c true if this inline object is valid; otherwise returns |
| false. |
| */ |
| |
| /*! |
| Returns the inline object's rectangle. |
| |
| \sa ascent(), descent(), width() |
| */ |
| QRectF QTextInlineObject::rect() const |
| { |
| QScriptItem& si = eng->layoutData->items[itm]; |
| return QRectF(0, -si.ascent.toReal(), si.width.toReal(), si.height().toReal()); |
| } |
| |
| /*! |
| Returns the inline object's width. |
| |
| \sa ascent(), descent(), rect() |
| */ |
| qreal QTextInlineObject::width() const |
| { |
| return eng->layoutData->items.at(itm).width.toReal(); |
| } |
| |
| /*! |
| Returns the inline object's ascent. |
| |
| \sa descent(), width(), rect() |
| */ |
| qreal QTextInlineObject::ascent() const |
| { |
| return eng->layoutData->items.at(itm).ascent.toReal(); |
| } |
| |
| /*! |
| Returns the inline object's descent. |
| |
| \sa ascent(), width(), rect() |
| */ |
| qreal QTextInlineObject::descent() const |
| { |
| return eng->layoutData->items.at(itm).descent.toReal(); |
| } |
| |
| /*! |
| Returns the inline object's total height. This is equal to |
| ascent() + descent() + 1. |
| |
| \sa ascent(), descent(), width(), rect() |
| */ |
| qreal QTextInlineObject::height() const |
| { |
| return eng->layoutData->items.at(itm).height().toReal(); |
| } |
| |
| /*! |
| Sets the inline object's width to \a w. |
| |
| \sa width(), ascent(), descent(), rect() |
| */ |
| void QTextInlineObject::setWidth(qreal w) |
| { |
| eng->layoutData->items[itm].width = QFixed::fromReal(w); |
| } |
| |
| /*! |
| Sets the inline object's ascent to \a a. |
| |
| \sa ascent(), setDescent(), width(), rect() |
| */ |
| void QTextInlineObject::setAscent(qreal a) |
| { |
| eng->layoutData->items[itm].ascent = QFixed::fromReal(a); |
| } |
| |
| /*! |
| Sets the inline object's descent to \a d. |
| |
| \sa descent(), setAscent(), width(), rect() |
| */ |
| void QTextInlineObject::setDescent(qreal d) |
| { |
| eng->layoutData->items[itm].descent = QFixed::fromReal(d); |
| } |
| |
| /*! |
| The position of the inline object within the text layout. |
| */ |
| int QTextInlineObject::textPosition() const |
| { |
| return eng->layoutData->items[itm].position; |
| } |
| |
| /*! |
| Returns an integer describing the format of the inline object |
| within the text layout. |
| */ |
| int QTextInlineObject::formatIndex() const |
| { |
| return eng->formatIndex(&eng->layoutData->items[itm]); |
| } |
| |
| /*! |
| Returns format of the inline object within the text layout. |
| */ |
| QTextFormat QTextInlineObject::format() const |
| { |
| return eng->format(&eng->layoutData->items[itm]); |
| } |
| |
| /*! |
| Returns if the object should be laid out right-to-left or left-to-right. |
| */ |
| Qt::LayoutDirection QTextInlineObject::textDirection() const |
| { |
| return (eng->layoutData->items[itm].analysis.bidiLevel % 2 ? Qt::RightToLeft : Qt::LeftToRight); |
| } |
| |
| /*! |
| \class QTextLayout |
| \reentrant |
| |
| \brief The QTextLayout class is used to lay out and render text. |
| \inmodule QtGui |
| |
| \ingroup richtext-processing |
| |
| It offers many features expected from a modern text layout |
| engine, including Unicode compliant rendering, line breaking and |
| handling of cursor positioning. It can also produce and render |
| device independent layout, something that is important for WYSIWYG |
| applications. |
| |
| The class has a rather low level API and unless you intend to |
| implement your own text rendering for some specialized widget, you |
| probably won't need to use it directly. |
| |
| QTextLayout can be used with both plain and rich text. |
| |
| QTextLayout can be used to create a sequence of QTextLine |
| instances with given widths and can position them independently |
| on the screen. Once the layout is done, these lines can be drawn |
| on a paint device. |
| |
| The text to be laid out can be provided in the constructor or set with |
| setText(). |
| |
| The layout can be seen as a sequence of QTextLine objects; use createLine() |
| to create a QTextLine instance, and lineAt() or lineForTextPosition() to retrieve |
| created lines. |
| |
| Here is a code snippet that demonstrates the layout phase: |
| \snippet code/src_gui_text_qtextlayout.cpp 0 |
| |
| The text can then be rendered by calling the layout's draw() function: |
| \snippet code/src_gui_text_qtextlayout.cpp 1 |
| |
| For a given position in the text you can find a valid cursor position with |
| isValidCursorPosition(), nextCursorPosition(), and previousCursorPosition(). |
| |
| The QTextLayout itself can be positioned with setPosition(); it has a |
| boundingRect(), and a minimumWidth() and a maximumWidth(). |
| |
| \sa QStaticText |
| */ |
| |
| /*! |
| \enum QTextLayout::CursorMode |
| |
| \value SkipCharacters |
| \value SkipWords |
| */ |
| |
| /*! |
| \fn QTextEngine *QTextLayout::engine() const |
| \internal |
| |
| Returns the text engine used to render the text layout. |
| */ |
| |
| /*! |
| Constructs an empty text layout. |
| |
| \sa setText() |
| */ |
| QTextLayout::QTextLayout() |
| { d = new QTextEngine(); } |
| |
| /*! |
| Constructs a text layout to lay out the given \a text. |
| */ |
| QTextLayout::QTextLayout(const QString& text) |
| { |
| d = new QTextEngine(); |
| d->text = text; |
| } |
| |
| /*! |
| \since 5.13 |
| \fn QTextLayout::QTextLayout(const QString &text, const QFont &font, const QPaintDevice *paintdevice) |
| Constructs a text layout to lay out the given \a text with the specified |
| \a font. |
| |
| All the metric and layout calculations will be done in terms of |
| the paint device, \a paintdevice. If \a paintdevice is 0 the |
| calculations will be done in screen metrics. |
| */ |
| |
| #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) |
| /*! |
| \fn QTextLayout::QTextLayout(const QString &text, const QFont &font, QPaintDevice *paintdevice) |
| \obsolete |
| Identical to QTextLayout::QTextLayout(const QString &text, const QFont &font, const QPaintDevice *paintdevice) |
| */ |
| |
| QTextLayout::QTextLayout(const QString &text, const QFont &font, QPaintDevice *paintdevice) |
| #else |
| QTextLayout::QTextLayout(const QString &text, const QFont &font, const QPaintDevice *paintdevice) |
| #endif |
| { |
| const QFont f(paintdevice ? QFont(font, paintdevice) : font); |
| d = new QTextEngine((text.isNull() ? (const QString&)QString::fromLatin1("") : text), f); |
| } |
| |
| /*! |
| \internal |
| Constructs a text layout to lay out the given \a block. |
| */ |
| QTextLayout::QTextLayout(const QTextBlock &block) |
| { |
| d = new QTextEngine(); |
| d->block = block; |
| } |
| |
| /*! |
| Destructs the layout. |
| */ |
| QTextLayout::~QTextLayout() |
| { |
| if (!d->stackEngine) |
| delete d; |
| } |
| |
| #ifndef QT_NO_RAWFONT |
| /*! |
| \internal |
| Sets a raw font, to be used with QTextLayout::glyphRuns. |
| Note that this only supports the needs of WebKit. |
| Use of this function with e.g. QTextLayout::draw will result |
| in undefined behaviour. |
| */ |
| void QTextLayout::setRawFont(const QRawFont &rawFont) |
| { |
| d->rawFont = rawFont; |
| d->useRawFont = true; |
| d->resetFontEngineCache(); |
| } |
| #endif |
| |
| /*! |
| Sets the layout's font to the given \a font. The layout is |
| invalidated and must be laid out again. |
| |
| \sa font() |
| */ |
| void QTextLayout::setFont(const QFont &font) |
| { |
| d->fnt = font; |
| #ifndef QT_NO_RAWFONT |
| d->useRawFont = false; |
| #endif |
| d->resetFontEngineCache(); |
| } |
| |
| /*! |
| Returns the current font that is used for the layout, or a default |
| font if none is set. |
| |
| \sa setFont() |
| */ |
| QFont QTextLayout::font() const |
| { |
| return d->font(); |
| } |
| |
| /*! |
| Sets the layout's text to the given \a string. The layout is |
| invalidated and must be laid out again. |
| |
| Notice that when using this QTextLayout as part of a QTextDocument this |
| method will have no effect. |
| |
| \sa text() |
| */ |
| void QTextLayout::setText(const QString& string) |
| { |
| d->invalidate(); |
| d->clearLineData(); |
| d->text = string; |
| } |
| |
| /*! |
| Returns the layout's text. |
| |
| \sa setText() |
| */ |
| QString QTextLayout::text() const |
| { |
| return d->text; |
| } |
| |
| /*! |
| Sets the text option structure that controls the layout process to the |
| given \a option. |
| |
| \sa textOption() |
| */ |
| void QTextLayout::setTextOption(const QTextOption &option) |
| { |
| d->option = option; |
| } |
| |
| /*! |
| Returns the current text option used to control the layout process. |
| |
| \sa setTextOption() |
| */ |
| const QTextOption &QTextLayout::textOption() const |
| { |
| return d->option; |
| } |
| |
| /*! |
| Sets the \a position and \a text of the area in the layout that is |
| processed before editing occurs. The layout is |
| invalidated and must be laid out again. |
| |
| \sa preeditAreaPosition(), preeditAreaText() |
| */ |
| void QTextLayout::setPreeditArea(int position, const QString &text) |
| { |
| if (d->preeditAreaPosition() == position && d->preeditAreaText() == text) |
| return; |
| d->setPreeditArea(position, text); |
| |
| if (d->block.docHandle()) |
| d->block.docHandle()->documentChange(d->block.position(), d->block.length()); |
| } |
| |
| /*! |
| Returns the position of the area in the text layout that will be |
| processed before editing occurs. |
| |
| \sa preeditAreaText() |
| */ |
| int QTextLayout::preeditAreaPosition() const |
| { |
| return d->preeditAreaPosition(); |
| } |
| |
| /*! |
| Returns the text that is inserted in the layout before editing occurs. |
| |
| \sa preeditAreaPosition() |
| */ |
| QString QTextLayout::preeditAreaText() const |
| { |
| return d->preeditAreaText(); |
| } |
| |
| #if QT_DEPRECATED_SINCE(5, 6) |
| /*! |
| \obsolete Use setFormats() instead. |
| */ |
| void QTextLayout::setAdditionalFormats(const QList<FormatRange> &formatList) |
| { |
| setFormats(formatList.toVector()); |
| } |
| #endif // deprecated since 5.6 |
| |
| /*! |
| \since 5.6 |
| |
| Sets the additional formats supported by the text layout to \a formats. |
| The formats are applied with preedit area text in place. |
| |
| \sa formats(), clearFormats() |
| */ |
| void QTextLayout::setFormats(const QVector<FormatRange> &formats) |
| { |
| d->setFormats(formats); |
| |
| if (d->block.docHandle()) |
| d->block.docHandle()->documentChange(d->block.position(), d->block.length()); |
| } |
| |
| #if QT_DEPRECATED_SINCE(5, 6) |
| /*! |
| \obsolete Use formats() instead. |
| |
| \sa setAdditionalFormats(), clearAdditionalFormats() |
| */ |
| QList<QTextLayout::FormatRange> QTextLayout::additionalFormats() const |
| { |
| return formats().toList(); |
| } |
| #endif // deprecated since 5.6 |
| |
| /*! |
| \since 5.6 |
| |
| Returns the list of additional formats supported by the text layout. |
| |
| \sa setFormats(), clearFormats() |
| */ |
| QVector<QTextLayout::FormatRange> QTextLayout::formats() const |
| { |
| return d->formats(); |
| } |
| |
| #if QT_DEPRECATED_SINCE(5, 6) |
| /*! |
| \obsolete Use clearFormats() instead. |
| */ |
| void QTextLayout::clearAdditionalFormats() |
| { |
| clearFormats(); |
| } |
| #endif // deprecated since 5.6 |
| |
| /*! |
| \since 5.6 |
| |
| Clears the list of additional formats supported by the text layout. |
| |
| \sa formats(), setFormats() |
| */ |
| void QTextLayout::clearFormats() |
| { |
| setFormats(QVector<FormatRange>()); |
| } |
| |
| /*! |
| Enables caching of the complete layout information if \a enable is |
| true; otherwise disables layout caching. Usually |
| QTextLayout throws most of the layouting information away after a |
| call to endLayout() to reduce memory consumption. If you however |
| want to draw the laid out text directly afterwards enabling caching |
| might speed up drawing significantly. |
| |
| \sa cacheEnabled() |
| */ |
| void QTextLayout::setCacheEnabled(bool enable) |
| { |
| d->cacheGlyphs = enable; |
| } |
| |
| /*! |
| Returns \c true if the complete layout information is cached; otherwise |
| returns \c false. |
| |
| \sa setCacheEnabled() |
| */ |
| bool QTextLayout::cacheEnabled() const |
| { |
| return d->cacheGlyphs; |
| } |
| |
| /*! |
| Sets the visual cursor movement style to the given \a style. If the |
| QTextLayout is backed by a document, you can ignore this and use the option |
| in QTextDocument, this option is for widgets like QLineEdit or custom |
| widgets without a QTextDocument. Default value is Qt::LogicalMoveStyle. |
| |
| \sa cursorMoveStyle() |
| */ |
| void QTextLayout::setCursorMoveStyle(Qt::CursorMoveStyle style) |
| { |
| d->visualMovement = style == Qt::VisualMoveStyle; |
| } |
| |
| /*! |
| The cursor movement style of this QTextLayout. The default is |
| Qt::LogicalMoveStyle. |
| |
| \sa setCursorMoveStyle() |
| */ |
| Qt::CursorMoveStyle QTextLayout::cursorMoveStyle() const |
| { |
| return d->visualMovement ? Qt::VisualMoveStyle : Qt::LogicalMoveStyle; |
| } |
| |
| /*! |
| Begins the layout process. |
| |
| \warning This will invalidate the layout, so all existing QTextLine objects |
| that refer to the previous contents should now be discarded. |
| |
| \sa endLayout() |
| */ |
| void QTextLayout::beginLayout() |
| { |
| #ifndef QT_NO_DEBUG |
| if (d->layoutData && d->layoutData->layoutState == QTextEngine::InLayout) { |
| qWarning("QTextLayout::beginLayout: Called while already doing layout"); |
| return; |
| } |
| #endif |
| d->invalidate(); |
| d->clearLineData(); |
| d->itemize(); |
| d->layoutData->layoutState = QTextEngine::InLayout; |
| } |
| |
| /*! |
| Ends the layout process. |
| |
| \sa beginLayout() |
| */ |
| void QTextLayout::endLayout() |
| { |
| #ifndef QT_NO_DEBUG |
| if (!d->layoutData || d->layoutData->layoutState == QTextEngine::LayoutEmpty) { |
| qWarning("QTextLayout::endLayout: Called without beginLayout()"); |
| return; |
| } |
| #endif |
| int l = d->lines.size(); |
| if (l && d->lines.at(l-1).length < 0) { |
| QTextLine(l-1, d).setNumColumns(INT_MAX); |
| } |
| d->layoutData->layoutState = QTextEngine::LayoutEmpty; |
| if (!d->cacheGlyphs) |
| d->freeMemory(); |
| } |
| |
| /*! |
| \since 4.4 |
| |
| Clears the line information in the layout. After having called |
| this function, lineCount() returns 0. |
| |
| \warning This will invalidate the layout, so all existing QTextLine objects |
| that refer to the previous contents should now be discarded. |
| */ |
| void QTextLayout::clearLayout() |
| { |
| d->clearLineData(); |
| } |
| |
| /*! |
| Returns the next valid cursor position after \a oldPos that |
| respects the given cursor \a mode. |
| Returns value of \a oldPos, if \a oldPos is not a valid cursor position. |
| |
| \sa isValidCursorPosition(), previousCursorPosition() |
| */ |
| int QTextLayout::nextCursorPosition(int oldPos, CursorMode mode) const |
| { |
| const QCharAttributes *attributes = d->attributes(); |
| int len = d->block.isValid() ? d->block.length() - 1 |
| : d->layoutData->string.length(); |
| Q_ASSERT(len <= d->layoutData->string.length()); |
| if (!attributes || oldPos < 0 || oldPos >= len) |
| return oldPos; |
| |
| if (mode == SkipCharacters) { |
| oldPos++; |
| while (oldPos < len && !attributes[oldPos].graphemeBoundary) |
| oldPos++; |
| } else { |
| if (oldPos < len && d->atWordSeparator(oldPos)) { |
| oldPos++; |
| while (oldPos < len && d->atWordSeparator(oldPos)) |
| oldPos++; |
| } else { |
| while (oldPos < len && !attributes[oldPos].whiteSpace && !d->atWordSeparator(oldPos)) |
| oldPos++; |
| } |
| while (oldPos < len && attributes[oldPos].whiteSpace) |
| oldPos++; |
| } |
| |
| return oldPos; |
| } |
| |
| /*! |
| Returns the first valid cursor position before \a oldPos that |
| respects the given cursor \a mode. |
| Returns value of \a oldPos, if \a oldPos is not a valid cursor position. |
| |
| \sa isValidCursorPosition(), nextCursorPosition() |
| */ |
| int QTextLayout::previousCursorPosition(int oldPos, CursorMode mode) const |
| { |
| const QCharAttributes *attributes = d->attributes(); |
| int len = d->block.isValid() ? d->block.length() - 1 |
| : d->layoutData->string.length(); |
| Q_ASSERT(len <= d->layoutData->string.length()); |
| if (!attributes || oldPos <= 0 || oldPos > len) |
| return oldPos; |
| |
| if (mode == SkipCharacters) { |
| oldPos--; |
| while (oldPos && !attributes[oldPos].graphemeBoundary) |
| oldPos--; |
| } else { |
| while (oldPos > 0 && attributes[oldPos - 1].whiteSpace) |
| oldPos--; |
| |
| if (oldPos && d->atWordSeparator(oldPos-1)) { |
| oldPos--; |
| while (oldPos && d->atWordSeparator(oldPos-1)) |
| oldPos--; |
| } else { |
| while (oldPos > 0 && !attributes[oldPos - 1].whiteSpace && !d->atWordSeparator(oldPos-1)) |
| oldPos--; |
| } |
| } |
| |
| return oldPos; |
| } |
| |
| /*! |
| Returns the cursor position to the right of \a oldPos, next to it. |
| It's dependent on the visual position of characters, after bi-directional |
| reordering. |
| |
| \sa leftCursorPosition(), nextCursorPosition() |
| */ |
| int QTextLayout::rightCursorPosition(int oldPos) const |
| { |
| int newPos = d->positionAfterVisualMovement(oldPos, QTextCursor::Right); |
| // qDebug("%d -> %d", oldPos, newPos); |
| return newPos; |
| } |
| |
| /*! |
| Returns the cursor position to the left of \a oldPos, next to it. |
| It's dependent on the visual position of characters, after bi-directional |
| reordering. |
| |
| \sa rightCursorPosition(), previousCursorPosition() |
| */ |
| int QTextLayout::leftCursorPosition(int oldPos) const |
| { |
| int newPos = d->positionAfterVisualMovement(oldPos, QTextCursor::Left); |
| // qDebug("%d -> %d", oldPos, newPos); |
| return newPos; |
| } |
| |
| /*!/ |
| Returns \c true if position \a pos is a valid cursor position. |
| |
| In a Unicode context some positions in the text are not valid |
| cursor positions, because the position is inside a Unicode |
| surrogate or a grapheme cluster. |
| |
| A grapheme cluster is a sequence of two or more Unicode characters |
| that form one indivisible entity on the screen. For example the |
| latin character `\unicode{0xC4}' can be represented in Unicode by two |
| characters, `A' (0x41), and the combining diaresis (0x308). A text |
| cursor can only validly be positioned before or after these two |
| characters, never between them since that wouldn't make sense. In |
| indic languages every syllable forms a grapheme cluster. |
| */ |
| bool QTextLayout::isValidCursorPosition(int pos) const |
| { |
| const QCharAttributes *attributes = d->attributes(); |
| if (!attributes || pos < 0 || pos > (int)d->layoutData->string.length()) |
| return false; |
| return attributes[pos].graphemeBoundary; |
| } |
| |
| /*! |
| Returns a new text line to be laid out if there is text to be |
| inserted into the layout; otherwise returns an invalid text line. |
| |
| The text layout creates a new line object that starts after the |
| last line in the layout, or at the beginning if the layout is empty. |
| The layout maintains an internal cursor, and each line is filled |
| with text from the cursor position onwards when the |
| QTextLine::setLineWidth() function is called. |
| |
| Once QTextLine::setLineWidth() is called, a new line can be created and |
| filled with text. Repeating this process will lay out the whole block |
| of text contained in the QTextLayout. If there is no text left to be |
| inserted into the layout, the QTextLine returned will not be valid |
| (isValid() will return false). |
| */ |
| QTextLine QTextLayout::createLine() |
| { |
| #ifndef QT_NO_DEBUG |
| if (!d->layoutData || d->layoutData->layoutState == QTextEngine::LayoutEmpty) { |
| qWarning("QTextLayout::createLine: Called without layouting"); |
| return QTextLine(); |
| } |
| #endif |
| if (d->layoutData->layoutState == QTextEngine::LayoutFailed) |
| return QTextLine(); |
| |
| int l = d->lines.size(); |
| if (l && d->lines.at(l-1).length < 0) { |
| QTextLine(l-1, d).setNumColumns(INT_MAX); |
| } |
| int from = l > 0 ? d->lines.at(l-1).from + d->lines.at(l-1).length + d->lines.at(l-1).trailingSpaces : 0; |
| int strlen = d->layoutData->string.length(); |
| if (l && from >= strlen) { |
| if (!d->lines.at(l-1).length || d->layoutData->string.at(strlen - 1) != QChar::LineSeparator) |
| return QTextLine(); |
| } |
| |
| QScriptLine line; |
| line.from = from; |
| line.length = -1; |
| line.justified = false; |
| line.gridfitted = false; |
| |
| d->lines.append(line); |
| return QTextLine(l, d); |
| } |
| |
| /*! |
| Returns the number of lines in this text layout. |
| |
| \sa lineAt() |
| */ |
| int QTextLayout::lineCount() const |
| { |
| return d->lines.size(); |
| } |
| |
| /*! |
| Returns the \a{i}-th line of text in this text layout. |
| |
| \sa lineCount(), lineForTextPosition() |
| */ |
| QTextLine QTextLayout::lineAt(int i) const |
| { |
| return i < lineCount() ? QTextLine(i, d) : QTextLine(); |
| } |
| |
| /*! |
| Returns the line that contains the cursor position specified by \a pos. |
| |
| \sa isValidCursorPosition(), lineAt() |
| */ |
| QTextLine QTextLayout::lineForTextPosition(int pos) const |
| { |
| int lineNum = d->lineNumberForTextPosition(pos); |
| return lineNum >= 0 ? lineAt(lineNum) : QTextLine(); |
| } |
| |
| /*! |
| \since 4.2 |
| |
| The global position of the layout. This is independent of the |
| bounding rectangle and of the layout process. |
| |
| \sa setPosition() |
| */ |
| QPointF QTextLayout::position() const |
| { |
| return d->position; |
| } |
| |
| /*! |
| Moves the text layout to point \a p. |
| |
| \sa position() |
| */ |
| void QTextLayout::setPosition(const QPointF &p) |
| { |
| d->position = p; |
| } |
| |
| /*! |
| The smallest rectangle that contains all the lines in the layout. |
| */ |
| QRectF QTextLayout::boundingRect() const |
| { |
| if (d->lines.isEmpty()) |
| return QRectF(); |
| |
| QFixed xmax, ymax; |
| QFixed xmin = d->lines.at(0).x; |
| QFixed ymin = d->lines.at(0).y; |
| |
| for (int i = 0; i < d->lines.size(); ++i) { |
| const QScriptLine &si = d->lines.at(i); |
| xmin = qMin(xmin, si.x); |
| ymin = qMin(ymin, si.y); |
| QFixed lineWidth = si.width < QFIXED_MAX ? qMax(si.width, si.textWidth) : si.textWidth; |
| xmax = qMax(xmax, si.x+lineWidth); |
| // ### shouldn't the ascent be used in ymin??? |
| ymax = qMax(ymax, si.y+si.height().ceil()); |
| } |
| return QRectF(xmin.toReal(), ymin.toReal(), (xmax-xmin).toReal(), (ymax-ymin).toReal()); |
| } |
| |
| /*! |
| The minimum width the layout needs. This is the width of the |
| layout's smallest non-breakable substring. |
| |
| \warning This function only returns a valid value after the layout |
| has been done. |
| |
| \sa maximumWidth() |
| */ |
| qreal QTextLayout::minimumWidth() const |
| { |
| return d->minWidth.toReal(); |
| } |
| |
| /*! |
| The maximum width the layout could expand to; this is essentially |
| the width of the entire text. |
| |
| \warning This function only returns a valid value after the layout |
| has been done. |
| |
| \sa minimumWidth() |
| */ |
| qreal QTextLayout::maximumWidth() const |
| { |
| return d->maxWidth.toReal(); |
| } |
| |
| |
| /*! |
| \internal |
| */ |
| void QTextLayout::setFlags(int flags) |
| { |
| if (flags & Qt::TextJustificationForced) { |
| d->option.setAlignment(Qt::AlignJustify); |
| d->forceJustification = true; |
| } |
| |
| if (flags & (Qt::TextForceLeftToRight|Qt::TextForceRightToLeft)) { |
| d->ignoreBidi = true; |
| d->option.setTextDirection((flags & Qt::TextForceLeftToRight) ? Qt::LeftToRight : Qt::RightToLeft); |
| } |
| } |
| |
| static void addSelectedRegionsToPath(QTextEngine *eng, int lineNumber, const QPointF &pos, QTextLayout::FormatRange *selection, |
| QPainterPath *region, const QRectF &boundingRect) |
| { |
| const QScriptLine &line = eng->lines[lineNumber]; |
| |
| QTextLineItemIterator iterator(eng, lineNumber, pos, selection); |
| |
| |
| |
| const qreal selectionY = pos.y() + line.y.toReal(); |
| const qreal lineHeight = line.height().toReal(); |
| |
| QFixed lastSelectionX = iterator.x; |
| QFixed lastSelectionWidth; |
| |
| while (!iterator.atEnd()) { |
| iterator.next(); |
| |
| QFixed selectionX, selectionWidth; |
| if (iterator.getSelectionBounds(&selectionX, &selectionWidth)) { |
| if (selectionX == lastSelectionX + lastSelectionWidth) { |
| lastSelectionWidth += selectionWidth; |
| continue; |
| } |
| |
| if (lastSelectionWidth > 0) { |
| const QRectF rect = boundingRect & QRectF(lastSelectionX.toReal(), selectionY, lastSelectionWidth.toReal(), lineHeight); |
| region->addRect(rect.toAlignedRect()); |
| } |
| |
| lastSelectionX = selectionX; |
| lastSelectionWidth = selectionWidth; |
| } |
| } |
| if (lastSelectionWidth > 0) { |
| const QRectF rect = boundingRect & QRectF(lastSelectionX.toReal(), selectionY, lastSelectionWidth.toReal(), lineHeight); |
| region->addRect(rect.toAlignedRect()); |
| } |
| } |
| |
| static inline QRectF clipIfValid(const QRectF &rect, const QRectF &clip) |
| { |
| return clip.isValid() ? (rect & clip) : rect; |
| } |
| |
| |
| /*! |
| Returns the glyph indexes and positions for all glyphs corresponding to the \a length characters |
| starting at the position \a from in this QTextLayout. This is an expensive function, and should |
| not be called in a time sensitive context. |
| |
| If \a from is less than zero, then the glyph run will begin at the first character in the |
| layout. If \a length is less than zero, it will span the entire string from the start position. |
| |
| \since 4.8 |
| |
| \sa draw(), QPainter::drawGlyphRun() |
| */ |
| #if !defined(QT_NO_RAWFONT) |
| QList<QGlyphRun> QTextLayout::glyphRuns(int from, int length) const |
| { |
| if (from < 0) |
| from = 0; |
| if (length < 0) |
| length = text().length(); |
| |
| QHash<QPair<QFontEngine *, int>, QGlyphRun> glyphRunHash; |
| for (int i=0; i<d->lines.size(); ++i) { |
| if (d->lines.at(i).from > from + length) |
| break; |
| else if (d->lines.at(i).from + d->lines[i].length >= from) { |
| QList<QGlyphRun> glyphRuns = QTextLine(i, d).glyphRuns(from, length); |
| |
| for (int j = 0; j < glyphRuns.size(); j++) { |
| const QGlyphRun &glyphRun = glyphRuns.at(j); |
| QRawFont rawFont = glyphRun.rawFont(); |
| |
| QFontEngine *fontEngine = rawFont.d->fontEngine; |
| QGlyphRun::GlyphRunFlags flags = glyphRun.flags(); |
| QPair<QFontEngine *, int> key(fontEngine, int(flags)); |
| // merge the glyph runs using the same font |
| QGlyphRun &oldGlyphRun = glyphRunHash[key]; |
| if (oldGlyphRun.isEmpty()) { |
| oldGlyphRun = glyphRun; |
| } else { |
| QVector<quint32> indexes = oldGlyphRun.glyphIndexes(); |
| QVector<QPointF> positions = oldGlyphRun.positions(); |
| QRectF boundingRect = oldGlyphRun.boundingRect(); |
| |
| indexes += glyphRun.glyphIndexes(); |
| positions += glyphRun.positions(); |
| boundingRect = boundingRect.united(glyphRun.boundingRect()); |
| |
| oldGlyphRun.setGlyphIndexes(indexes); |
| oldGlyphRun.setPositions(positions); |
| oldGlyphRun.setBoundingRect(boundingRect); |
| } |
| } |
| } |
| } |
| |
| return glyphRunHash.values(); |
| } |
| #endif // QT_NO_RAWFONT |
| |
| /*! |
| Draws the whole layout on the painter \a p at the position specified by \a pos. |
| The rendered layout includes the given \a selections and is clipped within |
| the rectangle specified by \a clip. |
| */ |
| void QTextLayout::draw(QPainter *p, const QPointF &pos, const QVector<FormatRange> &selections, const QRectF &clip) const |
| { |
| if (d->lines.isEmpty()) |
| return; |
| |
| if (!d->layoutData) |
| d->itemize(); |
| |
| QPointF position = pos + d->position; |
| |
| QFixed clipy = (INT_MIN/256); |
| QFixed clipe = (INT_MAX/256); |
| if (clip.isValid()) { |
| clipy = QFixed::fromReal(clip.y() - position.y()); |
| clipe = clipy + QFixed::fromReal(clip.height()); |
| } |
| |
| int firstLine = 0; |
| int lastLine = d->lines.size(); |
| for (int i = 0; i < d->lines.size(); ++i) { |
| QTextLine l(i, d); |
| const QScriptLine &sl = d->lines.at(i); |
| |
| if (sl.y > clipe) { |
| lastLine = i; |
| break; |
| } |
| if ((sl.y + sl.height()) < clipy) { |
| firstLine = i; |
| continue; |
| } |
| } |
| |
| QPainterPath excludedRegion; |
| QPainterPath textDoneRegion; |
| for (int i = 0; i < selections.size(); ++i) { |
| FormatRange selection = selections.at(i); |
| QPainterPath region; |
| region.setFillRule(Qt::WindingFill); |
| |
| for (int line = firstLine; line < lastLine; ++line) { |
| const QScriptLine &sl = d->lines.at(line); |
| QTextLine tl(line, d); |
| |
| QRectF lineRect(tl.naturalTextRect()); |
| lineRect.translate(position); |
| lineRect.adjust(0, 0, d->leadingSpaceWidth(sl).toReal(), 0); |
| |
| bool isLastLineInBlock = (line == d->lines.size()-1); |
| int sl_length = sl.length + (isLastLineInBlock? 1 : 0); // the infamous newline |
| |
| |
| if (sl.from > selection.start + selection.length || sl.from + sl_length <= selection.start) |
| continue; // no actual intersection |
| |
| const bool selectionStartInLine = sl.from <= selection.start; |
| const bool selectionEndInLine = selection.start + selection.length < sl.from + sl_length; |
| |
| if (sl.length && (selectionStartInLine || selectionEndInLine)) { |
| addSelectedRegionsToPath(d, line, position, &selection, ®ion, clipIfValid(lineRect, clip)); |
| } else { |
| region.addRect(clipIfValid(lineRect, clip)); |
| } |
| |
| if (selection.format.boolProperty(QTextFormat::FullWidthSelection)) { |
| QRectF fullLineRect(tl.rect()); |
| fullLineRect.translate(position); |
| fullLineRect.setRight(QFIXED_MAX); |
| if (!selectionEndInLine) |
| region.addRect(clipIfValid(QRectF(lineRect.topRight(), fullLineRect.bottomRight()), clip)); |
| if (!selectionStartInLine) |
| region.addRect(clipIfValid(QRectF(fullLineRect.topLeft(), lineRect.bottomLeft()), clip)); |
| } else if (!selectionEndInLine |
| && isLastLineInBlock |
| &&!(d->option.flags() & QTextOption::ShowLineAndParagraphSeparators)) { |
| region.addRect(clipIfValid(QRectF(lineRect.right(), lineRect.top(), |
| lineRect.height()/4, lineRect.height()), clip)); |
| } |
| |
| } |
| { |
| const QPen oldPen = p->pen(); |
| const QBrush oldBrush = p->brush(); |
| |
| p->setPen(selection.format.penProperty(QTextFormat::OutlinePen)); |
| p->setBrush(selection.format.brushProperty(QTextFormat::BackgroundBrush)); |
| p->drawPath(region); |
| |
| p->setPen(oldPen); |
| p->setBrush(oldBrush); |
| } |
| |
| |
| |
| bool hasText = (selection.format.foreground().style() != Qt::NoBrush); |
| bool hasBackground= (selection.format.background().style() != Qt::NoBrush); |
| |
| if (hasBackground) { |
| selection.format.setProperty(ObjectSelectionBrush, selection.format.property(QTextFormat::BackgroundBrush)); |
| // don't just clear the property, set an empty brush that overrides a potential |
| // background brush specified in the text |
| selection.format.setProperty(QTextFormat::BackgroundBrush, QBrush()); |
| selection.format.clearProperty(QTextFormat::OutlinePen); |
| } |
| |
| selection.format.setProperty(SuppressText, !hasText); |
| |
| if (hasText && !hasBackground && !(textDoneRegion & region).isEmpty()) |
| continue; |
| |
| p->save(); |
| p->setClipPath(region, Qt::IntersectClip); |
| |
| for (int line = firstLine; line < lastLine; ++line) { |
| QTextLine l(line, d); |
| l.draw(p, position, &selection); |
| } |
| p->restore(); |
| |
| if (hasText) { |
| textDoneRegion += region; |
| } else { |
| if (hasBackground) |
| textDoneRegion -= region; |
| } |
| |
| excludedRegion += region; |
| } |
| |
| QPainterPath needsTextButNoBackground = excludedRegion - textDoneRegion; |
| if (!needsTextButNoBackground.isEmpty()){ |
| p->save(); |
| p->setClipPath(needsTextButNoBackground, Qt::IntersectClip); |
| FormatRange selection; |
| selection.start = 0; |
| selection.length = INT_MAX; |
| selection.format.setProperty(SuppressBackground, true); |
| for (int line = firstLine; line < lastLine; ++line) { |
| QTextLine l(line, d); |
| l.draw(p, position, &selection); |
| } |
| p->restore(); |
| } |
| |
| if (!excludedRegion.isEmpty()) { |
| p->save(); |
| QPainterPath path; |
| QRectF br = boundingRect().translated(position); |
| br.setRight(QFIXED_MAX); |
| if (!clip.isNull()) |
| br = br.intersected(clip); |
| path.addRect(br); |
| path -= excludedRegion; |
| p->setClipPath(path, Qt::IntersectClip); |
| } |
| |
| for (int i = firstLine; i < lastLine; ++i) { |
| QTextLine l(i, d); |
| l.draw(p, position); |
| } |
| if (!excludedRegion.isEmpty()) |
| p->restore(); |
| |
| |
| if (!d->cacheGlyphs) |
| d->freeMemory(); |
| } |
| |
| /*! |
| \fn void QTextLayout::drawCursor(QPainter *painter, const QPointF &position, int cursorPosition) const |
| \overload |
| |
| Draws a text cursor with the current pen at the given \a position using the |
| \a painter specified. |
| The corresponding position within the text is specified by \a cursorPosition. |
| */ |
| void QTextLayout::drawCursor(QPainter *p, const QPointF &pos, int cursorPosition) const |
| { |
| drawCursor(p, pos, cursorPosition, 1); |
| } |
| |
| /*! |
| \fn void QTextLayout::drawCursor(QPainter *painter, const QPointF &position, int cursorPosition, int width) const |
| |
| Draws a text cursor with the current pen and the specified \a width at the given \a position using the |
| \a painter specified. |
| The corresponding position within the text is specified by \a cursorPosition. |
| */ |
| void QTextLayout::drawCursor(QPainter *p, const QPointF &pos, int cursorPosition, int width) const |
| { |
| if (d->lines.isEmpty()) |
| return; |
| |
| if (!d->layoutData) |
| d->itemize(); |
| |
| QPointF position = pos + d->position; |
| |
| cursorPosition = qBound(0, cursorPosition, d->layoutData->string.length()); |
| int line = d->lineNumberForTextPosition(cursorPosition); |
| if (line < 0) |
| line = 0; |
| if (line >= d->lines.size()) |
| return; |
| |
| QTextLine l(line, d); |
| const QScriptLine &sl = d->lines.at(line); |
| |
| qreal x = position.x() + l.cursorToX(cursorPosition); |
| |
| int itm; |
| |
| if (d->visualCursorMovement()) { |
| if (cursorPosition == sl.from + sl.length) |
| cursorPosition--; |
| itm = d->findItem(cursorPosition); |
| } else |
| itm = d->findItem(cursorPosition - 1); |
| |
| QFixed base = sl.base(); |
| QFixed descent = sl.descent; |
| bool rightToLeft = d->isRightToLeft(); |
| if (itm >= 0) { |
| const QScriptItem &si = d->layoutData->items.at(itm); |
| if (si.ascent > 0) |
| base = si.ascent; |
| if (si.descent > 0) |
| descent = si.descent; |
| rightToLeft = si.analysis.bidiLevel % 2; |
| } |
| qreal y = position.y() + (sl.y + sl.base() - base).toReal(); |
| bool toggleAntialiasing = !(p->renderHints() & QPainter::Antialiasing) |
| && (p->transform().type() > QTransform::TxTranslate); |
| if (toggleAntialiasing) |
| p->setRenderHint(QPainter::Antialiasing); |
| QPainter::CompositionMode origCompositionMode = p->compositionMode(); |
| if (p->paintEngine()->hasFeature(QPaintEngine::RasterOpModes)) |
| p->setCompositionMode(QPainter::RasterOp_NotDestination); |
| p->fillRect(QRectF(x, y, qreal(width), (base + descent).toReal()), p->pen().brush()); |
| p->setCompositionMode(origCompositionMode); |
| if (toggleAntialiasing) |
| p->setRenderHint(QPainter::Antialiasing, false); |
| if (d->layoutData->hasBidi) { |
| const int arrow_extent = 4; |
| int sign = rightToLeft ? -1 : 1; |
| p->drawLine(QLineF(x, y, x + (sign * arrow_extent/2), y + arrow_extent/2)); |
| p->drawLine(QLineF(x, y+arrow_extent, x + (sign * arrow_extent/2), y + arrow_extent/2)); |
| } |
| return; |
| } |
| |
| /*! |
| \class QTextLine |
| \reentrant |
| |
| \brief The QTextLine class represents a line of text inside a QTextLayout. |
| \inmodule QtGui |
| |
| \ingroup richtext-processing |
| |
| A text line is usually created by QTextLayout::createLine(). |
| |
| After being created, the line can be filled using the setLineWidth() |
| or setNumColumns() functions. A line has a number of attributes including the |
| rectangle it occupies, rect(), its coordinates, x() and y(), its |
| textLength(), width() and naturalTextWidth(), and its ascent() and descent() |
| relative to the text. The position of the cursor in terms of the |
| line is available from cursorToX() and its inverse from |
| xToCursor(). A line can be moved with setPosition(). |
| */ |
| |
| /*! |
| \enum QTextLine::Edge |
| |
| \value Leading |
| \value Trailing |
| */ |
| |
| /*! |
| \enum QTextLine::CursorPosition |
| |
| \value CursorBetweenCharacters |
| \value CursorOnCharacter |
| */ |
| |
| /*! |
| \fn QTextLine::QTextLine(int line, QTextEngine *e) |
| \internal |
| |
| Constructs a new text line using the line at position \a line in |
| the text engine \a e. |
| */ |
| |
| /*! |
| \fn QTextLine::QTextLine() |
| |
| Creates an invalid line. |
| */ |
| |
| /*! |
| \fn bool QTextLine::isValid() const |
| |
| Returns \c true if this text line is valid; otherwise returns \c false. |
| */ |
| |
| /*! |
| \fn int QTextLine::lineNumber() const |
| |
| Returns the position of the line in the text engine. |
| */ |
| |
| |
| /*! |
| Returns the line's bounding rectangle. |
| |
| \sa x(), y(), textLength(), width() |
| */ |
| QRectF QTextLine::rect() const |
| { |
| const QScriptLine& sl = eng->lines.at(index); |
| return QRectF(sl.x.toReal(), sl.y.toReal(), sl.width.toReal(), sl.height().toReal()); |
| } |
| |
| /*! |
| Returns the rectangle covered by the line. |
| */ |
| QRectF QTextLine::naturalTextRect() const |
| { |
| const QScriptLine& sl = eng->lines.at(index); |
| QFixed x = sl.x + eng->alignLine(sl); |
| |
| QFixed width = sl.textWidth; |
| if (sl.justified) |
| width = sl.width; |
| |
| return QRectF(x.toReal(), sl.y.toReal(), width.toReal(), sl.height().toReal()); |
| } |
| |
| /*! |
| Returns the line's x position. |
| |
| \sa rect(), y(), textLength(), width() |
| */ |
| qreal QTextLine::x() const |
| { |
| return eng->lines.at(index).x.toReal(); |
| } |
| |
| /*! |
| Returns the line's y position. |
| |
| \sa x(), rect(), textLength(), width() |
| */ |
| qreal QTextLine::y() const |
| { |
| return eng->lines.at(index).y.toReal(); |
| } |
| |
| /*! |
| Returns the line's width as specified by the layout() function. |
| |
| \sa naturalTextWidth(), x(), y(), textLength(), rect() |
| */ |
| qreal QTextLine::width() const |
| { |
| return eng->lines.at(index).width.toReal(); |
| } |
| |
| |
| /*! |
| Returns the line's ascent. |
| |
| \sa descent(), height() |
| */ |
| qreal QTextLine::ascent() const |
| { |
| return eng->lines.at(index).ascent.toReal(); |
| } |
| |
| /*! |
| Returns the line's descent. |
| |
| \sa ascent(), height() |
| */ |
| qreal QTextLine::descent() const |
| { |
| return eng->lines.at(index).descent.toReal(); |
| } |
| |
| /*! |
| Returns the line's height. This is equal to ascent() + descent() |
| if leading is not included. If leading is included, this equals to |
| ascent() + descent() + leading(). |
| |
| \sa ascent(), descent(), leading(), setLeadingIncluded() |
| */ |
| qreal QTextLine::height() const |
| { |
| return eng->lines.at(index).height().ceil().toReal(); |
| } |
| |
| /*! |
| \since 4.6 |
| |
| Returns the line's leading. |
| |
| \sa ascent(), descent(), height() |
| */ |
| qreal QTextLine::leading() const |
| { |
| return eng->lines.at(index).leading.toReal(); |
| } |
| |
| /*! |
| \since 4.6 |
| |
| Includes positive leading into the line's height if \a included is true; |
| otherwise does not include leading. |
| |
| By default, leading is not included. |
| |
| Note that negative leading is ignored, it must be handled |
| in the code using the text lines by letting the lines overlap. |
| |
| \sa leadingIncluded() |
| |
| */ |
| void QTextLine::setLeadingIncluded(bool included) |
| { |
| eng->lines[index].leadingIncluded= included; |
| |
| } |
| |
| /*! |
| \since 4.6 |
| |
| Returns \c true if positive leading is included into the line's height; |
| otherwise returns \c false. |
| |
| By default, leading is not included. |
| |
| \sa setLeadingIncluded() |
| */ |
| bool QTextLine::leadingIncluded() const |
| { |
| return eng->lines.at(index).leadingIncluded; |
| } |
| |
| /*! |
| Returns the width of the line that is occupied by text. This is |
| always \<= to width(), and is the minimum width that could be used |
| by layout() without changing the line break position. |
| */ |
| qreal QTextLine::naturalTextWidth() const |
| { |
| return eng->lines.at(index).textWidth.toReal(); |
| } |
| |
| /*! |
| \since 4.7 |
| Returns the horizontal advance of the text. The advance of the text |
| is the distance from its position to the next position at which |
| text would naturally be drawn. |
| |
| By adding the advance to the position of the text line and using this |
| as the position of a second text line, you will be able to position |
| the two lines side-by-side without gaps in-between. |
| */ |
| qreal QTextLine::horizontalAdvance() const |
| { |
| return eng->lines.at(index).textAdvance.toReal(); |
| } |
| |
| /*! |
| Lays out the line with the given \a width. The line is filled from |
| its starting position with as many characters as will fit into |
| the line. In case the text cannot be split at the end of the line, |
| it will be filled with additional characters to the next whitespace |
| or end of the text. |
| */ |
| void QTextLine::setLineWidth(qreal width) |
| { |
| QScriptLine &line = eng->lines[index]; |
| if (!eng->layoutData) { |
| qWarning("QTextLine: Can't set a line width while not layouting."); |
| return; |
| } |
| |
| if (width > QFIXED_MAX) |
| width = QFIXED_MAX; |
| |
| line.width = QFixed::fromReal(width); |
| if (line.length |
| && line.textWidth <= line.width |
| && line.from + line.length == eng->layoutData->string.length()) |
| // no need to do anything if the line is already layouted and the last one. This optimization helps |
| // when using things in a single line layout. |
| return; |
| line.length = 0; |
| line.textWidth = 0; |
| |
| layout_helper(INT_MAX); |
| } |
| |
| /*! |
| Lays out the line. The line is filled from its starting position |
| with as many characters as are specified by \a numColumns. In case |
| the text cannot be split until \a numColumns characters, the line |
| will be filled with as many characters to the next whitespace or |
| end of the text. |
| */ |
| void QTextLine::setNumColumns(int numColumns) |
| { |
| QScriptLine &line = eng->lines[index]; |
| line.width = QFIXED_MAX; |
| line.length = 0; |
| line.textWidth = 0; |
| layout_helper(numColumns); |
| } |
| |
| /*! |
| Lays out the line. The line is filled from its starting position |
| with as many characters as are specified by \a numColumns. In case |
| the text cannot be split until \a numColumns characters, the line |
| will be filled with as many characters to the next whitespace or |
| end of the text. The provided \a alignmentWidth is used as reference |
| width for alignment. |
| */ |
| void QTextLine::setNumColumns(int numColumns, qreal alignmentWidth) |
| { |
| QScriptLine &line = eng->lines[index]; |
| line.width = QFixed::fromReal(alignmentWidth); |
| line.length = 0; |
| line.textWidth = 0; |
| layout_helper(numColumns); |
| } |
| |
| #if 0 |
| #define LB_DEBUG qDebug |
| #else |
| #define LB_DEBUG if (0) qDebug |
| #endif |
| |
| namespace { |
| |
| struct LineBreakHelper |
| { |
| LineBreakHelper() |
| : glyphCount(0), maxGlyphs(0), currentPosition(0), fontEngine(0), logClusters(0), |
| manualWrap(false), whiteSpaceOrObject(true) |
| { |
| } |
| |
| |
| QScriptLine tmpData; |
| QScriptLine spaceData; |
| |
| QGlyphLayout glyphs; |
| |
| int glyphCount; |
| int maxGlyphs; |
| int currentPosition; |
| glyph_t previousGlyph; |
| QFontEngine *previousGlyphFontEngine; |
| |
| QFixed minw; |
| QFixed softHyphenWidth; |
| QFixed rightBearing; |
| QFixed minimumRightBearing; |
| |
| QFontEngine *fontEngine; |
| const unsigned short *logClusters; |
| |
| bool manualWrap; |
| bool whiteSpaceOrObject; |
| |
| bool checkFullOtherwiseExtend(QScriptLine &line); |
| |
| QFixed calculateNewWidth(const QScriptLine &line) const { |
| return line.textWidth + tmpData.textWidth + spaceData.textWidth |
| + softHyphenWidth + negativeRightBearing(); |
| } |
| |
| 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 |
| previousGlyphFontEngine = fontEngine; |
| } |
| } |
| |
| 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) |
| return; |
| calculateRightBearing(fontEngine, currentGlyph()); |
| } |
| |
| inline void calculateRightBearingForPreviousGlyph() |
| { |
| if (previousGlyph > 0) |
| calculateRightBearing(previousGlyphFontEngine, previousGlyph); |
| } |
| |
| 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); |
| |
| inline bool LineBreakHelper::checkFullOtherwiseExtend(QScriptLine &line) |
| { |
| LB_DEBUG("possible break width %f, spacew=%f", tmpData.textWidth.toReal(), spaceData.textWidth.toReal()); |
| |
| QFixed newWidth = calculateNewWidth(line); |
| if (line.length && !manualWrap && (newWidth > line.width || glyphCount > maxGlyphs)) |
| return true; |
| |
| minw = qMax(minw, tmpData.textWidth); |
| line += tmpData; |
| line.textWidth += spaceData.textWidth; |
| |
| line.length += spaceData.length; |
| tmpData.textWidth = 0; |
| tmpData.length = 0; |
| spaceData.textWidth = 0; |
| spaceData.length = 0; |
| |
| return false; |
| } |
| |
| } // anonymous namespace |
| |
| |
| static inline void addNextCluster(int &pos, int end, QScriptLine &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; |
| } |
| |
| |
| // fill QScriptLine |
| void QTextLine::layout_helper(int maxGlyphs) |
| { |
| QScriptLine &line = eng->lines[index]; |
| line.length = 0; |
| line.trailingSpaces = 0; |
| line.textWidth = 0; |
| line.hasTrailingSpaces = false; |
| |
| if (!eng->layoutData->items.size() || line.from >= eng->layoutData->string.length()) { |
| line.setDefaultHeight(eng); |
| return; |
| } |
| |
| Q_ASSERT(line.from < eng->layoutData->string.length()); |
| |
| LineBreakHelper lbh; |
| |
| lbh.maxGlyphs = maxGlyphs; |
| |
| QTextOption::WrapMode wrapMode = eng->option.wrapMode(); |
| bool breakany = (wrapMode == QTextOption::WrapAnywhere); |
| lbh.manualWrap = (wrapMode == QTextOption::ManualWrap || wrapMode == QTextOption::NoWrap); |
| |
| int item = -1; |
| int newItem = eng->findItem(line.from); |
| Q_ASSERT(newItem >= 0); |
| |
| LB_DEBUG("from: %d: item=%d, total %d, width available %f", line.from, newItem, eng->layoutData->items.size(), line.width.toReal()); |
| |
| Qt::Alignment alignment = eng->option.alignment(); |
| |
| const QCharAttributes *attributes = eng->attributes(); |
| if (!attributes) |
| return; |
| lbh.currentPosition = line.from; |
| int end = 0; |
| lbh.logClusters = eng->layoutData->logClustersPtr; |
| lbh.previousGlyph = 0; |
| |
| bool hasInlineObject = false; |
| QFixed maxInlineObjectHeight = 0; |
| |
| while (newItem < eng->layoutData->items.size()) { |
| lbh.resetRightBearing(); |
| lbh.softHyphenWidth = 0; |
| if (newItem != item) { |
| item = newItem; |
| const QScriptItem ¤t = eng->layoutData->items.at(item); |
| if (!current.num_glyphs) { |
| eng->shape(item); |
| attributes = eng->attributes(); |
| if (!attributes) |
| return; |
| lbh.logClusters = eng->layoutData->logClustersPtr; |
| } |
| lbh.currentPosition = qMax(line.from, 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; |
| lbh.minimumRightBearing = qMin(QFixed(), |
| QFixed::fromReal(fontEngine->minRightBearing())); |
| } |
| } |
| const QScriptItem ¤t = eng->layoutData->items.at(item); |
| |
| lbh.tmpData.leading = qMax(lbh.tmpData.leading + lbh.tmpData.ascent, |
| current.leading + current.ascent) - qMax(lbh.tmpData.ascent, |
| current.ascent); |
| if (current.analysis.flags != QScriptAnalysis::Object) { |
| // objects need some special treatment as they can special alignment or be floating |
| lbh.tmpData.ascent = qMax(lbh.tmpData.ascent, current.ascent); |
| lbh.tmpData.descent = qMax(lbh.tmpData.descent, current.descent); |
| } |
| |
| if (current.analysis.flags == QScriptAnalysis::Tab && (alignment & (Qt::AlignLeft | Qt::AlignRight | Qt::AlignCenter | Qt::AlignJustify))) { |
| lbh.whiteSpaceOrObject = true; |
| if (lbh.checkFullOtherwiseExtend(line)) |
| goto found; |
| |
| QFixed x = line.x + line.textWidth + lbh.tmpData.textWidth + lbh.spaceData.textWidth; |
| QFixed tabWidth = eng->calculateTabWidth(item, x); |
| attributes = eng->attributes(); |
| if (!attributes) |
| return; |
| lbh.logClusters = eng->layoutData->logClustersPtr; |
| lbh.glyphs = eng->shapedGlyphs(¤t); |
| |
| lbh.spaceData.textWidth += tabWidth; |
| lbh.spaceData.length++; |
| newItem = item + 1; |
| |
| QFixed averageCharWidth = eng->fontEngine(current)->averageCharWidth(); |
| lbh.glyphCount += qRound(tabWidth / averageCharWidth); |
| |
| if (lbh.checkFullOtherwiseExtend(line)) |
| goto found; |
| } else if (current.analysis.flags == QScriptAnalysis::LineOrParagraphSeparator) { |
| lbh.whiteSpaceOrObject = true; |
| // if the line consists only of the line separator make sure |
| // we have a sane height |
| if (!line.length && !lbh.tmpData.length) |
| line.setDefaultHeight(eng); |
| if (eng->option.flags() & QTextOption::ShowLineAndParagraphSeparators) { |
| if (lbh.checkFullOtherwiseExtend(line)) |
| goto found; |
| |
| addNextCluster(lbh.currentPosition, end, lbh.tmpData, lbh.glyphCount, |
| current, lbh.logClusters, lbh.glyphs); |
| } else { |
| lbh.tmpData.length++; |
| lbh.calculateRightBearingForPreviousGlyph(); |
| } |
| line += lbh.tmpData; |
| goto found; |
| } else if (current.analysis.flags == QScriptAnalysis::Object) { |
| lbh.whiteSpaceOrObject = true; |
| lbh.tmpData.length++; |
| |
| if (eng->block.docHandle()) { |
| QTextInlineObject inlineObject(item, eng); |
| QTextFormat f = inlineObject.format(); |
| eng->docLayout()->positionInlineObject(inlineObject, eng->block.position() + current.position, f); |
| QTextCharFormat::VerticalAlignment valign = f.toCharFormat().verticalAlignment(); |
| if (valign != QTextCharFormat::AlignTop && valign != QTextCharFormat::AlignBottom) { |
| lbh.tmpData.ascent = qMax(lbh.tmpData.ascent, current.ascent); |
| lbh.tmpData.descent = qMax(lbh.tmpData.descent, current.descent); |
| } |
| } |
| |
| hasInlineObject = true; |
| maxInlineObjectHeight = qMax(maxInlineObjectHeight, current.ascent + current.descent); |
| |
| lbh.tmpData.textWidth += current.width; |
| |
| newItem = item + 1; |
| ++lbh.glyphCount; |
| if (lbh.checkFullOtherwiseExtend(line)) |
| goto found; |
| } else if (attributes[lbh.currentPosition].whiteSpace |
| && eng->layoutData->string.at(lbh.currentPosition).decompositionTag() != QChar::NoBreak) { |
| lbh.whiteSpaceOrObject = true; |
| while (lbh.currentPosition < end |
| && attributes[lbh.currentPosition].whiteSpace |
| && eng->layoutData->string.at(lbh.currentPosition).decompositionTag() != QChar::NoBreak) { |
| addNextCluster(lbh.currentPosition, end, lbh.spaceData, lbh.glyphCount, |
| current, lbh.logClusters, lbh.glyphs); |
| } |
| |
| if (!lbh.manualWrap && lbh.spaceData.textWidth > line.width) { |
| lbh.spaceData.textWidth = line.width; // ignore spaces that fall out of the line. |
| goto found; |
| } |
| } else { |
| lbh.whiteSpaceOrObject = false; |
| bool sb_or_ws = false; |
| lbh.saveCurrentGlyph(); |
| do { |
| addNextCluster(lbh.currentPosition, end, lbh.tmpData, lbh.glyphCount, |
| current, lbh.logClusters, lbh.glyphs); |
| |
| // This is a hack to fix a regression caused by the introduction of the |
| // whitespace flag to non-breakable spaces and will cause the non-breakable |
| // spaces to behave as in previous Qt versions in the line breaking algorithm. |
| // The line breaks do not currently follow the Unicode specs, but fixing this would |
| // require refactoring the code and would cause behavioral regressions. |
| bool isBreakableSpace = lbh.currentPosition < eng->layoutData->string.length() |
| && attributes[lbh.currentPosition].whiteSpace |
| && eng->layoutData->string.at(lbh.currentPosition).decompositionTag() != QChar::NoBreak; |
| |
| if (lbh.currentPosition >= eng->layoutData->string.length() |
| || isBreakableSpace |
| || attributes[lbh.currentPosition].lineBreak) { |
| sb_or_ws = true; |
| break; |
| } else if (breakany && attributes[lbh.currentPosition].graphemeBoundary) { |
| break; |
| } |
| } while (lbh.currentPosition < end); |
| lbh.minw = qMax(lbh.tmpData.textWidth, lbh.minw); |
| |
| if (lbh.currentPosition > 0 && lbh.currentPosition < end |
| && attributes[lbh.currentPosition].lineBreak |
| && eng->layoutData->string.at(lbh.currentPosition - 1).unicode() == QChar::SoftHyphen) { |
| // if we are splitting up a word because of |
| // a soft hyphen then we ... |
| // |
| // a) have to take the width of the soft hyphen into |
| // account to see if the first syllable(s) /and/ |
| // the soft hyphen fit into the line |
| // |
| // b) if we are so short of available width that the |
| // soft hyphen is the first breakable position, then |
| // we don't want to show it. However we initially |
| // have to take the width for it into account so that |
| // the text document layout sees the overflow and |
| // switch to break-anywhere mode, in which we |
| // want the soft-hyphen to slip into the next line |
| // and thus become invisible again. |
| // |
| if (line.length) |
| lbh.softHyphenWidth = lbh.glyphs.advances[lbh.logClusters[lbh.currentPosition - 1]]; |
| else if (breakany) |
| lbh.tmpData.textWidth += lbh.glyphs.advances[lbh.logClusters[lbh.currentPosition - 1]]; |
| } |
| |
| if (sb_or_ws|breakany) { |
| // To compute the final width of the text we need to take negative right bearing |
| // into account (negative right bearing means the glyph has pixel data past the |
| // advance length). Note that the negative right bearing is an absolute number, |
| // so that we can apply it to the width using straight forward addition. |
| |
| // Store previous right bearing (for the already accepted glyph) in case we |
| // end up breaking due to the current glyph being too wide. |
| QFixed previousRightBearing = lbh.rightBearing; |
| |
| // We skip calculating the right bearing if the minimum negative bearing is too |
| // small to possibly expand the text beyond the edge. Note that this optimization |
| // will in some cases fail, as the minimum right bearing reported by the font |
| // engine may not cover all the glyphs in the font. The result is that we think |
| // we don't need to break at the current glyph (because the right bearing is 0), |
| // and when we then end up breaking on the next glyph we compute the right bearing |
| // and end up with a line width that is slightly larger width than what was requested. |
| // Unfortunately we can't remove this optimization as it will slow down text |
| // layouting significantly, so we accept the slight correctnes issue. |
| if ((lbh.calculateNewWidth(line) + qAbs(lbh.minimumRightBearing)) > line.width) |
| lbh.calculateRightBearing(); |
| |
| if (lbh.checkFullOtherwiseExtend(line)) { |
| // We are too wide to accept the next glyph with its bearing, so we restore the |
| // right bearing to that of the previous glyph (the one that was already accepted), |
| // so that the bearing can be be applied to the final width of the text below. |
| if (previousRightBearing != LineBreakHelper::RightBearingNotCalculated) |
| lbh.rightBearing = previousRightBearing; |
| else |
| lbh.calculateRightBearingForPreviousGlyph(); |
| |
| if (!breakany) { |
| line.textWidth += lbh.softHyphenWidth; |
| } |
| |
| goto found; |
| } |
| } |
| lbh.saveCurrentGlyph(); |
| } |
| if (lbh.currentPosition == end) |
| newItem = item + 1; |
| } |
| LB_DEBUG("reached end of line"); |
| lbh.checkFullOtherwiseExtend(line); |
| found: |
| line.textAdvance = line.textWidth; |
| |
| // If right bearing has not been calculated yet, do that now |
| if (lbh.rightBearing == LineBreakHelper::RightBearingNotCalculated && !lbh.whiteSpaceOrObject) |
| lbh.calculateRightBearing(); |
| |
| // Then apply any negative right bearing |
| line.textWidth += lbh.negativeRightBearing(); |
| |
| if (line.length == 0) { |
| LB_DEBUG("no break available in line, adding temp: length %d, width %f, space: length %d, width %f", |
| lbh.tmpData.length, lbh.tmpData.textWidth.toReal(), |
| lbh.spaceData.length, lbh.spaceData.textWidth.toReal()); |
| line += lbh.tmpData; |
| } |
| |
| if (hasInlineObject && eng->block.docHandle()) { |
| // position top/bottom aligned inline objects |
| if (maxInlineObjectHeight > line.ascent + line.descent) { |
| // extend line height if required |
| QFixed toAdd = (maxInlineObjectHeight - line.ascent - line.descent)/2; |
| line.ascent += toAdd; |
| line.descent = maxInlineObjectHeight - line.ascent; |
| } |
| int startItem = eng->findItem(line.from); |
| int endItem = eng->findItem(line.from + line.length); |
| if (endItem < 0) |
| endItem = eng->layoutData->items.size(); |
| for (int item = startItem; item < endItem; ++item) { |
| QScriptItem ¤t = eng->layoutData->items[item]; |
| if (current.analysis.flags == QScriptAnalysis::Object) { |
| QTextInlineObject inlineObject(item, eng); |
| QTextCharFormat::VerticalAlignment align = inlineObject.format().toCharFormat().verticalAlignment(); |
| QFixed height = current.ascent + current.descent; |
| switch (align) { |
| case QTextCharFormat::AlignTop: |
| current.ascent = line.ascent; |
| current.descent = height - line.ascent; |
| break; |
| case QTextCharFormat::AlignBottom: |
| current.descent = line.descent; |
| current.ascent = height - line.descent; |
| break; |
| default: |
| break; |
| } |
| Q_ASSERT(line.ascent >= current.ascent); |
| Q_ASSERT(line.descent >= current.descent); |
| } |
| } |
| } |
| |
| |
| LB_DEBUG("line length = %d, ascent=%f, descent=%f, textWidth=%f (spacew=%f)", line.length, line.ascent.toReal(), |
| line.descent.toReal(), line.textWidth.toReal(), lbh.spaceData.width.toReal()); |
| LB_DEBUG(" : '%s'", eng->layoutData->string.mid(line.from, line.length).toUtf8().data()); |
| |
| if (lbh.manualWrap) { |
| eng->minWidth = qMax(eng->minWidth, line.textWidth); |
| eng->maxWidth = qMax(eng->maxWidth, line.textWidth); |
| } else { |
| eng->minWidth = qMax(eng->minWidth, lbh.minw); |
| eng->maxWidth += line.textWidth; |
| } |
| |
| if (line.textWidth > 0 && item < eng->layoutData->items.size()) |
| eng->maxWidth += lbh.spaceData.textWidth; |
| if (eng->option.flags() & QTextOption::IncludeTrailingSpaces) |
| line.textWidth += lbh.spaceData.textWidth; |
| if (lbh.spaceData.length) { |
| line.trailingSpaces = lbh.spaceData.length; |
| line.hasTrailingSpaces = true; |
| } |
| |
| line.justified = false; |
| line.gridfitted = false; |
| |
| if (eng->option.wrapMode() == QTextOption::WrapAtWordBoundaryOrAnywhere) { |
| if ((lbh.maxGlyphs != INT_MAX && lbh.glyphCount > lbh.maxGlyphs) |
| || (lbh.maxGlyphs == INT_MAX && line.textWidth > line.width)) { |
| |
| eng->option.setWrapMode(QTextOption::WrapAnywhere); |
| line.length = 0; |
| line.textWidth = 0; |
| layout_helper(lbh.maxGlyphs); |
| eng->option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); |
| } |
| } |
| } |
| |
| /*! |
| Moves the line to position \a pos. |
| */ |
| void QTextLine::setPosition(const QPointF &pos) |
| { |
| eng->lines[index].x = QFixed::fromReal(pos.x()); |
| eng->lines[index].y = QFixed::fromReal(pos.y()); |
| } |
| |
| /*! |
| Returns the line's position relative to the text layout's position. |
| */ |
| QPointF QTextLine::position() const |
| { |
| return QPointF(eng->lines.at(index).x.toReal(), eng->lines.at(index).y.toReal()); |
| } |
| |
| // ### DOC: I have no idea what this means/does. |
| // You create a text layout with a string of text. Once you laid |
| // it out, it contains a number of QTextLines. from() returns the position |
| // inside the text string where this line starts. If you e.g. has a |
| // text of "This is a string", laid out into two lines (the second |
| // starting at the word 'a'), layout.lineAt(0).from() == 0 and |
| // layout.lineAt(1).from() == 8. |
| /*! |
| Returns the start of the line from the beginning of the string |
| passed to the QTextLayout. |
| */ |
| int QTextLine::textStart() const |
| { |
| return eng->lines.at(index).from; |
| } |
| |
| /*! |
| Returns the length of the text in the line. |
| |
| \sa naturalTextWidth() |
| */ |
| int QTextLine::textLength() const |
| { |
| if (eng->option.flags() & QTextOption::ShowLineAndParagraphSeparators |
| && eng->block.isValid() && index == eng->lines.count()-1) { |
| return eng->lines.at(index).length - 1; |
| } |
| return eng->lines.at(index).length + eng->lines.at(index).trailingSpaces; |
| } |
| |
| static void setPenAndDrawBackground(QPainter *p, const QPen &defaultPen, const QTextCharFormat &chf, const QRectF &r) |
| { |
| QBrush c = chf.foreground(); |
| if (c.style() == Qt::NoBrush) { |
| p->setPen(defaultPen); |
| } |
| |
| QBrush bg = chf.background(); |
| if (bg.style() != Qt::NoBrush && !chf.property(SuppressBackground).toBool()) |
| p->fillRect(r.toAlignedRect(), bg); |
| if (c.style() != Qt::NoBrush) { |
| p->setPen(QPen(c, 0)); |
| } |
| |
| } |
| |
| #if !defined(QT_NO_RAWFONT) |
| static QGlyphRun glyphRunWithInfo(QFontEngine *fontEngine, |
| const QGlyphLayout &glyphLayout, |
| const QPointF &pos, |
| const QGlyphRun::GlyphRunFlags &flags, |
| const QFixed &selectionX, |
| const QFixed &selectionWidth, |
| int glyphsStart, |
| int glyphsEnd, |
| unsigned short *logClusters, |
| int textPosition, |
| int textLength) |
| { |
| Q_ASSERT(logClusters != 0); |
| |
| QGlyphRun glyphRun; |
| |
| QGlyphRunPrivate *d = QGlyphRunPrivate::get(glyphRun); |
| |
| int rangeStart = textPosition; |
| while (*logClusters != glyphsStart && rangeStart < textPosition + textLength) { |
| ++logClusters; |
| ++rangeStart; |
| } |
| |
| int rangeEnd = rangeStart; |
| while (*logClusters != glyphsEnd && rangeEnd < textPosition + textLength) { |
| ++logClusters; |
| ++rangeEnd; |
| } |
| |
| d->textRangeStart = rangeStart; |
| d->textRangeEnd = rangeEnd; |
| |
| // Make a font for this particular engine |
| QRawFont font; |
| QRawFontPrivate *fontD = QRawFontPrivate::get(font); |
| fontD->setFontEngine(fontEngine); |
| |
| QVarLengthArray<glyph_t> glyphsArray; |
| QVarLengthArray<QFixedPoint> positionsArray; |
| |
| QTextItem::RenderFlags renderFlags; |
| if (flags.testFlag(QGlyphRun::Overline)) |
| renderFlags |= QTextItem::Overline; |
| if (flags.testFlag(QGlyphRun::Underline)) |
| renderFlags |= QTextItem::Underline; |
| if (flags.testFlag(QGlyphRun::StrikeOut)) |
| renderFlags |= QTextItem::StrikeOut; |
| if (flags.testFlag(QGlyphRun::RightToLeft)) |
| renderFlags |= QTextItem::RightToLeft; |
| |
| fontEngine->getGlyphPositions(glyphLayout, QTransform(), renderFlags, glyphsArray, |
| positionsArray); |
| Q_ASSERT(glyphsArray.size() == positionsArray.size()); |
| |
| qreal fontHeight = font.ascent() + font.descent(); |
| qreal minY = 0; |
| qreal maxY = 0; |
| QVector<quint32> glyphs; |
| glyphs.reserve(glyphsArray.size()); |
| QVector<QPointF> positions; |
| positions.reserve(glyphsArray.size()); |
| for (int i=0; i<glyphsArray.size(); ++i) { |
| glyphs.append(glyphsArray.at(i) & 0xffffff); |
| |
| QPointF position = positionsArray.at(i).toPointF() + pos; |
| positions.append(position); |
| |
| if (i == 0) { |
| maxY = minY = position.y(); |
| } else { |
| minY = qMin(minY, position.y()); |
| maxY = qMax(maxY, position.y()); |
| } |
| } |
| |
| qreal height = maxY + fontHeight - minY; |
| |
| glyphRun.setGlyphIndexes(glyphs); |
| glyphRun.setPositions(positions); |
| glyphRun.setFlags(flags); |
| glyphRun.setRawFont(font); |
| |
| glyphRun.setBoundingRect(QRectF(selectionX.toReal(), minY - font.ascent(), |
| selectionWidth.toReal(), height)); |
| |
| return glyphRun; |
| } |
| |
| /*! |
| Returns the glyph indexes and positions for all glyphs in this QTextLine for characters |
| in the range defined by \a from and \a length. The \a from index is relative to the beginning |
| of the text in the containing QTextLayout, and the range must be within the range of QTextLine |
| as given by functions textStart() and textLength(). |
| |
| If \a from is negative, it will default to textStart(), and if \a length is negative it will |
| default to the return value of textLength(). |
| |
| \since 5.0 |
| |
| \sa QTextLayout::glyphRuns() |
| */ |
| QList<QGlyphRun> QTextLine::glyphRuns(int from, int length) const |
| { |
| const QScriptLine &line = eng->lines.at(index); |
| |
| if (line.length == 0) |
| return QList<QGlyphRun>(); |
| |
| if (from < 0) |
| from = textStart(); |
| |
| if (length < 0) |
| length = textLength(); |
| |
| if (length == 0) |
| return QList<QGlyphRun>(); |
| |
| QTextLayout::FormatRange selection; |
| selection.start = from; |
| selection.length = length; |
| |
| QTextLineItemIterator iterator(eng, index, QPointF(), &selection); |
| qreal y = line.y.toReal() + line.base().toReal(); |
| QList<QGlyphRun> glyphRuns; |
| while (!iterator.atEnd()) { |
| QScriptItem &si = iterator.next(); |
| if (si.analysis.flags >= QScriptAnalysis::TabOrObject) |
| continue; |
| |
| if (from >= 0 && length >= 0 && (from >= iterator.itemEnd || from + length <= iterator.itemStart)) |
| continue; |
| |
| QPointF pos(iterator.x.toReal(), y); |
| |
| QFont font; |
| QGlyphRun::GlyphRunFlags flags; |
| if (!eng->useRawFont) { |
| font = eng->font(si); |
| if (font.overline()) |
| flags |= QGlyphRun::Overline; |
| if (font.underline()) |
| flags |= QGlyphRun::Underline; |
| if (font.strikeOut()) |
| flags |= QGlyphRun::StrikeOut; |
| } |
| |
| bool rtl = false; |
| if (si.analysis.bidiLevel % 2) { |
| flags |= QGlyphRun::RightToLeft; |
| rtl = true; |
| } |
| |
| int relativeFrom = qMax(iterator.itemStart, from) - si.position; |
| int relativeTo = qMin(iterator.itemEnd, from + length) - 1 - si.position; |
| |
| unsigned short *logClusters = eng->logClusters(&si); |
| int glyphsStart = logClusters[relativeFrom]; |
| int glyphsEnd = (relativeTo == iterator.itemLength) ? si.num_glyphs - 1 : logClusters[relativeTo]; |
| // the glyph index right next to the requested range |
| int nextGlyphIndex = (relativeTo < iterator.itemLength - 1) ? logClusters[relativeTo + 1] : si.num_glyphs; |
| if (nextGlyphIndex - 1 > glyphsEnd) |
| glyphsEnd = nextGlyphIndex - 1; |
| bool startsInsideLigature = relativeFrom > 0 && logClusters[relativeFrom - 1] == glyphsStart; |
| bool endsInsideLigature = nextGlyphIndex == glyphsEnd; |
| |
| int itemGlyphsStart = logClusters[iterator.itemStart - si.position]; |
| int itemGlyphsEnd = logClusters[iterator.itemEnd - 1 - si.position]; |
| |
| QGlyphLayout glyphLayout = eng->shapedGlyphs(&si); |
| |
| // Calculate new x position of glyph layout for a subset. This becomes somewhat complex |
| // when we're breaking a RTL script item, since the expected position passed into |
| // getGlyphPositions() is the left-most edge of the left-most glyph in an RTL run. |
| if (relativeFrom != (iterator.itemStart - si.position) && !rtl) { |
| for (int i=itemGlyphsStart; i<glyphsStart; ++i) { |
| QFixed justification = QFixed::fromFixed(glyphLayout.justifications[i].space_18d6); |
| pos.rx() += (glyphLayout.advances[i] + justification).toReal(); |
| } |
| } else if (relativeTo != (iterator.itemEnd - si.position - 1) && rtl) { |
| for (int i=itemGlyphsEnd; i>glyphsEnd; --i) { |
| QFixed justification = QFixed::fromFixed(glyphLayout.justifications[i].space_18d6); |
| pos.rx() += (glyphLayout.advances[i] + justification).toReal(); |
| } |
| } |
| |
| glyphLayout = glyphLayout.mid(glyphsStart, glyphsEnd - glyphsStart + 1); |
| |
| QFixed x; |
| QFixed width; |
| iterator.getSelectionBounds(&x, &width); |
| |
| if (glyphLayout.numGlyphs > 0) { |
| QFontEngine *mainFontEngine; |
| #ifndef QT_NO_RAWFONT |
| if (eng->useRawFont && eng->rawFont.isValid()) |
| mainFontEngine= eng->fontEngine(si); |
| else |
| #endif |
| mainFontEngine = font.d->engineForScript(si.analysis.script); |
| |
| if (mainFontEngine->type() == QFontEngine::Multi) { |
| QFontEngineMulti *multiFontEngine = static_cast<QFontEngineMulti *>(mainFontEngine); |
| int start = rtl ? glyphLayout.numGlyphs : 0; |
| int end = start - 1; |
| int which = glyphLayout.glyphs[rtl ? start - 1 : end + 1] >> 24; |
| for (; (rtl && start > 0) || (!rtl && end < glyphLayout.numGlyphs - 1); |
| rtl ? --start : ++end) { |
| const int e = glyphLayout.glyphs[rtl ? start - 1 : end + 1] >> 24; |
| if (e == which) |
| continue; |
| |
| QGlyphLayout subLayout = glyphLayout.mid(start, end - start + 1); |
| multiFontEngine->ensureEngineAt(which); |
| |
| QGlyphRun::GlyphRunFlags subFlags = flags; |
| if (start == 0 && startsInsideLigature) |
| subFlags |= QGlyphRun::SplitLigature; |
| |
| glyphRuns.append(glyphRunWithInfo(multiFontEngine->engine(which), |
| subLayout, |
| pos, |
| subFlags, |
| x, |
| width, |
| glyphsStart + start, |
| glyphsStart + end, |
| logClusters + relativeFrom, |
| relativeFrom + si.position, |
| relativeTo - relativeFrom + 1)); |
| for (int i = 0; i < subLayout.numGlyphs; ++i) { |
| QFixed justification = QFixed::fromFixed(subLayout.justifications[i].space_18d6); |
| pos.rx() += (subLayout.advances[i] + justification).toReal(); |
| } |
| |
| if (rtl) |
| end = start - 1; |
| else |
| start = end + 1; |
| which = e; |
| } |
| |
| QGlyphLayout subLayout = glyphLayout.mid(start, end - start + 1); |
| multiFontEngine->ensureEngineAt(which); |
| |
| QGlyphRun::GlyphRunFlags subFlags = flags; |
| if ((start == 0 && startsInsideLigature) || endsInsideLigature) |
| subFlags |= QGlyphRun::SplitLigature; |
| |
| QGlyphRun glyphRun = glyphRunWithInfo(multiFontEngine->engine(which), |
| subLayout, |
| pos, |
| subFlags, |
| x, |
| width, |
| glyphsStart + start, |
| glyphsStart + end, |
| logClusters + relativeFrom, |
| relativeFrom + si.position, |
| relativeTo - relativeFrom + 1); |
| if (!glyphRun.isEmpty()) |
| glyphRuns.append(glyphRun); |
| } else { |
| if (startsInsideLigature || endsInsideLigature) |
| flags |= QGlyphRun::SplitLigature; |
| QGlyphRun glyphRun = glyphRunWithInfo(mainFontEngine, |
| glyphLayout, |
| pos, |
| flags, |
| x, |
| width, |
| glyphsStart, |
| glyphsEnd, |
| logClusters + relativeFrom, |
| relativeFrom + si.position, |
| relativeTo - relativeFrom + 1); |
| if (!glyphRun.isEmpty()) |
| glyphRuns.append(glyphRun); |
| } |
| } |
| } |
| |
| return glyphRuns; |
| } |
| #endif // QT_NO_RAWFONT |
| |
| /*! |
| \fn void QTextLine::draw(QPainter *painter, const QPointF &position, const QTextLayout::FormatRange *selection) const |
| |
| Draws a line on the given \a painter at the specified \a position. |
| The \a selection is reserved for internal use. |
| */ |
| void QTextLine::draw(QPainter *p, const QPointF &pos, const QTextLayout::FormatRange *selection) const |
| { |
| #ifndef QT_NO_RAWFONT |
| // Not intended to work with rawfont |
| Q_ASSERT(!eng->useRawFont); |
| #endif |
| const QScriptLine &line = eng->lines[index]; |
| QPen pen = p->pen(); |
| |
| bool noText = (selection && selection->format.property(SuppressText).toBool()); |
| |
| if (!line.length) { |
| if (selection |
| && selection->start <= line.from |
| && selection->start + selection->length > line.from) { |
| |
| const qreal lineHeight = line.height().toReal(); |
| QRectF r(pos.x() + line.x.toReal(), pos.y() + line.y.toReal(), |
| lineHeight / 2, QFontMetrics(eng->font()).horizontalAdvance(QLatin1Char(' '))); |
| setPenAndDrawBackground(p, QPen(), selection->format, r); |
| p->setPen(pen); |
| } |
| return; |
| } |
| |
| |
| QTextLineItemIterator iterator(eng, index, pos, selection); |
| QFixed lineBase = line.base(); |
| eng->clearDecorations(); |
| eng->enableDelayDecorations(); |
| |
| const QFixed y = QFixed::fromReal(pos.y()) + line.y + lineBase; |
| |
| bool suppressColors = (eng->option.flags() & QTextOption::SuppressColors); |
| while (!iterator.atEnd()) { |
| QScriptItem &si = iterator.next(); |
| |
| if (selection && selection->start >= 0 && iterator.isOutsideSelection()) |
| continue; |
| |
| if (si.analysis.flags == QScriptAnalysis::LineOrParagraphSeparator |
| && !(eng->option.flags() & QTextOption::ShowLineAndParagraphSeparators)) |
| continue; |
| |
| QFixed itemBaseLine = y; |
| QFont f = eng->font(si); |
| QTextCharFormat format; |
| |
| if (eng->hasFormats() || selection) { |
| format = eng->format(&si); |
| if (suppressColors) { |
| format.clearForeground(); |
| format.clearBackground(); |
| format.clearProperty(QTextFormat::TextUnderlineColor); |
| } |
| if (selection) |
| format.merge(selection->format); |
| |
| setPenAndDrawBackground(p, pen, format, QRectF(iterator.x.toReal(), (y - lineBase).toReal(), |
| iterator.itemWidth.toReal(), line.height().toReal())); |
| |
| QTextCharFormat::VerticalAlignment valign = format.verticalAlignment(); |
| if (valign == QTextCharFormat::AlignSuperScript || valign == QTextCharFormat::AlignSubScript) { |
| QFontEngine *fe = f.d->engineForScript(si.analysis.script); |
| QFixed height = fe->ascent() + fe->descent(); |
| if (valign == QTextCharFormat::AlignSubScript) |
| itemBaseLine += height / 6; |
| else if (valign == QTextCharFormat::AlignSuperScript) |
| itemBaseLine -= height / 2; |
| } |
| } |
| |
| if (si.analysis.flags >= QScriptAnalysis::TabOrObject) { |
| |
| if (eng->hasFormats()) { |
| p->save(); |
| if (si.analysis.flags == QScriptAnalysis::Object && eng->block.docHandle()) { |
| QFixed itemY = y - si.ascent; |
| if (format.verticalAlignment() == QTextCharFormat::AlignTop) { |
| itemY = y - lineBase; |
| } else if (format.verticalAlignment() == QTextCharFormat::AlignBottom) { |
| itemY = y + line.descent - si.ascent - si.descent; |
| } |
| |
| QRectF itemRect(iterator.x.toReal(), itemY.toReal(), iterator.itemWidth.toReal(), si.height().toReal()); |
| |
| eng->docLayout()->drawInlineObject(p, itemRect, |
| QTextInlineObject(iterator.item, eng), |
| si.position + eng->block.position(), |
| format); |
| if (selection) { |
| QBrush bg = format.brushProperty(ObjectSelectionBrush); |
| if (bg.style() != Qt::NoBrush) { |
| QColor c = bg.color(); |
| c.setAlpha(128); |
| p->fillRect(itemRect, c); |
| } |
| } |
| } else { // si.isTab |
| QFont f = eng->font(si); |
| QTextItemInt gf(si, &f, format); |
| gf.chars = 0; |
| gf.num_chars = 0; |
| gf.width = iterator.itemWidth; |
| QPainterPrivate::get(p)->drawTextItem(QPointF(iterator.x.toReal(), y.toReal()), gf, eng); |
| if (eng->option.flags() & QTextOption::ShowTabsAndSpaces) { |
| QChar visualTab(0x2192); |
| int w = QFontMetrics(f).horizontalAdvance(visualTab); |
| qreal x = iterator.itemWidth.toReal() - w; // Right-aligned |
| if (x < 0) |
| p->setClipRect(QRectF(iterator.x.toReal(), line.y.toReal(), |
| iterator.itemWidth.toReal(), line.height().toReal()), |
| Qt::IntersectClip); |
| else |
| x /= 2; // Centered |
| p->setFont(f); |
| p->drawText(QPointF(iterator.x.toReal() + x, |
| y.toReal()), visualTab); |
| } |
| |
| } |
| p->restore(); |
| } |
| |
| continue; |
| } |
| |
| unsigned short *logClusters = eng->logClusters(&si); |
| QGlyphLayout glyphs = eng->shapedGlyphs(&si); |
| |
| QTextItemInt gf(glyphs.mid(iterator.glyphsStart, iterator.glyphsEnd - iterator.glyphsStart), |
| &f, eng->layoutData->string.unicode() + iterator.itemStart, |
| iterator.itemEnd - iterator.itemStart, eng->fontEngine(si), format); |
| gf.logClusters = logClusters + iterator.itemStart - si.position; |
| gf.width = iterator.itemWidth; |
| gf.justified = line.justified; |
| gf.initWithScriptItem(si); |
| |
| Q_ASSERT(gf.fontEngine); |
| |
| QPointF pos(iterator.x.toReal(), itemBaseLine.toReal()); |
| if (format.penProperty(QTextFormat::TextOutline).style() != Qt::NoPen) { |
| QPainterPath path; |
| path.setFillRule(Qt::WindingFill); |
| |
| if (gf.glyphs.numGlyphs) |
| gf.fontEngine->addOutlineToPath(pos.x(), pos.y(), gf.glyphs, &path, gf.flags); |
| if (gf.flags) { |
| const QFontEngine *fe = gf.fontEngine; |
| const qreal lw = fe->lineThickness().toReal(); |
| if (gf.flags & QTextItem::Underline) { |
| qreal offs = fe->underlinePosition().toReal(); |
| path.addRect(pos.x(), pos.y() + offs, gf.width.toReal(), lw); |
| } |
| if (gf.flags & QTextItem::Overline) { |
| qreal offs = fe->ascent().toReal() + 1; |
| path.addRect(pos.x(), pos.y() - offs, gf.width.toReal(), lw); |
| } |
| if (gf.flags & QTextItem::StrikeOut) { |
| qreal offs = fe->ascent().toReal() / 3; |
| path.addRect(pos.x(), pos.y() - offs, gf.width.toReal(), lw); |
| } |
| } |
| |
| p->save(); |
| p->setRenderHint(QPainter::Antialiasing); |
| //Currently QPen with a Qt::NoPen style still returns a default |
| //QBrush which != Qt::NoBrush so we need this specialcase to reset it |
| if (p->pen().style() == Qt::NoPen) |
| p->setBrush(Qt::NoBrush); |
| else |
| p->setBrush(p->pen().brush()); |
| |
| p->setPen(format.textOutline()); |
| p->drawPath(path); |
| p->restore(); |
| } else { |
| if (noText) |
| gf.glyphs.numGlyphs = 0; // slightly less elegant than it should be |
| QPainterPrivate::get(p)->drawTextItem(pos, gf, eng); |
| } |
| |
| if ((si.analysis.flags == QScriptAnalysis::Space |
| || si.analysis.flags == QScriptAnalysis::Nbsp) |
| && (eng->option.flags() & QTextOption::ShowTabsAndSpaces)) { |
| QBrush c = format.foreground(); |
| if (c.style() != Qt::NoBrush) |
| p->setPen(c.color()); |
| QChar visualSpace(si.analysis.flags == QScriptAnalysis::Space ? (ushort)0xb7 : (ushort)0xb0); |
| QFont oldFont = p->font(); |
| p->setFont(eng->font(si)); |
| p->drawText(QPointF(iterator.x.toReal(), itemBaseLine.toReal()), visualSpace); |
| p->setPen(pen); |
| p->setFont(oldFont); |
| } |
| } |
| eng->drawDecorations(p); |
| |
| if (eng->hasFormats()) |
| p->setPen(pen); |
| } |
| |
| /*! |
| \fn int QTextLine::cursorToX(int cursorPos, Edge edge) const |
| |
| \overload |
| */ |
| |
| /*! |
| Converts the cursor position \a cursorPos to the corresponding x position |
| inside the line, taking account of the \a edge. |
| |
| If \a cursorPos is not a valid cursor position, the nearest valid |
| cursor position will be used instead, and \a cursorPos will be modified to |
| point to this valid cursor position. |
| |
| \sa xToCursor() |
| */ |
| qreal QTextLine::cursorToX(int *cursorPos, Edge edge) const |
| { |
| const QScriptLine &line = eng->lines[index]; |
| bool lastLine = index >= eng->lines.size() - 1; |
| |
| QFixed x = line.x + eng->alignLine(line) - eng->leadingSpaceWidth(line); |
| |
| if (!eng->layoutData) |
| eng->itemize(); |
| if (!eng->layoutData->items.size()) { |
| *cursorPos = line.from; |
| return x.toReal(); |
| } |
| |
| int lineEnd = line.from + line.length + line.trailingSpaces; |
| int pos = qBound(line.from, *cursorPos, lineEnd); |
| int itm; |
| const QCharAttributes *attributes = eng->attributes(); |
| if (!attributes) { |
| *cursorPos = line.from; |
| return x.toReal(); |
| } |
| while (pos < lineEnd && !attributes[pos].graphemeBoundary) |
| pos++; |
| if (pos == lineEnd) { |
| // end of line ensure we have the last item on the line |
| itm = eng->findItem(pos-1); |
| } |
| else |
| itm = eng->findItem(pos); |
| if (itm < 0) { |
| *cursorPos = line.from; |
| return x.toReal(); |
| } |
| eng->shapeLine(line); |
| |
| const QScriptItem *si = &eng->layoutData->items[itm]; |
| if (!si->num_glyphs) |
| eng->shape(itm); |
| |
| const int l = eng->length(itm); |
| pos = qBound(0, pos - si->position, l); |
| |
| QGlyphLayout glyphs = eng->shapedGlyphs(si); |
| unsigned short *logClusters = eng->logClusters(si); |
| Q_ASSERT(logClusters); |
| |
| int glyph_pos = pos == l ? si->num_glyphs : logClusters[pos]; |
| if (edge == Trailing && glyph_pos < si->num_glyphs) { |
| // trailing edge is leading edge of next cluster |
| glyph_pos++; |
| while (glyph_pos < si->num_glyphs && !glyphs.attributes[glyph_pos].clusterStart) |
| glyph_pos++; |
| } |
| |
| bool reverse = si->analysis.bidiLevel % 2; |
| |
| |
| // add the items left of the cursor |
| |
| int firstItem = eng->findItem(line.from); |
| int lastItem = eng->findItem(lineEnd - 1, itm); |
| int nItems = (firstItem >= 0 && lastItem >= firstItem)? (lastItem-firstItem+1) : 0; |
| |
| QVarLengthArray<int> visualOrder(nItems); |
| QVarLengthArray<uchar> levels(nItems); |
| for (int i = 0; i < nItems; ++i) |
| levels[i] = eng->layoutData->items[i+firstItem].analysis.bidiLevel; |
| QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data()); |
| |
| for (int i = 0; i < nItems; ++i) { |
| int item = visualOrder[i]+firstItem; |
| if (item == itm) |
| break; |
| QScriptItem &si = eng->layoutData->items[item]; |
| if (!si.num_glyphs) |
| eng->shape(item); |
| |
| if (si.analysis.flags >= QScriptAnalysis::TabOrObject) { |
| x += si.width; |
| continue; |
| } |
| |
| const int itemLength = eng->length(item); |
| int start = qMax(line.from, si.position); |
| int end = qMin(lineEnd, si.position + itemLength); |
| |
| logClusters = eng->logClusters(&si); |
| |
| int gs = logClusters[start-si.position]; |
| int ge = (end == si.position + itemLength) ? si.num_glyphs-1 : logClusters[end-si.position-1]; |
| |
| QGlyphLayout glyphs = eng->shapedGlyphs(&si); |
| |
| while (gs <= ge) { |
| x += glyphs.effectiveAdvance(gs); |
| ++gs; |
| } |
| } |
| |
| logClusters = eng->logClusters(si); |
| glyphs = eng->shapedGlyphs(si); |
| if (si->analysis.flags >= QScriptAnalysis::TabOrObject) { |
| if (pos == (reverse ? 0 : l)) |
| x += si->width; |
| } else { |
| bool rtl = eng->isRightToLeft(); |
| bool visual = eng->visualCursorMovement(); |
| int end = qMin(lineEnd, si->position + l) - si->position; |
| if (reverse) { |
| int glyph_end = end == l ? si->num_glyphs : logClusters[end]; |
| int glyph_start = glyph_pos; |
| if (visual && !rtl && !(lastLine && itm == (visualOrder[nItems - 1] + firstItem))) |
| glyph_start++; |
| for (int i = glyph_end - 1; i >= glyph_start; i--) |
| x += glyphs.effectiveAdvance(i); |
| x -= eng->offsetInLigature(si, pos, end, glyph_pos); |
| } else { |
| int start = qMax(line.from - si->position, 0); |
| int glyph_start = logClusters[start]; |
| int glyph_end = glyph_pos; |
| if (!visual || !rtl || (lastLine && itm == visualOrder[0] + firstItem)) |
| glyph_end--; |
| for (int i = glyph_start; i <= glyph_end; i++) |
| x += glyphs.effectiveAdvance(i); |
| x += eng->offsetInLigature(si, pos, end, glyph_pos); |
| } |
| } |
| |
| if (eng->option.wrapMode() != QTextOption::NoWrap && x > line.x + line.width) |
| x = line.x + line.width; |
| if (eng->option.wrapMode() != QTextOption::NoWrap && x < 0) |
| x = 0; |
| |
| *cursorPos = pos + si->position; |
| return x.toReal(); |
| } |
| |
| /*! |
| \fn int QTextLine::xToCursor(qreal x, CursorPosition cpos) const |
| |
| Converts the x-coordinate \a x, to the nearest matching cursor |
| position, depending on the cursor position type, \a cpos. |
| Note that result cursor position includes possible preedit area text. |
| |
| \sa cursorToX() |
| */ |
| int QTextLine::xToCursor(qreal _x, CursorPosition cpos) const |
| { |
| QFixed x = QFixed::fromReal(_x); |
| const QScriptLine &line = eng->lines[index]; |
| bool lastLine = index >= eng->lines.size() - 1; |
| int lineNum = index; |
| |
| if (!eng->layoutData) |
| eng->itemize(); |
| |
| int line_length = textLength(); |
| |
| if (!line_length) |
| return line.from; |
| |
| int firstItem = eng->findItem(line.from); |
| int lastItem = eng->findItem(line.from + line_length - 1, firstItem); |
| int nItems = (firstItem >= 0 && lastItem >= firstItem)? (lastItem-firstItem+1) : 0; |
| |
| if (!nItems) |
| return 0; |
| |
| x -= line.x; |
| x -= eng->alignLine(line); |
| // qDebug("xToCursor: x=%f, cpos=%d", x.toReal(), cpos); |
| |
| QVarLengthArray<int> visualOrder(nItems); |
| QVarLengthArray<unsigned char> levels(nItems); |
| for (int i = 0; i < nItems; ++i) |
| levels[i] = eng->layoutData->items[i+firstItem].analysis.bidiLevel; |
| QTextEngine::bidiReorder(nItems, levels.data(), visualOrder.data()); |
| |
| bool visual = eng->visualCursorMovement(); |
| if (x <= 0) { |
| // left of first item |
| int item = visualOrder[0]+firstItem; |
| QScriptItem &si = eng->layoutData->items[item]; |
| if (!si.num_glyphs) |
| eng->shape(item); |
| int pos = si.position; |
| if (si.analysis.bidiLevel % 2) |
| pos += eng->length(item); |
| pos = qMax(line.from, pos); |
| pos = qMin(line.from + line_length, pos); |
| return pos; |
| } else if (x < line.textWidth |
| || (line.justified && x < line.width)) { |
| // has to be in one of the runs |
| QFixed pos; |
| bool rtl = eng->isRightToLeft(); |
| |
| eng->shapeLine(line); |
| const auto insertionPoints = (visual && rtl) ? eng->insertionPointsForLine(lineNum) : std::vector<int>(); |
| int nchars = 0; |
| for (int i = 0; i < nItems; ++i) { |
| int item = visualOrder[i]+firstItem; |
| QScriptItem &si = eng->layoutData->items[item]; |
| int item_length = eng->length(item); |
| // qDebug(" item %d, visual %d x_remain=%f", i, item, x.toReal()); |
| |
| int start = qMax(line.from - si.position, 0); |
| int end = qMin(line.from + line_length - si.position, item_length); |
| |
| unsigned short *logClusters = eng->logClusters(&si); |
| |
| int gs = logClusters[start]; |
| int ge = (end == item_length ? si.num_glyphs : logClusters[end]) - 1; |
| QGlyphLayout glyphs = eng->shapedGlyphs(&si); |
| |
| QFixed item_width = 0; |
| if (si.analysis.flags >= QScriptAnalysis::TabOrObject) { |
| item_width = si.width; |
| } else { |
| int g = gs; |
| while (g <= ge) { |
| item_width += glyphs.effectiveAdvance(g); |
| ++g; |
| } |
| } |
| // qDebug(" start=%d, end=%d, gs=%d, ge=%d item_width=%f", start, end, gs, ge, item_width.toReal()); |
| |
| if (pos + item_width < x) { |
| pos += item_width; |
| nchars += end; |
| continue; |
| } |
| // qDebug(" inside run"); |
| if (si.analysis.flags >= QScriptAnalysis::TabOrObject) { |
| if (cpos == QTextLine::CursorOnCharacter) |
| return si.position; |
| bool left_half = (x - pos) < item_width/2; |
| |
| if (bool(si.analysis.bidiLevel % 2) != left_half) |
| return si.position; |
| return si.position + 1; |
| } |
| |
| int glyph_pos = -1; |
| QFixed edge; |
| // has to be inside run |
| if (cpos == QTextLine::CursorOnCharacter) { |
| if (si.analysis.bidiLevel % 2) { |
| pos += item_width; |
| glyph_pos = gs; |
| while (gs <= ge) { |
| if (glyphs.attributes[gs].clusterStart) { |
| if (pos < x) |
| break; |
| glyph_pos = gs; |
| edge = pos; |
| } |
| pos -= glyphs.effectiveAdvance(gs); |
| ++gs; |
| } |
| } else { |
| glyph_pos = gs; |
| while (gs <= ge) { |
| if (glyphs.attributes[gs].clusterStart) { |
| if (pos > x) |
| break; |
| glyph_pos = gs; |
| edge = pos; |
| } |
| pos += glyphs.effectiveAdvance(gs); |
| ++gs; |
| } |
| } |
| } else { |
| QFixed dist = INT_MAX/256; |
| if (si.analysis.bidiLevel % 2) { |
| if (!visual || rtl || (lastLine && i == nItems - 1)) { |
| pos += item_width; |
| while (gs <= ge) { |
| if (glyphs.attributes[gs].clusterStart && qAbs(x-pos) < dist) { |
| glyph_pos = gs; |
| edge = pos; |
| dist = qAbs(x-pos); |
| } |
| pos -= glyphs.effectiveAdvance(gs); |
| ++gs; |
| } |
| } else { |
| while (ge >= gs) { |
| if (glyphs.attributes[ge].clusterStart && qAbs(x-pos) < dist) { |
| glyph_pos = ge; |
| edge = pos; |
| dist = qAbs(x-pos); |
| } |
| pos += glyphs.effectiveAdvance(ge); |
| --ge; |
| } |
| } |
| } else { |
| if (!visual || !rtl || (lastLine && i == 0)) { |
| while (gs <= ge) { |
| if (glyphs.attributes[gs].clusterStart && qAbs(x-pos) < dist) { |
| glyph_pos = gs; |
| edge = pos; |
| dist = qAbs(x-pos); |
| } |
| pos += glyphs.effectiveAdvance(gs); |
| ++gs; |
| } |
| } else { |
| QFixed oldPos = pos; |
| while (gs <= ge) { |
| pos += glyphs.effectiveAdvance(gs); |
| if (glyphs.attributes[gs].clusterStart && qAbs(x-pos) < dist) { |
| glyph_pos = gs; |
| edge = pos; |
| dist = qAbs(x-pos); |
| } |
| ++gs; |
| } |
| pos = oldPos; |
| } |
| } |
| if (qAbs(x-pos) < dist) { |
| if (visual) { |
| if (!rtl && i < nItems - 1) { |
| nchars += end; |
| continue; |
| } |
| if (rtl && nchars > 0) |
| return insertionPoints[size_t(lastLine ? nchars : nchars - 1)]; |
| } |
| return eng->positionInLigature(&si, end, x, pos, -1, |
| cpos == QTextLine::CursorOnCharacter); |
| } |
| } |
| Q_ASSERT(glyph_pos != -1); |
| return eng->positionInLigature(&si, end, x, edge, glyph_pos, |
| cpos == QTextLine::CursorOnCharacter); |
| } |
| } |
| // right of last item |
| // qDebug("right of last"); |
| int item = visualOrder[nItems-1]+firstItem; |
| QScriptItem &si = eng->layoutData->items[item]; |
| if (!si.num_glyphs) |
| eng->shape(item); |
| int pos = si.position; |
| if (!(si.analysis.bidiLevel % 2)) |
| pos += eng->length(item); |
| pos = qMax(line.from, pos); |
| |
| int maxPos = line.from + line_length; |
| |
| // except for the last line we assume that the |
| // character between lines is a space and we want |
| // to position the cursor to the left of that |
| // character. |
| if (this->index < eng->lines.count() - 1) |
| maxPos = eng->previousLogicalPosition(maxPos); |
| |
| pos = qMin(pos, maxPos); |
| return pos; |
| } |
| |
| QT_END_NAMESPACE |