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/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 
     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   TrimWhitespace(text, TRIM_TRAILING, &text_);
     87 }
     88 
     89 StyledLabel::~StyledLabel() {}
     90 
     91 void StyledLabel::SetText(const string16& text) {
     92   text_ = text;
     93   style_ranges_ = std::priority_queue<StyleRange>();
     94   RemoveAllChildViews(true);
     95   PreferredSizeChanged();
     96 }
     97 
     98 void 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 
    109 void StyledLabel::SetDefaultStyle(const RangeStyleInfo& style_info) {
    110   default_style_info_ = style_info;
    111   PreferredSizeChanged();
    112 }
    113 
    114 void StyledLabel::SetDisplayedOnBackgroundColor(SkColor color) {
    115   displayed_on_background_color_ = color;
    116   displayed_on_background_color_set_ = true;
    117 }
    118 
    119 gfx::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 
    126 int 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 
    133 void StyledLabel::Layout() {
    134   CalculateAndDoLayout(GetLocalBounds().width(), false);
    135 }
    136 
    137 void StyledLabel::PreferredSizeChanged() {
    138   calculated_size_ = gfx::Size();
    139   View::PreferredSizeChanged();
    140 }
    141 
    142 void StyledLabel::LinkClicked(Link* source, int event_flags) {
    143   if (listener_)
    144     listener_->StyledLabelLinkClicked(link_targets_[source], event_flags);
    145 }
    146 
    147 int 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