summaryrefslogtreecommitdiffstats
path: root/views/controls
diff options
context:
space:
mode:
authoroshima@google.com <oshima@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2011-06-01 23:58:51 +0000
committeroshima@google.com <oshima@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2011-06-01 23:58:51 +0000
commitb9d79b23c4eb4f8bf09e1f67f245f58b1915ff19 (patch)
tree00c419dd57f67dee69794bce50a2bddc602d916b /views/controls
parentc9717965e7a4a38e4b3e0e30a33618ba35c372e3 (diff)
downloadchromium_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.cc14
-rw-r--r--views/controls/textfield/native_textfield_gtk.h4
-rw-r--r--views/controls/textfield/native_textfield_views.cc56
-rw-r--r--views/controls/textfield/native_textfield_views.h6
-rw-r--r--views/controls/textfield/native_textfield_win.cc14
-rw-r--r--views/controls/textfield/native_textfield_win.h4
-rw-r--r--views/controls/textfield/native_textfield_wrapper.h13
-rw-r--r--views/controls/textfield/text_style.cc54
-rw-r--r--views/controls/textfield/text_style.h65
-rw-r--r--views/controls/textfield/textfield.cc16
-rw-r--r--views/controls/textfield/textfield.h20
-rw-r--r--views/controls/textfield/textfield_views_model.cc332
-rw-r--r--views/controls/textfield/textfield_views_model.h55
-rw-r--r--views/controls/textfield/textfield_views_model_unittest.cc301
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