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/base/text/text_elider.h"
11#include "ui/native_theme/native_theme.h"
12#include "ui/views/controls/label.h"
13#include "ui/views/controls/link.h"
14#include "ui/views/controls/styled_label_listener.h"
15
16namespace views {
17
18namespace {
19
20// Calculates the height of a line of text. Currently returns the height of
21// a label.
22int CalculateLineHeight() {
23  Label label;
24  return label.GetPreferredSize().height();
25}
26
27scoped_ptr<Label> CreateLabelRange(
28    const string16& text,
29    const StyledLabel::RangeStyleInfo& style_info,
30    views::LinkListener* link_listener) {
31  scoped_ptr<Label> result;
32
33  if (style_info.is_link) {
34    Link* link = new Link(text);
35    link->set_listener(link_listener);
36    link->SetUnderline((style_info.font_style & gfx::Font::UNDERLINE) != 0);
37    result.reset(link);
38  } else {
39    Label* label = new Label(text);
40    // Give the label a focus border so that its preferred size matches
41    // links' preferred sizes
42    label->SetHasFocusBorder(true);
43
44    result.reset(label);
45  }
46
47  result->SetEnabledColor(style_info.color);
48
49  if (!style_info.tooltip.empty())
50    result->SetTooltipText(style_info.tooltip);
51  if (style_info.font_style != gfx::Font::NORMAL)
52    result->SetFont(result->font().DeriveFont(0, style_info.font_style));
53
54  return scoped_ptr<Label>(result.release());
55}
56
57}  // namespace
58
59StyledLabel::RangeStyleInfo::RangeStyleInfo()
60    : font_style(gfx::Font::NORMAL),
61      color(ui::NativeTheme::instance()->GetSystemColor(
62          ui::NativeTheme::kColorId_LabelEnabledColor)),
63      disable_line_wrapping(false),
64      is_link(false) {}
65
66StyledLabel::RangeStyleInfo::~RangeStyleInfo() {}
67
68// static
69StyledLabel::RangeStyleInfo StyledLabel::RangeStyleInfo::CreateForLink() {
70  RangeStyleInfo result;
71  result.disable_line_wrapping = true;
72  result.is_link = true;
73  result.color = Link::GetDefaultEnabledColor();
74  return result;
75}
76
77bool StyledLabel::StyleRange::operator<(
78    const StyledLabel::StyleRange& other) const {
79  // Intentionally reversed so the priority queue is sorted by smallest first.
80  return range.start() > other.range.start();
81}
82
83StyledLabel::StyledLabel(const string16& text, StyledLabelListener* listener)
84    : listener_(listener),
85      displayed_on_background_color_set_(false) {
86  TrimWhitespace(text, TRIM_TRAILING, &text_);
87}
88
89StyledLabel::~StyledLabel() {}
90
91void StyledLabel::SetText(const string16& text) {
92  text_ = text;
93  style_ranges_ = std::priority_queue<StyleRange>();
94  RemoveAllChildViews(true);
95  PreferredSizeChanged();
96}
97
98void StyledLabel::AddStyleRange(const ui::Range& range,
99                                const RangeStyleInfo& style_info) {
100  DCHECK(!range.is_reversed());
101  DCHECK(!range.is_empty());
102  DCHECK(ui::Range(0, text_.size()).Contains(range));
103
104  style_ranges_.push(StyleRange(range, style_info));
105
106  PreferredSizeChanged();
107}
108
109void StyledLabel::SetDefaultStyle(const RangeStyleInfo& style_info) {
110  default_style_info_ = style_info;
111  PreferredSizeChanged();
112}
113
114void StyledLabel::SetDisplayedOnBackgroundColor(SkColor color) {
115  displayed_on_background_color_ = color;
116  displayed_on_background_color_set_ = true;
117}
118
119gfx::Insets StyledLabel::GetInsets() const {
120  gfx::Insets insets = View::GetInsets();
121  const gfx::Insets focus_border_padding(1, 1, 1, 1);
122  insets += focus_border_padding;
123  return insets;
124}
125
126int StyledLabel::GetHeightForWidth(int w) {
127  if (w != calculated_size_.width())
128    calculated_size_ = gfx::Size(w, CalculateAndDoLayout(w, true));
129
130  return calculated_size_.height();
131}
132
133void StyledLabel::Layout() {
134  CalculateAndDoLayout(GetLocalBounds().width(), false);
135}
136
137void StyledLabel::PreferredSizeChanged() {
138  calculated_size_ = gfx::Size();
139  View::PreferredSizeChanged();
140}
141
142void StyledLabel::LinkClicked(Link* source, int event_flags) {
143  if (listener_)
144    listener_->StyledLabelLinkClicked(link_targets_[source], event_flags);
145}
146
147int StyledLabel::CalculateAndDoLayout(int width, bool dry_run) {
148  if (!dry_run) {
149    RemoveAllChildViews(true);
150    link_targets_.clear();
151  }
152
153  width -= GetInsets().width();
154  if (width <= 0 || text_.empty())
155    return 0;
156
157  const int line_height = CalculateLineHeight();
158  // The index of the line we're on.
159  int line = 0;
160  // The x position (in pixels) of the line we're on, relative to content
161  // bounds.
162  int x = 0;
163
164  string16 remaining_string = text_;
165  std::priority_queue<StyleRange> style_ranges = style_ranges_;
166
167  // Iterate over the text, creating a bunch of labels and links and laying them
168  // out in the appropriate positions.
169  while (!remaining_string.empty()) {
170    // Don't put whitespace at beginning of a line with an exception for the
171    // first line (so the text's leading whitespace is respected).
172    if (x == 0 && line > 0)
173      TrimWhitespace(remaining_string, TRIM_LEADING, &remaining_string);
174
175    ui::Range range(ui::Range::InvalidRange());
176    if (!style_ranges.empty())
177      range = style_ranges.top().range;
178
179    const size_t position = text_.size() - remaining_string.size();
180
181    const gfx::Rect chunk_bounds(x, 0, width - x, 2 * line_height);
182    std::vector<string16> substrings;
183    gfx::Font text_font;
184    // If the start of the remaining text is inside a styled range, the font
185    // style may differ from the base font. The font specified by the range
186    // should be used when eliding text.
187    if (position >= range.start()) {
188      text_font =
189          text_font.DeriveFont(0, style_ranges.top().style_info.font_style);
190    }
191    ui::ElideRectangleText(remaining_string,
192                           text_font,
193                           chunk_bounds.width(),
194                           chunk_bounds.height(),
195                           ui::IGNORE_LONG_WORDS,
196                           &substrings);
197
198    DCHECK(!substrings.empty());
199    string16 chunk = substrings[0];
200    if (chunk.empty()) {
201      // Nothing fits on this line. Start a new line.
202      // If x is 0, first line may have leading whitespace that doesn't fit in a
203      // single line, so try trimming those. Otherwise there is no room for
204      // anything; abort.
205      if (x == 0) {
206        if (line == 0) {
207          TrimWhitespace(remaining_string, TRIM_LEADING, &remaining_string);
208          continue;
209        }
210        break;
211      }
212
213      x = 0;
214      line++;
215      continue;
216    }
217
218    scoped_ptr<Label> label;
219    if (position >= range.start()) {
220      const RangeStyleInfo& style_info = style_ranges.top().style_info;
221
222      if (style_info.disable_line_wrapping && chunk.size() < range.length() &&
223          position == range.start() && x != 0) {
224        // If the chunk should not be wrapped, try to fit it entirely on the
225        // next line.
226        x = 0;
227        line++;
228        continue;
229      }
230
231      chunk = chunk.substr(0, std::min(chunk.size(), range.end() - position));
232
233      label = CreateLabelRange(chunk, style_info, this);
234
235      if (style_info.is_link && !dry_run)
236        link_targets_[label.get()] = range;
237
238      if (position + chunk.size() >= range.end())
239        style_ranges.pop();
240    } else {
241      // This chunk is normal text.
242      if (position + chunk.size() > range.start())
243        chunk = chunk.substr(0, range.start() - position);
244      label = CreateLabelRange(chunk, default_style_info_, this);
245    }
246
247    if (displayed_on_background_color_set_)
248      label->SetBackgroundColor(displayed_on_background_color_);
249
250    // Lay out the views to overlap by 1 pixel to compensate for their border
251    // spacing. Otherwise, "<a>link</a>," will render as "link ,".
252    const int overlap = 1;
253    const gfx::Size view_size = label->GetPreferredSize();
254    DCHECK_EQ(line_height, view_size.height() - 2 * overlap);
255    if (!dry_run) {
256      label->SetBoundsRect(gfx::Rect(
257          gfx::Point(GetInsets().left() + x - overlap,
258                     GetInsets().top() + line * line_height - overlap),
259          view_size));
260      AddChildView(label.release());
261    }
262    x += view_size.width() - 2 * overlap;
263
264    remaining_string = remaining_string.substr(chunk.size());
265  }
266
267  return (line + 1) * line_height + GetInsets().height();
268}
269
270}  // namespace views
271