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