mirror of
https://github.com/amyinspace/MagicSetEditor2.git
synced 2026-06-10 04:57:00 -04:00
436c437189
add compiler directives
875 lines
33 KiB
C++
875 lines
33 KiB
C++
//+----------------------------------------------------------------------------+
|
|
//| Description: Magic Set Editor - Program to make card games |
|
|
//| Copyright: (C) Twan van Laarhoven and the other MSE developers |
|
|
//| License: GNU General Public License 2 or later (see file COPYING) |
|
|
//+----------------------------------------------------------------------------+
|
|
|
|
// ----------------------------------------------------------------------------- : Includes
|
|
|
|
#include <util/prec.hpp>
|
|
#include <render/text/viewer.hpp>
|
|
#include <algorithm>
|
|
|
|
// ----------------------------------------------------------------------------- : Line
|
|
|
|
struct TextViewer::Line {
|
|
size_t start; ///< Index of the first character in this line
|
|
size_t end_or_soft; ///< Index just beyond the last non-soft character
|
|
vector<double> positions; ///< x position of each character in this line, gives the number of characters + 1, never empty
|
|
double top; ///< y position of (the top of) this line
|
|
double line_height; ///< The height of this line in pixels
|
|
LineBreak break_after; ///< Is there a saparator after this line?
|
|
optional<Alignment> alignment; ///< Alignment of this line
|
|
bool justifying; ///< Is the text justified? Only true when *really* justifying.
|
|
double margin_left; ///< Left margin
|
|
double margin_right;///< Rightmargin
|
|
|
|
Line()
|
|
: start(0), end_or_soft(0), top(0), line_height(0)
|
|
, break_after(LineBreak::NO), justifying(false)
|
|
{}
|
|
|
|
/// The position (just beyond) the bottom of this line
|
|
double bottom() const { return top + line_height; }
|
|
/// The width of this line
|
|
double width() const { return positions[end_or_soft-start] - positions.front(); }
|
|
/// Index just beyond the last character on this line
|
|
size_t end() const { return start + positions.size() - 1; }
|
|
/// Find the index of the character at the given position on this line
|
|
/** Always returns a value in the range [start..end()] */
|
|
size_t posToIndex(double x) const;
|
|
|
|
/// Is this line visible using the given rectangle?
|
|
bool visible(const Rotation& rot) const {
|
|
return top + line_height > 0 && top < rot.getHeight();
|
|
}
|
|
|
|
/// Get a rectangle of the selection on this line
|
|
/** start and end need not be in this line */
|
|
RealRect selectionRectangle(const Rotation& rot, size_t start, size_t end);
|
|
|
|
/// Align the contents of this line *horizontally* inside the given rectangle
|
|
void alignHorizontal(const vector<CharInfo>& chars, const TextStyle& style, const RealRect& s);
|
|
};
|
|
|
|
size_t TextViewer::Line::posToIndex(double x) const {
|
|
// largest index with pos <= x
|
|
vector<double>::const_iterator it2 = lower_bound(positions.begin(), positions.end(), x);
|
|
if (it2 == positions.begin()) return start;
|
|
if (it2 == positions.end()) --it2; // we don't want to find the position beyond the end
|
|
if (it2 == positions.begin()) return start;
|
|
// first index with pos > x
|
|
vector<double>::const_iterator it1 = it2 - 1;
|
|
if (x - *it1 <= *it2 - x) return it1 - positions.begin() + start; // it1 is closer
|
|
else return it2 - positions.begin() + start; // it2 is closer
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------- : TextViewer
|
|
|
|
// can't be declared in header because we need to know sizeof(Line)
|
|
TextViewer:: TextViewer() {}
|
|
TextViewer::~TextViewer() {}
|
|
|
|
// ----------------------------------------------------------------------------- : Drawing
|
|
|
|
void TextViewer::draw(RotatedDC& dc, const TextStyle& style, DrawWhat what, bool native_look) {
|
|
assert(!lines.empty());
|
|
// draw anything?
|
|
if (what == DRAW_NOTHING) return;
|
|
// separator lines?
|
|
// do this first, so pen is still set from drawing the field border
|
|
if (what & DRAW_BORDERS) {
|
|
drawSeparators(dc);
|
|
}
|
|
// Draw the text, line by line
|
|
FOR_EACH(l, lines) {
|
|
if (l.visible(dc)) {
|
|
if (l.justifying) {
|
|
// Draw characters separatly
|
|
for (size_t i = 0 ; i < l.positions.size() - 1 ; ++i) {
|
|
RealRect rect(l.positions[i], l.top, l.positions[i+1] - l.positions[i] , l.line_height);
|
|
elements.draw(dc, scale, rect, &l.positions[i], what, l.start + i, l.start + i + 1, native_look);
|
|
}
|
|
} else {
|
|
RealRect rect(l.positions.front(), l.top, l.width(), l.line_height);
|
|
elements.draw(dc, scale, rect, &*l.positions.begin(), what, l.start, l.end(), native_look);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Intersection between two rectangles
|
|
RealRect intersect(const RealRect& a, const RealRect& b) {
|
|
RealPoint tl = piecewise_max(a.topLeft(), b.topLeft());
|
|
RealPoint br = piecewise_min(a.bottomRight(), b.bottomRight());
|
|
return RealRect(tl, RealSize(br - tl));
|
|
}
|
|
|
|
void TextViewer::drawSelection(RotatedDC& dc, const TextStyle& style, size_t sel_start, size_t sel_end) {
|
|
if (sel_start == sel_end) return;
|
|
if (sel_end < sel_start) swap(sel_start, sel_end);
|
|
dc.SetBrush(*wxBLACK_BRUSH);
|
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
|
dc.SetLogicalFunction(wxINVERT);
|
|
RealRect prev_rect(0,0,0,0);
|
|
FOR_EACH(l, lines) {
|
|
RealRect rect = l.selectionRectangle(dc, sel_start, sel_end);
|
|
if (rect.height > 0) dc.DrawRectangle(rect);
|
|
// compensate for overlap between lines
|
|
RealRect overlap = intersect(rect, prev_rect);
|
|
if (overlap.height > 0 && overlap.width > 0) dc.DrawRectangle(overlap);
|
|
prev_rect = rect;
|
|
}
|
|
dc.SetLogicalFunction(wxCOPY);
|
|
}
|
|
|
|
RealRect TextViewer::Line::selectionRectangle(const Rotation& rot, size_t sel_start, size_t sel_end) {
|
|
if (visible(rot) && sel_start < end() && sel_end > start) {
|
|
double x1 = positions[max(start, sel_start) - start];
|
|
double x2 = positions[min(end(), sel_end) - start];
|
|
return RealRect(x1, top, x2 - x1, line_height);
|
|
} else {
|
|
return RealRect(0,0,0,0);
|
|
}
|
|
}
|
|
|
|
void TextViewer::drawSeparators(RotatedDC& dc) {
|
|
// separator lines
|
|
bool separator = false;
|
|
double y = 0;
|
|
FOR_EACH(l, lines) {
|
|
double y2 = l.top + l.line_height;
|
|
if (separator && l.visible(dc)) {
|
|
// between the two lines
|
|
y = (y + l.top) / 2;
|
|
dc.DrawLine(RealPoint(0, y), RealPoint(dc.getInternalRect().width, y));
|
|
}
|
|
separator = l.break_after == LineBreak::LINE;
|
|
y = y2;
|
|
}
|
|
// separator at the end?
|
|
if (separator) {
|
|
dc.DrawLine(RealPoint(0, y), RealPoint(dc.getInternalRect().width, y));
|
|
}
|
|
}
|
|
|
|
bool TextViewer::prepare(RotatedDC& dc, const String& text, TextStyle& style, Context& ctx) {
|
|
if (!prepared()) {
|
|
// not prepared yet
|
|
prepareElements(text, style, ctx);
|
|
prepareLines(dc, text, style, ctx);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
void TextViewer::reset(bool related) {
|
|
elements.clear();
|
|
lines.clear();
|
|
if (!related) scale = 1.0;
|
|
}
|
|
bool TextViewer::prepared() const {
|
|
return !lines.empty();
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------- : Positions
|
|
|
|
const TextViewer::Line& TextViewer::findLine(size_t index) const {
|
|
assert(!lines.empty());
|
|
FOR_EACH_CONST(l, lines) {
|
|
if (l.end() >= index) return l;
|
|
}
|
|
return lines.front();
|
|
}
|
|
|
|
size_t TextViewer::moveLine(size_t index, int delta) const {
|
|
if (lines.empty()) return index;
|
|
const Line* line1 = &findLine(index);
|
|
const Line* line2 = line1 + delta;
|
|
if (line2 >= &lines.front() && line2 <= &lines.back()) {
|
|
size_t idx = index - line1->start;
|
|
if (idx < 0 || idx >= line1->positions.size()) return index; // can't move
|
|
return line2->posToIndex(line1->positions[idx]); // character at the same position
|
|
} else {
|
|
return index; // can't move
|
|
}
|
|
}
|
|
|
|
size_t TextViewer::lineStart(size_t index) const {
|
|
if (lines.empty()) return 0;
|
|
return findLine(index).start;
|
|
}
|
|
|
|
size_t TextViewer::lineEnd(size_t index) const {
|
|
if (lines.empty()) return 0;
|
|
return findLine(index).end();
|
|
}
|
|
|
|
struct CompareTop {
|
|
inline bool operator () (double a, double b) const { return a < b; }
|
|
inline bool operator () (const TextViewer::Line& a, double b) const { return a.top < b; }
|
|
inline bool operator () (double a, const TextViewer::Line& b) const { return a < b.top; }
|
|
inline bool operator () (const TextViewer::Line& a, const TextViewer::Line& b) const { return a.top < b.top; }
|
|
};
|
|
size_t TextViewer::indexAt(const RealPoint& pos) const {
|
|
// 1. find the line
|
|
if (lines.empty()) return 0;
|
|
vector<Line>::const_iterator l = lower_bound(lines.begin(), lines.end(), pos.y, CompareTop());
|
|
if (l != lines.begin()) l--;
|
|
assert(l != lines.end());
|
|
// 2. find char on line
|
|
return l->posToIndex(pos.x);
|
|
}
|
|
|
|
RealRect TextViewer::charRect(size_t index, bool first) const {
|
|
if (lines.empty()) return RealRect(0,0,0,0);
|
|
const Line& l = findLine(index);
|
|
size_t pos = index - l.start;
|
|
if (pos + 1 >= l.positions.size()) {
|
|
if (!first && &l < &lines.back()) {
|
|
// try the start of the next line
|
|
const Line& l2 = *(&l+1);
|
|
if (index == l2.start) {
|
|
return RealRect(l2.positions.front(), l2.top, 0, l2.line_height);
|
|
}
|
|
}
|
|
return RealRect(l.positions.back(), l.top, 0, l.line_height);
|
|
} else {
|
|
return RealRect(l.positions[pos], l.top, l.positions[pos + 1] - l.positions[pos], l.line_height);
|
|
}
|
|
}
|
|
|
|
bool TextViewer::isVisible(size_t index) const {
|
|
if (lines.empty()) return false;
|
|
const Line& l = findLine(index);
|
|
size_t pos = index - l.start;
|
|
if (pos >= l.positions.size()) {
|
|
return false;
|
|
} else if (pos + 1 == l.positions.size()) {
|
|
return true; // last char of the line
|
|
} else {
|
|
return l.positions[pos + 1] - l.positions[pos] > 0.0001;
|
|
}
|
|
}
|
|
size_t TextViewer::firstVisibleChar(size_t index, int delta) const {
|
|
if (lines.empty()) return index;
|
|
const Line* l = &findLine(index);
|
|
while (true) {
|
|
int pos = (int)(index - l->start);
|
|
while (index == l->end() || (pos + delta >= 0 && (size_t)pos + delta < l->positions.size())) {
|
|
if (index == l->end() || l->positions[pos + 1] - l->positions[pos] > 0.0001) {
|
|
return index;
|
|
}
|
|
pos += delta;
|
|
index += delta;
|
|
}
|
|
// move to another line, if not at start/end
|
|
if (l + delta < &lines.front()) return 0;
|
|
if (l + delta > &lines.back()) return l->end();
|
|
index += delta;
|
|
l += delta;
|
|
}
|
|
}
|
|
|
|
double TextViewer::heightOfLastLine() const {
|
|
if (lines.empty()) return 0;
|
|
return lines.back().line_height;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------- : Scrolling
|
|
|
|
size_t TextViewer::lineCount() const {
|
|
return lines.size();
|
|
}
|
|
size_t TextViewer::visibleLineCount(double height) const {
|
|
size_t count = 0;
|
|
FOR_EACH_CONST(l, lines) {
|
|
if (l.top + l.line_height > height) return count;
|
|
if (l.top >= 0) ++count;
|
|
}
|
|
return count;
|
|
}
|
|
size_t TextViewer::firstVisibleLine() const {
|
|
size_t i = 0;
|
|
FOR_EACH_CONST(l, lines) {
|
|
if (l.top >= 0) return i;
|
|
i++;
|
|
}
|
|
return 0; //no visible lines
|
|
}
|
|
|
|
void TextViewer::scrollTo(size_t line_id) {
|
|
scrollBy(-lines.at(line_id).top);
|
|
}
|
|
void TextViewer::scrollBy(double delta) {
|
|
if (delta == 0) return;
|
|
FOR_EACH(l, lines) {
|
|
l.top += delta;
|
|
}
|
|
}
|
|
|
|
bool TextViewer::ensureVisible(double height, size_t char_id) {
|
|
if (lines.empty()) return true;
|
|
const Line& line = findLine(char_id);
|
|
if (line.top < 0) {
|
|
// scroll up
|
|
scrollBy(-line.top);
|
|
return true;
|
|
} else if (line.bottom() > height) {
|
|
// scroll down
|
|
FOR_EACH(l, lines) {
|
|
if (l.top > 0) scrollBy(-l.line_height); // scroll down a single line ...
|
|
if (line.bottom() <= height) break; // ... until we can see the current line
|
|
}
|
|
return true;
|
|
} else {
|
|
return false; // line was already visible
|
|
}
|
|
}
|
|
|
|
double TextViewer::getExactScrollPosition() const {
|
|
if (lines.empty()) return 0;
|
|
return -lines.front().top;
|
|
}
|
|
void TextViewer::setExactScrollPosition(double pos) {
|
|
if (lines.empty()) return; // no scrolling is needed
|
|
pos += lines.front().top;
|
|
scrollBy(-pos);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------- : Elements
|
|
|
|
void TextViewer::prepareElements(const String& text, const TextStyle& style, Context& ctx) {
|
|
elements.fromString(text, style, ctx);
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------- : 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>();
|
|
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 == LineBreak::LINE) {
|
|
paragraph = block = nullptr;
|
|
} else if (l.break_after == LineBreak::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+1]->top)/2);
|
|
}
|
|
return layout;
|
|
}
|
|
|
|
void TextViewer::prepareLines(RotatedDC& dc, const String& text, TextStyle& style, Context& ctx) {
|
|
vector<CharInfo> chars;
|
|
prepareLinesTryScales(dc, text, style, chars);
|
|
assert(!lines.empty());
|
|
|
|
// 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()) {
|
|
lines[0].line_height = style.symbol_font.font->defaultSymbolSize(style.symbol_font.size).height;
|
|
} else {
|
|
dc.SetFont(style.font, scale);
|
|
lines[0].line_height = dc.GetCharHeight();
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
// HACK : fix empty first line before <line>, do this after align, so layout is not affected
|
|
if (lines.size() > 1 && lines[0].line_height == 0) {
|
|
dc.SetFont(style.font, scale);
|
|
double h = dc.GetCharHeight();
|
|
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
|
|
inline double bound_on_max_scale(RotatedDC& dc, const TextStyle& style, const vector<TextViewer::Line>& lines, double scale) {
|
|
if (lines.empty()) return 1.0;
|
|
double tot_height = dc.getInternalSize().height + 1;
|
|
double height = min(tot_height, lines.back().bottom() + style.padding_bottom);
|
|
if (height < 1) return 1.0;
|
|
return scale * tot_height / height;
|
|
}
|
|
// bound on min_scale, given that scale doesn't fit and produces the given lines
|
|
inline double bound_on_min_scale(RotatedDC& dc, const TextStyle& style, const vector<TextViewer::Line>& lines, double scale) {
|
|
if (lines.empty()) return 0.0;
|
|
double tot_height = dc.getInternalSize().height;
|
|
double height = lines.back().bottom() + style.padding_bottom;
|
|
if (height < 1) return 0.0;
|
|
return scale * tot_height / height;
|
|
}
|
|
|
|
void TextViewer::prepareLinesTryScales(RotatedDC& dc, const String& text, const TextStyle& style, vector<CharInfo>& chars) {
|
|
// Bounds
|
|
double min_scale = elements.minScale();
|
|
double scale_step = max(0.01,elements.scaleStep());
|
|
// Is there any scaling (common case is: no)
|
|
if (min_scale >= 1.0) {
|
|
scale = 1.0;
|
|
elements.getCharInfo(dc, scale, chars);
|
|
prepareLinesAtScale(dc, chars, style, false, lines);
|
|
return;
|
|
}
|
|
|
|
// More complicated fitting
|
|
double max_scale = 1.0 + scale_step;
|
|
double best_scale;
|
|
|
|
// Assumption:
|
|
// It is likely that the text should have the same scale as the previous render attempt
|
|
// So:
|
|
// - try that scale first
|
|
// - if it fits
|
|
// - change min_scale
|
|
// - then try the scale just before it
|
|
// - if that doesn't fit, we are done
|
|
// - if it doesn't, we have just (almost) wasted 1 cycle, start binary search
|
|
// - if it doesn't
|
|
// - change max_scale
|
|
|
|
// Try the layout at the previous scale, this could give a quick upper bound
|
|
elements.getCharInfo(dc, scale, chars);
|
|
bool fits = prepareLinesAtScale(dc, chars, style, false, lines);
|
|
if (fits) {
|
|
min_scale = scale;
|
|
max_scale = min(max_scale, bound_on_max_scale(dc,style,lines,scale));
|
|
// is there a before?
|
|
if (scale + scale_step >= max_scale) return;
|
|
// try just before
|
|
scale += scale_step;
|
|
vector<Line> lines_before;
|
|
vector<CharInfo> chars_before;
|
|
elements.getCharInfo(dc, scale, chars_before);
|
|
fits = prepareLinesAtScale(dc, chars_before, style, false, lines_before);
|
|
if (fits) {
|
|
// too bad
|
|
swap(lines, lines_before);
|
|
swap(chars, chars_before);
|
|
best_scale = min_scale = scale;
|
|
max_scale = min(max_scale, bound_on_max_scale(dc,style,lines,scale));
|
|
} else {
|
|
// yay
|
|
scale = min_scale;
|
|
return;
|
|
}
|
|
} else {
|
|
max_scale = scale;
|
|
min_scale = max(min_scale, bound_on_min_scale(dc,style,lines,scale));
|
|
// ensure invariant d (below)
|
|
best_scale = scale = min_scale;
|
|
chars.clear();
|
|
elements.getCharInfo(dc, scale, chars);
|
|
prepareLinesAtScale(dc, chars, style, false, lines);
|
|
max_scale = min(max_scale, bound_on_max_scale(dc,style,lines,scale));
|
|
}
|
|
|
|
// 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
|
|
// c. 0 < min_scale <= real_scale < max_scale <= 1.0+epsilon
|
|
// d. lines and chars give the best fitting positioning, at best_scale
|
|
// try: e. min_scale <= best_scale
|
|
|
|
// go binary search!
|
|
while(min_scale + scale_step < max_scale) {
|
|
scale = (min_scale + max_scale) / 2;
|
|
vector<Line> lines_try;
|
|
vector<CharInfo> chars_try;
|
|
elements.getCharInfo(dc, scale, chars_try);
|
|
fits = prepareLinesAtScale(dc, chars_try, style, false, lines_try);
|
|
if (fits) {
|
|
min_scale = scale;
|
|
max_scale = min(max_scale, bound_on_max_scale(dc,style,lines_try,scale));
|
|
best_scale = scale; // invariant d
|
|
swap(lines,lines_try);
|
|
swap(chars,chars_try);
|
|
} else {
|
|
max_scale = scale;
|
|
min_scale = max(min_scale, bound_on_min_scale(dc,style,lines_try,scale));
|
|
// the above can break pseudo invariant e
|
|
}
|
|
}
|
|
if (best_scale != min_scale) {
|
|
// we'd better update lines, e doesn't hold
|
|
scale = min_scale;
|
|
chars.clear();
|
|
elements.getCharInfo(dc, scale, chars);
|
|
fits = prepareLinesAtScale(dc, chars, style, false, lines);
|
|
}
|
|
scale = min_scale;
|
|
}
|
|
|
|
// 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 {
|
|
RealSize line_size(line.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
|
|
line.top += 1;
|
|
line_size.width = line.margin_left + lineLeft(dc, style, line.top);
|
|
}
|
|
return line_size;
|
|
}
|
|
|
|
bool TextViewer::prepareLinesAtScale(RotatedDC& dc, const vector<CharInfo>& chars, const TextStyle& style, bool stop_if_too_long, vector<Line>& lines) const {
|
|
// Try to layout the text at the current scale
|
|
lines.clear();
|
|
|
|
// 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_right = elements.paragraphs[0].margin_right;
|
|
line.alignment = elements.paragraphs[0].alignment;
|
|
// size of the line so far
|
|
RealSize line_size = fitLineWidth(line, dc, style);
|
|
line.positions.push_back(line_size.width);
|
|
|
|
// The word we are currently reading
|
|
RealSize word_size;
|
|
vector<double> positions_word; // positios for this word
|
|
size_t word_end_or_soft = 0;
|
|
size_t word_start = 0;
|
|
// For each character ...
|
|
for (size_t i = 0 ; i < chars.size() ; ++i) {
|
|
const CharInfo& c = chars[i];
|
|
assert(i_para < elements.paragraphs.size());
|
|
assert(c.size.width == 0 || elements.paragraphs[i_para].start <= i && i < elements.paragraphs[i_para].end);
|
|
// 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) {
|
|
break_now = true;
|
|
accept_word = true;
|
|
line.break_after = c.break_after;
|
|
} else if (c.break_after == LineBreak::SPACE && style.field().multi_line) {
|
|
// 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;
|
|
hide_breaker = false;
|
|
line.break_after = LineBreak::SOFT;
|
|
}
|
|
// Add size of the character
|
|
if (c.break_after != LineBreak::LINE) {
|
|
// ^^ HACK: don't count the line height of <line> tags, if they are the only thing on a line
|
|
// then the linebreak is 'ignored'.
|
|
word_size = add_horizontal(word_size, c.size);
|
|
}
|
|
positions_word.push_back(word_size.width);
|
|
if (!c.soft) word_end_or_soft = i + 1;
|
|
if (i < elements.paragraphs[i_para].margin_end_char) {
|
|
line.margin_left += c.size.width; // character in left margin
|
|
}
|
|
// Did the word become too long?
|
|
if (!break_now) {
|
|
double max_width = lineRight(dc, style, line.top) - line.margin_right;
|
|
if (line_size.width + word_size.width > max_width) {
|
|
if (!style.field().multi_line) {
|
|
// single line word does not fit
|
|
if (stop_if_too_long) return false;
|
|
} else if (word_start == line.start) {
|
|
// single word on this line; the word is too long
|
|
if (stop_if_too_long) {
|
|
return false; // just give up
|
|
} else {
|
|
// force a word break
|
|
break_now = true;
|
|
accept_word = true;
|
|
hide_breaker = false;
|
|
word_too_long = true;
|
|
line.break_after = LineBreak::SOFT;
|
|
}
|
|
} else {
|
|
// line would become too long, break before the current word
|
|
break_now = true;
|
|
line.break_after = LineBreak::SOFT;
|
|
}
|
|
}
|
|
}
|
|
// Ending the current word
|
|
if (accept_word) {
|
|
// move word pos to line
|
|
FOR_EACH(p, positions_word) {
|
|
line.positions.push_back(line_size.width + p);
|
|
}
|
|
if (word_end_or_soft != 0) line.end_or_soft = word_end_or_soft;
|
|
line_size = add_horizontal(line_size, word_size);
|
|
// next word
|
|
word_size = RealSize(0, 0);
|
|
word_start = i + 1;
|
|
positions_word.clear();
|
|
word_end_or_soft = 0;
|
|
// move character that goes outside the box to the next line
|
|
if (word_too_long && line.positions.size() > 2) {
|
|
line.positions.pop_back();
|
|
word_start = i;
|
|
word_size = add_horizontal(word_size, c.size);
|
|
positions_word.push_back(word_size.width);
|
|
if (!c.soft) word_end_or_soft = i + 1;
|
|
}
|
|
}
|
|
// Breaking (ending the current line)
|
|
if (break_now) {
|
|
// remove the _('\n') or _(' ') that caused the break
|
|
if (hide_breaker && line.positions.size() > 1) {
|
|
line.positions.pop_back();
|
|
}
|
|
// height of the line
|
|
if (line_size.height < 0.01 && !lines.empty()) {
|
|
// if a line has 0 height, use the height of the line above it, but at most once
|
|
} else {
|
|
line.line_height = line_size.height;
|
|
}
|
|
line.end_or_soft = max(line.start, min(line.end_or_soft, line.end()));
|
|
// push
|
|
lines.push_back(line);
|
|
// reset line object for next line
|
|
double line_height_multiplier = line.break_after == LineBreak::HARD ? style.line_height_hard
|
|
: line.break_after == LineBreak::LINE ? style.line_height_line
|
|
: style.line_height_soft;
|
|
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::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;
|
|
assert(elements.paragraphs[i_para].start == i + 1);
|
|
line.margin_left = elements.paragraphs[i_para].margin_left;
|
|
line.margin_right = elements.paragraphs[i_para].margin_right;
|
|
line.top += elements.paragraphs[i_para].margin_top;
|
|
line.alignment = elements.paragraphs[i_para].alignment;
|
|
}
|
|
line.break_after = LineBreak::NO;
|
|
// reset line_size
|
|
line_size = fitLineWidth(line, dc, style);
|
|
line.positions.push_back(line_size.width); // start position
|
|
}
|
|
}
|
|
// the last word
|
|
FOR_EACH(p, positions_word) {
|
|
line.positions.push_back(line_size.width + p);
|
|
}
|
|
if (word_end_or_soft != 0) line.end_or_soft = word_end_or_soft;
|
|
line_size = add_horizontal(line_size, word_size);
|
|
// the last line
|
|
if (line_size.height < 0.01 && !lines.empty()) {
|
|
// if a line has 0 height, use the height of the line above it, but at most once
|
|
} else {
|
|
line.line_height = line_size.height;
|
|
}
|
|
line.end_or_soft = max(line.start, min(line.end_or_soft, line.end()));
|
|
lines.push_back(line);
|
|
// does it fit vertically?
|
|
if (style.paragraph_height > 0) {
|
|
// height = max(paragraph_height) * paragraph_count
|
|
double max_height = 0;
|
|
// per paragraph alignment
|
|
size_t start = 0;
|
|
for (size_t last = 0 ; last < lines.size() ; ++last) {
|
|
if (lines[last].break_after != LineBreak::SOFT || last == lines.size()) {
|
|
max_height = max(max_height, lines[last].bottom() - lines[start].top);
|
|
start = last + 1;
|
|
}
|
|
}
|
|
// how many paragraphs would fit?
|
|
int n = int(floor(0.5 + (dc.getInternalSize().height - style.padding_bottom) / style.paragraph_height));
|
|
lines.back().top = max_height * n - lines.back().line_height;
|
|
}
|
|
return lines.back().bottom() <= dc.getInternalSize().height - style.padding_bottom;
|
|
}
|
|
|
|
double TextViewer::lineLeft(RotatedDC& dc, const TextStyle& style, double y) const {
|
|
return style.mask.getFromCache().rowLeft(y, dc.getInternalSize()) + style.padding_left;
|
|
}
|
|
double TextViewer::lineRight(RotatedDC& dc, const TextStyle& style, double y) const {
|
|
return style.mask.getFromCache().rowRight(y, dc.getInternalSize()) - style.padding_right;
|
|
}
|
|
|
|
|
|
void TextViewer::alignLines(RotatedDC& dc, const vector<CharInfo>& chars, const TextStyle& style) {
|
|
// Size of the box
|
|
RealSize s = add_diagonal(
|
|
dc.getInternalSize(),
|
|
-RealSize(style.padding_left+style.padding_right, style.padding_top + style.padding_bottom));
|
|
if (style.paragraph_height <= 0) {
|
|
// whole text box alignment
|
|
assert(!lines.empty());
|
|
double top = lines[0].top;
|
|
alignParagraph(0, lines.size(), chars, style, RealRect(RealPoint(0,top),s));
|
|
} else {
|
|
// per paragraph alignment
|
|
size_t start = 0;
|
|
int n = 0;
|
|
for (size_t last = 0 ; last < lines.size() ; ++last) {
|
|
if (lines[last].break_after != LineBreak::SOFT || last+1 == lines.size()) {
|
|
alignParagraph(start, last + 1, chars, style, RealRect(0, style.padding_top+n*style.paragraph_height, s.width, style.paragraph_height));
|
|
start = last + 1;
|
|
++n;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TextViewer::alignParagraph(size_t start_line, size_t end_line, const vector<CharInfo>& chars, const TextStyle& style, const RealRect& s) {
|
|
if (start_line >= end_line) return;
|
|
|
|
// Find height of the text, don't count the last lines if they are empty
|
|
double height = 0;
|
|
for (size_t li = end_line - 1 ; li + 1 > start_line ; --li) {
|
|
Line& l = lines[li];
|
|
height = l.top + l.line_height;
|
|
if (l.line_height) break; // not an empty line
|
|
}
|
|
height -= lines[start_line].top;
|
|
|
|
// stretch lines by increasing the space between them
|
|
if (height < s.height) {
|
|
double d_soft = max(0.0, style.line_height_soft_max - style.line_height_soft);
|
|
double d_hard = max(0.0, style.line_height_hard_max - style.line_height_hard);
|
|
double d_line = max(0.0, style.line_height_line_max - style.line_height_line);
|
|
double stops[] = {0.0, d_soft, d_hard, d_line};
|
|
sort(stops + 1, stops + 4);
|
|
for (int i = 1 ; i < 4 && height < s.height ; ++i) {
|
|
double stop = stops[i] - stops[i-1];
|
|
if (stop <= 0) continue;
|
|
// which types can use this stop?
|
|
bool soft = d_soft >= stop;
|
|
bool hard = d_hard >= stop;
|
|
bool line = d_line >= stop;
|
|
// sum of the line height we can apply this to?
|
|
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 (sum == 0) break;
|
|
// how much do we need to add?
|
|
double to_add = min(stop, (s.height - height) / sum);
|
|
// apply
|
|
double add = 0;
|
|
for (size_t li = start_line ; li < end_line ; ++li) {
|
|
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;
|
|
}
|
|
height += add;
|
|
}
|
|
}
|
|
if (style.alignment == ALIGN_TOP_LEFT) return;
|
|
|
|
// align
|
|
double vdelta = align_delta_y(style.alignment, s.height, height)
|
|
+ s.y - lines[start_line].top;
|
|
// align all lines
|
|
for (size_t li = start_line ; li < end_line ; ++li) {
|
|
Line& l = lines[li];
|
|
l.top += vdelta;
|
|
// amount to shift all characters horizontally
|
|
l.alignHorizontal(chars, style, s);
|
|
}
|
|
// TODO : work well with mask
|
|
}
|
|
|
|
void TextViewer::Line::alignHorizontal(const vector<CharInfo>& chars, const TextStyle& style, const RealRect& s) {
|
|
double width = this->width() - margin_left;
|
|
double target_width = s.width - margin_left - 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);
|
|
if ((alignment & ALIGN_JUSTIFY_ALL) && should_fill) {
|
|
// justify text, by characters
|
|
justifying = true;
|
|
double hdelta = target_width - width; // amount of space to distribute
|
|
int count = (int)(end_or_soft - start); // distribute it among this many characters
|
|
if (count <= 0) count = 1; // prevent div by 0
|
|
int i = 0;
|
|
for (auto& c : positions) {
|
|
c += s.x + hdelta * i++ / count;
|
|
}
|
|
} else if ((alignment & ALIGN_JUSTIFY_WORDS) && should_fill) {
|
|
// justify text, by words
|
|
justifying = true;
|
|
double hdelta = target_width - width; // amount of space to distribute
|
|
int count = 0; // distribute it among this many word breaks
|
|
for (size_t k = start + 1 ; k < end_or_soft - 1 ; ++k) {
|
|
if (chars[k].break_after == LineBreak::SPACE) ++count;
|
|
}
|
|
if (count == 0) count = 1; // prevent div by 0
|
|
int i = 0; size_t j = start;
|
|
for (auto& c : positions) {
|
|
c += s.x + hdelta * i / count;
|
|
if (j < end_or_soft && chars[j++].break_after == LineBreak::SPACE) i++;
|
|
}
|
|
} else if ((alignment & ALIGN_STRETCH) && should_fill) {
|
|
// stretching, don't center or align right
|
|
justifying = false;
|
|
} else {
|
|
// simple alignment
|
|
justifying = false;
|
|
double hdelta = s.x + align_delta_x(alignment, target_width, width);
|
|
for (auto& c : positions) {
|
|
c += hdelta;
|
|
}
|
|
}
|
|
}
|