1 // Copyright (c) 2012 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/label.h" 6 7 #include <algorithm> 8 #include <cmath> 9 #include <limits> 10 #include <vector> 11 12 #include "base/i18n/rtl.h" 13 #include "base/logging.h" 14 #include "base/strings/string_split.h" 15 #include "base/strings/string_util.h" 16 #include "base/strings/utf_string_conversions.h" 17 #include "ui/accessibility/ax_view_state.h" 18 #include "ui/gfx/canvas.h" 19 #include "ui/gfx/color_utils.h" 20 #include "ui/gfx/insets.h" 21 #include "ui/gfx/text_elider.h" 22 #include "ui/gfx/text_utils.h" 23 #include "ui/gfx/utf16_indexing.h" 24 #include "ui/native_theme/native_theme.h" 25 #include "ui/views/background.h" 26 27 namespace { 28 29 const int kCachedSizeLimit = 10; 30 const base::char16 kPasswordReplacementChar = '*'; 31 32 } // namespace 33 34 namespace views { 35 36 // static 37 const char Label::kViewClassName[] = "Label"; 38 const int Label::kFocusBorderPadding = 1; 39 40 Label::Label() { 41 Init(base::string16(), gfx::FontList()); 42 } 43 44 Label::Label(const base::string16& text) { 45 Init(text, gfx::FontList()); 46 } 47 48 Label::Label(const base::string16& text, const gfx::FontList& font_list) { 49 Init(text, font_list); 50 } 51 52 Label::~Label() { 53 } 54 55 void Label::SetFontList(const gfx::FontList& font_list) { 56 font_list_ = font_list; 57 ResetCachedSize(); 58 PreferredSizeChanged(); 59 SchedulePaint(); 60 } 61 62 void Label::SetText(const base::string16& text) { 63 if (text != text_) 64 SetTextInternal(text); 65 } 66 67 void Label::SetTextInternal(const base::string16& text) { 68 text_ = text; 69 70 if (obscured_) { 71 size_t obscured_text_length = 72 static_cast<size_t>(gfx::UTF16IndexToOffset(text_, 0, text_.length())); 73 layout_text_.assign(obscured_text_length, kPasswordReplacementChar); 74 } else { 75 layout_text_ = text_; 76 } 77 78 ResetCachedSize(); 79 PreferredSizeChanged(); 80 SchedulePaint(); 81 } 82 83 void Label::SetAutoColorReadabilityEnabled(bool enabled) { 84 auto_color_readability_ = enabled; 85 RecalculateColors(); 86 } 87 88 void Label::SetEnabledColor(SkColor color) { 89 requested_enabled_color_ = color; 90 enabled_color_set_ = true; 91 RecalculateColors(); 92 } 93 94 void Label::SetDisabledColor(SkColor color) { 95 requested_disabled_color_ = color; 96 disabled_color_set_ = true; 97 RecalculateColors(); 98 } 99 100 void Label::SetBackgroundColor(SkColor color) { 101 background_color_ = color; 102 background_color_set_ = true; 103 RecalculateColors(); 104 } 105 106 void Label::SetShadows(const gfx::ShadowValues& shadows) { 107 shadows_ = shadows; 108 text_size_valid_ = false; 109 } 110 111 void Label::SetSubpixelRenderingEnabled(bool subpixel_rendering_enabled) { 112 subpixel_rendering_enabled_ = subpixel_rendering_enabled; 113 } 114 115 void Label::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) { 116 // If the UI layout is right-to-left, flip the alignment direction. 117 if (base::i18n::IsRTL() && 118 (alignment == gfx::ALIGN_LEFT || alignment == gfx::ALIGN_RIGHT)) { 119 alignment = (alignment == gfx::ALIGN_LEFT) ? 120 gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT; 121 } 122 if (horizontal_alignment_ != alignment) { 123 horizontal_alignment_ = alignment; 124 SchedulePaint(); 125 } 126 } 127 128 gfx::HorizontalAlignment Label::GetHorizontalAlignment() const { 129 if (horizontal_alignment_ != gfx::ALIGN_TO_HEAD) 130 return horizontal_alignment_; 131 132 const base::i18n::TextDirection dir = 133 base::i18n::GetFirstStrongCharacterDirection(layout_text_); 134 return dir == base::i18n::RIGHT_TO_LEFT ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT; 135 } 136 137 void Label::SetLineHeight(int height) { 138 if (height != line_height_) { 139 line_height_ = height; 140 ResetCachedSize(); 141 PreferredSizeChanged(); 142 SchedulePaint(); 143 } 144 } 145 146 void Label::SetMultiLine(bool multi_line) { 147 DCHECK(!multi_line || (elide_behavior_ == gfx::ELIDE_TAIL || 148 elide_behavior_ == gfx::NO_ELIDE)); 149 if (multi_line != multi_line_) { 150 multi_line_ = multi_line; 151 ResetCachedSize(); 152 PreferredSizeChanged(); 153 SchedulePaint(); 154 } 155 } 156 157 void Label::SetObscured(bool obscured) { 158 if (obscured != obscured_) { 159 obscured_ = obscured; 160 SetTextInternal(text_); 161 } 162 } 163 164 void Label::SetAllowCharacterBreak(bool allow_character_break) { 165 if (allow_character_break != allow_character_break_) { 166 allow_character_break_ = allow_character_break; 167 ResetCachedSize(); 168 PreferredSizeChanged(); 169 SchedulePaint(); 170 } 171 } 172 173 void Label::SetElideBehavior(gfx::ElideBehavior elide_behavior) { 174 DCHECK(!multi_line_ || (elide_behavior_ == gfx::ELIDE_TAIL || 175 elide_behavior_ == gfx::NO_ELIDE)); 176 if (elide_behavior != elide_behavior_) { 177 elide_behavior_ = elide_behavior; 178 ResetCachedSize(); 179 PreferredSizeChanged(); 180 SchedulePaint(); 181 } 182 } 183 184 void Label::SetTooltipText(const base::string16& tooltip_text) { 185 tooltip_text_ = tooltip_text; 186 } 187 188 void Label::SizeToFit(int max_width) { 189 DCHECK(multi_line_); 190 191 std::vector<base::string16> lines; 192 base::SplitString(layout_text_, '\n', &lines); 193 194 int label_width = 0; 195 for (std::vector<base::string16>::const_iterator iter = lines.begin(); 196 iter != lines.end(); ++iter) { 197 label_width = std::max(label_width, gfx::GetStringWidth(*iter, font_list_)); 198 } 199 200 label_width += GetInsets().width(); 201 202 if (max_width > 0) 203 label_width = std::min(label_width, max_width); 204 205 SetBounds(x(), y(), label_width, 0); 206 SizeToPreferredSize(); 207 } 208 209 const base::string16& Label::GetLayoutTextForTesting() const { 210 return layout_text_; 211 } 212 213 gfx::Insets Label::GetInsets() const { 214 gfx::Insets insets = View::GetInsets(); 215 if (focusable()) { 216 insets += gfx::Insets(kFocusBorderPadding, kFocusBorderPadding, 217 kFocusBorderPadding, kFocusBorderPadding); 218 } 219 return insets; 220 } 221 222 int Label::GetBaseline() const { 223 return GetInsets().top() + font_list_.GetBaseline(); 224 } 225 226 gfx::Size Label::GetPreferredSize() const { 227 // Return a size of (0, 0) if the label is not visible and if the 228 // collapse_when_hidden_ flag is set. 229 // TODO(munjal): This logic probably belongs to the View class. But for now, 230 // put it here since putting it in View class means all inheriting classes 231 // need ot respect the collapse_when_hidden_ flag. 232 if (!visible() && collapse_when_hidden_) 233 return gfx::Size(); 234 235 gfx::Size size(GetTextSize()); 236 gfx::Insets insets = GetInsets(); 237 size.Enlarge(insets.width(), insets.height()); 238 return size; 239 } 240 241 gfx::Size Label::GetMinimumSize() const { 242 gfx::Size text_size(GetTextSize()); 243 if ((!visible() && collapse_when_hidden_) || text_size.IsEmpty()) 244 return gfx::Size(); 245 246 gfx::Size size(gfx::GetStringWidth(base::string16(gfx::kEllipsisUTF16), 247 font_list_), 248 font_list_.GetHeight()); 249 size.SetToMin(text_size); // The actual text may be shorter than an ellipsis. 250 gfx::Insets insets = GetInsets(); 251 size.Enlarge(insets.width(), insets.height()); 252 return size; 253 } 254 255 int Label::GetHeightForWidth(int w) const { 256 if (!multi_line_) 257 return View::GetHeightForWidth(w); 258 259 w = std::max(0, w - GetInsets().width()); 260 261 for (size_t i = 0; i < cached_heights_.size(); ++i) { 262 const gfx::Size& s = cached_heights_[i]; 263 if (s.width() == w) 264 return s.height() + GetInsets().height(); 265 } 266 267 int cache_width = w; 268 269 int h = font_list_.GetHeight(); 270 const int flags = ComputeDrawStringFlags(); 271 gfx::Canvas::SizeStringInt( 272 layout_text_, font_list_, &w, &h, line_height_, flags); 273 cached_heights_[cached_heights_cursor_] = gfx::Size(cache_width, h); 274 cached_heights_cursor_ = (cached_heights_cursor_ + 1) % kCachedSizeLimit; 275 return h + GetInsets().height(); 276 } 277 278 const char* Label::GetClassName() const { 279 return kViewClassName; 280 } 281 282 View* Label::GetTooltipHandlerForPoint(const gfx::Point& point) { 283 if (tooltip_text_.empty() && !ShouldShowDefaultTooltip()) 284 return NULL; 285 286 return HitTestPoint(point) ? this : NULL; 287 } 288 289 bool Label::CanProcessEventsWithinSubtree() const { 290 // Send events to the parent view for handling. 291 return false; 292 } 293 294 void Label::GetAccessibleState(ui::AXViewState* state) { 295 state->role = ui::AX_ROLE_STATIC_TEXT; 296 state->AddStateFlag(ui::AX_STATE_READ_ONLY); 297 state->name = layout_text_; 298 } 299 300 bool Label::GetTooltipText(const gfx::Point& p, base::string16* tooltip) const { 301 if (!tooltip_text_.empty()) { 302 tooltip->assign(tooltip_text_); 303 return true; 304 } 305 306 if (ShouldShowDefaultTooltip()) { 307 *tooltip = layout_text_; 308 return true; 309 } 310 311 return false; 312 } 313 314 void Label::PaintText(gfx::Canvas* canvas, 315 const base::string16& text, 316 const gfx::Rect& text_bounds, 317 int flags) { 318 SkColor color = enabled() ? actual_enabled_color_ : actual_disabled_color_; 319 if (elide_behavior_ == gfx::FADE_TAIL) { 320 canvas->DrawFadedString(text, font_list_, color, text_bounds, flags); 321 } else { 322 canvas->DrawStringRectWithShadows(text, font_list_, color, text_bounds, 323 line_height_, flags, shadows_); 324 } 325 326 if (HasFocus()) { 327 gfx::Rect focus_bounds = text_bounds; 328 focus_bounds.Inset(-kFocusBorderPadding, -kFocusBorderPadding); 329 canvas->DrawFocusRect(focus_bounds); 330 } 331 } 332 333 gfx::Size Label::GetTextSize() const { 334 if (!text_size_valid_) { 335 // For single-line strings, we supply the largest possible width, because 336 // while adding NO_ELLIPSIS to the flags works on Windows for forcing 337 // SizeStringInt() to calculate the desired width, it doesn't seem to work 338 // on Linux. 339 int w = multi_line_ ? 340 GetAvailableRect().width() : std::numeric_limits<int>::max(); 341 int h = font_list_.GetHeight(); 342 // For single-line strings, ignore the available width and calculate how 343 // wide the text wants to be. 344 int flags = ComputeDrawStringFlags(); 345 if (!multi_line_) 346 flags |= gfx::Canvas::NO_ELLIPSIS; 347 gfx::Canvas::SizeStringInt( 348 layout_text_, font_list_, &w, &h, line_height_, flags); 349 text_size_.SetSize(w, h); 350 const gfx::Insets shadow_margin = -gfx::ShadowValue::GetMargin(shadows_); 351 text_size_.Enlarge(shadow_margin.width(), shadow_margin.height()); 352 text_size_valid_ = true; 353 } 354 355 return text_size_; 356 } 357 358 void Label::OnBoundsChanged(const gfx::Rect& previous_bounds) { 359 text_size_valid_ &= !multi_line_; 360 } 361 362 void Label::OnPaint(gfx::Canvas* canvas) { 363 OnPaintBackground(canvas); 364 // We skip painting the focus border because it is being handled seperately by 365 // some subclasses of Label. We do not want View's focus border painting to 366 // interfere with that. 367 OnPaintBorder(canvas); 368 369 base::string16 paint_text; 370 gfx::Rect text_bounds; 371 int flags = 0; 372 CalculateDrawStringParams(&paint_text, &text_bounds, &flags); 373 PaintText(canvas, paint_text, text_bounds, flags); 374 } 375 376 void Label::OnNativeThemeChanged(const ui::NativeTheme* theme) { 377 UpdateColorsFromTheme(theme); 378 } 379 380 void Label::Init(const base::string16& text, const gfx::FontList& font_list) { 381 font_list_ = font_list; 382 enabled_color_set_ = disabled_color_set_ = background_color_set_ = false; 383 subpixel_rendering_enabled_ = true; 384 auto_color_readability_ = true; 385 UpdateColorsFromTheme(ui::NativeTheme::instance()); 386 horizontal_alignment_ = gfx::ALIGN_CENTER; 387 line_height_ = 0; 388 multi_line_ = false; 389 obscured_ = false; 390 allow_character_break_ = false; 391 elide_behavior_ = gfx::ELIDE_TAIL; 392 collapse_when_hidden_ = false; 393 cached_heights_.resize(kCachedSizeLimit); 394 ResetCachedSize(); 395 396 SetText(text); 397 } 398 399 void Label::RecalculateColors() { 400 actual_enabled_color_ = auto_color_readability_ ? 401 color_utils::GetReadableColor(requested_enabled_color_, 402 background_color_) : 403 requested_enabled_color_; 404 actual_disabled_color_ = auto_color_readability_ ? 405 color_utils::GetReadableColor(requested_disabled_color_, 406 background_color_) : 407 requested_disabled_color_; 408 } 409 410 gfx::Rect Label::GetTextBounds() const { 411 gfx::Rect available(GetAvailableRect()); 412 gfx::Size text_size(GetTextSize()); 413 text_size.set_width(std::min(available.width(), text_size.width())); 414 gfx::Point origin(GetInsets().left(), GetInsets().top()); 415 switch (GetHorizontalAlignment()) { 416 case gfx::ALIGN_LEFT: 417 break; 418 case gfx::ALIGN_CENTER: 419 // Put any extra margin pixel on the left to match the legacy behavior 420 // from the use of GetTextExtentPoint32() on Windows. 421 origin.Offset((available.width() + 1 - text_size.width()) / 2, 0); 422 break; 423 case gfx::ALIGN_RIGHT: 424 origin.set_x(available.right() - text_size.width()); 425 break; 426 default: 427 NOTREACHED(); 428 break; 429 } 430 if (!multi_line_) 431 text_size.set_height(available.height()); 432 // Support vertical centering of multi-line labels: http://crbug.com/429595 433 origin.Offset(0, std::max(0, (available.height() - text_size.height())) / 2); 434 return gfx::Rect(origin, text_size); 435 } 436 437 int Label::ComputeDrawStringFlags() const { 438 int flags = 0; 439 440 // We can't use subpixel rendering if the background is non-opaque. 441 if (SkColorGetA(background_color_) != 0xFF || !subpixel_rendering_enabled_) 442 flags |= gfx::Canvas::NO_SUBPIXEL_RENDERING; 443 444 base::i18n::TextDirection direction = 445 base::i18n::GetFirstStrongCharacterDirection(layout_text_); 446 if (direction == base::i18n::RIGHT_TO_LEFT) 447 flags |= gfx::Canvas::FORCE_RTL_DIRECTIONALITY; 448 else 449 flags |= gfx::Canvas::FORCE_LTR_DIRECTIONALITY; 450 451 switch (GetHorizontalAlignment()) { 452 case gfx::ALIGN_LEFT: 453 flags |= gfx::Canvas::TEXT_ALIGN_LEFT; 454 break; 455 case gfx::ALIGN_CENTER: 456 flags |= gfx::Canvas::TEXT_ALIGN_CENTER; 457 break; 458 case gfx::ALIGN_RIGHT: 459 flags |= gfx::Canvas::TEXT_ALIGN_RIGHT; 460 break; 461 default: 462 NOTREACHED(); 463 break; 464 } 465 466 if (!multi_line_) 467 return flags; 468 469 flags |= gfx::Canvas::MULTI_LINE; 470 #if !defined(OS_WIN) 471 // Don't elide multiline labels on Linux. 472 // Todo(davemoore): Do we depend on eliding multiline text? 473 // Pango insists on limiting the number of lines to one if text is 474 // elided. You can get around this if you can pass a maximum height 475 // but we don't currently have that data when we call the pango code. 476 flags |= gfx::Canvas::NO_ELLIPSIS; 477 #endif 478 if (allow_character_break_) 479 flags |= gfx::Canvas::CHARACTER_BREAK; 480 481 return flags; 482 } 483 484 gfx::Rect Label::GetAvailableRect() const { 485 gfx::Rect bounds(size()); 486 bounds.Inset(GetInsets()); 487 return bounds; 488 } 489 490 void Label::CalculateDrawStringParams(base::string16* paint_text, 491 gfx::Rect* text_bounds, 492 int* flags) const { 493 DCHECK(paint_text && text_bounds && flags); 494 495 const bool forbid_ellipsis = elide_behavior_ == gfx::NO_ELIDE || 496 elide_behavior_ == gfx::FADE_TAIL; 497 if (multi_line_ || forbid_ellipsis) { 498 *paint_text = layout_text_; 499 } else { 500 *paint_text = gfx::ElideText(layout_text_, font_list_, 501 GetAvailableRect().width(), elide_behavior_); 502 } 503 504 *text_bounds = GetTextBounds(); 505 *flags = ComputeDrawStringFlags(); 506 // TODO(msw): Elide multi-line text with ElideRectangleText instead. 507 if (!multi_line_ || forbid_ellipsis) 508 *flags |= gfx::Canvas::NO_ELLIPSIS; 509 } 510 511 void Label::UpdateColorsFromTheme(const ui::NativeTheme* theme) { 512 if (!enabled_color_set_) { 513 requested_enabled_color_ = theme->GetSystemColor( 514 ui::NativeTheme::kColorId_LabelEnabledColor); 515 } 516 if (!disabled_color_set_) { 517 requested_disabled_color_ = theme->GetSystemColor( 518 ui::NativeTheme::kColorId_LabelDisabledColor); 519 } 520 if (!background_color_set_) { 521 background_color_ = theme->GetSystemColor( 522 ui::NativeTheme::kColorId_LabelBackgroundColor); 523 } 524 RecalculateColors(); 525 } 526 527 void Label::ResetCachedSize() { 528 text_size_valid_ = false; 529 cached_heights_cursor_ = 0; 530 for (int i = 0; i < kCachedSizeLimit; ++i) 531 cached_heights_[i] = gfx::Size(); 532 } 533 534 bool Label::ShouldShowDefaultTooltip() const { 535 const gfx::Size text_size = GetTextSize(); 536 const gfx::Size size = GetContentsBounds().size(); 537 return !obscured() && (text_size.width() > size.width() || 538 (multi_line_ && text_size.height() > size.height())); 539 } 540 541 } // namespace views 542