]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/GuiFontMetrics.cpp
Stephan has checked that caching is still needed on macOS
[lyx.git] / src / frontends / qt / GuiFontMetrics.cpp
1 /**
2  * \file GuiFontMetrics.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author unknown
7  * \author John Levon
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13
14 #include "GuiFontMetrics.h"
15
16 #include "qt_helpers.h"
17
18 #include "Dimension.h"
19
20 #include "support/convert.h"
21 #include "support/debug.h"
22 #include "support/lassert.h"
23 #include "support/lyxlib.h"
24 #include "support/textutils.h"
25
26 #define DISABLE_PMPROF
27 #include "support/pmprof.h"
28
29 #include <QByteArray>
30 #include <QRawFont>
31 #include <QtEndian>
32
33 #if QT_VERSION >= 0x050100
34 #include <QtMath>
35 #else
36 #define qDegreesToRadians(degree) (degree * (M_PI / 180))
37 #endif
38
39 using namespace std;
40 using namespace lyx::support;
41
42 /* Define what mechanisms are used to enforce text direction. There
43  * are two methods that work with different Qt versions. Here we try
44  * to use both methods together.
45  */
46 // Define to use unicode override characters to force direction
47 #define BIDI_USE_OVERRIDE
48 // Define to use QTextLayout flag to force direction
49 #define BIDI_USE_FLAG
50
51 #if !defined(BIDI_USE_OVERRIDE) && !defined(BIDI_USE_FLAG)
52 #  error "Define at least one of BIDI_USE_OVERRIDE or BIDI_USE_FLAG"
53 #endif
54
55
56 namespace std {
57
58 /*
59  * Argument-dependent lookup implies that this function shall be
60  * declared in the namespace of its argument. But this is std
61  * namespace, since lyx::docstring is just std::basic_string<wchar_t>.
62  */
63 uint qHash(lyx::docstring const & s)
64 {
65         return qHash(QByteArray(reinterpret_cast<char const *>(s.data()),
66                                 s.size() * sizeof(lyx::docstring::value_type)));
67 }
68
69 } // namespace std
70
71 namespace lyx {
72 namespace frontend {
73
74
75 namespace {
76 // Maximal size/cost for various caches. See QCache documentation to
77 // see what cost means.
78
79 // Limit strwidth_cache_ total cost to 1MB of string data.
80 int const strwidth_cache_max_cost = 1024 * 1024;
81 // Limit breakstr_cache_ total cost to 10MB of string data.
82 // This is useful for documents with very large insets.
83 int const breakstr_cache_max_cost = 10 * 1024 * 1024;
84 // Qt 5.x already has its own caching of QTextLayout objects
85 // but it does not seem to work well on MacOS X.
86 #if defined(Q_OS_MAC)
87 // For some reason, the built-in cache of QTextLayout does not work or
88 // exist on macOS.
89 // Limit qtextlayout_cache_ size to 500 elements (we do not know the
90 // size of the QTextLayout objects anyway).
91 int const qtextlayout_cache_max_size = 500;
92 #else
93 // Disable the cache
94 int const qtextlayout_cache_max_size = 0;
95 #endif
96
97
98 /**
99  * Convert a UCS4 character into a QChar.
100  * This is a hack (it does only make sense for the common part of the UCS4
101  * and UTF16 encodings) and should not be used.
102  * This does only exist because of performance reasons (a real conversion
103  * using iconv is too slow on windows).
104  *
105  * This is no real conversion but a simple cast in reality. This is the reason
106  * why this works well for symbol fonts used in mathed too, even though
107  * these are not real ucs4 characters. These are codepoints in the
108  * computer modern fonts used, nothing unicode related.
109  * See comment in GuiPainter::text() for more explanation.
110  **/
111 inline QChar const ucs4_to_qchar(char_type const ucs4)
112 {
113         LATTEST(is_utf16(ucs4));
114         return QChar(static_cast<unsigned short>(ucs4));
115 }
116 } // namespace
117
118
119 GuiFontMetrics::GuiFontMetrics(QFont const & font)
120         : font_(font), metrics_(font, 0),
121           strwidth_cache_(strwidth_cache_max_cost),
122           breakstr_cache_(breakstr_cache_max_cost),
123           qtextlayout_cache_(qtextlayout_cache_max_size)
124 {
125         // Determine italic slope
126         double const defaultSlope = tan(qDegreesToRadians(19.0));
127         QRawFont raw = QRawFont::fromFont(font);
128         QByteArray post(raw.fontTable("post"));
129         if (post.length() == 0) {
130                 slope_ = defaultSlope;
131                 LYXERR(Debug::FONT, "Screen font doesn't have 'post' table.");
132         } else {
133                 // post table description:
134                 // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html
135                 int32_t italicAngle = qFromBigEndian(*reinterpret_cast<int32_t *>(post.data() + 4));
136                 double angle = italicAngle / 65536.0; // Fixed-point 16.16 to floating-point
137                 slope_ = -tan(qDegreesToRadians(angle));
138                 // Correct italic fonts with zero slope
139                 if (slope_ == 0.0 && font.italic())
140                         slope_ = defaultSlope;
141                 LYXERR(Debug::FONT, "Italic slope: " << slope_);
142         }
143 }
144
145
146 int GuiFontMetrics::maxAscent() const
147 {
148         return metrics_.ascent();
149 }
150
151
152 int GuiFontMetrics::maxDescent() const
153 {
154         // We add 1 as the value returned by QT is different than X
155         // See http://doc.trolltech.com/2.3/qfontmetrics.html#200b74
156         // FIXME: check this
157         return metrics_.descent() + 1;
158 }
159
160
161 int GuiFontMetrics::em() const
162 {
163         return QFontInfo(font_).pixelSize();
164 }
165
166
167 int GuiFontMetrics::xHeight() const
168 {
169 //      LATTEST(metrics_.xHeight() == ascent('x'));
170         return metrics_.xHeight();
171 }
172
173
174 int GuiFontMetrics::lineWidth() const
175 {
176         return metrics_.lineWidth();
177 }
178
179
180 int GuiFontMetrics::underlinePos() const
181 {
182         return metrics_.underlinePos();
183 }
184
185
186 int GuiFontMetrics::strikeoutPos() const
187 {
188         return metrics_.strikeOutPos();
189 }
190
191
192 bool GuiFontMetrics::italic() const
193 {
194         return font_.italic();
195 }
196
197
198 double GuiFontMetrics::italicSlope() const
199 {
200         return slope_;
201 }
202
203
204 namespace {
205 int const outOfLimitMetric = -10000;
206 }
207
208
209 int GuiFontMetrics::lbearing(char_type c) const
210 {
211         int value = lbearing_cache_.value(c, outOfLimitMetric);
212         if (value != outOfLimitMetric)
213                 return value;
214
215         if (is_utf16(c))
216                 value = metrics_.leftBearing(ucs4_to_qchar(c));
217         else {
218                 // FIXME: QFontMetrics::leftBearing does not support the
219                 //        full unicode range. Once it does, we could use:
220                 // metrics_.leftBearing(toqstr(docstring(1, c)));
221                 value = 0;
222         }
223
224         lbearing_cache_.insert(c, value);
225
226         return value;
227 }
228
229
230 int GuiFontMetrics::rbearing(char_type c) const
231 {
232         int value = rbearing_cache_.value(c, outOfLimitMetric);
233         if (value != outOfLimitMetric)
234                 return value;
235
236         // Qt rbearing is from the right edge of the char's width().
237         if (is_utf16(c)) {
238                 QChar sc = ucs4_to_qchar(c);
239                 value = width(c) - metrics_.rightBearing(sc);
240         } else {
241                 // FIXME: QFontMetrics::leftBearing does not support the
242                 //        full unicode range. Once it does, we could use:
243                 // metrics_.rightBearing(toqstr(docstring(1, c)));
244                 value = width(c);
245         }
246
247         rbearing_cache_.insert(c, value);
248
249         return value;
250 }
251
252
253 int GuiFontMetrics::width(docstring const & s) const
254 {
255         PROFILE_THIS_BLOCK(width);
256         if (int * wid_p = strwidth_cache_.object_ptr(s))
257                 return *wid_p;
258         PROFILE_CACHE_MISS(width);
259         /* Several problems have to be taken into account:
260          * * QFontMetrics::width does not returns a wrong value with Qt5 with
261          *   some arabic text, since the glyph-shaping operations are not
262          *   done (documented in Qt5).
263          * * QTextLayout is broken for single characters with null width
264          *   (like \not in mathed).
265          * * While QTextLine::horizontalAdvance is the right thing to use
266      *   for text strings, it does not give a good result with some
267      *   characters like the \int (gyph 4) of esint.
268
269          * The metrics of some of our math fonts (eg. esint) are such that
270          * QTextLine::horizontalAdvance leads, more or less, in the middle
271          * of a symbol. This is the horizontal position where a subscript
272          * should be drawn, so that the superscript has to be moved rightward.
273          * This is done when the kerning() method of the math insets returns
274          * a positive value. The problem with this choice is that navigating
275          * a formula becomes weird. For example, a selection extends only over
276          * about half of the symbol. In order to avoid this, with our math
277          * fonts we use QTextLine::naturalTextWidth, so that a superscript can
278          * be drawn right after the symbol, and move the subscript leftward by
279          * recording a negative value for the kerning.
280         */
281         int w = 0;
282         // is the string a single character from a math font ?
283         bool const math_char = s.length() == 1 && font_.styleName() == "LyX";
284         if (math_char) {
285                 QString const qs = toqstr(s);
286                 int br_width = metrics_.boundingRect(qs).width();
287 #if QT_VERSION >= 0x050b00
288                 int s_width = metrics_.horizontalAdvance(qs);
289 #else
290                 int s_width = metrics_.width(qs);
291 #endif
292                 // keep value 0 for math chars with width 0
293                 if (s_width != 0)
294                         w = max(br_width, s_width);
295         } else {
296                 QTextLayout tl;
297                 tl.setText(toqstr(s));
298                 tl.setFont(font_);
299                 tl.beginLayout();
300                 QTextLine line = tl.createLine();
301                 tl.endLayout();
302                 w = iround(line.horizontalAdvance());
303         }
304         strwidth_cache_.insert(s, w, s.size() * sizeof(char_type));
305         return w;
306 }
307
308
309 int GuiFontMetrics::width(QString const & ucs2) const
310 {
311         return width(qstring_to_ucs4(ucs2));
312 }
313
314
315 int GuiFontMetrics::signedWidth(docstring const & s) const
316 {
317         if (s.empty())
318                 return 0;
319
320         if (s[0] == '-')
321                 return -width(s.substr(1, s.size() - 1));
322         else
323                 return width(s);
324 }
325
326
327 uint qHash(TextLayoutKey const & key)
328 {
329         double params = (2 * key.rtl - 1) * key.ws;
330         return std::qHash(key.s) ^ ::qHash(params);
331 }
332
333
334 // This holds a translation table between the original string and the
335 // QString that we can use with QTextLayout.
336 struct TextLayoutHelper
337 {
338         /// Create the helper
339         /// \c s is the original string
340         /// \c isrtl is true if the string is right-to-left
341         TextLayoutHelper(docstring const & s, bool isrtl);
342
343         /// translate QString index to docstring index
344         docstring::size_type qpos2pos(int qpos) const
345         {
346                 return lower_bound(pos2qpos_.begin(), pos2qpos_.end(), qpos) - pos2qpos_.begin();
347         }
348
349         /// Translate docstring index to QString index
350         int pos2qpos(docstring::size_type pos) const { return pos2qpos_[pos]; }
351
352         // The original string
353         docstring docstr;
354         // The mirror string
355         QString qstr;
356         // is string right-to-left?
357         bool rtl;
358
359 private:
360         // This vector contains the QString pos for each string position
361         vector<int> pos2qpos_;
362 };
363
364
365 TextLayoutHelper::TextLayoutHelper(docstring const & s, bool isrtl)
366         : docstr(s), rtl(isrtl)
367 {
368         // Reserve memory for performance purpose
369         pos2qpos_.reserve(s.size());
370         qstr.reserve(2 * s.size());
371
372         /* Qt will not break at a leading or trailing space, and we need
373          * that sometimes, see http://www.lyx.org/trac/ticket/9921.
374          *
375          * To work around the problem, we enclose the string between
376          * word joiner characters so that the QTextLayout algorithm will
377          * agree to break the text at these extremal spaces.
378          */
379         // Unicode character WORD JOINER
380         QChar const word_joiner(0x2060);
381         qstr += word_joiner;
382
383 #ifdef BIDI_USE_OVERRIDE
384         /* Unicode override characters enforce drawing direction
385          * Source: http://www.iamcal.com/understanding-bidirectional-text/
386          * Left-to-right override is 0x202d and right-to-left override is 0x202e.
387          */
388         qstr += QChar(rtl ? 0x202e : 0x202d);
389 #endif
390
391         // Now translate the string character-by-character.
392         bool was_space = false;
393         for (char_type const c : s) {
394                 // insert a word joiner character between consecutive spaces
395                 bool const is_space = isSpace(c);
396                 if (is_space && was_space)
397                         qstr += word_joiner;
398                 was_space = is_space;
399                 // Remember the QString index at this point
400                 pos2qpos_.push_back(qstr.size());
401                 // Performance: UTF-16 characters are easier
402                 if (is_utf16(c))
403                         qstr += ucs4_to_qchar(c);
404                 else
405                         qstr += toqstr(c);
406         }
407
408         // Final word joiner (see above)
409         qstr += word_joiner;
410
411         // Add virtual position at the end of the string
412         pos2qpos_.push_back(qstr.size());
413
414         //QString dump = qstr;
415         //LYXERR0("TLH: " << dump.replace(word_joiner, "|").toStdString());
416 }
417
418
419 namespace {
420
421 shared_ptr<QTextLayout>
422 getTextLayout_helper(TextLayoutHelper const & tlh, double const wordspacing,
423                      QFont font)
424 {
425         auto const ptl = make_shared<QTextLayout>();
426         ptl->setCacheEnabled(true);
427         font.setWordSpacing(wordspacing);
428         ptl->setFont(font);
429 #ifdef BIDI_USE_FLAG
430         /* Use undocumented flag to enforce drawing direction
431          * FIXME: This does not work with Qt 5.11 (ticket #11284).
432          */
433         ptl->setFlags(tlh.rtl ? Qt::TextForceRightToLeft : Qt::TextForceLeftToRight);
434 #endif
435         ptl->setText(tlh.qstr);
436
437         ptl->beginLayout();
438         ptl->createLine();
439         ptl->endLayout();
440
441         return ptl;
442 }
443
444 }
445
446 shared_ptr<QTextLayout const>
447 GuiFontMetrics::getTextLayout(TextLayoutHelper const & tlh,
448                               double const wordspacing) const
449 {
450         PROFILE_THIS_BLOCK(getTextLayout_TLH);
451         TextLayoutKey key{tlh.docstr, tlh.rtl, wordspacing};
452         if (auto ptl = qtextlayout_cache_[key])
453                 return ptl;
454         PROFILE_CACHE_MISS(getTextLayout_TLH);
455         auto const ptl = getTextLayout_helper(tlh, wordspacing, font_);
456         qtextlayout_cache_.insert(key, ptl);
457         return ptl;
458 }
459
460
461 shared_ptr<QTextLayout const>
462 GuiFontMetrics::getTextLayout(docstring const & s, bool const rtl,
463                               double const wordspacing) const
464 {
465         PROFILE_THIS_BLOCK(getTextLayout);
466         TextLayoutKey key{s, rtl, wordspacing};
467         if (auto ptl = qtextlayout_cache_[key])
468                 return ptl;
469         PROFILE_CACHE_MISS(getTextLayout);
470         TextLayoutHelper tlh(s, rtl);
471         auto const ptl = getTextLayout_helper(tlh, wordspacing, font_);
472         qtextlayout_cache_.insert(key, ptl);
473         return ptl;
474 }
475
476
477 int GuiFontMetrics::pos2x(docstring const & s, int pos, bool const rtl,
478                           double const wordspacing) const
479 {
480         TextLayoutHelper tlh(s, rtl);
481         auto ptl = getTextLayout(tlh, wordspacing);
482         // pos can be negative, see #10506.
483         int const qpos = tlh.pos2qpos(max(pos, 0));
484         return static_cast<int>(ptl->lineForTextPosition(qpos).cursorToX(qpos));
485 }
486
487
488 int GuiFontMetrics::x2pos(docstring const & s, int & x, bool const rtl,
489                           double const wordspacing) const
490 {
491         TextLayoutHelper tlh(s, rtl);
492         auto ptl = getTextLayout(tlh, wordspacing);
493         QTextLine const & tline = ptl->lineForTextPosition(0);
494         int qpos = tline.xToCursor(x);
495         int newx = static_cast<int>(tline.cursorToX(qpos));
496         // The value of qpos may be wrong in rtl text (see ticket #10569).
497         // To work around this, let's have a look at adjacent positions to
498         // see whether we find closer matches.
499         if (rtl && newx < x) {
500                 while (qpos > 0) {
501                         int const xm = static_cast<int>(tline.cursorToX(qpos - 1));
502                         if (abs(xm - x) < abs(newx - x)) {
503                                 --qpos;
504                                 newx = xm;
505                         } else
506                                 break;
507                 }
508         } else if (rtl && newx > x) {
509                 while (qpos < tline.textLength()) {
510                         int const xp = static_cast<int>(tline.cursorToX(qpos + 1));
511                         if (abs(xp - x) < abs(newx - x)) {
512                                 ++qpos;
513                                 newx = xp;
514                         } else
515                                 break;
516                 }
517         }
518         // correct x value to the actual cursor position.
519         x = newx;
520
521         return tlh.qpos2pos(qpos);
522 }
523
524
525 FontMetrics::Breaks
526 GuiFontMetrics::breakString_helper(docstring const & s, int first_wid, int wid,
527                                    bool rtl, bool force) const
528 {
529         TextLayoutHelper const tlh(s, rtl);
530
531         QTextLayout tl;
532 #ifdef BIDI_USE_FLAG
533         /* Use undocumented flag to enforce drawing direction
534          * FIXME: This does not work with Qt 5.11 (ticket #11284).
535          */
536         tl.setFlags(rtl ? Qt::TextForceRightToLeft : Qt::TextForceLeftToRight);
537 #endif
538         tl.setText(tlh.qstr);
539         tl.setFont(font_);
540         QTextOption to;
541         /*
542          * Some Asian languages split lines anywhere (no notion of
543          * word). It seems that QTextLayout is not aware of this fact.
544          * See for reference:
545          *    https://en.wikipedia.org/wiki/Line_breaking_rules_in_East_Asian_languages
546          *
547          * FIXME: Something shall be done about characters which are
548          * not allowed at the beginning or end of line.
549          */
550         to.setWrapMode(force ? QTextOption::WrapAtWordBoundaryOrAnywhere
551                              : QTextOption::WordWrap);
552         tl.setTextOption(to);
553
554         bool first = true;
555         tl.beginLayout();
556         while(true) {
557                 QTextLine line = tl.createLine();
558                 if (!line.isValid())
559                         break;
560                 line.setLineWidth(first ? first_wid : wid);
561                 first = false;
562         }
563         tl.endLayout();
564
565         Breaks breaks;
566         int pos = 0;
567         for (int i = 0 ; i < tl.lineCount() ; ++i) {
568                 QTextLine const & line = tl.lineAt(i);
569                 int const line_epos = line.textStart() + line.textLength();
570                 int const epos = tlh.qpos2pos(line_epos);
571                 // This does not take trailing spaces into account, except for the last line.
572                 int const wid = iround(line.naturalTextWidth());
573                 // If the line is not the last one, trailing space is always omitted.
574                 int nspc_wid = wid;
575                 // For the last line, compute the width without trailing space
576                 if (i + 1 == tl.lineCount() && !s.empty() && isSpace(s.back())
577                     && line.textStart() <= tlh.pos2qpos(s.size() - 1))
578                         nspc_wid = iround(line.cursorToX(tlh.pos2qpos(s.size() - 1)));
579                 breaks.emplace_back(epos - pos, wid, nspc_wid);
580                 pos = epos;
581         }
582
583         return breaks;
584 }
585
586
587 uint qHash(BreakStringKey const & key)
588 {
589         // assume widths are less than 10000. This fits in 32 bits.
590         uint params = key.force + 2 * key.rtl + 4 * key.first_wid + 10000 * key.wid;
591         return std::qHash(key.s) ^ ::qHash(params);
592 }
593
594
595 FontMetrics::Breaks GuiFontMetrics::breakString(docstring const & s, int first_wid, int wid,
596                                                 bool rtl, bool force) const
597 {
598         PROFILE_THIS_BLOCK(breakString);
599         if (s.empty())
600                 return Breaks();
601
602         BreakStringKey key{s, first_wid, wid, rtl, force};
603         Breaks brks;
604         if (auto * brks_ptr = breakstr_cache_.object_ptr(key))
605                 brks = *brks_ptr;
606         else {
607                 PROFILE_CACHE_MISS(breakString);
608                 brks = breakString_helper(s, first_wid, wid, rtl, force);
609                 breakstr_cache_.insert(key, brks, sizeof(key) + s.size() * sizeof(char_type));
610         }
611         return brks;
612 }
613
614
615 void GuiFontMetrics::rectText(docstring const & str,
616         int & w, int & ascent, int & descent) const
617 {
618         // FIXME: let offset depend on font (this is Inset::TEXT_TO_OFFSET)
619         int const offset = 4;
620
621         w = width(str) + offset;
622         ascent = metrics_.ascent() + offset / 2;
623         descent = metrics_.descent() + offset / 2;
624 }
625
626
627 void GuiFontMetrics::buttonText(docstring const & str, const int offset,
628         int & w, int & ascent, int & descent) const
629 {
630         rectText(str, w, ascent, descent);
631         w += offset;
632 }
633
634
635 Dimension const GuiFontMetrics::defaultDimension() const
636 {
637         return Dimension(0, maxAscent(), maxDescent());
638 }
639
640
641 Dimension const GuiFontMetrics::dimension(char_type c) const
642 {
643         return Dimension(width(c), ascent(c), descent(c));
644 }
645
646
647 GuiFontMetrics::AscendDescend const GuiFontMetrics::fillMetricsCache(
648                 char_type c) const
649 {
650         QRect r;
651         if (is_utf16(c))
652                 r = metrics_.boundingRect(ucs4_to_qchar(c));
653         else
654                 r = metrics_.boundingRect(toqstr(docstring(1, c)));
655
656         AscendDescend ad = { -r.top(), r.bottom() + 1};
657         // We could as well compute the width but this is not really
658         // needed for now as it is done directly in width() below.
659         metrics_cache_.insert(c, ad);
660
661         return ad;
662 }
663
664
665 int GuiFontMetrics::width(char_type c) const
666 {
667         int value = width_cache_.value(c, outOfLimitMetric);
668         if (value != outOfLimitMetric)
669                 return value;
670
671 #if QT_VERSION >= 0x050b00
672         if (is_utf16(c))
673                 value = metrics_.horizontalAdvance(ucs4_to_qchar(c));
674         else
675                 value = metrics_.horizontalAdvance(toqstr(docstring(1, c)));
676 #else
677         if (is_utf16(c))
678                 value = metrics_.width(ucs4_to_qchar(c));
679         else
680                 value = metrics_.width(toqstr(docstring(1, c)));
681 #endif
682
683         width_cache_.insert(c, value);
684
685         return value;
686 }
687
688
689 int GuiFontMetrics::ascent(char_type c) const
690 {
691         static AscendDescend const outOfLimitAD =
692                 {outOfLimitMetric, outOfLimitMetric};
693         AscendDescend value = metrics_cache_.value(c, outOfLimitAD);
694         if (value.ascent != outOfLimitMetric)
695                 return value.ascent;
696
697         value = fillMetricsCache(c);
698         return value.ascent;
699 }
700
701
702 int GuiFontMetrics::descent(char_type c) const
703 {
704         static AscendDescend const outOfLimitAD =
705                 {outOfLimitMetric, outOfLimitMetric};
706         AscendDescend value = metrics_cache_.value(c, outOfLimitAD);
707         if (value.descent != outOfLimitMetric)
708                 return value.descent;
709
710         value = fillMetricsCache(c);
711         return value.descent;
712 }
713
714 } // namespace frontend
715 } // namespace lyx