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