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"
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"
21 // Helpers --------------------------------------------------------------------
25 // Calculates the height of a line of text. Currently returns the height of
27 int CalculateLineHeight(const gfx::FontList
& font_list
) {
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);
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
) {
56 result
->font_list().DeriveWithStyle(style_info
.font_style
));
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),
74 StyledLabel::RangeStyleInfo::~RangeStyleInfo() {}
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
);
87 // StyledLabel::StyleRange ----------------------------------------------------
89 bool StyledLabel::StyleRange::operator<(
90 const StyledLabel::StyleRange
& other
) const {
91 return range
.start() < other
.range
.start();
95 // StyledLabel ----------------------------------------------------------------
98 const char StyledLabel::kViewClassName
[] = "StyledLabel";
100 StyledLabel::StyledLabel(const base::string16
& text
,
101 StyledLabelListener
* listener
)
102 : specified_line_height_(0),
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
) {
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_
)
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
) {
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
;
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
) {
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
;
230 width_at_last_layout_
= width
;
232 width
-= GetInsets().width();
235 RemoveAllChildViews(true);
236 link_targets_
.clear();
239 if (width
<= 0 || text_
.empty())
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.
246 // The x position (in pixels) of the line we're on, relative to content
249 // The width that was actually used. Guaranteed to be no larger than |width|.
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
,
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
,
283 chunk_bounds
.width(),
284 chunk_bounds
.height(),
285 gfx::WRAP_LONG_WORDS
,
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
295 base::TrimWhitespace(remaining_string
, base::TRIM_LEADING
,
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
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())
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();
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()),
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()) {
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_
;