• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4 
5 #include "ui/views/controls/styled_label.h"
6 
7 #include <vector>
8 
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"
16 
17 namespace views {
18 
19 
20 // Helpers --------------------------------------------------------------------
21 
22 namespace {
23 
24 // Calculates the height of a line of text. Currently returns the height of
25 // a label.
CalculateLineHeight(const gfx::FontList & font_list)26 int CalculateLineHeight(const gfx::FontList& font_list) {
27   Label label;
28   label.SetFontList(font_list);
29   return label.GetPreferredSize().height();
30 }
31 
CreateLabelRange(const base::string16 & text,const gfx::FontList & font_list,const StyledLabel::RangeStyleInfo & style_info,views::LinkListener * link_listener)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;
38 
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);
43     result.reset(link);
44   } else {
45     result.reset(new Label(text));
46   }
47 
48   result->SetEnabledColor(style_info.color);
49   result->SetFontList(font_list);
50 
51   if (!style_info.tooltip.empty())
52     result->SetTooltipText(style_info.tooltip);
53   if (style_info.font_style != gfx::Font::NORMAL) {
54     result->SetFontList(
55         result->font_list().DeriveWithStyle(style_info.font_style));
56   }
57 
58   return result.Pass();
59 }
60 
61 }  // namespace
62 
63 
64 // StyledLabel::RangeStyleInfo ------------------------------------------------
65 
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),
71       is_link(false) {}
72 
~RangeStyleInfo()73 StyledLabel::RangeStyleInfo::~RangeStyleInfo() {}
74 
75 // static
CreateForLink()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();
81   return result;
82 }
83 
84 
85 // StyledLabel::StyleRange ----------------------------------------------------
86 
operator <(const StyledLabel::StyleRange & other) const87 bool StyledLabel::StyleRange::operator<(
88     const StyledLabel::StyleRange& other) const {
89   return range.start() < other.range.start();
90 }
91 
92 
93 // StyledLabel ----------------------------------------------------------------
94 
StyledLabel(const base::string16 & text,StyledLabelListener * listener)95 StyledLabel::StyledLabel(const base::string16& text,
96                          StyledLabelListener* listener)
97     : specified_line_height_(0),
98       listener_(listener),
99       displayed_on_background_color_set_(false),
100       auto_color_readability_enabled_(true) {
101   base::TrimWhitespace(text, base::TRIM_TRAILING, &text_);
102 }
103 
~StyledLabel()104 StyledLabel::~StyledLabel() {}
105 
SetText(const base::string16 & text)106 void StyledLabel::SetText(const base::string16& text) {
107   text_ = text;
108   style_ranges_.clear();
109   RemoveAllChildViews(true);
110   PreferredSizeChanged();
111 }
112 
SetBaseFontList(const gfx::FontList & font_list)113 void StyledLabel::SetBaseFontList(const gfx::FontList& font_list) {
114   font_list_ = font_list;
115   PreferredSizeChanged();
116 }
117 
AddStyleRange(const gfx::Range & range,const RangeStyleInfo & style_info)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));
123 
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);
128 
129   PreferredSizeChanged();
130 }
131 
SetDefaultStyle(const RangeStyleInfo & style_info)132 void StyledLabel::SetDefaultStyle(const RangeStyleInfo& style_info) {
133   default_style_info_ = style_info;
134   PreferredSizeChanged();
135 }
136 
SetLineHeight(int line_height)137 void StyledLabel::SetLineHeight(int line_height) {
138   specified_line_height_ = line_height;
139   PreferredSizeChanged();
140 }
141 
SetDisplayedOnBackgroundColor(SkColor color)142 void StyledLabel::SetDisplayedOnBackgroundColor(SkColor color) {
143   displayed_on_background_color_ = color;
144   displayed_on_background_color_set_ = true;
145 }
146 
GetInsets() const147 gfx::Insets StyledLabel::GetInsets() const {
148   gfx::Insets insets = View::GetInsets();
149 
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;
159       break;
160     }
161   }
162 
163   return insets;
164 }
165 
GetHeightForWidth(int w) const166 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.
172     calculated_size_ =
173         const_cast<StyledLabel*>(this)->CalculateAndDoLayout(w, true);
174   }
175   return calculated_size_.height();
176 }
177 
Layout()178 void StyledLabel::Layout() {
179   calculated_size_ = CalculateAndDoLayout(GetLocalBounds().width(), false);
180 }
181 
PreferredSizeChanged()182 void StyledLabel::PreferredSizeChanged() {
183   calculated_size_ = gfx::Size();
184   View::PreferredSizeChanged();
185 }
186 
LinkClicked(Link * source,int event_flags)187 void StyledLabel::LinkClicked(Link* source, int event_flags) {
188   if (listener_)
189     listener_->StyledLabelLinkClicked(link_targets_[source], event_flags);
190 }
191 
CalculateAndDoLayout(int width,bool dry_run)192 gfx::Size StyledLabel::CalculateAndDoLayout(int width, bool dry_run) {
193   if (!dry_run) {
194     RemoveAllChildViews(true);
195     link_targets_.clear();
196   }
197 
198   width -= GetInsets().width();
199   if (width <= 0 || text_.empty())
200     return gfx::Size();
201 
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.
205   int line = 0;
206   // The x position (in pixels) of the line we're on, relative to content
207   // bounds.
208   int x = 0;
209 
210   base::string16 remaining_string = text_;
211   StyleRanges::const_iterator current_range = style_ranges_.begin();
212 
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,
220                            &remaining_string);
221     }
222 
223     gfx::Range range(gfx::Range::InvalidRange());
224     if (current_range != style_ranges_.end())
225       range = current_range->range;
226 
227     const size_t position = text_.size() - remaining_string.size();
228 
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);
238     }
239     gfx::ElideRectangleText(remaining_string,
240                             text_font_list,
241                             chunk_bounds.width(),
242                             chunk_bounds.height(),
243                             gfx::IGNORE_LONG_WORDS,
244                             &substrings);
245 
246     DCHECK(!substrings.empty());
247     base::string16 chunk = substrings[0];
248     if (chunk.empty()) {
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
252       // anything; abort.
253       if (x == 0) {
254         if (line == 0) {
255           base::TrimWhitespace(remaining_string, base::TRIM_LEADING,
256                                &remaining_string);
257           continue;
258         }
259         break;
260       }
261 
262       x = 0;
263       line++;
264       continue;
265     }
266 
267     scoped_ptr<Label> label;
268     if (position >= range.start()) {
269       const RangeStyleInfo& style_info = current_range->style_info;
270 
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
274         // next line.
275         x = 0;
276         line++;
277         continue;
278       }
279 
280       chunk = chunk.substr(0, std::min(chunk.size(), range.end() - position));
281 
282       label = CreateLabelRange(chunk, font_list_, style_info, this);
283 
284       if (style_info.is_link && !dry_run)
285         link_targets_[label.get()] = range;
286 
287       if (position + chunk.size() >= range.end())
288         ++current_range;
289     } else {
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);
294     }
295 
296     if (displayed_on_background_color_set_)
297       label->SetBackgroundColor(displayed_on_background_color_);
298     label->SetAutoColorReadabilityEnabled(auto_color_readability_enabled_);
299 
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();
305     if (!dry_run) {
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()),
310           view_size));
311       AddChildView(label.release());
312     }
313     x += view_size.width() - focus_border_insets.width();
314 
315     remaining_string = remaining_string.substr(chunk.size());
316   }
317 
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);
323 }
324 
325 }  // namespace views
326