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.
5 #include "ui/views/controls/label.h"
12 #include "base/i18n/rtl.h"
13 #include "base/logging.h"
14 #include "base/profiler/scoped_tracker.h"
15 #include "base/strings/string_split.h"
16 #include "base/strings/string_util.h"
17 #include "base/strings/utf_string_conversions.h"
18 #include "ui/accessibility/ax_view_state.h"
19 #include "ui/gfx/canvas.h"
20 #include "ui/gfx/color_utils.h"
21 #include "ui/gfx/geometry/insets.h"
22 #include "ui/gfx/text_elider.h"
23 #include "ui/gfx/text_utils.h"
24 #include "ui/gfx/utf16_indexing.h"
25 #include "ui/native_theme/native_theme.h"
26 #include "ui/views/background.h"
30 const int kCachedSizeLimit
= 10;
31 const base::char16 kPasswordReplacementChar
= '*';
38 const char Label::kViewClassName
[] = "Label";
39 const int Label::kFocusBorderPadding
= 1;
42 Init(base::string16(), gfx::FontList());
45 Label::Label(const base::string16
& text
) {
46 Init(text
, gfx::FontList());
49 Label::Label(const base::string16
& text
, const gfx::FontList
& font_list
) {
50 Init(text
, font_list
);
56 void Label::SetFontList(const gfx::FontList
& font_list
) {
57 is_first_paint_text_
= true;
58 font_list_
= font_list
;
60 PreferredSizeChanged();
64 void Label::SetText(const base::string16
& text
) {
66 SetTextInternal(text
);
69 void Label::SetTextInternal(const base::string16
& text
) {
70 is_first_paint_text_
= true;
74 size_t obscured_text_length
=
75 static_cast<size_t>(gfx::UTF16IndexToOffset(text_
, 0, text_
.length()));
76 layout_text_
.assign(obscured_text_length
, kPasswordReplacementChar
);
82 PreferredSizeChanged();
86 void Label::SetAutoColorReadabilityEnabled(bool enabled
) {
87 is_first_paint_text_
= true;
88 auto_color_readability_
= enabled
;
92 void Label::SetEnabledColor(SkColor color
) {
93 is_first_paint_text_
= true;
94 requested_enabled_color_
= color
;
95 enabled_color_set_
= true;
99 void Label::SetDisabledColor(SkColor color
) {
100 is_first_paint_text_
= true;
101 requested_disabled_color_
= color
;
102 disabled_color_set_
= true;
106 void Label::SetBackgroundColor(SkColor color
) {
107 is_first_paint_text_
= true;
108 background_color_
= color
;
109 background_color_set_
= true;
111 cached_draw_params_
.text
.clear();
114 void Label::SetShadows(const gfx::ShadowValues
& shadows
) {
115 is_first_paint_text_
= true;
120 void Label::SetSubpixelRenderingEnabled(bool subpixel_rendering_enabled
) {
121 is_first_paint_text_
= true;
122 subpixel_rendering_enabled_
= subpixel_rendering_enabled
;
125 void Label::SetHorizontalAlignment(gfx::HorizontalAlignment alignment
) {
126 is_first_paint_text_
= true;
127 // If the UI layout is right-to-left, flip the alignment direction.
128 if (base::i18n::IsRTL() &&
129 (alignment
== gfx::ALIGN_LEFT
|| alignment
== gfx::ALIGN_RIGHT
)) {
130 alignment
= (alignment
== gfx::ALIGN_LEFT
) ?
131 gfx::ALIGN_RIGHT
: gfx::ALIGN_LEFT
;
133 if (horizontal_alignment_
!= alignment
) {
134 horizontal_alignment_
= alignment
;
139 gfx::HorizontalAlignment
Label::GetHorizontalAlignment() const {
140 if (horizontal_alignment_
!= gfx::ALIGN_TO_HEAD
)
141 return horizontal_alignment_
;
143 const base::i18n::TextDirection dir
=
144 base::i18n::GetFirstStrongCharacterDirection(layout_text_
);
145 return dir
== base::i18n::RIGHT_TO_LEFT
? gfx::ALIGN_RIGHT
: gfx::ALIGN_LEFT
;
148 void Label::SetLineHeight(int height
) {
149 is_first_paint_text_
= true;
150 if (height
!= line_height_
) {
151 line_height_
= height
;
153 PreferredSizeChanged();
158 void Label::SetMultiLine(bool multi_line
) {
159 is_first_paint_text_
= true;
160 DCHECK(!multi_line
|| (elide_behavior_
== gfx::ELIDE_TAIL
||
161 elide_behavior_
== gfx::NO_ELIDE
));
162 if (multi_line
!= multi_line_
) {
163 multi_line_
= multi_line
;
165 PreferredSizeChanged();
170 void Label::SetObscured(bool obscured
) {
171 is_first_paint_text_
= true;
172 if (obscured
!= obscured_
) {
173 obscured_
= obscured
;
174 SetTextInternal(text_
);
178 void Label::SetAllowCharacterBreak(bool allow_character_break
) {
179 is_first_paint_text_
= true;
180 if (allow_character_break
!= allow_character_break_
) {
181 allow_character_break_
= allow_character_break
;
183 PreferredSizeChanged();
188 void Label::SetElideBehavior(gfx::ElideBehavior elide_behavior
) {
189 is_first_paint_text_
= true;
190 DCHECK(!multi_line_
|| (elide_behavior_
== gfx::ELIDE_TAIL
||
191 elide_behavior_
== gfx::NO_ELIDE
));
192 if (elide_behavior
!= elide_behavior_
) {
193 elide_behavior_
= elide_behavior
;
195 PreferredSizeChanged();
200 void Label::SetTooltipText(const base::string16
& tooltip_text
) {
201 DCHECK(handles_tooltips_
);
202 tooltip_text_
= tooltip_text
;
205 void Label::SetHandlesTooltips(bool enabled
) {
206 handles_tooltips_
= enabled
;
209 void Label::SizeToFit(int max_width
) {
212 std::vector
<base::string16
> lines
;
213 base::SplitString(layout_text_
, '\n', &lines
);
216 for (std::vector
<base::string16
>::const_iterator iter
= lines
.begin();
217 iter
!= lines
.end(); ++iter
) {
218 label_width
= std::max(label_width
, gfx::GetStringWidth(*iter
, font_list_
));
221 label_width
+= GetInsets().width();
224 label_width
= std::min(label_width
, max_width
);
226 SetBounds(x(), y(), label_width
, 0);
227 SizeToPreferredSize();
230 const base::string16
& Label::GetLayoutTextForTesting() const {
234 gfx::Insets
Label::GetInsets() const {
235 gfx::Insets insets
= View::GetInsets();
237 insets
+= gfx::Insets(kFocusBorderPadding
, kFocusBorderPadding
,
238 kFocusBorderPadding
, kFocusBorderPadding
);
243 int Label::GetBaseline() const {
244 return GetInsets().top() + font_list_
.GetBaseline();
247 gfx::Size
Label::GetPreferredSize() const {
248 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed.
249 tracked_objects::ScopedTracker
tracking_profile(
250 FROM_HERE_WITH_EXPLICIT_FUNCTION("431326 Label::GetPreferredSize"));
252 // Return a size of (0, 0) if the label is not visible and if the
253 // collapse_when_hidden_ flag is set.
254 // TODO(munjal): This logic probably belongs to the View class. But for now,
255 // put it here since putting it in View class means all inheriting classes
256 // need ot respect the collapse_when_hidden_ flag.
257 if (!visible() && collapse_when_hidden_
)
260 gfx::Size
size(GetTextSize());
261 gfx::Insets insets
= GetInsets();
262 size
.Enlarge(insets
.width(), insets
.height());
266 gfx::Size
Label::GetMinimumSize() const {
267 gfx::Size
text_size(GetTextSize());
268 if ((!visible() && collapse_when_hidden_
) || text_size
.IsEmpty())
271 gfx::Size
size(gfx::GetStringWidth(base::string16(gfx::kEllipsisUTF16
),
273 font_list_
.GetHeight());
274 size
.SetToMin(text_size
); // The actual text may be shorter than an ellipsis.
275 gfx::Insets insets
= GetInsets();
276 size
.Enlarge(insets
.width(), insets
.height());
280 int Label::GetHeightForWidth(int w
) const {
282 return View::GetHeightForWidth(w
);
284 w
= std::max(0, w
- GetInsets().width());
286 for (size_t i
= 0; i
< cached_heights_
.size(); ++i
) {
287 const gfx::Size
& s
= cached_heights_
[i
];
289 return s
.height() + GetInsets().height();
294 int h
= font_list_
.GetHeight();
295 // Flags returned in the cached |DrawStringParams| has a different value
296 // from the result of |ComputeDrawStringFlags()|. The latter is needed here.
297 const int flags
= ComputeDrawStringFlags();
298 gfx::Canvas::SizeStringInt(
299 layout_text_
, font_list_
, &w
, &h
, line_height_
, flags
);
300 cached_heights_
[cached_heights_cursor_
] = gfx::Size(cache_width
, h
);
301 cached_heights_cursor_
= (cached_heights_cursor_
+ 1) % kCachedSizeLimit
;
302 return h
+ GetInsets().height();
305 const char* Label::GetClassName() const {
306 return kViewClassName
;
309 View
* Label::GetTooltipHandlerForPoint(const gfx::Point
& point
) {
310 if (!handles_tooltips_
||
311 (tooltip_text_
.empty() && !ShouldShowDefaultTooltip()))
314 return HitTestPoint(point
) ? this : NULL
;
317 bool Label::CanProcessEventsWithinSubtree() const {
318 // Send events to the parent view for handling.
322 void Label::GetAccessibleState(ui::AXViewState
* state
) {
323 state
->role
= ui::AX_ROLE_STATIC_TEXT
;
324 state
->AddStateFlag(ui::AX_STATE_READ_ONLY
);
325 state
->name
= layout_text_
;
328 bool Label::GetTooltipText(const gfx::Point
& p
, base::string16
* tooltip
) const {
329 if (!handles_tooltips_
)
332 if (!tooltip_text_
.empty()) {
333 tooltip
->assign(tooltip_text_
);
337 if (ShouldShowDefaultTooltip()) {
338 *tooltip
= layout_text_
;
345 void Label::PaintText(gfx::Canvas
* canvas
,
346 const base::string16
& text
,
347 const gfx::Rect
& text_bounds
,
349 SkColor color
= enabled() ? actual_enabled_color_
: actual_disabled_color_
;
350 if (elide_behavior_
== gfx::FADE_TAIL
&&
351 text_bounds
.width() < GetTextSize().width()) {
352 canvas
->DrawFadedString(text
, font_list_
, color
, text_bounds
, flags
);
354 canvas
->DrawStringRectWithShadows(text
, font_list_
, color
, text_bounds
,
355 line_height_
, flags
, shadows_
);
359 gfx::Rect focus_bounds
= text_bounds
;
360 focus_bounds
.Inset(-kFocusBorderPadding
, -kFocusBorderPadding
);
361 canvas
->DrawFocusRect(focus_bounds
);
365 gfx::Size
Label::GetTextSize() const {
366 if (!text_size_valid_
) {
367 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed.
368 tracked_objects::ScopedTracker
tracking_profile1(
369 FROM_HERE_WITH_EXPLICIT_FUNCTION("431326 Label::GetTextSize1"));
371 // For single-line strings, we supply the largest possible width, because
372 // while adding NO_ELLIPSIS to the flags works on Windows for forcing
373 // SizeStringInt() to calculate the desired width, it doesn't seem to work
375 int w
= multi_line_
?
376 GetAvailableRect().width() : std::numeric_limits
<int>::max();
377 int h
= font_list_
.GetHeight();
378 // For single-line strings, ignore the available width and calculate how
379 // wide the text wants to be.
380 // Call |ComputeDrawStringFlags()| instead of |CalculateDrawStringParams()|
381 // here since the latter calls this function and causes infinite recursion.
382 int flags
= ComputeDrawStringFlags();
384 flags
|= gfx::Canvas::NO_ELLIPSIS
;
386 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is
388 tracked_objects::ScopedTracker
tracking_profile2(
389 FROM_HERE_WITH_EXPLICIT_FUNCTION("431326 Label::GetTextSize2"));
391 gfx::Canvas::SizeStringInt(layout_text_
, font_list_
, &w
, &h
, line_height_
,
394 text_size_
.SetSize(w
, h
);
395 const gfx::Insets shadow_margin
= -gfx::ShadowValue::GetMargin(shadows_
);
396 text_size_
.Enlarge(shadow_margin
.width(), shadow_margin
.height());
397 text_size_valid_
= true;
403 void Label::OnBoundsChanged(const gfx::Rect
& previous_bounds
) {
404 text_size_valid_
&= !multi_line_
;
405 cached_draw_params_
.text
.clear();
408 void Label::OnPaint(gfx::Canvas
* canvas
) {
409 OnPaintBackground(canvas
);
410 // We skip painting the focus border because it is being handled seperately by
411 // some subclasses of Label. We do not want View's focus border painting to
412 // interfere with that.
413 OnPaintBorder(canvas
);
414 if (layout_text_
.empty())
417 const DrawStringParams
* params
= CalculateDrawStringParams();
418 if (is_first_paint_text_
) {
419 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed.
420 tracked_objects::ScopedTracker
tracking_profile(
421 FROM_HERE_WITH_EXPLICIT_FUNCTION("431326 Label::PaintText first"));
423 is_first_paint_text_
= false;
424 PaintText(canvas
, params
->text
, params
->bounds
, params
->flags
);
426 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed.
427 tracked_objects::ScopedTracker
tracking_profile(
428 FROM_HERE_WITH_EXPLICIT_FUNCTION("431326 Label::PaintText not first"));
430 PaintText(canvas
, params
->text
, params
->bounds
, params
->flags
);
434 void Label::OnNativeThemeChanged(const ui::NativeTheme
* theme
) {
435 UpdateColorsFromTheme(theme
);
438 void Label::OnDeviceScaleFactorChanged(float device_scale_factor
) {
439 View::OnDeviceScaleFactorChanged(device_scale_factor
);
440 // When the device scale factor is changed, some font rendering parameters is
441 // changed (especially, hinting). The bounding box of the text has to be
442 // re-computed based on the new parameters. See crbug.com/441439
444 PreferredSizeChanged();
448 void Label::Init(const base::string16
& text
, const gfx::FontList
& font_list
) {
449 font_list_
= font_list
;
450 enabled_color_set_
= disabled_color_set_
= background_color_set_
= false;
451 subpixel_rendering_enabled_
= true;
452 auto_color_readability_
= true;
453 UpdateColorsFromTheme(ui::NativeTheme::instance());
454 horizontal_alignment_
= gfx::ALIGN_CENTER
;
458 allow_character_break_
= false;
459 elide_behavior_
= gfx::ELIDE_TAIL
;
460 handles_tooltips_
= true;
461 collapse_when_hidden_
= false;
462 cached_heights_
.resize(kCachedSizeLimit
);
464 is_first_paint_text_
= true;
469 void Label::RecalculateColors() {
470 actual_enabled_color_
= auto_color_readability_
?
471 color_utils::GetReadableColor(requested_enabled_color_
,
473 requested_enabled_color_
;
474 actual_disabled_color_
= auto_color_readability_
?
475 color_utils::GetReadableColor(requested_disabled_color_
,
477 requested_disabled_color_
;
480 gfx::Rect
Label::GetTextBounds() const {
481 gfx::Rect
available(GetAvailableRect());
482 gfx::Size
text_size(GetTextSize());
483 text_size
.set_width(std::min(available
.width(), text_size
.width()));
484 gfx::Point
origin(GetInsets().left(), GetInsets().top());
485 switch (GetHorizontalAlignment()) {
486 case gfx::ALIGN_LEFT
:
488 case gfx::ALIGN_CENTER
:
489 // Put any extra margin pixel on the left to match the legacy behavior
490 // from the use of GetTextExtentPoint32() on Windows.
491 origin
.Offset((available
.width() + 1 - text_size
.width()) / 2, 0);
493 case gfx::ALIGN_RIGHT
:
494 origin
.set_x(available
.right() - text_size
.width());
501 text_size
.set_height(available
.height());
502 // Support vertical centering of multi-line labels: http://crbug.com/429595
503 origin
.Offset(0, std::max(0, (available
.height() - text_size
.height())) / 2);
504 return gfx::Rect(origin
, text_size
);
507 int Label::ComputeDrawStringFlags() const {
510 // We can't use subpixel rendering if the background is non-opaque.
511 if (SkColorGetA(background_color_
) != 0xFF || !subpixel_rendering_enabled_
)
512 flags
|= gfx::Canvas::NO_SUBPIXEL_RENDERING
;
514 base::i18n::TextDirection direction
=
515 base::i18n::GetFirstStrongCharacterDirection(layout_text_
);
516 if (direction
== base::i18n::RIGHT_TO_LEFT
)
517 flags
|= gfx::Canvas::FORCE_RTL_DIRECTIONALITY
;
519 flags
|= gfx::Canvas::FORCE_LTR_DIRECTIONALITY
;
521 switch (GetHorizontalAlignment()) {
522 case gfx::ALIGN_LEFT
:
523 flags
|= gfx::Canvas::TEXT_ALIGN_LEFT
;
525 case gfx::ALIGN_CENTER
:
526 flags
|= gfx::Canvas::TEXT_ALIGN_CENTER
;
528 case gfx::ALIGN_RIGHT
:
529 flags
|= gfx::Canvas::TEXT_ALIGN_RIGHT
;
539 flags
|= gfx::Canvas::MULTI_LINE
;
541 // Don't elide multiline labels on Linux.
542 // Todo(davemoore): Do we depend on eliding multiline text?
543 // Pango insists on limiting the number of lines to one if text is
544 // elided. You can get around this if you can pass a maximum height
545 // but we don't currently have that data when we call the pango code.
546 flags
|= gfx::Canvas::NO_ELLIPSIS
;
548 if (allow_character_break_
)
549 flags
|= gfx::Canvas::CHARACTER_BREAK
;
554 gfx::Rect
Label::GetAvailableRect() const {
555 gfx::Rect
bounds(size());
556 bounds
.Inset(GetInsets());
560 const Label::DrawStringParams
* Label::CalculateDrawStringParams() const {
561 if (cached_draw_params_
.text
.empty()) {
562 const bool forbid_ellipsis
= elide_behavior_
== gfx::NO_ELIDE
||
563 elide_behavior_
== gfx::FADE_TAIL
;
564 if (multi_line_
|| forbid_ellipsis
) {
565 cached_draw_params_
.text
= layout_text_
;
567 cached_draw_params_
.text
= gfx::ElideText(layout_text_
, font_list_
,
568 GetAvailableRect().width(), elide_behavior_
);
571 cached_draw_params_
.bounds
= GetTextBounds();
572 cached_draw_params_
.flags
= ComputeDrawStringFlags();
573 // TODO(msw): Elide multi-line text with ElideRectangleText instead.
574 if (!multi_line_
|| forbid_ellipsis
)
575 cached_draw_params_
.flags
|= gfx::Canvas::NO_ELLIPSIS
;
578 return &cached_draw_params_
;
581 void Label::UpdateColorsFromTheme(const ui::NativeTheme
* theme
) {
582 if (!enabled_color_set_
) {
583 requested_enabled_color_
= theme
->GetSystemColor(
584 ui::NativeTheme::kColorId_LabelEnabledColor
);
586 if (!disabled_color_set_
) {
587 requested_disabled_color_
= theme
->GetSystemColor(
588 ui::NativeTheme::kColorId_LabelDisabledColor
);
590 if (!background_color_set_
) {
591 background_color_
= theme
->GetSystemColor(
592 ui::NativeTheme::kColorId_LabelBackgroundColor
);
597 void Label::ResetLayoutCache() {
598 cached_draw_params_
.text
.clear();
599 text_size_valid_
= false;
600 cached_heights_cursor_
= 0;
601 for (int i
= 0; i
< kCachedSizeLimit
; ++i
)
602 cached_heights_
[i
] = gfx::Size();
605 bool Label::ShouldShowDefaultTooltip() const {
606 const gfx::Size text_size
= GetTextSize();
607 const gfx::Size size
= GetContentsBounds().size();
608 return !obscured() && (text_size
.width() > size
.width() ||
609 (multi_line_
&& text_size
.height() > size
.height()));