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
27namespace {
28
29const int kCachedSizeLimit = 10;
30const base::char16 kPasswordReplacementChar = '*';
31
32}  // namespace
33
34namespace views {
35
36// static
37const char Label::kViewClassName[] = "Label";
38const int Label::kFocusBorderPadding = 1;
39
40Label::Label() {
41  Init(base::string16(), gfx::FontList());
42}
43
44Label::Label(const base::string16& text) {
45  Init(text, gfx::FontList());
46}
47
48Label::Label(const base::string16& text, const gfx::FontList& font_list) {
49  Init(text, font_list);
50}
51
52Label::~Label() {
53}
54
55void Label::SetFontList(const gfx::FontList& font_list) {
56  font_list_ = font_list;
57  ResetCachedSize();
58  PreferredSizeChanged();
59  SchedulePaint();
60}
61
62void Label::SetText(const base::string16& text) {
63  if (text != text_)
64    SetTextInternal(text);
65}
66
67void 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
83void Label::SetAutoColorReadabilityEnabled(bool enabled) {
84  auto_color_readability_ = enabled;
85  RecalculateColors();
86}
87
88void Label::SetEnabledColor(SkColor color) {
89  requested_enabled_color_ = color;
90  enabled_color_set_ = true;
91  RecalculateColors();
92}
93
94void Label::SetDisabledColor(SkColor color) {
95  requested_disabled_color_ = color;
96  disabled_color_set_ = true;
97  RecalculateColors();
98}
99
100void Label::SetBackgroundColor(SkColor color) {
101  background_color_ = color;
102  background_color_set_ = true;
103  RecalculateColors();
104}
105
106void Label::SetShadows(const gfx::ShadowValues& shadows) {
107  shadows_ = shadows;
108  text_size_valid_ = false;
109}
110
111void Label::SetSubpixelRenderingEnabled(bool subpixel_rendering_enabled) {
112  subpixel_rendering_enabled_ = subpixel_rendering_enabled;
113}
114
115void 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
128gfx::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
137void Label::SetLineHeight(int height) {
138  if (height != line_height_) {
139    line_height_ = height;
140    ResetCachedSize();
141    PreferredSizeChanged();
142    SchedulePaint();
143  }
144}
145
146void 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
157void Label::SetObscured(bool obscured) {
158  if (obscured != obscured_) {
159    obscured_ = obscured;
160    SetTextInternal(text_);
161  }
162}
163
164void 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
173void 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
184void Label::SetTooltipText(const base::string16& tooltip_text) {
185  tooltip_text_ = tooltip_text;
186}
187
188void 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
209const base::string16& Label::GetLayoutTextForTesting() const {
210  return layout_text_;
211}
212
213gfx::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
222int Label::GetBaseline() const {
223  return GetInsets().top() + font_list_.GetBaseline();
224}
225
226gfx::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
241gfx::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
255int 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
278const char* Label::GetClassName() const {
279  return kViewClassName;
280}
281
282View* Label::GetTooltipHandlerForPoint(const gfx::Point& point) {
283  if (tooltip_text_.empty() && !ShouldShowDefaultTooltip())
284    return NULL;
285
286  return HitTestPoint(point) ? this : NULL;
287}
288
289bool Label::CanProcessEventsWithinSubtree() const {
290  // Send events to the parent view for handling.
291  return false;
292}
293
294void 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
300bool 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
314void 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
333gfx::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
358void Label::OnBoundsChanged(const gfx::Rect& previous_bounds) {
359  text_size_valid_ &= !multi_line_;
360}
361
362void 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
376void Label::OnNativeThemeChanged(const ui::NativeTheme* theme) {
377  UpdateColorsFromTheme(theme);
378}
379
380void 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
399void 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
410gfx::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
437int 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
484gfx::Rect Label::GetAvailableRect() const {
485  gfx::Rect bounds(size());
486  bounds.Inset(GetInsets());
487  return bounds;
488}
489
490void 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
511void 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
527void 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
534bool 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