summaryrefslogtreecommitdiffstats
path: root/ui/base
diff options
context:
space:
mode:
authorasvitkine@chromium.org <asvitkine@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-12-15 23:58:58 +0000
committerasvitkine@chromium.org <asvitkine@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-12-15 23:58:58 +0000
commit3785b3711fcfdbc8983fc9d8d24dd60fe511bdbc (patch)
tree663eaa476ce15ecb6eafc1afb1d2be11e9522690 /ui/base
parente9597ea273d45338b3f8391b32a5a6d6ef86f318 (diff)
downloadchromium_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.cc268
-rw-r--r--ui/base/text/text_elider.h34
-rw-r--r--ui/base/text/text_elider_unittest.cc139
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;