diff --git a/src/data/field/text.cpp b/src/data/field/text.cpp index 4e33dc9a..bc75a2bc 100644 --- a/src/data/field/text.cpp +++ b/src/data/field/text.cpp @@ -38,16 +38,21 @@ IMPLEMENT_REFLECTION(TextField) { // ----------------------------------------------------------------------------- : TextStyle TextLayoutP dummy_layout() { - auto layout = make_intrusive(); - auto line = make_intrusive(0, 0, 0, LineLayout::Type::LINE); + auto layout = make_intrusive(); + auto line = make_intrusive(0, 0, 0, LineLayout::Type::LINE); + auto clause = make_intrusive(0, 0, 0, LineLayout::Type::CLAUSE); auto paragraph = make_intrusive(0, 0, 0, LineLayout::Type::PARAGRAPH); - auto block = make_intrusive(0, 0, 0, LineLayout::Type::BLOCK); + auto block = make_intrusive(0, 0, 0, LineLayout::Type::BLOCK); + clause ->lines.push_back(line); paragraph->lines.push_back(line); - block->lines.push_back(line); - layout->lines.push_back(line); - block->paragraphs.push_back(paragraph); - layout->paragraphs.push_back(paragraph); - layout->blocks.push_back(block); + block ->lines.push_back(line); + layout ->lines.push_back(line); + paragraph->clauses.push_back(clause); + block ->clauses.push_back(clause); + layout ->clauses.push_back(clause); + block ->paragraphs.push_back(paragraph); + layout ->paragraphs.push_back(paragraph); + layout ->blocks.push_back(block); return layout; } @@ -129,6 +134,7 @@ void LineLayout::reflect(GetMember& handler) const { REFLECT_N("bottom", bottom()); REFLECT_N("middle", middle()); if (type > Type::LINE) REFLECT(lines); + if (type > Type::CLAUSE) REFLECT(clauses); if (type > Type::PARAGRAPH) REFLECT(paragraphs); if (type > Type::BLOCK) REFLECT(blocks); } diff --git a/src/data/field/text.hpp b/src/data/field/text.hpp index 9a967ac4..f54c343e 100644 --- a/src/data/field/text.hpp +++ b/src/data/field/text.hpp @@ -50,8 +50,8 @@ public: class LineLayout : public IntrusivePtrVirtualBase { public: double width, top, height; - enum class Type { LINE, PARAGRAPH, BLOCK, ALL } type; - vector lines, paragraphs, blocks; + enum class Type { LINE, CLAUSE, PARAGRAPH, BLOCK, ALL } type; + vector lines, clauses, paragraphs, blocks; LineLayout() {} LineLayout(double width, double top, double height, Type type) : width(width), top(top), height(height), type(type) {} diff --git a/src/render/text/element.cpp b/src/render/text/element.cpp index 26c14ac2..7595b304 100644 --- a/src/render/text/element.cpp +++ b/src/render/text/element.cpp @@ -46,16 +46,20 @@ struct TextElementsFromString { const TextStyle& style; Context& ctx; + vector& clauses; vector& paragraphs; TextElementsFromString(TextElements& out, const String& text, const TextStyle& style, Context& ctx) - : style(style), ctx(ctx), paragraphs(out.paragraphs) + : style(style), ctx(ctx), clauses(out.clauses), paragraphs(out.paragraphs) { out.start = 0; out.end = text.size(); + clauses.emplace_back(); + clauses.back().start = 0; paragraphs.emplace_back(); paragraphs.back().start = 0; fromString(out.children, text, 0, text.size()); + clauses.back().end = text.size(); paragraphs.back().end = text.size(); } @@ -70,7 +74,7 @@ private: if (text_start < pos) { // text element before this tag? addText(elements, text, text_start, pos); - addParagraphs(text, text_start, pos); + addClausesAndParagraphs(text, text_start, pos); } // a (formatting) tag size_t tag_start = pos; @@ -175,7 +179,8 @@ private: } else if (is_tag(text, tag_start, _(":"), colon + 1); size_t colon3 = colon2 < pos-1 ? text.find_first_of(_(">:"), colon2 + 1) : colon2; Margins m = {0.,0.,0.}; - text.substr(colon + 1, colon2 - colon - 2).ToDouble(&m.left); + text.substr(colon + 1, colon2 - colon - 2).ToDouble(&m.left); text.substr(colon2 + 1, colon3 - colon2 - 2).ToDouble(&m.right); - text.substr(colon3 + 1, pos - colon3 - 2).ToDouble(&m.top); + text.substr(colon3 + 1, pos - colon3 - 2).ToDouble(&m.top); if (!margins.empty()) { m.left += margins.back().left; m.right += margins.back().right; m.top += margins.back().top; } margins.emplace_back(m); - paragraphs.back().margin_left = m.left; - paragraphs.back().margin_right = m.right; - paragraphs.back().margin_top = m.top; + clauses.back().margin_left = m.left; + clauses.back().margin_right = m.right; + clauses.back().margin_top = m.top; } } else if (is_tag(text, tag_start, _(" 0 ? DRAW_ACTIVE : DRAW_NORMAL; - LineBreak line_break = line > 0 ? LineBreak::LINE : - soft_line > 0 ? LineBreak::SOFT : LineBreak::HARD; + LineBreak line_break = line > 0 ? LineBreak::LINE : + soft_line > 0 ? LineBreak::SOFT : + LineBreak::HARD; if (kwpph > 0 || param > 0) { // bracket the text content = String(LEFT_ANGLE_BRACKET) + content + RIGHT_ANGLE_BRACKET; @@ -285,25 +292,28 @@ private: } } } - // Find paragraph breaks in text - void addParagraphs(const String& text, size_t start, size_t end) { - if (line == 0 && soft_line > 0) return; + // Find clause and paragraph breaks in text + void addClausesAndParagraphs(const String& text, size_t start, size_t end) { for (size_t i = start; i < end; ++i) { wxUniChar c = text.GetChar(i); - if (c == '\n') { + if (c == '\n') { + clauses.back().end = i + 1; + clauses.emplace_back(); + clauses.back().start = i + 1; + if (!margins.empty()) { + clauses.back().margin_left = margins.back().left; + clauses.back().margin_right = margins.back().right; + clauses.back().margin_top = margins.back().top; + } + if (line < 1 && soft_line > 0) continue; paragraphs.back().end = i + 1; paragraphs.emplace_back(); paragraphs.back().start = i + 1; paragraphs.back().margin_before_bullet = i + 1; paragraphs.back().margin_after_bullet = i + 1; - if (!margins.empty()) { - paragraphs.back().margin_left = margins.back().left; - paragraphs.back().margin_right = margins.back().right; - paragraphs.back().margin_top = margins.back().top; - } if (!aligns.empty()) { paragraphs.back().alignment = aligns.back(); - } + } } } } @@ -333,6 +343,7 @@ private: void TextElements::clear() { children.clear(); + clauses.clear(); paragraphs.clear(); } diff --git a/src/render/text/element.hpp b/src/render/text/element.hpp index c7c5a048..13c16f5e 100644 --- a/src/render/text/element.hpp +++ b/src/render/text/element.hpp @@ -25,19 +25,20 @@ class SymbolFontRef; /// Information on a linebreak enum class LineBreak { NO, // no line break ever - MAYBE, // break here when in "direction:vertical" mode - SPACE, // optional line break (' ') - SOFT, // always a line break, spacing as a soft break, doesn't end paragraphs - HARD, // always a line break ('\n') - LINE, // line break with a separator line () + MAYBE, // break here when in "direction:vertical" mode (break as WRAP) + SPACE, // optional line break, spacing as a soft break, ends a line, ( ) + WRAP, // always a line break, spacing as a soft break, ends a line, ( ) + SOFT, // always a line break, spacing as a soft break, ends a clause, (\n) + HARD, // always a line break, spacing as a hard break, ends a paragraph, (\n) + LINE, // always a line break, spacing as a line break, ends a block, (\n), has separator }; /// Information on a character in a TextElement struct CharInfo { - RealSize size; ///< Size of this character - LineBreak break_after : 16; ///< How/when to break after it? - bool soft : 1; ///< Is this a 'soft' character? soft characters are ignored for alignment - bool bullet : 1; ///< Is this a bullet point? + RealSize size; ///< Size of this character + LineBreak break_after; ///< How/when to break after it? + bool soft : 1; ///< Is this a 'soft' character? soft characters are ignored for alignment + bool bullet : 1; ///< Is this a bullet point? explicit CharInfo() : break_after(LineBreak::NO), soft(true), bullet(false) @@ -146,14 +147,20 @@ public: // ----------------------------------------------------------------------------- : TextElements +class TextClause { +public: + double margin_left = 0., margin_right = 0., margin_top = 0.; + size_t start = String::npos, end = String::npos; +}; + class TextParagraph { public: - optional alignment; - double margin_left = 0., margin_right = 0.; - double margin_top = 0.; //, margin_bottom = 0.; // TODO: more margin options? - size_t start = String::npos, end = String::npos; + optional alignment; + bool before_bullet_found = false; + bool after_bullet_found = false; size_t margin_before_bullet = 0; // position of the bullet tag - size_t margin_after_bullet = 0; // position of the first character after the bullet tag + size_t margin_after_bullet = 0; // position of the first character after the bullet tag + size_t start = String::npos, end = String::npos; }; /// A list of text elements extracted from a string @@ -161,8 +168,12 @@ class TextElements : public CompoundTextElement { public: TextElements() : CompoundTextElement(String::npos,String::npos) {} - /// Information on the paragraphs/blocks in the string - /// Text segments separated by newlines are considered paragraphs + /// Information on the clauses/paragraphs/blocks in the string + /// Text segments separated by wrapping are considered lines + /// Text segments separated by soft newlines are considered clauses + /// Text segments separated by hard newlines are considered paragraphs + /// Text segments separated by line newlines are considered blocks + vector clauses; vector paragraphs; void clear(); diff --git a/src/render/text/viewer.cpp b/src/render/text/viewer.cpp index dbc644fa..ee63acc5 100644 --- a/src/render/text/viewer.cpp +++ b/src/render/text/viewer.cpp @@ -21,9 +21,9 @@ struct TextViewer::Line { LineBreak break_after; ///< Is there a saparator after this line? optional alignment; ///< Alignment of this line bool justifying; ///< Is the text justified? Only true when *really* justifying. - double margin_left; ///< Left margin including the margin tag and bullet point - double margin_left_before_bullet; ///< Left margin but just before the bullet point - double margin_right; ///< Rightmargin + double margin_left_after_bullet; ///< Left margin including the margin tag and bullet point + double margin_left_before_bullet; ///< Left margin including the margin tag but just before the bullet point + double margin_right; ///< Right margin bool bullet; ///< Does this line start with a bullet point? Line() @@ -357,7 +357,7 @@ void update_size(LineLayout& layout, TextViewer::Line const& l) { TextLayoutP TextViewer::extractLayoutInfo() const { // store information about the content/layout TextLayoutP layout = make_intrusive(); - LineLayoutP paragraph, block; + LineLayoutP clause, paragraph, block; for (auto const& l : lines) { LineLayoutP line = make_intrusive(l.width(), l.top, l.line_height, LineLayout::Type::LINE); if (!block) { @@ -370,19 +370,30 @@ TextLayoutP TextViewer::extractLayoutInfo() const { paragraph->type = LineLayout::Type::PARAGRAPH; block->paragraphs.push_back(paragraph); layout->paragraphs.push_back(paragraph); + } + if (!clause) { + clause = make_intrusive(*line); + clause->type = LineLayout::Type::CLAUSE; + paragraph->clauses.push_back(clause); + block->clauses.push_back(clause); + layout->clauses.push_back(clause); } + clause->lines.push_back(line); paragraph->lines.push_back(line); block->lines.push_back(line); layout->lines.push_back(line); if (l.line_height > 0) { + update_size(*clause, l); update_size(*paragraph, l); update_size(*block, l); update_size(*layout, l); } if (l.break_after == LineBreak::LINE) { - paragraph = block = nullptr; + clause = paragraph = block = nullptr; } else if (l.break_after == LineBreak::HARD) { - paragraph = nullptr; + clause = paragraph = nullptr; + } else if (l.break_after == LineBreak::SOFT) { + clause = nullptr; } } for (size_t i=0; i+1 < layout->blocks.size() ; ++i) { @@ -547,7 +558,7 @@ void TextViewer::prepareLinesTryScales(RotatedDC& dc, const String& text, const // Try to fit a blank line in the masked image, move down until it fits RealSize TextViewer::fitLineWidth(Line& line, RotatedDC& dc, const TextStyle& style) const { - double margin_left = line.bullet ? line.margin_left_before_bullet : line.margin_left; + double margin_left = line.bullet ? line.margin_left_before_bullet : line.margin_left_after_bullet; RealSize line_size(margin_left + lineLeft(dc, style, line.top), 0); while (line.top < dc.getHeight() && line_size.width + 1 >= dc.getWidth() - style.padding_right - line.margin_right) { // nothing fits on this line, move down one pixel @@ -561,16 +572,20 @@ bool TextViewer::prepareLinesAtScale(RotatedDC& dc, const vector& char // Try to layout the text at the current scale lines.clear(); + // The current "clause" in the input string + size_t i_clause = 0; + assert(elements.clauses.size() > 0); + // The current "paragraph" in the input string size_t i_para = 0; assert(elements.paragraphs.size() > 0); // first line Line line; - line.top = style.padding_top; - line.margin_left = elements.paragraphs[0].margin_left; - line.margin_left_before_bullet = elements.paragraphs[0].margin_left; - line.margin_right = elements.paragraphs[0].margin_right; + line.top = style.padding_top + elements.clauses[0].margin_top; + line.margin_left_after_bullet = elements.clauses[0].margin_left; + line.margin_left_before_bullet = elements.clauses[0].margin_left; + line.margin_right = elements.clauses[0].margin_right; line.alignment = elements.paragraphs[0].alignment; // size of the line so far RealSize line_size = fitLineWidth(line, dc, style); @@ -584,14 +599,26 @@ bool TextViewer::prepareLinesAtScale(RotatedDC& dc, const vector& char // For each character ... for (size_t i = 0 ; i < chars.size() ; ++i) { const CharInfo& c = chars[i]; + assert(i_clause < elements.clauses.size()); assert(i_para < elements.paragraphs.size()); - assert(c.size.width == 0 || elements.paragraphs[i_para].start <= i && i < elements.paragraphs[i_para].end); + assert(c.size.width == 0 || elements.paragraphs[i_para].start <= i && i < elements.paragraphs[i_para].end); + // If we found the paragraph's bullet point, calculate the margins + if (i == elements.paragraphs[i_para].margin_after_bullet) { + for (size_t j = line.start; j < elements.paragraphs[i_para].margin_after_bullet; ++j) { + line.margin_left_after_bullet += chars[j].size.width; + } + } + if (i == elements.paragraphs[i_para].margin_before_bullet) { + for (size_t j = line.start; j < elements.paragraphs[i_para].margin_before_bullet; ++j) { + line.margin_left_before_bullet += chars[j].size.width; + } + } // Should we break? bool word_too_long = false; bool break_now = false; bool accept_word = false; // the current word should be added to the line bool hide_breaker = true; // hide the \n or _(' ') that caused a line break - if (c.break_after == LineBreak::SOFT || c.break_after == LineBreak::HARD || c.break_after == LineBreak::LINE) { + if (c.break_after >= LineBreak::WRAP) { break_now = true; accept_word = true; line.break_after = c.break_after; @@ -599,10 +626,10 @@ bool TextViewer::prepareLinesAtScale(RotatedDC& dc, const vector& char // Soft break == end of word accept_word = true; } else if (c.break_after == LineBreak::MAYBE && style.direction == TOP_TO_BOTTOM) { - break_now = true; - accept_word = true; + break_now = true; + accept_word = true; hide_breaker = false; - line.break_after = LineBreak::SOFT; + line.break_after = LineBreak::WRAP; } // Add size of the character if (c.break_after != LineBreak::LINE) { @@ -612,12 +639,6 @@ bool TextViewer::prepareLinesAtScale(RotatedDC& dc, const vector& char } positions_word.push_back(word_size.width); if (!c.soft) word_end_or_soft = i + 1; - if (i < elements.paragraphs[i_para].margin_after_bullet) { - line.margin_left += c.size.width; // character in left margin - if (i < elements.paragraphs[i_para].margin_before_bullet) { - line.margin_left_before_bullet += c.size.width; - } - } // Did the word become too long? if (!break_now) { double max_width = lineRight(dc, style, line.top) - line.margin_right; @@ -635,12 +656,12 @@ bool TextViewer::prepareLinesAtScale(RotatedDC& dc, const vector& char accept_word = true; hide_breaker = false; word_too_long = true; - line.break_after = LineBreak::SOFT; + line.break_after = LineBreak::WRAP; } } else { // line would become too long, break before the current word break_now = true; - line.break_after = LineBreak::SOFT; + line.break_after = LineBreak::WRAP; } } } @@ -688,20 +709,28 @@ bool TextViewer::prepareLinesAtScale(RotatedDC& dc, const vector& char line.top += line.line_height * line_height_multiplier; line.start = word_start; line.positions.clear(); - if (line.break_after == LineBreak::LINE) line.line_height = 0; + if (line.break_after >= LineBreak::SOFT) { + // end of clause + assert(elements.clauses[i_clause].end == i + 1); + assert(i_clause + 1 < elements.clauses.size()); + if (i_clause + 1 < elements.clauses.size()) ++i_clause; + assert(elements.clauses[i_clause].start == i + 1); + line.margin_right = elements.clauses[i_clause].margin_right; + //if (line.start == elements.clauses[i_clause].start) { + line.top += elements.clauses[i_clause].margin_top; + //} + } if (line.break_after >= LineBreak::HARD) { // end of paragraph - // look at next paragraph assert(elements.paragraphs[i_para].end == i + 1); assert(i_para + 1 < elements.paragraphs.size()); - if (i_para+1 < elements.paragraphs.size()) ++i_para; + if (i_para + 1 < elements.paragraphs.size()) ++i_para; assert(elements.paragraphs[i_para].start == i + 1); - line.margin_left = elements.paragraphs[i_para].margin_left; - line.margin_left_before_bullet = elements.paragraphs[i_para].margin_left; - line.margin_right = elements.paragraphs[i_para].margin_right; - line.top += elements.paragraphs[i_para].margin_top; + line.margin_left_after_bullet = elements.clauses[i_clause].margin_left; + line.margin_left_before_bullet = elements.clauses[i_clause].margin_left; line.alignment = elements.paragraphs[i_para].alignment; - } + } + if (line.break_after == LineBreak::LINE) line.line_height = 0; line.break_after = LineBreak::NO; // is the first visible character of the line a bullet point? line.bullet = false; @@ -811,9 +840,9 @@ void TextViewer::alignParagraph(size_t start_line, size_t end_line, const vector double sum = 0; for (size_t li = start_line ; li < end_line ; ++li) { const Line& l = lines[li]; - if ((soft && l.break_after == LineBreak::SOFT) - || (hard && l.break_after == LineBreak::HARD) - || (line && l.break_after == LineBreak::LINE)) sum += l.line_height; + if ((soft && (l.break_after == LineBreak::SOFT || l.break_after == LineBreak::WRAP)) + || (hard && l.break_after == LineBreak::HARD) + || (line && l.break_after == LineBreak::LINE)) sum += l.line_height; } if (sum == 0) break; // how much do we need to add? @@ -824,9 +853,9 @@ void TextViewer::alignParagraph(size_t start_line, size_t end_line, const vector Line& l = lines[li]; l.top += add; // adjust next line by.. - if ((soft && l.break_after == LineBreak::SOFT) - || (hard && l.break_after == LineBreak::HARD) - || (line && l.break_after == LineBreak::LINE)) add += to_add * l.line_height; + if ((soft && (l.break_after == LineBreak::SOFT || l.break_after == LineBreak::WRAP)) + || (hard && l.break_after == LineBreak::HARD) + || (line && l.break_after == LineBreak::LINE)) add += to_add * l.line_height; } height += add; } @@ -847,12 +876,12 @@ void TextViewer::alignParagraph(size_t start_line, size_t end_line, const vector } void TextViewer::Line::alignHorizontal(const vector& chars, const TextStyle& style, const RealRect& s) { - double margin_bullet = bullet ? margin_left_before_bullet : margin_left; + double margin_bullet = bullet ? margin_left_before_bullet : margin_left_after_bullet; double width = this->width() - margin_bullet; double target_width = s.width - margin_bullet - margin_right; Alignment alignment = this->alignment.value_or(style.alignment); bool should_fill = (alignment & ALIGN_IF_OVERFLOW ? width > target_width : true) - && (alignment & ALIGN_IF_SOFTBREAK ? break_after == LineBreak::SOFT || !style.field().multi_line : true); + && (alignment & ALIGN_IF_SOFTBREAK ? break_after == LineBreak::WRAP || break_after == LineBreak::SOFT || !style.field().multi_line : true); if ((alignment & ALIGN_JUSTIFY_ALL) && should_fill) { // justify text, by characters justifying = true; diff --git a/src/script/functions/json.cpp b/src/script/functions/json.cpp index 975e47db..79a4f45f 100644 --- a/src/script/functions/json.cpp +++ b/src/script/functions/json.cpp @@ -591,6 +591,7 @@ boost::json::object mse_to_json(const StyleP& style) { layoutv.emplace("content_width", wxString::Format(wxT("%.2f"), s->layout->width)); layoutv.emplace("content_height", wxString::Format(wxT("%.2f"), s->layout->height)); layoutv.emplace("content_lines", wxString::Format(wxT("%i"), s->layout->lines.size())); + layoutv.emplace("content_clauses", wxString::Format(wxT("%i"), s->layout->clauses.size())); layoutv.emplace("content_paragraphs", wxString::Format(wxT("%i"), s->layout->paragraphs.size())); layoutv.emplace("content_blocks", wxString::Format(wxT("%i"), s->layout->blocks.size())); boost::json::array separatorsv; diff --git a/src/util/alignment.hpp b/src/util/alignment.hpp index e8fe48b4..d77db1c8 100644 --- a/src/util/alignment.hpp +++ b/src/util/alignment.hpp @@ -27,7 +27,7 @@ enum Alignment { ALIGN_FILL = ALIGN_STRETCH | ALIGN_JUSTIFY_WORDS | ALIGN_JUSTIFY_ALL, // horizontal fill modifiers ALIGN_IF_OVERFLOW = 0x1000, // only fill if text_width > box_width - ALIGN_IF_SOFTBREAK = 0x2000, // only fill before soft line breaks + ALIGN_IF_SOFTBREAK = 0x2000, // only fill before soft line breaks and wraps // vertical ALIGN_TOP = 0x100, ALIGN_MIDDLE = 0x200,