From e8eacac5e76791e171e09018b40d185a2ad5a772 Mon Sep 17 00:00:00 2001 From: Twan van Laarhoven Date: Sun, 10 May 2020 14:12:53 +0200 Subject: [PATCH] Expose more information about text layout. --- doc/type/style.txt | 7 ++-- doc/type/text_layout.txt | 28 ++++++++++++++++ src/data/field/text.cpp | 39 ++++++++++++++++++---- src/data/field/text.hpp | 23 +++++++++++++ src/render/text/viewer.cpp | 68 ++++++++++++++++++++++++++++++-------- src/render/text/viewer.hpp | 3 ++ 6 files changed, 145 insertions(+), 23 deletions(-) create mode 100644 doc/type/text_layout.txt diff --git a/doc/type/style.txt b/doc/type/style.txt index f42459d7..0907c6e4 100644 --- a/doc/type/style.txt +++ b/doc/type/style.txt @@ -110,9 +110,10 @@ The rest of the properties depend on the type of [[type:field]] this style is fo
The same mask image is also used to determine the size and shape of the box. To include a certain pixel in the size/shape but not allow text to be placed there, it can be made dark gray (a value less than 128). -| ^^^ @content width@ [[type:double]] ''automatic'' When read from a script, gives the width of the current content in this box. -| ^^^ @content height@ [[type:double]] ''automatic'' When read from a script, gives the height of the current content in this box. -| ^^^ @content lines@ [[type:int]] ''automatic'' When read from a script, gives the number of lines of the current content in this box. +| ^^^ @layout@ [[type:text layout]] ''automatic'' When read from a script, gives information on the layout of text in this box. +| ^^^ @content width@ [[type:double]] ''automatic'' When read from a script, gives the width of the current content in this box. Equivalent to @layout.width@ +| ^^^ @content height@ [[type:double]] ''automatic'' When read from a script, gives the height of the current content in this box. Equivalent to @layout.height@ +| ^^^ @content lines@ [[type:int]] ''automatic'' When read from a script, gives the number of lines of the current content in this box. Equivalent to @length(layout.lines)@ ! <<< <<< <<< <<< | @"choice"@,
@"multiple choice"@,
@"boolean"@ diff --git a/doc/type/text_layout.txt b/doc/type/text_layout.txt new file mode 100644 index 00000000..7853b980 --- /dev/null +++ b/doc/type/text_layout.txt @@ -0,0 +1,28 @@ +Data type: text layout info + +DOC_MSE_VERSION: since 2.0.2 + +This type contains information on rendered text. + +The text is devided into 'lines', 'paragraphs' and 'blocks. +A line is a line on the screen. +A paragraph is one or more lines, ending in an explicit line break, a "\n" in the text. +A block is one or more paragraphs, ending in a line, "\n". + +It is possible to dig deeper into blocks, for example + +> card_style.text.layout.blocks[1].lines[0].middle + +Is the middle of the first line of the second block. + +--Properties-- +! Property Type Description +| @width@ [[type:double]] Width of this line or group of lines in pixels. +| @height@ [[type:double]] Height of this line or group of lines in pixels. +| @top@ [[type:double]] Top y coordinate +| @middle@ [[type:double]] Middle y coordinate +| @bottom@ [[type:double]] Bottom y coordinate +| @lines@ [[type:list]] of [[type:text layout]]s The lines in this part of the text. +| @paragraphs@ [[type:list]] of [[type:text layout]]s The paragraphs in this part of the text. +| @blocks@ [[type:list]] of [[type:text layout]]s The blocks in this part of the text. +| @separators@ [[type:list]] of [[type:double]]s The y coordinates of separators between blocks. diff --git a/src/data/field/text.cpp b/src/data/field/text.cpp index 0fae699f..da4f7e79 100644 --- a/src/data/field/text.cpp +++ b/src/data/field/text.cpp @@ -97,11 +97,38 @@ void TextStyle::checkContentDependencies(Context& ctx, const Dependency& dep) co alignment.initDependencies(ctx, dep); } -template void reflect_content(T& handler, const TextStyle& ts) {} -template <> void reflect_content(GetMember& handler, const TextStyle& ts) { - REFLECT_N("content_width", ts.content_width); - REFLECT_N("content_height", ts.content_height); - REFLECT_N("content_lines", ts.content_lines); +template void reflect_layout(T& handler, const TextStyle& ts) {} +template <> void reflect_layout(GetMember& handler, const TextStyle& ts) { + REFLECT(ts.layout); + if (ts.layout) { + REFLECT_N("content_width", ts.layout->width); + REFLECT_N("content_height", ts.layout->height); + REFLECT_N("content_lines", ts.layout->lines.size()); + } else { + REFLECT_N("content_width", 0.); + REFLECT_N("content_height", 0.); + REFLECT_N("content_lines", 0); + } +} + +template <> void GetMember::handle(LineLayout const& obj) { obj.reflect(*this); } +template <> void GetDefaultMember::handle(LineLayout const& obj) {} +void LineLayout::reflect(GetMember& handler) const { + REFLECT(width); + REFLECT(top); + REFLECT(height); + REFLECT_N("bottom", bottom()); + REFLECT_N("middle", top + height/2); + if (type > Type::LINE) REFLECT(lines); + if (type > Type::PARAGRAPH) REFLECT(paragraphs); + if (type > Type::BLOCK) REFLECT(blocks); +} + +template <> void GetMember::handle(TextLayout const& obj) { obj.reflect(*this); } +template <> void GetDefaultMember::handle(TextLayout const& obj) {} +void TextLayout::reflect(GetMember& handler) const { + REFLECT_BASE(LineLayout); + REFLECT(separators); } IMPLEMENT_REFLECTION(TextStyle) { @@ -127,7 +154,7 @@ IMPLEMENT_REFLECTION(TextStyle) { REFLECT(line_height_line_max); REFLECT(paragraph_height); REFLECT(direction); - reflect_content(handler, *this); + reflect_layout(handler, *this); } // ----------------------------------------------------------------------------- : TextValue diff --git a/src/data/field/text.hpp b/src/data/field/text.hpp index 02ae6ae2..cf5e6b53 100644 --- a/src/data/field/text.hpp +++ b/src/data/field/text.hpp @@ -25,6 +25,8 @@ DECLARE_POINTER_TYPE(TextField); DECLARE_POINTER_TYPE(TextStyle); DECLARE_POINTER_TYPE(TextValue); DECLARE_POINTER_TYPE(TextBackground); +DECLARE_POINTER_TYPE(TextLayout); +DECLARE_POINTER_TYPE(LineLayout); /// A field for values containing tagged text class TextField : public Field { @@ -44,6 +46,26 @@ class TextField : public Field { // ----------------------------------------------------------------------------- : TextStyle +// information coming from text rendering +class LineLayout : public IntrusivePtrVirtualBase { +public: + double width, top, height; + enum class Type { LINE, PARAGRAPH, BLOCK, ALL } type; + vector lines, paragraphs, blocks; + + LineLayout() {} + LineLayout(double width, double top, double height, Type type) : width(width), top(top), height(height), type(type) {} + inline double bottom() const { return top+height; } + void reflect(GetMember& gm) const; +}; + +class TextLayout : public LineLayout { +public: + vector separators; + TextLayout() : LineLayout(0,0,0,Type::ALL) {} + void reflect(GetMember& gm) const; +}; + /// The Style for a TextField class TextStyle : public Style { public: @@ -69,6 +91,7 @@ class TextStyle : public Style { paragraph_height; ///< Fixed height of paragraphs Direction direction; ///< In what direction is text layed out? // information from text rendering + TextLayoutP layout; double content_width, content_height; ///< Size of the rendered text int content_lines; ///< Number of rendered lines diff --git a/src/render/text/viewer.cpp b/src/render/text/viewer.cpp index 68bfca15..2160fbc8 100644 --- a/src/render/text/viewer.cpp +++ b/src/render/text/viewer.cpp @@ -344,24 +344,55 @@ void TextViewer::prepareElements(const String& text, const TextStyle& style, Con // ----------------------------------------------------------------------------- : Layout + +void update_size(LineLayout& layout, TextViewer::Line const& l) { + layout.width = max(layout.width, l.width()); + layout.height = max(layout.height, l.bottom() - layout.top); +} + +TextLayoutP TextViewer::extractLayoutInfo() const { + // store information about the content/layout + TextLayoutP layout = make_intrusive(); + LineBreak last_break = BREAK_LINE; + LineLayoutP paragraph, block; + for (auto const& l : lines) { + LineLayoutP line = make_intrusive(l.width(), l.top, l.line_height, LineLayout::Type::LINE); + if (!block) { + block = make_intrusive(*line); + block->type = LineLayout::Type::BLOCK; + layout->blocks.push_back(block); + } + if (!paragraph) { + paragraph = make_intrusive(*line); + paragraph->type = LineLayout::Type::PARAGRAPH; + block->paragraphs.push_back(paragraph); + layout->paragraphs.push_back(paragraph); + } + paragraph->lines.push_back(line); + block->lines.push_back(line); + layout->lines.push_back(line); + if (l.line_height > 0) { + update_size(*paragraph, l); + update_size(*block, l); + update_size(*layout, l); + } + if (l.break_after == BREAK_LINE) { + paragraph = block = nullptr; + } else if (l.break_after == BREAK_HARD) { + paragraph = nullptr; + } + } + for (size_t i=0; i+1 < layout->blocks.size() ; ++i) { + layout->separators.push_back((layout->blocks[i]->bottom() + layout->blocks[i]->top)/2); + } + return layout; +} + void TextViewer::prepareLines(RotatedDC& dc, const String& text, TextStyle& style, Context& ctx) { vector chars; prepareLinesTryScales(dc, text, style, chars); assert(!lines.empty()); - // store information about the content/layout, allow this to change alignment - style.content_width = 0; - FOR_EACH(l, lines) { - style.content_width = max(style.content_width, l.width()); - } - style.content_height = 0; - FOR_EACH_REVERSE(l, lines) { - style.content_height = l.top + l.line_height; - if (l.line_height) break; // not an empty line - } - style.content_lines = (int)lines.size(); - style.alignment.update(ctx); // allow this to affect the alignment - // no text, find a dummy height for the single line we have if (lines.size() == 1 && lines[0].width() < 0.0001) { if (style.always_symbol && style.symbol_font.valid()) { @@ -372,6 +403,12 @@ void TextViewer::prepareLines(RotatedDC& dc, const String& text, TextStyle& styl } } + // store information about the content/layout, allow this to change alignment + if (style.alignment.isScripted()) { + style.layout = extractLayoutInfo(); + style.alignment.update(ctx); // allow this to affect the alignment + } + // align alignLines(dc, chars, style); @@ -382,6 +419,9 @@ void TextViewer::prepareLines(RotatedDC& dc, const String& text, TextStyle& styl lines[0].line_height = h; lines[0].top -= h; } + + // make layout available to scripts + style.layout = extractLayoutInfo(); } // bound on max_scale, given that scale fits and produces the given lines @@ -465,7 +505,7 @@ void TextViewer::prepareLinesTryScales(RotatedDC& dc, const String& text, const max_scale = min(max_scale, bound_on_max_scale(dc,style,lines,scale)); } - // The common case optimization fialed, try a binary search + // The common case optimization failed, try a binary search // Invariant: // a. The text fits at min_scale (or we force it anyway) // b. but not at max_scale diff --git a/src/render/text/viewer.hpp b/src/render/text/viewer.hpp index cb3e2f54..576c053d 100644 --- a/src/render/text/viewer.hpp +++ b/src/render/text/viewer.hpp @@ -145,6 +145,9 @@ class TextViewer { /// Align the lines of a single paragraph (a set of lines) void alignParagraph(size_t start_line, size_t end_line, const vector& chars, const TextStyle& style, const RealRect& box); + /// Make layout info available to scripts + TextLayoutP extractLayoutInfo() const; + /// Find the line the given index is on, returns the first line if the index is not found const Line& findLine(size_t index) const;