blob: 96cc275c2d128f421739a944c04a684aae5c548c [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/controls/styled_label.h"
#include <vector>
#include "base/strings/string_util.h"
#include "ui/gfx/text_elider.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/styled_label_listener.h"
namespace views {
namespace {
// Calculates the height of a line of text. Currently returns the height of
// a label.
int CalculateLineHeight() {
Label label;
return label.GetPreferredSize().height();
}
scoped_ptr<Label> CreateLabelRange(
const string16& text,
const StyledLabel::RangeStyleInfo& style_info,
views::LinkListener* link_listener) {
scoped_ptr<Label> result;
if (style_info.is_link) {
Link* link = new Link(text);
link->set_listener(link_listener);
link->SetUnderline((style_info.font_style & gfx::Font::UNDERLINE) != 0);
result.reset(link);
} else {
Label* label = new Label(text);
// Give the label a focus border so that its preferred size matches
// links' preferred sizes
label->SetHasFocusBorder(true);
result.reset(label);
}
result->SetEnabledColor(style_info.color);
if (!style_info.tooltip.empty())
result->SetTooltipText(style_info.tooltip);
if (style_info.font_style != gfx::Font::NORMAL)
result->SetFont(result->font().DeriveFont(0, style_info.font_style));
return scoped_ptr<Label>(result.release());
}
} // namespace
StyledLabel::RangeStyleInfo::RangeStyleInfo()
: font_style(gfx::Font::NORMAL),
color(ui::NativeTheme::instance()->GetSystemColor(
ui::NativeTheme::kColorId_LabelEnabledColor)),
disable_line_wrapping(false),
is_link(false) {}
StyledLabel::RangeStyleInfo::~RangeStyleInfo() {}
// static
StyledLabel::RangeStyleInfo StyledLabel::RangeStyleInfo::CreateForLink() {
RangeStyleInfo result;
result.disable_line_wrapping = true;
result.is_link = true;
result.color = Link::GetDefaultEnabledColor();
return result;
}
bool StyledLabel::StyleRange::operator<(
const StyledLabel::StyleRange& other) const {
// Intentionally reversed so the priority queue is sorted by smallest first.
return range.start() > other.range.start();
}
StyledLabel::StyledLabel(const string16& text, StyledLabelListener* listener)
: listener_(listener),
displayed_on_background_color_set_(false),
auto_color_readability_enabled_(true) {
TrimWhitespace(text, TRIM_TRAILING, &text_);
}
StyledLabel::~StyledLabel() {}
void StyledLabel::SetText(const string16& text) {
text_ = text;
style_ranges_ = std::priority_queue<StyleRange>();
RemoveAllChildViews(true);
PreferredSizeChanged();
}
void StyledLabel::AddStyleRange(const gfx::Range& range,
const RangeStyleInfo& style_info) {
DCHECK(!range.is_reversed());
DCHECK(!range.is_empty());
DCHECK(gfx::Range(0, text_.size()).Contains(range));
style_ranges_.push(StyleRange(range, style_info));
PreferredSizeChanged();
}
void StyledLabel::SetDefaultStyle(const RangeStyleInfo& style_info) {
default_style_info_ = style_info;
PreferredSizeChanged();
}
void StyledLabel::SetDisplayedOnBackgroundColor(SkColor color) {
displayed_on_background_color_ = color;
displayed_on_background_color_set_ = true;
}
gfx::Insets StyledLabel::GetInsets() const {
gfx::Insets insets = View::GetInsets();
const gfx::Insets focus_border_padding(1, 1, 1, 1);
insets += focus_border_padding;
return insets;
}
int StyledLabel::GetHeightForWidth(int w) {
if (w != calculated_size_.width())
calculated_size_ = gfx::Size(w, CalculateAndDoLayout(w, true));
return calculated_size_.height();
}
void StyledLabel::Layout() {
CalculateAndDoLayout(GetLocalBounds().width(), false);
}
void StyledLabel::PreferredSizeChanged() {
calculated_size_ = gfx::Size();
View::PreferredSizeChanged();
}
void StyledLabel::LinkClicked(Link* source, int event_flags) {
if (listener_)
listener_->StyledLabelLinkClicked(link_targets_[source], event_flags);
}
int StyledLabel::CalculateAndDoLayout(int width, bool dry_run) {
if (!dry_run) {
RemoveAllChildViews(true);
link_targets_.clear();
}
width -= GetInsets().width();
if (width <= 0 || text_.empty())
return 0;
const int line_height = CalculateLineHeight();
// The index of the line we're on.
int line = 0;
// The x position (in pixels) of the line we're on, relative to content
// bounds.
int x = 0;
string16 remaining_string = text_;
std::priority_queue<StyleRange> style_ranges = style_ranges_;
// Iterate over the text, creating a bunch of labels and links and laying them
// out in the appropriate positions.
while (!remaining_string.empty()) {
// Don't put whitespace at beginning of a line with an exception for the
// first line (so the text's leading whitespace is respected).
if (x == 0 && line > 0)
TrimWhitespace(remaining_string, TRIM_LEADING, &remaining_string);
gfx::Range range(gfx::Range::InvalidRange());
if (!style_ranges.empty())
range = style_ranges.top().range;
const size_t position = text_.size() - remaining_string.size();
const gfx::Rect chunk_bounds(x, 0, width - x, 2 * line_height);
std::vector<string16> substrings;
gfx::FontList text_font_list;
// If the start of the remaining text is inside a styled range, the font
// style may differ from the base font. The font specified by the range
// should be used when eliding text.
if (position >= range.start()) {
text_font_list = text_font_list.DeriveFontListWithSizeDeltaAndStyle(
0, style_ranges.top().style_info.font_style);
}
gfx::ElideRectangleText(remaining_string,
text_font_list,
chunk_bounds.width(),
chunk_bounds.height(),
gfx::IGNORE_LONG_WORDS,
&substrings);
DCHECK(!substrings.empty());
string16 chunk = substrings[0];
if (chunk.empty()) {
// Nothing fits on this line. Start a new line.
// If x is 0, first line may have leading whitespace that doesn't fit in a
// single line, so try trimming those. Otherwise there is no room for
// anything; abort.
if (x == 0) {
if (line == 0) {
TrimWhitespace(remaining_string, TRIM_LEADING, &remaining_string);
continue;
}
break;
}
x = 0;
line++;
continue;
}
scoped_ptr<Label> label;
if (position >= range.start()) {
const RangeStyleInfo& style_info = style_ranges.top().style_info;
if (style_info.disable_line_wrapping && chunk.size() < range.length() &&
position == range.start() && x != 0) {
// If the chunk should not be wrapped, try to fit it entirely on the
// next line.
x = 0;
line++;
continue;
}
chunk = chunk.substr(0, std::min(chunk.size(), range.end() - position));
label = CreateLabelRange(chunk, style_info, this);
if (style_info.is_link && !dry_run)
link_targets_[label.get()] = range;
if (position + chunk.size() >= range.end())
style_ranges.pop();
} else {
// This chunk is normal text.
if (position + chunk.size() > range.start())
chunk = chunk.substr(0, range.start() - position);
label = CreateLabelRange(chunk, default_style_info_, this);
}
if (displayed_on_background_color_set_)
label->SetBackgroundColor(displayed_on_background_color_);
label->SetAutoColorReadabilityEnabled(auto_color_readability_enabled_);
// Lay out the views to overlap by 1 pixel to compensate for their border
// spacing. Otherwise, "<a>link</a>," will render as "link ,".
const int overlap = 1;
const gfx::Size view_size = label->GetPreferredSize();
DCHECK_EQ(line_height, view_size.height() - 2 * overlap);
if (!dry_run) {
label->SetBoundsRect(gfx::Rect(
gfx::Point(GetInsets().left() + x - overlap,
GetInsets().top() + line * line_height - overlap),
view_size));
AddChildView(label.release());
}
x += view_size.width() - 2 * overlap;
remaining_string = remaining_string.substr(chunk.size());
}
return (line + 1) * line_height + GetInsets().height();
}
} // namespace views