diff options
author | xji@google.com <xji@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-09-21 20:32:29 +0000 |
---|---|---|
committer | xji@google.com <xji@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-09-21 20:32:29 +0000 |
commit | 53c0b1ba80612d92849dc02e2e7a1aa97df0e6a1 (patch) | |
tree | 28201e71c8163fc73b4b849925bdf87d51bf1636 /ui | |
parent | df09e1b4e119de37af4226a20cd745c209b75f70 (diff) | |
download | chromium_src-53c0b1ba80612d92849dc02e2e7a1aa97df0e6a1.zip chromium_src-53c0b1ba80612d92849dc02e2e7a1aa97df0e6a1.tar.gz chromium_src-53c0b1ba80612d92849dc02e2e7a1aa97df0e6a1.tar.bz2 |
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
Diffstat (limited to 'ui')
-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 |
7 files changed, 442 insertions, 114 deletions
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. |