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;