Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / views / controls / styled_label.cc
blob66a820f04f52a7bda44895e38f07c452bc3d1135
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.
5 #include "ui/views/controls/styled_label.h"
7 #include <limits>
8 #include <vector>
10 #include "base/strings/string_util.h"
11 #include "ui/gfx/font_list.h"
12 #include "ui/gfx/text_elider.h"
13 #include "ui/native_theme/native_theme.h"
14 #include "ui/views/controls/label.h"
15 #include "ui/views/controls/link.h"
16 #include "ui/views/controls/styled_label_listener.h"
18 namespace views {
21 // Helpers --------------------------------------------------------------------
23 namespace {
25 // Calculates the height of a line of text. Currently returns the height of
26 // a label.
27 int CalculateLineHeight(const gfx::FontList& font_list) {
28 Label label;
29 label.SetFontList(font_list);
30 return label.GetPreferredSize().height();
33 scoped_ptr<Label> CreateLabelRange(
34 const base::string16& text,
35 const gfx::FontList& font_list,
36 const StyledLabel::RangeStyleInfo& style_info,
37 views::LinkListener* link_listener) {
38 scoped_ptr<Label> result;
40 if (style_info.is_link) {
41 Link* link = new Link(text);
42 link->set_listener(link_listener);
43 link->SetUnderline((style_info.font_style & gfx::Font::UNDERLINE) != 0);
44 result.reset(link);
45 } else {
46 result.reset(new Label(text));
49 result->SetEnabledColor(style_info.color);
50 result->SetFontList(font_list);
52 if (!style_info.tooltip.empty())
53 result->SetTooltipText(style_info.tooltip);
54 if (style_info.font_style != gfx::Font::NORMAL) {
55 result->SetFontList(
56 result->font_list().DeriveWithStyle(style_info.font_style));
59 return result.Pass();
62 } // namespace
65 // StyledLabel::RangeStyleInfo ------------------------------------------------
67 StyledLabel::RangeStyleInfo::RangeStyleInfo()
68 : font_style(gfx::Font::NORMAL),
69 color(ui::NativeTheme::instance()->GetSystemColor(
70 ui::NativeTheme::kColorId_LabelEnabledColor)),
71 disable_line_wrapping(false),
72 is_link(false) {}
74 StyledLabel::RangeStyleInfo::~RangeStyleInfo() {}
76 // static
77 StyledLabel::RangeStyleInfo StyledLabel::RangeStyleInfo::CreateForLink() {
78 RangeStyleInfo result;
79 result.disable_line_wrapping = true;
80 result.is_link = true;
81 result.color = ui::NativeTheme::instance()->GetSystemColor(
82 ui::NativeTheme::kColorId_LinkEnabled);
83 return result;
87 // StyledLabel::StyleRange ----------------------------------------------------
89 bool StyledLabel::StyleRange::operator<(
90 const StyledLabel::StyleRange& other) const {
91 return range.start() < other.range.start();
95 // StyledLabel ----------------------------------------------------------------
97 // static
98 const char StyledLabel::kViewClassName[] = "StyledLabel";
100 StyledLabel::StyledLabel(const base::string16& text,
101 StyledLabelListener* listener)
102 : specified_line_height_(0),
103 listener_(listener),
104 width_at_last_size_calculation_(0),
105 width_at_last_layout_(0),
106 displayed_on_background_color_(SkColorSetRGB(0xFF, 0xFF, 0xFF)),
107 displayed_on_background_color_set_(false),
108 auto_color_readability_enabled_(true) {
109 base::TrimWhitespace(text, base::TRIM_TRAILING, &text_);
112 StyledLabel::~StyledLabel() {}
114 void StyledLabel::SetText(const base::string16& text) {
115 text_ = text;
116 style_ranges_.clear();
117 RemoveAllChildViews(true);
118 PreferredSizeChanged();
121 void StyledLabel::SetBaseFontList(const gfx::FontList& font_list) {
122 font_list_ = font_list;
123 PreferredSizeChanged();
126 void StyledLabel::AddStyleRange(const gfx::Range& range,
127 const RangeStyleInfo& style_info) {
128 DCHECK(!range.is_reversed());
129 DCHECK(!range.is_empty());
130 DCHECK(gfx::Range(0, text_.size()).Contains(range));
132 // Insert the new range in sorted order.
133 StyleRanges new_range;
134 new_range.push_front(StyleRange(range, style_info));
135 style_ranges_.merge(new_range);
137 PreferredSizeChanged();
140 void StyledLabel::SetDefaultStyle(const RangeStyleInfo& style_info) {
141 default_style_info_ = style_info;
142 PreferredSizeChanged();
145 void StyledLabel::SetLineHeight(int line_height) {
146 specified_line_height_ = line_height;
147 PreferredSizeChanged();
150 void StyledLabel::SetDisplayedOnBackgroundColor(SkColor color) {
151 if (displayed_on_background_color_ == color &&
152 displayed_on_background_color_set_)
153 return;
155 displayed_on_background_color_ = color;
156 displayed_on_background_color_set_ = true;
158 for (int i = 0, count = child_count(); i < count; ++i) {
159 DCHECK((child_at(i)->GetClassName() == Label::kViewClassName) ||
160 (child_at(i)->GetClassName() == Link::kViewClassName));
161 static_cast<Label*>(child_at(i))->SetBackgroundColor(color);
165 void StyledLabel::SizeToFit(int max_width) {
166 if (max_width == 0)
167 max_width = std::numeric_limits<int>::max();
169 SetSize(CalculateAndDoLayout(max_width, true));
172 const char* StyledLabel::GetClassName() const {
173 return kViewClassName;
176 gfx::Insets StyledLabel::GetInsets() const {
177 gfx::Insets insets = View::GetInsets();
179 // We need a focus border iff we contain a link that will have a focus border.
180 // That in turn will be true only if the link is non-empty.
181 for (StyleRanges::const_iterator i(style_ranges_.begin());
182 i != style_ranges_.end(); ++i) {
183 if (i->style_info.is_link && !i->range.is_empty()) {
184 const gfx::Insets focus_border_padding(
185 Label::kFocusBorderPadding, Label::kFocusBorderPadding,
186 Label::kFocusBorderPadding, Label::kFocusBorderPadding);
187 insets += focus_border_padding;
188 break;
192 return insets;
195 gfx::Size StyledLabel::GetPreferredSize() const {
196 return calculated_size_;
199 int StyledLabel::GetHeightForWidth(int w) const {
200 // TODO(erg): Munge the const-ness of the style label. CalculateAndDoLayout
201 // doesn't actually make any changes to member variables when |dry_run| is
202 // set to true. In general, the mutating and non-mutating parts shouldn't
203 // be in the same codepath.
204 return const_cast<StyledLabel*>(this)->CalculateAndDoLayout(w, true).height();
207 void StyledLabel::Layout() {
208 CalculateAndDoLayout(GetLocalBounds().width(), false);
211 void StyledLabel::PreferredSizeChanged() {
212 calculated_size_ = gfx::Size();
213 width_at_last_size_calculation_ = 0;
214 width_at_last_layout_ = 0;
215 View::PreferredSizeChanged();
218 void StyledLabel::LinkClicked(Link* source, int event_flags) {
219 if (listener_)
220 listener_->StyledLabelLinkClicked(link_targets_[source], event_flags);
223 gfx::Size StyledLabel::CalculateAndDoLayout(int width, bool dry_run) {
224 if (width == width_at_last_size_calculation_ &&
225 (dry_run || width == width_at_last_layout_))
226 return calculated_size_;
228 width_at_last_size_calculation_ = width;
229 if (!dry_run)
230 width_at_last_layout_ = width;
232 width -= GetInsets().width();
234 if (!dry_run) {
235 RemoveAllChildViews(true);
236 link_targets_.clear();
239 if (width <= 0 || text_.empty())
240 return gfx::Size();
242 const int line_height = specified_line_height_ > 0 ? specified_line_height_
243 : CalculateLineHeight(font_list_);
244 // The index of the line we're on.
245 int line = 0;
246 // The x position (in pixels) of the line we're on, relative to content
247 // bounds.
248 int x = 0;
249 // The width that was actually used. Guaranteed to be no larger than |width|.
250 int used_width = 0;
252 base::string16 remaining_string = text_;
253 StyleRanges::const_iterator current_range = style_ranges_.begin();
255 // Iterate over the text, creating a bunch of labels and links and laying them
256 // out in the appropriate positions.
257 while (!remaining_string.empty()) {
258 // Don't put whitespace at beginning of a line with an exception for the
259 // first line (so the text's leading whitespace is respected).
260 if (x == 0 && line > 0) {
261 base::TrimWhitespace(remaining_string, base::TRIM_LEADING,
262 &remaining_string);
265 gfx::Range range(gfx::Range::InvalidRange());
266 if (current_range != style_ranges_.end())
267 range = current_range->range;
269 const size_t position = text_.size() - remaining_string.size();
271 const gfx::Rect chunk_bounds(x, 0, width - x, 2 * line_height);
272 std::vector<base::string16> substrings;
273 gfx::FontList text_font_list = font_list_;
274 // If the start of the remaining text is inside a styled range, the font
275 // style may differ from the base font. The font specified by the range
276 // should be used when eliding text.
277 if (position >= range.start()) {
278 text_font_list = text_font_list.DeriveWithStyle(
279 current_range->style_info.font_style);
281 gfx::ElideRectangleText(remaining_string,
282 text_font_list,
283 chunk_bounds.width(),
284 chunk_bounds.height(),
285 gfx::WRAP_LONG_WORDS,
286 &substrings);
288 if (substrings.empty() || substrings[0].empty()) {
289 // Nothing fits on this line. Start a new line.
290 // If x is 0, first line may have leading whitespace that doesn't fit in a
291 // single line, so try trimming those. Otherwise there is no room for
292 // anything; abort.
293 if (x == 0) {
294 if (line == 0) {
295 base::TrimWhitespace(remaining_string, base::TRIM_LEADING,
296 &remaining_string);
297 continue;
299 break;
302 x = 0;
303 line++;
304 continue;
307 base::string16 chunk = substrings[0];
309 scoped_ptr<Label> label;
310 if (position >= range.start()) {
311 const RangeStyleInfo& style_info = current_range->style_info;
313 if (style_info.disable_line_wrapping && chunk.size() < range.length() &&
314 position == range.start() && x != 0) {
315 // If the chunk should not be wrapped, try to fit it entirely on the
316 // next line.
317 x = 0;
318 line++;
319 continue;
322 if (chunk.size() > range.end() - position)
323 chunk = chunk.substr(0, range.end() - position);
325 label = CreateLabelRange(chunk, font_list_, style_info, this);
327 if (style_info.is_link && !dry_run)
328 link_targets_[label.get()] = range;
330 if (position + chunk.size() >= range.end())
331 ++current_range;
332 } else {
333 // This chunk is normal text.
334 if (position + chunk.size() > range.start())
335 chunk = chunk.substr(0, range.start() - position);
336 label = CreateLabelRange(chunk, font_list_, default_style_info_, this);
339 if (displayed_on_background_color_set_)
340 label->SetBackgroundColor(displayed_on_background_color_);
341 label->SetAutoColorReadabilityEnabled(auto_color_readability_enabled_);
343 // Calculate the size of the optional focus border, and overlap by that
344 // amount. Otherwise, "<a>link</a>," will render as "link ,".
345 gfx::Insets focus_border_insets(label->GetInsets());
346 focus_border_insets += -label->View::GetInsets();
347 const gfx::Size view_size = label->GetPreferredSize();
348 if (!dry_run) {
349 label->SetBoundsRect(gfx::Rect(
350 gfx::Point(GetInsets().left() + x - focus_border_insets.left(),
351 GetInsets().top() + line * line_height -
352 focus_border_insets.top()),
353 view_size));
354 AddChildView(label.release());
356 x += view_size.width() - focus_border_insets.width();
357 used_width = std::max(used_width, x);
359 // If |gfx::ElideRectangleText| returned more than one substring, that
360 // means the whole text did not fit into remaining line width, with text
361 // after |susbtring[0]| spilling into next line. If whole |substring[0]|
362 // was added to the current line (this may not be the case if part of the
363 // substring has different style), proceed to the next line.
364 if (substrings.size() > 1 && chunk.size() == substrings[0].size()) {
365 x = 0;
366 ++line;
369 remaining_string = remaining_string.substr(chunk.size());
372 DCHECK_LE(used_width, width);
373 // The user-specified line height only applies to interline spacing, so the
374 // final line's height is unaffected.
375 int total_height = line * line_height +
376 CalculateLineHeight(font_list_) + GetInsets().height();
377 calculated_size_ = gfx::Size(used_width + GetInsets().width(), total_height);
378 return calculated_size_;
381 } // namespace views