diff --git a/src/gui/value/text.cpp b/src/gui/value/text.cpp index 92751989..0a39223e 100644 --- a/src/gui/value/text.cpp +++ b/src/gui/value/text.cpp @@ -568,7 +568,7 @@ void TextValueEditor::onValueChange() { } void TextValueEditor::onAction(const Action& action, bool undone) { - TextValueViewer::onValueChange(); + TextValueViewer::onAction(action, undone); findWordLists(); TYPE_CASE(action, TextValueAction) { selection_start = action.selection_start; @@ -1001,7 +1001,7 @@ void TextValueEditor::determineSize(bool force_fit) { (int)pos.y - 1, (int)sbw, (int)rot.trY(style().height) + 2); - v.reset(); + v.reset(true); } else { // Height depends on font wxMemoryDC dc; diff --git a/src/render/text/font.cpp b/src/render/text/font.cpp index 053ae1d3..097c6898 100644 --- a/src/render/text/font.cpp +++ b/src/render/text/font.cpp @@ -54,5 +54,5 @@ double FontTextElement::minScale() const { return min(font->size(), font->scale_down_to) / max(0.01, font->size()); } double FontTextElement::scaleStep() const { - return 1. / max(font->size(), 1.); + return 1. / max(font->size() * 4, 1.); } diff --git a/src/render/text/symbol.cpp b/src/render/text/symbol.cpp index a0047984..d651a57f 100644 --- a/src/render/text/symbol.cpp +++ b/src/render/text/symbol.cpp @@ -28,5 +28,5 @@ double SymbolTextElement::minScale() const { return min(font.size, font.scale_down_to) / max(0.01, font.size); } double SymbolTextElement::scaleStep() const { - return 1. / max(font.size, 1.); + return 1. / max(font.size * 4, 1.); } diff --git a/src/render/text/viewer.cpp b/src/render/text/viewer.cpp index c60be9da..180bf993 100644 --- a/src/render/text/viewer.cpp +++ b/src/render/text/viewer.cpp @@ -156,9 +156,10 @@ bool TextViewer::prepare(RotatedDC& dc, const String& text, TextStyle& style, Co return false; } } -void TextViewer::reset() { +void TextViewer::reset(bool related) { elements.clear(); lines.clear(); + if (!related) scale = 1.0; } bool TextViewer::prepared() const { return !lines.empty(); @@ -335,55 +336,8 @@ void TextViewer::prepareElements(const String& text, const TextStyle& style, Con // ----------------------------------------------------------------------------- : Layout void TextViewer::prepareLines(RotatedDC& dc, const String& text, TextStyle& style, Context& ctx) { - // try to layout, at different scales vector chars; - scale = 1; - double min_scale = elements.minScale(); - double scale_step = max(0.05,elements.scaleStep()); - while (true) { - double next_scale = scale - scale_step; - bool last = next_scale < min_scale; - // fits? - chars.clear(); - elements.getCharInfo(dc, scale, 0, text.size(), chars); - bool fits = prepareLinesScale(dc, chars, style, last); - if (fits && (lines.empty() || lines.back().bottom() <= dc.getInternalSize().height - style.padding_bottom)) { - break; // text fits in box - } - if (last) break; - // TODO: smarter iteration - scale = next_scale; - } - - /* - double scale_1 = 1. - double fit_1 = fitLines(dc, text, style, scale_1); - if (fit_1 <= 0 || scale_1 >= scale_2) { - // ok - } else { - // find best text size, using the 'false position' root finding method - double scale_2 = elements.minScale(); - double fit_2 = fitLines(dc, text, style, scale_2); - if (fit_2 > 0) { - // still doesn't fit at smallest size - } else { - // invariant: fit_1 > 0 && fit_2 <= 0 - while (abs(scale_2 - scale_1) > 0.01) { - double scale_3 = scale_2 - fit_2 * (scale_2 - scale_1)/(fit_2 - fit_1); - double fit_3 = fitLines(dc, text, style, scale_3); - if (fit_3 > 0) { - scale_2 = scale_3; - fit_2 = fit_3; - } else { - scale_2 = scale_3; - fit_2 = fit_3; - } - } - } - } - - // returns negative values if it fits, positive if it doesn't - */ + prepareLinesTryScales(dc, text, style, chars); // store information about the content/layout, allow this to change alignment style.content_width = 0; @@ -420,7 +374,122 @@ void TextViewer::prepareLines(RotatedDC& dc, const String& text, TextStyle& styl } } -bool TextViewer::prepareLinesScale(RotatedDC& dc, const vector& chars, const TextStyle& style, bool stop_if_too_long) { +// 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& lines, double scale) { + double tot_height = dc.getInternalSize().height + 1; + double height = min(tot_height, lines.back().bottom() + style.padding_bottom); + 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& lines, double scale) { + double tot_height = dc.getInternalSize().height; + double height = lines.back().bottom() + style.padding_bottom; + return scale * tot_height / height; +} + +void TextViewer::prepareLinesTryScales(RotatedDC& dc, const String& text, const TextStyle& style, vector& 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, 0, text.size(), chars); + prepareLinesScale(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, 0, text.size(), chars); + bool fits = prepareLinesScale(dc, chars, style, true, 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 lines_before; + vector chars_before; + elements.getCharInfo(dc, scale, 0, text.size(), chars_before); + fits = prepareLinesScale(dc, chars_before, style, true, 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, 0, text.size(), chars); + prepareLinesScale(dc, chars, style, false, lines); + max_scale = min(max_scale, bound_on_max_scale(dc,style,lines,scale)); + } + + // The common case optimization fialed, 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 lines_try; + vector chars_try; + elements.getCharInfo(dc, scale, 0, text.size(), chars_try); + fits = prepareLinesScale(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, 0, text.size(), chars); + fits = prepareLinesScale(dc, chars, style, false, lines); + } + scale = min_scale; +} + + +bool TextViewer::prepareLinesScale(RotatedDC& dc, const vector& chars, const TextStyle& style, bool stop_if_too_long, vector& lines) const { // Try to layout the text at the current scale // first line lines.clear(); @@ -519,6 +588,10 @@ bool TextViewer::prepareLinesScale(RotatedDC& dc, const vector& chars, } else { line.line_height = line_size.height; } +// // too low? +// if (stop_if_too_long && line.bottom() > dc.getInternalSize().height - style.padding_bottom) { +// return false; +// } // push lines.push_back(line); // reset line object for next line @@ -549,16 +622,19 @@ bool TextViewer::prepareLinesScale(RotatedDC& dc, const vector& chars, line.line_height = line_size.height; } lines.push_back(line); - return true; + // does it fit vertically? + return lines.empty() || + lines.back().bottom() <= dc.getInternalSize().height - style.padding_bottom; } -double TextViewer::lineLeft(RotatedDC& dc, const TextStyle& style, double y) { +double TextViewer::lineLeft(RotatedDC& dc, const TextStyle& style, double y) const { return style.mask.rowLeft(y, dc.getInternalSize()) + style.padding_left; } -double TextViewer::lineRight(RotatedDC& dc, const TextStyle& style, double y) { +double TextViewer::lineRight(RotatedDC& dc, const TextStyle& style, double y) const { return style.mask.rowRight(y, dc.getInternalSize()) - style.padding_right; } + void TextViewer::alignLines(RotatedDC& dc, const vector& chars, const TextStyle& style) { if (style.alignment == ALIGN_TOP_LEFT) return; // Find height of the text, don't count the last lines if they are empty diff --git a/src/render/text/viewer.hpp b/src/render/text/viewer.hpp index bf79c168..b2368df4 100644 --- a/src/render/text/viewer.hpp +++ b/src/render/text/viewer.hpp @@ -57,7 +57,9 @@ class TextViewer { /** Returns true if something has been done */ bool prepare(RotatedDC& dc, const String& text, TextStyle& style, Context&); /// Reset the cached data, at a new call to draw it will be recalculated - void reset(); + /** If related, the new value is related to the old one, and layout information should be reused where possible + */ + void reset(bool related); /// Is the viewer prepare()d? bool prepared() const; @@ -134,8 +136,11 @@ class TextViewer { /// Prepare the lines, layout the text void prepareLines(RotatedDC& dc, const String& text, TextStyle& style, Context& ctx); + /// Find the scale to use for the text + void prepareLinesTryScales(RotatedDC& dc, const String& text, const TextStyle& style, vector& chars_out); /// Prepare the lines, layout the text; at a specific scale - bool prepareLinesScale(RotatedDC& dc, const vector& chars, const TextStyle& style, bool stop_if_too_long); + /** Stores output in lines_out */ + bool prepareLinesScale(RotatedDC& dc, const vector& chars, const TextStyle& style, bool stop_if_too_long, vector& lines_out) const; /// Align the lines within the textbox void alignLines(RotatedDC& dc, const vector& chars, const TextStyle& style); @@ -143,9 +148,9 @@ class TextViewer { const Line& findLine(size_t index) const; // helper : get the start coordinate of a line, this is 0 unless there is a contour mask - double lineLeft (RotatedDC& dc, const TextStyle& style, double y); + double lineLeft (RotatedDC& dc, const TextStyle& style, double y) const; // helper : get the end coordinate of a line, this is width unless there is a contour mask - double lineRight(RotatedDC& dc, const TextStyle& style, double y); + double lineRight(RotatedDC& dc, const TextStyle& style, double y) const; }; diff --git a/src/render/value/text.cpp b/src/render/value/text.cpp index c638f1e6..fb24c806 100644 --- a/src/render/value/text.cpp +++ b/src/render/value/text.cpp @@ -39,10 +39,14 @@ void TextValueViewer::draw(RotatedDC& dc) { } void TextValueViewer::onValueChange() { - v.reset(); + v.reset(false); } void TextValueViewer::onStyleChange(bool already_prepared) { - v.reset(); + v.reset(true); if (!already_prepared) viewer.redraw(*this); } + +void TextValueViewer::onAction(const Action&, bool undone) { + v.reset(true); +} diff --git a/src/render/value/text.hpp b/src/render/value/text.hpp index 9eafd66e..e84b6018 100644 --- a/src/render/value/text.hpp +++ b/src/render/value/text.hpp @@ -25,6 +25,7 @@ class TextValueViewer : public ValueViewer { virtual void draw(RotatedDC& dc); virtual void onValueChange(); virtual void onStyleChange(bool); + virtual void onAction(const Action&, bool undone); protected: TextViewer v;