diff options
author | xji@google.com <xji@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-09-20 20:30:06 +0000 |
---|---|---|
committer | xji@google.com <xji@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-09-20 20:30:06 +0000 |
commit | 4eb0f75ebb5ed7d029700b6647a64a9917f1e7b4 (patch) | |
tree | 6d39c50a8e5def9399fa86955615e675204b58fc | |
parent | 9cabbaf78c3a3bbcb52c0ae6fbb06afd98224bf1 (diff) | |
download | chromium_src-4eb0f75ebb5ed7d029700b6647a64a9917f1e7b4.zip chromium_src-4eb0f75ebb5ed7d029700b6647a64a9917f1e7b4.tar.gz chromium_src-4eb0f75ebb5ed7d029700b6647a64a9917f1e7b4.tar.bz2 |
fix know issues in RenderText
1. add tests.
2. change SelectWord() to use BreakIterator, so it works for Chinese and Complex script.
3. DELETE/ReplaceChar delete/replace a whole grapheme. ReplaceTextInternal should only replace one grapheme when there is no selection.
4. pointing to position outside of text returns HOME/END position.
5. based on Chrome Linux omnibox and gedit, given
"abc| def", double click should select " " instead of "abc". Change test expectation.
BUG=90426
TEST=compile with touchui=1 test omnibox. run views_unittests.NativeTextfieldViewsTest/TextfieldViewsModelTest
Review URL: http://codereview.chromium.org/7841056
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@102006 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | base/i18n/break_iterator.cc | 21 | ||||
-rw-r--r-- | base/i18n/break_iterator.h | 6 | ||||
-rw-r--r-- | ui/gfx/render_text.cc | 69 | ||||
-rw-r--r-- | ui/gfx/render_text.h | 19 | ||||
-rw-r--r-- | ui/gfx/render_text_linux.cc | 38 | ||||
-rw-r--r-- | ui/gfx/render_text_linux.h | 20 | ||||
-rw-r--r-- | ui/gfx/render_text_unittest.cc | 337 | ||||
-rw-r--r-- | ui/gfx/render_text_win.cc | 61 | ||||
-rw-r--r-- | ui/gfx/render_text_win.h | 12 | ||||
-rw-r--r-- | views/controls/textfield/native_textfield_views_unittest.cc | 394 | ||||
-rw-r--r-- | views/controls/textfield/textfield_views_model.cc | 11 | ||||
-rw-r--r-- | views/controls/textfield/textfield_views_model_unittest.cc | 294 |
12 files changed, 1156 insertions, 126 deletions
diff --git a/base/i18n/break_iterator.cc b/base/i18n/break_iterator.cc index afa1fd8..eae531c 100644 --- a/base/i18n/break_iterator.cc +++ b/base/i18n/break_iterator.cc @@ -92,6 +92,27 @@ bool BreakIterator::IsWord() const { return (break_type_ == BREAK_WORD && status != UBRK_WORD_NONE); } +bool BreakIterator::IsEndOfWord(size_t position) const { + if (break_type_ != BREAK_WORD) + return false; + + UBreakIterator* iter = static_cast<UBreakIterator*>(iter_); + UBool boundary = ubrk_isBoundary(iter, static_cast<int32_t>(position)); + int32_t status = ubrk_getRuleStatus(iter); + return (!!boundary && status != UBRK_WORD_NONE); +} + +bool BreakIterator::IsStartOfWord(size_t position) const { + if (break_type_ != BREAK_WORD) + return false; + + UBreakIterator* iter = static_cast<UBreakIterator*>(iter_); + UBool boundary = ubrk_isBoundary(iter, static_cast<int32_t>(position)); + ubrk_next(iter); + int32_t next_status = ubrk_getRuleStatus(iter); + return (!!boundary && next_status != UBRK_WORD_NONE); +} + string16 BreakIterator::GetString() const { DCHECK(prev_ != npos && pos_ != npos); return string_.substr(prev_, pos_ - prev_); diff --git a/base/i18n/break_iterator.h b/base/i18n/break_iterator.h index f375962..3207256 100644 --- a/base/i18n/break_iterator.h +++ b/base/i18n/break_iterator.h @@ -88,6 +88,12 @@ class BASE_I18N_EXPORT BreakIterator { // this distinction doesn't apply and it always retuns false. bool IsWord() const; + // Under BREAK_WORD mode, returns true if |position| is at the end of word or + // at the start of word. It always retuns false under BREAK_LINE and + // BREAK_NEWLINE modes. + bool IsEndOfWord(size_t position) const; + bool IsStartOfWord(size_t position) const; + // Returns the string between prev() and pos(). // Advance() must have been called successfully at least once for pos() to // have advanced to somewhere useful. diff --git a/ui/gfx/render_text.cc b/ui/gfx/render_text.cc index 1111842..6d15e5c 100644 --- a/ui/gfx/render_text.cc +++ b/ui/gfx/render_text.cc @@ -247,6 +247,12 @@ bool RenderText::MoveCursorTo(const SelectionModel& selection_model) { sel.set_caret_pos(end.caret_pos()); sel.set_caret_placement(end.caret_placement()); } + + if (!IsCursorablePosition(sel.selection_start()) || + !IsCursorablePosition(sel.selection_end()) || + !IsCursorablePosition(sel.caret_pos())) + return false; + bool changed = !sel.Equals(selection_model_); SetSelectionModel(sel); return changed; @@ -281,41 +287,28 @@ void RenderText::SelectAll() { SetSelectionModel(sel); } -// TODO(xji): it does not work for languages do not use space as word breaker, -// such as Chinese. Should use BreakIterator. void RenderText::SelectWord() { - size_t selection_start = GetSelectionStart(); size_t cursor_position = GetCursorPosition(); - // First we setup selection_start_ and selection_end_. There are so many cases - // because we try to emulate what select-word looks like in a gtk textfield. - // See associated testcase for different cases. - if (cursor_position > 0 && cursor_position < text().length()) { - if (u_isalnum(text()[cursor_position])) { - selection_start = cursor_position; - cursor_position++; - } else - selection_start = cursor_position - 1; - } else if (cursor_position == 0) { - selection_start = cursor_position; - if (text().length() > 0) - cursor_position++; - } else { - selection_start = cursor_position - 1; - } - // Now we move selection_start_ to beginning of selection. Selection boundary - // is defined as the position where we have alpha-num character on one side - // and non-alpha-num char on the other side. - for (; selection_start > 0; selection_start--) { - if (IsPositionAtWordSelectionBoundary(selection_start)) + base::i18n::BreakIterator iter(text(), base::i18n::BreakIterator::BREAK_WORD); + bool success = iter.Init(); + DCHECK(success); + if (!success) + return; + + size_t selection_start = cursor_position; + for (; selection_start != 0; --selection_start) { + if (iter.IsStartOfWord(selection_start) || + iter.IsEndOfWord(selection_start)) break; } - // Now we move selection_end_ to end of selection. Selection boundary - // is defined as the position where we have alpha-num character on one side - // and non-alpha-num char on the other side. - for (; cursor_position < text().length(); cursor_position++) { - if (IsPositionAtWordSelectionBoundary(cursor_position)) + if (selection_start == cursor_position) + ++cursor_position; + + for (; cursor_position < text().length(); ++cursor_position) { + if (iter.IsEndOfWord(cursor_position) || + iter.IsStartOfWord(cursor_position)) break; } @@ -462,6 +455,10 @@ const Rect& RenderText::GetUpdatedCursorBounds() { return cursor_bounds_; } +size_t RenderText::GetIndexOfNextGrapheme(size_t position) { + return IndexOfAdjacentGrapheme(position, true); +} + RenderText::RenderText() : text_(), selection_model_(), @@ -557,8 +554,7 @@ SelectionModel RenderText::RightEndSelectionModel() { } size_t RenderText::GetIndexOfPreviousGrapheme(size_t position) { - // TODO(msw): Handle complex script. - return std::max(static_cast<long>(position - 1), static_cast<long>(0)); + return IndexOfAdjacentGrapheme(position, false); } std::vector<Rect> RenderText::GetSubstringBounds(size_t from, size_t to) { @@ -632,13 +628,10 @@ void RenderText::MoveCursorTo(size_t position, bool select) { SelectionModel::CaretPlacement placement = (caret_pos == cursor) ? SelectionModel::LEADING : SelectionModel::TRAILING; size_t selection_start = select ? GetSelectionStart() : cursor; - SelectionModel sel(selection_start, cursor, caret_pos, placement); - SetSelectionModel(sel); -} - -bool RenderText::IsPositionAtWordSelectionBoundary(size_t pos) { - return pos == 0 || (u_isalnum(text()[pos - 1]) && !u_isalnum(text()[pos])) || - (!u_isalnum(text()[pos - 1]) && u_isalnum(text()[pos])); + if (IsCursorablePosition(cursor)) { + SelectionModel sel(selection_start, cursor, caret_pos, placement); + SetSelectionModel(sel); + } } void RenderText::UpdateCachedBoundsAndOffset() { diff --git a/ui/gfx/render_text.h b/ui/gfx/render_text.h index 82eab91..4a1f9a9 100644 --- a/ui/gfx/render_text.h +++ b/ui/gfx/render_text.h @@ -193,7 +193,9 @@ class UI_EXPORT RenderText { // Set the selection_model_ to the value of |selection|. // The selection model components are modified if invalid. // Returns true if the cursor position or selection range changed. - // TODO(xji): need to check the cursor is set at grapheme boundary. + // If |selectin_start_| or |selection_end_| or |caret_pos_| in + // |selection_model| is not a cursorable position (not on grapheme boundary), + // it is a NO-OP and returns false. bool MoveCursorTo(const SelectionModel& selection_model); // Move the cursor to the position associated with the clicked point. @@ -254,6 +256,9 @@ class UI_EXPORT RenderText { // Subsequent text, cursor, or bounds changes may invalidate returned values. const Rect& GetUpdatedCursorBounds(); + // Get the logical index of the grapheme following the argument |position|. + virtual size_t GetIndexOfNextGrapheme(size_t position); + protected: RenderText(); @@ -288,6 +293,10 @@ class UI_EXPORT RenderText { // TODO(msw) Re-evaluate this function's necessity and signature. virtual std::vector<Rect> GetSubstringBounds(size_t from, size_t to); + // Return true if cursor can appear in front of the character at |position|, + // which means it is a grapheme boundary or the first character in the text. + virtual bool IsCursorablePosition(size_t position) = 0; + // Apply composition style (underline) to composition range and selection // style (foreground) to selection range. void ApplyCompositionAndSelectionStyles(StyleRanges* style_ranges) const; @@ -305,16 +314,20 @@ class UI_EXPORT RenderText { FRIEND_TEST_ALL_PREFIXES(RenderTextTest, ApplyStyleRange); FRIEND_TEST_ALL_PREFIXES(RenderTextTest, StyleRangesAdjust); + // Return an index belonging to the |next| or previous logical grapheme. + // The return value is bounded by 0 and the text length, inclusive. + virtual size_t IndexOfAdjacentGrapheme(size_t index, bool next) = 0; + // Sets the selection model, the argument is assumed to be valid. void SetSelectionModel(const SelectionModel& selection_model); // Set the cursor to |position|, with the caret trailing the previous // grapheme, or if there is no previous grapheme, leading the cursor position. // If |select| is false, the selection start is moved to the same position. + // If the |position| is not a cursorable position (not on grapheme boundary), + // it is a NO-OP. void MoveCursorTo(size_t position, bool select); - bool IsPositionAtWordSelectionBoundary(size_t pos); - // Update the cached bounds and display offset to ensure that the current // cursor is within the visible display area. void UpdateCachedBoundsAndOffset(); diff --git a/ui/gfx/render_text_linux.cc b/ui/gfx/render_text_linux.cc index 1e5e865..d5ac9c1 100644 --- a/ui/gfx/render_text_linux.cc +++ b/ui/gfx/render_text_linux.cc @@ -123,13 +123,19 @@ void RenderTextLinux::Draw(Canvas* canvas) { } SelectionModel RenderTextLinux::FindCursorPosition(const Point& point) { - // TODO(xji): when points outside of text, return HOME/END position. PangoLayout* layout = EnsureLayout(); if (text().empty()) return SelectionModel(0, 0, SelectionModel::LEADING); Point p(ToTextPoint(point)); + + // When the point is outside of text, return HOME/END position. + if (p.x() < 0) + return LeftEndSelectionModel(); + else if (p.x() > GetStringWidth()) + return RightEndSelectionModel(); + int caret_pos, trailing; pango_layout_xy_to_index(layout, p.x() * PANGO_SCALE, p.y() * PANGO_SCALE, &caret_pos, &trailing); @@ -212,7 +218,7 @@ SelectionModel RenderTextLinux::LeftEndSelectionModel() { return SelectionModel(text().length(), caret, SelectionModel::LEADING); } else { // RTL. size_t caret = Utf16IndexOfAdjacentGrapheme(item->offset + item->length, - PREVIOUS); + false); return SelectionModel(text().length(), caret, SelectionModel::TRAILING); } } @@ -227,7 +233,7 @@ SelectionModel RenderTextLinux::RightEndSelectionModel() { PangoItem* item = last_visual_run->item; if (item->analysis.level % 2 == 0) { // LTR. size_t caret = Utf16IndexOfAdjacentGrapheme(item->offset + item->length, - PREVIOUS); + false); return SelectionModel(text().length(), caret, SelectionModel::TRAILING); } else { // RTL. size_t caret = Utf8IndexToUtf16Index(item->offset); @@ -238,16 +244,18 @@ SelectionModel RenderTextLinux::RightEndSelectionModel() { return SelectionModel(0, 0, SelectionModel::LEADING); } -size_t RenderTextLinux::GetIndexOfPreviousGrapheme(size_t position) { +bool RenderTextLinux::IsCursorablePosition(size_t position) { + if (position == 0 && text().empty()) + return true; + EnsureLayout(); - size_t index = Utf16IndexToUtf8Index(position); - return Utf16IndexOfAdjacentGrapheme(index, PREVIOUS); + return (position >= 0 && position < static_cast<size_t>(num_log_attrs_) && + log_attrs_[position].is_cursor_position); } -size_t RenderTextLinux::GetIndexOfNextGrapheme(size_t position) { +size_t RenderTextLinux::IndexOfAdjacentGrapheme(size_t index, bool next) { EnsureLayout(); - size_t index = Utf16IndexToUtf8Index(position); - return Utf16IndexOfAdjacentGrapheme(index, NEXT); + return Utf16IndexOfAdjacentGrapheme(Utf16IndexToUtf8Index(index), next); } GSList* RenderTextLinux::GetRunContainingPosition(size_t position) const { @@ -266,12 +274,12 @@ GSList* RenderTextLinux::GetRunContainingPosition(size_t position) const { size_t RenderTextLinux::Utf8IndexOfAdjacentGrapheme( size_t utf8_index_of_current_grapheme, - RelativeLogicalPosition pos) const { + bool next) const { const char* ch = layout_text_ + utf8_index_of_current_grapheme; int char_offset = static_cast<int>(g_utf8_pointer_to_offset(layout_text_, ch)); int start_char_offset = char_offset; - if (pos == PREVIOUS) { + if (!next) { if (char_offset > 0) { do { --char_offset; @@ -292,23 +300,23 @@ size_t RenderTextLinux::Utf8IndexOfAdjacentGrapheme( size_t RenderTextLinux::Utf16IndexOfAdjacentGrapheme( size_t utf8_index_of_current_grapheme, - RelativeLogicalPosition pos) const { + bool next) const { size_t utf8_index = Utf8IndexOfAdjacentGrapheme( - utf8_index_of_current_grapheme, pos); + utf8_index_of_current_grapheme, next); return Utf8IndexToUtf16Index(utf8_index); } SelectionModel RenderTextLinux::FirstSelectionModelInsideRun( const PangoItem* item) const { size_t caret = Utf8IndexToUtf16Index(item->offset); - size_t cursor = Utf16IndexOfAdjacentGrapheme(item->offset, NEXT); + size_t cursor = Utf16IndexOfAdjacentGrapheme(item->offset, true); return SelectionModel(cursor, caret, SelectionModel::TRAILING); } SelectionModel RenderTextLinux::LastSelectionModelInsideRun( const PangoItem* item) const { size_t caret = Utf16IndexOfAdjacentGrapheme(item->offset + item->length, - PREVIOUS); + false); return SelectionModel(caret, caret, SelectionModel::LEADING); } diff --git a/ui/gfx/render_text_linux.h b/ui/gfx/render_text_linux.h index 5d67b41..641e261 100644 --- a/ui/gfx/render_text_linux.h +++ b/ui/gfx/render_text_linux.h @@ -39,28 +39,22 @@ class RenderTextLinux : public RenderText { BreakType break_type) OVERRIDE; virtual SelectionModel LeftEndSelectionModel() OVERRIDE; virtual SelectionModel RightEndSelectionModel() OVERRIDE; - virtual size_t GetIndexOfPreviousGrapheme(size_t position) OVERRIDE; + virtual bool IsCursorablePosition(size_t position) OVERRIDE; private: - enum RelativeLogicalPosition { - PREVIOUS, - NEXT - }; - - // Get the logical start index of the next grapheme after |position|. - size_t GetIndexOfNextGrapheme(size_t position); + virtual size_t IndexOfAdjacentGrapheme(size_t index, bool next) OVERRIDE; // Returns the run that contains |position|. Return NULL if not found. GSList* GetRunContainingPosition(size_t position) const; // Given |utf8_index_of_current_grapheme|, returns the UTF8 or UTF16 index of - // next grapheme in the text if |pos| is NEXT, otherwise, returns the index of - // previous grapheme. Returns 0 if there is no previous grapheme, and returns - // the |text_| length if there is no next grapheme. + // next grapheme in the text if |next| is true, otherwise, returns the index + // of previous grapheme. Returns 0 if there is no previous grapheme, and + // returns the |text_| length if there is no next grapheme. size_t Utf8IndexOfAdjacentGrapheme(size_t utf8_index_of_current_grapheme, - RelativeLogicalPosition pos) const; + bool next) const; size_t Utf16IndexOfAdjacentGrapheme(size_t utf8_index_of_current_grapheme, - RelativeLogicalPosition pos) const; + bool next) const; // Given a |run|, returns the SelectionModel that contains the logical first // or last caret position inside (not at a boundary of) the run. diff --git a/ui/gfx/render_text_unittest.cc b/ui/gfx/render_text_unittest.cc index 26389df..6898537 100644 --- a/ui/gfx/render_text_unittest.cc +++ b/ui/gfx/render_text_unittest.cc @@ -15,14 +15,14 @@ class RenderTextTest : public testing::Test { TEST_F(RenderTextTest, DefaultStyle) { // Defaults to empty text with no styles. - scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateRenderText()); + scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); EXPECT_TRUE(render_text->text().empty()); EXPECT_TRUE(render_text->style_ranges().empty()); // Test that the built-in default style is applied for new text. render_text->SetText(ASCIIToUTF16("abc")); EXPECT_EQ(1U, render_text->style_ranges().size()); - gfx::StyleRange style; + StyleRange style; EXPECT_EQ(style.font.GetFontName(), render_text->style_ranges()[0].font.GetFontName()); EXPECT_EQ(style.foreground, render_text->style_ranges()[0].foreground); @@ -38,8 +38,8 @@ TEST_F(RenderTextTest, DefaultStyle) { TEST_F(RenderTextTest, CustomDefaultStyle) { // Test a custom default style. - scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateRenderText()); - gfx::StyleRange color; + scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); + StyleRange color; color.foreground = SK_ColorRED; render_text->set_default_style(color); render_text->SetText(ASCIIToUTF16("abc")); @@ -54,7 +54,7 @@ TEST_F(RenderTextTest, CustomDefaultStyle) { EXPECT_EQ(color.foreground, render_text->style_ranges()[0].foreground); // Test ApplyDefaultStyle after setting a new default. - gfx::StyleRange strike; + StyleRange strike; strike.strike = true; render_text->set_default_style(strike); render_text->ApplyDefaultStyle(); @@ -64,24 +64,24 @@ TEST_F(RenderTextTest, CustomDefaultStyle) { } TEST_F(RenderTextTest, ApplyStyleRange) { - scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateRenderText()); + scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); render_text->SetText(ASCIIToUTF16("01234")); EXPECT_EQ(1U, render_text->style_ranges().size()); // Test ApplyStyleRange (no-op on empty range). - gfx::StyleRange empty; + StyleRange empty; empty.range = ui::Range(1, 1); render_text->ApplyStyleRange(empty); EXPECT_EQ(1U, render_text->style_ranges().size()); // Test ApplyStyleRange (no-op on invalid range). - gfx::StyleRange invalid; + StyleRange invalid; invalid.range = ui::Range::InvalidRange(); render_text->ApplyStyleRange(invalid); EXPECT_EQ(1U, render_text->style_ranges().size()); // Apply a style with a range contained by an existing range. - gfx::StyleRange underline; + StyleRange underline; underline.underline = true; underline.range = ui::Range(2, 3); render_text->ApplyStyleRange(underline); @@ -94,7 +94,7 @@ TEST_F(RenderTextTest, ApplyStyleRange) { EXPECT_FALSE(render_text->style_ranges()[2].underline); // Apply a style with a range equal to another range. - gfx::StyleRange color; + StyleRange color; color.foreground = SK_ColorWHITE; color.range = ui::Range(2, 3); render_text->ApplyStyleRange(color); @@ -111,7 +111,7 @@ TEST_F(RenderTextTest, ApplyStyleRange) { // Apply a style with a range containing an existing range. // This new style also overlaps portions of neighboring ranges. - gfx::StyleRange strike; + StyleRange strike; strike.strike = true; strike.range = ui::Range(1, 4); render_text->ApplyStyleRange(strike); @@ -124,7 +124,7 @@ TEST_F(RenderTextTest, ApplyStyleRange) { EXPECT_FALSE(render_text->style_ranges()[2].strike); // Apply a style overlapping all ranges. - gfx::StyleRange strike_underline; + StyleRange strike_underline; strike_underline.strike = true; strike_underline.underline = true; strike_underline.range = ui::Range(0, render_text->text().length()); @@ -144,7 +144,7 @@ TEST_F(RenderTextTest, ApplyStyleRange) { TEST_F(RenderTextTest, StyleRangesAdjust) { // Test that style ranges adjust to the text size. - scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateRenderText()); + scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); render_text->SetText(ASCIIToUTF16("abcdef")); EXPECT_EQ(1U, render_text->style_ranges().size()); EXPECT_EQ(ui::Range(0, 6), render_text->style_ranges()[0].range); @@ -155,7 +155,7 @@ TEST_F(RenderTextTest, StyleRangesAdjust) { EXPECT_EQ(ui::Range(0, 3), render_text->style_ranges()[0].range); // Test that the last range extends to the length of longer text. - gfx::StyleRange strike; + StyleRange strike; strike.strike = true; strike.range = ui::Range(2, 3); render_text->ApplyStyleRange(strike); @@ -178,4 +178,313 @@ TEST_F(RenderTextTest, StyleRangesAdjust) { EXPECT_FALSE(render_text->style_ranges()[0].strike); } +void RunMoveCursorLeftRightTest(RenderText* render_text, + const std::vector<SelectionModel>& expected, + bool move_right) { + for (int i = 0; i < static_cast<int>(expected.size()); ++i) { + SelectionModel sel = expected[i]; + EXPECT_TRUE(render_text->selection_model().Equals(sel)); + if (move_right) + render_text->MoveCursorRight(CHARACTER_BREAK, false); + else + render_text->MoveCursorLeft(CHARACTER_BREAK, false); + } + + SelectionModel sel = expected[expected.size() - 1]; + if (move_right) + render_text->MoveCursorRight(LINE_BREAK, false); + else + render_text->MoveCursorLeft(LINE_BREAK, false); + EXPECT_TRUE(render_text->selection_model().Equals(sel)); +} + +TEST_F(RenderTextTest, MoveCursorLeftRightInLtr) { + scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); + + // Pure LTR. + render_text->SetText(ASCIIToUTF16("abc")); + // |expected| saves the expected SelectionModel when moving cursor from left + // to right. + std::vector<SelectionModel> expected; + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(1, 0, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(2, 1, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); + // The last element is to test the clamped line ends. + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); + RunMoveCursorLeftRightTest(render_text.get(), expected, true); + + expected.clear(); + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(2, 2, SelectionModel::LEADING)); + expected.push_back(SelectionModel(1, 1, SelectionModel::LEADING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + RunMoveCursorLeftRightTest(render_text.get(), expected, false); +} + +TEST_F(RenderTextTest, MoveCursorLeftRightInLtrRtl) { + scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); + // LTR-RTL + render_text->SetText(WideToUTF16(L"abc\x05d0\x05d1\x05d2")); + // The last one is the expected END position. + std::vector<SelectionModel> expected; + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(1, 0, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(2, 1, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(5, 5, SelectionModel::LEADING)); + expected.push_back(SelectionModel(4, 4, SelectionModel::LEADING)); + expected.push_back(SelectionModel(3, 3, SelectionModel::LEADING)); + expected.push_back(SelectionModel(6, 3, SelectionModel::LEADING)); + expected.push_back(SelectionModel(6, 3, SelectionModel::LEADING)); + RunMoveCursorLeftRightTest(render_text.get(), expected, true); + + expected.clear(); + expected.push_back(SelectionModel(6, 3, SelectionModel::LEADING)); + expected.push_back(SelectionModel(4, 3, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(5, 4, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(6, 5, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(2, 2, SelectionModel::LEADING)); + expected.push_back(SelectionModel(1, 1, SelectionModel::LEADING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + RunMoveCursorLeftRightTest(render_text.get(), expected, false); +} + +TEST_F(RenderTextTest, MoveCursorLeftRightInLtrRtlLtr) { + scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); + // LTR-RTL-LTR. + render_text->SetText(WideToUTF16(L"a"L"\x05d1"L"b")); + std::vector<SelectionModel> expected; + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(1, 0, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(1, 1, SelectionModel::LEADING)); + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); + RunMoveCursorLeftRightTest(render_text.get(), expected, true); + + expected.clear(); + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(2, 2, SelectionModel::LEADING)); + expected.push_back(SelectionModel(2, 1, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + RunMoveCursorLeftRightTest(render_text.get(), expected, false); +} + +TEST_F(RenderTextTest, MoveCursorLeftRightInRtl) { + scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); + // Pure RTL. + render_text->SetText(WideToUTF16(L"\x05d0\x05d1\x05d2")); + render_text->MoveCursorRight(LINE_BREAK, false); + std::vector<SelectionModel> expected; + +#if defined(OS_LINUX) + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); +#else + expected.push_back(SelectionModel(3, 0, SelectionModel::LEADING)); +#endif + expected.push_back(SelectionModel(1, 0, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(2, 1, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); +#if defined(OS_LINUX) + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); +#else + expected.push_back(SelectionModel(0, 2, SelectionModel::TRAILING)); + // TODO(xji): expected (0, 2, TRAILING), actual (3, 0, LEADING). + // cursor moves from leftmost to rightmost. + // expected.push_back(SelectionModel(0, 2, SelectionModel::TRAILING)); +#endif + RunMoveCursorLeftRightTest(render_text.get(), expected, false); + + expected.clear(); + +#if defined(OS_LINUX) + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); +#else + expected.push_back(SelectionModel(0, 2, SelectionModel::TRAILING)); +#endif + expected.push_back(SelectionModel(2, 2, SelectionModel::LEADING)); + expected.push_back(SelectionModel(1, 1, SelectionModel::LEADING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); +#if defined(OS_LINUX) + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); +#else + expected.push_back(SelectionModel(3, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(3, 0, SelectionModel::LEADING)); +#endif + RunMoveCursorLeftRightTest(render_text.get(), expected, true); +} + +TEST_F(RenderTextTest, MoveCursorLeftRightInRtlLtr) { + scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); + // RTL-LTR + render_text->SetText(WideToUTF16(L"\x05d0\x05d1\x05d2"L"abc")); + render_text->MoveCursorRight(LINE_BREAK, false); + std::vector<SelectionModel> expected; +#if defined(OS_LINUX) + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(1, 0, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(2, 1, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(5, 5, SelectionModel::LEADING)); + expected.push_back(SelectionModel(4, 4, SelectionModel::LEADING)); + expected.push_back(SelectionModel(3, 3, SelectionModel::LEADING)); + expected.push_back(SelectionModel(6, 3, SelectionModel::LEADING)); + expected.push_back(SelectionModel(6, 3, SelectionModel::LEADING)); +#else + expected.push_back(SelectionModel(6, 5, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(5, 5, SelectionModel::LEADING)); + expected.push_back(SelectionModel(4, 4, SelectionModel::LEADING)); + expected.push_back(SelectionModel(3, 3, SelectionModel::LEADING)); + expected.push_back(SelectionModel(1, 0, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(2, 1, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(0, 2, SelectionModel::TRAILING)); + // TODO(xji): expected (0, 2, TRAILING), actual (3, 0, LEADING). + // cursor moves from leftmost to middle. + // expected.push_back(SelectionModel(0, 2, SelectionModel::TRAILING)); +#endif + + RunMoveCursorLeftRightTest(render_text.get(), expected, false); + + expected.clear(); +#if defined(OS_LINUX) + expected.push_back(SelectionModel(6, 3, SelectionModel::LEADING)); + expected.push_back(SelectionModel(4, 3, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(5, 4, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(6, 5, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(2, 2, SelectionModel::LEADING)); + expected.push_back(SelectionModel(1, 1, SelectionModel::LEADING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); +#else + expected.push_back(SelectionModel(0, 2, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(2, 2, SelectionModel::LEADING)); + expected.push_back(SelectionModel(1, 1, SelectionModel::LEADING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(4, 3, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(5, 4, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(6, 5, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(6, 5, SelectionModel::TRAILING)); +#endif + RunMoveCursorLeftRightTest(render_text.get(), expected, true); +} + +TEST_F(RenderTextTest, MoveCursorLeftRightInRtlLtrRtl) { + scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); + // RTL-LTR-RTL. + render_text->SetText(WideToUTF16(L"\x05d0"L"a"L"\x05d1")); + render_text->MoveCursorRight(LINE_BREAK, false); + std::vector<SelectionModel> expected; +#if defined(OS_LINUX) + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(1, 0, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(1, 1, SelectionModel::LEADING)); + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); +#else + expected.push_back(SelectionModel(3, 2, SelectionModel::LEADING)); + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(1, 1, SelectionModel::LEADING)); + expected.push_back(SelectionModel(1, 0, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::TRAILING)); + // TODO(xji): expected (0, 0, TRAILING), actual (2, 1, LEADING). + // cursor moves from leftmost to middle. + // expected.push_back(SelectionModel(0, 0, SelectionModel::TRAILING)); +#endif + RunMoveCursorLeftRightTest(render_text.get(), expected, false); + + expected.clear(); +#if defined(OS_LINUX) + expected.push_back(SelectionModel(3, 2, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(2, 2, SelectionModel::LEADING)); + expected.push_back(SelectionModel(2, 1, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); +#else + expected.push_back(SelectionModel(0, 0, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(0, 0, SelectionModel::LEADING)); + expected.push_back(SelectionModel(2, 1, SelectionModel::TRAILING)); + expected.push_back(SelectionModel(2, 2, SelectionModel::LEADING)); + expected.push_back(SelectionModel(3, 2, SelectionModel::LEADING)); + expected.push_back(SelectionModel(3, 2, SelectionModel::LEADING)); +#endif + RunMoveCursorLeftRightTest(render_text.get(), expected, true); +} + +// TODO(xji): temporarily disable in platform Win since the complex script +// characters turned into empty square due to font regression. So, not able +// to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) +TEST_F(RenderTextTest, MoveCursorLeftRight_ComplexScript) { + scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); + + render_text->SetText(WideToUTF16(L"\x0915\x093f\x0915\x094d\x0915")); + EXPECT_EQ(0U, render_text->GetCursorPosition()); + render_text->MoveCursorRight(CHARACTER_BREAK, false); + EXPECT_EQ(2U, render_text->GetCursorPosition()); + render_text->MoveCursorRight(CHARACTER_BREAK, false); + EXPECT_EQ(4U, render_text->GetCursorPosition()); + render_text->MoveCursorRight(CHARACTER_BREAK, false); + EXPECT_EQ(5U, render_text->GetCursorPosition()); + render_text->MoveCursorRight(CHARACTER_BREAK, false); + EXPECT_EQ(5U, render_text->GetCursorPosition()); + + render_text->MoveCursorLeft(CHARACTER_BREAK, false); + EXPECT_EQ(4U, render_text->GetCursorPosition()); + render_text->MoveCursorLeft(CHARACTER_BREAK, false); + EXPECT_EQ(2U, render_text->GetCursorPosition()); + render_text->MoveCursorLeft(CHARACTER_BREAK, false); + EXPECT_EQ(0U, render_text->GetCursorPosition()); + render_text->MoveCursorLeft(CHARACTER_BREAK, false); + EXPECT_EQ(0U, render_text->GetCursorPosition()); +} +#endif + +TEST_F(RenderTextTest, MoveCursorLeftRightWithSelection) { + scoped_ptr<RenderText> render_text(RenderText::CreateRenderText()); + render_text->SetText(WideToUTF16(L"abc\x05d0\x05d1\x05d2")); + // Left arrow on select ranging (6, 4). + render_text->MoveCursorRight(LINE_BREAK, false); + EXPECT_EQ(6U, render_text->GetCursorPosition()); + render_text->MoveCursorLeft(CHARACTER_BREAK, false); + EXPECT_EQ(4U, render_text->GetCursorPosition()); + render_text->MoveCursorLeft(CHARACTER_BREAK, false); + EXPECT_EQ(5U, render_text->GetCursorPosition()); + render_text->MoveCursorLeft(CHARACTER_BREAK, false); + EXPECT_EQ(6U, render_text->GetCursorPosition()); + render_text->MoveCursorRight(CHARACTER_BREAK, true); + EXPECT_EQ(6U, render_text->GetSelectionStart()); + EXPECT_EQ(5U, render_text->GetCursorPosition()); + render_text->MoveCursorRight(CHARACTER_BREAK, true); + EXPECT_EQ(6U, render_text->GetSelectionStart()); + EXPECT_EQ(4U, render_text->GetCursorPosition()); + render_text->MoveCursorLeft(CHARACTER_BREAK, false); + EXPECT_EQ(6U, render_text->GetCursorPosition()); + + // Right arrow on select ranging (4, 6). + render_text->MoveCursorLeft(LINE_BREAK, false); + EXPECT_EQ(0U, render_text->GetCursorPosition()); + render_text->MoveCursorRight(CHARACTER_BREAK, false); + EXPECT_EQ(1U, render_text->GetCursorPosition()); + render_text->MoveCursorRight(CHARACTER_BREAK, false); + EXPECT_EQ(2U, render_text->GetCursorPosition()); + render_text->MoveCursorRight(CHARACTER_BREAK, false); + EXPECT_EQ(3U, render_text->GetCursorPosition()); + render_text->MoveCursorRight(CHARACTER_BREAK, false); + EXPECT_EQ(5U, render_text->GetCursorPosition()); + render_text->MoveCursorRight(CHARACTER_BREAK, false); + EXPECT_EQ(4U, render_text->GetCursorPosition()); + render_text->MoveCursorLeft(CHARACTER_BREAK, true); + EXPECT_EQ(4U, render_text->GetSelectionStart()); + EXPECT_EQ(5U, render_text->GetCursorPosition()); + render_text->MoveCursorLeft(CHARACTER_BREAK, true); + EXPECT_EQ(4U, render_text->GetSelectionStart()); + EXPECT_EQ(6U, render_text->GetCursorPosition()); + render_text->MoveCursorRight(CHARACTER_BREAK, false); + EXPECT_EQ(4U, render_text->GetCursorPosition()); +} + } // namespace gfx diff --git a/ui/gfx/render_text_win.cc b/ui/gfx/render_text_win.cc index c2c9acf..f6b983d 100644 --- a/ui/gfx/render_text_win.cc +++ b/ui/gfx/render_text_win.cc @@ -237,10 +237,6 @@ SelectionModel RenderTextWin::RightEndSelectionModel() { return SelectionModel(cursor, caret, placement); } -size_t RenderTextWin::GetIndexOfPreviousGrapheme(size_t position) { - return IndexOfAdjacentGrapheme(position, false); -} - std::vector<Rect> RenderTextWin::GetSubstringBounds(size_t from, size_t to) { ui::Range range(from, to); DCHECK(ui::Range(0, text().length()).Contains(range)); @@ -295,6 +291,41 @@ std::vector<Rect> RenderTextWin::GetSubstringBounds(size_t from, size_t to) { return bounds; } +bool RenderTextWin::IsCursorablePosition(size_t position) { + if (position == 0 || position == text().length()) + return true; + + size_t run_index = GetRunContainingPosition(position); + if (run_index >= runs_.size()) + return false; + + internal::TextRun* run = runs_[run_index]; + size_t start = run->range.start(); + if (position == start) + return true; + return run->logical_clusters[position - start] != + run->logical_clusters[position - start - 1]; +} + +size_t RenderTextWin::IndexOfAdjacentGrapheme(size_t index, bool next) { + size_t run_index = GetRunContainingPosition(index); + internal::TextRun* run = run_index < runs_.size() ? runs_[run_index] : NULL; + long start = run ? run->range.start() : 0; + long length = run ? run->range.length() : text().length(); + long ch = index - start; + WORD cluster = run ? run->logical_clusters[ch] : 0; + + if (!next) { + do { + ch--; + } while (ch >= 0 && run && run->logical_clusters[ch] == cluster); + } else { + while (ch < length && run && run->logical_clusters[ch] == cluster) + ch++; + } + return std::max(static_cast<long>(std::min(ch, length) + start), 0L); +} + void RenderTextWin::ItemizeLogicalText() { text_is_dirty_ = false; STLDeleteContainerPointers(runs_.begin(), runs_.end()); @@ -461,34 +492,16 @@ size_t RenderTextWin::GetRunContainingPoint(const Point& point) const { return run; } -size_t RenderTextWin::IndexOfAdjacentGrapheme(size_t index, bool next) const { - size_t run_index = GetRunContainingPosition(index); - internal::TextRun* run = run_index < runs_.size() ? runs_[run_index] : NULL; - long start = run ? run->range.start() : 0; - long length = run ? run->range.length() : text().length(); - long ch = index - start; - WORD cluster = run ? run->logical_clusters[ch] : 0; - - if (!next) { - do { - ch--; - } while (ch >= 0 && run && run->logical_clusters[ch] == cluster); - } else { - while (ch < length && run && run->logical_clusters[ch] == cluster) - ch++; - } - return std::max(static_cast<long>(std::min(ch, length) + start), 0L); -} SelectionModel RenderTextWin::FirstSelectionModelInsideRun( - internal::TextRun* run) const { + internal::TextRun* run) { size_t caret = run->range.start(); size_t cursor = IndexOfAdjacentGrapheme(caret, true); return SelectionModel(cursor, caret, SelectionModel::TRAILING); } SelectionModel RenderTextWin::LastSelectionModelInsideRun( - internal::TextRun* run) const { + internal::TextRun* run) { size_t caret = IndexOfAdjacentGrapheme(run->range.end(), false); return SelectionModel(caret, caret, SelectionModel::LEADING); } diff --git a/ui/gfx/render_text_win.h b/ui/gfx/render_text_win.h index d812f50..53eea69 100644 --- a/ui/gfx/render_text_win.h +++ b/ui/gfx/render_text_win.h @@ -72,10 +72,12 @@ class RenderTextWin : public RenderText { BreakType break_type) OVERRIDE; virtual SelectionModel LeftEndSelectionModel() OVERRIDE; virtual SelectionModel RightEndSelectionModel() OVERRIDE; - virtual size_t GetIndexOfPreviousGrapheme(size_t position) OVERRIDE; virtual std::vector<Rect> GetSubstringBounds(size_t from, size_t to) OVERRIDE; + virtual bool IsCursorablePosition(size_t position) OVERRIDE; private: + virtual size_t IndexOfAdjacentGrapheme(size_t index, bool next) OVERRIDE; + void ItemizeLogicalText(); void LayoutVisualText(HDC hdc); @@ -84,15 +86,11 @@ class RenderTextWin : public RenderText { size_t GetRunContainingPosition(size_t position) const; size_t GetRunContainingPoint(const Point& point) const; - // Return an index belonging to the |next| or previous logical grapheme. - // The return value is bounded by 0 and the text length, inclusive. - size_t IndexOfAdjacentGrapheme(size_t index, bool next) const; - // Given a |run|, returns the SelectionModel that contains the logical first // or last caret position inside (not at a boundary of) the run. // The returned value represents a cursor/caret position without a selection. - SelectionModel FirstSelectionModelInsideRun(internal::TextRun*) const; - SelectionModel LastSelectionModelInsideRun(internal::TextRun*) const; + SelectionModel FirstSelectionModelInsideRun(internal::TextRun*); + SelectionModel LastSelectionModelInsideRun(internal::TextRun*); // Get the selection model visually left/right of |selection| by one grapheme. // The returned value represents a cursor/caret position without a selection. diff --git a/views/controls/textfield/native_textfield_views_unittest.cc b/views/controls/textfield/native_textfield_views_unittest.cc index ea8e023..f75c2b7 100644 --- a/views/controls/textfield/native_textfield_views_unittest.cc +++ b/views/controls/textfield/native_textfield_views_unittest.cc @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include <string> +#include <vector> + #include "base/auto_reset.h" #include "base/bind.h" #include "base/bind_helpers.h" @@ -15,6 +18,7 @@ #include "ui/base/clipboard/scoped_clipboard_writer.h" #include "ui/base/dragdrop/drag_drop_types.h" #include "ui/base/keycodes/keyboard_codes.h" +#include "ui/base/l10n/l10n_util.h" #include "ui/gfx/render_text.h" #include "views/controls/textfield/native_textfield_views.h" #include "views/controls/textfield/textfield.h" @@ -88,6 +92,8 @@ class GetTextHelper { DISALLOW_COPY_AND_ASSIGN(GetTextHelper); }; +const char16 kHebrewLetterSamekh = 0x05E1; + } // namespace namespace views { @@ -215,6 +221,19 @@ class NativeTextfieldViewsTest : public ViewsTestBase, SendKeyEvent(key_code, false, false); } + void SendKeyEvent(char16 ch) { + if (ch < 0x80) { + ui::KeyboardCode code = + ch == ' ' ? ui::VKEY_SPACE : + static_cast<ui::KeyboardCode>(ui::VKEY_A + ch - 'a'); + SendKeyEvent(code); + } else { + KeyEvent event(ui::ET_KEY_PRESSED, ui::VKEY_UNKNOWN, 0); + event.set_character(ch); + input_method_->DispatchKeyEvent(event); + } + } + View* GetFocusedView() { return widget_->GetFocusManager()->GetFocusedView(); } @@ -225,6 +244,45 @@ class NativeTextfieldViewsTest : public ViewsTestBase, gfx::SelectionModel(cursor_pos), false).x(); } + // Get the current cursor bounds. + gfx::Rect GetCursorBounds() { + gfx::RenderText* render_text = textfield_view_->GetRenderText(); + gfx::Rect bounds = render_text->GetUpdatedCursorBounds(); + return bounds; + } + + // Get the cursor bounds of |sel|. + gfx::Rect GetCursorBounds(const gfx::SelectionModel& sel) { + gfx::RenderText* render_text = textfield_view_->GetRenderText(); + gfx::Rect bounds = render_text->GetCursorBounds(sel, true); + return bounds; + } + + gfx::Rect GetDisplayRect() { + return textfield_view_->GetRenderText()->display_rect(); + } + + // Mouse click on the point whose x-axis is |bound|'s x plus |x_offset| and + // y-axis is in the middle of |bound|'s vertical range. + void MouseClick(const gfx::Rect bound, int x_offset) { + int x = bound.x() + x_offset; + int y = bound.y() + bound.height() / 2; + MouseEvent click(ui::ET_MOUSE_PRESSED, x, y, ui::EF_LEFT_BUTTON_DOWN); + textfield_view_->OnMousePressed(click); + MouseEvent release(ui::ET_MOUSE_RELEASED, x, y, ui::EF_LEFT_BUTTON_DOWN); + textfield_view_->OnMouseReleased(release); + } + + // This is to avoid double/triple click. + void NonClientMouseClick() { + MouseEvent click(ui::ET_MOUSE_PRESSED, 0, 0, + ui::EF_LEFT_BUTTON_DOWN | ui::EF_IS_NON_CLIENT); + textfield_view_->OnMousePressed(click); + MouseEvent release(ui::ET_MOUSE_RELEASED, 0, 0, + ui::EF_LEFT_BUTTON_DOWN | ui::EF_IS_NON_CLIENT); + textfield_view_->OnMouseReleased(release); + } + // Wrap for visibility in test classes. ui::TextInputType GetTextInputType() { return textfield_view_->GetTextInputType(); @@ -1141,4 +1199,340 @@ TEST_F(NativeTextfieldViewsTest, UndoRedoTest) { EXPECT_STR_EQ("ab3", textfield_->text()); } +TEST_F(NativeTextfieldViewsTest, TextCursorDisplayTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + // LTR-RTL string in LTR context. + SendKeyEvent('a'); + EXPECT_STR_EQ("a", textfield_->text()); + int x = GetCursorBounds().x(); + int prev_x = x; + + SendKeyEvent('b'); + EXPECT_STR_EQ("ab", textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_LT(prev_x, x); + prev_x = x; + + SendKeyEvent(0x05E1); + EXPECT_EQ(WideToUTF16(L"ab\x05E1"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_EQ(prev_x, x); + + SendKeyEvent(0x05E2); + EXPECT_EQ(WideToUTF16(L"ab\x05E1\x5E2"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_EQ(prev_x, x); + + // Clear text. + SendKeyEvent(ui::VKEY_A, false, true); + SendKeyEvent('\n'); + + // RTL-LTR string in LTR context. + SendKeyEvent(0x05E1); + EXPECT_EQ(WideToUTF16(L"\x05E1"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_EQ(GetDisplayRect().x(), x); + prev_x = x; + + SendKeyEvent(0x05E2); + EXPECT_EQ(WideToUTF16(L"\x05E1\x05E2"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_EQ(prev_x, x); + + SendKeyEvent('a'); + EXPECT_EQ(WideToUTF16(L"\x05E1\x5E2"L"a"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_LT(prev_x, x); + prev_x = x; + + SendKeyEvent('b'); + EXPECT_EQ(WideToUTF16(L"\x05E1\x5E2"L"ab"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_LT(prev_x, x); +} + +TEST_F(NativeTextfieldViewsTest, TextCursorDisplayInRTLTest) { + std::string locale = l10n_util::GetApplicationLocale(""); + base::i18n::SetICUDefaultLocale("he"); + + InitTextfield(Textfield::STYLE_DEFAULT); + // LTR-RTL string in RTL context. + SendKeyEvent('a'); + EXPECT_STR_EQ("a", textfield_->text()); + int x = GetCursorBounds().x(); + EXPECT_EQ(GetDisplayRect().right() - 1, x); + int prev_x = x; + + SendKeyEvent('b'); + EXPECT_STR_EQ("ab", textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_EQ(prev_x, x); + + SendKeyEvent(0x05E1); + EXPECT_EQ(WideToUTF16(L"ab\x05E1"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_GT(prev_x, x); + prev_x = x; + + SendKeyEvent(0x05E2); + EXPECT_EQ(WideToUTF16(L"ab\x05E1\x5E2"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_GT(prev_x, x); + + SendKeyEvent(ui::VKEY_A, false, true); + SendKeyEvent('\n'); + + // RTL-LTR string in RTL context. + SendKeyEvent(0x05E1); + EXPECT_EQ(WideToUTF16(L"\x05E1"), textfield_->text()); + x = GetCursorBounds().x(); + prev_x = x; + + SendKeyEvent(0x05E2); + EXPECT_EQ(WideToUTF16(L"\x05E1\x05E2"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_GT(prev_x, x); + prev_x = x; + + SendKeyEvent('a'); + EXPECT_EQ(WideToUTF16(L"\x05E1\x5E2"L"a"), textfield_->text()); + x = GetCursorBounds().x(); +#if defined(OS_WIN) + // In Windows, the text is always in LTR directionality even in RTL UI. + // TODO(xji): it should change if we fix the directionality in Window's + // NativeTextfieldViews + EXPECT_LT(prev_x, x); +#else + EXPECT_EQ(prev_x, x); +#endif + prev_x = x; + + SendKeyEvent('b'); + EXPECT_EQ(WideToUTF16(L"\x05E1\x5E2"L"ab"), textfield_->text()); + x = GetCursorBounds().x(); + EXPECT_EQ(prev_x, x); + + // Reset locale. + base::i18n::SetICUDefaultLocale(locale); +} + +TEST_F(NativeTextfieldViewsTest, HitInsideTextAreaTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + textfield_->SetText(WideToUTF16(L"ab\x05E1\x5E2")); + std::vector<gfx::Rect> cursor_bounds; + + // Save each cursor bound. + gfx::SelectionModel sel(0, 0, gfx::SelectionModel::LEADING); + cursor_bounds.push_back(GetCursorBounds(sel)); + + sel = gfx::SelectionModel(1, 0, gfx::SelectionModel::TRAILING); + gfx::Rect bound = GetCursorBounds(sel); + sel = gfx::SelectionModel(1, 1, gfx::SelectionModel::LEADING); + EXPECT_EQ(bound, GetCursorBounds(sel)); + cursor_bounds.push_back(bound); + + sel = gfx::SelectionModel(2, 1, gfx::SelectionModel::TRAILING); + bound = GetCursorBounds(sel); + sel = gfx::SelectionModel(4, 3, gfx::SelectionModel::TRAILING); + EXPECT_EQ(bound, GetCursorBounds(sel)); + cursor_bounds.push_back(bound); + + sel = gfx::SelectionModel(3, 2, gfx::SelectionModel::TRAILING); + bound = GetCursorBounds(sel); + sel = gfx::SelectionModel(3, 3, gfx::SelectionModel::LEADING); + EXPECT_EQ(bound, GetCursorBounds(sel)); + cursor_bounds.push_back(bound); + + sel = gfx::SelectionModel(2, 2, gfx::SelectionModel::LEADING); + bound = GetCursorBounds(sel); + sel = gfx::SelectionModel(4, 2, gfx::SelectionModel::LEADING); + EXPECT_EQ(bound, GetCursorBounds(sel)); + cursor_bounds.push_back(bound); + + // Expected cursor position when clicking left and right of each character. + size_t cursor_pos_expected[] = {0, 1, 1, 2, 4, 3, 3, 2}; + + int index = 0; + for (int i = 0; i < static_cast<int>(cursor_bounds.size() - 1); ++i) { + int half_width = (cursor_bounds[i + 1].x() - cursor_bounds[i].x()) / 2; + MouseClick(cursor_bounds[i], half_width / 2); + EXPECT_EQ(cursor_pos_expected[index++], textfield_->GetCursorPosition()); + + // To avoid trigger double click. Not using sleep() since it takes longer + // for the test to run if using sleep(). + NonClientMouseClick(); + + MouseClick(cursor_bounds[i + 1], - (half_width / 2)); + EXPECT_EQ(cursor_pos_expected[index++], textfield_->GetCursorPosition()); + + NonClientMouseClick(); + } +} + +TEST_F(NativeTextfieldViewsTest, HitOutsideTextAreaTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + + // LTR-RTL string in LTR context. + textfield_->SetText(WideToUTF16(L"ab\x05E1\x5E2")); + + SendKeyEvent(ui::VKEY_HOME); + gfx::Rect bound = GetCursorBounds(); + MouseClick(bound, -10); + EXPECT_EQ(bound, GetCursorBounds()); + + SendKeyEvent(ui::VKEY_END); + bound = GetCursorBounds(); + MouseClick(bound, 10); + EXPECT_EQ(bound, GetCursorBounds()); + + NonClientMouseClick(); + + // RTL-LTR string in LTR context. + textfield_->SetText(WideToUTF16(L"\x05E1\x5E2"L"ab")); + + SendKeyEvent(ui::VKEY_HOME); + bound = GetCursorBounds(); +#if defined(OS_WIN) + MouseClick(bound, -10); +#else + MouseClick(bound, 10); +#endif + EXPECT_EQ(bound, GetCursorBounds()); + + SendKeyEvent(ui::VKEY_END); + bound = GetCursorBounds(); +#if defined(OS_WIN) + MouseClick(bound, 10); +#else + MouseClick(bound, -10); +#endif + EXPECT_EQ(bound, GetCursorBounds()); +} + +TEST_F(NativeTextfieldViewsTest, HitOutsideTextAreaInRTLTest) { + std::string locale = l10n_util::GetApplicationLocale(""); + base::i18n::SetICUDefaultLocale("he"); + + InitTextfield(Textfield::STYLE_DEFAULT); + + // RTL-LTR string in RTL context. + textfield_->SetText(WideToUTF16(L"\x05E1\x5E2"L"ab")); + SendKeyEvent(ui::VKEY_HOME); + gfx::Rect bound = GetCursorBounds(); + MouseClick(bound, 10); + EXPECT_EQ(bound, GetCursorBounds()); + + SendKeyEvent(ui::VKEY_END); + bound = GetCursorBounds(); + MouseClick(bound, -10); + EXPECT_EQ(bound, GetCursorBounds()); + + NonClientMouseClick(); + + // LTR-RTL string in RTL context. + textfield_->SetText(WideToUTF16(L"ab\x05E1\x5E2")); + SendKeyEvent(ui::VKEY_HOME); + bound = GetCursorBounds(); +#if defined(OS_WIN) + MouseClick(bound, 10); +#else + MouseClick(bound, -10); +#endif + EXPECT_EQ(bound, GetCursorBounds()); + + SendKeyEvent(ui::VKEY_END); + bound = GetCursorBounds(); +#if defined(OS_WIN) + MouseClick(bound, -10); +#else + MouseClick(bound, 10); +#endif + EXPECT_EQ(bound, GetCursorBounds()); + + // Reset locale. + base::i18n::SetICUDefaultLocale(locale); +} + +// This verifies that |bound| is contained by |display|. |bound|'s right edge +// must be less than |diaplay|'s right edge. +void OverflowCursorBoundTestVerifier(const gfx::Rect& display, + const gfx::Rect& bound) { + EXPECT_LE(display.x(), bound.x()); + EXPECT_GT(display.right(), bound.right()); + EXPECT_LE(display.y(), bound.y()); + EXPECT_GE(display.bottom(), bound.bottom()); +} + +TEST_F(NativeTextfieldViewsTest, OverflowTest) { + InitTextfield(Textfield::STYLE_DEFAULT); + + string16 str; + for (int i = 0; i < 500; ++i) + SendKeyEvent('a'); + SendKeyEvent(kHebrewLetterSamekh); + gfx::Rect bound = GetCursorBounds(); + gfx::Rect display = GetDisplayRect(); + OverflowCursorBoundTestVerifier(display, bound); + + // Test mouse pointing. + MouseClick(bound, -1); + EXPECT_EQ(500U, textfield_->GetCursorPosition()); + + // Clear text. + SendKeyEvent(ui::VKEY_A, false, true); + SendKeyEvent('\n'); + + for (int i = 0; i < 500; ++i) + SendKeyEvent(kHebrewLetterSamekh); + SendKeyEvent('a'); + bound = GetCursorBounds(); + display = GetDisplayRect(); + OverflowCursorBoundTestVerifier(display, bound); + + MouseClick(bound, -1); + EXPECT_EQ(501U, textfield_->GetCursorPosition()); +} + +TEST_F(NativeTextfieldViewsTest, OverflowInRTLTest) { + std::string locale = l10n_util::GetApplicationLocale(""); + base::i18n::SetICUDefaultLocale("he"); + + InitTextfield(Textfield::STYLE_DEFAULT); + + string16 str; + for (int i = 0; i < 500; ++i) + SendKeyEvent('a'); + SendKeyEvent(kHebrewLetterSamekh); + gfx::Rect bound = GetCursorBounds(); + gfx::Rect display = GetDisplayRect(); + OverflowCursorBoundTestVerifier(display, bound); + + MouseClick(bound, 1); + EXPECT_EQ(501U, textfield_->GetCursorPosition()); + + // Clear text. + SendKeyEvent(ui::VKEY_A, false, true); + SendKeyEvent('\n'); + + for (int i = 0; i < 500; ++i) + SendKeyEvent(kHebrewLetterSamekh); + SendKeyEvent('a'); + bound = GetCursorBounds(); + display = GetDisplayRect(); + OverflowCursorBoundTestVerifier(display, bound); + + MouseClick(bound, 1); +#if defined(OS_WIN) + // In Windows, the text is always in LTR directionality even in RTL UI. + // TODO(xji): it should change if we fix the directionality in Window's + // NativeTextfieldViews + EXPECT_EQ(0U, textfield_->GetCursorPosition()); +#else + EXPECT_EQ(500U, textfield_->GetCursorPosition()); +#endif + + // Reset locale. + base::i18n::SetICUDefaultLocale(locale); +} + } // namespace views diff --git a/views/controls/textfield/textfield_views_model.cc b/views/controls/textfield/textfield_views_model.cc index ea1825d..dec5b2e 100644 --- a/views/controls/textfield/textfield_views_model.cc +++ b/views/controls/textfield/textfield_views_model.cc @@ -320,7 +320,10 @@ void TextfieldViewsModel::Append(const string16& text) { if (HasCompositionText()) ConfirmCompositionText(); size_t save = GetCursorPosition(); - MoveCursorRight(gfx::LINE_BREAK, false); + if (render_text_->GetTextDirection() == base::i18n::LEFT_TO_RIGHT) + MoveCursorRight(gfx::LINE_BREAK, false); + else + MoveCursorLeft(gfx::LINE_BREAK, false); InsertText(text); render_text_->SetCursorPosition(save); ClearSelection(); @@ -338,7 +341,9 @@ bool TextfieldViewsModel::Delete() { } if (GetText().length() > GetCursorPosition()) { size_t cursor_position = GetCursorPosition(); - ExecuteAndRecordDelete(cursor_position, cursor_position + 1, true); + size_t next_grapheme_index = + render_text_->GetIndexOfNextGrapheme(cursor_position); + ExecuteAndRecordDelete(cursor_position, next_grapheme_index, true); return true; } return false; @@ -656,7 +661,7 @@ void TextfieldViewsModel::ReplaceTextInternal(const string16& text, } else if (!HasSelection()) { size_t cursor = GetCursorPosition(); gfx::SelectionModel sel(render_text_->selection_model()); - sel.set_selection_start(cursor + text.length()); + sel.set_selection_start(render_text_->GetIndexOfNextGrapheme(cursor)); render_text_->MoveCursorTo(sel); } // Edit history is recorded in InsertText. diff --git a/views/controls/textfield/textfield_views_model_unittest.cc b/views/controls/textfield/textfield_views_model_unittest.cc index 5169231..e9b6218 100644 --- a/views/controls/textfield/textfield_views_model_unittest.cc +++ b/views/controls/textfield/textfield_views_model_unittest.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include <vector> + #include "base/auto_reset.h" #include "base/memory/scoped_ptr.h" #include "base/message_loop.h" @@ -17,6 +19,20 @@ #include "views/test/views_test_base.h" #include "views/views_delegate.h" +namespace { + +struct WordAndCursor { + WordAndCursor(const wchar_t* w, size_t c) + : word(w), + cursor(c) { + } + + const wchar_t* word; + size_t cursor; +}; + +} // namespace + namespace views { class TextfieldViewsModelTest : public ViewsTestBase, @@ -83,6 +99,144 @@ TEST_F(TextfieldViewsModelTest, EditString) { // but backspace should work. EXPECT_TRUE(model.Backspace()); EXPECT_STR_EQ("HELLORL", model.GetText()); + + model.MoveCursorTo(gfx::SelectionModel(5)); + model.ReplaceText(ASCIIToUTF16(" WOR")); + EXPECT_STR_EQ("HELLO WORL", model.GetText()); +} + +TEST_F(TextfieldViewsModelTest, EditString_SimpleRTL) { + TextfieldViewsModel model(NULL); + // Append two strings. + model.Append(WideToUTF16(L"\x05d0\x05d1\x05d2")); + EXPECT_EQ(WideToUTF16(L"\x05d0\x05d1\x05d2"), model.GetText()); + model.Append(WideToUTF16(L"\x05e0\x05e1\x05e2")); + EXPECT_EQ(WideToUTF16(L"\x05d0\x05d1\x05d2\x05e0\x05e1\x05e2"), + model.GetText()); + + // Insert 0x05f0. + model.MoveCursorTo(gfx::SelectionModel(1U)); + model.InsertChar(0x05f0); + EXPECT_EQ(WideToUTF16(L"\x05d0\x05f0\x05d1\x05d2\x05e0\x05e1\x05e2"), + model.GetText()); + + // Replace "\x05d1" with "\x05f1". + model.ReplaceChar(0x05f1); + EXPECT_EQ(WideToUTF16(L"\x05d0\x05f0\x5f1\x05d2\x05e0\x05e1\x05e2"), + model.GetText()); + + // Delete and backspace. + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_TRUE(model.Delete()); + EXPECT_EQ(WideToUTF16(L"\x05d0\x05f0\x5f1\x05e0\x05e1\x05e2"), + model.GetText()); + EXPECT_TRUE(model.Backspace()); + EXPECT_EQ(2U, model.GetCursorPosition()); + EXPECT_EQ(WideToUTF16(L"\x05d0\x05f0\x05e0\x05e1\x05e2"), model.GetText()); +} + +TEST_F(TextfieldViewsModelTest, EditString_ComplexScript) { + TextfieldViewsModel model(NULL); + // Append two Hindi strings. + model.Append(WideToUTF16(L"\x0915\x093f\x0915\x094d\x0915")); + EXPECT_EQ(WideToUTF16(L"\x0915\x093f\x0915\x094d\x0915"), + model.GetText()); + model.Append(WideToUTF16(L"\x0915\x094d\x092e\x094d")); + EXPECT_EQ(WideToUTF16( + L"\x0915\x093f\x0915\x094d\x0915\x0915\x094d\x092e\x094d"), + model.GetText()); + + // Check it is not able to place cursor in middle of a grapheme. + // TODO(xji): temporarily disable in platform Win since the complex script + // characters turned into empty square due to font regression. So, not able + // to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) + model.MoveCursorTo(gfx::SelectionModel(1U)); + EXPECT_EQ(0U, model.GetCursorPosition()); +#endif + + model.MoveCursorTo(gfx::SelectionModel(2U)); + EXPECT_EQ(2U, model.GetCursorPosition()); + model.InsertChar('a'); + EXPECT_EQ(WideToUTF16( + L"\x0915\x093f\x0061\x0915\x094d\x0915\x0915\x094d\x092e\x094d"), + model.GetText()); + + // ReplaceChar will replace the whole grapheme. + model.ReplaceChar('b'); + // TODO(xji): temporarily disable in platform Win since the complex script + // characters turned into empty square due to font regression. So, not able + // to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) + EXPECT_EQ(WideToUTF16( + L"\x0915\x093f\x0061\x0062\x0915\x0915\x094d\x092e\x094d"), + model.GetText()); +#endif + EXPECT_EQ(4U, model.GetCursorPosition()); + + // Delete should delete the whole grapheme. + model.MoveCursorTo(gfx::SelectionModel(0U)); + // TODO(xji): temporarily disable in platform Win since the complex script + // characters turned into empty square due to font regression. So, not able + // to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) + EXPECT_TRUE(model.Delete()); + EXPECT_EQ(WideToUTF16(L"\x0061\x0062\x0915\x0915\x094d\x092e\x094d"), + model.GetText()); + model.MoveCursorTo(gfx::SelectionModel(model.GetText().length())); + EXPECT_TRUE(model.Backspace()); + EXPECT_EQ(WideToUTF16(L"\x0061\x0062\x0915\x0915\x094d\x092e"), + model.GetText()); +#endif + + // Test cursor position and deletion for Hindi Virama. + model.SetText(WideToUTF16(L"\x0D38\x0D4D\x0D15\x0D16\x0D2E")); + model.MoveCursorTo(gfx::SelectionModel(0)); + EXPECT_EQ(0U, model.GetCursorPosition()); + + // TODO(xji): temporarily disable in platform Win since the complex script + // characters turned into empty square due to font regression. So, not able + // to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) + model.MoveCursorTo(gfx::SelectionModel(1)); + EXPECT_EQ(0U, model.GetCursorPosition()); +#endif + + model.MoveCursorTo(gfx::SelectionModel(2)); + EXPECT_EQ(2U, model.GetCursorPosition()); + + model.MoveCursorTo(gfx::SelectionModel(3)); + EXPECT_EQ(3U, model.GetCursorPosition()); + + model.MoveCursorTo(gfx::SelectionModel(2)); + + EXPECT_TRUE(model.Backspace()); + EXPECT_EQ(WideToUTF16(L"\x0D38\x0D15\x0D16\x0D2E"), model.GetText()); + + // Test Delete/Backspace on Hebrew with non-spacing marks. + // TODO(xji): temporarily disable in platform Win since the complex script + // characters turned into empty square due to font regression. So, not able + // to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) + model.SetText(WideToUTF16(L"\x05d5\x05b7\x05D9\x05B0\x05D4\x05B4\x05D9")); + model.MoveCursorTo(gfx::SelectionModel(0)); + EXPECT_TRUE(model.Delete()); + EXPECT_TRUE(model.Delete()); + EXPECT_TRUE(model.Delete()); + EXPECT_TRUE(model.Delete()); + EXPECT_EQ(WideToUTF16(L""), model.GetText()); +#endif + + // The first 2 characters are not strong directionality characters. + model.SetText(WideToUTF16(L"\x002C\x0020\x05D1\x05BC\x05B7\x05E9\x05BC")); +#if defined(OS_WIN) + model.MoveCursorRight(gfx::LINE_BREAK, false); +#else + model.MoveCursorLeft(gfx::LINE_BREAK, false); +#endif + EXPECT_TRUE(model.Backspace()); + EXPECT_EQ(WideToUTF16(L"\x002C\x0020\x05D1\x05BC\x05B7\x05E9"), + model.GetText()); } TEST_F(TextfieldViewsModelTest, EmptyString) { @@ -143,6 +297,93 @@ TEST_F(TextfieldViewsModelTest, Selection) { EXPECT_EQ(5U, model.GetCursorPosition()); } +TEST_F(TextfieldViewsModelTest, Selection_BidiWithNonSpacingMarks) { + // Selection is a logical operation. And it should work with the arrow + // keys doing visual movements, while the selection is logical between + // the (logical) start and end points. Selection is simply defined as + // the portion of text between the logical positions of the start and end + // caret positions. + TextfieldViewsModel model(NULL); + // TODO(xji): temporarily disable in platform Win since the complex script + // characters turned into empty square due to font regression. So, not able + // to test 2 characters belong to the same grapheme. +#if defined(OS_LINUX) + model.Append(WideToUTF16( + L"abc\x05E9\x05BC\x05C1\x05B8\x05E0\x05B8"L"def")); + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(2U, model.render_text()->GetSelectionStart()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_EQ(WideToUTF16(L"c"), model.GetSelectedText()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(2U, model.render_text()->GetSelectionStart()); + EXPECT_EQ(7U, model.GetCursorPosition()); + EXPECT_EQ(WideToUTF16(L"c\x05E9\x05BC\x05C1\x05B8"), + model.GetSelectedText()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(2U, model.render_text()->GetSelectionStart()); + EXPECT_EQ(3U, model.GetCursorPosition()); + EXPECT_EQ(WideToUTF16(L"c"), model.GetSelectedText()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(2U, model.render_text()->GetSelectionStart()); + EXPECT_EQ(10U, model.GetCursorPosition()); + EXPECT_EQ(WideToUTF16(L"c\x05E9\x05BC\x05C1\x05B8\x05E0\x05B8"L"d"), + model.GetSelectedText()); + + model.ClearSelection(); + EXPECT_EQ(string16(), model.GetSelectedText()); + model.SelectAll(); + EXPECT_EQ(WideToUTF16(L"abc\x05E9\x05BC\x05C1\x05B8\x05E0\x05B8"L"def"), + model.GetSelectedText()); +#endif + + // In case of "aBc", this test shows how to select "aB" or "Bc", assume 'B' is + // an RTL character. + model.SetText(WideToUTF16(L"a\x05E9"L"b")); + model.MoveCursorTo(gfx::SelectionModel(0)); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"a"), model.GetSelectedText()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"a"), model.GetSelectedText()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"a\x05E9"L"b"), model.GetSelectedText()); + + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + EXPECT_EQ(3U, model.GetCursorPosition()); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"b"), model.GetSelectedText()); + + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"b"), model.GetSelectedText()); + + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"a\x05E9"L"b"), model.GetSelectedText()); + + model.MoveCursorLeft(gfx::LINE_BREAK, false); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"a\x05E9"), model.GetSelectedText()); + + model.MoveCursorRight(gfx::LINE_BREAK, false); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + model.MoveCursorLeft(gfx::CHARACTER_BREAK, true); + model.MoveCursorRight(gfx::CHARACTER_BREAK, true); + EXPECT_EQ(WideToUTF16(L"\x05E9"L"b"), model.GetSelectedText()); + + model.ClearSelection(); + EXPECT_EQ(string16(), model.GetSelectedText()); + model.SelectAll(); + EXPECT_EQ(WideToUTF16(L"a\x05E9"L"b"), model.GetSelectedText()); +} + TEST_F(TextfieldViewsModelTest, SelectionAndEdit) { TextfieldViewsModel model(NULL); model.Append(ASCIIToUTF16("HELLO")); @@ -314,8 +555,8 @@ TEST_F(TextfieldViewsModelTest, Clipboard) { } void SelectWordTestVerifier(TextfieldViewsModel &model, - const std::string &expected_selected_string, size_t expected_cursor_pos) { - EXPECT_STR_EQ(expected_selected_string, model.GetSelectedText()); + const string16 &expected_selected_string, size_t expected_cursor_pos) { + EXPECT_EQ(expected_selected_string, model.GetSelectedText()); EXPECT_EQ(expected_cursor_pos, model.GetCursorPosition()); } @@ -326,39 +567,74 @@ TEST_F(TextfieldViewsModelTest, SelectWordTest) { // Test when cursor is at the beginning. model.MoveCursorLeft(gfx::LINE_BREAK, false); model.SelectWord(); - SelectWordTestVerifier(model, " ", 2U); + SelectWordTestVerifier(model, ASCIIToUTF16(" "), 2U); // Test when cursor is at the beginning of a word. gfx::SelectionModel selection(2U); model.MoveCursorTo(selection); model.SelectWord(); - SelectWordTestVerifier(model, "HELLO", 7U); + SelectWordTestVerifier(model, ASCIIToUTF16("HELLO"), 7U); // Test when cursor is at the end of a word. selection = gfx::SelectionModel(15U); model.MoveCursorTo(selection); model.SelectWord(); - SelectWordTestVerifier(model, "WO", 15U); + SelectWordTestVerifier(model, ASCIIToUTF16(" "), 20U); - // Test when cursor is somewhere in a non-alph-numeric fragment. + // Test when cursor is somewhere in a non-alpha-numeric fragment. for (size_t cursor_pos = 8; cursor_pos < 13U; cursor_pos++) { selection = gfx::SelectionModel(cursor_pos); model.MoveCursorTo(selection); model.SelectWord(); - SelectWordTestVerifier(model, " !! ", 13U); + SelectWordTestVerifier(model, ASCIIToUTF16(" !! "), 13U); } // Test when cursor is somewhere in a whitespace fragment. selection = gfx::SelectionModel(17U); model.MoveCursorTo(selection); model.SelectWord(); - SelectWordTestVerifier(model, " ", 20U); + SelectWordTestVerifier(model, ASCIIToUTF16(" "), 20U); // Test when cursor is at the end. model.MoveCursorRight(gfx::LINE_BREAK, false); model.SelectWord(); - SelectWordTestVerifier(model, " ", 24U); + SelectWordTestVerifier(model, ASCIIToUTF16(" "), 24U); +} + +// TODO(xji): temporarily disable in platform Win since the complex script +// characters and Chinese characters are turned into empty square due to font +// regression. +#if defined(OS_LINUX) +TEST_F(TextfieldViewsModelTest, SelectWordTest_MixScripts) { + TextfieldViewsModel model(NULL); + std::vector<WordAndCursor> word_and_cursor; + word_and_cursor.push_back(WordAndCursor(L"a\x05d0", 2)); + word_and_cursor.push_back(WordAndCursor(L"a\x05d0", 2)); + word_and_cursor.push_back(WordAndCursor(L"\x05d1\x05d2", 5)); + word_and_cursor.push_back(WordAndCursor(L"\x05d1\x05d2", 5)); + word_and_cursor.push_back(WordAndCursor(L" ", 3)); + word_and_cursor.push_back(WordAndCursor(L"a\x05d0", 2)); + word_and_cursor.push_back(WordAndCursor(L"\x0915\x094d\x0915", 9)); + word_and_cursor.push_back(WordAndCursor(L"\x0915\x094d\x0915", 9)); + word_and_cursor.push_back(WordAndCursor(L" ", 10)); + word_and_cursor.push_back(WordAndCursor(L"\x4E2D\x56FD", 12)); + word_and_cursor.push_back(WordAndCursor(L"\x4E2D\x56FD", 12)); + word_and_cursor.push_back(WordAndCursor(L"\x82B1", 13)); + word_and_cursor.push_back(WordAndCursor(L"\x5929", 14)); + + // The text consists of Ascii, Hebrew, Hindi with Virama sign, and Chinese. + model.SetText(WideToUTF16(L"a\x05d0 \x05d1\x05d2 \x0915\x094d\x0915 " + L"\x4E2D\x56FD\x82B1\x5929")); + for (size_t i = 0; i < word_and_cursor.size(); ++i) { + model.MoveCursorLeft(gfx::LINE_BREAK, false); + for (size_t j = 0; j < i; ++j) + model.MoveCursorRight(gfx::CHARACTER_BREAK, false); + model.SelectWord(); + SelectWordTestVerifier(model, WideToUTF16(word_and_cursor[i].word), + word_and_cursor[i].cursor); + } } +#endif TEST_F(TextfieldViewsModelTest, RangeTest) { TextfieldViewsModel model(NULL); |