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
= Link::GetDefaultEnabledColor();
86 // StyledLabel::StyleRange ----------------------------------------------------
88 bool StyledLabel::StyleRange::operator<(
89 const StyledLabel::StyleRange
& other
) const {
90 return range
.start() < other
.range
.start();
94 // StyledLabel ----------------------------------------------------------------
97 const char StyledLabel::kViewClassName
[] = "StyledLabel";
99 StyledLabel::StyledLabel(const base::string16
& text
,
100 StyledLabelListener
* listener
)
101 : specified_line_height_(0),
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
) {
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_
)
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
) {
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
;
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
) {
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
;
229 width_at_last_layout_
= width
;
231 width
-= GetInsets().width();
234 RemoveAllChildViews(true);
235 link_targets_
.clear();
238 if (width
<= 0 || text_
.empty())
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.
245 // The x position (in pixels) of the line we're on, relative to content
248 // The width that was actually used. Guaranteed to be no larger than |width|.
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
,
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
,
282 chunk_bounds
.width(),
283 chunk_bounds
.height(),
284 gfx::WRAP_LONG_WORDS
,
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
294 base::TrimWhitespace(remaining_string
, base::TRIM_LEADING
,
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
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())
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();
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()),
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()) {
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_
;