Handle multiple spaces at row break

In order to work around the Qt row breaking algorithm, which considers
multiple spaces as one at QTextLine break, we insert word_joiner unicode
characters beteween each pair of spaces.

The TextLayoutHelper class makes it easy to handle that.

Update Row::Element::rtrim() to only remove one space at row end.

Update support::countExpanders() to count all spaces, without special
handling for consecutive ones.

Fixes bug #10117.
This commit is contained in:
Jean-Marc Lasgouttes 2022-07-11 23:56:35 +02:00
parent a48efa03b1
commit 201c95a76e
3 changed files with 22 additions and 31 deletions

View File

@ -27,6 +27,7 @@
#include "support/lassert.h" #include "support/lassert.h"
#include "support/lstrings.h" #include "support/lstrings.h"
#include "support/lyxlib.h" #include "support/lyxlib.h"
#include "support/textutils.h"
#include <algorithm> #include <algorithm>
#include <ostream> #include <ostream>
@ -203,14 +204,13 @@ bool Row::Element::splitAt(int const width, int next_width, bool force,
void Row::Element::rtrim() void Row::Element::rtrim()
{ {
if (type != STRING) if (type != STRING || str.empty() || !isSpace(str.back()))
return; return;
/* This is intended for strings that have been created by splitAt. /* This is intended for strings that have been created by splitAt.
* They may have trailing spaces, but they are not counted in the * If There is a trailing space, we remove it and decrease endpos,
* string length (QTextLayout feature, actually). We remove them, * since spaces at row break are invisible.
* and decrease endpos, since spaces at row break are invisible.
*/ */
str = support::rtrim(str); str.pop_back();
endpos = pos + str.length(); endpos = pos + str.length();
dim.wid = nspc_wid; dim.wid = nspc_wid;
} }

View File

@ -20,8 +20,8 @@
#include "support/convert.h" #include "support/convert.h"
#include "support/debug.h" #include "support/debug.h"
#include "support/lassert.h" #include "support/lassert.h"
#include "support/lstrings.h" // for breakString_helper with qt4
#include "support/lyxlib.h" #include "support/lyxlib.h"
#include "support/textutils.h"
#define DISABLE_PMPROF #define DISABLE_PMPROF
#include "support/pmprof.h" #include "support/pmprof.h"
@ -411,7 +411,13 @@ TextLayoutHelper::TextLayoutHelper(docstring const & s, bool isrtl, bool naked)
#endif #endif
// Now translate the string character-by-character. // Now translate the string character-by-character.
bool was_space = false;
for (char_type const c : s) { for (char_type const c : s) {
// insert a word joiner character between consecutive spaces
bool const is_space = isSpace(c);
if (!naked && is_space && was_space)
qstr += word_joiner;
was_space = is_space;
// Remember the QString index at this point // Remember the QString index at this point
pos2qpos_.push_back(qstr.size()); pos2qpos_.push_back(qstr.size());
// Performance: UTF-16 characters are easier // Performance: UTF-16 characters are easier
@ -599,27 +605,19 @@ GuiFontMetrics::breakString_helper(docstring const & s, int first_wid, int wid,
// If the line is not the last one, trailing space is always omitted. // If the line is not the last one, trailing space is always omitted.
int nspc_wid = wid; int nspc_wid = wid;
// For the last line, compute the width without trailing space // For the last line, compute the width without trailing space
if (i + 1 == tl.lineCount()) { if (i + 1 == tl.lineCount() && !s.empty() && isSpace(s.back())
// trim_pos points to the last character that is not a space && line.textStart() <= tlh.pos2qpos(s.size() - 1))
auto trim_pos = s.find_last_not_of(from_ascii(" ")); nspc_wid = iround(line.cursorToX(tlh.pos2qpos(s.size() - 1)));
if (trim_pos == docstring::npos)
nspc_wid = 0;
else if (trim_pos + 1 < s.length()) {
int const num_spaces = s.length() - trim_pos - 1;
// find the position on the line before trailing
// spaces. Remove 1 to account for the ending
// non-breaking space of qs.
nspc_wid = iround(line.cursorToX(line_epos - num_spaces - 1));
}
}
#else #else
// With some monospace fonts, the value of horizontalAdvance() // With some monospace fonts, the value of horizontalAdvance()
// can be wrong with Qt4. One hypothesis is that the invisible // can be wrong with Qt4. One hypothesis is that the invisible
// characters that we use are given a non-null width. // characters that we use are given a non-null width.
// FIXME: this is slower than it could be but we'll get rid of Qt4 anyway // FIXME: this is slower than it could be but we'll get rid of Qt4 anyway
docstring const ss = s.substr(pos, epos - pos); docstring ss = s.substr(pos, epos - pos);
int const wid = width(ss); int const wid = width(ss);
int const nspc_wid = i + 1 < tl.lineCount() ? width(rtrim(ss)) : wid; if (!ss.empty() && isSpace(ss.back()))
ss.pop_back();
int const nspc_wid = i + 1 < tl.lineCount() ? width(ss) : wid;
#endif #endif
breaks.emplace_back(epos - pos, wid, nspc_wid); breaks.emplace_back(epos - pos, wid, nspc_wid);
pos = epos; pos = epos;

View File

@ -1507,18 +1507,11 @@ int countExpanders(docstring const & str)
// Numbers of characters that are expanded by inter-word spacing. These // Numbers of characters that are expanded by inter-word spacing. These
// characters are spaces, except for characters 09-0D which are treated // characters are spaces, except for characters 09-0D which are treated
// specially. (From a combination of testing with the notepad found in qt's // specially. (From a combination of testing with the notepad found in qt's
// examples, and reading the source code.) In addition, consecutive spaces // examples, and reading the source code.)
// only count as one expander.
bool wasspace = false;
int nexp = 0; int nexp = 0;
for (char_type c : str) for (char_type c : str)
if (c > 0x0d && isSpace(c)) { if (c > 0x0d && isSpace(c))
if (!wasspace) {
++nexp; ++nexp;
wasspace = true;
}
} else
wasspace = false;
return nexp; return nexp;
} }