diff options
author | asvitkine@chromium.org <asvitkine@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-12-15 23:58:58 +0000 |
---|---|---|
committer | asvitkine@chromium.org <asvitkine@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-12-15 23:58:58 +0000 |
commit | 3785b3711fcfdbc8983fc9d8d24dd60fe511bdbc (patch) | |
tree | 663eaa476ce15ecb6eafc1afb1d2be11e9522690 /ui/base | |
parent | e9597ea273d45338b3f8391b32a5a6d6ef86f318 (diff) | |
download | chromium_src-3785b3711fcfdbc8983fc9d8d24dd60fe511bdbc.zip chromium_src-3785b3711fcfdbc8983fc9d8d24dd60fe511bdbc.tar.gz chromium_src-3785b3711fcfdbc8983fc9d8d24dd60fe511bdbc.tar.bz2 |
Add utility function to wrap a paragraph of text.
This is needed for my upcoming change to use this functionality
in a new CanvasSkia text drawing implementation.
This is exposed as a standalone function in text_elider.h but
implemented using an internal helper class in text_elider.cc.
This is similar to how the existing |ElideRectangleString()|
is implemented.
The new function provides four modes of dealing with long words
when wrapping text which correspond to the different ways the
current implementation of |CanvasSkia::DrawStringInt()| can wrap
text, based on the input flags.
BUG=105550
TEST=New unit tests added to text_elider_unittest.cc
Review URL: http://codereview.chromium.org/8234003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@114727 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/base')
-rw-r--r-- | ui/base/text/text_elider.cc | 268 | ||||
-rw-r--r-- | ui/base/text/text_elider.h | 34 | ||||
-rw-r--r-- | ui/base/text/text_elider_unittest.cc | 139 |
3 files changed, 441 insertions, 0 deletions
diff --git a/ui/base/text/text_elider.cc b/ui/base/text/text_elider.cc index 08435a2..dd39e48 100644 --- a/ui/base/text/text_elider.cc +++ b/ui/base/text/text_elider.cc @@ -712,6 +712,258 @@ void RectangleString::NewLine(bool output) { current_col_ = 0; } +// Internal class used to track progress of a rectangular text elide +// operation. Exists so the top-level ElideRectangleText() function +// can be broken into smaller methods sharing this state. +class RectangleText { + public: + RectangleText(const gfx::Font& font, + int available_pixel_width, + int available_pixel_height, + ui::WordWrapBehavior wrap_behavior, + std::vector<string16>* lines) + : font_(font), + line_height_(font.GetHeight()), + available_pixel_width_(available_pixel_width), + available_pixel_height_(available_pixel_height), + wrap_behavior_(wrap_behavior), + current_width_(0), + current_height_(0), + lines_(lines), + full_(false) {} + + // Perform deferred initializions following creation. Must be called + // before any input can be added via AddString(). + void Init() { lines_->clear(); } + + // Add an input string, reformatting to fit the desired dimensions. + // AddString() may be called multiple times to concatenate together + // multiple strings into the region (the current caller doesn't do + // this, however). + void AddString(const string16& input); + + // Perform any deferred output processing. Must be called after the last + // AddString() call has occured. Returns |true| if the text had to be + // truncated to fit the available height. + bool Finalize(); + + private: + // Returns |true| if |text| is entirely composed of whitespace. + bool IsWhitespaceString(const string16& text) const; + + // Add a line to the rectangular region at the current position, + // either by itself or by breaking it into words. + void AddLine(const string16& line); + + // Wrap the specified word across multiple lines. + int WrapWord(const string16& word); + + // Add a long word - wrapping, eliding or truncating per the wrap behavior. + int AddWordOverflow(const string16& word); + + // Add a word to the rectangluar region at the current position. + int AddWord(const string16& word); + + // Append the specified |text| to the current output line, incrementing the + // running width by the specified amount. This is an optimization over + // |AddToCurrentLine()| when |text_width| is already known. + void AddToCurrentLineWithWidth(const string16& text, int text_width); + + // Append the specified |text| to the current output line. + void AddToCurrentLine(const string16& text); + + // Set the current position to the beginning of the next line. + bool NewLine(); + + // The font used for measuring text width. + const gfx::Font& font_; + + // The height of each line of text. + const int line_height_; + + // The number of pixels of available width in the rectangle. + const int available_pixel_width_; + + // The number of pixels of available height in the rectangle. + const int available_pixel_height_; + + // The wrap behavior for words that are too long to fit on a single line. + const ui::WordWrapBehavior wrap_behavior_; + + // The current running width. + int current_width_; + + // The current running height. + int current_height_; + + // The current line of text. + string16 current_line_; + + // The output vector of lines. + std::vector<string16>* lines_; + + // Indicates whether there were too many lines for the available height. + bool full_; + + DISALLOW_COPY_AND_ASSIGN(RectangleText); +}; + +void RectangleText::AddString(const string16& input) { + base::i18n::BreakIterator lines(input, + base::i18n::BreakIterator::BREAK_NEWLINE); + if (lines.Init()) { + while (!full_ && lines.Advance()) { + string16 line = lines.GetString(); + // The BREAK_NEWLINE iterator will keep the trailing newline character, + // except in the case of the last line, which may not have one. Remove + // the newline character, if it exists. + if (!line.empty() && line[line.length() - 1] == '\n') + line.resize(line.length() - 1); + AddLine(line); + } + } else { + NOTREACHED() << "BreakIterator (lines) init failed"; + } +} + +bool RectangleText::Finalize() { + // Remove trailing whitespace from the last line or remove the last line + // completely, if it's just whitespace. + if (!full_ && !lines_->empty()) { + TrimWhitespace(lines_->back(), TRIM_TRAILING, &lines_->back()); + if (lines_->back().empty()) + lines_->resize(lines_->size() - 1); + } + return full_; +} + +bool RectangleText::IsWhitespaceString(const string16& text) const { + return text.find_first_not_of(kWhitespaceUTF16) == string16::npos; +} + +void RectangleText::AddLine(const string16& line) { + int line_width = font_.GetStringWidth(line); + if (line_width < available_pixel_width_) { + AddToCurrentLineWithWidth(line, line_width); + } else { + // Iterate over positions that are valid to break the line at. In general, + // these are word boundaries but after any punctuation following the word. + base::i18n::BreakIterator words(line, + base::i18n::BreakIterator::BREAK_LINE); + if (words.Init()) { + while (words.Advance()) { + bool truncate = !current_line_.empty(); + const string16& word = words.GetString(); + int lines_added = AddWord(word); + if (lines_added) { + if (truncate) { + // Trim trailing whitespace from the line that was added. + int line = lines_->size() - lines_added; + TrimWhitespace(lines_->at(line), TRIM_TRAILING, &lines_->at(line)); + } + if (IsWhitespaceString(word)) { + // Skip the first space if the previous line was carried over. + current_width_ = 0; + current_line_.clear(); + } + } + } + } else { + NOTREACHED() << "BreakIterator (words) init failed"; + } + } + // Account for naturally-occuring newlines. + NewLine(); +} + +int RectangleText::WrapWord(const string16& word) { + // Word is so wide that it must be fragmented. + string16 text = word; + int lines_added = 0; + bool first_fragment = true; + while (!full_ && !text.empty()) { + string16 fragment = + ui::ElideText(text, font_, available_pixel_width_, ui::TRUNCATE_AT_END); + if (!first_fragment && NewLine()) + lines_added++; + AddToCurrentLine(fragment); + text = text.substr(fragment.length()); + first_fragment = false; + } + return lines_added; +} + +int RectangleText::AddWordOverflow(const string16& word) { + int lines_added = 0; + + // Unless this is the very first word, put it on a new line. + if (!current_line_.empty()) { + if (!NewLine()) + return 0; + lines_added++; + } + + if (wrap_behavior_ == ui::IGNORE_LONG_WORDS) { + current_line_ = word; + current_width_ = available_pixel_width_; + } else if (wrap_behavior_ == ui::WRAP_LONG_WORDS) { + lines_added += WrapWord(word); + } else { + ui::ElideBehavior elide_behavior = (wrap_behavior_ == ui::ELIDE_LONG_WORDS ? + ui::ELIDE_AT_END : ui::TRUNCATE_AT_END); + string16 elided_word = + ui::ElideText(word, font_, available_pixel_width_, elide_behavior); + AddToCurrentLine(elided_word); + } + + return lines_added; +} + +int RectangleText::AddWord(const string16& word) { + int lines_added = 0; + string16 trimmed; + TrimWhitespace(word, TRIM_TRAILING, &trimmed); + int trimmed_width = font_.GetStringWidth(trimmed); + if (trimmed_width <= available_pixel_width_) { + // Word can be made to fit, no need to fragment it. + if ((current_width_ + trimmed_width > available_pixel_width_) && NewLine()) + lines_added++; + // Append the non-trimmed word, in case more words are added after. + AddToCurrentLine(word); + } else { + lines_added = AddWordOverflow(word); + } + return lines_added; +} + +void RectangleText::AddToCurrentLine(const string16& text) { + AddToCurrentLineWithWidth(text, font_.GetStringWidth(text)); +} + +void RectangleText::AddToCurrentLineWithWidth(const string16& text, + int text_width) { + if (current_height_ >= available_pixel_height_) { + full_ = true; + return; + } + current_line_.append(text); + current_width_ += text_width; +} + +bool RectangleText::NewLine() { + bool line_added = false; + if (current_height_ < available_pixel_height_) { + lines_->push_back(current_line_); + current_line_.clear(); + line_added = true; + } else { + full_ = true; + } + current_height_ += line_height_; + current_width_ = 0; + return line_added; +} + } // namespace namespace ui { @@ -724,6 +976,22 @@ bool ElideRectangleString(const string16& input, size_t max_rows, return rect.Finalize(); } +bool ElideRectangleText(const string16& input, + const gfx::Font& font, + int available_pixel_width, + int available_pixel_height, + WordWrapBehavior wrap_behavior, + std::vector<string16>* lines) { + RectangleText rect(font, + available_pixel_width, + available_pixel_height, + wrap_behavior, + lines); + rect.Init(); + rect.AddString(input); + return rect.Finalize(); +} + string16 TruncateString(const string16& string, size_t length) { if (string.size() <= length) // String fits, return it. diff --git a/ui/base/text/text_elider.h b/ui/base/text/text_elider.h index a1dd86a..f422698 100644 --- a/ui/base/text/text_elider.h +++ b/ui/base/text/text_elider.h @@ -7,6 +7,7 @@ #pragma once #include <string> +#include <vector> #include "base/basictypes.h" #include "base/string16.h" @@ -128,6 +129,39 @@ UI_EXPORT bool ElideRectangleString(const string16& input, size_t max_rows, size_t max_cols, bool strict, string16* output); +// Specifies the word wrapping behavior of |ElideRectangleText()| when a word +// would exceed the available width. +enum WordWrapBehavior { + // Words that are too wide will be put on a new line, but will not be + // truncated or elided. + IGNORE_LONG_WORDS, + + // Words that are too wide will be put on a new line and will be truncated to + // the available width. + TRUNCATE_LONG_WORDS, + + // Words that are too wide will be put on a new line and will be elided to the + // available width. + ELIDE_LONG_WORDS, + + // Words that are too wide will be put on a new line and will be wrapped over + // multiple lines. + WRAP_LONG_WORDS, +}; + +// Reformats |text| into output vector |lines| so that the resulting text fits +// into an |available_pixel_width| by |available_pixel_height| rectangle with +// the specified |font|. Input newlines are respected, but lines that are too +// long are broken into pieces. For words that are too wide to fit on a single +// line, the wrapping behavior can be specified with the |wrap_behavior| param. +// Returns |true| if the input had to be truncated (and not just reformatted). +UI_EXPORT bool ElideRectangleText(const string16& text, + const gfx::Font& font, + int available_pixel_width, + int available_pixel_height, + WordWrapBehavior wrap_behavior, + std::vector<string16>* lines); + // Truncates the string to length characters. This breaks the string at // the first word break before length, adding the horizontal ellipsis // character (unicode character 0x2026) to render ... diff --git a/ui/base/text/text_elider_unittest.cc b/ui/base/text/text_elider_unittest.cc index 439db7b..b6d5b87 100644 --- a/ui/base/text/text_elider_unittest.cc +++ b/ui/base/text/text_elider_unittest.cc @@ -418,6 +418,145 @@ TEST(TextEliderTest, ElideString) { } } +TEST(TextEliderTest, ElideRectangleText) { + const gfx::Font font; + const int kLineHeight = font.GetHeight(); + const int kTestWidth = font.GetStringWidth(ASCIIToUTF16("Test")); + struct TestData { + const char* input; + int available_pixel_width; + int available_pixel_height; + bool truncated; + const char* output; + } cases[] = { + { "", 0, 0, false, NULL }, + { "", 1, 1, false, NULL }, + { "Test", kTestWidth, 0, true, NULL }, + { "Test", kTestWidth, 1, false, "Test" }, + { "Test", kTestWidth, kLineHeight, false, "Test" }, + { "Test Test", kTestWidth, kLineHeight, true, "Test" }, + { "Test Test", kTestWidth, kLineHeight + 1, false, "Test|Test" }, + { "Test Test", kTestWidth, kLineHeight * 2, false, "Test|Test" }, + { "Test Test", kTestWidth, kLineHeight * 3, false, "Test|Test" }, + { "Test Test", kTestWidth * 2, kLineHeight * 2, false, "Test|Test" }, + { "Test Test", kTestWidth * 3, kLineHeight, false, "Test Test" }, + { "Test\nTest", kTestWidth * 3, kLineHeight * 2, false, "Test|Test" }, + { "Te\nst Te", kTestWidth, kLineHeight * 3, false, "Te|st|Te" }, + { "\nTest", kTestWidth, kLineHeight * 2, false, "|Test" }, + { "\nTest", kTestWidth, kLineHeight, true, "" }, + { "\n\nTest", kTestWidth, kLineHeight * 3, false, "||Test" }, + { "\n\nTest", kTestWidth, kLineHeight * 2, true, "|" }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + std::vector<string16> lines; + EXPECT_EQ(cases[i].truncated, + ui::ElideRectangleText(UTF8ToUTF16(cases[i].input), + font, + cases[i].available_pixel_width, + cases[i].available_pixel_height, + ui::TRUNCATE_LONG_WORDS, + &lines)); + if (cases[i].output) + EXPECT_EQ(cases[i].output, UTF16ToUTF8(JoinString(lines, '|'))); + else + EXPECT_TRUE(lines.empty()); + } +} + +TEST(TextEliderTest, ElideRectangleTextPunctuation) { + const gfx::Font font; + const int kLineHeight = font.GetHeight(); + const int kTestWidth = font.GetStringWidth(ASCIIToUTF16("Test")); + const int kTestTWidth = font.GetStringWidth(ASCIIToUTF16("Test T")); + struct TestData { + const char* input; + int available_pixel_width; + int available_pixel_height; + bool wrap_words; + bool truncated; + const char* output; + } cases[] = { + { "Test T.", kTestTWidth, kLineHeight * 2, false, false, "Test|T." }, + { "Test T ?", kTestTWidth, kLineHeight * 2, false, false, "Test|T ?" }, + { "Test. Test", kTestWidth, kLineHeight * 3, false, false, "Test|Test" }, + { "Test. Test", kTestWidth, kLineHeight * 3, true, false, "Test|.|Test" }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + std::vector<string16> lines; + ui::WordWrapBehavior wrap_behavior = + (cases[i].wrap_words ? ui::WRAP_LONG_WORDS : ui::TRUNCATE_LONG_WORDS); + EXPECT_EQ(cases[i].truncated, + ui::ElideRectangleText(UTF8ToUTF16(cases[i].input), + font, + cases[i].available_pixel_width, + cases[i].available_pixel_height, + wrap_behavior, + &lines)); + if (cases[i].output) + EXPECT_EQ(cases[i].output, UTF16ToUTF8(JoinString(lines, '|'))); + else + EXPECT_TRUE(lines.empty()); + } +} + +TEST(TextEliderTest, ElideRectangleTextLongWords) { + const gfx::Font font; + const int kAvailableHeight = 1000; + const string16 kElidedTesting = UTF8ToUTF16(std::string("Tes") + kEllipsis); + const int kElidedWidth = font.GetStringWidth(kElidedTesting); + const int kTestWidth = font.GetStringWidth(ASCIIToUTF16("Test")); + + struct TestData { + const char* input; + int available_pixel_width; + ui::WordWrapBehavior wrap_behavior; + const char* output; + } cases[] = { + { "Testing", kTestWidth, ui::IGNORE_LONG_WORDS, "Testing" }, + { "X Testing", kTestWidth, ui::IGNORE_LONG_WORDS, "X|Testing" }, + { "Test Testing", kTestWidth, ui::IGNORE_LONG_WORDS, "Test|Testing" }, + { "Test\nTesting", kTestWidth, ui::IGNORE_LONG_WORDS, "Test|Testing" }, + { "Test Tests ", kTestWidth, ui::IGNORE_LONG_WORDS, "Test|Tests" }, + { "Test Tests T", kTestWidth, ui::IGNORE_LONG_WORDS, "Test|Tests|T" }, + + { "Testing", kElidedWidth, ui::ELIDE_LONG_WORDS, "Tes..." }, + { "X Testing", kElidedWidth, ui::ELIDE_LONG_WORDS, "X|Tes..." }, + { "Test Testing", kElidedWidth, ui::ELIDE_LONG_WORDS, "Test|Tes..." }, + { "Test\nTesting", kElidedWidth, ui::ELIDE_LONG_WORDS, "Test|Tes..." }, + + { "Testing", kTestWidth, ui::TRUNCATE_LONG_WORDS, "Test" }, + { "X Testing", kTestWidth, ui::TRUNCATE_LONG_WORDS, "X|Test" }, + { "Test Testing", kTestWidth, ui::TRUNCATE_LONG_WORDS, "Test|Test" }, + { "Test\nTesting", kTestWidth, ui::TRUNCATE_LONG_WORDS, "Test|Test" }, + { "Test Tests ", kTestWidth, ui::TRUNCATE_LONG_WORDS, "Test|Test" }, + { "Test Tests T", kTestWidth, ui::TRUNCATE_LONG_WORDS, "Test|Test|T" }, + + { "Testing", kTestWidth, ui::WRAP_LONG_WORDS, "Test|ing" }, + { "X Testing", kTestWidth, ui::WRAP_LONG_WORDS, "X|Test|ing" }, + { "Test Testing", kTestWidth, ui::WRAP_LONG_WORDS, "Test|Test|ing" }, + { "Test\nTesting", kTestWidth, ui::WRAP_LONG_WORDS, "Test|Test|ing" }, + { "Test Tests ", kTestWidth, ui::WRAP_LONG_WORDS, "Test|Test|s" }, + { "Test Tests T", kTestWidth, ui::WRAP_LONG_WORDS, "Test|Test|s T" }, + { "TestTestTest", kTestWidth, ui::WRAP_LONG_WORDS, "Test|Test|Test" }, + { "TestTestTestT", kTestWidth, ui::WRAP_LONG_WORDS, "Test|Test|Test|T" }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + std::vector<string16> lines; + ui::ElideRectangleText(UTF8ToUTF16(cases[i].input), + font, + cases[i].available_pixel_width, + kAvailableHeight, + cases[i].wrap_behavior, + &lines); + std::string expected_output(cases[i].output); + ReplaceSubstringsAfterOffset(&expected_output, 0, "...", kEllipsis); + EXPECT_EQ(expected_output, UTF16ToUTF8(JoinString(lines, '|'))); + } +} + TEST(TextEliderTest, ElideRectangleString) { struct TestData { const char* input; |