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