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/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.
     26 int CalculateLineHeight(const gfx::FontList& font_list) {
     27   Label label;
     28   label.SetFontList(font_list);
     29   return label.GetPreferredSize().height();
     30 }
     31 
     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 
     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 
     73 StyledLabel::RangeStyleInfo::~RangeStyleInfo() {}
     74 
     75 // static
     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 
     87 bool StyledLabel::StyleRange::operator<(
     88     const StyledLabel::StyleRange& other) const {
     89   return range.start() < other.range.start();
     90 }
     91 
     92 
     93 // StyledLabel ----------------------------------------------------------------
     94 
     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 
    104 StyledLabel::~StyledLabel() {}
    105 
    106 void StyledLabel::SetText(const base::string16& text) {
    107   text_ = text;
    108   style_ranges_.clear();
    109   RemoveAllChildViews(true);
    110   PreferredSizeChanged();
    111 }
    112 
    113 void StyledLabel::SetBaseFontList(const gfx::FontList& font_list) {
    114   font_list_ = font_list;
    115   PreferredSizeChanged();
    116 }
    117 
    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 
    132 void StyledLabel::SetDefaultStyle(const RangeStyleInfo& style_info) {
    133   default_style_info_ = style_info;
    134   PreferredSizeChanged();
    135 }
    136 
    137 void StyledLabel::SetLineHeight(int line_height) {
    138   specified_line_height_ = line_height;
    139   PreferredSizeChanged();
    140 }
    141 
    142 void StyledLabel::SetDisplayedOnBackgroundColor(SkColor color) {
    143   displayed_on_background_color_ = color;
    144   displayed_on_background_color_set_ = true;
    145 }
    146 
    147 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 
    166 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 
    178 void StyledLabel::Layout() {
    179   calculated_size_ = CalculateAndDoLayout(GetLocalBounds().width(), false);
    180 }
    181 
    182 void StyledLabel::PreferredSizeChanged() {
    183   calculated_size_ = gfx::Size();
    184   View::PreferredSizeChanged();
    185 }
    186 
    187 void StyledLabel::LinkClicked(Link* source, int event_flags) {
    188   if (listener_)
    189     listener_->StyledLabelLinkClicked(link_targets_[source], event_flags);
    190 }
    191 
    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