X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2FTextMetrics.cpp;h=f4241181c295e15f440d78a48f05dbf6d7621d5f;hb=4ed0312c51704780af1c452d3a82a84171b3725a;hp=89f8d92075d5cd99a89a43a37456a921015200f7;hpb=a04b5c3965faf79d534f0090712d3e47cebc46ab;p=lyx.git diff --git a/src/TextMetrics.cpp b/src/TextMetrics.cpp index 89f8d92075..f4241181c2 100644 --- a/src/TextMetrics.cpp +++ b/src/TextMetrics.cpp @@ -20,35 +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/lyxlib.h" +#include "support/Changer.h" #include #include @@ -62,10 +59,6 @@ using frontend::FontMetrics; namespace { -// the somewhat arbitrary leading added between rows. This is 20% of -// the characters height, inluding the possible leading of the font. -// 20% is a standard value used by LaTeX and word processors. -double const extra_leading = 0.2; int numberOfLabelHfills(Paragraph const & par, Row const & row) { @@ -114,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 @@ -130,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(); @@ -152,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); @@ -165,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_; @@ -185,7 +232,7 @@ 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) { // create rows, but do not set alignment yet changed |= redoParagraph(pit, false); @@ -332,13 +379,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| // | | ) @@ -356,7 +403,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()); @@ -383,11 +430,12 @@ bool TextMetrics::redoParagraph(pit_type const pit, bool const align_rows) // 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); @@ -444,10 +492,11 @@ bool TextMetrics::redoParagraph(pit_type const pit, bool const align_rows) // 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)); @@ -460,7 +509,7 @@ bool TextMetrics::redoParagraph(pit_type const pit, bool const align_rows) 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_); e.inset->metrics(mi, dim); if (!insetCache.has(e.inset) || insetCache.dim(e.inset) != dim) { insetCache.add(e.inset, dim); @@ -468,74 +517,75 @@ bool TextMetrics::redoParagraph(pit_type const pit, bool const align_rows) } } - 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); + + /* 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 setRowAlignment() below. - * We do nothing when inside a table cell. - */ - if (dim_.wid < max_width_) - dim_.wid = max_width_; - } if (align_rows) setRowAlignment(row, max(dim_.wid, row.width())); - first = row.endpos(); - ++row_index; - 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); + } + + // 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; + } + } - if (row_index < pm.rows().size()) - pm.rows().resize(row_index); + // 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().dim().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().dim().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); + // Add the top/bottom space to rows and paragraph metrics pm.rows().front().dim().asc += top; - int const bottom = parBottomSpacing(pit); 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(); @@ -545,7 +595,7 @@ bool TextMetrics::redoParagraph(pit_type const pit, bool const align_rows) 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(); @@ -570,19 +620,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; } } @@ -634,7 +678,7 @@ void TextMetrics::setRowAlignment(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; @@ -642,7 +686,7 @@ void TextMetrics::setRowAlignment(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) { @@ -743,31 +787,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 { /** @@ -826,49 +845,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.dim().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)) { @@ -876,26 +893,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. @@ -906,6 +919,7 @@ bool TextMetrics::breakRow(Row & row, int const right_margin) const char_type const screen_char = (c == 0x2028) ? 0x2936 : 0x00B6; row.add(i, screen_char, *fi, par.lookupChange(i)); } else + // row elements before body are unbreakable row.add(i, c, *fi, par.lookupChange(i)); // add inline completion width @@ -923,69 +937,225 @@ 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; +} - // 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()); - 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); +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: + + // + 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(); } + + 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; +} + + +} + + +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, false, 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); + } - return need_new_row; + if (!rows.empty()) { + // Last row in paragraph is flushed + rows.back().flushed(true); + cleanupRow(rows.back(), true); + } + + return rows; } + int TextMetrics::parTopSpacing(pit_type const pit) const { Paragraph const & par = text_->getPar(pit); @@ -1035,7 +1205,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())); @@ -1093,19 +1266,38 @@ void TextMetrics::setRowHeight(Row & row) const // Initial value for ascent (useful if row is empty). Font const font = displayFont(row.pit(), row.pos()); FontMetrics const & fm = theFontMetrics(font); - int maxasc = fm.maxAscent() + fm.leading(); - int maxdes = fm.maxDescent(); + int maxasc = int(fm.maxAscent() * spacing_val); + int maxdes = int(fm.maxDescent() * spacing_val); + + // 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() * spacing_val)); + maxdes = max(maxdes, int(lfm.maxDescent() * spacing_val)); + } // Find the ascent/descent of the row contents for (Row::Element const & e : row) { - maxasc = max(maxasc, e.dim.ascent()); - maxdes = max(maxdes, e.dim.descent()); + if (e.inset) { + maxasc = max(maxasc, e.dim.ascent()); + 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)); + } } - // Add some leading (split between before and after) - int const leading = support::iround(extra_leading * (maxasc + maxdes)); - row.dim().asc = int((maxasc + leading - leading / 2) * spacing_val); - row.dim().des = int((maxdes + leading / 2) * spacing_val); + // This is nicer with box insets + ++maxasc; + ++maxdes; + + row.dim().asc = maxasc; + row.dim().des = maxdes; + + // This is useful for selections + row.contents_dim() = row.dim(); } @@ -1179,7 +1371,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; @@ -1204,35 +1396,6 @@ 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) { @@ -1250,7 +1413,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. @@ -1265,7 +1428,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. @@ -1284,7 +1447,7 @@ pit_type TextMetrics::getPitNearY(int y) 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(); } @@ -1403,7 +1566,7 @@ void TextMetrics::setCursorFromCoordinates(Cursor & cur, int const x, int const ParagraphMetrics const & pm = par_metrics_[pit]; - int yy = pm.position() - pm.ascent(); + int yy = pm.position() - pm.rows().front().ascent(); LYXERR(Debug::DEBUG, "x: " << x << " y: " << y << " pit: " << pit << " yy: " << yy); @@ -1411,7 +1574,7 @@ void TextMetrics::setCursorFromCoordinates(Cursor & cur, int const x, int const 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(); } @@ -1450,7 +1613,7 @@ InsetList::Element * TextMetrics::checkInsetHit(pit_type pit, int x, int y) } LYXERR(Debug::DEBUG, "No inset hit. "); - return 0; + return nullptr; } @@ -1566,22 +1729,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()); } @@ -1594,7 +1745,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(); @@ -1639,11 +1793,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()) { @@ -1653,6 +1809,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); @@ -1709,30 +1874,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 @@ -1746,10 +1896,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 @@ -1795,6 +1945,9 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type const pit, int const if (pm.rows().empty()) return; size_t const nrows = pm.rows().size(); + // 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()) { @@ -1841,6 +1994,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]; @@ -1872,12 +2031,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(); } @@ -1885,10 +2040,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; @@ -1896,32 +2054,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 @@ -1939,8 +2089,15 @@ void TextMetrics::drawParagraph(PainterInfo & pi, pit_type const pit, int const if (i == nrows - 1) rp.paintLast(); rp.paintText(); - 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 @@ -1952,7 +2109,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 @@ -1970,26 +2127,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.dim(); + // 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 @@ -2002,8 +2155,7 @@ void TextMetrics::completionPosAndDim(Cursor const & cur, int & x, int & y, int defaultRowHeight() { - FontMetrics const & fm = theFontMetrics(sane_font); - return support::iround(fm.maxHeight() * (1 + extra_leading) + fm.leading()); + return int(theFontMetrics(sane_font).maxHeight() * 1.2); } } // namespace lyx