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/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/geometry/insets.h"
21 #include "ui/gfx/text_elider.h"
22 #include "ui/native_theme/native_theme.h"
27 const char Label::kViewClassName
[] = "Label";
28 const int Label::kFocusBorderPadding
= 1;
31 Init(base::string16(), gfx::FontList());
34 Label::Label(const base::string16
& text
) {
35 Init(text
, gfx::FontList());
38 Label::Label(const base::string16
& text
, const gfx::FontList
& font_list
) {
39 Init(text
, font_list
);
45 void Label::SetFontList(const gfx::FontList
& font_list
) {
46 is_first_paint_text_
= true;
47 render_text_
->SetFontList(font_list
);
51 void Label::SetText(const base::string16
& new_text
) {
52 if (new_text
== text())
54 is_first_paint_text_
= true;
55 render_text_
->SetText(new_text
);
59 void Label::SetAutoColorReadabilityEnabled(bool enabled
) {
60 if (auto_color_readability_
== enabled
)
62 is_first_paint_text_
= true;
63 auto_color_readability_
= enabled
;
67 void Label::SetEnabledColor(SkColor color
) {
68 if (enabled_color_set_
&& requested_enabled_color_
== color
)
70 is_first_paint_text_
= true;
71 requested_enabled_color_
= color
;
72 enabled_color_set_
= true;
76 void Label::SetDisabledColor(SkColor color
) {
77 if (disabled_color_set_
&& requested_disabled_color_
== color
)
79 is_first_paint_text_
= true;
80 requested_disabled_color_
= color
;
81 disabled_color_set_
= true;
85 void Label::SetBackgroundColor(SkColor color
) {
86 if (background_color_set_
&& background_color_
== color
)
88 is_first_paint_text_
= true;
89 background_color_
= color
;
90 background_color_set_
= true;
94 void Label::SetShadows(const gfx::ShadowValues
& shadows
) {
95 // TODO(mukai): early exit if the specified shadows are same.
96 is_first_paint_text_
= true;
97 render_text_
->set_shadows(shadows
);
101 void Label::SetSubpixelRenderingEnabled(bool subpixel_rendering_enabled
) {
102 if (subpixel_rendering_enabled_
== subpixel_rendering_enabled
)
104 is_first_paint_text_
= true;
105 subpixel_rendering_enabled_
= subpixel_rendering_enabled
;
109 void Label::SetHorizontalAlignment(gfx::HorizontalAlignment alignment
) {
110 // If the UI layout is right-to-left, flip the alignment direction.
111 if (base::i18n::IsRTL() &&
112 (alignment
== gfx::ALIGN_LEFT
|| alignment
== gfx::ALIGN_RIGHT
)) {
113 alignment
= (alignment
== gfx::ALIGN_LEFT
) ?
114 gfx::ALIGN_RIGHT
: gfx::ALIGN_LEFT
;
116 if (horizontal_alignment() == alignment
)
118 is_first_paint_text_
= true;
119 render_text_
->SetHorizontalAlignment(alignment
);
123 void Label::SetLineHeight(int height
) {
124 if (line_height() == height
)
126 is_first_paint_text_
= true;
127 render_text_
->SetMinLineHeight(height
);
131 void Label::SetMultiLine(bool multi_line
) {
132 DCHECK(!multi_line
|| (elide_behavior_
== gfx::ELIDE_TAIL
||
133 elide_behavior_
== gfx::NO_ELIDE
));
134 if (this->multi_line() == multi_line
)
136 is_first_paint_text_
= true;
137 multi_line_
= multi_line
;
138 if (render_text_
->MultilineSupported())
139 render_text_
->SetMultiline(multi_line
);
140 render_text_
->SetReplaceNewlineCharsWithSymbols(!multi_line
);
144 void Label::SetObscured(bool obscured
) {
145 if (this->obscured() == obscured
)
147 is_first_paint_text_
= true;
148 render_text_
->SetObscured(obscured
);
152 void Label::SetAllowCharacterBreak(bool allow_character_break
) {
153 const gfx::WordWrapBehavior behavior
=
154 allow_character_break
? gfx::WRAP_LONG_WORDS
: gfx::TRUNCATE_LONG_WORDS
;
155 if (render_text_
->word_wrap_behavior() == behavior
)
157 render_text_
->SetWordWrapBehavior(behavior
);
159 is_first_paint_text_
= true;
164 void Label::SetElideBehavior(gfx::ElideBehavior elide_behavior
) {
165 DCHECK(!multi_line() || (elide_behavior_
== gfx::ELIDE_TAIL
||
166 elide_behavior_
== gfx::NO_ELIDE
));
167 if (elide_behavior_
== elide_behavior
)
169 is_first_paint_text_
= true;
170 elide_behavior_
= elide_behavior
;
174 void Label::SetTooltipText(const base::string16
& tooltip_text
) {
175 DCHECK(handles_tooltips_
);
176 tooltip_text_
= tooltip_text
;
179 void Label::SetHandlesTooltips(bool enabled
) {
180 handles_tooltips_
= enabled
;
183 void Label::SizeToFit(int max_width
) {
184 DCHECK(multi_line());
185 max_width_
= max_width
;
186 SizeToPreferredSize();
189 base::string16
Label::GetDisplayTextForTesting() {
191 MaybeBuildRenderTextLines();
192 base::string16 result
;
195 result
.append(lines_
[0]->GetDisplayText());
196 for (size_t i
= 1; i
< lines_
.size(); ++i
) {
197 result
.append(1, '\n');
198 result
.append(lines_
[i
]->GetDisplayText());
203 gfx::Insets
Label::GetInsets() const {
204 gfx::Insets insets
= View::GetInsets();
206 insets
+= gfx::Insets(kFocusBorderPadding
, kFocusBorderPadding
,
207 kFocusBorderPadding
, kFocusBorderPadding
);
212 int Label::GetBaseline() const {
213 return GetInsets().top() + font_list().GetBaseline();
216 gfx::Size
Label::GetPreferredSize() const {
217 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed.
218 tracked_objects::ScopedTracker
tracking_profile(
219 FROM_HERE_WITH_EXPLICIT_FUNCTION("431326 Label::GetPreferredSize"));
221 // Return a size of (0, 0) if the label is not visible and if the
222 // |collapse_when_hidden_| flag is set.
223 // TODO(munjal): This logic probably belongs to the View class. But for now,
224 // put it here since putting it in View class means all inheriting classes
225 // need to respect the |collapse_when_hidden_| flag.
226 if (!visible() && collapse_when_hidden_
)
229 if (multi_line() && max_width_
!= 0 && !text().empty())
230 return gfx::Size(max_width_
, GetHeightForWidth(max_width_
));
232 gfx::Size
size(GetTextSize());
233 const gfx::Insets insets
= GetInsets();
234 size
.Enlarge(insets
.width(), insets
.height());
238 gfx::Size
Label::GetMinimumSize() const {
239 if (!visible() && collapse_when_hidden_
)
242 gfx::Size
size(0, font_list().GetHeight());
243 if (elide_behavior_
== gfx::ELIDE_HEAD
||
244 elide_behavior_
== gfx::ELIDE_MIDDLE
||
245 elide_behavior_
== gfx::ELIDE_TAIL
||
246 elide_behavior_
== gfx::ELIDE_EMAIL
) {
247 size
.set_width(gfx::Canvas::GetStringWidth(
248 base::string16(gfx::kEllipsisUTF16
), font_list()));
251 size
.SetToMin(GetTextSize());
252 size
.Enlarge(GetInsets().width(), GetInsets().height());
256 int Label::GetHeightForWidth(int w
) const {
257 if (!visible() && collapse_when_hidden_
)
260 w
-= GetInsets().width();
262 if (!multi_line() || text().empty() || w
<= 0) {
263 height
= std::max(line_height(), font_list().GetHeight());
264 } else if (render_text_
->MultilineSupported()) {
265 // SetDisplayRect() has a side effect for later calls of GetStringSize().
266 // Be careful to invoke |render_text_->SetDisplayRect(gfx::Rect())| to
267 // cancel this effect before the next time GetStringSize() is called.
268 // It would be beneficial not to cancel here, considering that some layout
269 // managers invoke GetHeightForWidth() for the same width multiple times
270 // and |render_text_| can cache the height.
271 render_text_
->SetDisplayRect(gfx::Rect(0, 0, w
, 0));
272 height
= render_text_
->GetStringSize().height();
274 std::vector
<base::string16
> lines
= GetLinesForWidth(w
);
275 height
= lines
.size() * std::max(line_height(), font_list().GetHeight());
277 height
-= gfx::ShadowValue::GetMargin(render_text_
->shadows()).height();
278 return height
+ GetInsets().height();
281 void Label::Layout() {
285 const char* Label::GetClassName() const {
286 return kViewClassName
;
289 View
* Label::GetTooltipHandlerForPoint(const gfx::Point
& point
) {
290 if (!handles_tooltips_
||
291 (tooltip_text_
.empty() && !ShouldShowDefaultTooltip()))
294 return HitTestPoint(point
) ? this : NULL
;
297 bool Label::CanProcessEventsWithinSubtree() const {
298 // Send events to the parent view for handling.
302 void Label::GetAccessibleState(ui::AXViewState
* state
) {
303 state
->role
= ui::AX_ROLE_STATIC_TEXT
;
304 state
->AddStateFlag(ui::AX_STATE_READ_ONLY
);
305 // Note that |render_text_| is never elided (see the comment in Init() too).
306 state
->name
= render_text_
->GetDisplayText();
309 bool Label::GetTooltipText(const gfx::Point
& p
, base::string16
* tooltip
) const {
310 if (!handles_tooltips_
)
313 if (!tooltip_text_
.empty()) {
314 tooltip
->assign(tooltip_text_
);
318 if (ShouldShowDefaultTooltip()) {
319 // Note that |render_text_| is never elided (see the comment in Init() too).
320 tooltip
->assign(render_text_
->GetDisplayText());
327 void Label::OnEnabledChanged() {
331 scoped_ptr
<gfx::RenderText
> Label::CreateRenderText(
332 const base::string16
& text
,
333 gfx::HorizontalAlignment alignment
,
334 gfx::DirectionalityMode directionality
,
335 gfx::ElideBehavior elide_behavior
) {
336 scoped_ptr
<gfx::RenderText
> render_text(
337 render_text_
->CreateInstanceOfSameType());
338 render_text
->SetHorizontalAlignment(alignment
);
339 render_text
->SetDirectionalityMode(directionality
);
340 render_text
->SetElideBehavior(elide_behavior
);
341 render_text
->SetObscured(obscured());
342 render_text
->SetMinLineHeight(line_height());
343 render_text
->SetFontList(font_list());
344 render_text
->set_shadows(shadows());
345 render_text
->SetCursorEnabled(false);
346 render_text
->SetText(text
);
347 return render_text
.Pass();
350 void Label::PaintText(gfx::Canvas
* canvas
) {
351 MaybeBuildRenderTextLines();
352 for (size_t i
= 0; i
< lines_
.size(); ++i
)
353 lines_
[i
]->Draw(canvas
);
356 void Label::OnBoundsChanged(const gfx::Rect
& previous_bounds
) {
357 if (previous_bounds
.size() != size())
361 void Label::OnPaint(gfx::Canvas
* canvas
) {
362 View::OnPaint(canvas
);
363 if (is_first_paint_text_
) {
364 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed.
365 tracked_objects::ScopedTracker
tracking_profile(
366 FROM_HERE_WITH_EXPLICIT_FUNCTION("431326 Label::PaintText first"));
368 is_first_paint_text_
= false;
371 // TODO(vadimt): Remove ScopedTracker below once crbug.com/431326 is fixed.
372 tracked_objects::ScopedTracker
tracking_profile(
373 FROM_HERE_WITH_EXPLICIT_FUNCTION("431326 Label::PaintText not first"));
378 canvas
->DrawFocusRect(GetFocusBounds());
381 void Label::OnNativeThemeChanged(const ui::NativeTheme
* theme
) {
382 UpdateColorsFromTheme(theme
);
385 void Label::OnDeviceScaleFactorChanged(float device_scale_factor
) {
386 View::OnDeviceScaleFactorChanged(device_scale_factor
);
387 // When the device scale factor is changed, some font rendering parameters is
388 // changed (especially, hinting). The bounding box of the text has to be
389 // re-computed based on the new parameters. See crbug.com/441439
393 void Label::VisibilityChanged(View
* starting_from
, bool is_visible
) {
398 void Label::Init(const base::string16
& text
, const gfx::FontList
& font_list
) {
399 render_text_
.reset(gfx::RenderText::CreateInstance());
400 render_text_
->SetHorizontalAlignment(gfx::ALIGN_CENTER
);
401 render_text_
->SetDirectionalityMode(gfx::DIRECTIONALITY_FROM_TEXT
);
402 // NOTE: |render_text_| should not be elided at all. This is used to keep some
403 // properties and to compute the size of the string.
404 render_text_
->SetElideBehavior(gfx::NO_ELIDE
);
405 render_text_
->SetFontList(font_list
);
406 render_text_
->SetCursorEnabled(false);
407 render_text_
->SetWordWrapBehavior(gfx::TRUNCATE_LONG_WORDS
);
409 elide_behavior_
= gfx::ELIDE_TAIL
;
410 enabled_color_set_
= disabled_color_set_
= background_color_set_
= false;
411 subpixel_rendering_enabled_
= true;
412 auto_color_readability_
= true;
414 UpdateColorsFromTheme(ui::NativeTheme::instance());
415 handles_tooltips_
= true;
416 collapse_when_hidden_
= false;
418 is_first_paint_text_
= true;
422 void Label::ResetLayout() {
424 PreferredSizeChanged();
429 void Label::MaybeBuildRenderTextLines() {
433 gfx::Rect rect
= GetContentsBounds();
435 rect
.Inset(kFocusBorderPadding
, kFocusBorderPadding
);
439 gfx::HorizontalAlignment alignment
= horizontal_alignment();
440 gfx::DirectionalityMode directionality
= render_text_
->directionality_mode();
442 // Force the directionality and alignment of the first line on other lines.
444 render_text_
->GetDisplayTextDirection() == base::i18n::RIGHT_TO_LEFT
;
445 if (alignment
== gfx::ALIGN_TO_HEAD
)
446 alignment
= rtl
? gfx::ALIGN_RIGHT
: gfx::ALIGN_LEFT
;
448 rtl
? gfx::DIRECTIONALITY_FORCE_RTL
: gfx::DIRECTIONALITY_FORCE_LTR
;
451 // Text eliding is not supported for multi-lined Labels.
452 // TODO(mukai): Add multi-lined elided text support.
453 gfx::ElideBehavior elide_behavior
=
454 multi_line() ? gfx::NO_ELIDE
: elide_behavior_
;
455 if (!multi_line() || render_text_
->MultilineSupported()) {
456 scoped_ptr
<gfx::RenderText
> render_text
=
457 CreateRenderText(text(), alignment
, directionality
, elide_behavior
);
458 render_text
->SetDisplayRect(rect
);
459 render_text
->SetMultiline(multi_line());
460 render_text
->SetWordWrapBehavior(render_text_
->word_wrap_behavior());
461 lines_
.push_back(render_text
.release());
463 std::vector
<base::string16
> lines
= GetLinesForWidth(rect
.width());
464 if (lines
.size() > 1)
465 rect
.set_height(std::max(line_height(), font_list().GetHeight()));
467 const int bottom
= GetContentsBounds().bottom();
468 for (size_t i
= 0; i
< lines
.size() && rect
.y() <= bottom
; ++i
) {
469 scoped_ptr
<gfx::RenderText
> line
=
470 CreateRenderText(lines
[i
], alignment
, directionality
, elide_behavior
);
471 line
->SetDisplayRect(rect
);
472 lines_
.push_back(line
.release());
473 rect
.set_y(rect
.y() + rect
.height());
475 // Append the remaining text to the last visible line.
476 for (size_t i
= lines_
.size(); i
< lines
.size(); ++i
)
477 lines_
.back()->SetText(lines_
.back()->text() + lines
[i
]);
482 gfx::Rect
Label::GetFocusBounds() {
483 MaybeBuildRenderTextLines();
485 gfx::Rect focus_bounds
;
486 if (lines_
.empty()) {
487 focus_bounds
= gfx::Rect(GetTextSize());
489 for (size_t i
= 0; i
< lines_
.size(); ++i
) {
491 origin
+= lines_
[i
]->GetLineOffset(0);
492 focus_bounds
.Union(gfx::Rect(origin
, lines_
[i
]->GetStringSize()));
496 focus_bounds
.Inset(-kFocusBorderPadding
, -kFocusBorderPadding
);
497 focus_bounds
.Intersect(GetLocalBounds());
501 std::vector
<base::string16
> Label::GetLinesForWidth(int width
) const {
502 std::vector
<base::string16
> lines
;
503 // |width| can be 0 when getting the default text size, in that case
504 // the ideal lines (i.e. broken at newline characters) are wanted.
506 base::SplitString(render_text_
->GetDisplayText(), '\n', &lines
);
508 gfx::ElideRectangleText(render_text_
->GetDisplayText(), font_list(), width
,
509 std::numeric_limits
<int>::max(),
510 render_text_
->word_wrap_behavior(), &lines
);
515 gfx::Size
Label::GetTextSize() const {
517 if (text().empty()) {
518 size
= gfx::Size(0, std::max(line_height(), font_list().GetHeight()));
519 } else if (!multi_line() || render_text_
->MultilineSupported()) {
520 // Cancel the display rect of |render_text_|. The display rect may be
521 // specified in GetHeightForWidth(), and specifying empty Rect cancels
522 // its effect. See also the comment in GetHeightForWidth().
523 // TODO(mukai): use gfx::Rect() to compute the ideal size rather than
524 // the current width(). See crbug.com/468494, crbug.com/467526, and
525 // the comment for MultilinePreferredSizeTest in label_unittest.cc.
526 render_text_
->SetDisplayRect(gfx::Rect(0, 0, width(), 0));
527 size
= render_text_
->GetStringSize();
529 // Get the natural text size, unelided and only wrapped on newlines.
530 std::vector
<base::string16
> lines
= GetLinesForWidth(width());
531 scoped_ptr
<gfx::RenderText
> render_text(gfx::RenderText::CreateInstance());
532 render_text
->SetFontList(font_list());
533 for (size_t i
= 0; i
< lines
.size(); ++i
) {
534 render_text
->SetText(lines
[i
]);
535 const gfx::Size line
= render_text
->GetStringSize();
536 size
.set_width(std::max(size
.width(), line
.width()));
537 size
.set_height(std::max(line_height(), size
.height() + line
.height()));
540 const gfx::Insets shadow_margin
= -gfx::ShadowValue::GetMargin(shadows());
541 size
.Enlarge(shadow_margin
.width(), shadow_margin
.height());
545 void Label::RecalculateColors() {
546 actual_enabled_color_
= auto_color_readability_
?
547 color_utils::GetReadableColor(requested_enabled_color_
,
549 requested_enabled_color_
;
550 actual_disabled_color_
= auto_color_readability_
?
551 color_utils::GetReadableColor(requested_disabled_color_
,
553 requested_disabled_color_
;
555 SkColor color
= enabled() ? actual_enabled_color_
: actual_disabled_color_
;
556 bool subpixel_rendering_suppressed
=
557 SkColorGetA(background_color_
) != 0xFF || !subpixel_rendering_enabled_
;
558 for (size_t i
= 0; i
< lines_
.size(); ++i
) {
559 lines_
[i
]->SetColor(color
);
560 lines_
[i
]->set_subpixel_rendering_suppressed(subpixel_rendering_suppressed
);
565 void Label::UpdateColorsFromTheme(const ui::NativeTheme
* theme
) {
566 if (!enabled_color_set_
) {
567 requested_enabled_color_
= theme
->GetSystemColor(
568 ui::NativeTheme::kColorId_LabelEnabledColor
);
570 if (!disabled_color_set_
) {
571 requested_disabled_color_
= theme
->GetSystemColor(
572 ui::NativeTheme::kColorId_LabelDisabledColor
);
574 if (!background_color_set_
) {
575 background_color_
= theme
->GetSystemColor(
576 ui::NativeTheme::kColorId_LabelBackgroundColor
);
581 bool Label::ShouldShowDefaultTooltip() const {
582 const gfx::Size text_size
= GetTextSize();
583 const gfx::Size size
= GetContentsBounds().size();
584 return !obscured() && (text_size
.width() > size
.width() ||
585 (multi_line() && text_size
.height() > size
.height()));