// Copyright (c) 2012, Thomas Goyne // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. /// @file line_wrap.h /// @brief Generic paragraph formatting logic #include #include #include #include namespace agi { enum WrapMode { /// Semi-balanced, with the first line guaranteed to be longest if possible Wrap_Balanced_FirstLonger = 0, /// Simple greedy matching with breaks as late as possible Wrap_Greedy = 1, /// No line breaking at all Wrap_None = 2, /// Semi-balanced, with the last line guaranteed to be longest if possible Wrap_Balanced_LastLonger = 3, /// Balanced, with lines as close to equal in length as possible Wrap_Balanced = 4 }; namespace line_wrap_detail { template Width waste(Width width, Width desired) { return (width - desired) * (width - desired); } template inline void get_line_widths(StartCont const& line_start_points, Iter begin, Iter end, WidthCont &line_widths) { size_t line_start = 0; for (auto & line_start_point : line_start_points) { line_widths.push_back(std::accumulate(begin + line_start, begin + line_start_point, 0)); line_start = line_start_point; } line_widths.push_back(std::accumulate(begin + line_start, end, 0)); } // For first-longer and last-longer, bubble words forward/backwards when // possible and needed to make the first/last lines longer // // This is done rather than just using VSFilter's simpler greedy // algorithm due to that VSFilter's algorithm is incorrect; it can // produce incorrectly unbalanced lines and even excess line breaks template void unbalance(StartCont &ret, WidthCont const& widths, Width max_width, WrapMode wrap_mode) { WidthCont line_widths; get_line_widths(ret, widths.begin(), widths.end(), line_widths); int from_offset = 0; int to_offset = 1; if (wrap_mode == agi::Wrap_Balanced_LastLonger) std::swap(from_offset, to_offset); for (size_t i = 0; i < ret.size(); ++i) { // shift words until they're unbalanced in the correct direction // or shifting a word would exceed the length limit while (line_widths[i + from_offset] < line_widths[i + to_offset]) { int shift_word_width = widths[ret[i]]; if (line_widths[i + from_offset] + shift_word_width > max_width) break; line_widths[i + from_offset] += shift_word_width; line_widths[i + to_offset] -= shift_word_width; ret[i] += to_offset + -from_offset; } } } template void break_greedy(StartCont &ret, WidthCont const& widths, Width max_width) { // Simple greedy matching that just starts a new line every time the // max length is exceeded Width cur_line_width = 0; for (size_t i = 0; i < widths.size(); ++i) { if (cur_line_width > 0 && widths[i] + cur_line_width > max_width) { ret.push_back(i); cur_line_width = 0; } cur_line_width += widths[i]; } } } /// Get the indices at which the blocks should be wrapped /// @tparam WidthCont A random-access container of Widths /// @tparam Width A numeric type which represents a width /// @param widths The widths of the objects to fit within the space /// @param max_width The available space for the objects /// @param wrap_mode WrapMode to use to decide where to insert breaks /// @return Indices into widths which breaks should be inserted before template std::vector get_wrap_points(WidthCont const& widths, Width max_width, WrapMode wrap_mode) { using namespace line_wrap_detail; std::vector ret; if (wrap_mode == Wrap_None || widths.size() < 2) return ret; // Check if any wrapping is actually needed Width total_width = std::accumulate(widths.begin(), widths.end(), 0); if (total_width <= max_width) return ret; if (wrap_mode == Wrap_Greedy) { break_greedy(ret, widths, max_width); return ret; } size_t num_words = distance(widths.begin(), widths.end()); // the cost of the optimal arrangement of words [0..i] std::vector optimal_costs(num_words, INT_MAX); // the optimal start word for a line ending at i std::vector line_starts(num_words, INT_MAX); // O(num_words * min(num_words, max_width)) for (size_t end_word = 0; end_word < num_words; ++end_word) { Width current_line_width = 0; for (int start_word = end_word; start_word >= 0; --start_word) { current_line_width += widths[start_word]; // Only evaluate lines over the limit if they're one word if (current_line_width > max_width && (size_t)start_word != end_word) break; Width cost = waste(current_line_width, max_width); if (start_word > 0) cost += optimal_costs[start_word - 1]; if (cost < optimal_costs[end_word]) { optimal_costs[end_word] = cost; line_starts[end_word] = start_word; } } } // Select the optimal start word for each line ending with last_word for (size_t last_word = num_words; last_word > 0 && line_starts[last_word - 1] > 0; last_word = line_starts[last_word]) { --last_word; ret.push_back(line_starts[last_word]); } std::reverse(ret.begin(), ret.end()); if (wrap_mode != Wrap_Balanced) unbalance(ret, widths, max_width, wrap_mode); return ret; } }