Add ICU message format support
[chromium-blink-merge.git] / ui / views / controls / styled_label.cc
bloba4c34b568e354f8c741b0ef0cce4a9223c989744
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 = Link::GetDefaultEnabledColor();
82 return result;
86 // StyledLabel::StyleRange ----------------------------------------------------
88 bool StyledLabel::StyleRange::operator<(
89 const StyledLabel::StyleRange& other) const {
90 return range.start() < other.range.start();
94 // StyledLabel ----------------------------------------------------------------
96 // static
97 const char StyledLabel::kViewClassName[] = "StyledLabel";
99 StyledLabel::StyledLabel(const base::string16& text,
100 StyledLabelListener* listener)
101 : specified_line_height_(0),
102 listener_(listener),
103 width_at_last_size_calculation_(0),
104 width_at_last_layout_(0),
105 displayed_on_background_color_(SkColorSetRGB(0xFF, 0xFF, 0xFF)),
106 displayed_on_background_color_set_(false),
107 auto_color_readability_enabled_(true) {
108 base::TrimWhitespace(text, base::TRIM_TRAILING, &text_);
111 StyledLabel::~StyledLabel() {}
113 void StyledLabel::SetText(const base::string16& text) {
114 text_ = text;
115 style_ranges_.clear();
116 RemoveAllChildViews(true);
117 PreferredSizeChanged();
120 void StyledLabel::SetBaseFontList(const gfx::FontList& font_list) {
121 font_list_ = font_list;
122 PreferredSizeChanged();
125 void StyledLabel::AddStyleRange(const gfx::Range& range,
126 const RangeStyleInfo& style_info) {
127 DCHECK(!range.is_reversed());
128 DCHECK(!range.is_empty());
129 DCHECK(gfx::Range(0, text_.size()).Contains(range));
131 // Insert the new range in sorted order.
132 StyleRanges new_range;
133 new_range.push_front(StyleRange(range, style_info));
134 style_ranges_.merge(new_range);
136 PreferredSizeChanged();
139 void StyledLabel::SetDefaultStyle(const RangeStyleInfo& style_info) {
140 default_style_info_ = style_info;
141 PreferredSizeChanged();
144 void StyledLabel::SetLineHeight(int line_height) {
145 specified_line_height_ = line_height;
146 PreferredSizeChanged();
149 void StyledLabel::SetDisplayedOnBackgroundColor(SkColor color) {
150 if (displayed_on_background_color_ == color &&
151 displayed_on_background_color_set_)
152 return;
154 displayed_on_background_color_ = color;
155 displayed_on_background_color_set_ = true;
157 for (int i = 0, count = child_count(); i < count; ++i) {
158 DCHECK((child_at(i)->GetClassName() == Label::kViewClassName) ||
159 (child_at(i)->GetClassName() == Link::kViewClassName));
160 static_cast<Label*>(child_at(i))->SetBackgroundColor(color);
164 void StyledLabel::SizeToFit(int max_width) {
165 if (max_width == 0)
166 max_width = std::numeric_limits<int>::max();
168 SetSize(CalculateAndDoLayout(max_width, true));
171 const char* StyledLabel::GetClassName() const {
172 return kViewClassName;
175 gfx::Insets StyledLabel::GetInsets() const {
176 gfx::Insets insets = View::GetInsets();
178 // We need a focus border iff we contain a link that will have a focus border.
179 // That in turn will be true only if the link is non-empty.
180 for (StyleRanges::const_iterator i(style_ranges_.begin());
181 i != style_ranges_.end(); ++i) {
182 if (i->style_info.is_link && !i->range.is_empty()) {
183 const gfx::Insets focus_border_padding(
184 Label::kFocusBorderPadding, Label::kFocusBorderPadding,
185 Label::kFocusBorderPadding, Label::kFocusBorderPadding);
186 insets += focus_border_padding;
187 break;
191 return insets;
194 gfx::Size StyledLabel::GetPreferredSize() const {
195 return calculated_size_;
198 int StyledLabel::GetHeightForWidth(int w) const {
199 // TODO(erg): Munge the const-ness of the style label. CalculateAndDoLayout
200 // doesn't actually make any changes to member variables when |dry_run| is
201 // set to true. In general, the mutating and non-mutating parts shouldn't
202 // be in the same codepath.
203 return const_cast<StyledLabel*>(this)->CalculateAndDoLayout(w, true).height();
206 void StyledLabel::Layout() {
207 CalculateAndDoLayout(GetLocalBounds().width(), false);
210 void StyledLabel::PreferredSizeChanged() {
211 calculated_size_ = gfx::Size();
212 width_at_last_size_calculation_ = 0;
213 width_at_last_layout_ = 0;
214 View::PreferredSizeChanged();
217 void StyledLabel::LinkClicked(Link* source, int event_flags) {
218 if (listener_)
219 listener_->StyledLabelLinkClicked(link_targets_[source], event_flags);
222 gfx::Size StyledLabel::CalculateAndDoLayout(int width, bool dry_run) {
223 if (width == width_at_last_size_calculation_ &&
224 (dry_run || width == width_at_last_layout_))
225 return calculated_size_;
227 width_at_last_size_calculation_ = width;
228 if (!dry_run)
229 width_at_last_layout_ = width;
231 width -= GetInsets().width();
233 if (!dry_run) {
234 RemoveAllChildViews(true);
235 link_targets_.clear();
238 if (width <= 0 || text_.empty())
239 return gfx::Size();
241 const int line_height = specified_line_height_ > 0 ? specified_line_height_
242 : CalculateLineHeight(font_list_);
243 // The index of the line we're on.
244 int line = 0;
245 // The x position (in pixels) of the line we're on, relative to content
246 // bounds.
247 int x = 0;
248 // The width that was actually used. Guaranteed to be no larger than |width|.
249 int used_width = 0;
251 base::string16 remaining_string = text_;
252 StyleRanges::const_iterator current_range = style_ranges_.begin();
254 // Iterate over the text, creating a bunch of labels and links and laying them
255 // out in the appropriate positions.
256 while (!remaining_string.empty()) {
257 // Don't put whitespace at beginning of a line with an exception for the
258 // first line (so the text's leading whitespace is respected).
259 if (x == 0 && line > 0) {
260 base::TrimWhitespace(remaining_string, base::TRIM_LEADING,
261 &remaining_string);
264 gfx::Range range(gfx::Range::InvalidRange());
265 if (current_range != style_ranges_.end())
266 range = current_range->range;
268 const size_t position = text_.size() - remaining_string.size();
270 const gfx::Rect chunk_bounds(x, 0, width - x, 2 * line_height);
271 std::vector<base::string16> substrings;
272 gfx::FontList text_font_list = font_list_;
273 // If the start of the remaining text is inside a styled range, the font
274 // style may differ from the base font. The font specified by the range
275 // should be used when eliding text.
276 if (position >= range.start()) {
277 text_font_list = text_font_list.DeriveWithStyle(
278 current_range->style_info.font_style);
280 gfx::ElideRectangleText(remaining_string,
281 text_font_list,
282 chunk_bounds.width(),
283 chunk_bounds.height(),
284 gfx::WRAP_LONG_WORDS,
285 &substrings);
287 if (substrings.empty() || substrings[0].empty()) {
288 // Nothing fits on this line. Start a new line.
289 // If x is 0, first line may have leading whitespace that doesn't fit in a
290 // single line, so try trimming those. Otherwise there is no room for
291 // anything; abort.
292 if (x == 0) {
293 if (line == 0) {
294 base::TrimWhitespace(remaining_string, base::TRIM_LEADING,
295 &remaining_string);
296 continue;
298 break;
301 x = 0;
302 line++;
303 continue;
306 base::string16 chunk = substrings[0];
308 scoped_ptr<Label> label;
309 if (position >= range.start()) {
310 const RangeStyleInfo& style_info = current_range->style_info;
312 if (style_info.disable_line_wrapping && chunk.size() < range.length() &&
313 position == range.start() && x != 0) {
314 // If the chunk should not be wrapped, try to fit it entirely on the
315 // next line.
316 x = 0;
317 line++;
318 continue;
321 if (chunk.size() > range.end() - position)
322 chunk = chunk.substr(0, range.end() - position);
324 label = CreateLabelRange(chunk, font_list_, style_info, this);
326 if (style_info.is_link && !dry_run)
327 link_targets_[label.get()] = range;
329 if (position + chunk.size() >= range.end())
330 ++current_range;
331 } else {
332 // This chunk is normal text.
333 if (position + chunk.size() > range.start())
334 chunk = chunk.substr(0, range.start() - position);
335 label = CreateLabelRange(chunk, font_list_, default_style_info_, this);
338 if (displayed_on_background_color_set_)
339 label->SetBackgroundColor(displayed_on_background_color_);
340 label->SetAutoColorReadabilityEnabled(auto_color_readability_enabled_);
342 // Calculate the size of the optional focus border, and overlap by that
343 // amount. Otherwise, "<a>link</a>," will render as "link ,".
344 gfx::Insets focus_border_insets(label->GetInsets());
345 focus_border_insets += -label->View::GetInsets();
346 const gfx::Size view_size = label->GetPreferredSize();
347 if (!dry_run) {
348 label->SetBoundsRect(gfx::Rect(
349 gfx::Point(GetInsets().left() + x - focus_border_insets.left(),
350 GetInsets().top() + line * line_height -
351 focus_border_insets.top()),
352 view_size));
353 AddChildView(label.release());
355 x += view_size.width() - focus_border_insets.width();
356 used_width = std::max(used_width, x);
358 // If |gfx::ElideRectangleText| returned more than one substring, that
359 // means the whole text did not fit into remaining line width, with text
360 // after |susbtring[0]| spilling into next line. If whole |substring[0]|
361 // was added to the current line (this may not be the case if part of the
362 // substring has different style), proceed to the next line.
363 if (substrings.size() > 1 && chunk.size() == substrings[0].size()) {
364 x = 0;
365 ++line;
368 remaining_string = remaining_string.substr(chunk.size());
371 DCHECK_LE(used_width, width);
372 // The user-specified line height only applies to interline spacing, so the
373 // final line's height is unaffected.
374 int total_height = line * line_height +
375 CalculateLineHeight(font_list_) + GetInsets().height();
376 calculated_size_ = gfx::Size(used_width + GetInsets().width(), total_height);
377 return calculated_size_;
380 } // namespace views