]> git.lyx.org Git - features.git/blob - src/rowpainter.cpp
#7120 adjust the line thickness to mark changes, foreign language and misspelled...
[features.git] / src / rowpainter.cpp
1 /**
2  * \file rowpainter.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author various
7  * \author John Levon
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13 #include <algorithm>
14
15 #include "rowpainter.h"
16
17 #include "Bidi.h"
18 #include "Buffer.h"
19 #include "CoordCache.h"
20 #include "Cursor.h"
21 #include "BufferParams.h"
22 #include "BufferView.h"
23 #include "Changes.h"
24 #include "Encoding.h"
25 #include "Language.h"
26 #include "Layout.h"
27 #include "LyXRC.h"
28 #include "Row.h"
29 #include "MetricsInfo.h"
30 #include "Paragraph.h"
31 #include "ParagraphMetrics.h"
32 #include "ParagraphParameters.h"
33 #include "TextMetrics.h"
34 #include "VSpace.h"
35
36 #include "frontends/FontMetrics.h"
37 #include "frontends/Painter.h"
38
39 #include "insets/InsetText.h"
40
41 #include "support/debug.h"
42 #include "support/gettext.h"
43 #include "support/textutils.h"
44
45 #include "support/lassert.h"
46 #include <boost/crc.hpp>
47
48 using namespace std;
49
50 namespace lyx {
51
52 using frontend::Painter;
53 using frontend::FontMetrics;
54
55 RowPainter::RowPainter(PainterInfo & pi,
56         Text const & text, pit_type pit, Row const & row, Bidi & bidi, int x, int y)
57         : pi_(pi), text_(text),
58           text_metrics_(pi_.base.bv->textMetrics(&text)),
59           pars_(text.paragraphs()),
60           row_(row), pit_(pit), par_(text.paragraphs()[pit]),
61           pm_(text_metrics_.parMetrics(pit)),
62           bidi_(bidi), change_(pi_.change_),
63           xo_(x), yo_(y), width_(text_metrics_.width())
64 {
65         // derive the line thickness from zoom factor
66         // the zoom is given in percent
67         double const scale_ = lyxrc.zoom / 100.0;
68
69         bidi_.computeTables(par_, pi_.base.bv->buffer(), row_);
70         // (increase thickness at 150%, 250% etc.)
71         line_thickness_ = scale_ < 1.0 ? 1.0 : int(scale_ + 0.5);
72         line_offset_ = int(1.5 * line_thickness_) + (scale_ < 1.0 ? 1 : 2);
73
74         x_ = row_.x + xo_;
75
76         //lyxerr << "RowPainter: x: " << x_ << " xo: " << xo_ << " yo: " << yo_ << endl;
77         //row_.dump();
78
79         LASSERT(pit >= 0, /**/);
80         LASSERT(pit < int(text.paragraphs().size()), /**/);
81 }
82
83
84 FontInfo RowPainter::labelFont() const
85 {
86         FontInfo f = text_.labelFont(par_);
87         // selected text?
88         if (row_.begin_margin_sel || pi_.selected)
89                 f.setPaintColor(Color_selectiontext);
90         return f;
91 }
92
93
94 int RowPainter::leftMargin() const
95 {
96         return text_metrics_.leftMargin(text_metrics_.width(), pit_,
97                 row_.pos());
98 }
99
100 // If you want to debug inset metrics uncomment the following line:
101 //#define DEBUG_METRICS
102 // This draws green lines around each inset.
103
104
105 void RowPainter::paintInset(Inset const * inset, pos_type const pos)
106 {
107         Font const font = text_metrics_.displayFont(pit_, pos);
108
109         LASSERT(inset, return);
110         // Backup full_repaint status because some insets (InsetTabular)
111         // requires a full repaint
112         bool pi_full_repaint = pi_.full_repaint;
113
114         // FIXME: We should always use font, see documentation of
115         // noFontChange() in Inset.h.
116         pi_.base.font = inset->noFontChange() ?
117                 pi_.base.bv->buffer().params().getFont().fontInfo() :
118                 font.fontInfo();
119         pi_.ltr_pos = (bidi_.level(pos) % 2 == 0);
120         pi_.change_ = change_.changed() ? change_ : par_.lookupChange(pos);
121
122         int const x1 = int(x_);
123         pi_.base.bv->coordCache().insets().add(inset, x1, yo_);
124         // insets are painted completely. Recursive
125         // FIXME: it is wrong to completely paint the background
126         // if we want to do single row painting.
127         inset->drawBackground(pi_, x1, yo_);
128         inset->drawSelection(pi_, x1, yo_);
129         inset->draw(pi_, x1, yo_);
130
131         Dimension const & dim = pm_.insetDimension(inset);
132
133         paintForeignMark(x_, font.language(), dim.descent());
134
135         x_ += dim.width();
136
137         // Restore full_repaint status.
138         pi_.full_repaint = pi_full_repaint;
139
140 #ifdef DEBUG_METRICS
141         int const x2 = x1 + dim.wid;
142         int const y1 = yo_ + dim.des;
143         int const y2 = yo_ - dim.asc;
144         pi_.pain.line(x1, y1, x1, y2, Color_green);
145         pi_.pain.line(x1, y1, x2, y1, Color_green);
146         pi_.pain.line(x2, y1, x2, y2, Color_green);
147         pi_.pain.line(x1, y2, x2, y2, Color_green);
148 #endif
149 }
150
151
152 void RowPainter::paintHebrewComposeChar(pos_type & vpos, FontInfo const & font)
153 {
154         pos_type pos = bidi_.vis2log(vpos);
155
156         docstring str;
157
158         // first char
159         char_type c = par_.getChar(pos);
160         str += c;
161         ++vpos;
162
163         int const width = theFontMetrics(font).width(c);
164         int dx = 0;
165
166         for (pos_type i = pos - 1; i >= 0; --i) {
167                 c = par_.getChar(i);
168                 if (!Encodings::isHebrewComposeChar(c)) {
169                         if (isPrintableNonspace(c)) {
170                                 int const width2 = pm_.singleWidth(i,
171                                         text_metrics_.displayFont(pit_, i));
172                                 dx = (c == 0x05e8 || // resh
173                                       c == 0x05d3)   // dalet
174                                         ? width2 - width
175                                         : (width2 - width) / 2;
176                         }
177                         break;
178                 }
179         }
180
181         // Draw nikud
182         pi_.pain.text(int(x_) + dx, yo_, str, font);
183 }
184
185
186 void RowPainter::paintArabicComposeChar(pos_type & vpos, FontInfo const & font)
187 {
188         pos_type pos = bidi_.vis2log(vpos);
189         docstring str;
190
191         // first char
192         char_type c = par_.getChar(pos);
193         c = par_.transformChar(c, pos);
194         str += c;
195         ++vpos;
196
197         int const width = theFontMetrics(font).width(c);
198         int dx = 0;
199
200         for (pos_type i = pos - 1; i >= 0; --i) {
201                 c = par_.getChar(i);
202                 if (!Encodings::isArabicComposeChar(c)) {
203                         if (isPrintableNonspace(c)) {
204                                 int const width2 = pm_.singleWidth(i,
205                                                 text_metrics_.displayFont(pit_, i));
206                                 dx = (width2 - width) / 2;
207                         }
208                         break;
209                 }
210         }
211         // Draw nikud
212         pi_.pain.text(int(x_) + dx, yo_, str, font);
213 }
214
215
216 void RowPainter::paintChars(pos_type & vpos, FontInfo const & font,
217                             bool hebrew, bool arabic)
218 {
219         // This method takes up 70% of time when typing
220         pos_type pos = bidi_.vis2log(vpos);
221         // first character
222         char_type prev_char = par_.getChar(pos);
223         vector<char_type> str;
224         str.reserve(100);
225         str.push_back(prev_char);
226
227         if (arabic) {
228                 char_type c = str[0];
229                 if (c == '(')
230                         c = ')';
231                 else if (c == ')')
232                         c = '(';
233                 str[0] = par_.transformChar(c, pos);
234         }
235
236         pos_type const end = row_.endpos();
237         FontSpan const font_span = par_.fontSpan(pos);
238         // Track-change status.
239         Change const & change_running = par_.lookupChange(pos);
240
241         // selected text?
242         bool const selection = (pos >= row_.sel_beg && pos < row_.sel_end)
243                 || pi_.selected;
244
245         // spelling correct?
246         bool const spell_state =
247                 lyxrc.spellcheck_continuously && par_.isMisspelled(pos);
248
249         // collect as much similar chars as we can
250         for (++vpos ; vpos < end ; ++vpos) {
251                 // Work-around bug #6920
252                 // The bug can be reproduced with DejaVu font under Linux.
253                 // The issue is that we compute the metrics character by character
254                 // in ParagraphMetrics::singleWidth(); but we paint word by word
255                 // for performance reason.
256                 // Maybe a more general fix would be draw character by character
257                 // for some predefined fonts on some platform. In arabic and
258                 // Hebrew we already do paint this way.
259                 if (prev_char == 'f')
260                         break;
261                 
262                 pos = bidi_.vis2log(vpos);
263                 if (pos < font_span.first || pos > font_span.last)
264                         break;
265
266                 bool const new_selection = pos >= row_.sel_beg && pos < row_.sel_end;
267                 if (new_selection != selection)
268                         // Selection ends or starts here.
269                         break;
270
271                 bool const new_spell_state =
272                         lyxrc.spellcheck_continuously && par_.isMisspelled(pos);
273                 if (new_spell_state != spell_state)
274                         // Spell checker state changed here.
275                         break;
276
277                 Change const & change = par_.lookupChange(pos);
278                 if (!change_running.isSimilarTo(change))
279                         // Track change type or author has changed.
280                         break;
281
282                 char_type c = par_.getChar(pos);
283
284                 if (c == '\t')
285                         break;
286
287                 if (!isPrintableNonspace(c))
288                         break;
289
290                 /* Because we do our own bidi, at this point the strings are
291                  * already in visual order. However, Qt also applies its own
292                  * bidi algorithm to strings that it paints to the screen.
293                  * Therefore, if we were to paint Hebrew/Arabic words as a
294                  * single string, the letters in the words would get reversed
295                  * again. In order to avoid that, we don't collect Hebrew/
296                  * Arabic characters, but rather paint them one at a time.
297                  * See also http://thread.gmane.org/gmane.editors.lyx.devel/79740
298                  */
299                 if (hebrew)
300                         break;
301
302                 /* FIXME: these checks are irrelevant, since 'arabic' and
303                  * 'hebrew' alone are already going to trigger a break.
304                  * However, this should not be removed completely, because
305                  * if an alternative solution is found which allows grouping
306                  * of arabic and hebrew characters, then these breaks may have
307                  * to be re-applied.
308
309                 if (arabic && Encodings::isArabicComposeChar(c))
310                         break;
311
312                 if (hebrew && Encodings::isHebrewComposeChar(c))
313                         break;
314                 */
315
316                 if (arabic) {
317                         if (c == '(')
318                                 c = ')';
319                         else if (c == ')')
320                                 c = '(';
321                         c = par_.transformChar(c, pos);
322                         /* see comment in hebrew, explaining why we break */
323                         break;
324                 }
325
326                 str.push_back(c);
327                 prev_char = c;
328         }
329
330         docstring s(&str[0], str.size());
331
332         if (s[0] == '\t')
333                 s.replace(0,1,from_ascii("    "));
334
335         if (!selection && !change_running.changed()) {
336                 x_ += pi_.pain.text(int(x_), yo_, s, font);
337                 return;
338         }
339
340         FontInfo copy = font;
341         if (change_running.changed())
342                 copy.setPaintColor(change_running.color());
343         else if (selection)
344                 copy.setPaintColor(Color_selectiontext);
345
346         x_ += pi_.pain.text(int(x_), yo_, s, copy);
347 }
348
349
350 void RowPainter::paintForeignMark(double orig_x, Language const * lang,
351                 int desc)
352 {
353         if (!lyxrc.mark_foreign_language)
354                 return;
355         if (lang == latex_language)
356                 return;
357         if (lang == pi_.base.bv->buffer().params().language)
358                 return;
359
360         int const y = yo_ + 1 + desc + int(line_thickness_/2);
361         pi_.pain.line(int(orig_x), y, int(x_), y, Color_language,
362                 Painter::line_solid, line_thickness_);
363 }
364
365
366 void RowPainter::paintMisspelledMark(double orig_x, bool changed)
367 {
368         // if changed the misspelled marker gets placed slightly lower than normal
369         // to avoid drawing at the same vertical offset
370         int const y = yo_ + (changed ? line_thickness_ + 1 : 0) + line_offset_;
371         pi_.pain.line(int(orig_x), y, int(x_), y, Color_error,
372                 Painter::line_onoffdash, line_thickness_);
373 }
374
375
376 void RowPainter::paintFromPos(pos_type & vpos, bool changed)
377 {
378         pos_type const pos = bidi_.vis2log(vpos);
379         Font const orig_font = text_metrics_.displayFont(pit_, pos);
380         double const orig_x = x_;
381
382         // usual characters, no insets
383         char_type const c = par_.getChar(pos);
384
385         // special case languages
386         string const & lang = orig_font.language()->lang();
387         bool const hebrew = lang == "hebrew";
388         bool const arabic = lang == "arabic_arabtex" || lang == "arabic_arabi" ||
389                                                 lang == "farsi";
390
391         // spelling correct?
392         bool const misspelled_ =
393                 lyxrc.spellcheck_continuously && par_.isMisspelled(pos);
394
395         // draw as many chars as we can
396         if ((!hebrew && !arabic)
397                 || (hebrew && !Encodings::isHebrewComposeChar(c))
398                 || (arabic && !Encodings::isArabicComposeChar(c))) {
399                 paintChars(vpos, orig_font.fontInfo(), hebrew, arabic);
400         } else if (hebrew) {
401                 paintHebrewComposeChar(vpos, orig_font.fontInfo());
402         } else if (arabic) {
403                 paintArabicComposeChar(vpos, orig_font.fontInfo());
404         }
405
406         paintForeignMark(orig_x, orig_font.language());
407
408         if (lyxrc.spellcheck_continuously && misspelled_) {
409                 paintMisspelledMark(orig_x, changed);
410         }
411 }
412
413
414 void RowPainter::paintChangeBar()
415 {
416         pos_type const start = row_.pos();
417         pos_type end = row_.endpos();
418
419         if (par_.size() == end) {
420                 // this is the last row of the paragraph;
421                 // thus, we must also consider the imaginary end-of-par character
422                 end++;
423         }
424
425         if (start == end || !par_.isChanged(start, end))
426                 return;
427
428         int const height = text_metrics_.isLastRow(pit_, row_)
429                 ? row_.ascent()
430                 : row_.height();
431
432         pi_.pain.fillRectangle(5, yo_ - row_.ascent(), 3, height, Color_changebar);
433 }
434
435
436 void RowPainter::paintAppendix()
437 {
438         // only draw the appendix frame once (for the main text)
439         if (!par_.params().appendix() || !text_.isMainText())
440                 return;
441
442         int y = yo_ - row_.ascent();
443
444         if (par_.params().startOfAppendix())
445                 y += 2 * defaultRowHeight();
446
447         pi_.pain.line(1, y, 1, yo_ + row_.height(), Color_appendix);
448         pi_.pain.line(width_ - 2, y, width_ - 2, yo_ + row_.height(), Color_appendix);
449 }
450
451
452 void RowPainter::paintDepthBar()
453 {
454         depth_type const depth = par_.getDepth();
455
456         if (depth <= 0)
457                 return;
458
459         depth_type prev_depth = 0;
460         if (!text_metrics_.isFirstRow(pit_, row_)) {
461                 pit_type pit2 = pit_;
462                 if (row_.pos() == 0)
463                         --pit2;
464                 prev_depth = pars_[pit2].getDepth();
465         }
466
467         depth_type next_depth = 0;
468         if (!text_metrics_.isLastRow(pit_, row_)) {
469                 pit_type pit2 = pit_;
470                 if (row_.endpos() >= pars_[pit2].size())
471                         ++pit2;
472                 next_depth = pars_[pit2].getDepth();
473         }
474
475         for (depth_type i = 1; i <= depth; ++i) {
476                 int const w = nestMargin() / 5;
477                 int x = int(xo_) + w * i;
478                 // only consider the changebar space if we're drawing outermost text
479                 if (text_.isMainText())
480                         x += changebarMargin();
481
482                 int const starty = yo_ - row_.ascent();
483                 int const h =  row_.height() - 1 - (i - next_depth - 1) * 3;
484
485                 pi_.pain.line(x, starty, x, starty + h, Color_depthbar);
486
487                 if (i > prev_depth)
488                         pi_.pain.fillRectangle(x, starty, w, 2, Color_depthbar);
489                 if (i > next_depth)
490                         pi_.pain.fillRectangle(x, starty + h, w, 2, Color_depthbar);
491         }
492 }
493
494
495 int RowPainter::paintAppendixStart(int y)
496 {
497         FontInfo pb_font = sane_font;
498         pb_font.setColor(Color_appendix);
499         pb_font.decSize();
500
501         int w = 0;
502         int a = 0;
503         int d = 0;
504
505         docstring const label = _("Appendix");
506         theFontMetrics(pb_font).rectText(label, w, a, d);
507
508         int const text_start = int(xo_ + (width_ - w) / 2);
509         int const text_end = text_start + w;
510
511         pi_.pain.rectText(text_start, y + d, label, pb_font, Color_none, Color_none);
512
513         pi_.pain.line(int(xo_ + 1), y, text_start, y, Color_appendix);
514         pi_.pain.line(text_end, y, int(xo_ + width_ - 2), y, Color_appendix);
515
516         return 3 * defaultRowHeight();
517 }
518
519
520 void RowPainter::paintFirst()
521 {
522         ParagraphParameters const & pparams = par_.params();
523         Buffer const & buffer = pi_.base.bv->buffer();
524         BufferParams const & bparams = buffer.params();
525         Layout const & layout = par_.layout();
526
527         int y_top = 0;
528
529         // start of appendix?
530         if (pparams.startOfAppendix())
531                 y_top += paintAppendixStart(yo_ - row_.ascent() + 2 * defaultRowHeight());
532
533         if (bparams.paragraph_separation == BufferParams::ParagraphSkipSeparation
534                 && pit_ != 0) {
535                 if (layout.latextype == LATEX_PARAGRAPH
536                     && !par_.getDepth()) {
537                         y_top += bparams.getDefSkip().inPixels(*pi_.base.bv);
538                 } else {
539                         Layout const & playout = pars_[pit_ - 1].layout();
540                         if (playout.latextype == LATEX_PARAGRAPH
541                             && !pars_[pit_ - 1].getDepth()) {
542                                 // is it right to use defskip here, too? (AS)
543                                 y_top += bparams.getDefSkip().inPixels(*pi_.base.bv);
544                         }
545                 }
546         }
547
548         bool const is_rtl = text_.isRTL(par_);
549         bool const is_seq = text_.isFirstInSequence(pit_);
550         //lyxerr << "paintFirst: " << par_.id() << " is_seq: " << is_seq << endl;
551
552         // should we print a label?
553         if (layout.labeltype >= LABEL_STATIC
554             && (layout.labeltype != LABEL_STATIC
555                       || layout.latextype != LATEX_ENVIRONMENT
556                       || is_seq)) {
557
558                 FontInfo const font = labelFont();
559                 FontMetrics const & fm = theFontMetrics(font);
560
561                 docstring const str = par_.labelString();
562                 if (!str.empty()) {
563                         double x = x_;
564
565                         // this is special code for the chapter layout. This is
566                         // printed in an extra row and has a pagebreak at
567                         // the top.
568                         if (layout.counter == "chapter") {
569                                 double spacing_val = 1.0;
570                                 if (!pparams.spacing().isDefault()) {
571                                         spacing_val = pparams.spacing().getValue();
572                                 } else {
573                                         spacing_val = bparams.spacing().getValue();
574                                 }
575
576                                 int const labeladdon = int(fm.maxHeight() * layout.spacing.getValue() * spacing_val);
577
578                                 int const maxdesc = int(fm.maxDescent() * layout.spacing.getValue() * spacing_val)
579                                         + int(layout.parsep) * defaultRowHeight();
580
581                                 if (is_rtl) {
582                                         x = width_ - leftMargin() -
583                                                 fm.width(str);
584                                 }
585
586                                 pi_.pain.text(int(x), yo_ - maxdesc - labeladdon, str, font);
587                         } else {
588                                 if (is_rtl) {
589                                         x = width_ - leftMargin()
590                                                 + fm.width(layout.labelsep);
591                                 } else {
592                                         x = x_ - fm.width(layout.labelsep)
593                                                 - fm.width(str);
594                                 }
595
596                                 pi_.pain.text(int(x), yo_, str, font);
597                         }
598                 }
599
600         // the labels at the top of an environment.
601         // More or less for bibliography
602         } else if (is_seq &&
603                 (layout.labeltype == LABEL_TOP_ENVIRONMENT ||
604                 layout.labeltype == LABEL_BIBLIO ||
605                 layout.labeltype == LABEL_CENTERED_TOP_ENVIRONMENT)) {
606                 FontInfo const font = labelFont();
607                 docstring const str = par_.labelString();
608                 if (!str.empty()) {
609                         double spacing_val = 1.0;
610                         if (!pparams.spacing().isDefault())
611                                 spacing_val = pparams.spacing().getValue();
612                         else
613                                 spacing_val = bparams.spacing().getValue();
614
615                         FontMetrics const & fm = theFontMetrics(font);
616
617                         int const labeladdon = int(fm.maxHeight()
618                                 * layout.spacing.getValue() * spacing_val);
619
620                         int maxdesc =
621                                 int(fm.maxDescent() * layout.spacing.getValue() * spacing_val
622                                 + (layout.labelbottomsep * defaultRowHeight()));
623
624                         double x = x_;
625                         if (layout.labeltype == LABEL_CENTERED_TOP_ENVIRONMENT) {
626                                 if (is_rtl)
627                                         x = leftMargin();
628                                 x += (width_ - text_metrics_.rightMargin(pm_) - leftMargin()) / 2;
629                                 x -= fm.width(str) / 2;
630                         } else if (is_rtl) {
631                                 x = width_ - leftMargin() -     fm.width(str);
632                         }
633                         pi_.pain.text(int(x), yo_ - maxdesc - labeladdon, str, font);
634                 }
635         }
636 }
637
638
639 /** Check if the current paragraph is the last paragraph in a
640     proof environment */
641 static int getEndLabel(pit_type p, Text const & text)
642 {
643         ParagraphList const & pars = text.paragraphs();
644         pit_type pit = p;
645         depth_type par_depth = pars[p].getDepth();
646         while (pit != pit_type(pars.size())) {
647                 Layout const & layout = pars[pit].layout();
648                 int const endlabeltype = layout.endlabeltype;
649
650                 if (endlabeltype != END_LABEL_NO_LABEL) {
651                         if (p + 1 == pit_type(pars.size()))
652                                 return endlabeltype;
653
654                         depth_type const next_depth =
655                                 pars[p + 1].getDepth();
656                         if (par_depth > next_depth ||
657                             (par_depth == next_depth && layout != pars[p + 1].layout()))
658                                 return endlabeltype;
659                         break;
660                 }
661                 if (par_depth == 0)
662                         break;
663                 pit = text.outerHook(pit);
664                 if (pit != pit_type(pars.size()))
665                         par_depth = pars[pit].getDepth();
666         }
667         return END_LABEL_NO_LABEL;
668 }
669
670
671 void RowPainter::paintLast()
672 {
673         bool const is_rtl = text_.isRTL(par_);
674         int const endlabel = getEndLabel(pit_, text_);
675
676         // paint imaginary end-of-paragraph character
677
678         Change const & change = par_.lookupChange(par_.size());
679         if (change.changed()) {
680                 FontMetrics const & fm =
681                         theFontMetrics(pi_.base.bv->buffer().params().getFont());
682                 int const length = fm.maxAscent() / 2;
683                 Color col = change.color();
684
685                 pi_.pain.line(int(x_) + 1, yo_ + 2, int(x_) + 1, yo_ + 2 - length, col,
686                            Painter::line_solid, 3);
687
688                 if (change.deleted()) {
689                         pi_.pain.line(int(x_) + 1 - length, yo_ + 2, int(x_) + 1 + length,
690                                 yo_ + 2, col, Painter::line_solid, 3);
691                 } else {
692                         pi_.pain.line(int(x_) + 1 - length, yo_ + 2, int(x_) + 1,
693                                 yo_ + 2, col, Painter::line_solid, 3);
694                 }
695         }
696
697         // draw an endlabel
698
699         switch (endlabel) {
700         case END_LABEL_BOX:
701         case END_LABEL_FILLED_BOX: {
702                 FontInfo const font = labelFont();
703                 FontMetrics const & fm = theFontMetrics(font);
704                 int const size = int(0.75 * fm.maxAscent());
705                 int const y = yo_ - size;
706                 int const max_row_width = width_ - size - Inset::TEXT_TO_INSET_OFFSET;
707                 int x = is_rtl ? nestMargin() + changebarMargin()
708                         : max_row_width - text_metrics_.rightMargin(pm_);
709
710                 // If needed, move the box a bit to avoid overlapping with text.
711                 int const rem = max_row_width - row_.width();
712                 if (rem <= 0)
713                         x += is_rtl ? rem : - rem;
714
715                 if (endlabel == END_LABEL_BOX)
716                         pi_.pain.rectangle(x, y, size, size, Color_eolmarker);
717                 else
718                         pi_.pain.fillRectangle(x, y, size, size, Color_eolmarker);
719                 break;
720         }
721
722         case END_LABEL_STATIC: {
723                 FontInfo const font = labelFont();
724                 FontMetrics const & fm = theFontMetrics(font);
725                 docstring const & str = par_.layout().endlabelstring();
726                 double const x = is_rtl ? x_ - fm.width(str) : x_;
727                 pi_.pain.text(int(x), yo_, str, font);
728                 break;
729         }
730
731         case END_LABEL_NO_LABEL:
732                 if (lyxrc.paragraph_markers && size_type(pit_ + 1) < pars_.size()) {
733                         docstring const s = docstring(1, char_type(0x00B6));
734                         FontInfo f = FontInfo();
735                         FontMetrics const & fm = theFontMetrics(f);
736                         f.setColor(Color_paragraphmarker);
737                         pi_.pain.text(int(x_), yo_, s, f);
738                         x_ += fm.width(s);
739                 }
740                 break;
741         }
742 }
743
744
745 void RowPainter::paintOnlyInsets()
746 {
747         CoordCache const & cache = pi_.base.bv->coordCache();
748         pos_type const end = row_.endpos();
749         for (pos_type pos = row_.pos(); pos != end; ++pos) {
750                 // If outer row has changed, nested insets are repaint completely.
751                 Inset const * inset = par_.getInset(pos);
752                 bool const nested_inset = inset &&
753                         (inset->asInsetText() || inset->asInsetTabular());
754                 if (!nested_inset)
755                         continue;
756                 if (x_ > pi_.base.bv->workWidth()
757                     || !cache.getInsets().has(inset))
758                         continue;
759                 x_ = cache.getInsets().x(inset);
760
761                 bool const pi_selected = pi_.selected;
762                 Cursor const & cur = pi_.base.bv->cursor();
763                 if (cur.selection() && cur.text() == &text_
764                           && cur.normalAnchor().text() == &text_)
765                         pi_.selected = row_.sel_beg <= pos && row_.sel_end > pos;
766                 paintInset(inset, pos);
767                 pi_.selected = pi_selected;
768         }
769 }
770
771
772 void RowPainter::paintText()
773 {
774         pos_type const end = row_.endpos();
775         // Spaces at logical line breaks in bidi text must be skipped during
776         // painting. However, they may appear visually in the middle
777         // of a row; they must be skipped, wherever they are...
778         // * logically "abc_[HEBREW_\nHEBREW]"
779         // * visually "abc_[_WERBEH\nWERBEH]"
780         pos_type skipped_sep_vpos = -1;
781         pos_type body_pos = par_.beginOfBody();
782         if (body_pos > 0 &&
783                 (body_pos > end || !par_.isLineSeparator(body_pos - 1))) {
784                 body_pos = 0;
785         }
786
787         Layout const & layout = par_.layout();
788
789         Change change_running;
790         int change_last_x = 0;
791
792         // check for possible inline completion
793         DocIterator const & inlineCompletionPos = pi_.base.bv->inlineCompletionPos();
794         pos_type inlineCompletionVPos = -1;
795         if (inlineCompletionPos.inTexted()
796             && inlineCompletionPos.text() == &text_
797             && inlineCompletionPos.pit() == pit_
798             && inlineCompletionPos.pos() - 1 >= row_.pos()
799             && inlineCompletionPos.pos() - 1 < row_.endpos()) {
800                 // draw logically behind the previous character
801                 inlineCompletionVPos = bidi_.log2vis(inlineCompletionPos.pos() - 1);
802         }
803
804         // Use font span to speed things up, see below
805         FontSpan font_span;
806         Font font;
807
808         // If the last logical character is a separator, don't paint it, unless
809         // it's in the last row of a paragraph; see skipped_sep_vpos declaration
810         if (end > 0 && end < par_.size() && par_.isSeparator(end - 1))
811                 skipped_sep_vpos = bidi_.log2vis(end - 1);
812
813         for (pos_type vpos = row_.pos(); vpos < end; ) {
814                 if (x_ > pi_.base.bv->workWidth())
815                         break;
816
817                 // Skip the separator at the logical end of the row
818                 if (vpos == skipped_sep_vpos) {
819                         ++vpos;
820                         continue;
821                 }
822
823                 pos_type const pos = bidi_.vis2log(vpos);
824
825                 if (pos >= par_.size()) {
826                         ++vpos;
827                         continue;
828                 }
829
830                 // Use font span to speed things up, see above
831                 if (vpos < font_span.first || vpos > font_span.last) {
832                         font_span = par_.fontSpan(vpos);
833                         font = text_metrics_.displayFont(pit_, vpos);
834
835                         // split font span if inline completion is inside
836                         if (font_span.first <= inlineCompletionVPos
837                             && font_span.last > inlineCompletionVPos)
838                                 font_span.last = inlineCompletionVPos;
839                 }
840
841                 const int width_pos = pm_.singleWidth(pos, font);
842
843                 if (x_ + width_pos < 0) {
844                         x_ += width_pos;
845                         ++vpos;
846                         continue;
847                 }
848                 Change const & change = par_.lookupChange(pos);
849                 if (change.changed() && !change_running.changed()) {
850                         change_running = change;
851                         change_last_x = int(x_);
852                 }
853
854                 Inset const * inset = par_.getInset(pos);
855                 bool const highly_editable_inset = inset
856                         && inset->editable();
857
858                 // If we reach the end of a change or if the author changes, paint it.
859                 // We also don't paint across things like tables
860                 if (change_running.changed() && (highly_editable_inset
861                         || !change.changed() || !change_running.isSimilarTo(change))) {
862                         // Calculate 1/3 height of the buffer's default font
863                         FontMetrics const & fm
864                                 = theFontMetrics(pi_.base.bv->buffer().params().getFont());
865                         int const y_bar = change_running.deleted() ?
866                                 yo_ - fm.maxAscent() / 3 : yo_ + line_offset_;
867                         pi_.pain.line(change_last_x, y_bar, int(x_), y_bar,
868                                 change_running.color(), Painter::line_solid, line_thickness_);
869
870                         // Change might continue with a different author or type
871                         if (change.changed() && !highly_editable_inset) {
872                                 change_running = change;
873                                 change_last_x = int(x_);
874                         } else
875                                 change_running.setUnchanged();
876                 }
877
878                 if (body_pos > 0 && pos == body_pos - 1) {
879                         int const lwidth = theFontMetrics(labelFont())
880                                 .width(layout.labelsep);
881
882                         x_ += row_.label_hfill + lwidth - width_pos;
883                 }
884
885                 // Is the inline completion in front of character?
886                 if (font.isRightToLeft() && vpos == inlineCompletionVPos)
887                         paintInlineCompletion(font);
888
889                 if (par_.isSeparator(pos)) {
890                         Font const orig_font = text_metrics_.displayFont(pit_, pos);
891                         double const orig_x = x_;
892                         x_ += width_pos;
893                         if (pos >= body_pos)
894                                 x_ += row_.separator;
895                         paintForeignMark(orig_x, orig_font.language());
896                         ++vpos;
897
898                 } else if (inset) {
899                         // If outer row has changed, nested insets are repaint completely.
900                         pi_.base.bv->coordCache().insets().add(inset, int(x_), yo_);
901
902                         bool const pi_selected = pi_.selected;
903                         Cursor const & cur = pi_.base.bv->cursor();
904                         if (cur.selection() && cur.text() == &text_
905                                   && cur.normalAnchor().text() == &text_)
906                                 pi_.selected = row_.sel_beg <= pos && row_.sel_end > pos;
907                         paintInset(inset, pos);
908                         pi_.selected = pi_selected;
909                         ++vpos;
910
911                 } else {
912                         // paint as many characters as possible.
913                         paintFromPos(vpos, change_running.changed());
914                 }
915
916                 // Is the inline completion after character?
917                 if (!font.isRightToLeft() && vpos - 1 == inlineCompletionVPos)
918                         paintInlineCompletion(font);
919         }
920
921         // if we reach the end of a struck out range, paint it
922         if (change_running.changed()) {
923                 FontMetrics const & fm
924                         = theFontMetrics(pi_.base.bv->buffer().params().getFont());
925                 int const y_bar = change_running.deleted() ?
926                                 yo_ - fm.maxAscent() / 3 : yo_ + line_offset_;
927                 pi_.pain.line(change_last_x, y_bar, int(x_), y_bar,
928                         change_running.color(), Painter::line_solid, line_thickness_);
929                 change_running.setUnchanged();
930         }
931 }
932
933
934 void RowPainter::paintSelection()
935 {
936         if (!row_.selection())
937                 return;
938         Cursor const & curs = pi_.base.bv->cursor();
939         DocIterator beg = curs.selectionBegin();
940         beg.pit() = pit_;
941         beg.pos() = row_.sel_beg;
942
943         DocIterator end = curs.selectionEnd();
944         end.pit() = pit_;
945         end.pos() = row_.sel_end;
946
947         bool const begin_boundary = beg.pos() >= row_.endpos();
948         bool const end_boundary = row_.sel_end == row_.endpos();
949
950         DocIterator cur = beg;
951         cur.boundary(begin_boundary);
952         int x1 = text_metrics_.cursorX(beg.top(), begin_boundary);
953         int x2 = text_metrics_.cursorX(end.top(), end_boundary);
954         int const y1 = yo_ - row_.ascent();
955         int const y2 = y1 + row_.height();
956
957         int const rm = text_.isMainText() ? pi_.base.bv->rightMargin() : 0;
958         int const lm = text_.isMainText() ? pi_.base.bv->leftMargin() : 0;
959
960         // draw the margins
961         if (row_.begin_margin_sel) {
962                 if (text_.isRTL(beg.paragraph())) {
963                         pi_.pain.fillRectangle(int(xo_ + x1), y1,
964                                 text_metrics_.width() - rm - x1, y2 - y1, Color_selection);
965                 } else {
966                         pi_.pain.fillRectangle(int(xo_ + lm), y1, x1 - lm, y2 - y1,
967                                 Color_selection);
968                 }
969         }
970
971         if (row_.end_margin_sel) {
972                 if (text_.isRTL(beg.paragraph())) {
973                         pi_.pain.fillRectangle(int(xo_ + lm), y1, x2 - lm, y2 - y1,
974                                 Color_selection);
975                 } else {
976                         pi_.pain.fillRectangle(int(xo_ + x2), y1, text_metrics_.width() - rm - x2,
977                                 y2 - y1, Color_selection);
978                 }
979         }
980
981         // if we are on a boundary from the beginning, it's probably
982         // a RTL boundary and we jump to the other side directly as this
983         // segement is 0-size and confuses the logic below
984         if (cur.boundary())
985                 cur.boundary(false);
986
987         // go through row and draw from RTL boundary to RTL boundary
988         while (cur < end) {
989                 bool draw_now = false;
990
991                 // simplified cursorForward code below which does not
992                 // descend into insets and which does not go into the
993                 // next line. Compare the logic with the original cursorForward
994
995                 // if left of boundary -> just jump to right side, but
996                 // for RTL boundaries don't, because: abc|DDEEFFghi -> abcDDEEF|Fghi
997                 if (cur.boundary()) {
998                         cur.boundary(false);
999                 }       else if (text_metrics_.isRTLBoundary(cur.pit(), cur.pos() + 1)) {
1000                         // in front of RTL boundary -> Stay on this side of the boundary
1001                         // because:  ab|cDDEEFFghi -> abc|DDEEFFghi
1002                         ++cur.pos();
1003                         cur.boundary(true);
1004                         draw_now = true;
1005                 } else {
1006                         // move right
1007                         ++cur.pos();
1008
1009                         // line end?
1010                         if (cur.pos() == row_.endpos())
1011                                 cur.boundary(true);
1012                 }
1013
1014                 if (x1 == -1) {
1015                         // the previous segment was just drawn, now the next starts
1016                         x1 = text_metrics_.cursorX(cur.top(), cur.boundary());
1017                 }
1018
1019                 if (!(cur < end) || draw_now) {
1020                         x2 = text_metrics_.cursorX(cur.top(), cur.boundary());
1021                         pi_.pain.fillRectangle(int(xo_ + min(x1, x2)), y1, abs(x2 - x1),
1022                                 y2 - y1, Color_selection);
1023
1024                         // reset x1, so it is set again next round (which will be on the
1025                         // right side of a boundary or at the selection end)
1026                         x1 = -1;
1027                 }
1028         }
1029 }
1030
1031
1032 void RowPainter::paintInlineCompletion(Font const & font)
1033 {
1034         docstring completion = pi_.base.bv->inlineCompletion();
1035         FontInfo f = font.fontInfo();
1036         bool rtl = font.isRightToLeft();
1037
1038         // draw the unique and the non-unique completion part
1039         // Note: this is not time-critical as it is
1040         // only done once per screen.
1041         size_t uniqueTo = pi_.base.bv->inlineCompletionUniqueChars();
1042         docstring s1 = completion.substr(0, uniqueTo);
1043         docstring s2 = completion.substr(uniqueTo);
1044         ColorCode c1 = Color_inlinecompletion;
1045         ColorCode c2 = Color_nonunique_inlinecompletion;
1046
1047         // right to left?
1048         if (rtl) {
1049                 swap(s1, s2);
1050                 swap(c1, c2);
1051         }
1052
1053         if (s1.size() > 0) {
1054                 f.setColor(c1);
1055                 pi_.pain.text(int(x_), yo_, s1, f);
1056                 x_ += theFontMetrics(font).width(s1);
1057         }
1058
1059         if (s2.size() > 0) {
1060                 f.setColor(c2);
1061                 pi_.pain.text(int(x_), yo_, s2, f);
1062                 x_ += theFontMetrics(font).width(s2);
1063         }
1064 }
1065
1066 } // namespace lyx