X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2FTextMetrics.cpp;h=06e00f42201c90de2dfd876af30370a7ce2cdd0d;hb=5e1c414a21cd5d3345e7d3ff9181d7fa08c86830;hp=2b0903c750d23f1398d462db0ddb8e8956300eb5;hpb=e4dba53232bc71d5a577466e7fb4a3251944d9d8;p=lyx.git diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp index 2b0903c750..06e00f4220 100644 --- a/src/TextMetrics.cpp +++ b/src/TextMetrics.cpp @@ -20,34 +20,32 @@ #include "TextMetrics.h" #include "Buffer.h" -#include "buffer_funcs.h" #include "BufferParams.h" #include "BufferView.h" #include "CoordCache.h" #include "Cursor.h" #include "CutAndPaste.h" -#include "InsetList.h" -#include "Language.h" #include "Layout.h" #include "LyXRC.h" #include "MetricsInfo.h" #include "ParagraphParameters.h" #include "RowPainter.h" +#include "Session.h" #include "Text.h" #include "TextClass.h" #include "VSpace.h" +#include "insets/InsetSeparator.h" #include "insets/InsetText.h" -#include "mathed/InsetMathMacroTemplate.h" +#include "mathed/MacroTable.h" #include "frontends/FontMetrics.h" -#include "frontends/Painter.h" #include "frontends/NullPainter.h" -#include "support/convert.h" #include "support/debug.h" #include "support/lassert.h" +#include "support/Changer.h" #include #include @@ -109,14 +107,9 @@ int numberOfHfills(Row const & row, ParagraphMetrics const & pm, TextMetrics::TextMetrics(BufferView * bv, Text * text) - : bv_(bv), text_(text) -{ - LBUFERR(bv_); - max_width_ = bv_->workWidth(); - dim_.wid = max_width_; - dim_.asc = 10; - dim_.des = 10; -} + : bv_(bv), text_(text), dim_(bv_->workWidth(), 10, 10), + max_width_(dim_.wid), tight_(false) +{} bool TextMetrics::contains(pit_type pit) const @@ -125,13 +118,6 @@ bool TextMetrics::contains(pit_type pit) const } -ParagraphMetrics const & TextMetrics::parMetrics(pit_type pit) const -{ - return const_cast(this)->parMetrics(pit, true); -} - - - pair TextMetrics::first() const { ParMetricsCache::const_iterator it = par_metrics_.begin(); @@ -147,6 +133,30 @@ pair TextMetrics::last() const } +bool TextMetrics::isLastRow(Row const & row) const +{ + ParagraphList const & pars = text_->paragraphs(); + return row.endpos() >= pars[row.pit()].size() + && row.pit() + 1 == pit_type(pars.size()); +} + + +bool TextMetrics::isFirstRow(Row const & row) const +{ + return row.pos() == 0 && row.pit() == 0; +} + + +void TextMetrics::setRowChanged(pit_type pit, pos_type pos) +{ + for (auto & pm_pair : par_metrics_) + if (pm_pair.first == pit) + for (Row & row : pm_pair.second.rows()) + if (row.pos() == pos) + row.changed(true); +} + + ParagraphMetrics & TextMetrics::parMetrics(pit_type pit, bool redo) { ParMetricsCache::iterator pmc_it = par_metrics_.find(pit); @@ -160,18 +170,60 @@ ParagraphMetrics & TextMetrics::parMetrics(pit_type pit, bool redo) } -bool TextMetrics::metrics(MetricsInfo & mi, Dimension & dim, int min_width, - bool const expand_on_multipars) +ParagraphMetrics const & TextMetrics::parMetrics(pit_type pit) const +{ + return const_cast(this)->parMetrics(pit, true); +} + + +ParagraphMetrics & TextMetrics::parMetrics(pit_type pit) +{ + return parMetrics(pit, true); +} + + +void TextMetrics::newParMetricsDown() +{ + pair const & last = *par_metrics_.rbegin(); + pit_type const pit = last.first + 1; + if (pit == int(text_->paragraphs().size())) + return; + + // do it and update its position. + redoParagraph(pit); + par_metrics_[pit].setPosition(last.second.position() + + last.second.descent() + par_metrics_[pit].ascent()); + updatePosCache(pit); +} + + +void TextMetrics::newParMetricsUp() +{ + pair const & first = *par_metrics_.begin(); + if (first.first == 0) + return; + + pit_type const pit = first.first - 1; + // do it and update its position. + redoParagraph(pit); + par_metrics_[pit].setPosition(first.second.position() + - first.second.ascent() - par_metrics_[pit].descent()); + updatePosCache(pit); +} + + +bool TextMetrics::metrics(MetricsInfo const & mi, Dimension & dim, int min_width) { LBUFERR(mi.base.textwidth > 0); max_width_ = mi.base.textwidth; + tight_ = mi.tight_insets; // backup old dimension. Dimension const old_dim = dim_; // reset dimension. dim_ = Dimension(); dim_.wid = min_width; pit_type const npar = text_->paragraphs().size(); - if (npar > 1 && expand_on_multipars) + if (npar > 1 && !tight_) // If there is more than one row, expand the text to // the full allowable width. dim_.wid = max_width_; @@ -180,15 +232,23 @@ bool TextMetrics::metrics(MetricsInfo & mi, Dimension & dim, int min_width, // << " maxWidth: " << max_width_ << "\nfont: " << mi.base.font << endl; bool changed = false; - unsigned int h = 0; + int h = 0; for (pit_type pit = 0; pit != npar; ++pit) { - changed |= redoParagraph(pit); + // create rows, but do not set alignment yet + changed |= redoParagraph(pit, false); ParagraphMetrics const & pm = par_metrics_[pit]; h += pm.height(); if (dim_.wid < pm.width()) dim_.wid = pm.width(); } + // Now set alignment for all rows (the width might not have been known before). + for (pit_type pit = 0; pit != npar; ++pit) { + ParagraphMetrics & pm = par_metrics_[pit]; + for (Row & row : pm.rows()) + setRowAlignment(row, dim_.wid); + } + dim_.asc = par_metrics_[0].ascent(); dim_.des = h - dim_.asc; //lyxerr << "dim_.wid " << dim_.wid << endl; @@ -229,55 +289,64 @@ void TextMetrics::applyOuterFont(Font & font) const } +// This is an arbitrary magic value +static const pos_type force_label = -12345; + +// if pos == force_label, this function will return the label font instead. +// This undocumented and only for use by labelDisplayFont() Font TextMetrics::displayFont(pit_type pit, pos_type pos) const { - LASSERT(pos >= 0, { static Font f; return f; }); + LASSERT(pos >= 0 || pos == force_label, { static Font f; return f; }); ParagraphList const & pars = text_->paragraphs(); Paragraph const & par = pars[pit]; Layout const & layout = par.layout(); Buffer const & buffer = bv_->buffer(); // FIXME: broken? - BufferParams const & params = buffer.params(); - pos_type const body_pos = par.beginOfBody(); + BufferParams const & bparams = buffer.params(); + bool const label = pos < par.beginOfBody() || pos == force_label; + + Font f = (pos == force_label) ? Font(inherit_font, bparams.language) + : par.getFontSettings(bparams, pos); + FontInfo const & lf = label ? layout.labelfont : layout.font; // We specialize the 95% common case: if (!par.getDepth()) { - Font f = par.getFontSettings(params, pos); if (!text_->isMainText()) applyOuterFont(f); - bool lab = layout.labeltype == LABEL_MANUAL && pos < body_pos; - FontInfo const & lf = lab ? layout.labelfont : layout.font; - FontInfo rlf = lab ? layout.reslabelfont : layout.resfont; + FontInfo rlf = label ? layout.reslabelfont : layout.resfont; // In case the default family has been customized if (lf.family() == INHERIT_FAMILY) - rlf.setFamily(params.getFont().fontInfo().family()); + rlf.setFamily(bparams.getFont().fontInfo().family()); f.fontInfo().realize(rlf); return f; } - // The uncommon case need not be optimized as much - FontInfo const & layoutfont = pos < body_pos ? - layout.labelfont : layout.font; - - Font font = par.getFontSettings(params, pos); - font.fontInfo().realize(layoutfont); + // The uncommon case need not be optimized as much. + // FIXME : the two code paths seem different in function. + f.fontInfo().realize(lf); if (!text_->isMainText()) - applyOuterFont(font); + applyOuterFont(f); // Realize against environment font information // NOTE: the cast to pit_type should be removed when pit_type // changes to a unsigned integer. if (pit < pit_type(pars.size())) - font.fontInfo().realize(text_->outerFont(pit).fontInfo()); + f.fontInfo().realize(text_->outerFont(pit).fontInfo()); // Realize with the fonts of lesser depth. - font.fontInfo().realize(params.getFont().fontInfo()); + f.fontInfo().realize(bparams.getFont().fontInfo()); - return font; + return f; +} + + +Font TextMetrics::labelDisplayFont(pit_type pit) const +{ + return displayFont(pit, force_label); } @@ -319,13 +388,13 @@ bool TextMetrics::isRTLBoundary(pit_type pit, pos_type pos, || !contains(pit)) return false; - ParagraphMetrics & pm = par_metrics_[pit]; + ParagraphMetrics const & pm = par_metrics_[pit]; // no RTL boundary in empty paragraph if (pm.rows().empty()) return false; - pos_type endpos = pm.getRow(pos - 1, false).endpos(); - pos_type startpos = pm.getRow(pos, false).pos(); + pos_type const endpos = pm.getRow(pos - 1, false).endpos(); + pos_type const startpos = pm.getRow(pos, false).pos(); // no RTL boundary at line start: // abc\n -> toggle to RTL -> abc\n (and not: abc\n| // | | ) @@ -343,7 +412,7 @@ bool TextMetrics::isRTLBoundary(pit_type pit, pos_type pos, || par.isSeparator(pos - 1))) return false; - bool left = font.isVisibleRightToLeft(); + bool const left = font.isVisibleRightToLeft(); bool right; if (pos == par.size()) right = par.isRTL(bv_->buffer().params()); @@ -354,7 +423,7 @@ bool TextMetrics::isRTLBoundary(pit_type pit, pos_type pos, } -bool TextMetrics::redoParagraph(pit_type const pit) +bool TextMetrics::redoParagraph(pit_type const pit, bool const align_rows) { Paragraph & par = text_->getPar(pit); // IMPORTANT NOTE: We pass 'false' explicitly in order to not call @@ -370,11 +439,12 @@ bool TextMetrics::redoParagraph(pit_type const pit) // FIXME: This check ought to be done somewhere else. It is the reason // why text_ is not const. But then, where else to do it? // Well, how can you end up with either (a) a biblio environment that - // has no InsetBibitem or (b) a biblio environment with more than one - // InsetBibitem? I think the answer is: when paragraphs are merged; + // has no InsetBibitem, (b) a biblio environment with more than one + // InsetBibitem or (c) a paragraph that has a bib item but is no biblio + // environment? I think the answer is: when paragraphs are merged; // when layout is set; when material is pasted. if (par.brokenBiblio()) { - Cursor & cur = const_cast(bv_->cursor()); + Cursor & cur = bv_->cursor(); // In some cases, we do not know how to record undo if (&cur.inset() == &text_->inset()) cur.recordUndo(pit, pit); @@ -415,6 +485,7 @@ bool TextMetrics::redoParagraph(pit_type const pit) par.setBeginOfBody(); Font const bufferfont = buffer.params().getFont(); CoordCache::Insets & insetCache = bv_->coordCache().insets(); + map extrawidths; for (auto const & e : par.insetList()) { // FIXME Doesn't this HAVE to be non-empty? // position already initialized? @@ -431,10 +502,11 @@ bool TextMetrics::redoParagraph(pit_type const pit) // If there is an end of paragraph marker, its size should be // substracted to the available width. The logic here is - // almost the same as in breakRow, remember keep them in sync. + // almost the same as in tokenizeParagraph, remember keep them in sync. int eop = 0; - if (lyxrc.paragraph_markers && e.pos + 1 == par.size() - && size_type(pit + 1) < text_->paragraphs().size()) { + if (e.pos + 1 == par.size() + && (lyxrc.paragraph_markers || par.lookupChange(par.size()).changed()) + && size_type(pit + 1) < text_->paragraphs().size()) { Font f(text_->layoutFont(pit)); // ¶ U+00B6 PILCROW SIGN eop = theFontMetrics(f).width(char_type(0x00B6)); @@ -447,82 +519,104 @@ bool TextMetrics::redoParagraph(pit_type const pit) Font const & font = e.inset->inheritFont() ? displayFont(pit, e.pos) : bufferfont; MacroContext mc(&buffer, parPos); - MetricsInfo mi(bv_, font.fontInfo(), w, mc); + MetricsInfo mi(bv_, font.fontInfo(), w, mc, e.pos == 0, tight_); + mi.base.outer_font = displayFont(pit, e.pos).fontInfo(); e.inset->metrics(mi, dim); + /* FIXME: This is a hack. This allows InsetMathHull to state + * that it needs some elbow room beyond its width, in order to + * fit the numbering and/or the left margin (with left + * alignment), which are outside of the inset itself. + * + * To this end, InsetMathHull::metrics() sets a value in + * MetricsInfo::extrawidth and this value is added later to + * the width of the row that contains the inset (when this row + * is tight or shorter than the max allowed width). + * + * See ticket #12320 for details. + */ + extrawidths[e.inset] = mi.extrawidth; + if (!insetCache.has(e.inset) || insetCache.dim(e.inset) != dim) { insetCache.add(e.inset, dim); changed = true; } } - pos_type first = 0; - size_t row_index = 0; - bool need_new_row = false; - // maximum pixel width of a row - do { - if (row_index == pm.rows().size()) - pm.rows().push_back(Row()); - Row & row = pm.rows()[row_index]; - row.pit(pit); - row.pos(first); - row.pit(pit); - need_new_row = breakRow(row, right_margin); + // Transform the paragraph into a single row containing all the elements. + Row const bigrow = tokenizeParagraph(pit); + // Split the row in several rows fitting in available width + pm.rows() = breakParagraph(bigrow); + + // Add the needed extra width to the rows that contain the insets that request it + for (Row & row : pm.rows()) + for (Row::Element & e : row) + if (e.type == Row::INSET && (row.width() < max_width_ || tight_)) + row.dim().wid += extrawidths[e.inset]; + + /* If there is more than one row, expand the text to the full + * allowable width. This setting here is needed for the + * setRowAlignment() below. We do nothing when tight insets are + * requested. + */ + if (pm.rows().size() > 1 && !tight_ && dim_.wid < max_width_) + dim_.wid = max_width_; + + // Compute height and alignment of the rows. + for (Row & row : pm.rows()) { setRowHeight(row); - row.changed(true); - if ((row_index || row.endpos() < par.size() || row.right_boundary()) - && par.inInset().lyxCode() != CELL_CODE) { - /* If there is more than one row or the row has been - * broken by a display inset or a newline, expand the text - * to the full allowable width. This setting here is - * needed for the computeRowMetrics() below. - * We do nothing when inside a table cell. - */ - if (dim_.wid < max_width_) - dim_.wid = max_width_; - } - int const max_row_width = max(dim_.wid, row.width()); - computeRowMetrics(row, max_row_width); - first = row.endpos(); - ++row_index; + if (align_rows) + setRowAlignment(row, max(dim_.wid, row.width())); - pm.dim().wid = max(pm.dim().wid, row.width()); + pm.dim().wid = max(pm.dim().wid, row.width() + row.right_margin); pm.dim().des += row.height(); - } while (first < par.size() || need_new_row); + } - if (row_index < pm.rows().size()) - pm.rows().resize(row_index); + // This type of margin can only be handled at the global paragraph level + if (par.layout().margintype == MARGIN_RIGHT_ADDRESS_BOX) { + int offset = 0; + if (par.isRTL(buffer.params())) { + // globally align the paragraph to the left. + int minleft = max_width_; + for (Row const & row : pm.rows()) + minleft = min(minleft, row.left_margin); + offset = right_margin - minleft; + } else { + // globally align the paragraph to the right. + int maxwid = 0; + for (Row const & row : pm.rows()) + maxwid = max(maxwid, row.width()); + offset = max_width_ - right_margin - maxwid; + } + for (Row & row : pm.rows()) { + row.left_margin += offset; + row.dim().wid += offset; + } + } + + // The space above and below the paragraph. + int top = parTopSpacing(pit); + int bottom = parBottomSpacing(pit); + + // Top and bottom margin of the document (only at top-level) // FIXME: It might be better to move this in another method // specially tailored for the main text. - // Top and bottom margin of the document (only at top-level) if (text_->isMainText()) { - // original value was 20px, which is 0.2in at 100dpi - int const margin = bv_->zoomedPixels(20); - if (pit == 0) { - pm.rows().front().dimension().asc += margin; - /* coverity thinks that we should update pm.dim().asc - * below, but all the rows heights are actually counted as - * part of the paragraph metric descent see loop above). - */ - // coverity[copy_paste_error] - pm.dim().des += margin; - } - ParagraphList const & pars = text_->paragraphs(); - if (pit + 1 == pit_type(pars.size())) { - pm.rows().back().dimension().des += margin; - pm.dim().des += margin; + if (pit == 0) + top += bv_->topMargin(); + if (pit + 1 == pit_type(text_->paragraphs().size())) { + bottom += bv_->bottomMargin(); } } - // The space above and below the paragraph. - int const top = parTopSpacing(pit); - pm.rows().front().dimension().asc += top; - int const bottom = parBottomSpacing(pit); - pm.rows().back().dimension().des += bottom; + // Add the top/bottom space to rows and paragraph metrics + pm.rows().front().dim().asc += top; + pm.rows().back().dim().des += bottom; pm.dim().des += top + bottom; - pm.dim().asc += pm.rows()[0].ascent(); - pm.dim().des -= pm.rows()[0].ascent(); + // Move the pm ascent to be the same as the first row ascent + pm.dim().asc += pm.rows().front().ascent(); + pm.dim().des -= pm.rows().front().ascent(); changed |= old_dim.height() != pm.dim().height(); @@ -532,7 +626,7 @@ bool TextMetrics::redoParagraph(pit_type const pit) LyXAlignment TextMetrics::getAlign(Paragraph const & par, Row const & row) const { - LyXAlignment align = par.getAlign(); + LyXAlignment align = par.getAlign(bv_->buffer().params()); // handle alignment inside tabular cells Inset const & owner = text_->inset(); @@ -557,19 +651,13 @@ LyXAlignment TextMetrics::getAlign(Paragraph const & par, Row const & row) const // Display-style insets should always be on a centered row if (Inset const * inset = par.getInset(row.pos())) { - switch (inset->display()) { - case Inset::AlignLeft: - align = LYX_ALIGN_BLOCK; - break; - case Inset::AlignCenter: - align = LYX_ALIGN_CENTER; - break; - case Inset::Inline: - // unchanged (use align) - break; - case Inset::AlignRight: - align = LYX_ALIGN_RIGHT; - break; + if (inset->rowFlags() & Display) { + if (inset->rowFlags() & AlignLeft) + align = LYX_ALIGN_LEFT; + else if (inset->rowFlags() & AlignRight) + align = LYX_ALIGN_RIGHT; + else + align = LYX_ALIGN_CENTER; } } @@ -588,7 +676,7 @@ LyXAlignment TextMetrics::getAlign(Paragraph const & par, Row const & row) const } -void TextMetrics::computeRowMetrics(Row & row, int width) const +void TextMetrics::setRowAlignment(Row & row, int width) const { row.label_hfill = 0; row.separator = 0; @@ -621,7 +709,7 @@ void TextMetrics::computeRowMetrics(Row & row, int width) const } // are there any hfills in the row? - ParagraphMetrics & pm = par_metrics_[row.pit()]; + ParagraphMetrics const & pm = par_metrics_[row.pit()]; int nh = numberOfHfills(row, pm, par.beginOfBody()); int hfill = 0; int hfill_rem = 0; @@ -629,7 +717,7 @@ void TextMetrics::computeRowMetrics(Row & row, int width) const // We don't have to look at the alignment if the row is already // larger then the permitted width as then we force the // LEFT_ALIGN'edness! - if (int(row.width()) >= max_width_) + if (row.width() >= max_width_) return; if (nh == 0) { @@ -641,28 +729,28 @@ void TextMetrics::computeRowMetrics(Row & row, int width) const if (!row.setExtraWidth(w) && row.isRTL()) { // Justification failed and the text is RTL: align to the right row.left_margin += w; - row.dimension().wid += w; + row.dim().wid += w; } break; case LYX_ALIGN_LEFT: // a displayed inset that is flushed if (Inset const * inset = par.getInset(row.pos())) { row.left_margin += inset->indent(*bv_); - row.dimension().wid += inset->indent(*bv_); + row.dim().wid += inset->indent(*bv_); } break; case LYX_ALIGN_RIGHT: if (Inset const * inset = par.getInset(row.pos())) { int const new_w = max(w - inset->indent(*bv_), 0); row.left_margin += new_w; - row.dimension().wid += new_w; + row.dim().wid += new_w; } else { row.left_margin += w; - row.dimension().wid += w; + row.dim().wid += w; } break; case LYX_ALIGN_CENTER: - row.dimension().wid += w / 2; + row.dim().wid += w / 2; row.left_margin += w / 2; break; case LYX_ALIGN_NONE: @@ -677,7 +765,7 @@ void TextMetrics::computeRowMetrics(Row & row, int width) const // Case nh > 0. There are hfill separators. hfill = w / nh; hfill_rem = w % nh; - row.dimension().wid += w; + row.dim().wid += w; // Set size of hfill insets pos_type const endpos = row.endpos(); pos_type body_pos = par.beginOfBody(); @@ -730,31 +818,6 @@ int TextMetrics::labelFill(Row const & row) const } -#if 0 -// Not used, see TextMetrics::breakRow -// this needs special handling - only newlines count as a break point -static pos_type addressBreakPoint(pos_type i, Paragraph const & par) -{ - pos_type const end = par.size(); - - for (; i < end; ++i) - if (par.isNewline(i)) - return i + 1; - - return end; -} -#endif - - -int TextMetrics::labelEnd(pit_type const pit) const -{ - // labelEnd is only needed if the layout fills a flushleft label. - if (text_->getPar(pit).layout().margintype != MARGIN_MANUAL) - return 0; - // return the beginning of the body - return leftMargin(pit); -} - namespace { /** @@ -813,49 +876,47 @@ private: } // namespace -/** This is the function where the hard work is done. The code here is - * very sensitive to small changes :) Note that part of the - * intelligence is also in Row::shortenIfNeeded. - */ -bool TextMetrics::breakRow(Row & row, int const right_margin) const + +Row TextMetrics::tokenizeParagraph(pit_type const pit) const { - Paragraph const & par = text_->getPar(row.pit()); + Row row; + row.pit(pit); + Paragraph const & par = text_->getPar(pit); + Buffer const & buf = text_->inset().buffer(); + BookmarksSection::BookmarkPosList bpl = + theSession().bookmarks().bookmarksInPar(buf.fileName(), par.id()); + pos_type const end = par.size(); - pos_type const pos = row.pos(); pos_type const body_pos = par.beginOfBody(); - bool const is_rtl = text_->isRTL(par); - bool need_new_row = false; - - row.clear(); - row.left_margin = leftMargin(row.pit(), pos); - row.right_margin = right_margin; - if (is_rtl) - swap(row.left_margin, row.right_margin); - // Remember that the row width takes into account the left_margin - // but not the right_margin. - row.dimension().wid = row.left_margin; - // the width available for the row. - int const width = max_width_ - row.right_margin; - -#if 0 - //FIXME: As long as leftMargin() is not correctly implemented for - // MARGIN_RIGHT_ADDRESS_BOX, we should also not do this here. - // Otherwise, long rows will be painted off the screen. - if (par.layout().margintype == MARGIN_RIGHT_ADDRESS_BOX) - return addressBreakPoint(pos, par); -#endif // check for possible inline completion DocIterator const & ic_it = bv_->inlineCompletionPos(); pos_type ic_pos = -1; - if (ic_it.inTexted() && ic_it.text() == text_ && ic_it.pit() == row.pit()) + if (ic_it.inTexted() && ic_it.text() == text_ && ic_it.pit() == pit) ic_pos = ic_it.pos(); // Now we iterate through until we reach the right margin // or the end of the par, then build a representation of the row. - pos_type i = pos; - FontIterator fi = FontIterator(*this, par, row.pit(), pos); - while (i < end && (i == pos || row.width() <= width)) { + pos_type i = 0; + FontIterator fi = FontIterator(*this, par, pit, 0); + // The real stopping condition is a few lines below. + while (true) { + // Firstly, check whether there is a bookmark here. + if (lyxrc.bookmarks_visibility == LyXRC::BMK_INLINE) + for (auto const & bp_p : bpl) + if (bp_p.second == i) { + Font f = *fi; + f.fontInfo().setColor(Color_bookmark); + // ❶ U+2776 DINGBAT NEGATIVE CIRCLED DIGIT ONE + char_type const ch = 0x2775 + bp_p.first; + row.addVirtual(i, docstring(1, ch), f, Change()); + } + + // The stopping condition is here so that the display of a + // bookmark can take place at paragraph start too. + if (i >= end) + break; + char_type c = par.getChar(i); // The most special cases are handled first. if (par.isInset(i)) { @@ -863,26 +924,22 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const Dimension dim = bv_->coordCache().insets().dim(ins); row.add(i, ins, dim, *fi, par.lookupChange(i)); } else if (c == ' ' && i + 1 == body_pos) { - // There is a space at i, but it should not be - // added as a separator, because it is just - // before body_pos. Instead, insert some spacing to - // align text + // This space is an \item separator. Represent it with a + // special space element, which dimension will be computed + // in breakRow. FontMetrics const & fm = theFontMetrics(text_->labelFont(par)); - // this is needed to make sure that the row width is correct - row.finalizeLast(); - int const add = max(fm.width(par.layout().labelsep), - labelEnd(row.pit()) - row.width()); - row.addSpace(i, add, *fi, par.lookupChange(i)); + int const wid = fm.width(par.layout().labelsep); + row.addMarginSpace(i, wid, *fi, par.lookupChange(i)); } else if (c == '\t') row.addSpace(i, theFontMetrics(*fi).width(from_ascii(" ")), - *fi, par.lookupChange(i)); + *fi, par.lookupChange(i)); else if (c == 0x2028 || c == 0x2029) { /** * U+2028 LINE SEPARATOR * U+2029 PARAGRAPH SEPARATOR * These are special unicode characters that break - * lines/pragraphs. Not handling them lead to trouble wrt + * lines/pragraphs. Not handling them leads to trouble wrt * Qt QTextLayout formatting. We add a visible character * on screen so that the user can see that something is * happening. @@ -892,17 +949,9 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const // ¶ U+00B6 PILCROW SIGN char_type const screen_char = (c == 0x2028) ? 0x2936 : 0x00B6; row.add(i, screen_char, *fi, par.lookupChange(i)); - } else { - // FIXME: please someone fix the Hebrew/Arabic parenthesis mess! - // see also Paragraph::getUChar. - if (fi->language()->lang() == "hebrew") { - if (c == '(') - c = ')'; - else if (c == ')') - c = '('; - } + } else + // row elements before body are unbreakable row.add(i, c, *fi, par.lookupChange(i)); - } // add inline completion width // draw logically behind the previous character @@ -919,69 +968,228 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const row.addVirtual(i + 1, comp.substr(uniqueTo), f, Change()); } - // Handle some situations that abruptly terminate the row - // - A newline inset - // - Before a display inset - // - After a display inset - Inset const * inset = 0; - if (par.isNewline(i) || par.isEnvSeparator(i) - || (i + 1 < end && (inset = par.getInset(i + 1)) - && inset->display()) - || (!row.empty() && row.back().inset - && row.back().inset->display())) { - row.flushed(true); - need_new_row = par.isNewline(i); - ++i; - break; - } - ++i; ++fi; } row.finalizeLast(); - row.endpos(i); + row.endpos(end); - // End of paragraph marker. The logic here is almost the + // End of paragraph marker, either if LyXRc requires it, or there + // is an end of paragraph change. The logic here is almost the // same as in redoParagraph, remember keep them in sync. ParagraphList const & pars = text_->paragraphs(); - if (lyxrc.paragraph_markers && !need_new_row - && i == end && size_type(row.pit() + 1) < pars.size()) { + Change const & endchange = par.lookupChange(end); + if (endchange.changed()) + row.needsChangeBar(true); + if ((lyxrc.paragraph_markers || endchange.changed()) + && size_type(pit + 1) < pars.size()) { // add a virtual element for the end-of-paragraph // marker; it is shown on screen, but does not exist // in the paragraph. - Font f(text_->layoutFont(row.pit())); + Font f(text_->layoutFont(pit)); f.fontInfo().setColor(Color_paragraphmarker); - BufferParams const & bparams - = text_->inset().buffer().params(); - f.setLanguage(par.getParLanguage(bparams)); + f.setLanguage(par.getParLanguage(buf.params())); // ¶ U+00B6 PILCROW SIGN - row.addVirtual(end, docstring(1, char_type(0x00B6)), f, Change()); + row.addVirtual(end, docstring(1, char_type(0x00B6)), f, endchange); } - // Is there a end-of-paragaph change? - if (i == end && par.lookupChange(end).changed() && !need_new_row) - row.needsChangeBar(true); + return row; +} + + +namespace { + +/** Helper template flexible_const_iterator + * A way to iterate over a const container, but insert fake elements in it. + * In the case of a row, we will have to break some elements, which + * create new ones. This class allows to abstract this. + * Only the required parts are implemented for now. + */ +template +class flexible_const_iterator { + typedef typename T::value_type value_type; +public: - // 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(row.pit(), row.endpos()) - - rightMargin(row.pit()); + // + flexible_const_iterator & operator++() { + if (pile_.empty()) + ++cit_; + else + pile_.pop_back(); + return *this; + } + + value_type operator*() const { return pile_.empty() ? *cit_ : pile_.back(); } + + value_type const * operator->() const { return pile_.empty() ? &*cit_ : &pile_.back(); } - if (row.shortenIfNeeded(body_pos, width, next_width)) - row.flushed(false); - row.right_boundary(!row.empty() && row.endpos() < end - && row.back().endpos == row.endpos()); - // Last row in paragraph is flushed - if (row.endpos() == end) - row.flushed(true); + void put(value_type const & e) { pile_.push_back(e); } + + // Put a sequence of elements on the pile (in reverse order!) + void put(vector const & elts) { + pile_.insert(pile_.end(), elts.rbegin(), elts.rend()); + } + +// This should be private, but declaring the friend functions is too much work +//private: + typename T::const_iterator cit_; + // A vector that is used as like a pile to store the elements to + // consider before incrementing the underlying iterator. + vector pile_; +}; + +template +flexible_const_iterator flexible_begin(T const & t) +{ + return { t.begin(), vector() }; +} + + +template +flexible_const_iterator flexible_end(T const & t) +{ + return { t.end(), vector() }; +} + + +// Equality is only possible if respective piles are empty +template +bool operator==(flexible_const_iterator const & t1, + flexible_const_iterator const & t2) +{ + return t1.cit_ == t2.cit_ && t1.pile_.empty() && t2.pile_.empty(); +} + + +Row newRow(TextMetrics const & tm, pit_type pit, pos_type pos, bool is_rtl) +{ + Row nrow; + nrow.pit(pit); + nrow.pos(pos); + nrow.left_margin = tm.leftMargin(pit, pos); + nrow.right_margin = tm.rightMargin(pit); + nrow.setRTL(is_rtl); + if (is_rtl) + swap(nrow.left_margin, nrow.right_margin); + // Remember that the row width takes into account the left_margin + // but not the right_margin. + nrow.dim().wid = nrow.left_margin; + return nrow; +} + + +void cleanupRow(Row & row, bool at_end) +{ + if (row.empty()) { + row.endpos(row.pos()); + return; + } + + row.endpos(row.back().endpos); + // remove trailing spaces on row break + if (!at_end && !row.flushed()) + row.back().rtrim(); + // boundary exists when there was no space at the end of row + row.end_boundary(!at_end && row.back().endpos == row.endpos()); // make sure that the RTL elements are in reverse ordering - row.reverseRTL(is_rtl); - //LYXERR0("breakrow: row is " << row); + row.reverseRTL(); +} + + +// Implement the priorities described in RowFlags.h. +bool needsRowBreak(int f1, int f2) +{ + if (f1 & AlwaysBreakAfter /*|| f2 & AlwaysBreakBefore*/) + return true; + if (f1 & NoBreakAfter || f2 & NoBreakBefore) + return false; + if (f1 & BreakAfter || f2 & BreakBefore) + return true; + return false; +} + + +} + - return need_new_row; +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 fcit = flexible_begin(bigrow); + flexible_const_iterator const end = flexible_end(bigrow); + while (true) { + bool const row_empty = rows.empty() || rows.back().empty(); + // The row flags of previous element, if there is one. + // Otherwise we use NoBreakAfter to avoid an empty row before + // e.g. a displayed equation. + int const f1 = row_empty ? NoBreakAfter : rows.back().back().row_flags; + // The row flags of next element, if there is one. + // Otherwise we use NoBreakBefore (see above), unless the + // paragraph has an end label (for which an empty row is OK). + int const f2 = (fcit == end) ? (end_label ? Inline : NoBreakBefore) + : fcit->row_flags; + if (rows.empty() || needsRowBreak(f1, f2)) { + if (!rows.empty()) { + // Flush row as requested by row flags + rows.back().flushed((f1 & Flush) || (f2 & FlushBefore)); + cleanupRow(rows.back(), false); + } + 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. + width = max_width_ - rows.back().right_margin; + } + + // The stopping condition is here because we may need a new + // empty row at the end. + if (fcit == end) + break; + + // 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::Elements tail; + elt.splitAt(width - rows.back().width(), next_width, Row::FIT, tail); + Row & rb = rows.back(); + if (elt.type == Row::MARGINSPACE) + elt.dim.wid = max(elt.dim.wid, leftMargin(bigrow.pit()) - rb.width()); + rb.push_back(elt); + rb.finalizeLast(); + if (rb.width() > width) { + // Keep the tail for later; this ought to be rare, but play safe. + if (!tail.empty()) + fcit.put(tail); + // 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()) { + // Last row in paragraph is flushed + rows.back().flushed(true); + cleanupRow(rows.back(), true); + // Is there an end-of-paragraph change? + if (bigrow.needsChangeBar()) + rows.back().needsChangeBar(true); + } + + return rows; } + int TextMetrics::parTopSpacing(pit_type const pit) const { Paragraph const & par = text_->getPar(pit); @@ -1031,7 +1239,10 @@ int TextMetrics::parTopSpacing(pit_type const pit) const && prevpar.getLabelWidthString() == par.getLabelWidthString()) { layoutasc = layout.itemsep * dh; } else if (pit != 0 && layout.topsep > 0) - layoutasc = layout.topsep * dh; + // combine the separation between different layouts (with same depth) + layoutasc = max(0.0, + prevpar.getDepth() != par.getDepth() ? layout.topsep + : layout.topsep - prevpar.layout().bottomsep) * dh; asc += int(layoutasc * 2 / (2 + pars[pit].getDepth())); @@ -1084,13 +1295,40 @@ void TextMetrics::setRowHeight(Row & row) const { Paragraph const & par = text_->getPar(row.pit()); Layout const & layout = par.layout(); - double const spacing_val = layout.spacing.getValue() * text_->spacing(par); + // leading space (line spacing) factor based on current paragraph + double spacing_val = layout.spacing.getValue() * text_->spacing(par); + + // if this is the first row but not the first paragraph, take into + // account the spacing of the previous paragraph. + if (row.pos() == 0 && row.pit() > 0) { + // for the first row in the paragraph, + // use previous paragraphs line spacing if it is larger + Paragraph const & previousPar = text_->getPar(row.pit() - 1); + Layout const & previousLayout = previousPar.layout(); + // leading space factor based on previous paragraph + double const previous_spacing_val + = previousLayout.spacing.getValue() * text_->spacing(previousPar); + if (previous_spacing_val > spacing_val) + spacing_val = previous_spacing_val; + } // Initial value for ascent (useful if row is empty). Font const font = displayFont(row.pit(), row.pos()); FontMetrics const & fm = theFontMetrics(font); - int maxasc = int(fm.maxAscent() * spacing_val); - int maxdes = int(fm.maxDescent() * spacing_val); + int maxasc = int(fm.maxAscent() + // add leading space + + fm.maxHeight() * (spacing_val - 1)); + int maxdes = int(fm.maxDescent()); + + // Take label string into account (useful if labelfont is large) + if (row.pos() == 0 && layout.labelIsInline()) { + FontInfo const labelfont = text_->labelFont(par); + FontMetrics const & lfm = theFontMetrics(labelfont); + maxasc = max(maxasc, int(lfm.maxAscent() + // add leading space + + lfm.maxHeight() * (spacing_val - 1))); + maxdes = max(maxdes, int(lfm.maxDescent())); + } // Find the ascent/descent of the row contents for (Row::Element const & e : row) { @@ -1099,8 +1337,10 @@ void TextMetrics::setRowHeight(Row & row) const maxdes = max(maxdes, e.dim.descent()); } else { FontMetrics const & fm2 = theFontMetrics(e.font); - maxasc = max(maxasc, int(fm2.maxAscent() * spacing_val)); - maxdes = max(maxdes, int(fm2.maxDescent() * spacing_val)); + maxasc = max(maxasc, int(fm2.maxAscent() + // add leading space + + fm2.maxHeight() * (spacing_val - 1))); + maxdes = max(maxdes, int(fm2.maxDescent())); } } @@ -1108,8 +1348,11 @@ void TextMetrics::setRowHeight(Row & row) const ++maxasc; ++maxdes; - row.dimension().asc = maxasc; - row.dimension().des = maxdes; + row.dim().asc = maxasc; + row.dim().des = maxdes; + + // This is useful for selections + row.contents_dim() = row.dim(); } @@ -1183,7 +1426,7 @@ pos_type TextMetrics::getPosNearX(Row const & row, int & x, || inset->lyxCode() == SEPARATOR_CODE)) pos = row.back().pos; else - boundary = row.right_boundary(); + boundary = row.end_boundary(); } x += xo - offset; @@ -1208,41 +1451,12 @@ pos_type TextMetrics::x2pos(pit_type pit, int row, int x) const } -void TextMetrics::newParMetricsDown() -{ - pair const & last = *par_metrics_.rbegin(); - pit_type const pit = last.first + 1; - if (pit == int(text_->paragraphs().size())) - return; - - // do it and update its position. - redoParagraph(pit); - par_metrics_[pit].setPosition(last.second.position() - + last.second.descent() + par_metrics_[pit].ascent()); - updatePosCache(pit); -} - - -void TextMetrics::newParMetricsUp() -{ - pair const & first = *par_metrics_.begin(); - if (first.first == 0) - return; - - pit_type const pit = first.first - 1; - // do it and update its position. - redoParagraph(pit); - par_metrics_[pit].setPosition(first.second.position() - - first.second.ascent() - par_metrics_[pit].descent()); - updatePosCache(pit); -} - // y is screen coordinate pit_type TextMetrics::getPitNearY(int y) { LASSERT(!text_->paragraphs().empty(), return -1); LASSERT(!par_metrics_.empty(), return -1); - LYXERR(Debug::DEBUG, "y: " << y << " cache size: " << par_metrics_.size()); + LYXERR(Debug::PAINTING, "y: " << y << " cache size: " << par_metrics_.size()); // look for highest numbered paragraph with y coordinate less than given y pit_type pit = -1; @@ -1254,7 +1468,7 @@ pit_type TextMetrics::getPitNearY(int y) ParagraphMetrics const & pm = it->second; - if (y < it->second.position() - int(pm.ascent())) { + if (y < it->second.position() - pm.ascent()) { // We are looking for a position that is before the first paragraph in // the cache (which is in priciple off-screen, that is before the // visible part. @@ -1269,7 +1483,7 @@ pit_type TextMetrics::getPitNearY(int y) ParagraphMetrics const & pm_last = par_metrics_[last->first]; - if (y >= last->second.position() + int(pm_last.descent())) { + if (y >= last->second.position() + pm_last.descent()) { // We are looking for a position that is after the last paragraph in // the cache (which is in priciple off-screen), that is before the // visible part. @@ -1283,18 +1497,18 @@ pit_type TextMetrics::getPitNearY(int y) } for (; it != et; ++it) { - LYXERR(Debug::DEBUG, "examining: pit: " << it->first + LYXERR(Debug::PAINTING, "examining: pit: " << it->first << " y: " << it->second.position()); ParagraphMetrics const & pm2 = par_metrics_[it->first]; - if (it->first >= pit && int(it->second.position()) - int(pm2.ascent()) <= y) { + if (it->first >= pit && it->second.position() - pm2.ascent() <= y) { pit = it->first; yy = it->second.position(); } } - LYXERR(Debug::DEBUG, "found best y: " << yy << " for pit: " << pit); + LYXERR(Debug::PAINTING, "found best y: " << yy << " for pit: " << pit); return pit; } @@ -1407,28 +1621,28 @@ void TextMetrics::setCursorFromCoordinates(Cursor & cur, int const x, int const ParagraphMetrics const & pm = par_metrics_[pit]; - int yy = pm.position() - pm.ascent(); - LYXERR(Debug::DEBUG, "x: " << x << " y: " << y << + int yy = pm.position() - pm.rows().front().ascent(); + LYXERR(Debug::PAINTING, "x: " << x << " y: " << y << " pit: " << pit << " yy: " << yy); int r = 0; LBUFERR(pm.rows().size()); for (; r < int(pm.rows().size()) - 1; ++r) { Row const & row = pm.rows()[r]; - if (int(yy + row.height()) > y) + if (yy + row.height() > y) break; yy += row.height(); } Row const & row = pm.rows()[r]; - LYXERR(Debug::DEBUG, "row " << r << " from pos: " << row.pos()); + LYXERR(Debug::PAINTING, "row " << r << " from pos: " << row.pos()); bool bound = false; int xx = x; pos_type const pos = getPosNearX(row, xx, bound); - LYXERR(Debug::DEBUG, "setting cursor pit: " << pit << " pos: " << pos); + LYXERR(Debug::PAINTING, "setting cursor pit: " << pit << " pos: " << pos); text_->setCursor(cur, pit, pos, true, bound); // remember new position. @@ -1442,19 +1656,19 @@ InsetList::Element * TextMetrics::checkInsetHit(pit_type pit, int x, int y) Paragraph const & par = text_->paragraphs()[pit]; CoordCache::Insets const & insetCache = bv_->coordCache().getInsets(); - LYXERR(Debug::DEBUG, "x: " << x << " y: " << y << " pit: " << pit); + LYXERR(Debug::PAINTING, "x: " << x << " y: " << y << " pit: " << pit); for (InsetList::Element const & e : par.insetList()) { - LYXERR(Debug::DEBUG, "examining inset " << e.inset); + LYXERR(Debug::PAINTING, "examining inset " << e.inset); if (insetCache.covers(e.inset, x, y)) { - LYXERR(Debug::DEBUG, "Hit inset: " << e.inset); + LYXERR(Debug::PAINTING, "Hit inset: " << e.inset); return const_cast(&e); } } - LYXERR(Debug::DEBUG, "No inset hit. "); - return 0; + LYXERR(Debug::PAINTING, "No inset hit. "); + return nullptr; } @@ -1570,22 +1784,10 @@ void TextMetrics::deleteLineForward(Cursor & cur) } -bool TextMetrics::isLastRow(Row const & row) const -{ - ParagraphList const & pars = text_->paragraphs(); - return row.endpos() >= pars[row.pit()].size() - && row.pit() + 1 == pit_type(pars.size()); -} - - -bool TextMetrics::isFirstRow(Row const & row) const -{ - return row.pos() == 0 && row.pit() == 0; -} - - int TextMetrics::leftMargin(pit_type pit) const { + // FIXME: what is the semantics? It depends on whether the + // paragraph is empty! return leftMargin(pit, text_->paragraphs()[pit].size()); } @@ -1598,7 +1800,10 @@ int TextMetrics::leftMargin(pit_type const pit, pos_type const pos) const LASSERT(pit < int(pars.size()), return 0); Paragraph const & par = pars[pit]; LASSERT(pos >= 0, return 0); - LASSERT(pos <= par.size(), return 0); + // We do not really care whether pos > par.size(), since we do not + // access the data. It can be actually useful, when querying the + // margin without indentation (see leftMargin(pit_type). + Buffer const & buffer = bv_->buffer(); //lyxerr << "TextMetrics::leftMargin: pit: " << pit << " pos: " << pos << endl; DocumentClass const & tclass = buffer.params().documentClass(); @@ -1643,11 +1848,13 @@ int TextMetrics::leftMargin(pit_type const pit, pos_type const pos) const } } - // This happens after sections or environments in standard classes. - // We have to check the previous layout at same depth. + // Check for reasons to remove indentation. + // First, at document level. if (buffer.params().paragraph_separation == BufferParams::ParagraphSkipSeparation) parindent.erase(); + // This happens after sections or environments in standard classes. + // We have to check the previous layout at same depth. else if (pit > 0 && pars[pit - 1].getDepth() >= par.getDepth()) { pit_type prev = text_->depthHook(pit, par.getDepth()); if (par.layout() == pars[prev].layout()) { @@ -1657,6 +1864,15 @@ int TextMetrics::leftMargin(pit_type const pit, pos_type const pos) const } else if (pars[prev].layout().nextnoindent) parindent.erase(); } + // The previous paragraph may have ended with a separator inset. + if (pit > 0) { + Paragraph const & ppar = pars[pit - 1]; + if (ppar.size() > 0) { + auto * in = dynamic_cast(ppar.getInset(ppar.size() - 1)); + if (in != nullptr && in->nextnoindent()) + parindent.erase(); + } + } FontInfo const labelfont = text_->labelFont(par); FontMetrics const & lfm = theFontMetrics(labelfont); @@ -1713,30 +1929,15 @@ int TextMetrics::leftMargin(pit_type const pit, pos_type const pos) const } break; - case MARGIN_RIGHT_ADDRESS_BOX: { -#if 0 - // The left margin depends on the widest row in this paragraph. - // This code is wrong because it depends on the rows, but at the - // same time this function is used in redoParagraph to construct - // the rows. - ParagraphMetrics const & pm = par_metrics_[pit]; - int minfill = max_width_; - for (row : pm.rows()) - if (row.fill() < minfill) - minfill = row.fill(); - l_margin += bfm.signedWidth(layout.leftmargin); - l_margin += minfill; -#endif - // also wrong, but much shorter. - l_margin += max_width_ / 2; + case MARGIN_RIGHT_ADDRESS_BOX: + // This is handled globally in redoParagraph(). break; } - } if (!par.params().leftIndent().zero()) l_margin += par.params().leftIndent().inPixels(max_width_, lfm.em()); - LyXAlignment align = par.getAlign(); + LyXAlignment align = par.getAlign(bv_->buffer().params()); // set the correct parindent if (pos == 0 @@ -1750,10 +1951,10 @@ int TextMetrics::leftMargin(pit_type const pit, pos_type const pos) const && !par.params().noindent() // in some insets, paragraphs are never indented && !text_->inset().neverIndent() - // display style insets are always centered, omit indentation + // display style insets do not need indentation && !(!par.empty() - && par.isInset(pos) - && par.getInset(pos)->display()) + && par.isInset(0) + && par.getInset(0)->rowFlags() & Display) && (!(tclass.isDefaultLayout(par.layout()) || tclass.isPlainLayout(par.layout())) || buffer.params().paragraph_separation @@ -1799,6 +2000,10 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type const pit, int const if (pm.rows().empty()) return; size_t const nrows = pm.rows().size(); + int const wh = bv_->workHeight(); + // Remember left and right margin for drawing math numbers + Changer changeleft = changeVar(pi.leftx, x + leftMargin(pit)); + Changer changeright = changeVar(pi.rightx, x + width() - rightMargin(pit)); // Use fast lane in nodraw stage. if (pi.pain.isNull()) { @@ -1810,15 +2015,17 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type const pit, int const if (i) y += row.ascent(); - RowPainter rp(pi, *text_, row, row_x, y); - - rp.paintOnlyInsets(); + // It is not needed to draw on screen if we are not inside + bool const inside = (y + row.descent() >= 0 && y - row.ascent() < wh); + if (inside) { + RowPainter rp(pi, *text_, row, row_x, y); + rp.paintOnlyInsets(); + } y += row.descent(); } return; } - int const ww = bv_->workHeight(); Cursor const & cur = bv_->cursor(); DocIterator sel_beg = cur.selectionBegin(); DocIterator sel_end = cur.selectionEnd(); @@ -1845,6 +2052,12 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type const pit, int const } } + if (text_->isRTL(pit)) + swap(pi.leftx, pi.rightx); + + BookmarksSection::BookmarkPosList bpl = + theSession().bookmarks().bookmarksInPar(bv_->buffer().fileName(), pm.id()); + for (size_t i = 0; i != nrows; ++i) { Row const & row = pm.rows()[i]; @@ -1855,7 +2068,7 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type const pit, int const // It is not needed to draw on screen if we are not inside. bool const inside = (y + row.descent() >= 0 - && y - row.ascent() < ww); + && y - row.ascent() < wh); if (!inside) { // Inset positions have already been set in nodraw stage. y += row.descent(); @@ -1876,12 +2089,8 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type const pit, int const row.change(row.end_margin_sel, sel_end.pit() > pit); } - // has row changed since last paint? - bool row_has_changed = row.changed() - || bv_->hadHorizScrollOffset(text_, pit, row.pos()); - // Take this opportunity to spellcheck the row contents. - if (row_has_changed && pi.do_spellcheck && lyxrc.spellcheck_continuously) { + if (row.changed() && pi.do_spellcheck && lyxrc.spellcheck_continuously) { text_->getPar(pit).spellCheck(); } @@ -1889,10 +2098,13 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type const pit, int const // Don't paint the row if a full repaint has not been requested // and if it has not changed. - if (!pi.full_repaint && !row_has_changed) { + if (!pi.full_repaint && !row.changed()) { // Paint only the insets if the text itself is // unchanged. rp.paintOnlyInsets(); + rp.paintTooLargeMarks( + row_x + row.left_x() < bv_->leftMargin(), + row_x + row.right_x() > bv_->workWidth() - bv_->rightMargin()); row.changed(false); y += row.descent(); continue; @@ -1900,32 +2112,24 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type const pit, int const // Clear background of this row if paragraph background was not // already cleared because of a full repaint. - if (!pi.full_repaint && row_has_changed) { + if (!pi.full_repaint && row.changed()) { LYXERR(Debug::PAINTING, "Clear rect@(" - << max(row_x, 0) << ", " << y - row.ascent() << ")=" + << x << ", " << y - row.ascent() << ")=" << width() << " x " << row.height()); - // FIXME: this is a hack. We know that at least this - // amount of pixels can be cleared on right and left. - // Doing so gets rid of caret ghosts when the cursor is at - // the begining/end of row. However, it will not work if - // the caret has a ridiculous width like 6. (see ticket - // #10797) - pi.pain.fillRectangle(max(row_x, 0) - Inset::TEXT_TO_INSET_OFFSET, - y - row.ascent(), - width() + 2 * Inset::TEXT_TO_INSET_OFFSET, - row.height(), pi.background_color); + pi.pain.fillRectangle(x, y - row.ascent(), + width(), row.height(), pi.background_color); } // Instrumentation for testing row cache (see also // 12 lines lower): if (lyxerr.debugging(Debug::PAINTING) - && (row.selection() || pi.full_repaint || row_has_changed)) { + && (row.selection() || pi.full_repaint || row.changed())) { string const foreword = text_->isMainText() ? "main text redraw " : "inset text redraw: "; LYXERR0(foreword << "pit=" << pit << " row=" << i << (row.selection() ? " row_selection": "") << (pi.full_repaint ? " full_repaint" : "") - << (row_has_changed ? " row_has_changed" : "")); + << (row.changed() ? " row.changed" : "")); } // Backup full_repaint status and force full repaint @@ -1938,17 +2142,20 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type const pit, int const rp.paintDepthBar(); if (row.needsChangeBar()) rp.paintChangeBar(); - if (i == 0 && !row.isRTL()) + if (i == 0) rp.paintFirst(); - if (i == nrows - 1 && row.isRTL()) + if (i == nrows - 1) rp.paintLast(); rp.paintText(); - if (i == nrows - 1 && !row.isRTL()) - rp.paintLast(); - if (i == 0 && row.isRTL()) - rp.paintFirst(); - rp.paintTooLargeMarks(row_x + row.left_x() < 0, - row_x + row.right_x() > bv_->workWidth()); + rp.paintTooLargeMarks( + row_x + row.left_x() < bv_->leftMargin(), + row_x + row.right_x() > bv_->workWidth() - bv_->rightMargin()); + // indicate bookmarks presence in margin + if (lyxrc.bookmarks_visibility == LyXRC::BMK_MARGIN) + for (auto const & bp_p : bpl) + if (bp_p.second >= row.pos() && bp_p.second < row.endpos()) + rp.paintBookmark(bp_p.first); + y += row.descent(); #if 0 @@ -1960,7 +2167,7 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type const pit, int const static int count = 0; ++count; FontInfo fi(sane_font); - fi.setSize(FONT_SIZE_TINY); + fi.setSize(TINY_SIZE); fi.setColor(Color_red); pi.pain.text(row_x, y, convert(count), fi); #endif @@ -1978,26 +2185,22 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type const pit, int const void TextMetrics::completionPosAndDim(Cursor const & cur, int & x, int & y, Dimension & dim) const { - Cursor const & bvcur = cur.bv().cursor(); - - // get word in front of cursor - docstring word = text_->previousWord(bvcur.top()); - DocIterator wordStart = bvcur; - wordStart.pos() -= word.length(); + DocIterator from = cur.bv().cursor(); + DocIterator to = from; + text_->getWord(from.top(), to.top(), PREVIOUS_WORD); - // calculate dimensions of the word - Row row; - row.pit(bvcur.pit()); - row.pos(wordStart.pos()); - row.endpos(bvcur.pos()); - setRowHeight(row); - dim = row.dimension(); + // The vertical dimension of the word + Font const font = displayFont(cur.pit(), from.pos()); + FontMetrics const & fm = theFontMetrics(font); + // the +1's below are related to the extra pixels added in setRowHeight + dim.asc = fm.maxAscent() + 1; + dim.des = fm.maxDescent() + 1; // get position on screen of the word start and end //FIXME: Is it necessary to explicitly set this to false? - wordStart.boundary(false); - Point lxy = cur.bv().getPos(wordStart); - Point rxy = cur.bv().getPos(bvcur); + from.boundary(false); + Point lxy = cur.bv().getPos(from); + Point rxy = cur.bv().getPos(to); dim.wid = abs(rxy.x_ - lxy.x_); // calculate position of word