mirror of
https://git.lyx.org/repos/lyx.git
synced 2024-11-23 02:14:50 +00:00
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 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.
This commit is contained in:
parent
71c2e2fda6
commit
d723b90344
156
src/Row.cpp
156
src/Row.cpp
@ -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;
|
||||
}
|
||||
|
||||
return Element();
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
33
src/Row.h
33
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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -27,14 +27,16 @@
|
||||
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_;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user