[LyX features/breakrows] Break multi-row strings in one pass
Jean-Marc Lasgouttes
lasgouttes at lyx.org
Mon Sep 20 15:38:05 UTC 2021
The branch, breakrows, has been updated.
- Log -----------------------------------------------------------------
commit 8006aa710f2bc52082538f1c37252cc98b2b5c46
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date: Mon Sep 20 17:42:45 2021 +0200
Break multi-row strings in one pass
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 vector of Element. Note that,
beyond the two widthes noted above, a new parameter wrap_any has beeen
added in addition to force.
In Row::finalize, remove the code that computed inconditionnally the
current element size, and make sure that this width will be compute
din all code paths of Row::Element::splitAt.
diff --git a/src/Row.cpp b/src/Row.cpp
index fe829b5..2b27f74 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -129,38 +129,72 @@ pos_type Row::Element::x2pos(int &x) const
}
-Row::Elements Row::Element::splitAt(int w, bool force)
+Row::Elements Row::Element::splitAt(int const width, int next_width, bool wrap_any, bool force)
{
- if (type != STRING || !(row_flags & CanBreakInside))
+ // Not a string or already OK.
+ if (type != STRING || (dim.wid > 0 && dim.wid < width))
return Elements();
FontMetrics const & fm = theFontMetrics(font);
- dim.wid = w;
- int const i = fm.breakAt(str, dim.wid, isRTL(), force);
- // No need to break
- if (i == -1)
+ // 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 Elements();
+ }
+
+ 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 Elements();
+ }
+ Element first_e(STRING, pos, font, change);
+ // should next element eventually replace *this?
+ bool first = true;
Elements tail;
- // Is there a second element to return?
- if (i < int(str.length())) {
- 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);
- tail.push_back(ret);
-
- // Now update ourselves
- str.erase(i);
- endpos = pos + i;
+ 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;
}
- // Row should be broken after the original element
- row_flags = (row_flags & ~AfterFlags) | BreakAfter;
- //LYXERR0("splitAt(" << w << ") Row element Broken at " << w << "(w(str)=" << fm.width(str) << "): e=" << *this);
+ 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 tail;
}
@@ -393,11 +427,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;
- }
}
@@ -545,12 +574,12 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
*/
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.
*/
- tail = brk.splitAt(min(w - wid_brk, brk.dim.wid - 2), !word_wrap);
+ tail = brk.splitAt(min(w - wid_brk, brk.dim.wid - 2), next_width, !word_wrap, false);
if (brk.row_flags & BreakAfter) {
/* if this element originally did not cause a row overflow
* in itself, and the remainder of the row would still be
@@ -587,10 +616,10 @@ Row::Elements Row::shortenIfNeeded(int const w, int const next_width)
}
/* 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.
*/
- tail = cit->splitAt(w - wid, true);
+ tail = cit->splitAt(w - wid, next_width, !cit->font.language()->wordWrap(), true);
if (cit->row_flags & BreakAfter) {
end_ = cit->endpos;
dim_.wid = wid + cit->dim.wid;
diff --git a/src/Row.h b/src/Row.h
index 0cc4879..7d8caba 100644
--- a/src/Row.h
+++ b/src/Row.h
@@ -92,12 +92,16 @@ public:
* than \param w.
* \return a vector of elements containing the remainder of
* the text (empty 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.
+ * \param width maximum width of the row.
+ * \param next_width available width on next row.
+ * \param wrap_any: if true, the string is cut at any place,
+ * otherwise it respects the row breaking rules of
+ * characters.
+ * \param force: if false, do not break at all if first
+ * element would larger than \c width.
*/
// FIXME: ideally return type should be Elements, but it is not possible.
- std::vector<Element> splitAt(int w, bool force);
+ std::vector<Element> splitAt(int width, int next_width, bool wrap_any, bool force);
// remove trailing spaces (useful for end of row)
void rtrim();
@@ -284,8 +288,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);
diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp
index f70f11a..610f03b 100644
--- a/src/TextMetrics.cpp
+++ b/src/TextMetrics.cpp
@@ -1098,6 +1098,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);
@@ -1130,18 +1132,14 @@ 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::Elements tail = elt.splitAt(width - rows.back().width(),
- !elt.font.language()->wordWrap());
+ Row::Elements tail = elt.splitAt(width - rows.back().width(), next_width,
+ !elt.font.language()->wordWrap(), false);
Row & rb = rows.back();
rb.push_back(elt);
rb.finalizeLast();
- if (rows.back().width() > width) {
+ if (rb.width() > width) {
LATTEST(tail.empty());
- // 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());
-
+ // if the row is too large, try to cut at last separator.
tail = rb.shortenIfNeeded(width, next_width);
}
diff --git a/src/frontends/FontMetrics.h b/src/frontends/FontMetrics.h
index b562ebf..b78bc2d 100644
--- a/src/frontends/FontMetrics.h
+++ b/src/frontends/FontMetrics.h
@@ -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;
/**
diff --git a/src/frontends/qt/GuiFontMetrics.cpp b/src/frontends/qt/GuiFontMetrics.cpp
index 83c2c0a..f7f41b1 100644
--- a/src/frontends/qt/GuiFontMetrics.cpp
+++ b/src/frontends/qt/GuiFontMetrics.cpp
@@ -19,6 +19,7 @@
#include "support/convert.h"
#include "support/lassert.h"
+#include "support/lstrings.h"
#include "support/lyxlib.h"
#include "support/debug.h"
@@ -77,14 +78,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)
@@ -119,7 +120,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
@@ -471,11 +472,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.
*
@@ -504,34 +507,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
@@ -541,47 +533,94 @@ 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;
+ 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;
}
-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();
docstring const s_cache =
- s + convert<docstring>(x) + (rtl ? "r" : "l") + (force ? "f" : "w");
- pair<int, int> pp;
+ s + ":" + convert<docstring>(first_wid) + ":" + convert<docstring>(wid)
+ + (rtl ? "r" : "l") + (force ? "f" : "w");
+ Breaks brks;
- if (breakat_cache_.contains(s_cache))
- pp = breakat_cache_[s_cache];
+ if (breakstr_cache_.contains(s_cache))
+ brks = breakstr_cache_[s_cache];
else {
- PROFILE_CACHE_MISS(breakAt);
- pp = breakAt_helper(s, x, rtl, force);
- breakat_cache_.insert(s_cache, pp, s_cache.size() * sizeof(char_type));
+ PROFILE_CACHE_MISS(breakString);
+ brks = breakString_helper(s, first_wid, wid, rtl, force);
+ breakstr_cache_.insert(s_cache, brks, s_cache.size() * sizeof(char_type));
}
- x = pp.second;
- return pp.first;
+ return brks;
}
diff --git a/src/frontends/qt/GuiFontMetrics.h b/src/frontends/qt/GuiFontMetrics.h
index 70bb7f6..6999df6 100644
--- a/src/frontends/qt/GuiFontMetrics.h
+++ b/src/frontends/qt/GuiFontMetrics.h
@@ -53,7 +53,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,
@@ -77,8 +77,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_;
@@ -93,8 +93,8 @@ private:
mutable QHash<char_type, int> width_cache_;
/// Cache of string widths
mutable Cache<docstring, int> strwidth_cache_;
- /// Cache for breakAt
- mutable Cache<docstring, std::pair<int, int>> breakat_cache_;
+ /// Cache for breakString
+ mutable Cache<docstring, Breaks> breakstr_cache_;
/// Cache for QTextLayout
mutable Cache<docstring, std::shared_ptr<QTextLayout>> qtextlayout_cache_;
commit 68c4867ba2f1de9489b9e20e2f708ede7111701e
Author: Jean-Marc Lasgouttes <lasgouttes at lyx.org>
Date: Mon Sep 20 17:32:18 2021 +0200
Add operator<< for Row::Elements
This is useful for debugging.
diff --git a/src/Row.cpp b/src/Row.cpp
index 093f280..fe829b5 100644
--- a/src/Row.cpp
+++ b/src/Row.cpp
@@ -275,6 +275,17 @@ ostream & operator<<(ostream & os, Row::Element const & e)
}
+ostream & operator<<(ostream & os, Row::Elements const & elts)
+{
+ double x = 0;
+ for (Row::Element const & e : elts) {
+ os << "x=" << x << " => " << e << endl;
+ x += e.full_width();
+ }
+ return os;
+}
+
+
ostream & operator<<(ostream & os, Row const & row)
{
os << " pos: " << row.pos_ << " end: " << row.end_
@@ -286,11 +297,11 @@ ostream & operator<<(ostream & os, Row const & row)
<< " separator: " << row.separator
<< " label_hfill: " << row.label_hfill
<< " row_boundary: " << row.right_boundary() << "\n";
+ // We cannot use the operator above, unfortunately
double x = row.left_margin;
- Row::Elements::const_iterator it = row.elements_.begin();
- for ( ; it != row.elements_.end() ; ++it) {
- os << "x=" << x << " => " << *it << endl;
- x += it->full_width();
+ for (Row::Element const & e : row.elements_) {
+ os << "x=" << x << " => " << e << endl;
+ x += e.full_width();
}
return os;
}
diff --git a/src/Row.h b/src/Row.h
index 552c2fe..0cc4879 100644
--- a/src/Row.h
+++ b/src/Row.h
@@ -373,6 +373,8 @@ private:
bool changebar_ = false;
};
+std::ostream & operator<<(std::ostream & os, Row::Elements const & elts);
+
/**
* Each paragraph is broken up into a number of rows on the screen.
-----------------------------------------------------------------------
Summary of changes:
src/Row.cpp | 110 ++++++++++++++++++---------
src/Row.h | 18 +++--
src/TextMetrics.cpp | 14 ++--
src/frontends/FontMetrics.h | 24 +++++-
src/frontends/qt/GuiFontMetrics.cpp | 141 ++++++++++++++++++++++-------------
src/frontends/qt/GuiFontMetrics.h | 10 +-
6 files changed, 207 insertions(+), 110 deletions(-)
hooks/post-receive
--
Repository for new features
More information about the lyx-cvs
mailing list