summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormsw@chromium.org <msw@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-06-29 13:02:47 +0000
committermsw@chromium.org <msw@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-06-29 13:02:47 +0000
commitfecf9f77fccd08c22e9655843a534c79ed0ba567 (patch)
treeb5b547490e03d455fedcb73a45cfdf5fe8ad81f9
parent9a8a76dea83aec91deac43479dd6c6f497510c47 (diff)
downloadchromium_src-fecf9f77fccd08c22e9655843a534c79ed0ba567.zip
chromium_src-fecf9f77fccd08c22e9655843a534c79ed0ba567.tar.gz
chromium_src-fecf9f77fccd08c22e9655843a534c79ed0ba567.tar.bz2
Clamp RenderTextWin layout length to 10,000 code points.
Add RenderText::truncate_length_ and setter to clamp layout text length. Avoid system limitations and performance degradation on Win/Uniscribe. This does not impact underlying text data, only what's shown on screen. Truncate text_ to layout_text_ in UpdateLayoutText() and append ellipsis. Only reveal obscured characters within the layout text range. Ensure the run and glyph limits are actually tried in their loops. Add unit tests for reasonable truncation behavior. Bail if layout fails, this will leave text un-rendered instead of crashing. Clamp run length to avoid exceeding the glyph limit later on. Clamp layout indices in conversion, fix run index bounds checks. TODO(followup): Truncate text on Linux/Pango as needed. TODO(followup): Increase supported text length and run counts. TODO(followup): UMA text lengths, run counts, failures, and truncations. BUG=235854,236406,248960,131660 TEST=Paste more than 10k characters on Win to see ellipsis and no crashes. (note that omnibox specific codepaths will cause additional performance degradation in that field). No crash on Windows with Search by Image extension use (tried various sizes 16x16 -> 96x96 -> 1900x1200 -> 3840x2160). Normal text behavior with <10k code points, and no behavior change on ChromeOS/Linux_Aura. R=asvitkine@chromium.org,oshima@chromium.org,sky@chromium.org Review URL: https://chromiumcodereview.appspot.com/17745005 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@209286 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--ui/base/text/text_elider.cc24
-rw-r--r--ui/base/text/text_elider.h1
-rw-r--r--ui/gfx/render_text.cc63
-rw-r--r--ui/gfx/render_text.h25
-rw-r--r--ui/gfx/render_text_unittest.cc144
-rw-r--r--ui/gfx/render_text_win.cc87
6 files changed, 239 insertions, 105 deletions
diff --git a/ui/base/text/text_elider.cc b/ui/base/text/text_elider.cc
index 5a4bc96..557607b 100644
--- a/ui/base/text/text_elider.cc
+++ b/ui/base/text/text_elider.cc
@@ -33,6 +33,7 @@ namespace ui {
// U+2026 in utf8
const char kEllipsis[] = "\xE2\x80\xA6";
+const char16 kEllipsisUTF16[] = { 0x2026, 0 };
const char16 kForwardSlash = '/';
namespace {
@@ -105,8 +106,6 @@ string16 BuildPathFromComponents(const string16& path_prefix,
const std::vector<string16>& path_elements,
const string16& filename,
size_t num_components) {
- const string16 kEllipsisAndSlash = UTF8ToUTF16(kEllipsis) + kForwardSlash;
-
// Add the initial elements of the path.
string16 path = path_prefix;
@@ -116,7 +115,7 @@ string16 BuildPathFromComponents(const string16& path_prefix,
// Add |filename|, ellipsis if necessary.
if (num_components != (path_elements.size() - 1))
- path += kEllipsisAndSlash;
+ path += UTF8ToUTF16(kEllipsis) + kForwardSlash;
path += filename;
return path;
@@ -133,8 +132,6 @@ string16 ElideComponentizedPath(const string16& url_path_prefix,
int available_pixel_width) {
const size_t url_path_number_of_elements = url_path_elements.size();
- const string16 kEllipsisAndSlash = UTF8ToUTF16(kEllipsis) + kForwardSlash;
-
CHECK(url_path_number_of_elements);
for (size_t i = url_path_number_of_elements - 1; i > 0; --i) {
string16 elided_path = BuildPathFromComponents(url_path_prefix,
@@ -167,8 +164,6 @@ string16 ElideEmail(const string16& email,
DCHECK(!username.empty());
DCHECK(!domain.empty());
- const string16 kEllipsisUTF16 = UTF8ToUTF16(kEllipsis);
-
// Subtract the @ symbol from the available width as it is mandatory.
const string16 kAtSignUTF16 = ASCIIToUTF16("@");
available_pixel_width -= font.GetStringWidth(kAtSignUTF16);
@@ -195,7 +190,7 @@ string16 ElideEmail(const string16& email,
// Failing to elide the domain such that at least one character remains
// (other than the ellipsis itself) remains: return a single ellipsis.
if (domain.length() <= 1U)
- return kEllipsisUTF16;
+ return string16(kEllipsisUTF16);
}
// Fit the username in the remaining width (at this point the elided username
@@ -367,11 +362,10 @@ string16 ElideUrl(const GURL& url,
// which means that this case has been resolved earlier.
string16 url_elided_domain = url_subdomain + url_domain;
if (pixel_width_url_subdomain > kPixelWidthDotsTrailer) {
- if (!url_subdomain.empty()) {
+ if (!url_subdomain.empty())
url_elided_domain = kEllipsisAndSlash[0] + url_domain;
- } else {
+ else
url_elided_domain = url_domain;
- }
elided_path = ElideComponentizedPath(url_elided_domain, url_path_elements,
url_filename, url_query, font,
@@ -455,13 +449,12 @@ string16 ElideText(const string16& text,
if (text.empty())
return text;
- const string16 kEllipsisUTF16 = UTF8ToUTF16(kEllipsis);
-
const int current_text_pixel_width = font.GetStringWidth(text);
const bool elide_in_middle = (elide_behavior == ELIDE_IN_MIDDLE);
const bool insert_ellipsis = (elide_behavior != TRUNCATE_AT_END);
- StringSlicer slicer(text, kEllipsisUTF16, elide_in_middle);
+ const string16 ellipsis = string16(kEllipsisUTF16);
+ StringSlicer slicer(text, ellipsis, elide_in_middle);
// Pango will return 0 width for absurdly long strings. Cut the string in
// half and try again.
@@ -479,8 +472,7 @@ string16 ElideText(const string16& text,
if (current_text_pixel_width <= available_pixel_width)
return text;
- if (insert_ellipsis &&
- font.GetStringWidth(kEllipsisUTF16) > available_pixel_width)
+ if (insert_ellipsis && font.GetStringWidth(ellipsis) > available_pixel_width)
return string16();
// Use binary search to compute the elided text.
diff --git a/ui/base/text/text_elider.h b/ui/base/text/text_elider.h
index c3cf215..62b4d0c 100644
--- a/ui/base/text/text_elider.h
+++ b/ui/base/text/text_elider.h
@@ -26,6 +26,7 @@ class FilePath;
namespace ui {
UI_EXPORT extern const char kEllipsis[];
+UI_EXPORT extern const char16 kEllipsisUTF16[];
// Elides a well-formed email address (e.g. username@domain.com) to fit into
// |available_pixel_width| using the specified |font|.
diff --git a/ui/gfx/render_text.cc b/ui/gfx/render_text.cc
index cdc706e..d734a84 100644
--- a/ui/gfx/render_text.cc
+++ b/ui/gfx/render_text.cc
@@ -9,9 +9,11 @@
#include "base/i18n/break_iterator.h"
#include "base/logging.h"
#include "base/stl_util.h"
+#include "third_party/icu/public/common/unicode/rbbi.h"
#include "third_party/icu/public/common/unicode/utf16.h"
#include "third_party/skia/include/core/SkTypeface.h"
#include "third_party/skia/include/effects/SkGradientShader.h"
+#include "ui/base/text/text_elider.h"
#include "ui/base/text/utf16_indexing.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/insets.h"
@@ -324,7 +326,7 @@ void RenderText::SetText(const base::string16& text) {
text_direction_ = base::i18n::UNKNOWN_DIRECTION;
obscured_reveal_index_ = -1;
- UpdateObscuredText();
+ UpdateLayoutText();
ResetLayout();
}
@@ -379,7 +381,7 @@ void RenderText::SetObscured(bool obscured) {
obscured_ = obscured;
obscured_reveal_index_ = -1;
cached_bounds_and_offset_valid_ = false;
- UpdateObscuredText();
+ UpdateLayoutText();
ResetLayout();
}
}
@@ -390,7 +392,7 @@ void RenderText::SetObscuredRevealIndex(int index) {
obscured_reveal_index_ = index;
cached_bounds_and_offset_valid_ = false;
- UpdateObscuredText();
+ UpdateLayoutText();
ResetLayout();
}
@@ -709,7 +711,7 @@ const Rect& RenderText::GetUpdatedCursorBounds() {
}
size_t RenderText::IndexOfAdjacentGrapheme(size_t index,
- LogicalCursorDirection direction) {
+ LogicalCursorDirection direction) {
if (index > text().length())
return text().length();
@@ -763,6 +765,7 @@ RenderText::RenderText()
composition_and_selection_styles_applied_(false),
obscured_(false),
obscured_reveal_index_(-1),
+ truncate_length_(0),
fade_head_(false),
fade_tail_(false),
background_is_transparent_(false),
@@ -803,7 +806,7 @@ void RenderText::SetSelectionModel(const SelectionModel& model) {
}
const base::string16& RenderText::GetLayoutText() const {
- return obscured() ? obscured_text_ : text();
+ return layout_text_.empty() ? text_ : layout_text_;
}
void RenderText::ApplyCompositionAndSelectionStyles() {
@@ -933,27 +936,37 @@ void RenderText::MoveCursorTo(size_t position, bool select) {
(cursor == 0) ? CURSOR_FORWARD : CURSOR_BACKWARD));
}
-void RenderText::UpdateObscuredText() {
- if (!obscured_)
- return;
+void RenderText::UpdateLayoutText() {
+ layout_text_.clear();
+
+ if (obscured_) {
+ size_t obscured_text_length =
+ static_cast<size_t>(ui::UTF16IndexToOffset(text_, 0, text_.length()));
+ layout_text_.assign(obscured_text_length, kPasswordReplacementChar);
+
+ if (obscured_reveal_index_ >= 0 &&
+ obscured_reveal_index_ < static_cast<int>(text_.length())) {
+ // Gets the index range in |text_| to be revealed.
+ size_t start = obscured_reveal_index_;
+ U16_SET_CP_START(text_.data(), 0, start);
+ size_t end = start;
+ UChar32 unused_char;
+ U16_NEXT(text_.data(), end, text_.length(), unused_char);
+
+ // Gets the index in |layout_text_| to be replaced.
+ const size_t cp_start =
+ static_cast<size_t>(ui::UTF16IndexToOffset(text_, 0, start));
+ if (layout_text_.length() > cp_start)
+ layout_text_.replace(cp_start, 1, text_.substr(start, end - start));
+ }
+ }
- const size_t obscured_text_length =
- static_cast<size_t>(ui::UTF16IndexToOffset(text_, 0, text_.length()));
- obscured_text_.assign(obscured_text_length, kPasswordReplacementChar);
-
- if (obscured_reveal_index_ >= 0 &&
- obscured_reveal_index_ < static_cast<int>(text_.length())) {
- // Gets the index range in |text_| to be revealed.
- size_t start = obscured_reveal_index_;
- U16_SET_CP_START(text_.data(), 0, start);
- size_t end = start;
- UChar32 unused_char;
- U16_NEXT(text_.data(), end, text_.length(), unused_char);
-
- // Gets the index in |obscured_text_| to be replaced.
- const size_t cp_start =
- static_cast<size_t>(ui::UTF16IndexToOffset(text_, 0, start));
- obscured_text_.replace(cp_start, 1, text_.substr(start, end - start));
+ const base::string16& text = obscured_ ? layout_text_ : text_;
+ if (truncate_length_ > 0 && truncate_length_ < text.length()) {
+ // Truncate the text at a valid character break and append an ellipsis.
+ icu::StringCharacterIterator iter(text.c_str());
+ iter.setIndex32(truncate_length_ - 1);
+ layout_text_.assign(text.substr(0, iter.getIndex()) + ui::kEllipsisUTF16);
}
}
diff --git a/ui/gfx/render_text.h b/ui/gfx/render_text.h
index c10b8ab..c0a078a 100644
--- a/ui/gfx/render_text.h
+++ b/ui/gfx/render_text.h
@@ -193,6 +193,12 @@ class UI_EXPORT RenderText {
// cleared when SetText or SetObscured is called.
void SetObscuredRevealIndex(int index);
+ // Set the maximum length of the displayed layout text, not the actual text.
+ // A |length| of 0 forgoes a hard limit, but does not guarantee proper
+ // functionality of very long strings. Applies to subsequent SetText calls.
+ // WARNING: Only use this for system limits, it lacks complex text support.
+ void set_truncate_length(size_t length) { truncate_length_ = length; }
+
const Rect& display_rect() const { return display_rect_; }
void SetDisplayRect(const Rect& r);
@@ -412,7 +418,7 @@ class UI_EXPORT RenderText {
// Draw the text.
virtual void DrawVisualText(Canvas* canvas) = 0;
- // Returns the text used for layout, which may be |obscured_text_|.
+ // Returns the text used for layout, which may be obscured or truncated.
const base::string16& GetLayoutText() const;
// Apply (and undo) temporary composition underlines and selection colors.
@@ -451,6 +457,8 @@ class UI_EXPORT RenderText {
FRIEND_TEST_ALL_PREFIXES(RenderTextTest, ApplyColorAndStyle);
FRIEND_TEST_ALL_PREFIXES(RenderTextTest, ObscuredText);
FRIEND_TEST_ALL_PREFIXES(RenderTextTest, RevealObscuredText);
+ FRIEND_TEST_ALL_PREFIXES(RenderTextTest, TruncatedText);
+ FRIEND_TEST_ALL_PREFIXES(RenderTextTest, TruncatedObscuredText);
FRIEND_TEST_ALL_PREFIXES(RenderTextTest, GraphemePositions);
FRIEND_TEST_ALL_PREFIXES(RenderTextTest, EdgeSelectionModels);
FRIEND_TEST_ALL_PREFIXES(RenderTextTest, GetTextOffset);
@@ -463,8 +471,8 @@ class UI_EXPORT RenderText {
// it is a NO-OP.
void MoveCursorTo(size_t position, bool select);
- // Updates |obscured_text_| if the text is obscured.
- void UpdateObscuredText();
+ // Updates |layout_text_| if the text is obscured or truncated.
+ void UpdateLayoutText();
// Update the cached bounds and display offset to ensure that the current
// cursor is within the visible display area.
@@ -537,14 +545,17 @@ class UI_EXPORT RenderText {
BreakList<bool> saved_underlines_;
bool composition_and_selection_styles_applied_;
- // A flag and the text to display for obscured (password) fields.
- // Asterisks are used instead of the actual text glyphs when true.
+ // A flag to obscure actual text with asterisks for password fields.
bool obscured_;
- base::string16 obscured_text_;
-
// The index at which the char should be revealed in the obscured text.
int obscured_reveal_index_;
+ // The maximum length of text to display, 0 forgoes a hard limit.
+ size_t truncate_length_;
+
+ // The obscured and/or truncated text that will be displayed.
+ base::string16 layout_text_;
+
// Fade text head and/or tail, if text doesn't fit into |display_rect_|.
bool fade_head_;
bool fade_tail_;
diff --git a/ui/gfx/render_text_unittest.cc b/ui/gfx/render_text_unittest.cc
index 151be2a..1af9135 100644
--- a/ui/gfx/render_text_unittest.cc
+++ b/ui/gfx/render_text_unittest.cc
@@ -5,6 +5,7 @@
#include "ui/gfx/render_text.h"
#include "base/memory/scoped_ptr.h"
+#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/break_list.h"
@@ -57,6 +58,23 @@ void SetRTL(bool rtl) {
EXPECT_EQ(rtl, base::i18n::IsRTL());
}
+// Ensure cursor movement in the specified |direction| yields |expected| values.
+void RunMoveCursorLeftRightTest(RenderText* render_text,
+ const std::vector<SelectionModel>& expected,
+ VisualCursorDirection direction) {
+ for (size_t i = 0; i < expected.size(); ++i) {
+ SCOPED_TRACE(base::StringPrintf("Going %s; expected value index %d.",
+ direction == CURSOR_LEFT ? "left" : "right", static_cast<int>(i)));
+ EXPECT_EQ(expected[i], render_text->selection_model());
+ render_text->MoveCursor(CHARACTER_BREAK, direction, false);
+ }
+ // Check that cursoring is clamped at the line edge.
+ EXPECT_EQ(expected.back(), render_text->selection_model());
+ // Check that it is the line edge.
+ render_text->MoveCursor(LINE_BREAK, direction, false);
+ EXPECT_EQ(expected.back(), render_text->selection_model());
+}
+
} // namespace
class RenderTextTest : public testing::Test {
@@ -373,6 +391,118 @@ TEST_F(RenderTextTest, RevealObscuredText) {
EXPECT_EQ(valid_expect_5_and_6, render_text->GetLayoutText());
}
+TEST_F(RenderTextTest, TruncatedText) {
+ struct {
+ const wchar_t* text;
+ const wchar_t* layout_text;
+ } cases[] = {
+ // Strings shorter than the truncation length should be laid out in full.
+ { L"", L"" },
+ { kWeak, kWeak },
+ { kLtr, kLtr },
+ { kLtrRtl, kLtrRtl },
+ { kLtrRtlLtr, kLtrRtlLtr },
+ { kRtl, kRtl },
+ { kRtlLtr, kRtlLtr },
+ { kRtlLtrRtl, kRtlLtrRtl },
+ // Strings as long as the truncation length should be laid out in full.
+ { L"01234", L"01234" },
+ // Long strings should be truncated with an ellipsis appended at the end.
+ { L"012345", L"0123\x2026" },
+ { L"012"L" . ", L"012 \x2026" },
+ { L"012"L"abc", L"012a\x2026" },
+ { L"012"L"a"L"\x5d0\x5d1", L"012a\x2026" },
+ { L"012"L"a"L"\x5d1"L"b", L"012a\x2026" },
+ { L"012"L"\x5d0\x5d1\x5d2", L"012\x5d0\x2026" },
+ { L"012"L"\x5d0\x5d1"L"a", L"012\x5d0\x2026" },
+ { L"012"L"\x5d0"L"a"L"\x5d1", L"012\x5d0\x2026" },
+ // Surrogate pairs should be truncated reasonably enough.
+ { L"0123\x0915\x093f", L"0123\x2026" },
+ { L"0\x05e9\x05bc\x05c1\x05b8", L"0\x05e9\x05bc\x05c1\x05b8" },
+ { L"01\x05e9\x05bc\x05c1\x05b8", L"01\x05e9\x05bc\x2026" },
+ { L"012\x05e9\x05bc\x05c1\x05b8", L"012\x05e9\x2026" },
+ { L"0123\x05e9\x05bc\x05c1\x05b8", L"0123\x2026" },
+ { L"01234\x05e9\x05bc\x05c1\x05b8", L"0123\x2026" },
+ { L"012\xF0\x9D\x84\x9E", L"012\xF0\x2026" },
+ };
+
+ scoped_ptr<RenderText> render_text(RenderText::CreateInstance());
+ render_text->set_truncate_length(5);
+ for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); i++) {
+ render_text->SetText(WideToUTF16(cases[i].text));
+ EXPECT_EQ(WideToUTF16(cases[i].text), render_text->text());
+ EXPECT_EQ(WideToUTF16(cases[i].layout_text), render_text->GetLayoutText())
+ << "For case " << i << ": " << cases[i].text;
+ }
+}
+
+TEST_F(RenderTextTest, TruncatedObscuredText) {
+ scoped_ptr<RenderText> render_text(RenderText::CreateInstance());
+ render_text->set_truncate_length(3);
+ render_text->SetObscured(true);
+ render_text->SetText(WideToUTF16(L"abcdef"));
+ EXPECT_EQ(WideToUTF16(L"abcdef"), render_text->text());
+ EXPECT_EQ(WideToUTF16(L"**\x2026"), render_text->GetLayoutText());
+}
+
+TEST_F(RenderTextTest, TruncatedCursorMovementLTR) {
+ scoped_ptr<RenderText> render_text(RenderText::CreateInstance());
+ render_text->set_truncate_length(2);
+ render_text->SetText(WideToUTF16(L"abcd"));
+
+ EXPECT_EQ(SelectionModel(0, CURSOR_BACKWARD), render_text->selection_model());
+ render_text->MoveCursor(LINE_BREAK, CURSOR_RIGHT, false);
+ EXPECT_EQ(SelectionModel(4, CURSOR_FORWARD), render_text->selection_model());
+ render_text->MoveCursor(LINE_BREAK, CURSOR_LEFT, false);
+ EXPECT_EQ(SelectionModel(0, CURSOR_BACKWARD), render_text->selection_model());
+
+ std::vector<SelectionModel> expected;
+ expected.push_back(SelectionModel(0, CURSOR_BACKWARD));
+ expected.push_back(SelectionModel(1, CURSOR_BACKWARD));
+ expected.push_back(SelectionModel(2, CURSOR_BACKWARD));
+ // The cursor hops over the elided text to the line end.
+ expected.push_back(SelectionModel(4, CURSOR_FORWARD));
+ expected.push_back(SelectionModel(4, CURSOR_FORWARD));
+ RunMoveCursorLeftRightTest(render_text.get(), expected, CURSOR_RIGHT);
+
+ expected.clear();
+ expected.push_back(SelectionModel(4, CURSOR_FORWARD));
+ // The cursor hops over the elided text to preceeding text.
+ expected.push_back(SelectionModel(1, CURSOR_FORWARD));
+ expected.push_back(SelectionModel(0, CURSOR_FORWARD));
+ expected.push_back(SelectionModel(0, CURSOR_BACKWARD));
+ RunMoveCursorLeftRightTest(render_text.get(), expected, CURSOR_LEFT);
+}
+
+TEST_F(RenderTextTest, TruncatedCursorMovementRTL) {
+ scoped_ptr<RenderText> render_text(RenderText::CreateInstance());
+ render_text->set_truncate_length(2);
+ render_text->SetText(WideToUTF16(L"\x5d0\x5d1\x5d2\x5d3"));
+
+ EXPECT_EQ(SelectionModel(0, CURSOR_BACKWARD), render_text->selection_model());
+ render_text->MoveCursor(LINE_BREAK, CURSOR_LEFT, false);
+ EXPECT_EQ(SelectionModel(4, CURSOR_FORWARD), render_text->selection_model());
+ render_text->MoveCursor(LINE_BREAK, CURSOR_RIGHT, false);
+ EXPECT_EQ(SelectionModel(0, CURSOR_BACKWARD), render_text->selection_model());
+
+ std::vector<SelectionModel> expected;
+ expected.push_back(SelectionModel(0, CURSOR_BACKWARD));
+ expected.push_back(SelectionModel(1, CURSOR_BACKWARD));
+ expected.push_back(SelectionModel(2, CURSOR_BACKWARD));
+ // The cursor hops over the elided text to the line end.
+ expected.push_back(SelectionModel(4, CURSOR_FORWARD));
+ expected.push_back(SelectionModel(4, CURSOR_FORWARD));
+ RunMoveCursorLeftRightTest(render_text.get(), expected, CURSOR_LEFT);
+
+ expected.clear();
+ expected.push_back(SelectionModel(4, CURSOR_FORWARD));
+ // The cursor hops over the elided text to preceeding text.
+ expected.push_back(SelectionModel(1, CURSOR_FORWARD));
+ expected.push_back(SelectionModel(0, CURSOR_FORWARD));
+ expected.push_back(SelectionModel(0, CURSOR_BACKWARD));
+ RunMoveCursorLeftRightTest(render_text.get(), expected, CURSOR_RIGHT);
+}
+
TEST_F(RenderTextTest, GetTextDirection) {
struct {
const wchar_t* text;
@@ -424,20 +554,6 @@ TEST_F(RenderTextTest, GetTextDirection) {
EXPECT_EQ(render_text->GetTextDirection(), base::i18n::RIGHT_TO_LEFT);
}
-void RunMoveCursorLeftRightTest(RenderText* render_text,
- const std::vector<SelectionModel>& expected,
- VisualCursorDirection direction) {
- for (size_t i = 0; i < expected.size(); ++i) {
- EXPECT_EQ(expected[i], render_text->selection_model());
- render_text->MoveCursor(CHARACTER_BREAK, direction, false);
- }
- // Check that cursoring is clamped at the line edge.
- EXPECT_EQ(expected.back(), render_text->selection_model());
- // Check that it is the line edge.
- render_text->MoveCursor(LINE_BREAK, direction, false);
- EXPECT_EQ(expected.back(), render_text->selection_model());
-}
-
TEST_F(RenderTextTest, MoveCursorLeftRightInLtr) {
scoped_ptr<RenderText> render_text(RenderText::CreateInstance());
diff --git a/ui/gfx/render_text_win.cc b/ui/gfx/render_text_win.cc
index 0469c0f..05be785 100644
--- a/ui/gfx/render_text_win.cc
+++ b/ui/gfx/render_text_win.cc
@@ -22,14 +22,18 @@ namespace gfx {
namespace {
-// The maximum supported number of Uniscribe runs; a SCRIPT_ITEM is 8 bytes.
-// TODO(msw): Review memory use/failure? Max string length? Alternate approach?
-const int kGuessItems = 100;
-const int kMaxItems = 10000;
+// The maximum length of text supported for Uniscribe layout and display.
+// This empirically chosen value should prevent major performance degradations.
+// TODO(msw): Support longer text, partial layout/painting, etc.
+const size_t kMaxUniscribeTextLength = 10000;
-// The maximum supported number of Uniscribe glyphs; a glyph is 1 word.
-// TODO(msw): Review memory use/failure? Max string length? Alternate approach?
-const int kMaxGlyphs = 100000;
+// The initial guess and maximum supported number of runs; arbitrary values.
+// TODO(msw): Support more runs, determine a better initial guess, etc.
+const int kGuessRuns = 100;
+const size_t kMaxRuns = 10000;
+
+// The maximum number of glyphs per run; ScriptShape fails on larger values.
+const size_t kMaxGlyphs = 65535;
// Callback to |EnumEnhMetaFile()| to intercept font creation.
int CALLBACK MetaFileEnumProc(HDC hdc,
@@ -181,6 +185,8 @@ RenderTextWin::RenderTextWin()
: RenderText(),
common_baseline_(0),
needs_layout_(false) {
+ set_truncate_length(kMaxUniscribeTextLength);
+
memset(&script_control_, 0, sizeof(script_control_));
memset(&script_state_, 0, sizeof(script_state_));
@@ -208,7 +214,7 @@ SelectionModel RenderTextWin::FindCursorPosition(const Point& point) {
// Find the run that contains the point and adjust the argument location.
int x = ToTextPoint(point).x();
size_t run_index = GetRunContainingXCoord(x);
- if (run_index == runs_.size())
+ if (run_index >= runs_.size())
return EdgeSelectionModel((x < 0) ? CURSOR_LEFT : CURSOR_RIGHT);
internal::TextRun* run = runs_[run_index];
@@ -345,7 +351,9 @@ void RenderTextWin::SetSelectionModel(const SelectionModel& model) {
ui::Range RenderTextWin::GetGlyphBounds(size_t index) {
const size_t run_index =
GetRunContainingCaret(SelectionModel(index, CURSOR_FORWARD));
- DCHECK_LT(run_index, runs_.size());
+ // Return edge bounds if the index is invalid or beyond the layout text size.
+ if (run_index >= runs_.size())
+ return ui::Range(string_size_.width());
internal::TextRun* run = runs_[run_index];
const size_t layout_index = TextIndexToLayoutIndex(index);
return ui::Range(GetGlyphXBoundary(run, layout_index, false),
@@ -386,14 +394,11 @@ std::vector<Rect> RenderTextWin::GetSubstringBounds(const ui::Range& range) {
}
size_t RenderTextWin::TextIndexToLayoutIndex(size_t index) const {
- if (!obscured())
- return index;
-
DCHECK_LE(index, text().length());
- const ptrdiff_t offset = ui::UTF16IndexToOffset(text(), 0, index);
- DCHECK_GE(offset, 0);
- DCHECK_LE(static_cast<size_t>(offset), GetLayoutText().length());
- return static_cast<size_t>(offset);
+ ptrdiff_t i = obscured() ? ui::UTF16IndexToOffset(text(), 0, index) : index;
+ CHECK_GE(i, 0);
+ // Clamp layout indices to the length of the text actually used for layout.
+ return std::min<size_t>(GetLayoutText().length(), i);
}
size_t RenderTextWin::LayoutIndexToTextIndex(size_t index) const {
@@ -496,23 +501,20 @@ void RenderTextWin::ItemizeLogicalText() {
HRESULT hr = E_OUTOFMEMORY;
int script_items_count = 0;
std::vector<SCRIPT_ITEM> script_items;
- const size_t text_length = GetLayoutText().length();
- for (size_t n = kGuessItems; hr == E_OUTOFMEMORY && n < kMaxItems; n *= 2) {
+ const size_t layout_text_length = GetLayoutText().length();
+ // Ensure that |kMaxRuns| is attempted and the loop terminates afterward.
+ for (size_t runs = kGuessRuns; hr == E_OUTOFMEMORY && runs <= kMaxRuns;
+ runs = std::max(runs + 1, std::min(runs * 2, kMaxRuns))) {
// Derive the array of Uniscribe script items from the logical text.
- // ScriptItemize always adds a terminal array item so that the length of the
- // last item can be derived from the terminal SCRIPT_ITEM::iCharPos.
- script_items.resize(n);
- hr = ScriptItemize(GetLayoutText().c_str(),
- text_length,
- n - 1,
- &script_control_,
- &script_state_,
- &script_items[0],
- &script_items_count);
+ // ScriptItemize always adds a terminal array item so that the length of
+ // the last item can be derived from the terminal SCRIPT_ITEM::iCharPos.
+ script_items.resize(runs);
+ hr = ScriptItemize(GetLayoutText().c_str(), layout_text_length,
+ runs - 1, &script_control_, &script_state_,
+ &script_items[0], &script_items_count);
}
DCHECK(SUCCEEDED(hr));
-
- if (script_items_count <= 0)
+ if (!SUCCEEDED(hr) || script_items_count <= 0)
return;
// Temporarily apply composition underlines and selection colors.
@@ -522,7 +524,7 @@ void RenderTextWin::ItemizeLogicalText() {
// TODO(msw): Only break for bold/italic, not color etc. See TextRun comment.
internal::StyleIterator style(colors(), styles());
SCRIPT_ITEM* script_item = &script_items[0];
- const size_t layout_text_length = GetLayoutText().length();
+ const size_t max_run_length = kMaxGlyphs / 2;
for (size_t run_break = 0; run_break < layout_text_length;) {
internal::TextRun* run = new internal::TextRun();
run->range.set_start(run_break);
@@ -541,6 +543,9 @@ void RenderTextWin::ItemizeLogicalText() {
const size_t script_item_break = (script_item + 1)->iCharPos;
run_break = std::min(script_item_break,
TextIndexToLayoutIndex(style.GetRange().end()));
+ // Clamp run lengths to avoid exceeding the maximum supported glyph count.
+ if ((run_break - run->range.start()) > max_run_length)
+ run_break = run->range.start() + max_run_length;
style.UpdatePosition(LayoutIndexToTextIndex(run_break));
if (script_item_break == run_break)
script_item++;
@@ -740,23 +745,19 @@ HRESULT RenderTextWin::ShapeTextRunWithFont(internal::TextRun* run,
HRESULT hr = E_OUTOFMEMORY;
const size_t run_length = run->range.length();
const wchar_t* run_text = &(GetLayoutText()[run->range.start()]);
- // Max glyph guess: http://msdn.microsoft.com/en-us/library/dd368564.aspx
+ // Guess the expected number of glyphs from the length of the run.
+ // MSDN suggests this at http://msdn.microsoft.com/en-us/library/dd368564.aspx
size_t max_glyphs = static_cast<size_t>(1.5 * run_length + 16);
- while (hr == E_OUTOFMEMORY && max_glyphs < kMaxGlyphs) {
+ while (hr == E_OUTOFMEMORY && max_glyphs <= kMaxGlyphs) {
run->glyph_count = 0;
run->glyphs.reset(new WORD[max_glyphs]);
run->visible_attributes.reset(new SCRIPT_VISATTR[max_glyphs]);
- hr = ScriptShape(cached_hdc_,
- &run->script_cache,
- run_text,
- run_length,
- max_glyphs,
- &run->script_analysis,
- run->glyphs.get(),
- run->logical_clusters.get(),
- run->visible_attributes.get(),
+ hr = ScriptShape(cached_hdc_, &run->script_cache, run_text, run_length,
+ max_glyphs, &run->script_analysis, run->glyphs.get(),
+ run->logical_clusters.get(), run->visible_attributes.get(),
&run->glyph_count);
- max_glyphs *= 2;
+ // Ensure that |kMaxGlyphs| is attempted and the loop terminates afterward.
+ max_glyphs = std::max(max_glyphs + 1, std::min(max_glyphs * 2, kMaxGlyphs));
}
return hr;
}