diff options
author | xji@google.com <xji@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-08-04 21:47:08 +0000 |
---|---|---|
committer | xji@google.com <xji@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-08-04 21:47:08 +0000 |
commit | 8e42ba22706ce8ca0fd374351f4221a26ebd5e0a (patch) | |
tree | f7b3e0d40d74fcee0ae6a7b88b4dc17689759d8c /ui/gfx | |
parent | 8316bdb602078971974041c5d62bf3afdf6cf2e5 (diff) | |
download | chromium_src-8e42ba22706ce8ca0fd374351f4221a26ebd5e0a.zip chromium_src-8e42ba22706ce8ca0fd374351f4221a26ebd5e0a.tar.gz chromium_src-8e42ba22706ce8ca0fd374351f4221a26ebd5e0a.tar.bz2 |
extend RenderText for inheritance. It
1. Moves temporary color definition to gfx, declares some function as virtual for override,
removes const from some functions so that the override function is able to modify local data.
2. Cache cursor bounds and compute it (along with display_offset_) when necessary.
3. Introduce SelectionModel (not derivable) for visual cursor positioning.
BUG=90426
TEST=--use-pure-views text editing
Review URL: http://codereview.chromium.org/7461102
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@95508 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/gfx')
-rw-r--r-- | ui/gfx/render_text.cc | 363 | ||||
-rw-r--r-- | ui/gfx/render_text.h | 184 |
2 files changed, 381 insertions, 166 deletions
diff --git a/ui/gfx/render_text.cc b/ui/gfx/render_text.cc index 1664b22..7e67d21 100644 --- a/ui/gfx/render_text.cc +++ b/ui/gfx/render_text.cc @@ -11,6 +11,7 @@ #include "base/stl_util.h" #include "ui/gfx/canvas.h" #include "ui/gfx/canvas_skia.h" +#include "unicode/uchar.h" namespace { @@ -38,26 +39,26 @@ void CheckStyleRanges(const gfx::StyleRanges& style_ranges, size_t length) { } #endif -void ApplyStyleRangeImpl(gfx::StyleRanges& style_ranges, +void ApplyStyleRangeImpl(gfx::StyleRanges* style_ranges, gfx::StyleRange style_range) { const ui::Range& new_range = style_range.range; // Follow StyleRanges invariant conditions: sorted and non-overlapping ranges. gfx::StyleRanges::iterator i; - for (i = style_ranges.begin(); i != style_ranges.end();) { + for (i = style_ranges->begin(); i != style_ranges->end();) { if (i->range.end() < new_range.start()) { i++; } else if (i->range.start() == new_range.end()) { break; } else if (new_range.Contains(i->range)) { - i = style_ranges.erase(i); - if (i == style_ranges.end()) + i = style_ranges->erase(i); + if (i == style_ranges->end()) break; } else if (i->range.start() < new_range.start() && i->range.end() > new_range.end()) { // Split the current style into two styles. gfx::StyleRange split_style = gfx::StyleRange(*i); split_style.range.set_end(new_range.start()); - i = style_ranges.insert(i, split_style) + 1; + i = style_ranges->insert(i, split_style) + 1; i->range.set_start(new_range.end()); break; } else if (i->range.start() < new_range.start()) { @@ -70,7 +71,7 @@ void ApplyStyleRangeImpl(gfx::StyleRanges& style_ranges, NOTREACHED(); } // Add the new range in its sorted location. - style_ranges.insert(i, style_range); + style_ranges->insert(i, style_range); } } // namespace @@ -85,6 +86,50 @@ StyleRange::StyleRange() range() { } +SelectionModel::SelectionModel() { + Init(0, 0, 0, PREVIOUS_GRAPHEME_TRAILING); +} + +SelectionModel::SelectionModel(size_t pos) { + Init(pos, pos, pos, PREVIOUS_GRAPHEME_TRAILING); +} + +SelectionModel::SelectionModel(size_t end, + size_t pos, + CaretPlacement placement) { + Init(end, end, pos, placement); +} + +SelectionModel::SelectionModel(size_t start, + size_t end, + size_t pos, + CaretPlacement placement) { + Init(start, end, pos, placement); +} + +SelectionModel::~SelectionModel() { +} + +bool SelectionModel::Equals(const SelectionModel& sel) const { + return selection_start_ == sel.selection_start() && + selection_end_ == sel.selection_end() && + caret_pos_ == sel.caret_pos() && + caret_placement_ == sel.caret_placement(); +} + +void SelectionModel::Init(size_t start, + size_t end, + size_t pos, + CaretPlacement placement) { + selection_start_ = start; + selection_end_ = end; + caret_pos_ = pos; + caret_placement_ = placement; +} + +RenderText::~RenderText() { +} + void RenderText::SetText(const string16& text) { size_t old_text_length = text_.length(); text_ = text; @@ -114,101 +159,107 @@ void RenderText::SetText(const string16& text) { #endif } +void RenderText::SetSelectionModel(const SelectionModel& sel) { + size_t start = sel.selection_start(); + size_t end = sel.selection_end(); + selection_model_.set_selection_start(std::min(start, text().length())); + selection_model_.set_selection_end(std::min(end, text().length())); + selection_model_.set_caret_pos(std::min(sel.caret_pos(), text().length())); + selection_model_.set_caret_placement(sel.caret_placement()); + + cursor_bounds_valid_ = false; +} + size_t RenderText::GetCursorPosition() const { - return GetSelection().end(); + return selection_model_.selection_end(); } void RenderText::SetCursorPosition(const size_t position) { - SetSelection(ui::Range(position, position)); + SelectionModel sel(selection_model()); + sel.set_selection_start(position); + sel.set_selection_end(position); + SetSelectionModel(sel); } void RenderText::MoveCursorLeft(BreakType break_type, bool select) { if (break_type == LINE_BREAK) { - MoveCursorTo(0, select); + SelectionModel selection(GetSelectionStart(), 0, + 0, SelectionModel::LEADING); + if (!select) + selection.set_selection_start(selection.selection_end()); + MoveCursorTo(selection); return; } - size_t position = GetCursorPosition(); + SelectionModel position = selection_model_; // Cancelling a selection moves to the edge of the selection. - if (!GetSelection().is_empty() && !select) { + if (!EmptySelection() && !select) { // Use the selection start if it is left of the selection end. - if (GetCursorBounds(GetSelection().start(), false).x() < + SelectionModel selection_start(GetSelectionStart(), GetSelectionStart(), + GetSelectionStart(), SelectionModel::LEADING); + if (GetCursorBounds(selection_start, false).x() < GetCursorBounds(position, false).x()) - position = GetSelection().start(); + position = selection_start; // If |move_by_word|, use the nearest word boundary left of the selection. if (break_type == WORD_BREAK) position = GetLeftCursorPosition(position, true); } else { position = GetLeftCursorPosition(position, break_type == WORD_BREAK); } - MoveCursorTo(position, select); + if (!select) + position.set_selection_start(position.selection_end()); + MoveCursorTo(position); } void RenderText::MoveCursorRight(BreakType break_type, bool select) { if (break_type == LINE_BREAK) { - MoveCursorTo(text().length(), select); + SelectionModel selection(GetSelectionStart(), text().length(), + text().length(), SelectionModel::PREVIOUS_GRAPHEME_TRAILING); + if (!select) + selection.set_selection_start(selection.selection_end()); + MoveCursorTo(selection); return; } - size_t position = GetCursorPosition(); + SelectionModel position = selection_model_; // Cancelling a selection moves to the edge of the selection. - if (!GetSelection().is_empty() && !select) { + if (!EmptySelection() && !select) { // Use the selection start if it is right of the selection end. - if (GetCursorBounds(GetSelection().start(), false).x() > + SelectionModel selection_start(GetSelectionStart(), GetSelectionStart(), + GetSelectionStart(), SelectionModel::LEADING); + if (GetCursorBounds(selection_start, false).x() > GetCursorBounds(position, false).x()) - position = GetSelection().start(); + position = selection_start; // If |move_by_word|, use the nearest word boundary right of the selection. if (break_type == WORD_BREAK) position = GetRightCursorPosition(position, true); } else { position = GetRightCursorPosition(position, break_type == WORD_BREAK); } - MoveCursorTo(position, select); + if (!select) + position.set_selection_start(position.selection_end()); + MoveCursorTo(position); } -bool RenderText::MoveCursorTo(size_t position, bool select) { - bool changed = GetCursorPosition() != position || - select == GetSelection().is_empty(); - if (select) - SetSelection(ui::Range(GetSelection().start(), position)); - else - SetSelection(ui::Range(position, position)); +bool RenderText::MoveCursorTo(const SelectionModel& selection) { + bool changed = !selection.Equals(selection_model_); + SetSelectionModel(selection); return changed; } bool RenderText::MoveCursorTo(const Point& point, bool select) { - // TODO(msw): Make this function support cursor placement via mouse near BiDi - // level changes. The visual cursor appearance will depend on the location - // clicked, not solely the resulting logical cursor position. See the TODO - // note pertaining to selection_range_ for more information. - return MoveCursorTo(FindCursorPosition(point), select); -} - -const ui::Range& RenderText::GetSelection() const { - return selection_range_; -} - -void RenderText::SetSelection(const ui::Range& range) { - selection_range_.set_end(std::min(range.end(), text().length())); - selection_range_.set_start(std::min(range.start(), text().length())); - - // Update |display_offset_| to ensure the current cursor is visible. - Rect cursor_bounds(GetCursorBounds(GetCursorPosition(), insert_mode())); - int display_width = display_rect_.width(); - int string_width = GetStringWidth(); - if (string_width < display_width) { - // Show all text whenever the text fits to the size. - display_offset_.set_x(0); - } else if ((display_offset_.x() + cursor_bounds.right()) > display_width) { - // Pan to show the cursor when it overflows to the right, - display_offset_.set_x(display_width - cursor_bounds.right()); - } else if ((display_offset_.x() + cursor_bounds.x()) < 0) { - // Pan to show the cursor when it overflows to the left. - display_offset_.set_x(-cursor_bounds.x()); - } + SelectionModel selection = FindCursorPosition(point); + if (select) + selection.set_selection_start(GetSelectionStart()); + else + selection.set_selection_start(selection.selection_end()); + return MoveCursorTo(selection); } -bool RenderText::IsPointInSelection(const Point& point) const { - size_t pos = FindCursorPosition(point); - return (pos >= GetSelection().GetMin() && pos < GetSelection().GetMax()); +bool RenderText::IsPointInSelection(const Point& point) { + // TODO(xji): should this check whether the point is inside the visual + // selection bounds? In case of "abcFED", if "ED" is selected, |point| points + // to the right half of 'c', is the point in selection? + size_t pos = FindCursorPosition(point).selection_end(); + return (pos >= MinOfSelection() && pos < MaxOfSelection()); } void RenderText::ClearSelection() { @@ -216,17 +267,19 @@ void RenderText::ClearSelection() { } void RenderText::SelectAll() { - SetSelection(ui::Range(0, text().length())); + SelectionModel sel(0, text().length(), + text().length(), SelectionModel::LEADING); + SetSelectionModel(sel); } void RenderText::SelectWord() { - size_t selection_start = GetSelection().start(); + size_t selection_start = GetSelectionStart(); size_t cursor_position = GetCursorPosition(); - // First we setup selection_start_ and cursor_pos_. There are so many cases + // 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 (isalnum(text()[cursor_position])) { + if (u_isalnum(text()[cursor_position])) { selection_start = cursor_position; cursor_position++; } else @@ -247,7 +300,7 @@ void RenderText::SelectWord() { break; } - // Now we move cursor_pos_ to end of selection. Selection boundary + // 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++) { @@ -255,7 +308,11 @@ void RenderText::SelectWord() { break; } - SetSelection(ui::Range(selection_start, cursor_position)); + SelectionModel sel(selection_model()); + sel.set_selection_start(selection_start); + sel.set_selection_end(cursor_position); + sel.set_caret_placement(SelectionModel::PREVIOUS_GRAPHEME_TRAILING); + SetSelectionModel(sel); } const ui::Range& RenderText::GetCompositionRange() const { @@ -275,10 +332,12 @@ void RenderText::ApplyStyleRange(StyleRange style_range) { return; CHECK(!new_range.is_reversed()); CHECK(ui::Range(0, text_.length()).Contains(new_range)); - ApplyStyleRangeImpl(style_ranges_, style_range); + ApplyStyleRangeImpl(&style_ranges_, style_range); #ifndef NDEBUG CheckStyleRanges(style_ranges_, text_.length()); #endif + // TODO(xji): only invalidate cursor_bounds if font or underline change. + cursor_bounds_valid_ = false; } void RenderText::ApplyDefaultStyle() { @@ -286,6 +345,7 @@ void RenderText::ApplyDefaultStyle() { StyleRange style = StyleRange(default_style_); style.range.set_end(text_.length()); style_ranges_.push_back(style); + cursor_bounds_valid_ = false; } base::i18n::TextDirection RenderText::GetTextDirection() const { @@ -294,8 +354,8 @@ base::i18n::TextDirection RenderText::GetTextDirection() const { return base::i18n::LEFT_TO_RIGHT; } -int RenderText::GetStringWidth() const { - return GetSubstringBounds(ui::Range(0, text_.length()))[0].width(); +int RenderText::GetStringWidth() { + return GetSubstringBounds(0, text_.length())[0].width(); } void RenderText::Draw(Canvas* canvas) { @@ -304,7 +364,8 @@ void RenderText::Draw(Canvas* canvas) { display_rect_.width(), display_rect_.height()); // Draw the selection. - std::vector<Rect> selection(GetSubstringBounds(GetSelection())); + std::vector<Rect> selection(GetSubstringBounds(GetSelectionStart(), + GetCursorPosition())); SkColor selection_color = focused() ? kFocusedSelectionColor : kUnfocusedSelectionColor; for (std::vector<Rect>::const_iterator i = selection.begin(); @@ -318,25 +379,8 @@ void RenderText::Draw(Canvas* canvas) { } // Create a temporary copy of the style ranges for composition and selection. - // TODO(msw): This pattern ought to be reconsidered; what about composition - // and selection overlaps, retain existing local style features? StyleRanges style_ranges(style_ranges_); - // Apply a composition style override to a copy of the style ranges. - if (composition_range_.IsValid() && !composition_range_.is_empty()) { - StyleRange composition_style(default_style_); - composition_style.underline = true; - composition_style.range.set_start(composition_range_.start()); - composition_style.range.set_end(composition_range_.end()); - ApplyStyleRangeImpl(style_ranges, composition_style); - } - // Apply a selection style override to a copy of the style ranges. - if (selection_range_.IsValid() && !selection_range_.is_empty()) { - StyleRange selection_style(default_style_); - selection_style.foreground = kSelectedTextColor; - selection_style.range.set_start(selection_range_.GetMin()); - selection_style.range.set_end(selection_range_.GetMax()); - ApplyStyleRangeImpl(style_ranges, selection_style); - } + ApplyCompositionAndSelectionStyles(&style_ranges); // Draw the text. Rect bounds(display_rect_); @@ -368,7 +412,7 @@ void RenderText::Draw(Canvas* canvas) { // Paint cursor. Replace cursor is drawn as rectangle for now. if (cursor_visible() && focused()) { - bounds = GetCursorBounds(GetCursorPosition(), insert_mode()); + bounds = CursorBounds(); bounds.Offset(display_offset_); if (!bounds.IsEmpty()) canvas->DrawRectInt(kCursorColor, @@ -379,7 +423,7 @@ void RenderText::Draw(Canvas* canvas) { } } -size_t RenderText::FindCursorPosition(const Point& point) const { +SelectionModel RenderText::FindCursorPosition(const Point& point) { const Font& font = default_style_.font; int left = 0; int left_pos = 0; @@ -387,31 +431,31 @@ size_t RenderText::FindCursorPosition(const Point& point) const { int right_pos = text().length(); int x = point.x(); - if (x <= left) return left_pos; - if (x >= right) return right_pos; + if (x <= left) return SelectionModel(left_pos); + if (x >= right) return SelectionModel(right_pos); // binary searching the cursor position. // TODO(oshima): use the center of character instead of edge. // Binary search may not work for language like arabic. - while (std::abs(static_cast<long>(right_pos - left_pos) > 1)) { + while (std::abs(static_cast<long>(right_pos - left_pos)) > 1) { int pivot_pos = left_pos + (right_pos - left_pos) / 2; int pivot = font.GetStringWidth(text().substr(0, pivot_pos)); if (pivot < x) { left = pivot; left_pos = pivot_pos; } else if (pivot == x) { - return pivot_pos; + return SelectionModel(pivot_pos); } else { right = pivot; right_pos = pivot_pos; } } - return left_pos; + return SelectionModel(left_pos); } std::vector<Rect> RenderText::GetSubstringBounds( - const ui::Range& range) const { - size_t start = range.GetMin(); - size_t end = range.GetMax(); + size_t from, size_t to) const { + size_t start = std::min(from, to); + size_t end = std::max(from, to); const Font& font = default_style_.font; int start_x = font.GetStringWidth(text().substr(0, start)); int end_x = font.GetStringWidth(text().substr(0, end)); @@ -420,7 +464,9 @@ std::vector<Rect> RenderText::GetSubstringBounds( return bounds; } -Rect RenderText::GetCursorBounds(size_t cursor_pos, bool insert_mode) const { +Rect RenderText::GetCursorBounds(const SelectionModel& selection, + bool insert_mode) { + size_t cursor_pos = selection.selection_end(); const Font& font = default_style_.font; int x = font.GetStringWidth(text_.substr(0U, cursor_pos)); DCHECK_GE(x, 0); @@ -431,10 +477,37 @@ Rect RenderText::GetCursorBounds(size_t cursor_pos, bool insert_mode) const { return bounds; } -size_t RenderText::GetLeftCursorPosition(size_t position, - bool move_by_word) const { - if (!move_by_word) - return position == 0? position : position - 1; +const Rect& RenderText::CursorBounds() { + if (cursor_bounds_valid_ == false) { + UpdateCursorBoundsAndDisplayOffset(); + cursor_bounds_valid_ = true; + } + return cursor_bounds_; +} + +RenderText::RenderText() + : text_(), + selection_model_(), + cursor_bounds_(), + cursor_bounds_valid_(false), + cursor_visible_(false), + insert_mode_(true), + composition_range_(), + style_ranges_(), + default_style_(), + display_rect_(), + display_offset_() { +} + +SelectionModel RenderText::GetLeftCursorPosition(const SelectionModel& current, + bool move_by_word) { + size_t position = current.selection_end(); + SelectionModel left = current; + if (!move_by_word) { + left.set_selection_end(std::max(static_cast<long>(position - 1), + static_cast<long>(0))); + return left; + } // Notes: We always iterate words from the begining. // This is probably fast enough for our usage, but we may // want to modify WordIterator so that it can start from the @@ -442,8 +515,10 @@ size_t RenderText::GetLeftCursorPosition(size_t position, base::i18n::BreakIterator iter(text(), base::i18n::BreakIterator::BREAK_WORD); bool success = iter.Init(); DCHECK(success); - if (!success) - return position; + if (!success) { + left.set_selection_end(position); + return left; + } int last = 0; while (iter.Advance()) { if (iter.IsWord()) { @@ -452,7 +527,7 @@ size_t RenderText::GetLeftCursorPosition(size_t position, // The cursor is at the beginning of a word. // Move to previous word. break; - } else if(iter.pos() >= position) { + } else if (iter.pos() >= position) { // The cursor is in the middle or at the end of a word. // Move to the top of current word. last = begin; @@ -463,18 +538,27 @@ size_t RenderText::GetLeftCursorPosition(size_t position, } } - return last; + left.set_selection_end(last); + return left; } -size_t RenderText::GetRightCursorPosition(size_t position, - bool move_by_word) const { - if (!move_by_word) - return std::min(position + 1, text().length()); +SelectionModel RenderText::GetRightCursorPosition(const SelectionModel& current, + bool move_by_word) { + size_t position = current.selection_end(); + SelectionModel right = current; + + if (!move_by_word) { + right.set_selection_end(std::min(position + 1, text().length())); + return right; + } + base::i18n::BreakIterator iter(text(), base::i18n::BreakIterator::BREAK_WORD); bool success = iter.Init(); DCHECK(success); - if (!success) - return position; + if (!success) { + right.set_selection_end(position); + return right; + } size_t pos = 0; while (iter.Advance()) { pos = iter.pos(); @@ -482,27 +566,52 @@ size_t RenderText::GetRightCursorPosition(size_t position, break; } } - return pos; + right.set_selection_end(pos); + return right; } -RenderText::RenderText() - : text_(), - selection_range_(), - cursor_visible_(false), - insert_mode_(true), - composition_range_(), - style_ranges_(), - default_style_(), - display_rect_(), - display_offset_() { +void RenderText::ApplyCompositionAndSelectionStyles( + StyleRanges* style_ranges) const { + // TODO(msw): This pattern ought to be reconsidered; what about composition + // and selection overlaps, retain existing local style features? + // Apply a composition style override to a copy of the style ranges. + if (composition_range_.IsValid() && !composition_range_.is_empty()) { + StyleRange composition_style(default_style_); + composition_style.underline = true; + composition_style.range.set_start(composition_range_.start()); + composition_style.range.set_end(composition_range_.end()); + ApplyStyleRangeImpl(style_ranges, composition_style); + } + // Apply a selection style override to a copy of the style ranges. + if (!EmptySelection()) { + StyleRange selection_style(default_style_); + selection_style.foreground = kSelectedTextColor; + selection_style.range.set_start(MinOfSelection()); + selection_style.range.set_end(MaxOfSelection()); + ApplyStyleRangeImpl(style_ranges, selection_style); + } } -RenderText::~RenderText() { +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])); } -bool RenderText::IsPositionAtWordSelectionBoundary(size_t pos) { - return pos == 0 || (isalnum(text()[pos - 1]) && !isalnum(text()[pos])) || - (!isalnum(text()[pos - 1]) && isalnum(text()[pos])); +void RenderText::UpdateCursorBoundsAndDisplayOffset() { + cursor_bounds_ = GetCursorBounds(selection_model_, insert_mode()); + // Update |display_offset_| to ensure the current cursor is visible. + int display_width = display_rect_.width(); + int string_width = GetStringWidth(); + if (string_width < display_width) { + // Show all text whenever the text fits to the size. + display_offset_.set_x(0); + } else if ((display_offset_.x() + cursor_bounds_.right()) > display_width) { + // Pan to show the cursor when it overflows to the right, + display_offset_.set_x(display_width - cursor_bounds_.right()); + } else if ((display_offset_.x() + cursor_bounds_.x()) < 0) { + // Pan to show the cursor when it overflows to the left. + display_offset_.set_x(-cursor_bounds_.x()); + } } } // namespace gfx diff --git a/ui/gfx/render_text.h b/ui/gfx/render_text.h index 40209ea..020386b 100644 --- a/ui/gfx/render_text.h +++ b/ui/gfx/render_text.h @@ -6,6 +6,7 @@ #define UI_GFX_RENDER_TEXT_H_ #pragma once +#include <algorithm> #include <vector> #include "base/gtest_prod_util.h" @@ -17,7 +18,7 @@ #include "ui/gfx/rect.h" #include "ui/gfx/point.h" -namespace { +namespace gfx { // Strike line width. const int kStrikeWidth = 2; @@ -32,10 +33,6 @@ const SkColor kFocusedSelectionColor = SK_ColorCYAN; const SkColor kUnfocusedSelectionColor = SK_ColorLTGRAY; const SkColor kCursorColor = SK_ColorBLACK; -} // namespace - -namespace gfx { - class Canvas; class RenderTextTest; @@ -59,6 +56,86 @@ enum BreakType { LINE_BREAK, }; +// TODO(xji): publish bidi-editing guide line and replace the place holder. +// SelectionModel is used to represent the logical selection and visual +// position of cursor. +// +// For bi-directional text, the mapping between visual position and logical +// position is not one-to-one. For example, logical text "abcDEF" where capital +// letters stand for Hebrew, the visual display is "abcFED". According to the +// bidi editing guide (http://bidi-editing-guideline): +// 1. If pointing to the right half of the cell of a LTR character, the current +// position must be set after this character and the caret must be displayed +// after this character. +// 2. If pointing to the right half of the cell of a RTL character, the current +// position must be set before this character and the caret must be displayed +// before this character. +// +// Pointing to the right half of 'c' and pointing to the right half of 'D' both +// set the logical cursor position to 3. But the cursor displayed visually at +// different places: +// Pointing to the right half of 'c' displays the cursor right of 'c' as +// "abc|FED". +// Pointing to the right half of 'D' displays the cursor right of 'D' as +// "abcFED|". +// So, besides the logical selection start point and end point, we need extra +// information to specify to which character and on which edge of the character +// the visual cursor is bound to. For example, the visual cursor is bound to +// the trailing side of the 2nd character 'c' when pointing to right half of +// 'c'. And it is bound to the leading edge of the 3rd character 'D' when +// pointing to right of 'D'. +class UI_API SelectionModel { + public: + enum CaretPlacement { + // PREVIOUS_GRAPHEME_TRAILING means cursor is visually attached to the + // trailing edge of previous grapheme. + PREVIOUS_GRAPHEME_TRAILING, + LEADING, + TRAILING, + }; + + SelectionModel(); + explicit SelectionModel(size_t pos); + SelectionModel(size_t end, size_t pos, CaretPlacement status); + SelectionModel(size_t start, size_t end, size_t pos, CaretPlacement status); + + virtual ~SelectionModel(); + + size_t selection_start() const { return selection_start_; } + void set_selection_start(size_t pos) { selection_start_ = pos; } + + size_t selection_end() const { return selection_end_; } + void set_selection_end(size_t pos) { selection_end_ = pos; } + + size_t caret_pos() const { return caret_pos_; } + void set_caret_pos(size_t pos) { caret_pos_ = pos; } + + CaretPlacement caret_placement() const { return caret_placement_; } + void set_caret_placement(CaretPlacement placement) { + caret_placement_ = placement; + } + + bool Equals(const SelectionModel& sel) const; + + private: + void Init(size_t start, size_t end, size_t pos, CaretPlacement status); + + // Logical selection start. If there is non-empty selection, the selection + // always starts visually at the leading edge of the selection_start. So, we + // do not need extra information for visual selection bounding. + size_t selection_start_; + + // The logical cursor position that next character will be inserted into. + // It is also the end of the selection. + size_t selection_end_; + + // The following two fields are used to guide cursor visual position. + // The index of the character that cursor is visually attached to. + size_t caret_pos_; + // The visual placement of the cursor, relative to its associated character. + CaretPlacement caret_placement_; +}; + // TODO(msw): Implement RenderText[Win|Linux] for Uniscribe/Pango BiDi... // RenderText represents an abstract model of styled text and its corresponding @@ -66,7 +143,6 @@ enum BreakType { // complex scripts, and bi-directional text. Implementations provide mechanisms // for rendering and translation between logical and visual data. class UI_API RenderText { - public: virtual ~RenderText(); @@ -76,6 +152,9 @@ class UI_API RenderText { const string16& text() const { return text_; } virtual void SetText(const string16& text); + const SelectionModel& selection_model() const { return selection_model_; } + void SetSelectionModel(const SelectionModel& sel); + bool cursor_visible() const { return cursor_visible_; } void set_cursor_visible(bool visible) { cursor_visible_ = visible; } @@ -89,13 +168,21 @@ class UI_API RenderText { void set_default_style(StyleRange style) { default_style_ = style; } const Rect& display_rect() const { return display_rect_; } - void set_display_rect(const Rect& r) { display_rect_ = r; } + virtual void set_display_rect(const Rect& r) { display_rect_ = r; } - const gfx::Point& display_offset() const { return display_offset_; } + const Point& display_offset() const { return display_offset_; } + // This cursor position corresponds to SelectionModel::selection_end. In + // addition to representing the selection end, it's also where logical text + // edits take place, and doesn't necessarily correspond to + // SelectionModel::caret_pos. size_t GetCursorPosition() const; void SetCursorPosition(const size_t position); + void SetCaretPlacement(SelectionModel::CaretPlacement placement) { + selection_model_.set_caret_placement(placement); + } + // Moves the cursor left or right. Cursor movement is visual, meaning that // left and right are relative to screen, not the directionality of the text. // If |select| is false, the selection range is emptied at the new position. @@ -105,20 +192,29 @@ class UI_API RenderText { void MoveCursorLeft(BreakType break_type, bool select); void MoveCursorRight(BreakType break_type, bool select); - // Moves the cursor to the specified logical |position|. - // If |select| is false, the selection range is emptied at the new position. + // Set the selection_model_ to the value of |selection|. // Returns true if the cursor position or selection range changed. - bool MoveCursorTo(size_t position, bool select); + bool MoveCursorTo(const SelectionModel& selection); // Move the cursor to the position associated with the clicked point. // If |select| is false, the selection range is emptied at the new position. bool MoveCursorTo(const Point& point, bool select); - const ui::Range& GetSelection() const; - void SetSelection(const ui::Range& range); + size_t GetSelectionStart() const { + return selection_model_.selection_start(); + } + size_t MinOfSelection() const { + return std::min(GetSelectionStart(), GetCursorPosition()); + } + size_t MaxOfSelection() const { + return std::max(GetSelectionStart(), GetCursorPosition()); + } + bool EmptySelection() const { + return GetSelectionStart() == GetCursorPosition(); + } // Returns true if the local point is over selected text. - bool IsPointInSelection(const Point& point) const; + bool IsPointInSelection(const Point& point); // Selects no text, all text, or the word at the current cursor position. void ClearSelection(); @@ -137,37 +233,46 @@ class UI_API RenderText { base::i18n::TextDirection GetTextDirection() const; // Get the width of the entire string. - int GetStringWidth() const; + virtual int GetStringWidth(); virtual void Draw(Canvas* canvas); - // TODO(msw): Deprecate this function. Logical and visual cursors are not - // mapped one-to-one. See the selection_range_ TODO for more information. - // Get the logical cursor position from a visual point in local coordinates. - virtual size_t FindCursorPosition(const Point& point) const; + // Gets the SelectionModel from a visual point in local coordinates. + virtual SelectionModel FindCursorPosition(const Point& point); - // Get the visual bounds containing the logical substring within |range|. - // These bounds could be visually discontiguous if the logical selection range - // is split by an odd number of LTR/RTL level change. + // Get the visual bounds containing the logical substring within |from| to + // |to|. These bounds could be visually discontinuous if the logical + // selection range is split by an odd number of LTR/RTL level change. virtual std::vector<Rect> GetSubstringBounds( - const ui::Range& range) const; + size_t from, size_t to) const; - // Get the visual bounds describing the cursor at |position|. These bounds + // Get the visual bounds describing the cursor at |selection|. These bounds // typically represent a vertical line, but if |insert_mode| is true they // contain the bounds of the associated glyph. - virtual Rect GetCursorBounds(size_t position, bool insert_mode) const; + virtual Rect GetCursorBounds(const SelectionModel& selection, + bool insert_mode); + + // Compute cursor_bounds_ and update display_offset_ when necessary. Cache + // the values for later use and return cursor_bounds_. + const Rect& CursorBounds(); protected: RenderText(); + void set_cursor_bounds_valid(bool valid) { cursor_bounds_valid_ = valid; } + const StyleRanges& style_ranges() const { return style_ranges_; } // Get the cursor position that visually neighbors |position|. // If |move_by_word| is true, return the neighboring word delimiter position. - virtual size_t GetLeftCursorPosition(size_t position, - bool move_by_word) const; - virtual size_t GetRightCursorPosition(size_t position, - bool move_by_word) const; + virtual SelectionModel GetLeftCursorPosition(const SelectionModel& current, + bool move_by_word); + virtual SelectionModel GetRightCursorPosition(const SelectionModel& current, + bool move_by_word); + + // Apply composition style (underline) to composition range and selection + // style (foreground) to selection range. + void ApplyCompositionAndSelectionStyles(StyleRanges* style_ranges) const; private: friend class RenderTextTest; @@ -182,19 +287,20 @@ class UI_API RenderText { bool IsPositionAtWordSelectionBoundary(size_t pos); + void UpdateCursorBoundsAndDisplayOffset(); + // Logical UTF-16 string data to be drawn. string16 text_; - // TODO(msw): A single logical cursor position doesn't support two potential - // visual cursor positions. For example, clicking right of 'c' & 'D' yeilds: - // (visually: 'abc|FEDghi' and 'abcFED|ghi', both logically: 'abc|DEFghi'). - // Similarly, one visual position may have two associated logical positions. - // For example, clicking the right side of 'D' and left side of 'g' yields: - // (both visually: 'abcFED|ghi', logically: 'abc|DEFghi' and 'abcDEF|ghi'). - // Update the cursor model with a leading/trailing flag, a level association, - // or a disjoint visual position to satisfy the proposed visual behavior. - // Logical selection range; the range end is also the logical cursor position. - ui::Range selection_range_; + // Logical selection range and visual cursor position. + SelectionModel selection_model_; + + // The cached cursor bounds. + Rect cursor_bounds_; + // cursor_bounds_ is computed when needed and cached afterwards. And it is + // invalidated in operations such as SetCursorPosition, SetSelection, Font + // related style change, and other operations that trigger re-layout. + bool cursor_bounds_valid_; // The cursor visibility and insert mode. bool cursor_visible_; |