On-screen justification: stretch in proportion with the em, up to a limit

1) Distinguish expanding characters from separators, to fit with Qt's notion of
expanding character which comes from the Unicode std.  CountExpanders() is moved
to FontMetrics to fix a discrepancy with the duplicate implementation from
598f7e4a.

2) Make these expanders stretch on-screen proportionally to the em of the font.
If a row mixes large and small text, LyX let us see which spaces are set in the
bigger font.

3) Now that the stretch is defined in ems, add a limit such that an expander
never stretches more than 1.5em to avoid weird and hard to read justified lines.

4) Add a return boolean to setSeparatorExtraWidth for future use.
This commit is contained in:
Guillaume Munch 2016-08-13 19:03:02 +01:00
parent 9651879253
commit b30f8d3c4b
7 changed files with 102 additions and 34 deletions

View File

@ -37,23 +37,39 @@ using support::rtrim;
using frontend::FontMetrics;
// Maximum length that a space can be stretched when justifying text
static double const MAX_SPACE_STRETCH = 1.5; //em
int Row::Element::countSeparators() const
{
if (type != STRING)
return 0;
// Consecutive spaces count as only one separator.
bool wasspace = false;
int nsep = 0;
for (size_t i = 0 ; i < str.size() ; ++i) {
if (str[i] == ' ') {
if (!wasspace) {
++nsep;
wasspace = true;
}
} else
wasspace = false;
}
return nsep;
return count(str.begin(), str.end(), ' ');
}
int Row::Element::countExpanders() const
{
if (type != STRING)
return 0;
return theFontMetrics(font).countExpanders(str);
}
int Row::Element::expansionAmount() const
{
if (type != STRING)
return 0;
return countExpanders() * theFontMetrics(font).em();
}
void Row::Element::setExtra(double extra_per_em)
{
if (type != STRING)
return;
extra = extra_per_em * theFontMetrics(font).em();
}
@ -234,7 +250,7 @@ ostream & operator<<(ostream & os, Row::Element const & e)
switch (e.type) {
case Row::STRING:
os << "STRING: `" << to_utf8(e.str) << "' ("
<< e.countSeparators() << " sep.), ";
<< e.countExpanders() << " expanders.), ";
break;
case Row::VIRTUAL:
os << "VIRTUAL: `" << to_utf8(e.str) << "', ";
@ -311,13 +327,28 @@ int Row::countSeparators() const
}
void Row::setSeparatorExtraWidth(double w)
bool Row::setExtraWidth(int w)
{
separator = w;
iterator const end = elements_.end();
for (iterator it = elements_.begin() ; it != end ; ++it)
if (it->type == Row::STRING)
it->extra = w;
if (w < 0)
// this is not expected to happen (but it does)
return false;
// amount of expansion: number of expanders time the em value for each
// string element
int exp_amount = 0;
for (Row::Element const & e : elements_)
exp_amount += e.expansionAmount();
// extra length per expander per em
double extra_per_em = double(w) / exp_amount;
if (extra_per_em > MAX_SPACE_STRETCH)
// do not stretch more than MAX_SPACE_STRETCH em per expander
return false;
// add extra length to each element proportionally to its em.
for (Row::Element & e : elements_)
if (e.type == Row::STRING)
e.setExtra(extra_per_em);
// update row dimension
dim_.wid += w;
return true;
}

View File

@ -61,12 +61,22 @@ public:
: type(t), pos(p), endpos(p + 1), inset(0),
extra(0), font(f), change(ch), final(false) {}
// Return total width of element, including separator overhead
// FIXME: Cache this value or the number of separators?
double full_width() const { return dim.wid + extra * countSeparators(); }
// Return the number of separator in the element (only STRING type)
int countSeparators() const;
// Return total width of element, including separator overhead
// FIXME: Cache this value or the number of expanders?
double full_width() const { return dim.wid + extra * countExpanders(); }
// Return the number of expanding characters in the element (only STRING
// type).
int countExpanders() const;
// Return the amount of expansion: the number of expanding characters
// that get stretched during justification, times the em of the font
// (only STRING type).
int expansionAmount() const;
// set extra proportionally to the font em value.
void setExtra(double extra_per_em);
/** Return position in pixels (from the left) of position
* \param i in the row element.
*/
@ -182,8 +192,10 @@ public:
// Return the number of separators in the row
int countSeparators() const;
// Set the extra spacing for every separator in STRING elements
void setSeparatorExtraWidth(double w);
// Set the extra spacing for every expanding character in STRING-type
// elements. \param w is the total amount of extra width for the row to be
// distributed among expanders. \return false if the justification fails.
bool setExtraWidth(int w);
///
void add(pos_type pos, Inset const * ins, Dimension const & dim,

View File

@ -625,18 +625,14 @@ void TextMetrics::computeRowMetrics(Row & row, int width) const
// Common case : there is no hfill, and the alignment will be
// meaningful
switch (getAlign(par, row)) {
case LYX_ALIGN_BLOCK: {
int const ns = row.countSeparators();
// If we have separators, then stretch the row
if (ns) {
row.setSeparatorExtraWidth(double(w) / ns);
row.dimension().wid += w;
} else if (text_->isRTL(par)) {
case LYX_ALIGN_BLOCK:
// Expand expanding characters by a total of w
if (!row.setExtraWidth(w) && text_->isRTL(par)) {
// Justification failed and the text is RTL: align to the right
row.left_margin += w;
row.dimension().wid += w;
}
break;
}
case LYX_ALIGN_RIGHT:
row.left_margin += w;
row.dimension().wid += w;
@ -655,6 +651,7 @@ void TextMetrics::computeRowMetrics(Row & row, int width) const
return;
}
// Case nh > 0. There are hfill separators.
hfill = w / nh;
hfill_rem = w % nh;
row.dimension().wid += w;

View File

@ -142,6 +142,10 @@ public:
inline int center(char_type c) const {
return (rbearing(c) - lbearing(c)) / 2;
}
/// return the number of expanding characters taken into account for
/// increased inter-word spacing during justification
virtual int countExpanders(docstring const & str) const = 0;
};

View File

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

View File

@ -60,6 +60,8 @@ public:
int & width,
int & ascent,
int & descent) const;
int countExpanders(docstring const & str) const;
///
int width(QString const & str) const;

View File

@ -423,7 +423,8 @@ void GuiPainter::text(int x, int y, docstring const & s,
int textwidth = 0;
if (tw == 0.0)
// Note that we have to take in account space stretching (word spacing)
textwidth = fm.width(s) + count(s.begin(), s.end(), ' ') * wordspacing;
textwidth = fm.width(s) +
static_cast<int>(fm.countExpanders(s) * wordspacing);
else
textwidth = static_cast<int>(tw);