]> git.lyx.org Git - lyx.git/commitdiff
Break multi-row strings in one pass
authorJean-Marc Lasgouttes <lasgouttes@lyx.org>
Mon, 6 Sep 2021 12:52:42 +0000 (14:52 +0200)
committerJean-Marc Lasgouttes <lasgouttes@lyx.org>
Tue, 7 Dec 2021 16:04:47 +0000 (17:04 +0100)
Replace FontMetrics::breakAt, which returned the next break point,
with FontMetrics::breakString, which returns a vector of break points.
To this end, an additional parameter gives the available width for
next rows.

Rename various variables and methods accordingly. Factor the code in
breakString_helper to be more manageable.

Adapt Row::Element::splitAt to return a bool on sucess and provide
remaining row elements in a vector. The width noted above has been
added as parameters.

Rename the helper function splitFrom to moveElements and rewrite the
code to be more efficient.

Remove type of row element INVALID, which is not needed anymore.

The code in TextMetrics::breakParagraph is now much simpler.

In Row::finalize, remove the code that computed inconditionnally the
current element size, and make sure that this width will be computed
in all code paths of Row::Element::splitAt.

src/Row.cpp
src/Row.h
src/RowPainter.cpp
src/TextMetrics.cpp
src/frontends/FontMetrics.h
src/frontends/qt/GuiFontMetrics.cpp
src/frontends/qt/GuiFontMetrics.h

index b7e4c07c95c3903c608f78996d0e4a6fe6bff95f..6b0faa329241fd59162d6fc290100dae467fd801 100644 (file)
@@ -123,41 +123,81 @@ pos_type Row::Element::x2pos(int &x) const
                        x = 0;
                        i = isRTL();
                }
-               break;
-       case INVALID:
-               LYXERR0("x2pos: INVALID row element !");
        }
        //lyxerr << "=> p=" << pos + i << " x=" << x << endl;
        return pos + i;
 }
 
 
-Row::Element Row::Element::splitAt(int w, bool force)
+bool Row::Element::splitAt(int const width, int next_width, bool force,
+                           Row::Elements & tail)
 {
-       if (type != STRING || !(row_flags & CanBreakInside))
-               return Element();
+       // Not a string or already OK.
+       if (type != STRING || (dim.wid > 0 && dim.wid < width))
+               return false;
 
        FontMetrics const & fm = theFontMetrics(font);
-       dim.wid = w;
-       int const i = fm.breakAt(str, dim.wid, isRTL(), force);
-       if (i != -1) {
-               //Create a second row element to return
-               Element ret(STRING, pos + i, font, change);
-               ret.str = str.substr(i);
-               ret.endpos = ret.pos + ret.str.length();
-               // Copy the after flags of the original element to the second one.
-               ret.row_flags = row_flags & (CanBreakInside | AfterFlags);
-
-               // Now update ourselves
-               str.erase(i);
-               endpos = pos + i;
-               // Row should be broken after the original element
-               row_flags = (row_flags & ~AfterFlags) | BreakAfter;
-               //LYXERR0("breakAt(" << w << ")  Row element Broken at " << w << "(w(str)=" << fm.width(str) << "): e=" << *this);
-               return ret;
+
+       // A a string that is not breakable
+       if (!(row_flags & CanBreakInside)) {
+               // has width been computed yet?
+               if (dim.wid == 0)
+                       dim.wid = fm.width(str);
+               return false;
+       }
+
+       bool const wrap_any = !font.language()->wordWrap();
+       FontMetrics::Breaks breaks = fm.breakString(str, width, next_width,
+                                                isRTL(), wrap_any | force);
+
+       // if breaking did not really work, give up
+       if (!force && breaks.front().wid > width) {
+               if (dim.wid == 0)
+                       dim.wid = fm.width(str);
+               return false;
+       }
+
+       Element first_e(STRING, pos, font, change);
+       // should next element eventually replace *this?
+       bool first = true;
+       docstring::size_type i = 0;
+       for (FontMetrics::Break const & brk : breaks) {
+               Element e(STRING, pos + i, font, change);
+               e.str = str.substr(i, brk.len);
+               e.endpos = e.pos + brk.len;
+               e.dim.wid = brk.wid;
+               e.row_flags = CanBreakInside | BreakAfter;
+               if (first) {
+                       // this element eventually goes to *this
+                       e.row_flags |= row_flags & ~AfterFlags;
+                       first_e = e;
+                       first = false;
+               } else
+                       tail.push_back(e);
+               i += brk.len;
        }
 
-       return Element();
+       if (!tail.empty()) {
+               // Avoid having a last empty element. This happens when
+               // breaking at the trailing space of string
+               if (tail.back().str.empty())
+                       tail.pop_back();
+               else {
+                       // Copy the after flags of the original element to the last one.
+                       tail.back().row_flags &= ~BreakAfter;
+                       tail.back().row_flags |= row_flags & AfterFlags;
+               }
+               // first_e row should be broken after the original element
+               first_e.row_flags |= BreakAfter;
+       } else {
+               // Restore the after flags of the original element.
+               first_e.row_flags &= ~BreakAfter;
+               first_e.row_flags |= row_flags & AfterFlags;
+       }
+
+       // update ourselves
+       swap(first_e, *this);
+       return true;
 }
 
 
@@ -265,10 +305,6 @@ ostream & operator<<(ostream & os, Row::Element const & e)
                break;
        case Row::SPACE:
                os << "SPACE: ";
-               break;
-       case Row::INVALID:
-               os << "INVALID: ";
-               break;
        }
        os << "width=" << e.full_width() << ", row_flags=" << e.row_flags;
        return os;
@@ -393,11 +429,6 @@ void Row::finalizeLast()
        elt.final = true;
        if (elt.change.changed())
                changebar_ = true;
-
-       if (elt.type == STRING && elt.dim.wid == 0) {
-               elt.dim.wid = theFontMetrics(elt.font).width(elt.str);
-               dim_.wid += elt.dim.wid;
-       }
 }
 
 
@@ -474,19 +505,14 @@ void Row::pop_back()
 
 namespace {
 
-// Remove stuff after \c it from \c elts, and return it.
-// if \c init is provided, it will prepended to the rest
-Row::Elements splitFrom(Row::Elements & elts, Row::Elements::iterator const & it,
-                        Row::Element const & init = Row::Element())
+// Move stuff after \c it from \c from and the end of \c to.
+void moveElements(Row::Elements & from, Row::Elements::iterator const & it,
+                  Row::Elements & to)
 {
-       Row::Elements ret;
-       if (init.isValid())
-               ret.push_back(init);
-       ret.insert(ret.end(), it, elts.end());
-       elts.erase(it, elts.end());
-       if (!elts.empty())
-               elts.back().row_flags = (elts.back().row_flags & ~AfterFlags) | BreakAfter;
-       return ret;
+       to.insert(to.end(), it, from.end());
+       from.erase(it, from.end());
+       if (!from.empty())
+               from.back().row_flags = (from.back().row_flags & ~AfterFlags) | BreakAfter;
 }
 
 }
@@ -522,6 +548,7 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
        Elements::iterator cit_brk = cit;
        int wid_brk = wid + cit_brk->dim.wid;
        ++cit_brk;
+       Elements tail;
        while (cit_brk != beg) {
                --cit_brk;
                // make a copy of the element to work on it.
@@ -533,28 +560,18 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
                if (wid_brk <= w && brk.row_flags & CanBreakAfter) {
                        end_ = brk.endpos;
                        dim_.wid = wid_brk;
-                       return splitFrom(elements_, cit_brk + 1);
+                       moveElements(elements_, cit_brk + 1, tail);
+                       return tail;
                }
                // assume now that the current element is not there
                wid_brk -= brk.dim.wid;
-               /*
-                * Some Asian languages split lines anywhere (no notion of
-                * word). It seems that QTextLayout is not aware of this fact.
-                * See for reference:
-                *    https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages
-                *
-                * FIXME: Something shall be done about characters which are
-                * not allowed at the beginning or end of line.
-               */
-               bool const word_wrap = brk.font.language()->wordWrap();
                /* We have found a suitable separable element. This is the common case.
-                * Try to break it cleanly (at word boundary) at a length that is both
+                * Try to break it cleanly at a length that is both
                 * - less than the available space on the row
                 * - shorter than the natural width of the element, in order to enforce
                 *   break-up.
                 */
-               Element remainder = brk.splitAt(min(w - wid_brk, brk.dim.wid - 2), !word_wrap);
-               if (brk.row_flags & BreakAfter) {
+               if (brk.splitAt(min(w - wid_brk, brk.dim.wid - 2), next_width, false, tail)) {
                        /* if this element originally did not cause a row overflow
                         * in itself, and the remainder of the row would still be
                         * too large after breaking, then we will have issues in
@@ -568,12 +585,10 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
                        *cit_brk = brk;
                        dim_.wid = wid_brk + brk.dim.wid;
                        // If there are other elements, they should be removed.
-                       // remainder can be empty when splitting at trailing space
-                       if (remainder.str.empty())
-                               return splitFrom(elements_, next(cit_brk, 1));
-                       else
-                               return splitFrom(elements_, next(cit_brk, 1), remainder);
+                       moveElements(elements_, cit_brk + 1, tail);
+                       return tail;
                }
+               LATTEST(tail.empty());
        }
 
        if (cit != beg && cit->row_flags & NoBreakBefore) {
@@ -588,20 +603,23 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
                // been added. We can cut right here.
                end_ = cit->pos;
                dim_.wid = wid;
-               return splitFrom(elements_, cit);
+               moveElements(elements_, cit, tail);
+               return tail;
        }
 
        /* If we are here, it means that we have not found a separator to
-        * shorten the row. Let's try to break it again, but not at word
-        * boundary this time.
+        * shorten the row. Let's try to break it again, but force
+        * splitting this time.
         */
-       Element remainder = cit->splitAt(w - wid, true);
-       if (cit->row_flags & BreakAfter) {
+       if (cit->splitAt(w - wid, next_width, true, tail)) {
+               LYXERR0(*cit);
                end_ = cit->endpos;
                dim_.wid = wid + cit->dim.wid;
                // If there are other elements, they should be removed.
-               return splitFrom(elements_, next(cit, 1), remainder);
+               moveElements(elements_, cit + 1, tail);
+               return tail;
        }
+
        return Elements();
 }
 
index 1272a0f6eabd7cdf7987c2ed86335e06f64c33fb..bf49eb1b33025c3238bb7ca9a0d5a59396f11aa1 100644 (file)
--- a/src/Row.h
+++ b/src/Row.h
@@ -50,9 +50,7 @@ public:
                // An inset
                INSET,
                // Some spacing described by its width, not a string
-               SPACE,
-               // Something that should not happen (for error handling)
-               INVALID
+               SPACE
        };
 
 /**
@@ -60,8 +58,6 @@ public:
  * by other methods that need to parse the Row contents.
  */
        struct Element {
-               //
-               Element() = default;
                //
                Element(Type const t, pos_type p, Font const & f, Change const & ch)
                        : type(t), pos(p), endpos(p + 1), font(f), change(ch) {}
@@ -94,13 +90,16 @@ public:
                pos_type x2pos(int &x) const;
                /** Break the element in two if possible, so that its width is less
                 * than \param w.
-                * \return an element containing the remainder of the text, or
-                *   an invalid element if nothing happened.
-                * \param w: the desired maximum width
-                * \param force: if true, the string is cut at any place, otherwise it
-                *   respects the row breaking rules of characters.
+                * \return a vector of elements containing the remainder of
+                *   the text (empty if nothing happened).
+                * \param width maximum width of the row.
+                * \param next_width available width on next row.
+                * \param force: if true, cut string at any place, even for
+                *   languages that wrap at word delimiters; if false, do not
+                *   break at all if first element would larger than \c width.
                 */
-               Element splitAt(int w, bool force);
+               // FIXME: ideally last parameter should be Elements&, but it is not possible.
+               bool splitAt(int width, int next_width, bool force, std::vector<Element> & tail);
                // remove trailing spaces (useful for end of row)
                void rtrim();
 
@@ -108,8 +107,6 @@ public:
                bool isRTL() const { return font.isVisibleRightToLeft(); }
                // This is true for virtual elements.
                bool isVirtual() const { return type == VIRTUAL; }
-               // Invalid element, for error handling
-               bool isValid() const { return type !=INVALID; }
 
                // Returns the position on left side of the element.
                pos_type left_pos() const { return isRTL() ? endpos : pos; };
@@ -117,11 +114,11 @@ public:
                pos_type right_pos() const { return isRTL() ? pos : endpos; };
 
                // The kind of row element
-               Type type = INVALID;
+               Type type;
                // position of the element in the paragraph
-               pos_type pos = 0;
+               pos_type pos;
                // first position after the element in the paragraph
-               pos_type endpos = 0;
+               pos_type endpos;
                // The dimension of the chunk (does not contains the
                // separator correction)
                Dimension dim;
@@ -289,8 +286,8 @@ public:
         * separator and update endpos if necessary. If all that
         * remains is a large word, cut it to \param width.
         * \param width maximum width of the row.
-        * \param available width on next row.
-        * \return true if the row has been shortened.
+        * \param next_width available width on next row.
+        * \return list of elements remaining after breaking.
         */
        Elements shortenIfNeeded(int const width, int const next_width);
 
index 656f89a4e0dbac12f5fffe9e2fd5c08cf7155329..400b7b66e83ce101d7b27474283ca465a2eb6978 100644 (file)
@@ -565,10 +565,6 @@ void RowPainter::paintText()
 
                case Row::SPACE:
                        paintTextDecoration(e);
-                       break;
-
-               case Row::INVALID:
-                       LYXERR0("Trying to paint INVALID row element.");
                }
 
                // The markings of foreign languages
index d681a633560d8454f479b75c4dc34e45ae6e4953..76e2fb117067af081a24c1753b5456255cf47f1b 100644 (file)
@@ -1059,7 +1059,7 @@ Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl)
 }
 
 
-void cleanupRow(Row & row, pos_type real_endpos, bool is_rtl)
+void cleanupRow(Row & row, bool at_end, bool is_rtl)
 {
        if (row.empty()) {
                row.endpos(0);
@@ -1067,11 +1067,12 @@ void cleanupRow(Row & row, pos_type real_endpos, bool is_rtl)
        }
 
        row.endpos(row.back().endpos);
+       row.flushed(at_end);
        // remove trailing spaces on row break
-       if (row.endpos() < real_endpos)
+       if (!at_end)
                row.back().rtrim();
        // boundary exists when there was no space at the end of row
-       row.right_boundary(row.endpos() < real_endpos && row.back().endpos == row.endpos());
+       row.right_boundary(!at_end && row.back().endpos == row.endpos());
        // make sure that the RTL elements are in reverse ordering
        row.reverseRTL(is_rtl);
 }
@@ -1098,6 +1099,8 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
        RowList rows;
        bool const is_rtl = text_->isRTL(bigrow.pit());
        bool const end_label = text_->getEndLabel(bigrow.pit()) != END_LABEL_NO_LABEL;
+       int const next_width = max_width_ - leftMargin(bigrow.pit(), bigrow.endpos())
+               - rightMargin(bigrow.pit());
 
        int width = 0;
        flexible_const_iterator<Row> fcit = flexible_begin(bigrow);
@@ -1115,7 +1118,7 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
                                             : fcit->row_flags;
                if (rows.empty() || needsRowBreak(f1, f2)) {
                        if (!rows.empty())
-                               cleanupRow(rows.back(), bigrow.endpos(), is_rtl);
+                               cleanupRow(rows.back(), false, is_rtl);
                        pos_type pos = rows.empty() ? 0 : rows.back().endpos();
                        rows.push_back(newRow(*this, bigrow.pit(), pos, is_rtl));
                        // the width available for the row.
@@ -1130,45 +1133,26 @@ RowList TextMetrics::breakParagraph(Row const & bigrow) const
                // Next element to consider is either the top of the temporary
                // pile, or the place when we were in main row
                Row::Element elt = *fcit;
-               Row::Element next_elt = elt.splitAt(width - rows.back().width(),
-                                                   !elt.font.language()->wordWrap());
-               if (elt.dim.wid > width - rows.back().width()) {
-                       Row & rb = rows.back();
-                       rb.push_back(*fcit);
-                       // if the row is too large, try to cut at last separator. In case
-                       // of success, reset indication that the row was broken abruptly.
-                       int const next_width = max_width_ - leftMargin(rb.pit(), rb.endpos())
-                               - rightMargin(rb.pit());
-
-                       Row::Elements next_elts = rb.shortenIfNeeded(width, next_width);
-
-                       // Go to next element
-                       ++fcit;
-
-                       // Handle later the elements returned by shortenIfNeeded.
-                       if (!next_elts.empty()) {
-                               rb.flushed(false);
-                               fcit.put(next_elts);
-                       }
-               } else {
-                       // a new element in the row
-                       rows.back().push_back(elt);
-                       rows.back().finalizeLast();
-
-                       // Go to next element
-                       ++fcit;
-
-                       // Add a new next element on the pile
-                       if (next_elt.isValid()) {
-                               // do as if we inserted this element in the original row
-                               if (!next_elt.str.empty())
-                                       fcit.put(next_elt);
-                       }
+               Row::Elements tail;
+               elt.splitAt(width - rows.back().width(), next_width, false, tail);
+               Row & rb = rows.back();
+               rb.push_back(elt);
+               rb.finalizeLast();
+               if (rb.width() > width) {
+                       LATTEST(tail.empty());
+                       // if the row is too large, try to cut at last separator.
+                       tail = rb.shortenIfNeeded(width, next_width);
                }
+
+               // Go to next element
+               ++fcit;
+
+               // Handle later the elements returned by splitAt or shortenIfNeeded.
+               fcit.put(tail);
        }
 
        if (!rows.empty()) {
-               cleanupRow(rows.back(), bigrow.endpos(), is_rtl);
+               cleanupRow(rows.back(), true, is_rtl);
                // Last row in paragraph is flushed
                rows.back().flushed(true);
        }
index b562ebfac18fa03ee1042d0fb9951c9895ac8659..b78bc2dbcd083c6c62443133173bf7b3c5809e5e 100644 (file)
@@ -16,6 +16,8 @@
 
 #include "support/strfwd.h"
 
+#include <vector>
+
 /**
  * A class holding helper functions for determining
  * the screen dimensions of fonts.
@@ -121,15 +123,27 @@ public:
         * \param ws is the amount of extra inter-word space applied text justification.
         */
        virtual int x2pos(docstring const & s, int & x, bool rtl, double ws) const = 0;
+
+       // The places where to break a string and the width of the resulting lines.
+       struct Break {
+               Break(int l, int w) : len(l), wid(w) {}
+               int len = 0;
+               int wid = 0;
+       };
+       typedef std::vector<Break> Breaks;
        /**
-        * Break string s at width at most x.
-        * \return break position (-1 if not successful)
-        * \param position x is updated to real width
-        * \param rtl is true for right-to-left layout
+        * Break a string in multiple fragments according to width limits.
+        * \return a sequence of Break elements.
+        * \param s is the string to break.
+        * \param first_wid is the available width for first line.
+        * \param wid is the available width for the next lines.
+        * \param rtl is true for right-to-left layout.
         * \param force is false for breaking at word separator, true for
         *   arbitrary position.
         */
-       virtual int breakAt(docstring const & s, int & x, bool rtl, bool force) const = 0;
+       virtual Breaks
+       breakString(docstring const & s, int first_wid, int wid, bool rtl, bool force) const = 0;
+
        /// return char dimension for the font.
        virtual Dimension const dimension(char_type c) const = 0;
        /**
index 47537ae0a37f548ee00c660863fbb699465e8c24..3feb33e6a85444ca542fb44e1fcdcc0704c7e11a 100644 (file)
@@ -19,6 +19,7 @@
 
 #include "support/convert.h"
 #include "support/lassert.h"
+#include "support/lstrings.h"
 #include "support/lyxlib.h"
 #include "support/debug.h"
 
@@ -86,14 +87,14 @@ namespace frontend {
 
 
 /*
- * Limit (strwidth|breakat)_cache_ size to 512kB of string data.
+ * Limit (strwidth|breakstr)_cache_ size to 512kB of string data.
  * Limit qtextlayout_cache_ size to 500 elements (we do not know the
  * size of the QTextLayout objects anyway).
  * Note that all these numbers are arbitrary.
  * Also, setting size to 0 is tantamount to disabling the cache.
  */
 int cache_metrics_width_size = 1 << 19;
-int cache_metrics_breakat_size = 1 << 19;
+int cache_metrics_breakstr_size = 1 << 19;
 // Qt 5.x already has its own caching of QTextLayout objects
 // but it does not seem to work well on MacOS X.
 #if (QT_VERSION < 0x050000) || defined(Q_OS_MAC)
@@ -128,7 +129,7 @@ inline QChar const ucs4_to_qchar(char_type const ucs4)
 GuiFontMetrics::GuiFontMetrics(QFont const & font)
        : font_(font), metrics_(font, 0),
          strwidth_cache_(cache_metrics_width_size),
-         breakat_cache_(cache_metrics_breakat_size),
+         breakstr_cache_(cache_metrics_breakstr_size),
          qtextlayout_cache_(cache_metrics_qtextlayout_size)
 {
        // Determine italic slope
@@ -485,11 +486,13 @@ int GuiFontMetrics::countExpanders(docstring const & str) const
 }
 
 
-pair<int, int>
-GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
-                               bool const rtl, bool const force) const
+namespace {
+
+const int brkStrOffset = 1 + BIDI_OFFSET;
+
+
+QString createBreakableString(docstring const & s, bool rtl, QTextLayout & tl)
 {
-       QTextLayout tl;
        /* Qt will not break at a leading or trailing space, and we need
         * that sometimes, see http://www.lyx.org/trac/ticket/9921.
         *
@@ -518,34 +521,23 @@ GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
                // Left-to-right override: forces to draw text left-to-right
                qs =  QChar(0x202D) + qs;
 #endif
-       int const offset = 1 + BIDI_OFFSET;
+       return qs;
+}
 
-       tl.setText(qs);
-       tl.setFont(font_);
-       QTextOption to;
-       to.setWrapMode(force ? QTextOption::WrapAtWordBoundaryOrAnywhere
-                            : QTextOption::WordWrap);
-       // Let QTextLine::naturalTextWidth() account for trailing spaces
-       // (horizontalAdvance() still does not).
-       to.setFlags(QTextOption::IncludeTrailingSpaces);
-       tl.setTextOption(to);
-       tl.beginLayout();
-       QTextLine line = tl.createLine();
-       line.setLineWidth(x);
-       tl.createLine();
-       tl.endLayout();
-       int line_wid = iround(line.horizontalAdvance());
-       if ((force && line.textLength() == offset) || line_wid > x)
-               return {-1, line_wid};
+
+docstring::size_type brkstr2str_pos(QString brkstr, docstring const & str, int pos)
+{
        /* Since QString is UTF-16 and docstring is UCS-4, the offsets may
         * not be the same when there are high-plan unicode characters
         * (bug #10443).
         */
-       // The variable `offset' is here to account for the extra leading characters.
+       // The variable `brkStrOffset' is here to account for the extra leading characters.
        // The ending character zerow_nbsp has to be ignored if the line is complete.
-       int const qlen = line.textLength() - offset - (line.textLength() == qs.length());
+       int const qlen = pos - brkStrOffset - (pos == brkstr.length());
 #if QT_VERSION < 0x040801 || QT_VERSION >= 0x050100
-       int len = qstring_to_ucs4(qs.mid(offset, qlen)).length();
+       auto const len = qstring_to_ucs4(brkstr.mid(brkStrOffset, qlen)).length();
+       // Avoid warning
+       (void)str;
 #else
        /* Due to QTBUG-25536 in 4.8.1 <= Qt < 5.1.0, the string returned
         * by QString::toUcs4 (used by qstring_to_ucs4) may have wrong
@@ -555,52 +547,108 @@ GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
         * worthwhile to implement a dichotomy search if this shows up
         * under a profiler.
         */
-       int len = min(qlen, static_cast<int>(s.length()));
-       while (len >= 0 && toqstr(s.substr(0, len)).length() != qlen)
+       int len = min(qlen, static_cast<int>(str.length()));
+       while (len >= 0 && toqstr(str.substr(0, len)).length() != qlen)
                --len;
        LASSERT(len > 0 || qlen == 0, /**/);
 #endif
-       // Do not cut is the string is already short enough. We rely on
-       // naturalTextWidth() to catch the case where we cut at the trailing
-       // space.
-       if (len == static_cast<int>(s.length())
-               && line.naturalTextWidth() <= x) {
-               len = -1;
-#if QT_VERSION < 0x050000
+       return len;
+}
+
+}
+
+FontMetrics::Breaks
+GuiFontMetrics::breakString_helper(docstring const & s, int first_wid, int wid,
+                                   bool rtl, bool force) const
+{
+       QTextLayout tl;
+       QString qs = createBreakableString(s, rtl, tl);
+       tl.setText(qs);
+       tl.setFont(font_);
+       QTextOption to;
+       /*
+        * Some Asian languages split lines anywhere (no notion of
+        * word). It seems that QTextLayout is not aware of this fact.
+        * See for reference:
+        *    https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages
+        *
+        * FIXME: Something shall be done about characters which are
+        * not allowed at the beginning or end of line.
+        */
+       to.setWrapMode(force ? QTextOption::WrapAtWordBoundaryOrAnywhere
+                            : QTextOption::WordWrap);
+       // Let QTextLine::naturalTextWidth() account for trailing spaces
+       // (horizontalAdvance() still does not).
+       to.setFlags(QTextOption::IncludeTrailingSpaces);
+       tl.setTextOption(to);
+
+       bool first = true;
+       tl.beginLayout();
+       while(true) {
+               QTextLine line = tl.createLine();
+               if (!line.isValid())
+                       break;
+               line.setLineWidth(first ? first_wid : wid);
+               tl.createLine();
+               first = false;
+       }
+       tl.endLayout();
+
+       Breaks breaks;
+       int pos = 0;
+       for (int i = 0 ; i < tl.lineCount() ; ++i) {
+               QTextLine const & line = tl.lineAt(i);
+               int const epos = brkstr2str_pos(qs, s, line.textStart() + line.textLength());
+#if QT_VERSION >= 0x050000
+               int const wid = i + 1 < tl.lineCount() ? iround(line.horizontalAdvance())
+                                                      : iround(line.naturalTextWidth());
+#else
                // With some monospace fonts, the value of horizontalAdvance()
                // can be wrong with Qt4. One hypothesis is that the invisible
                // characters that we use are given a non-null width.
-               line_wid = width(s);
+               // FIXME: this is slower than it could be but we'll get rid of Qt4 anyway
+               int const wid = i + 1 < tl.lineCount() ? width(rtrim(s.substr(pos, epos - pos)))
+                                                      : width(s.substr(pos, epos - pos));
+#endif
+               breaks.emplace_back(epos - pos, wid);
+               pos = epos;
+#if 0
+               // FIXME: should it be kept in some form?
+               if ((force && line.textLength() == brkStrOffset) || line_wid > x)
+                       return {-1, line_wid};
 #endif
+
        }
-       return {len, line_wid};
+
+       return breaks;
 }
 
 
-uint qHash(BreakAtKey const & key)
+uint qHash(BreakStringKey const & key)
 {
-       int params = key.force + 2 * key.rtl + 4 * key.x;
+       // assume widths are less than 10000. This fits in 32 bits.
+       uint params = key.force + 2 * key.rtl + 4 * key.first_wid + 10000 * key.wid;
        return std::qHash(key.s) ^ ::qHash(params);
 }
 
 
-int GuiFontMetrics::breakAt(docstring const & s, int & x, bool const rtl, bool const force) const
+FontMetrics::Breaks GuiFontMetrics::breakString(docstring const & s, int first_wid, int wid,
+                                                bool rtl, bool force) const
 {
-       PROFILE_THIS_BLOCK(breakAt);
+       PROFILE_THIS_BLOCK(breakString);
        if (s.empty())
-               return false;
+               return Breaks();
 
-       BreakAtKey key{s, x, rtl, force};
-       pair<int, int> pp;
-       if (auto * pp_ptr = breakat_cache_.object_ptr(key))
-               pp = *pp_ptr;
+       BreakStringKey key{s, first_wid, wid, rtl, force};
+       Breaks brks;
+       if (auto * brks_ptr = breakstr_cache_.object_ptr(key))
+               brks = *brks_ptr;
        else {
-               PROFILE_CACHE_MISS(breakAt);
-               pp = breakAt_helper(s, x, rtl, force);
-               breakat_cache_.insert(key, pp, sizeof(key) + s.size() * sizeof(char_type));
+               PROFILE_CACHE_MISS(breakString);
+               brks = breakString_helper(s, first_wid, wid, rtl, force);
+               breakstr_cache_.insert(key, brks, sizeof(key) + s.size() * sizeof(char_type));
        }
-       x = pp.second;
-       return pp.first;
+       return brks;
 }
 
 
index ef8588a5f81a57ad83688b0dc282ef09e9c506ff..9501eb866d17b891eb4e921dcc2c9013e7ef5b07 100644 (file)
 namespace lyx {
 namespace frontend {
 
-struct BreakAtKey
+struct BreakStringKey
 {
-       bool operator==(BreakAtKey const & key) const {
-               return key.s == s && key.x == x && key.rtl == rtl && key.force == force;
+       bool operator==(BreakStringKey const & key) const {
+               return key.s == s && key.first_wid == first_wid && key.wid == wid
+                       && key.rtl == rtl && key.force == force;
        }
 
        docstring s;
-       int x;
+       int first_wid;
+       int wid;
        bool rtl;
        bool force;
 };
@@ -77,7 +79,7 @@ public:
        int signedWidth(docstring const & s) const override;
        int pos2x(docstring const & s, int pos, bool rtl, double ws) const override;
        int x2pos(docstring const & s, int & x, bool rtl, double ws) const override;
-       int breakAt(docstring const & s, int & x, bool rtl, bool force) const override;
+       Breaks breakString(docstring const & s, int first_wid, int wid, bool rtl, bool force) const override;
        Dimension const dimension(char_type c) const override;
 
        void rectText(docstring const & str,
@@ -101,8 +103,8 @@ public:
 
 private:
 
-       std::pair<int, int> breakAt_helper(docstring const & s, int const x,
-                                          bool const rtl, bool const force) const;
+       Breaks breakString_helper(docstring const & s, int first_wid, int wid,
+                                 bool rtl, bool force) const;
 
        /// The font
        QFont font_;
@@ -117,8 +119,8 @@ private:
        mutable QHash<char_type, int> width_cache_;
        /// Cache of string widths
        mutable Cache<docstring, int> strwidth_cache_;
-       /// Cache for breakAt
-       mutable Cache<BreakAtKey, std::pair<int, int>> breakat_cache_;
+       /// Cache for breakString
+       mutable Cache<BreakStringKey, Breaks> breakstr_cache_;
        /// Cache for QTextLayout
        mutable Cache<TextLayoutKey, std::shared_ptr<QTextLayout>> qtextlayout_cache_;