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