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/base/resource/resource_bundle.h"
19#include "ui/gfx/canvas.h"
20#include "ui/gfx/color_utils.h"
21#include "ui/gfx/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"
27
28namespace {
29
30const int kCachedSizeLimit = 10;
31const base::char16 kPasswordReplacementChar = '*';
32
33}  // namespace
34
35namespace views {
36
37// static
38const char Label::kViewClassName[] = "Label";
39const int Label::kFocusBorderPadding = 1;
40
41Label::Label() {
42  Init(base::string16(), gfx::FontList());
43}
44
45Label::Label(const base::string16& text) {
46  Init(text, gfx::FontList());
47}
48
49Label::Label(const base::string16& text, const gfx::FontList& font_list) {
50  Init(text, font_list);
51}
52
53Label::~Label() {
54}
55
56void Label::SetFontList(const gfx::FontList& font_list) {
57  font_list_ = font_list;
58  ResetCachedSize();
59  PreferredSizeChanged();
60  SchedulePaint();
61}
62
63void Label::SetText(const base::string16& text) {
64  if (text != text_)
65    SetTextInternal(text);
66}
67
68void Label::SetTextInternal(const base::string16& text) {
69  text_ = text;
70
71  if (is_obscured_) {
72    size_t obscured_text_length =
73        static_cast<size_t>(gfx::UTF16IndexToOffset(text_, 0, text_.length()));
74    layout_text_.assign(obscured_text_length, kPasswordReplacementChar);
75  } else {
76    layout_text_ = text_;
77  }
78
79  ResetCachedSize();
80  PreferredSizeChanged();
81  SchedulePaint();
82}
83
84void Label::SetAutoColorReadabilityEnabled(bool enabled) {
85  auto_color_readability_ = enabled;
86  RecalculateColors();
87}
88
89void Label::SetEnabledColor(SkColor color) {
90  requested_enabled_color_ = color;
91  enabled_color_set_ = true;
92  RecalculateColors();
93}
94
95void Label::SetDisabledColor(SkColor color) {
96  requested_disabled_color_ = color;
97  disabled_color_set_ = true;
98  RecalculateColors();
99}
100
101void Label::SetBackgroundColor(SkColor color) {
102  background_color_ = color;
103  background_color_set_ = true;
104  RecalculateColors();
105}
106
107void Label::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) {
108  // If the UI layout is right-to-left, flip the alignment direction.
109  if (base::i18n::IsRTL() &&
110      (alignment == gfx::ALIGN_LEFT || alignment == gfx::ALIGN_RIGHT)) {
111    alignment = (alignment == gfx::ALIGN_LEFT) ?
112        gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT;
113  }
114  if (horizontal_alignment_ != alignment) {
115    horizontal_alignment_ = alignment;
116    SchedulePaint();
117  }
118}
119
120gfx::HorizontalAlignment Label::GetHorizontalAlignment() const {
121  if (horizontal_alignment_ != gfx::ALIGN_TO_HEAD)
122    return horizontal_alignment_;
123
124  const base::i18n::TextDirection dir =
125      base::i18n::GetFirstStrongCharacterDirection(layout_text());
126  return dir == base::i18n::RIGHT_TO_LEFT ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT;
127}
128
129void Label::SetLineHeight(int height) {
130  if (height != line_height_) {
131    line_height_ = height;
132    ResetCachedSize();
133    PreferredSizeChanged();
134    SchedulePaint();
135  }
136}
137
138void Label::SetMultiLine(bool multi_line) {
139  DCHECK(!multi_line || (elide_behavior_ == gfx::ELIDE_TAIL ||
140                         elide_behavior_ == gfx::TRUNCATE));
141  if (multi_line != is_multi_line_) {
142    is_multi_line_ = multi_line;
143    ResetCachedSize();
144    PreferredSizeChanged();
145    SchedulePaint();
146  }
147}
148
149void Label::SetObscured(bool obscured) {
150  if (obscured != is_obscured_) {
151    is_obscured_ = obscured;
152    SetTextInternal(text_);
153  }
154}
155
156void Label::SetAllowCharacterBreak(bool allow_character_break) {
157  if (allow_character_break != allow_character_break_) {
158    allow_character_break_ = allow_character_break;
159    ResetCachedSize();
160    PreferredSizeChanged();
161    SchedulePaint();
162  }
163}
164
165void Label::SetElideBehavior(gfx::ElideBehavior elide_behavior) {
166  DCHECK(!is_multi_line_ || (elide_behavior_ == gfx::ELIDE_TAIL ||
167                             elide_behavior_ == gfx::TRUNCATE));
168  if (elide_behavior != elide_behavior_) {
169    elide_behavior_ = elide_behavior;
170    ResetCachedSize();
171    PreferredSizeChanged();
172    SchedulePaint();
173  }
174}
175
176void Label::SetTooltipText(const base::string16& tooltip_text) {
177  tooltip_text_ = tooltip_text;
178}
179
180void Label::SizeToFit(int max_width) {
181  DCHECK(is_multi_line_);
182
183  std::vector<base::string16> lines;
184  base::SplitString(layout_text(), '\n', &lines);
185
186  int label_width = 0;
187  for (std::vector<base::string16>::const_iterator iter = lines.begin();
188       iter != lines.end(); ++iter) {
189    label_width = std::max(label_width, gfx::GetStringWidth(*iter, font_list_));
190  }
191
192  label_width += GetInsets().width();
193
194  if (max_width > 0)
195    label_width = std::min(label_width, max_width);
196
197  SetBounds(x(), y(), label_width, 0);
198  SizeToPreferredSize();
199}
200
201gfx::Insets Label::GetInsets() const {
202  gfx::Insets insets = View::GetInsets();
203  if (focusable()) {
204    insets += gfx::Insets(kFocusBorderPadding, kFocusBorderPadding,
205                          kFocusBorderPadding, kFocusBorderPadding);
206  }
207  return insets;
208}
209
210int Label::GetBaseline() const {
211  return GetInsets().top() + font_list_.GetBaseline();
212}
213
214gfx::Size Label::GetPreferredSize() const {
215  // Return a size of (0, 0) if the label is not visible and if the
216  // collapse_when_hidden_ flag is set.
217  // TODO(munjal): This logic probably belongs to the View class. But for now,
218  // put it here since putting it in View class means all inheriting classes
219  // need ot respect the collapse_when_hidden_ flag.
220  if (!visible() && collapse_when_hidden_)
221    return gfx::Size();
222
223  gfx::Size size(GetTextSize());
224  gfx::Insets insets = GetInsets();
225  size.Enlarge(insets.width(), insets.height());
226  return size;
227}
228
229gfx::Size Label::GetMinimumSize() const {
230  gfx::Size text_size(GetTextSize());
231  if ((!visible() && collapse_when_hidden_) || text_size.IsEmpty())
232    return gfx::Size();
233
234  gfx::Size size(gfx::GetStringWidth(base::string16(gfx::kEllipsisUTF16),
235                                     font_list_),
236                 font_list_.GetHeight());
237  size.SetToMin(text_size);  // The actual text may be shorter than an ellipsis.
238  gfx::Insets insets = GetInsets();
239  size.Enlarge(insets.width(), insets.height());
240  return size;
241}
242
243int Label::GetHeightForWidth(int w) const {
244  if (!is_multi_line_)
245    return View::GetHeightForWidth(w);
246
247  w = std::max(0, w - GetInsets().width());
248
249  for (size_t i = 0; i < cached_heights_.size(); ++i) {
250    const gfx::Size& s = cached_heights_[i];
251    if (s.width() == w)
252      return s.height() + GetInsets().height();
253  }
254
255  int cache_width = w;
256
257  int h = font_list_.GetHeight();
258  const int flags = ComputeDrawStringFlags();
259  gfx::Canvas::SizeStringInt(
260      layout_text(), font_list_, &w, &h, line_height_, flags);
261  cached_heights_[cached_heights_cursor_] = gfx::Size(cache_width, h);
262  cached_heights_cursor_ = (cached_heights_cursor_ + 1) % kCachedSizeLimit;
263  return h + GetInsets().height();
264}
265
266const char* Label::GetClassName() const {
267  return kViewClassName;
268}
269
270View* Label::GetTooltipHandlerForPoint(const gfx::Point& point) {
271  // Bail out if the label does not contain the point.
272  // Note that HitTestPoint() cannot be used here as it uses
273  // Label::HitTestRect() to determine if the point hits the label; and
274  // Label::HitTestRect() always fails. Instead, default HitTestRect()
275  // implementation should be used.
276  if (!View::HitTestRect(gfx::Rect(point, gfx::Size(1, 1))))
277    return NULL;
278
279  if (tooltip_text_.empty() && !ShouldShowDefaultTooltip())
280    return NULL;
281
282  return this;
283}
284
285bool Label::CanProcessEventsWithinSubtree() const {
286  // Send events to the parent view for handling.
287  return false;
288}
289
290bool Label::GetTooltipText(const gfx::Point& p, base::string16* tooltip) const {
291  DCHECK(tooltip);
292
293  // If a tooltip has been explicitly set, use it.
294  if (!tooltip_text_.empty()) {
295    tooltip->assign(tooltip_text_);
296    return true;
297  }
298
299  // Show the full text if the text does not fit.
300  if (ShouldShowDefaultTooltip()) {
301    *tooltip = layout_text();
302    return true;
303  }
304
305  return false;
306}
307
308void Label::GetAccessibleState(ui::AXViewState* state) {
309  state->role = ui::AX_ROLE_STATIC_TEXT;
310  state->AddStateFlag(ui::AX_STATE_READ_ONLY);
311  state->name = layout_text();
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 = is_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 (!is_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_ &= !is_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  is_multi_line_ = false;
389  is_obscured_ = false;
390  allow_character_break_ = false;
391  elide_behavior_ = gfx::ELIDE_TAIL;
392  collapse_when_hidden_ = false;
393  directionality_mode_ = gfx::DIRECTIONALITY_FROM_UI;
394  cached_heights_.resize(kCachedSizeLimit);
395  ResetCachedSize();
396
397  SetText(text);
398}
399
400void Label::RecalculateColors() {
401  actual_enabled_color_ = auto_color_readability_ ?
402      color_utils::GetReadableColor(requested_enabled_color_,
403                                    background_color_) :
404      requested_enabled_color_;
405  actual_disabled_color_ = auto_color_readability_ ?
406      color_utils::GetReadableColor(requested_disabled_color_,
407                                    background_color_) :
408      requested_disabled_color_;
409}
410
411gfx::Rect Label::GetTextBounds() const {
412  gfx::Rect available(GetAvailableRect());
413  gfx::Size text_size(GetTextSize());
414  text_size.set_width(std::min(available.width(), text_size.width()));
415  gfx::Point origin(GetInsets().left(), GetInsets().top());
416  switch (GetHorizontalAlignment()) {
417    case gfx::ALIGN_LEFT:
418      break;
419    case gfx::ALIGN_CENTER:
420      // Put any extra margin pixel on the left to match the legacy behavior
421      // from the use of GetTextExtentPoint32() on Windows.
422      origin.Offset((available.width() + 1 - text_size.width()) / 2, 0);
423      break;
424    case gfx::ALIGN_RIGHT:
425      origin.set_x(available.right() - text_size.width());
426      break;
427    default:
428      NOTREACHED();
429      break;
430  }
431  origin.Offset(0, std::max(0, (available.height() - text_size.height())) / 2);
432  return gfx::Rect(origin, text_size);
433}
434
435int Label::ComputeDrawStringFlags() const {
436  int flags = 0;
437
438  // We can't use subpixel rendering if the background is non-opaque.
439  if (SkColorGetA(background_color_) != 0xFF || !subpixel_rendering_enabled_)
440    flags |= gfx::Canvas::NO_SUBPIXEL_RENDERING;
441
442  if (directionality_mode_ == gfx::DIRECTIONALITY_FORCE_LTR) {
443    flags |= gfx::Canvas::FORCE_LTR_DIRECTIONALITY;
444  } else if (directionality_mode_ == gfx::DIRECTIONALITY_FORCE_RTL) {
445    flags |= gfx::Canvas::FORCE_RTL_DIRECTIONALITY;
446  } else if (directionality_mode_ == gfx::DIRECTIONALITY_FROM_TEXT) {
447    base::i18n::TextDirection direction =
448        base::i18n::GetFirstStrongCharacterDirection(layout_text());
449    if (direction == base::i18n::RIGHT_TO_LEFT)
450      flags |= gfx::Canvas::FORCE_RTL_DIRECTIONALITY;
451    else
452      flags |= gfx::Canvas::FORCE_LTR_DIRECTIONALITY;
453  }
454
455  switch (GetHorizontalAlignment()) {
456    case gfx::ALIGN_LEFT:
457      flags |= gfx::Canvas::TEXT_ALIGN_LEFT;
458      break;
459    case gfx::ALIGN_CENTER:
460      flags |= gfx::Canvas::TEXT_ALIGN_CENTER;
461      break;
462    case gfx::ALIGN_RIGHT:
463      flags |= gfx::Canvas::TEXT_ALIGN_RIGHT;
464      break;
465    default:
466      NOTREACHED();
467      break;
468  }
469
470  if (!is_multi_line_)
471    return flags;
472
473  flags |= gfx::Canvas::MULTI_LINE;
474#if !defined(OS_WIN)
475    // Don't elide multiline labels on Linux.
476    // Todo(davemoore): Do we depend on eliding multiline text?
477    // Pango insists on limiting the number of lines to one if text is
478    // elided. You can get around this if you can pass a maximum height
479    // but we don't currently have that data when we call the pango code.
480    flags |= gfx::Canvas::NO_ELLIPSIS;
481#endif
482  if (allow_character_break_)
483    flags |= gfx::Canvas::CHARACTER_BREAK;
484
485  return flags;
486}
487
488gfx::Rect Label::GetAvailableRect() const {
489  gfx::Rect bounds(size());
490  bounds.Inset(GetInsets());
491  return bounds;
492}
493
494void Label::CalculateDrawStringParams(base::string16* paint_text,
495                                      gfx::Rect* text_bounds,
496                                      int* flags) const {
497  DCHECK(paint_text && text_bounds && flags);
498
499  const bool forbid_ellipsis = elide_behavior_ == gfx::TRUNCATE ||
500                               elide_behavior_ == gfx::FADE_TAIL;
501  if (is_multi_line_ || forbid_ellipsis) {
502    *paint_text = layout_text();
503  } else {
504    *paint_text = gfx::ElideText(layout_text(), font_list_,
505                                 GetAvailableRect().width(), elide_behavior_);
506  }
507
508  *text_bounds = GetTextBounds();
509  *flags = ComputeDrawStringFlags();
510  // TODO(msw): Elide multi-line text with ElideRectangleText instead.
511  if (!is_multi_line_ || forbid_ellipsis)
512    *flags |= gfx::Canvas::NO_ELLIPSIS;
513}
514
515void Label::UpdateColorsFromTheme(const ui::NativeTheme* theme) {
516  if (!enabled_color_set_) {
517    requested_enabled_color_ = theme->GetSystemColor(
518        ui::NativeTheme::kColorId_LabelEnabledColor);
519  }
520  if (!disabled_color_set_) {
521    requested_disabled_color_ = theme->GetSystemColor(
522        ui::NativeTheme::kColorId_LabelDisabledColor);
523  }
524  if (!background_color_set_) {
525    background_color_ = theme->GetSystemColor(
526        ui::NativeTheme::kColorId_LabelBackgroundColor);
527  }
528  RecalculateColors();
529}
530
531void Label::ResetCachedSize() {
532  text_size_valid_ = false;
533  cached_heights_cursor_ = 0;
534  for (int i = 0; i < kCachedSizeLimit; ++i)
535    cached_heights_[i] = gfx::Size();
536}
537
538bool Label::ShouldShowDefaultTooltip() const {
539  return !is_multi_line_ && !is_obscured_ &&
540         gfx::GetStringWidth(layout_text(), font_list_) >
541             GetAvailableRect().width();
542}
543
544}  // namespace views
545