Expose more information about text layout.

This commit is contained in:
Twan van Laarhoven
2020-05-10 14:12:53 +02:00
parent 1c35183839
commit e8eacac5e7
6 changed files with 145 additions and 23 deletions
+4 -3
View File
@@ -110,9 +110,10 @@ The rest of the properties depend on the type of [[type:field]] this style is fo
<img src="style-text-mask.png" alt=""/><br/> <img src="style-text-mask.png" alt=""/><br/>
The same mask image is also used to determine the size and shape of the box. 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). 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. | ^^^ @layout@ [[type:text layout]] ''automatic'' When read from a script, gives information on the layout of text in this box.
| ^^^ @content height@ [[type:double]] ''automatic'' When read from a script, gives the height of the current content 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 lines@ [[type:int]] ''automatic'' When read from a script, gives the number of lines 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. 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"@,<br/>@"multiple choice"@,<br/>@"boolean"@ | @"choice"@,<br/>@"multiple choice"@,<br/>@"boolean"@
+28
View File
@@ -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, "<line>\n</line>".
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.
+33 -6
View File
@@ -97,11 +97,38 @@ void TextStyle::checkContentDependencies(Context& ctx, const Dependency& dep) co
alignment.initDependencies(ctx, dep); alignment.initDependencies(ctx, dep);
} }
template <typename T> void reflect_content(T& handler, const TextStyle& ts) {} template <typename T> void reflect_layout(T& handler, const TextStyle& ts) {}
template <> void reflect_content(GetMember& handler, const TextStyle& ts) { template <> void reflect_layout(GetMember& handler, const TextStyle& ts) {
REFLECT_N("content_width", ts.content_width); REFLECT(ts.layout);
REFLECT_N("content_height", ts.content_height); if (ts.layout) {
REFLECT_N("content_lines", ts.content_lines); 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) { IMPLEMENT_REFLECTION(TextStyle) {
@@ -127,7 +154,7 @@ IMPLEMENT_REFLECTION(TextStyle) {
REFLECT(line_height_line_max); REFLECT(line_height_line_max);
REFLECT(paragraph_height); REFLECT(paragraph_height);
REFLECT(direction); REFLECT(direction);
reflect_content(handler, *this); reflect_layout(handler, *this);
} }
// ----------------------------------------------------------------------------- : TextValue // ----------------------------------------------------------------------------- : TextValue
+23
View File
@@ -25,6 +25,8 @@ DECLARE_POINTER_TYPE(TextField);
DECLARE_POINTER_TYPE(TextStyle); DECLARE_POINTER_TYPE(TextStyle);
DECLARE_POINTER_TYPE(TextValue); DECLARE_POINTER_TYPE(TextValue);
DECLARE_POINTER_TYPE(TextBackground); DECLARE_POINTER_TYPE(TextBackground);
DECLARE_POINTER_TYPE(TextLayout);
DECLARE_POINTER_TYPE(LineLayout);
/// A field for values containing tagged text /// A field for values containing tagged text
class TextField : public Field { class TextField : public Field {
@@ -44,6 +46,26 @@ class TextField : public Field {
// ----------------------------------------------------------------------------- : TextStyle // ----------------------------------------------------------------------------- : TextStyle
// information coming from text rendering
class LineLayout : public IntrusivePtrVirtualBase {
public:
double width, top, height;
enum class Type { LINE, PARAGRAPH, BLOCK, ALL } type;
vector<LineLayoutP> 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<double> separators;
TextLayout() : LineLayout(0,0,0,Type::ALL) {}
void reflect(GetMember& gm) const;
};
/// The Style for a TextField /// The Style for a TextField
class TextStyle : public Style { class TextStyle : public Style {
public: public:
@@ -69,6 +91,7 @@ class TextStyle : public Style {
paragraph_height; ///< Fixed height of paragraphs paragraph_height; ///< Fixed height of paragraphs
Direction direction; ///< In what direction is text layed out? Direction direction; ///< In what direction is text layed out?
// information from text rendering // information from text rendering
TextLayoutP layout;
double content_width, content_height; ///< Size of the rendered text double content_width, content_height; ///< Size of the rendered text
int content_lines; ///< Number of rendered lines int content_lines; ///< Number of rendered lines
+54 -14
View File
@@ -344,24 +344,55 @@ void TextViewer::prepareElements(const String& text, const TextStyle& style, Con
// ----------------------------------------------------------------------------- : Layout // ----------------------------------------------------------------------------- : 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<TextLayout>();
LineBreak last_break = BREAK_LINE;
LineLayoutP paragraph, block;
for (auto const& l : lines) {
LineLayoutP line = make_intrusive<LineLayout>(l.width(), l.top, l.line_height, LineLayout::Type::LINE);
if (!block) {
block = make_intrusive<LineLayout>(*line);
block->type = LineLayout::Type::BLOCK;
layout->blocks.push_back(block);
}
if (!paragraph) {
paragraph = make_intrusive<LineLayout>(*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) { void TextViewer::prepareLines(RotatedDC& dc, const String& text, TextStyle& style, Context& ctx) {
vector<CharInfo> chars; vector<CharInfo> chars;
prepareLinesTryScales(dc, text, style, chars); prepareLinesTryScales(dc, text, style, chars);
assert(!lines.empty()); 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 // no text, find a dummy height for the single line we have
if (lines.size() == 1 && lines[0].width() < 0.0001) { if (lines.size() == 1 && lines[0].width() < 0.0001) {
if (style.always_symbol && style.symbol_font.valid()) { 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 // align
alignLines(dc, chars, style); 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].line_height = h;
lines[0].top -= 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 // 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)); 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: // Invariant:
// a. The text fits at min_scale (or we force it anyway) // a. The text fits at min_scale (or we force it anyway)
// b. but not at max_scale // b. but not at max_scale
+3
View File
@@ -145,6 +145,9 @@ class TextViewer {
/// Align the lines of a single paragraph (a set of lines) /// Align the lines of a single paragraph (a set of lines)
void alignParagraph(size_t start_line, size_t end_line, const vector<CharInfo>& chars, const TextStyle& style, const RealRect& box); void alignParagraph(size_t start_line, size_t end_line, const vector<CharInfo>& 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 /// 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; const Line& findLine(size_t index) const;