- add sources.
[platform/framework/web/crosswalk.git] / src / ui / views / controls / styled_label.cc
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ui/views/controls/styled_label.h"
6
7 #include <vector>
8
9 #include "base/strings/string_util.h"
10 #include "ui/gfx/text_elider.h"
11 #include "ui/native_theme/native_theme.h"
12 #include "ui/views/controls/label.h"
13 #include "ui/views/controls/link.h"
14 #include "ui/views/controls/styled_label_listener.h"
15
16 namespace views {
17
18 namespace {
19
20 // Calculates the height of a line of text. Currently returns the height of
21 // a label.
22 int CalculateLineHeight() {
23   Label label;
24   return label.GetPreferredSize().height();
25 }
26
27 scoped_ptr<Label> CreateLabelRange(
28     const string16& text,
29     const StyledLabel::RangeStyleInfo& style_info,
30     views::LinkListener* link_listener) {
31   scoped_ptr<Label> result;
32
33   if (style_info.is_link) {
34     Link* link = new Link(text);
35     link->set_listener(link_listener);
36     link->SetUnderline((style_info.font_style & gfx::Font::UNDERLINE) != 0);
37     result.reset(link);
38   } else {
39     Label* label = new Label(text);
40     // Give the label a focus border so that its preferred size matches
41     // links' preferred sizes
42     label->SetHasFocusBorder(true);
43
44     result.reset(label);
45   }
46
47   result->SetEnabledColor(style_info.color);
48
49   if (!style_info.tooltip.empty())
50     result->SetTooltipText(style_info.tooltip);
51   if (style_info.font_style != gfx::Font::NORMAL)
52     result->SetFont(result->font().DeriveFont(0, style_info.font_style));
53
54   return scoped_ptr<Label>(result.release());
55 }
56
57 }  // namespace
58
59 StyledLabel::RangeStyleInfo::RangeStyleInfo()
60     : font_style(gfx::Font::NORMAL),
61       color(ui::NativeTheme::instance()->GetSystemColor(
62           ui::NativeTheme::kColorId_LabelEnabledColor)),
63       disable_line_wrapping(false),
64       is_link(false) {}
65
66 StyledLabel::RangeStyleInfo::~RangeStyleInfo() {}
67
68 // static
69 StyledLabel::RangeStyleInfo StyledLabel::RangeStyleInfo::CreateForLink() {
70   RangeStyleInfo result;
71   result.disable_line_wrapping = true;
72   result.is_link = true;
73   result.color = Link::GetDefaultEnabledColor();
74   return result;
75 }
76
77 bool StyledLabel::StyleRange::operator<(
78     const StyledLabel::StyleRange& other) const {
79   // Intentionally reversed so the priority queue is sorted by smallest first.
80   return range.start() > other.range.start();
81 }
82
83 StyledLabel::StyledLabel(const string16& text, StyledLabelListener* listener)
84     : listener_(listener),
85       displayed_on_background_color_set_(false),
86       auto_color_readability_enabled_(true) {
87   TrimWhitespace(text, TRIM_TRAILING, &text_);
88 }
89
90 StyledLabel::~StyledLabel() {}
91
92 void StyledLabel::SetText(const string16& text) {
93   text_ = text;
94   style_ranges_ = std::priority_queue<StyleRange>();
95   RemoveAllChildViews(true);
96   PreferredSizeChanged();
97 }
98
99 void StyledLabel::AddStyleRange(const gfx::Range& range,
100                                 const RangeStyleInfo& style_info) {
101   DCHECK(!range.is_reversed());
102   DCHECK(!range.is_empty());
103   DCHECK(gfx::Range(0, text_.size()).Contains(range));
104
105   style_ranges_.push(StyleRange(range, style_info));
106
107   PreferredSizeChanged();
108 }
109
110 void StyledLabel::SetDefaultStyle(const RangeStyleInfo& style_info) {
111   default_style_info_ = style_info;
112   PreferredSizeChanged();
113 }
114
115 void StyledLabel::SetDisplayedOnBackgroundColor(SkColor color) {
116   displayed_on_background_color_ = color;
117   displayed_on_background_color_set_ = true;
118 }
119
120 gfx::Insets StyledLabel::GetInsets() const {
121   gfx::Insets insets = View::GetInsets();
122   const gfx::Insets focus_border_padding(1, 1, 1, 1);
123   insets += focus_border_padding;
124   return insets;
125 }
126
127 int StyledLabel::GetHeightForWidth(int w) {
128   if (w != calculated_size_.width())
129     calculated_size_ = gfx::Size(w, CalculateAndDoLayout(w, true));
130
131   return calculated_size_.height();
132 }
133
134 void StyledLabel::Layout() {
135   CalculateAndDoLayout(GetLocalBounds().width(), false);
136 }
137
138 void StyledLabel::PreferredSizeChanged() {
139   calculated_size_ = gfx::Size();
140   View::PreferredSizeChanged();
141 }
142
143 void StyledLabel::LinkClicked(Link* source, int event_flags) {
144   if (listener_)
145     listener_->StyledLabelLinkClicked(link_targets_[source], event_flags);
146 }
147
148 int StyledLabel::CalculateAndDoLayout(int width, bool dry_run) {
149   if (!dry_run) {
150     RemoveAllChildViews(true);
151     link_targets_.clear();
152   }
153
154   width -= GetInsets().width();
155   if (width <= 0 || text_.empty())
156     return 0;
157
158   const int line_height = CalculateLineHeight();
159   // The index of the line we're on.
160   int line = 0;
161   // The x position (in pixels) of the line we're on, relative to content
162   // bounds.
163   int x = 0;
164
165   string16 remaining_string = text_;
166   std::priority_queue<StyleRange> style_ranges = style_ranges_;
167
168   // Iterate over the text, creating a bunch of labels and links and laying them
169   // out in the appropriate positions.
170   while (!remaining_string.empty()) {
171     // Don't put whitespace at beginning of a line with an exception for the
172     // first line (so the text's leading whitespace is respected).
173     if (x == 0 && line > 0)
174       TrimWhitespace(remaining_string, TRIM_LEADING, &remaining_string);
175
176     gfx::Range range(gfx::Range::InvalidRange());
177     if (!style_ranges.empty())
178       range = style_ranges.top().range;
179
180     const size_t position = text_.size() - remaining_string.size();
181
182     const gfx::Rect chunk_bounds(x, 0, width - x, 2 * line_height);
183     std::vector<string16> substrings;
184     gfx::Font text_font;
185     // If the start of the remaining text is inside a styled range, the font
186     // style may differ from the base font. The font specified by the range
187     // should be used when eliding text.
188     if (position >= range.start()) {
189       text_font =
190           text_font.DeriveFont(0, style_ranges.top().style_info.font_style);
191     }
192     gfx::ElideRectangleText(remaining_string,
193                            text_font,
194                            chunk_bounds.width(),
195                            chunk_bounds.height(),
196                            gfx::IGNORE_LONG_WORDS,
197                            &substrings);
198
199     DCHECK(!substrings.empty());
200     string16 chunk = substrings[0];
201     if (chunk.empty()) {
202       // Nothing fits on this line. Start a new line.
203       // If x is 0, first line may have leading whitespace that doesn't fit in a
204       // single line, so try trimming those. Otherwise there is no room for
205       // anything; abort.
206       if (x == 0) {
207         if (line == 0) {
208           TrimWhitespace(remaining_string, TRIM_LEADING, &remaining_string);
209           continue;
210         }
211         break;
212       }
213
214       x = 0;
215       line++;
216       continue;
217     }
218
219     scoped_ptr<Label> label;
220     if (position >= range.start()) {
221       const RangeStyleInfo& style_info = style_ranges.top().style_info;
222
223       if (style_info.disable_line_wrapping && chunk.size() < range.length() &&
224           position == range.start() && x != 0) {
225         // If the chunk should not be wrapped, try to fit it entirely on the
226         // next line.
227         x = 0;
228         line++;
229         continue;
230       }
231
232       chunk = chunk.substr(0, std::min(chunk.size(), range.end() - position));
233
234       label = CreateLabelRange(chunk, style_info, this);
235
236       if (style_info.is_link && !dry_run)
237         link_targets_[label.get()] = range;
238
239       if (position + chunk.size() >= range.end())
240         style_ranges.pop();
241     } else {
242       // This chunk is normal text.
243       if (position + chunk.size() > range.start())
244         chunk = chunk.substr(0, range.start() - position);
245       label = CreateLabelRange(chunk, default_style_info_, this);
246     }
247
248     if (displayed_on_background_color_set_)
249       label->SetBackgroundColor(displayed_on_background_color_);
250     label->SetAutoColorReadabilityEnabled(auto_color_readability_enabled_);
251
252     // Lay out the views to overlap by 1 pixel to compensate for their border
253     // spacing. Otherwise, "<a>link</a>," will render as "link ,".
254     const int overlap = 1;
255     const gfx::Size view_size = label->GetPreferredSize();
256     DCHECK_EQ(line_height, view_size.height() - 2 * overlap);
257     if (!dry_run) {
258       label->SetBoundsRect(gfx::Rect(
259           gfx::Point(GetInsets().left() + x - overlap,
260                      GetInsets().top() + line * line_height - overlap),
261           view_size));
262       AddChildView(label.release());
263     }
264     x += view_size.width() - 2 * overlap;
265
266     remaining_string = remaining_string.substr(chunk.size());
267   }
268
269   return (line + 1) * line_height + GetInsets().height();
270 }
271
272 }  // namespace views