styled_label.cc revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
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
17namespace views {
18
19
20// Helpers --------------------------------------------------------------------
21
22namespace {
23
24// Calculates the height of a line of text. Currently returns the height of
25// a label.
26int CalculateLineHeight(const gfx::FontList& font_list) {
27  Label label;
28  label.SetFontList(font_list);
29  return label.GetPreferredSize().height();
30}
31
32scoped_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
66StyledLabel::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
73StyledLabel::RangeStyleInfo::~RangeStyleInfo() {}
74
75// static
76StyledLabel::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
87bool StyledLabel::StyleRange::operator<(
88    const StyledLabel::StyleRange& other) const {
89  return range.start() < other.range.start();
90}
91
92
93// StyledLabel ----------------------------------------------------------------
94
95StyledLabel::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  TrimWhitespace(text, TRIM_TRAILING, &text_);
101}
102
103StyledLabel::~StyledLabel() {}
104
105void StyledLabel::SetText(const base::string16& text) {
106  text_ = text;
107  style_ranges_.clear();
108  RemoveAllChildViews(true);
109  PreferredSizeChanged();
110}
111
112void StyledLabel::SetBaseFontList(const gfx::FontList& font_list) {
113  font_list_ = font_list;
114  PreferredSizeChanged();
115}
116
117void 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));
122
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);
127
128  PreferredSizeChanged();
129}
130
131void StyledLabel::SetDefaultStyle(const RangeStyleInfo& style_info) {
132  default_style_info_ = style_info;
133  PreferredSizeChanged();
134}
135
136void StyledLabel::SetDisplayedOnBackgroundColor(SkColor color) {
137  displayed_on_background_color_ = color;
138  displayed_on_background_color_set_ = true;
139}
140
141gfx::Insets StyledLabel::GetInsets() const {
142  gfx::Insets insets = View::GetInsets();
143
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;
153      break;
154    }
155  }
156
157  return insets;
158}
159
160int StyledLabel::GetHeightForWidth(int w) {
161  if (w != calculated_size_.width())
162    calculated_size_ = CalculateAndDoLayout(w, true);
163  return calculated_size_.height();
164}
165
166void StyledLabel::Layout() {
167  calculated_size_ = CalculateAndDoLayout(GetLocalBounds().width(), false);
168}
169
170void StyledLabel::PreferredSizeChanged() {
171  calculated_size_ = gfx::Size();
172  View::PreferredSizeChanged();
173}
174
175void StyledLabel::LinkClicked(Link* source, int event_flags) {
176  if (listener_)
177    listener_->StyledLabelLinkClicked(link_targets_[source], event_flags);
178}
179
180gfx::Size StyledLabel::CalculateAndDoLayout(int width, bool dry_run) {
181  if (!dry_run) {
182    RemoveAllChildViews(true);
183    link_targets_.clear();
184  }
185
186  width -= GetInsets().width();
187  if (width <= 0 || text_.empty())
188    return gfx::Size();
189
190  const int line_height = CalculateLineHeight(font_list_);
191  // The index of the line we're on.
192  int line = 0;
193  // The x position (in pixels) of the line we're on, relative to content
194  // bounds.
195  int x = 0;
196
197  base::string16 remaining_string = text_;
198  StyleRanges::const_iterator current_range = style_ranges_.begin();
199
200  // Iterate over the text, creating a bunch of labels and links and laying them
201  // out in the appropriate positions.
202  while (!remaining_string.empty()) {
203    // Don't put whitespace at beginning of a line with an exception for the
204    // first line (so the text's leading whitespace is respected).
205    if (x == 0 && line > 0)
206      TrimWhitespace(remaining_string, TRIM_LEADING, &remaining_string);
207
208    gfx::Range range(gfx::Range::InvalidRange());
209    if (current_range != style_ranges_.end())
210      range = current_range->range;
211
212    const size_t position = text_.size() - remaining_string.size();
213
214    const gfx::Rect chunk_bounds(x, 0, width - x, 2 * line_height);
215    std::vector<base::string16> substrings;
216    gfx::FontList text_font_list = font_list_;
217    // If the start of the remaining text is inside a styled range, the font
218    // style may differ from the base font. The font specified by the range
219    // should be used when eliding text.
220    if (position >= range.start()) {
221      text_font_list = text_font_list.DeriveWithStyle(
222          current_range->style_info.font_style);
223    }
224    gfx::ElideRectangleText(remaining_string,
225                            text_font_list,
226                            chunk_bounds.width(),
227                            chunk_bounds.height(),
228                            gfx::IGNORE_LONG_WORDS,
229                            &substrings);
230
231    DCHECK(!substrings.empty());
232    base::string16 chunk = substrings[0];
233    if (chunk.empty()) {
234      // Nothing fits on this line. Start a new line.
235      // If x is 0, first line may have leading whitespace that doesn't fit in a
236      // single line, so try trimming those. Otherwise there is no room for
237      // anything; abort.
238      if (x == 0) {
239        if (line == 0) {
240          TrimWhitespace(remaining_string, TRIM_LEADING, &remaining_string);
241          continue;
242        }
243        break;
244      }
245
246      x = 0;
247      line++;
248      continue;
249    }
250
251    scoped_ptr<Label> label;
252    if (position >= range.start()) {
253      const RangeStyleInfo& style_info = current_range->style_info;
254
255      if (style_info.disable_line_wrapping && chunk.size() < range.length() &&
256          position == range.start() && x != 0) {
257        // If the chunk should not be wrapped, try to fit it entirely on the
258        // next line.
259        x = 0;
260        line++;
261        continue;
262      }
263
264      chunk = chunk.substr(0, std::min(chunk.size(), range.end() - position));
265
266      label = CreateLabelRange(chunk, font_list_, style_info, this);
267
268      if (style_info.is_link && !dry_run)
269        link_targets_[label.get()] = range;
270
271      if (position + chunk.size() >= range.end())
272        ++current_range;
273    } else {
274      // This chunk is normal text.
275      if (position + chunk.size() > range.start())
276        chunk = chunk.substr(0, range.start() - position);
277      label = CreateLabelRange(chunk, font_list_, default_style_info_, this);
278    }
279
280    if (displayed_on_background_color_set_)
281      label->SetBackgroundColor(displayed_on_background_color_);
282    label->SetAutoColorReadabilityEnabled(auto_color_readability_enabled_);
283
284    // Calculate the size of the optional focus border, and overlap by that
285    // amount. Otherwise, "<a>link</a>," will render as "link ,".
286    gfx::Insets focus_border_insets(label->GetInsets());
287    focus_border_insets += -label->View::GetInsets();
288    const gfx::Size view_size = label->GetPreferredSize();
289    DCHECK_EQ(line_height, view_size.height() - focus_border_insets.height());
290    if (!dry_run) {
291      label->SetBoundsRect(gfx::Rect(
292          gfx::Point(GetInsets().left() + x - focus_border_insets.left(),
293                     GetInsets().top() + line * line_height -
294                         focus_border_insets.top()),
295          view_size));
296      AddChildView(label.release());
297    }
298    x += view_size.width() - focus_border_insets.width();
299
300    remaining_string = remaining_string.substr(chunk.size());
301  }
302
303  return gfx::Size(width, (line + 1) * line_height + GetInsets().height());
304}
305
306}  // namespace views
307