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"
9 #include "base/strings/string_util.h"
10 #include "ui/gfx/font_list.h"
11 #include "ui/gfx/text_elider.h"
12 #include "ui/native_theme/native_theme.h"
13 #include "ui/views/controls/label.h"
14 #include "ui/views/controls/link.h"
15 #include "ui/views/controls/styled_label_listener.h"
20 // Helpers --------------------------------------------------------------------
24 // Calculates the height of a line of text. Currently returns the height of
26 int CalculateLineHeight(const gfx::FontList
& font_list
) {
28 label
.SetFontList(font_list
);
29 return label
.GetPreferredSize().height();
32 scoped_ptr
<Label
> CreateLabelRange(
33 const base::string16
& text
,
34 const gfx::FontList
& font_list
,
35 const StyledLabel::RangeStyleInfo
& style_info
,
36 views::LinkListener
* link_listener
) {
37 scoped_ptr
<Label
> result
;
39 if (style_info
.is_link
) {
40 Link
* link
= new Link(text
);
41 link
->set_listener(link_listener
);
42 link
->SetUnderline((style_info
.font_style
& gfx::Font::UNDERLINE
) != 0);
45 result
.reset(new Label(text
));
48 result
->SetEnabledColor(style_info
.color
);
49 result
->SetFontList(font_list
);
51 if (!style_info
.tooltip
.empty())
52 result
->SetTooltipText(style_info
.tooltip
);
53 if (style_info
.font_style
!= gfx::Font::NORMAL
) {
55 result
->font_list().DeriveWithStyle(style_info
.font_style
));
64 // StyledLabel::RangeStyleInfo ------------------------------------------------
66 StyledLabel::RangeStyleInfo::RangeStyleInfo()
67 : font_style(gfx::Font::NORMAL
),
68 color(ui::NativeTheme::instance()->GetSystemColor(
69 ui::NativeTheme::kColorId_LabelEnabledColor
)),
70 disable_line_wrapping(false),
73 StyledLabel::RangeStyleInfo::~RangeStyleInfo() {}
76 StyledLabel::RangeStyleInfo
StyledLabel::RangeStyleInfo::CreateForLink() {
77 RangeStyleInfo result
;
78 result
.disable_line_wrapping
= true;
79 result
.is_link
= true;
80 result
.color
= Link::GetDefaultEnabledColor();
85 // StyledLabel::StyleRange ----------------------------------------------------
87 bool StyledLabel::StyleRange::operator<(
88 const StyledLabel::StyleRange
& other
) const {
89 return range
.start() < other
.range
.start();
93 // StyledLabel ----------------------------------------------------------------
95 StyledLabel::StyledLabel(const base::string16
& text
,
96 StyledLabelListener
* listener
)
97 : specified_line_height_(0),
99 displayed_on_background_color_set_(false),
100 auto_color_readability_enabled_(true) {
101 base::TrimWhitespace(text
, base::TRIM_TRAILING
, &text_
);
104 StyledLabel::~StyledLabel() {}
106 void StyledLabel::SetText(const base::string16
& text
) {
108 style_ranges_
.clear();
109 RemoveAllChildViews(true);
110 PreferredSizeChanged();
113 void StyledLabel::SetBaseFontList(const gfx::FontList
& font_list
) {
114 font_list_
= font_list
;
115 PreferredSizeChanged();
118 void StyledLabel::AddStyleRange(const gfx::Range
& range
,
119 const RangeStyleInfo
& style_info
) {
120 DCHECK(!range
.is_reversed());
121 DCHECK(!range
.is_empty());
122 DCHECK(gfx::Range(0, text_
.size()).Contains(range
));
124 // Insert the new range in sorted order.
125 StyleRanges new_range
;
126 new_range
.push_front(StyleRange(range
, style_info
));
127 style_ranges_
.merge(new_range
);
129 PreferredSizeChanged();
132 void StyledLabel::SetDefaultStyle(const RangeStyleInfo
& style_info
) {
133 default_style_info_
= style_info
;
134 PreferredSizeChanged();
137 void StyledLabel::SetLineHeight(int line_height
) {
138 specified_line_height_
= line_height
;
139 PreferredSizeChanged();
142 void StyledLabel::SetDisplayedOnBackgroundColor(SkColor color
) {
143 displayed_on_background_color_
= color
;
144 displayed_on_background_color_set_
= true;
147 gfx::Insets
StyledLabel::GetInsets() const {
148 gfx::Insets insets
= View::GetInsets();
150 // We need a focus border iff we contain a link that will have a focus border.
151 // That in turn will be true only if the link is non-empty.
152 for (StyleRanges::const_iterator
i(style_ranges_
.begin());
153 i
!= style_ranges_
.end(); ++i
) {
154 if (i
->style_info
.is_link
&& !i
->range
.is_empty()) {
155 const gfx::Insets
focus_border_padding(
156 Label::kFocusBorderPadding
, Label::kFocusBorderPadding
,
157 Label::kFocusBorderPadding
, Label::kFocusBorderPadding
);
158 insets
+= focus_border_padding
;
166 int StyledLabel::GetHeightForWidth(int w
) const {
167 if (w
!= calculated_size_
.width()) {
168 // TODO(erg): Munge the const-ness of the style label. CalculateAndDoLayout
169 // doesn't actually make any changes to member variables when |dry_run| is
170 // set to true. In general, the mutating and non-mutating parts shouldn't
171 // be in the same codepath.
173 const_cast<StyledLabel
*>(this)->CalculateAndDoLayout(w
, true);
175 return calculated_size_
.height();
178 void StyledLabel::Layout() {
179 calculated_size_
= CalculateAndDoLayout(GetLocalBounds().width(), false);
182 void StyledLabel::PreferredSizeChanged() {
183 calculated_size_
= gfx::Size();
184 View::PreferredSizeChanged();
187 void StyledLabel::LinkClicked(Link
* source
, int event_flags
) {
189 listener_
->StyledLabelLinkClicked(link_targets_
[source
], event_flags
);
192 gfx::Size
StyledLabel::CalculateAndDoLayout(int width
, bool dry_run
) {
194 RemoveAllChildViews(true);
195 link_targets_
.clear();
198 width
-= GetInsets().width();
199 if (width
<= 0 || text_
.empty())
202 const int line_height
= specified_line_height_
> 0 ? specified_line_height_
203 : CalculateLineHeight(font_list_
);
204 // The index of the line we're on.
206 // The x position (in pixels) of the line we're on, relative to content
210 base::string16 remaining_string
= text_
;
211 StyleRanges::const_iterator current_range
= style_ranges_
.begin();
213 // Iterate over the text, creating a bunch of labels and links and laying them
214 // out in the appropriate positions.
215 while (!remaining_string
.empty()) {
216 // Don't put whitespace at beginning of a line with an exception for the
217 // first line (so the text's leading whitespace is respected).
218 if (x
== 0 && line
> 0) {
219 base::TrimWhitespace(remaining_string
, base::TRIM_LEADING
,
223 gfx::Range
range(gfx::Range::InvalidRange());
224 if (current_range
!= style_ranges_
.end())
225 range
= current_range
->range
;
227 const size_t position
= text_
.size() - remaining_string
.size();
229 const gfx::Rect
chunk_bounds(x
, 0, width
- x
, 2 * line_height
);
230 std::vector
<base::string16
> substrings
;
231 gfx::FontList text_font_list
= font_list_
;
232 // If the start of the remaining text is inside a styled range, the font
233 // style may differ from the base font. The font specified by the range
234 // should be used when eliding text.
235 if (position
>= range
.start()) {
236 text_font_list
= text_font_list
.DeriveWithStyle(
237 current_range
->style_info
.font_style
);
239 gfx::ElideRectangleText(remaining_string
,
241 chunk_bounds
.width(),
242 chunk_bounds
.height(),
243 gfx::IGNORE_LONG_WORDS
,
246 DCHECK(!substrings
.empty());
247 base::string16 chunk
= substrings
[0];
249 // Nothing fits on this line. Start a new line.
250 // If x is 0, first line may have leading whitespace that doesn't fit in a
251 // single line, so try trimming those. Otherwise there is no room for
255 base::TrimWhitespace(remaining_string
, base::TRIM_LEADING
,
267 scoped_ptr
<Label
> label
;
268 if (position
>= range
.start()) {
269 const RangeStyleInfo
& style_info
= current_range
->style_info
;
271 if (style_info
.disable_line_wrapping
&& chunk
.size() < range
.length() &&
272 position
== range
.start() && x
!= 0) {
273 // If the chunk should not be wrapped, try to fit it entirely on the
280 chunk
= chunk
.substr(0, std::min(chunk
.size(), range
.end() - position
));
282 label
= CreateLabelRange(chunk
, font_list_
, style_info
, this);
284 if (style_info
.is_link
&& !dry_run
)
285 link_targets_
[label
.get()] = range
;
287 if (position
+ chunk
.size() >= range
.end())
290 // This chunk is normal text.
291 if (position
+ chunk
.size() > range
.start())
292 chunk
= chunk
.substr(0, range
.start() - position
);
293 label
= CreateLabelRange(chunk
, font_list_
, default_style_info_
, this);
296 if (displayed_on_background_color_set_
)
297 label
->SetBackgroundColor(displayed_on_background_color_
);
298 label
->SetAutoColorReadabilityEnabled(auto_color_readability_enabled_
);
300 // Calculate the size of the optional focus border, and overlap by that
301 // amount. Otherwise, "<a>link</a>," will render as "link ,".
302 gfx::Insets
focus_border_insets(label
->GetInsets());
303 focus_border_insets
+= -label
->View::GetInsets();
304 const gfx::Size view_size
= label
->GetPreferredSize();
306 label
->SetBoundsRect(gfx::Rect(
307 gfx::Point(GetInsets().left() + x
- focus_border_insets
.left(),
308 GetInsets().top() + line
* line_height
-
309 focus_border_insets
.top()),
311 AddChildView(label
.release());
313 x
+= view_size
.width() - focus_border_insets
.width();
315 remaining_string
= remaining_string
.substr(chunk
.size());
318 // The user-specified line height only applies to interline spacing, so the
319 // final line's height is unaffected.
320 int total_height
= line
* line_height
+
321 CalculateLineHeight(font_list_
) + GetInsets().height();
322 return gfx::Size(width
, total_height
);