From 53c0b1ba80612d92849dc02e2e7a1aa97df0e6a1 Mon Sep 17 00:00:00 2001
From: "xji@google.com" <xji@google.com@0039d316-1c4b-4281-b951-d872f2087c98>
Date: Wed, 21 Sep 2011 20:32:29 +0000
Subject: This is a reapply of
 http://src.chromium.org/viewvc/chrome?view=rev&revision=102006

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@102160 0039d316-1c4b-4281-b951-d872f2087c98
---
 ui/gfx/render_text.cc          |  69 ++++-----
 ui/gfx/render_text.h           |  19 ++-
 ui/gfx/render_text_linux.cc    |  38 +++--
 ui/gfx/render_text_linux.h     |  20 +--
 ui/gfx/render_text_unittest.cc | 337 +++++++++++++++++++++++++++++++++++++++--
 ui/gfx/render_text_win.cc      |  61 +++++---
 ui/gfx/render_text_win.h       |  12 +-
 7 files changed, 442 insertions(+), 114 deletions(-)

(limited to 'ui')

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..acf4fc1 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 < 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.
-- 
cgit v1.1