diff options
author | oshima@google.com <oshima@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-06-01 23:58:51 +0000 |
---|---|---|
committer | oshima@google.com <oshima@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-06-01 23:58:51 +0000 |
commit | b9d79b23c4eb4f8bf09e1f67f245f58b1915ff19 (patch) | |
tree | 00c419dd57f67dee69794bce50a2bddc602d916b /views/controls | |
parent | c9717965e7a4a38e4b3e0e30a33618ba35c372e3 (diff) | |
download | chromium_src-b9d79b23c4eb4f8bf09e1f67f245f58b1915ff19.zip chromium_src-b9d79b23c4eb4f8bf09e1f67f245f58b1915ff19.tar.gz chromium_src-b9d79b23c4eb4f8bf09e1f67f245f58b1915ff19.tar.bz2 |
TextStyles in TextfieldViews
- TextStyle class that specify the styles.
- Model owns TextStyle object. No need for client to manage
memory.
- It updates style list each time new item is added
and resolves overlap so that Paint method can simply
iterate and paint them.
- I changed selection so that it simply changes background of the selected text. This seems to be how webkit does and is much simpler.
URL decoration in omnibox
Renamed ClearCompositionText -> CancelCompositionText
No need to review changes to range. (http://codereview.chromium.org/7039051/)
BUG=none
TEST=added tests for textfield views.
Review URL: http://codereview.chromium.org/7047023
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@87552 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'views/controls')
-rw-r--r-- | views/controls/textfield/native_textfield_gtk.cc | 14 | ||||
-rw-r--r-- | views/controls/textfield/native_textfield_gtk.h | 4 | ||||
-rw-r--r-- | views/controls/textfield/native_textfield_views.cc | 56 | ||||
-rw-r--r-- | views/controls/textfield/native_textfield_views.h | 6 | ||||
-rw-r--r-- | views/controls/textfield/native_textfield_win.cc | 14 | ||||
-rw-r--r-- | views/controls/textfield/native_textfield_win.h | 4 | ||||
-rw-r--r-- | views/controls/textfield/native_textfield_wrapper.h | 13 | ||||
-rw-r--r-- | views/controls/textfield/text_style.cc | 54 | ||||
-rw-r--r-- | views/controls/textfield/text_style.h | 65 | ||||
-rw-r--r-- | views/controls/textfield/textfield.cc | 16 | ||||
-rw-r--r-- | views/controls/textfield/textfield.h | 20 | ||||
-rw-r--r-- | views/controls/textfield/textfield_views_model.cc | 332 | ||||
-rw-r--r-- | views/controls/textfield/textfield_views_model.h | 55 | ||||
-rw-r--r-- | views/controls/textfield/textfield_views_model_unittest.cc | 301 |
14 files changed, 743 insertions, 211 deletions
diff --git a/views/controls/textfield/native_textfield_gtk.cc b/views/controls/textfield/native_textfield_gtk.cc index 2fe5c60..d97d920 100644 --- a/views/controls/textfield/native_textfield_gtk.cc +++ b/views/controls/textfield/native_textfield_gtk.cc @@ -381,6 +381,20 @@ TextInputClient* NativeTextfieldGtk::GetTextInputClient() { return NULL; } +TextStyle* NativeTextfieldGtk::CreateTextStyle() { + NOTREACHED(); + return NULL; +} + +void NativeTextfieldGtk::ApplyTextStyle(const TextStyle* style, + const ui::Range& range) { + NOTREACHED(); +} + +void NativeTextfieldGtk::ClearAllTextStyles() { + NOTREACHED(); +} + void NativeTextfieldGtk::OnActivate(GtkWidget* native_widget) { GdkEvent* event = gtk_get_current_event(); if (!event || event->type != GDK_KEY_PRESS) diff --git a/views/controls/textfield/native_textfield_gtk.h b/views/controls/textfield/native_textfield_gtk.h index a129365..66b2b91 100644 --- a/views/controls/textfield/native_textfield_gtk.h +++ b/views/controls/textfield/native_textfield_gtk.h @@ -57,6 +57,10 @@ class NativeTextfieldGtk : public NativeControlGtk, virtual void HandleFocus() OVERRIDE; virtual void HandleBlur() OVERRIDE; virtual TextInputClient* GetTextInputClient() OVERRIDE; + virtual TextStyle* CreateTextStyle() OVERRIDE; + virtual void ApplyTextStyle(const TextStyle* style, + const ui::Range& range) OVERRIDE; + virtual void ClearAllTextStyles() OVERRIDE; // Overridden from NativeControlGtk: virtual void CreateNativeControl() OVERRIDE; diff --git a/views/controls/textfield/native_textfield_views.cc b/views/controls/textfield/native_textfield_views.cc index 0eeb291..4889b29 100644 --- a/views/controls/textfield/native_textfield_views.cc +++ b/views/controls/textfield/native_textfield_views.cc @@ -15,12 +15,12 @@ #include "ui/base/dragdrop/drag_drop_types.h" #include "ui/base/range/range.h" #include "ui/gfx/canvas.h" -#include "ui/gfx/canvas_skia.h" #include "ui/gfx/insets.h" #include "views/background.h" #include "views/border.h" #include "views/controls/focusable_border.h" #include "views/controls/menu/menu_2.h" +#include "views/controls/textfield/text_style.h" #include "views/controls/textfield/textfield.h" #include "views/controls/textfield/textfield_controller.h" #include "views/controls/textfield/textfield_views_model.h" @@ -39,12 +39,13 @@ namespace { // A global flag to switch the Textfield wrapper to TextfieldViews. bool textfield_view_enabled = false; -// Color setttings for text, backgrounds and cursor. +// Color settings for text, backgrounds and cursor. // These are tentative, and should be derived from theme, system // settings and current settings. +// TODO(oshima): Change this to match the standard chrome +// before dogfooding textfield views. const SkColor kSelectedTextColor = SK_ColorWHITE; -const SkColor kReadonlyTextColor = SK_ColorDKGRAY; -const SkColor kFocusedSelectionColor = SK_ColorBLUE; +const SkColor kFocusedSelectionColor = SK_ColorCYAN; const SkColor kUnfocusedSelectionColor = SK_ColorLTGRAY; const SkColor kCursorColor = SK_ColorBLACK; @@ -525,6 +526,21 @@ void NativeTextfieldViews::ExecuteCommand(int command_id) { OnAfterUserAction(); } +TextStyle* NativeTextfieldViews::CreateTextStyle() { + return model_->CreateTextStyle(); +} + +void NativeTextfieldViews::ApplyTextStyle(const TextStyle* style, + const ui::Range& range) { + model_->ApplyTextStyle(style, range); + SchedulePaint(); +} + +void NativeTextfieldViews::ClearAllTextStyles() { + model_->ClearAllTextStyles(); + SchedulePaint(); +} + // static bool NativeTextfieldViews::IsTextfieldViewsEnabled() { #if defined(TOUCH_UI) @@ -578,7 +594,7 @@ void NativeTextfieldViews::ClearCompositionText() { OnBeforeUserAction(); skip_input_method_cancel_composition_ = true; - model_->ClearCompositionText(); + model_->CancelCompositionText(); skip_input_method_cancel_composition_ = false; UpdateAfterChange(true, true); OnAfterUserAction(); @@ -796,31 +812,27 @@ void NativeTextfieldViews::PaintTextAndCursor(gfx::Canvas* canvas) { SkColor selection_color = textfield_->HasFocus() ? kFocusedSelectionColor : kUnfocusedSelectionColor; - SkColor text_color = - textfield_->read_only() ? kReadonlyTextColor : GetTextColor(); + gfx::Font font = GetFont(); + gfx::Rect selection_bounds = model_->GetSelectionBounds(font); + if (!selection_bounds.IsEmpty()) { + canvas->FillRectInt(selection_color, + x_offset + selection_bounds.x(), + y + selection_bounds.y(), + selection_bounds.width(), + selection_bounds.height()); + } for (TextfieldViewsModel::TextFragments::const_iterator iter = fragments.begin(); iter != fragments.end(); iter++) { - string16 text = model_->GetVisibleText(iter->start, iter->end); - - gfx::Font font = GetFont(); - if (iter->underline) - font = font.DeriveFont(0, font.GetStyle() | gfx::Font::UNDERLINED); - + string16 text = model_->GetVisibleText(iter->range.start(), + iter->range.end()); // TODO(oshima): This does not give the accurate position due to - // kerning. Figure out how webkit does this with skia. + // kerning. Figure out how to do. int width = font.GetStringWidth(text); - - if (iter->selected) { - canvas->FillRectInt(selection_color, x_offset, y, width, text_height); - canvas->DrawStringInt(text, font, kSelectedTextColor, - x_offset, y, width, text_height); - } else { - canvas->DrawStringInt(text, font, text_color, + iter->style->DrawString(canvas, text, font, textfield_->read_only(), x_offset, y, width, text_height); - } x_offset += width; } canvas->Restore(); diff --git a/views/controls/textfield/native_textfield_views.h b/views/controls/textfield/native_textfield_views.h index 512657c..0031144 100644 --- a/views/controls/textfield/native_textfield_views.h +++ b/views/controls/textfield/native_textfield_views.h @@ -38,6 +38,8 @@ class Menu2; // * X selection (only if we want to support). // * STYLE_MULTILINE, STYLE_LOWERCASE text. (These are not used in // chromeos, so we may not need them) +// Once completed, this will replace Textfield, NativeTextfieldWin and +// NativeTextfieldGtk. class NativeTextfieldViews : public View, public ContextMenuController, public DragController, @@ -111,6 +113,10 @@ class NativeTextfieldViews : public View, virtual void HandleFocus() OVERRIDE; virtual void HandleBlur() OVERRIDE; virtual TextInputClient* GetTextInputClient() OVERRIDE; + virtual TextStyle* CreateTextStyle() OVERRIDE; + virtual void ApplyTextStyle(const TextStyle* style, + const ui::Range& range) OVERRIDE; + virtual void ClearAllTextStyles() OVERRIDE; // ui::SimpleMenuModel::Delegate overrides virtual bool IsCommandIdChecked(int command_id) const OVERRIDE; diff --git a/views/controls/textfield/native_textfield_win.cc b/views/controls/textfield/native_textfield_win.cc index b59e168..91a3fc2 100644 --- a/views/controls/textfield/native_textfield_win.cc +++ b/views/controls/textfield/native_textfield_win.cc @@ -371,6 +371,20 @@ TextInputClient* NativeTextfieldWin::GetTextInputClient() { return NULL; } +TextStyle* NativeTextfieldWin::CreateTextStyle() { + NOTREACHED(); + return NULL; +} + +void NativeTextfieldWin::ApplyTextStyle(const TextStyle* style, + const ui::Range& range) { + NOTREACHED(); +} + +void NativeTextfieldWin::ClearAllTextStyles() { + NOTREACHED(); +} + //////////////////////////////////////////////////////////////////////////////// // NativeTextfieldWin, ui::SimpleMenuModel::Delegate implementation: diff --git a/views/controls/textfield/native_textfield_win.h b/views/controls/textfield/native_textfield_win.h index 69a7103..d20e45e 100644 --- a/views/controls/textfield/native_textfield_win.h +++ b/views/controls/textfield/native_textfield_win.h @@ -86,6 +86,10 @@ class NativeTextfieldWin virtual void HandleFocus() OVERRIDE; virtual void HandleBlur() OVERRIDE; virtual TextInputClient* GetTextInputClient() OVERRIDE; + virtual TextStyle* CreateTextStyle() OVERRIDE; + virtual void ApplyTextStyle(const TextStyle* style, + const ui::Range& range) OVERRIDE; + virtual void ClearAllTextStyles() OVERRIDE; // Overridden from ui::SimpleMenuModel::Delegate: virtual bool IsCommandIdChecked(int command_id) const OVERRIDE; diff --git a/views/controls/textfield/native_textfield_wrapper.h b/views/controls/textfield/native_textfield_wrapper.h index b3a6edb..49eadd1 100644 --- a/views/controls/textfield/native_textfield_wrapper.h +++ b/views/controls/textfield/native_textfield_wrapper.h @@ -22,6 +22,7 @@ namespace views { class KeyEvent; class Textfield; class TextInputClient; +class TextStyle; class View; // An interface implemented by an object that provides a platform-native @@ -125,6 +126,18 @@ class NativeTextfieldWrapper { // support text input. virtual TextInputClient* GetTextInputClient() = 0; + // Creates a new TextStyle for this textfield. + // See |Textfield::CreateTextStyle| for detail. + virtual TextStyle* CreateTextStyle() = 0; + + // Applies the |style| to the text specified by the |range|. + // See |Textfield::ApplyTextStyle| for detail. + virtual void ApplyTextStyle(const TextStyle* style, + const ui::Range& range) = 0; + + // Clears all text styles in this textfield. + virtual void ClearAllTextStyles() = 0; + // Creates an appropriate NativeTextfieldWrapper for the platform. static NativeTextfieldWrapper* CreateWrapper(Textfield* field); }; diff --git a/views/controls/textfield/text_style.cc b/views/controls/textfield/text_style.cc new file mode 100644 index 0000000..18400c6 --- /dev/null +++ b/views/controls/textfield/text_style.cc @@ -0,0 +1,54 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "views/controls/textfield/text_style.h" + +#include "ui/gfx/canvas.h" +#include "ui/gfx/canvas_skia.h" +#include "ui/gfx/font.h" + +namespace { +// Text color for read only. +const SkColor kReadonlyTextColor = SK_ColorDKGRAY; + +// Strike line width. +const int kStrikeWidth = 2; +} + +namespace views { + +TextStyle::TextStyle() + : foreground_(SK_ColorBLACK), + strike_(false), + underline_(false) { +} + +TextStyle::~TextStyle() { +} + +void TextStyle::DrawString(gfx::Canvas* canvas, + string16& text, + gfx::Font& base_font, + bool readonly, + int x, int y, int width, int height) const { + SkColor text_color = readonly ? kReadonlyTextColor : foreground_; + + gfx::Font font = underline_ ? + base_font.DeriveFont(0, base_font.GetStyle() | gfx::Font::UNDERLINED) : + base_font; + canvas->DrawStringInt(text, font, text_color, x, y, width, height); + if (strike_) { + SkPaint paint; + paint.setAntiAlias(true); + paint.setStyle(SkPaint::kFill_Style); + paint.setColor(text_color); + paint.setStrokeWidth(kStrikeWidth); + canvas->AsCanvasSkia()->drawLine( + SkIntToScalar(x), SkIntToScalar(y + height), + SkIntToScalar(x + width), SkIntToScalar(y), + paint); + } +} + +} // namespace views diff --git a/views/controls/textfield/text_style.h b/views/controls/textfield/text_style.h new file mode 100644 index 0000000..e489aee --- /dev/null +++ b/views/controls/textfield/text_style.h @@ -0,0 +1,65 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef VIEWS_CONTROLS_TEXTFIELD_TEXT_STYLE_H_ +#define VIEWS_CONTROLS_TEXTFIELD_TEXT_STYLE_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/gtest_prod_util.h" +#include "base/string16.h" +#include "third_party/skia/include/core/SkColor.h" + +namespace gfx { +class Canvas; +class Font; +} + +namespace views { + +// A class that specifies text style for TextfieldViews. +// TODO(suzhe|oshima): support underline color and thick style. +class TextStyle { + public: + // Foreground color for the text. + void set_foreground(SkColor color) { foreground_ = color; } + + // Draws diagnoal strike acrosss the text. + void set_strike(bool strike) { strike_ = strike; } + + // Adds underline to the text. + void set_underline(bool underline) { underline_ = underline; } + + private: + friend class NativeTextfieldViews; + friend class TextfieldViewsModel; + + FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, TextStyleTest); + FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, UndoRedo_CompositionText); + FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, CompositionTextTest); + + TextStyle(); + virtual ~TextStyle(); + + SkColor foreground() const { return foreground_; } + bool underline() const { return underline_; } + + // Draw string to the canvas within the region given + // by |x|,|y|,|width|,|height|. + void DrawString(gfx::Canvas* canvas, + string16& text, + gfx::Font& base_font, + bool read_only, + int x, int y, int width, int height) const; + + SkColor foreground_; + bool strike_; + bool underline_; + + DISALLOW_COPY_AND_ASSIGN(TextStyle); +}; + +} // namespace views + +#endif // VIEWS_CONTROLS_TEXTFIELD_TEXT_STYLE_H_ diff --git a/views/controls/textfield/textfield.cc b/views/controls/textfield/textfield.cc index 52fa17a..8775871 100644 --- a/views/controls/textfield/textfield.cc +++ b/views/controls/textfield/textfield.cc @@ -273,6 +273,22 @@ size_t Textfield::GetCursorPosition() const { return native_wrapper_->GetCursorPosition(); } +TextStyle* Textfield::CreateTextStyle() { + DCHECK(native_wrapper_); + return native_wrapper_->CreateTextStyle(); +} + +void Textfield::ApplyTextStyle(const TextStyle* style, + const ui::Range& range) { + DCHECK(native_wrapper_); + return native_wrapper_->ApplyTextStyle(style, range); +} + +void Textfield::ClearAllTextStyles() { + DCHECK(native_wrapper_); + native_wrapper_->ClearAllTextStyles(); +} + void Textfield::SetAccessibleName(const string16& name) { accessible_name_ = name; } diff --git a/views/controls/textfield/textfield.h b/views/controls/textfield/textfield.h index a07b6f1..9a688fc 100644 --- a/views/controls/textfield/textfield.h +++ b/views/controls/textfield/textfield.h @@ -39,6 +39,7 @@ namespace views { class KeyEvent; class NativeTextfieldWrapper; class TextfieldController; +class TextStyle; // This class implements a View that wraps a native text (edit) field. class Textfield : public View { @@ -189,6 +190,25 @@ class Textfield : public View { // only and has to be called after the wrapper is created. size_t GetCursorPosition() const; + // Creates a new TextStyle for this textfield. The object is owned + // by the textfield and gets deleted when the textfield is deleted. + // This is views-implementation only and has to be called after the + // wrapper is created. + TextStyle* CreateTextStyle(); + + // Applies the |style| to the text specified by the |range|. If + // there is already a style applied in the |range|, the style of the + // overlapping part will be replaced by this sytle. The style will + // be ignored if range is empty or invalid. This is + // views-implementation only and has to be called after the wrapper + // is created. + void ApplyTextStyle(const TextStyle* style, const ui::Range& range); + + // Clears All TextStyles. + // This is views-implementation only and has to be called after the + // wrapper is created. + void ClearAllTextStyles(); + // Set the accessible name of the text field. void SetAccessibleName(const string16& name); diff --git a/views/controls/textfield/textfield_views_model.cc b/views/controls/textfield/textfield_views_model.cc index a50fc01..89253a6 100644 --- a/views/controls/textfield/textfield_views_model.cc +++ b/views/controls/textfield/textfield_views_model.cc @@ -14,6 +14,7 @@ #include "ui/base/clipboard/scoped_clipboard_writer.h" #include "ui/base/range/range.h" #include "ui/gfx/font.h" +#include "views/controls/textfield/text_style.h" #include "views/controls/textfield/textfield.h" #include "views/views_delegate.h" @@ -200,8 +201,114 @@ class DeleteEdit : public Edit { } }; +struct TextStyleRange { + TextStyleRange(const TextStyle* s, size_t start, size_t end) + : style(s), + range(start, end) { + } + TextStyleRange(const TextStyle* s, const ui::Range& r) + : style(s), + range(r) { + } + const TextStyle *style; + ui::Range range; +}; + } // namespace internal +namespace { + +using views::internal::TextStyleRange; + +static bool TextStyleRangeComparator(const TextStyleRange* i, + const TextStyleRange* j) { + return i->range.start() < j->range.start(); +} + +#ifndef NDEBUG +// A test function to check TextStyleRanges' invariant condition: +// "no overlapping range". +bool CheckInvariant(const TextStyleRanges* style_ranges) { + TextStyleRanges copy = *style_ranges; + std::sort(copy.begin(), copy.end(), TextStyleRangeComparator); + + for (TextStyleRanges::size_type i = 0; i < copy.size() - 1; i++) { + ui::Range& former = copy[i]->range; + ui::Range& latter = copy[i + 1]->range; + if (former.is_empty()) { + LOG(WARNING) << "Empty range at " << i << " :" << former; + return false; + } + if (!former.IsValid()) { + LOG(WARNING) << "Invalid range at " << i << " :" << former; + return false; + } + if (former.GetMax() > latter.GetMin()) { + LOG(WARNING) << + "Sorting error. former:" << former << " latter:" << latter; + return false; + } + if (former.Intersects(latter)) { + LOG(ERROR) << "overlapping style range found: former=" << former + << ", latter=" << latter; + return false; + } + } + if ((*copy.rbegin())->range.is_empty()) { + LOG(WARNING) << "Empty range at end"; + return false; + } + if (!(*copy.rbegin())->range.IsValid()) { + LOG(WARNING) << "Invalid range at end"; + return false; + } + return true; +} +#endif + +void InsertStyle(TextStyleRanges* style_ranges, + TextStyleRange* text_style_range) { + const ui::Range& range = text_style_range->range; + if (range.is_empty() || !range.IsValid()) + return; + CHECK(!range.is_reversed()); + + // Invariant condition: all items in the range has no overlaps. + TextStyleRanges::size_type index = 0; + while (index < style_ranges->size()) { + TextStyleRange* current = (*style_ranges)[index]; + if (range.Contains(current->range)) { + style_ranges->erase(style_ranges->begin() + index); + delete current; + continue; + } else if (current->range.Contains(range) && + range.start() != current->range.start() && + range.end() != current->range.end()) { + // Split current style into two styles. + style_ranges->push_back( + new TextStyleRange(current->style, + range.GetMax(), current->range.GetMax())); + current->range.set_end(range.GetMin()); + } else if (range.Intersects(current->range)) { + if (current->range.GetMax() <= range.GetMax()) { + current->range.set_end(range.GetMin()); + } else { + current->range.set_start(range.GetMax()); + } + } else { + // No change needed. Pass it through. + } + index ++; + } + // Add the new range at the end. + style_ranges->push_back(text_style_range); +#ifndef NDEBUG + DCHECK(CheckInvariant(style_ranges)); +#endif +} + +} // namespace + using internal::Edit; using internal::DeleteEdit; using internal::InsertEdit; @@ -220,116 +327,65 @@ TextfieldViewsModel::TextfieldViewsModel(Delegate* delegate) composition_start_(0), composition_end_(0), is_password_(false), - current_edit_(edit_history_.end()) { + current_edit_(edit_history_.end()), + sort_style_ranges_(false) { } TextfieldViewsModel::~TextfieldViewsModel() { ClearEditHistory(); + ClearComposition(); + ClearAllTextStyles(); + TextStyles::iterator begin = text_styles_.begin(); + TextStyles::iterator end = text_styles_.end(); + while (begin != end) { + TextStyles::iterator temp = begin; + ++begin; + delete *temp; + } } -void TextfieldViewsModel::GetFragments(TextFragments* fragments) const { - DCHECK(fragments); - fragments->clear(); - if (HasCompositionText()) { - if (composition_start_) - fragments->push_back(TextFragment(0, composition_start_, false, false)); - - size_t selection_start = std::min(selection_start_, cursor_pos_); - size_t selection_end = std::max(selection_start_, cursor_pos_); - size_t last_end = composition_start_; - for (ui::CompositionUnderlines::const_iterator i = - composition_underlines_.begin(); - i != composition_underlines_.end(); ++i) { - size_t fragment_start = - std::min(i->start_offset, i->end_offset) + composition_start_; - size_t fragment_end = - std::max(i->start_offset, i->end_offset) + composition_start_; - - fragment_start = std::max(last_end, fragment_start); - fragment_end = std::min(fragment_end, composition_end_); - - if (fragment_start >= fragment_end) - break; +void TextfieldViewsModel::GetFragments(TextFragments* fragments) { + static const TextStyle* kNormalStyle = new TextStyle(); - // If there is no selection, then just add a text fragment with underline. - if (selection_start == selection_end) { - if (last_end < fragment_start) { - fragments->push_back( - TextFragment(last_end, fragment_start, false, false)); - } - fragments->push_back( - TextFragment(fragment_start, fragment_end, false, true)); - last_end = fragment_end; - continue; - } + if (sort_style_ranges_) { + sort_style_ranges_ = false; + std::sort(style_ranges_.begin(), style_ranges_.end(), + TextStyleRangeComparator); + } - size_t end = std::min(fragment_start, selection_start); - if (last_end < end) - fragments->push_back(TextFragment(last_end, end, false, false)); - - last_end = fragment_end; - - if (selection_start < fragment_start) { - end = std::min(selection_end, fragment_start); - fragments->push_back(TextFragment(selection_start, end, true, false)); - selection_start = end; - } else if (selection_start > fragment_start) { - end = std::min(selection_start, fragment_end); - fragments->push_back(TextFragment(fragment_start, end, false, true)); - fragment_start = end; - if (fragment_start == fragment_end) - continue; - } + // If a user is compositing text, use composition's style. + // TODO(oshima): ask suzhe for expected behavior. + const TextStyleRanges& ranges = composition_style_ranges_.size() > 0 ? + composition_style_ranges_ : style_ranges_; + TextStyleRanges::const_iterator next_ = ranges.begin(); - if (fragment_start < selection_end) { - DCHECK_EQ(selection_start, fragment_start); - end = std::min(fragment_end, selection_end); - fragments->push_back(TextFragment(fragment_start, end, true, true)); - fragment_start = end; - selection_start = end; - if (fragment_start == fragment_end) - continue; - } + DCHECK(fragments); + fragments->clear(); + size_t current = 0; + size_t end = text_.length(); + while(next_ != ranges.end()) { + const TextStyleRange* text_style_range = *next_++; + const ui::Range& range = text_style_range->range; + const TextStyle* style = text_style_range->style; - DCHECK_LT(fragment_start, fragment_end); - fragments->push_back( - TextFragment(fragment_start, fragment_end, false, true)); - } + DCHECK(!range.is_empty()); + DCHECK(range.IsValid()); + if (range.is_empty() || !range.IsValid()) + continue; - if (last_end < composition_end_) { - if (selection_start < selection_end) { - DCHECK_LE(last_end, selection_start); - if (last_end < selection_start) { - fragments->push_back( - TextFragment(last_end, selection_start, false, false)); - } - fragments->push_back( - TextFragment(selection_start, selection_end, true, false)); - if (selection_end < composition_end_) { - fragments->push_back( - TextFragment(selection_end, composition_end_, false, false)); - } - } else { - fragments->push_back( - TextFragment(last_end, composition_end_, false, false)); - } - } + size_t start = std::min(range.start(), end); - size_t len = text_.length(); - if (composition_end_ < len) - fragments->push_back(TextFragment(composition_end_, len, false, false)); - } else if (HasSelection()) { - size_t start = std::min(selection_start_, cursor_pos_); - size_t end = std::max(selection_start_, cursor_pos_); - if (start) - fragments->push_back(TextFragment(0, start, false, false)); - fragments->push_back(TextFragment(start, end, true, false)); - size_t len = text_.length(); - if (end != len) - fragments->push_back(TextFragment(end, len, false, false)); - } else { - fragments->push_back(TextFragment(0, text_.length(), false, false)); + if (start == end) // Exit loop if it reached the end. + break; + else if (current < start) // Fill the gap to next style with normal text. + fragments->push_back(TextFragment(current, start, kNormalStyle)); + + current = std::min(range.end(), end); + fragments->push_back(TextFragment(start, current, style)); } + // If there is any text left add it as normal text. + if (current != end) + fragments->push_back(TextFragment(current, end, kNormalStyle)); } bool TextfieldViewsModel::SetText(const string16& text) { @@ -362,7 +418,7 @@ void TextfieldViewsModel::Append(const string16& text) { bool TextfieldViewsModel::Delete() { if (HasCompositionText()) { // No undo/redo for composition text. - ClearCompositionText(); + CancelCompositionText(); return true; } if (HasSelection()) { @@ -379,7 +435,7 @@ bool TextfieldViewsModel::Delete() { bool TextfieldViewsModel::Backspace() { if (HasCompositionText()) { // No undo/redo for composition text. - ClearCompositionText(); + CancelCompositionText(); return true; } if (HasSelection()) { @@ -511,6 +567,16 @@ gfx::Rect TextfieldViewsModel::GetCursorBounds(const gfx::Font& font) const { } } +gfx::Rect TextfieldViewsModel::GetSelectionBounds(const gfx::Font& font) const { + if (!HasSelection()) + return gfx::Rect(); + size_t start = std::min(selection_start_, cursor_pos_); + size_t end = std::max(selection_start_, cursor_pos_); + int start_x = font.GetStringWidth(text_.substr(0, start)); + int end_x = font.GetStringWidth(text_.substr(0, end)); + return gfx::Rect(start_x, 0, end_x - start_x, font.GetHeight()); +} + string16 TextfieldViewsModel::GetSelectedText() const { return text_.substr( std::min(cursor_pos_, selection_start_), @@ -598,7 +664,7 @@ bool TextfieldViewsModel::Undo() { return false; DCHECK(!HasCompositionText()); if (HasCompositionText()) // safe guard for release build. - ClearCompositionText(); + CancelCompositionText(); string16 old = text_; (*current_edit_)->Commit(); @@ -616,7 +682,7 @@ bool TextfieldViewsModel::Redo() { return false; DCHECK(!HasCompositionText()); if (HasCompositionText()) // safe guard for release build. - ClearCompositionText(); + CancelCompositionText(); if (current_edit_ == edit_history_.end()) current_edit_ = edit_history_.begin(); @@ -668,7 +734,7 @@ void TextfieldViewsModel::DeleteSelection() { void TextfieldViewsModel::DeleteSelectionAndInsertTextAt( const string16& text, size_t position) { if (HasCompositionText()) - ClearCompositionText(); + CancelCompositionText(); ExecuteAndRecordReplaceAt(text, position, false); } @@ -684,8 +750,10 @@ void TextfieldViewsModel::GetTextRange(ui::Range* range) const { void TextfieldViewsModel::SetCompositionText( const ui::CompositionText& composition) { + static const TextStyle* composition_style = CreateUnderlineStyle(); + if (HasCompositionText()) - ClearCompositionText(); + CancelCompositionText(); else if (HasSelection()) DeleteSelection(); @@ -696,7 +764,17 @@ void TextfieldViewsModel::SetCompositionText( text_.insert(cursor_pos_, composition.text); composition_start_ = cursor_pos_; composition_end_ = composition_start_ + length; - composition_underlines_ = composition.underlines; + for (ui::CompositionUnderlines::const_iterator iter = + composition.underlines.begin(); + iter != composition.underlines.end(); + iter++) { + size_t start = composition_start_ + iter->start_offset; + size_t end = composition_start_ + iter->end_offset; + InsertStyle(&composition_style_ranges_, + new TextStyleRange(composition_style, start, end)); + } + std::sort(composition_style_ranges_.begin(), + composition_style_ranges_.end(), TextStyleRangeComparator); if (composition.selection.IsValid()) { selection_start_ = @@ -719,24 +797,38 @@ void TextfieldViewsModel::ConfirmCompositionText() { // sure exactly how this should work. Find out and fix if necessary. AddOrMergeEditHistory(new InsertEdit(false, new_text, composition_start_)); cursor_pos_ = composition_end_; - composition_start_ = composition_end_ = string16::npos; - composition_underlines_.clear(); + ClearComposition(); ClearSelection(); if (delegate_) delegate_->OnCompositionTextConfirmedOrCleared(); } -void TextfieldViewsModel::ClearCompositionText() { +void TextfieldViewsModel::CancelCompositionText() { DCHECK(HasCompositionText()); text_.erase(composition_start_, composition_end_ - composition_start_); cursor_pos_ = composition_start_; - composition_start_ = composition_end_ = string16::npos; - composition_underlines_.clear(); + ClearComposition(); ClearSelection(); if (delegate_) delegate_->OnCompositionTextConfirmedOrCleared(); } +void TextfieldViewsModel::ClearComposition() { + composition_start_ = composition_end_ = string16::npos; + STLDeleteContainerPointers(composition_style_ranges_.begin(), + composition_style_ranges_.end()); + composition_style_ranges_.clear(); +} + +void TextfieldViewsModel::ApplyTextStyle(const TextStyle* style, + const ui::Range& range) { + TextStyleRange* new_text_style_range = range.is_reversed() ? + new TextStyleRange(style, ui::Range(range.end(), range.start())) : + new TextStyleRange(style, range); + InsertStyle(&style_ranges_, new_text_style_range); + sort_style_ranges_ = true; +} + void TextfieldViewsModel::GetCompositionTextRange(ui::Range* range) const { if (HasCompositionText()) *range = ui::Range(composition_start_, composition_end_); @@ -748,6 +840,17 @@ bool TextfieldViewsModel::HasCompositionText() const { return composition_start_ != composition_end_; } +TextStyle* TextfieldViewsModel::CreateTextStyle() { + TextStyle* style = new TextStyle(); + text_styles_.push_back(style); + return style; +} + +void TextfieldViewsModel::ClearAllTextStyles() { + STLDeleteContainerPointers(style_ranges_.begin(), style_ranges_.end()); + style_ranges_.clear(); +} + ///////////////////////////////////////////////////////////////// // TextfieldViewsModel: private @@ -773,7 +876,7 @@ size_t TextfieldViewsModel::GetSafePosition(size_t position) const { void TextfieldViewsModel::InsertTextInternal(const string16& text, bool mergeable) { if (HasCompositionText()) { - ClearCompositionText(); + CancelCompositionText(); ExecuteAndRecordInsert(text, mergeable); } else if (HasSelection()) { ExecuteAndRecordReplace(text, mergeable); @@ -785,7 +888,7 @@ void TextfieldViewsModel::InsertTextInternal(const string16& text, void TextfieldViewsModel::ReplaceTextInternal(const string16& text, bool mergeable) { if (HasCompositionText()) - ClearCompositionText(); + CancelCompositionText(); else if (!HasSelection()) SelectRange(ui::Range(cursor_pos_ + text.length(), cursor_pos_)); // Edit history is recorded in InsertText. @@ -897,4 +1000,11 @@ void TextfieldViewsModel::ModifyText(size_t delete_from, // This looks fine feature and we may want to do the same. } +// static +TextStyle* TextfieldViewsModel::CreateUnderlineStyle() { + TextStyle* style = new TextStyle(); + style->set_underline(true); + return style; +} + } // namespace views diff --git a/views/controls/textfield/textfield_views_model.h b/views/controls/textfield/textfield_views_model.h index 78ae425..de7e520 100644 --- a/views/controls/textfield/textfield_views_model.h +++ b/views/controls/textfield/textfield_views_model.h @@ -25,11 +25,19 @@ class Range; namespace views { +class TextStyle; +typedef std::vector<TextStyle*> TextStyles; + namespace internal { // Internal Edit class that keeps track of edits for undo/redo. class Edit; + +struct TextStyleRange; + } // namespace internal +typedef std::vector<internal::TextStyleRange*> TextStyleRanges; + // A model that represents a text content for TextfieldViews. // It supports editing, selection and cursor manipulation. class TextfieldViewsModel { @@ -54,21 +62,17 @@ class TextfieldViewsModel { // in the future to support multi-color text // for omnibox. struct TextFragment { - TextFragment(size_t s, size_t e, bool sel, bool u) - : start(s), end(e), selected(sel), underline(u) { + TextFragment(size_t start, size_t end, const views::TextStyle* s) + : range(start, end), style(s) { } // The start and end position of text fragment. - size_t start, end; - // True if the text is selected. - bool selected; - // True if the text has underline. - // TODO(suzhe): support underline color and thick style. - bool underline; + ui::Range range; + const TextStyle* style; }; typedef std::vector<TextFragment> TextFragments; // Gets the text element info. - void GetFragments(TextFragments* elements) const; + void GetFragments(TextFragments* elements); void set_is_password(bool is_password) { is_password_ = is_password; @@ -167,6 +171,9 @@ class TextfieldViewsModel { // Returns the bounds of character at the current cursor. gfx::Rect GetCursorBounds(const gfx::Font& font) const; + // Returns the bounds of selected text. + gfx::Rect GetSelectionBounds(const gfx::Font& font) const; + // Selection related method // Returns the selected text. @@ -252,7 +259,7 @@ class TextfieldViewsModel { void ConfirmCompositionText(); // Removes current composition text. - void ClearCompositionText(); + void CancelCompositionText(); // Retrieves the range of current composition text. void GetCompositionTextRange(ui::Range* range) const; @@ -260,10 +267,15 @@ class TextfieldViewsModel { // Returns true if there is composition text. bool HasCompositionText() const; + TextStyle* CreateTextStyle(); + + void ClearAllTextStyles(); + private: friend class NativeTextfieldViews; friend class NativeTextfieldViewsTest; friend class TextfieldViewsModelTest; + friend class TextStyle; friend class UndoRedo_BasicTest; friend class UndoRedo_CutCopyPasteTest; friend class UndoRedo_ReplaceTest; @@ -272,6 +284,7 @@ class TextfieldViewsModel { FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, UndoRedo_BasicTest); FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, UndoRedo_CutCopyPasteTest); FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, UndoRedo_ReplaceTest); + FRIEND_TEST_ALL_PREFIXES(TextfieldViewsModelTest, TextStyleTest); // Returns the visible text given |start| and |end|. string16 GetVisibleText(size_t start, size_t end) const; @@ -321,6 +334,12 @@ class TextfieldViewsModel { size_t new_text_insert_at, size_t new_cursor_pos); + void ClearComposition(); + + void ApplyTextStyle(const TextStyle* style, const ui::Range& range); + + static TextStyle* CreateUnderlineStyle(); + // Pointer to a TextfieldViewsModel::Delegate instance, should be provided by // the View object. Delegate* delegate_; @@ -338,9 +357,6 @@ class TextfieldViewsModel { size_t composition_start_; size_t composition_end_; - // Underline information of the composition text. - ui::CompositionUnderlines composition_underlines_; - // True if the text is the password. bool is_password_; @@ -360,6 +376,19 @@ class TextfieldViewsModel { // 3) redone all undone edits. EditHistory::iterator current_edit_; + // This manages all styles objects. + TextStyles text_styles_; + + // List of style ranges. Elements in the list never overlap each other. + // Elements are not sorted at the time of insertion, and gets sorted + // when it's painted (if necessary). + TextStyleRanges style_ranges_; + // True if the style_ranges_ needs to be sorted. + bool sort_style_ranges_; + + // List of style ranges for composition text. + TextStyleRanges composition_style_ranges_; + DISALLOW_COPY_AND_ASSIGN(TextfieldViewsModel); }; diff --git a/views/controls/textfield/textfield_views_model_unittest.cc b/views/controls/textfield/textfield_views_model_unittest.cc index 1a5a642..d9d5721 100644 --- a/views/controls/textfield/textfield_views_model_unittest.cc +++ b/views/controls/textfield/textfield_views_model_unittest.cc @@ -10,6 +10,7 @@ #include "ui/base/clipboard/clipboard.h" #include "ui/base/clipboard/scoped_clipboard_writer.h" #include "ui/base/range/range.h" +#include "views/controls/textfield/text_style.h" #include "views/controls/textfield/textfield.h" #include "views/controls/textfield/textfield_views_model.h" #include "views/test/test_views_delegate.h" @@ -247,58 +248,23 @@ TEST_F(TextfieldViewsModelTest, Word) { TEST_F(TextfieldViewsModelTest, TextFragment) { TextfieldViewsModel model(NULL); TextfieldViewsModel::TextFragments fragments; - // Empty string + // Empty string has no fragment. model.GetFragments(&fragments); - EXPECT_EQ(1U, fragments.size()); - EXPECT_EQ(0U, fragments[0].start); - EXPECT_EQ(0U, fragments[0].end); - EXPECT_FALSE(fragments[0].selected); + EXPECT_EQ(0U, fragments.size()); // Some string model.Append(ASCIIToUTF16("Hello world")); model.GetFragments(&fragments); EXPECT_EQ(1U, fragments.size()); - EXPECT_EQ(0U, fragments[0].start); - EXPECT_EQ(11U, fragments[0].end); - EXPECT_FALSE(fragments[0].selected); + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(11U, fragments[0].range.end()); - // Select 1st word + // Selection won't change fragment. model.MoveCursorToNextWord(true); model.GetFragments(&fragments); - EXPECT_EQ(2U, fragments.size()); - EXPECT_EQ(0U, fragments[0].start); - EXPECT_EQ(5U, fragments[0].end); - EXPECT_TRUE(fragments[0].selected); - EXPECT_EQ(5U, fragments[1].start); - EXPECT_EQ(11U, fragments[1].end); - EXPECT_FALSE(fragments[1].selected); - - // Select empty string - model.ClearSelection(); - model.MoveCursorRight(true); - model.GetFragments(&fragments); - EXPECT_EQ(3U, fragments.size()); - EXPECT_EQ(0U, fragments[0].start); - EXPECT_EQ(5U, fragments[0].end); - EXPECT_FALSE(fragments[0].selected); - EXPECT_EQ(5U, fragments[1].start); - EXPECT_EQ(6U, fragments[1].end); - EXPECT_TRUE(fragments[1].selected); - - EXPECT_EQ(6U, fragments[2].start); - EXPECT_EQ(11U, fragments[2].end); - EXPECT_FALSE(fragments[2].selected); - - // Select to the end. - model.MoveCursorToEnd(true); - model.GetFragments(&fragments); - EXPECT_EQ(2U, fragments.size()); - EXPECT_EQ(0U, fragments[0].start); - EXPECT_EQ(5U, fragments[0].end); - EXPECT_FALSE(fragments[0].selected); - EXPECT_EQ(5U, fragments[1].start); - EXPECT_EQ(11U, fragments[1].end); - EXPECT_TRUE(fragments[1].selected); + EXPECT_EQ(1U, fragments.size()); + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(11U, fragments[0].range.end()); } TEST_F(TextfieldViewsModelTest, SetText) { @@ -546,41 +512,36 @@ TEST_F(TextfieldViewsModelTest, CompositionTextTest) { model.GetTextRange(&range); EXPECT_EQ(10U, range.end()); + EXPECT_STR_EQ("1234567890", model.text()); model.GetCompositionTextRange(&range); EXPECT_EQ(5U, range.start()); EXPECT_EQ(8U, range.end()); + // composition text + EXPECT_STR_EQ("456", model.GetTextFromRange(ui::Range(3, 6))); model.GetSelectedRange(&range); EXPECT_EQ(7U, range.start()); EXPECT_EQ(8U, range.end()); - - EXPECT_STR_EQ("1234567890", model.text()); EXPECT_STR_EQ("8", model.GetSelectedText()); - EXPECT_STR_EQ("456", model.GetTextFromRange(ui::Range(3, 6))); TextfieldViewsModel::TextFragments fragments; model.GetFragments(&fragments); - EXPECT_EQ(4U, fragments.size()); - EXPECT_EQ(0U, fragments[0].start); - EXPECT_EQ(5U, fragments[0].end); - EXPECT_FALSE(fragments[0].selected); - EXPECT_FALSE(fragments[0].underline); - EXPECT_EQ(5U, fragments[1].start); - EXPECT_EQ(7U, fragments[1].end); - EXPECT_FALSE(fragments[1].selected); - EXPECT_TRUE(fragments[1].underline); - EXPECT_EQ(7U, fragments[2].start); - EXPECT_EQ(8U, fragments[2].end); - EXPECT_TRUE(fragments[2].selected); - EXPECT_TRUE(fragments[2].underline); - EXPECT_EQ(8U, fragments[3].start); - EXPECT_EQ(10U, fragments[3].end); - EXPECT_FALSE(fragments[3].selected); - EXPECT_FALSE(fragments[3].underline); + EXPECT_EQ(3U, fragments.size()); + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(5U, fragments[0].range.end()); + EXPECT_FALSE(fragments[0].style->underline()); + + EXPECT_EQ(5U, fragments[1].range.start()); + EXPECT_EQ(8U, fragments[1].range.end()); + EXPECT_TRUE(fragments[1].style->underline()); + + EXPECT_EQ(8U, fragments[2].range.start()); + EXPECT_EQ(10U, fragments[2].range.end()); + EXPECT_FALSE(fragments[2].style->underline()); EXPECT_FALSE(composition_text_confirmed_or_cleared_); - model.ClearCompositionText(); + model.CancelCompositionText(); EXPECT_TRUE(composition_text_confirmed_or_cleared_); composition_text_confirmed_or_cleared_ = false; EXPECT_FALSE(model.HasCompositionText()); @@ -1047,7 +1008,7 @@ TEST_F(TextfieldViewsModelTest, UndoRedo_CompositionText) { model.MoveCursorToHome(false); model.SetCompositionText(composition); EXPECT_STR_EQ("abcABCDEabc", model.text()); - model.ClearCompositionText(); + model.CancelCompositionText(); EXPECT_STR_EQ("ABCDEabc", model.text()); EXPECT_FALSE(model.Redo()); EXPECT_STR_EQ("ABCDEabc", model.text()); @@ -1089,4 +1050,214 @@ TEST_F(TextfieldViewsModelTest, UndoRedo_CompositionText) { // TODO(oshima): We need MockInputMethod to test the behavior with IME. } +TEST_F(TextfieldViewsModelTest, TextStyleTest) { + const SkColor black = 0xFF000000; // black is default text color. + const SkColor white = 0xFFFFFFFF; + TextfieldViewsModel model(NULL); + TextStyle* color = model.CreateTextStyle(); + color->set_foreground(white); + TextStyle* underline = model.CreateTextStyle(); + underline->set_underline(true); + underline->set_foreground(white); + TextStyle* strike = model.CreateTextStyle(); + strike->set_strike(true); + strike->set_foreground(white); + + // Case 1: No overlaps + model.ApplyTextStyle(color, ui::Range(1, 3)); + model.ApplyTextStyle(underline, ui::Range(5, 6)); + + TextfieldViewsModel::TextFragments fragments; + model.GetFragments(&fragments); + // Styles with empty string simply returns an empty fragments. + EXPECT_EQ(0U, fragments.size()); + + // 1st style only. + model.SetText(ASCIIToUTF16("01234")); // SetText doesn't change styles. + model.GetFragments(&fragments); + EXPECT_EQ(3U, fragments.size()); + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(1U, fragments[0].range.end()); + EXPECT_EQ(black, fragments[0].style->foreground()); + + EXPECT_EQ(1U, fragments[1].range.start()); + EXPECT_EQ(3U, fragments[1].range.end()); + EXPECT_EQ(color, fragments[1].style); + + EXPECT_EQ(3U, fragments[2].range.start()); + EXPECT_EQ(5U, fragments[2].range.end()); + EXPECT_EQ(black, fragments[2].style->foreground()); + + // Clear styles + model.ClearAllTextStyles(); + model.GetFragments(&fragments); + EXPECT_EQ(1U, fragments.size()); + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(5U, fragments[0].range.end()); + EXPECT_EQ(black, fragments[0].style->foreground()); + + // Case 2: Overlaps on left and on right + model.ApplyTextStyle(color, ui::Range(1, 3)); + model.ApplyTextStyle(strike, ui::Range(6, 8)); + model.ApplyTextStyle(underline, ui::Range(2, 7)); + + // With short string + model.SetText(ASCIIToUTF16("0")); + model.GetFragments(&fragments); + EXPECT_EQ(1U, fragments.size()); + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(1U, fragments[0].range.end()); + EXPECT_EQ(black, fragments[0].style->foreground()); + + // With mid-length string + model.SetText(ASCIIToUTF16("0123")); + model.GetFragments(&fragments); + EXPECT_EQ(3U, fragments.size()); + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(1U, fragments[0].range.end()); + EXPECT_EQ(black, fragments[0].style->foreground()); + + EXPECT_EQ(1U, fragments[1].range.start()); + EXPECT_EQ(2U, fragments[1].range.end()); + EXPECT_EQ(color, fragments[1].style); + + EXPECT_EQ(2U, fragments[2].range.start()); + EXPECT_EQ(4U, fragments[2].range.end()); + EXPECT_EQ(underline, fragments[2].style); + + // With long (longer than styles) string + model.SetText(ASCIIToUTF16("0123456789")); + model.GetFragments(&fragments); + EXPECT_EQ(5U, fragments.size()); + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(1U, fragments[0].range.end()); + EXPECT_EQ(black, fragments[0].style->foreground()); + + EXPECT_EQ(1U, fragments[1].range.start()); + EXPECT_EQ(2U, fragments[1].range.end()); + EXPECT_EQ(color, fragments[1].style); + + EXPECT_EQ(2U, fragments[2].range.start()); + EXPECT_EQ(7U, fragments[2].range.end()); + EXPECT_EQ(underline, fragments[2].style); + + EXPECT_EQ(7U, fragments[3].range.start()); + EXPECT_EQ(8U, fragments[3].range.end()); + EXPECT_EQ(strike, fragments[3].style); + + EXPECT_EQ(8U, fragments[4].range.start()); + EXPECT_EQ(10U, fragments[4].range.end()); + EXPECT_EQ(black, fragments[4].style->foreground()); + + model.ClearAllTextStyles(); + + // Case 3: The underline style splits the color style underneath. + model.ApplyTextStyle(color, ui::Range(0, 15)); + model.ApplyTextStyle(underline, ui::Range(5, 6)); + model.GetFragments(&fragments); + EXPECT_EQ(3U, fragments.size()); + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(5U, fragments[0].range.end()); + EXPECT_EQ(color, fragments[0].style); + + EXPECT_EQ(5U, fragments[1].range.start()); + EXPECT_EQ(6U, fragments[1].range.end()); + EXPECT_EQ(underline, fragments[1].style); + + EXPECT_EQ(6U, fragments[2].range.start()); + EXPECT_EQ(10U, fragments[2].range.end()); + EXPECT_EQ(color, fragments[2].style); + + model.ClearAllTextStyles(); + + // Case 4: The underline style moves the color style underneath. + model.ApplyTextStyle(color, ui::Range(0, 15)); + model.ApplyTextStyle(underline, ui::Range(0, 6)); + model.GetFragments(&fragments); + EXPECT_EQ(2U, fragments.size()); + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(6U, fragments[0].range.end()); + EXPECT_EQ(underline, fragments[0].style); + + EXPECT_EQ(6U, fragments[1].range.start()); + EXPECT_EQ(10U, fragments[1].range.end()); + EXPECT_EQ(color, fragments[1].style); + + model.ClearAllTextStyles(); + + model.ApplyTextStyle(color, ui::Range(0, 10)); + model.ApplyTextStyle(underline, ui::Range(6, 10)); + model.GetFragments(&fragments); + EXPECT_EQ(2U, fragments.size()); + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(6U, fragments[0].range.end()); + EXPECT_EQ(color, fragments[0].style); + + EXPECT_EQ(6U, fragments[1].range.start()); + EXPECT_EQ(10U, fragments[1].range.end()); + EXPECT_EQ(underline, fragments[1].style); + + model.ClearAllTextStyles(); + // Case 5: The strike style hides the unerline style underneath. + model.ApplyTextStyle(color, ui::Range(0, 15)); + model.ApplyTextStyle(underline, ui::Range(0, 6)); + model.ApplyTextStyle(strike, ui::Range(4, 7)); + model.GetFragments(&fragments); + EXPECT_EQ(3U, fragments.size()); + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(4U, fragments[0].range.end()); + EXPECT_EQ(underline, fragments[0].style); + + EXPECT_EQ(4U, fragments[1].range.start()); + EXPECT_EQ(7U, fragments[1].range.end()); + EXPECT_EQ(strike, fragments[1].style); + + EXPECT_EQ(7U, fragments[2].range.start()); + EXPECT_EQ(10U, fragments[2].range.end()); + EXPECT_EQ(color, fragments[2].style); + + // Case 6: Reversed range. + model.ClearAllTextStyles(); + model.ApplyTextStyle(color, ui::Range(3, 1)); + model.ApplyTextStyle(underline, ui::Range(6, 4)); + model.ApplyTextStyle(strike, ui::Range(5, 2)); + model.GetFragments(&fragments); + EXPECT_EQ(5U, fragments.size()); + + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(1U, fragments[0].range.end()); + EXPECT_EQ(black, fragments[0].style->foreground()); + + EXPECT_EQ(1U, fragments[1].range.start()); + EXPECT_EQ(2U, fragments[1].range.end()); + EXPECT_EQ(color, fragments[1].style); + + EXPECT_EQ(2U, fragments[2].range.start()); + EXPECT_EQ(5U, fragments[2].range.end()); + EXPECT_EQ(strike, fragments[2].style); + + EXPECT_EQ(5U, fragments[3].range.start()); + EXPECT_EQ(6U, fragments[3].range.end()); + EXPECT_EQ(underline, fragments[3].style); + + EXPECT_EQ(6U, fragments[4].range.start()); + EXPECT_EQ(10U, fragments[4].range.end()); + EXPECT_EQ(black, fragments[4].style->foreground()); + + // Case 7: empty / invald range + model.ClearAllTextStyles(); + model.ApplyTextStyle(color, ui::Range(0, 0)); + model.ApplyTextStyle(underline, ui::Range(4, 4)); + ui::Range invalid = ui::Range(0, 2).Intersect(ui::Range(3, 4)); + ASSERT_FALSE(invalid.IsValid()); + + model.ApplyTextStyle(strike, invalid); + model.GetFragments(&fragments); + EXPECT_EQ(1U, fragments.size()); + + EXPECT_EQ(0U, fragments[0].range.start()); + EXPECT_EQ(10U, fragments[0].range.end()); + EXPECT_EQ(black, fragments[0].style->foreground()); +} + } // namespace views |