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