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 : listener_(listener
),
98 displayed_on_background_color_set_(false),
99 auto_color_readability_enabled_(true) {
100 base::TrimWhitespace(text
, base::TRIM_TRAILING
, &text_
);
103 StyledLabel::~StyledLabel() {}
105 void StyledLabel::SetText(const base::string16
& text
) {
107 style_ranges_
.clear();
108 RemoveAllChildViews(true);
109 PreferredSizeChanged();
112 void StyledLabel::SetBaseFontList(const gfx::FontList
& font_list
) {
113 font_list_
= font_list
;
114 PreferredSizeChanged();
117 void StyledLabel::AddStyleRange(const gfx::Range
& range
,
118 const RangeStyleInfo
& style_info
) {
119 DCHECK(!range
.is_reversed());
120 DCHECK(!range
.is_empty());
121 DCHECK(gfx::Range(0, text_
.size()).Contains(range
));
123 // Insert the new range in sorted order.
124 StyleRanges new_range
;
125 new_range
.push_front(StyleRange(range
, style_info
));
126 style_ranges_
.merge(new_range
);
128 PreferredSizeChanged();
131 void StyledLabel::SetDefaultStyle(const RangeStyleInfo
& style_info
) {
132 default_style_info_
= style_info
;
133 PreferredSizeChanged();
136 void StyledLabel::SetDisplayedOnBackgroundColor(SkColor color
) {
137 displayed_on_background_color_
= color
;
138 displayed_on_background_color_set_
= true;
141 gfx::Insets
StyledLabel::GetInsets() const {
142 gfx::Insets insets
= View::GetInsets();
144 // We need a focus border iff we contain a link that will have a focus border.
145 // That in turn will be true only if the link is non-empty.
146 for (StyleRanges::const_iterator
i(style_ranges_
.begin());
147 i
!= style_ranges_
.end(); ++i
) {
148 if (i
->style_info
.is_link
&& !i
->range
.is_empty()) {
149 const gfx::Insets
focus_border_padding(
150 Label::kFocusBorderPadding
, Label::kFocusBorderPadding
,
151 Label::kFocusBorderPadding
, Label::kFocusBorderPadding
);
152 insets
+= focus_border_padding
;
160 int StyledLabel::GetHeightForWidth(int w
) const {
161 if (w
!= calculated_size_
.width()) {
162 // TODO(erg): Munge the const-ness of the style label. CalculateAndDoLayout
163 // doesn't actually make any changes to member variables when |dry_run| is
164 // set to true. In general, the mutating and non-mutating parts shouldn't
165 // be in the same codepath.
167 const_cast<StyledLabel
*>(this)->CalculateAndDoLayout(w
, true);
169 return calculated_size_
.height();
172 void StyledLabel::Layout() {
173 calculated_size_
= CalculateAndDoLayout(GetLocalBounds().width(), false);
176 void StyledLabel::PreferredSizeChanged() {
177 calculated_size_
= gfx::Size();
178 View::PreferredSizeChanged();
181 void StyledLabel::LinkClicked(Link
* source
, int event_flags
) {
183 listener_
->StyledLabelLinkClicked(link_targets_
[source
], event_flags
);
186 gfx::Size
StyledLabel::CalculateAndDoLayout(int width
, bool dry_run
) {
188 RemoveAllChildViews(true);
189 link_targets_
.clear();
192 width
-= GetInsets().width();
193 if (width
<= 0 || text_
.empty())
196 const int line_height
= CalculateLineHeight(font_list_
);
197 // The index of the line we're on.
199 // The x position (in pixels) of the line we're on, relative to content
203 base::string16 remaining_string
= text_
;
204 StyleRanges::const_iterator current_range
= style_ranges_
.begin();
206 // Iterate over the text, creating a bunch of labels and links and laying them
207 // out in the appropriate positions.
208 while (!remaining_string
.empty()) {
209 // Don't put whitespace at beginning of a line with an exception for the
210 // first line (so the text's leading whitespace is respected).
211 if (x
== 0 && line
> 0) {
212 base::TrimWhitespace(remaining_string
, base::TRIM_LEADING
,
216 gfx::Range
range(gfx::Range::InvalidRange());
217 if (current_range
!= style_ranges_
.end())
218 range
= current_range
->range
;
220 const size_t position
= text_
.size() - remaining_string
.size();
222 const gfx::Rect
chunk_bounds(x
, 0, width
- x
, 2 * line_height
);
223 std::vector
<base::string16
> substrings
;
224 gfx::FontList text_font_list
= font_list_
;
225 // If the start of the remaining text is inside a styled range, the font
226 // style may differ from the base font. The font specified by the range
227 // should be used when eliding text.
228 if (position
>= range
.start()) {
229 text_font_list
= text_font_list
.DeriveWithStyle(
230 current_range
->style_info
.font_style
);
232 gfx::ElideRectangleText(remaining_string
,
234 chunk_bounds
.width(),
235 chunk_bounds
.height(),
236 gfx::IGNORE_LONG_WORDS
,
239 DCHECK(!substrings
.empty());
240 base::string16 chunk
= substrings
[0];
242 // Nothing fits on this line. Start a new line.
243 // If x is 0, first line may have leading whitespace that doesn't fit in a
244 // single line, so try trimming those. Otherwise there is no room for
248 base::TrimWhitespace(remaining_string
, base::TRIM_LEADING
,
260 scoped_ptr
<Label
> label
;
261 if (position
>= range
.start()) {
262 const RangeStyleInfo
& style_info
= current_range
->style_info
;
264 if (style_info
.disable_line_wrapping
&& chunk
.size() < range
.length() &&
265 position
== range
.start() && x
!= 0) {
266 // If the chunk should not be wrapped, try to fit it entirely on the
273 chunk
= chunk
.substr(0, std::min(chunk
.size(), range
.end() - position
));
275 label
= CreateLabelRange(chunk
, font_list_
, style_info
, this);
277 if (style_info
.is_link
&& !dry_run
)
278 link_targets_
[label
.get()] = range
;
280 if (position
+ chunk
.size() >= range
.end())
283 // This chunk is normal text.
284 if (position
+ chunk
.size() > range
.start())
285 chunk
= chunk
.substr(0, range
.start() - position
);
286 label
= CreateLabelRange(chunk
, font_list_
, default_style_info_
, this);
289 if (displayed_on_background_color_set_
)
290 label
->SetBackgroundColor(displayed_on_background_color_
);
291 label
->SetAutoColorReadabilityEnabled(auto_color_readability_enabled_
);
293 // Calculate the size of the optional focus border, and overlap by that
294 // amount. Otherwise, "<a>link</a>," will render as "link ,".
295 gfx::Insets
focus_border_insets(label
->GetInsets());
296 focus_border_insets
+= -label
->View::GetInsets();
297 const gfx::Size view_size
= label
->GetPreferredSize();
298 DCHECK_EQ(line_height
, view_size
.height() - focus_border_insets
.height());
300 label
->SetBoundsRect(gfx::Rect(
301 gfx::Point(GetInsets().left() + x
- focus_border_insets
.left(),
302 GetInsets().top() + line
* line_height
-
303 focus_border_insets
.top()),
305 AddChildView(label
.release());
307 x
+= view_size
.width() - focus_border_insets
.width();
309 remaining_string
= remaining_string
.substr(chunk
.size());
312 return gfx::Size(width
, (line
+ 1) * line_height
+ GetInsets().height());