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