blob: ac3158e214272ae0e4e136c80a775d3dde489b3f [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 <stddef.h>
#include <limits>
#include <vector>
#include "base/strings/string_util.h"
#include "ui/gfx/font_list.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 {
// Helpers --------------------------------------------------------------------
namespace {
gfx::Insets FocusBorderInsets(const Label& label) {
// StyledLabel never adds a border, so the only Insets added are for the
// possible focus ring.
DCHECK(label.View::GetInsets().IsEmpty());
return label.GetInsets();
}
std::unique_ptr<Label> CreateLabelRange(
const base::string16& text,
int text_context,
int default_style,
const StyledLabel::RangeStyleInfo& style_info,
views::LinkListener* link_listener) {
std::unique_ptr<Label> result;
if (style_info.IsLink()) {
// Nothing should (and nothing does) use a custom font for links.
DCHECK(!style_info.custom_font);
// Note this ignores |default_style|, in favor of style::STYLE_LINK.
Link* link = new Link(text, text_context);
link->set_listener(link_listener);
// Links in a StyledLabel do not get underlines.
link->SetUnderline(false);
result.reset(link);
} else if (style_info.custom_font) {
result.reset(new Label(text, {style_info.custom_font.value()}));
} else {
result.reset(new Label(text, text_context,
style_info.text_style.value_or(default_style)));
}
if (style_info.override_color != SK_ColorTRANSPARENT)
result->SetEnabledColor(style_info.override_color);
if (!style_info.tooltip.empty())
result->SetTooltipText(style_info.tooltip);
return result;
}
} // namespace
// StyledLabel::RangeStyleInfo ------------------------------------------------
StyledLabel::RangeStyleInfo::RangeStyleInfo() = default;
StyledLabel::RangeStyleInfo::RangeStyleInfo(const RangeStyleInfo& copy) =
default;
StyledLabel::RangeStyleInfo::~RangeStyleInfo() {}
// static
StyledLabel::RangeStyleInfo StyledLabel::RangeStyleInfo::CreateForLink() {
RangeStyleInfo result;
result.disable_line_wrapping = true;
result.text_style = style::STYLE_LINK;
return result;
}
bool StyledLabel::RangeStyleInfo::IsLink() const {
return text_style && text_style.value() == style::STYLE_LINK;
}
// StyledLabel::StyleRange ----------------------------------------------------
bool StyledLabel::StyleRange::operator<(
const StyledLabel::StyleRange& other) const {
return range.start() < other.range.start();
}
// StyledLabel ----------------------------------------------------------------
// static
const char StyledLabel::kViewClassName[] = "StyledLabel";
StyledLabel::StyledLabel(const base::string16& text,
StyledLabelListener* listener)
: specified_line_height_(0),
listener_(listener),
width_at_last_size_calculation_(0),
width_at_last_layout_(0),
displayed_on_background_color_(SkColorSetRGB(0xFF, 0xFF, 0xFF)),
displayed_on_background_color_set_(false),
auto_color_readability_enabled_(true) {
base::TrimWhitespace(text, base::TRIM_TRAILING, &text_);
}
StyledLabel::~StyledLabel() {}
void StyledLabel::SetText(const base::string16& text) {
text_ = text;
style_ranges_.clear();
RemoveAllChildViews(true);
PreferredSizeChanged();
}
gfx::FontList StyledLabel::GetDefaultFontList() const {
return style::GetFont(text_context_, default_text_style_);
}
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));
// Insert the new range in sorted order.
StyleRanges new_range;
new_range.push_front(StyleRange(range, style_info));
style_ranges_.merge(new_range);
PreferredSizeChanged();
}
void StyledLabel::SetTextContext(int text_context) {
if (text_context_ == text_context)
return;
text_context_ = text_context;
PreferredSizeChanged();
}
void StyledLabel::SetDefaultTextStyle(int text_style) {
if (default_text_style_ == text_style)
return;
default_text_style_ = text_style;
PreferredSizeChanged();
}
void StyledLabel::SetLineHeight(int line_height) {
specified_line_height_ = line_height;
PreferredSizeChanged();
}
void StyledLabel::SetDisplayedOnBackgroundColor(SkColor color) {
if (displayed_on_background_color_ == color &&
displayed_on_background_color_set_)
return;
displayed_on_background_color_ = color;
displayed_on_background_color_set_ = true;
for (int i = 0, count = child_count(); i < count; ++i) {
DCHECK((child_at(i)->GetClassName() == Label::kViewClassName) ||
(child_at(i)->GetClassName() == Link::kViewClassName));
static_cast<Label*>(child_at(i))->SetBackgroundColor(color);
}
}
void StyledLabel::SizeToFit(int max_width) {
if (max_width == 0)
max_width = std::numeric_limits<int>::max();
SetSize(CalculateAndDoLayout(max_width, true));
}
const char* StyledLabel::GetClassName() const {
return kViewClassName;
}
gfx::Insets StyledLabel::GetInsets() const {
gfx::Insets insets = View::GetInsets();
if (Link::GetDefaultFocusStyle() != Link::FocusStyle::RING)
return insets;
// We need a focus border iff we contain a link that will have a focus border.
// That in turn will be true only if the link is non-empty.
for (StyleRanges::const_iterator i(style_ranges_.begin());
i != style_ranges_.end(); ++i) {
if (i->style_info.IsLink() && !i->range.is_empty()) {
insets += gfx::Insets(Link::kFocusBorderPadding);
break;
}
}
return insets;
}
gfx::Size StyledLabel::CalculatePreferredSize() const {
return calculated_size_;
}
int StyledLabel::GetHeightForWidth(int w) const {
// TODO(erg): Munge the const-ness of the style label. CalculateAndDoLayout
// doesn't actually make any changes to member variables when |dry_run| is
// set to true. In general, the mutating and non-mutating parts shouldn't
// be in the same codepath.
return const_cast<StyledLabel*>(this)->CalculateAndDoLayout(w, true).height();
}
void StyledLabel::Layout() {
CalculateAndDoLayout(GetLocalBounds().width(), false);
}
void StyledLabel::PreferredSizeChanged() {
calculated_size_ = gfx::Size();
width_at_last_size_calculation_ = 0;
width_at_last_layout_ = 0;
View::PreferredSizeChanged();
}
void StyledLabel::LinkClicked(Link* source, int event_flags) {
if (listener_)
listener_->StyledLabelLinkClicked(this, link_targets_[source], event_flags);
}
int StyledLabel::GetDefaultLineHeight() const {
return specified_line_height_ > 0
? specified_line_height_
: std::max(
style::GetLineHeight(text_context_, default_text_style_),
GetDefaultFontList().GetHeight());
}
gfx::FontList StyledLabel::GetFontListForRange(
const StyleRanges::const_iterator& range) const {
if (range == style_ranges_.end())
return GetDefaultFontList();
return range->style_info.custom_font
? range->style_info.custom_font.value()
: style::GetFont(
text_context_,
range->style_info.text_style.value_or(default_text_style_));
}
gfx::Size StyledLabel::CalculateAndDoLayout(int width, bool dry_run) {
if (width == width_at_last_size_calculation_ &&
(dry_run || width == width_at_last_layout_))
return calculated_size_;
width_at_last_size_calculation_ = width;
if (!dry_run)
width_at_last_layout_ = width;
width -= GetInsets().width();
if (!dry_run) {
RemoveAllChildViews(true);
link_targets_.clear();
}
if (width <= 0 || text_.empty())
return gfx::Size();
const int line_height = GetDefaultLineHeight();
// 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;
int total_height = 0;
// The width that was actually used. Guaranteed to be no larger than |width|.
int used_width = 0;
RangeStyleInfo default_style;
default_style.text_style = default_text_style_;
base::string16 remaining_string = text_;
StyleRanges::const_iterator current_range = style_ranges_.begin();
bool first_loop_iteration = true;
// Iterate over the text, creating a bunch of labels and links and laying them
// out in the appropriate positions.
while (!remaining_string.empty()) {
if (x == 0 && !first_loop_iteration) {
if (remaining_string.front() == L'\n') {
// Wrapped to the next line on \n, remove it. Other whitespace,
// eg, spaces to indent next line, are preserved.
remaining_string.erase(0, 1);
} else {
// Wrapped on whitespace character or characters in the middle of the
// line - none of them are needed at the beginning of the next line.
base::TrimWhitespace(remaining_string, base::TRIM_LEADING,
&remaining_string);
}
}
first_loop_iteration = false;
gfx::Range range(gfx::Range::InvalidRange());
if (current_range != style_ranges_.end())
range = current_range->range;
const size_t position = text_.size() - remaining_string.size();
const gfx::Rect chunk_bounds(x, 0, width - x, 2 * line_height);
std::vector<base::string16> substrings;
// 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.
gfx::FontList text_font_list = position >= range.start()
? GetFontListForRange(current_range)
: GetDefaultFontList();
gfx::ElideRectangleText(remaining_string,
text_font_list,
chunk_bounds.width(),
chunk_bounds.height(),
gfx::WRAP_LONG_WORDS,
&substrings);
if (substrings.empty()) {
// there is no room for anything; abort.
break;
}
if (substrings[0].empty()) {
x = 0;
// Nothing fits on this line. Start a new line.
// As for the first line, don't advance line number so that it will be
// handled again at the beginning of the loop.
if (line > 0) {
++line;
}
continue;
}
base::string16 chunk = substrings[0];
std::unique_ptr<Label> label;
if (position >= range.start()) {
const RangeStyleInfo& style_info = current_range->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;
}
if (chunk.size() > range.end() - position)
chunk = chunk.substr(0, range.end() - position);
label = CreateLabelRange(chunk, text_context_, default_text_style_,
style_info, this);
if (style_info.IsLink() && !dry_run)
link_targets_[label.get()] = range;
if (position + chunk.size() >= range.end())
++current_range;
} else {
// This chunk is normal text.
if (position + chunk.size() > range.start())
chunk = chunk.substr(0, range.start() - position);
label = CreateLabelRange(chunk, text_context_, default_text_style_,
default_style, this);
}
if (displayed_on_background_color_set_)
label->SetBackgroundColor(displayed_on_background_color_);
label->SetAutoColorReadabilityEnabled(auto_color_readability_enabled_);
const gfx::Size view_size = label->GetPreferredSize();
const gfx::Insets insets = GetInsets();
gfx::Point view_origin(insets.left() + x,
insets.top() + line * line_height);
if (Link::GetDefaultFocusStyle() == Link::FocusStyle::RING) {
// Calculate the size of the optional focus border, and overlap by that
// amount. Otherwise, "<a>link</a>," will render as "link ,".
const gfx::Insets focus_border_insets = FocusBorderInsets(*label);
view_origin.Offset(-focus_border_insets.left(),
-focus_border_insets.top());
label->SetBoundsRect(gfx::Rect(view_origin, view_size));
x += view_size.width() - focus_border_insets.width();
used_width = std::max(used_width, x);
total_height =
std::max(total_height, label->bounds().bottom() + insets.bottom() -
focus_border_insets.bottom());
} else {
label->SetBoundsRect(gfx::Rect(view_origin, view_size));
x += view_size.width();
total_height =
std::max(total_height, label->bounds().bottom() + insets.bottom());
}
used_width = std::max(used_width, x);
if (!dry_run)
AddChildView(label.release());
// If |gfx::ElideRectangleText| returned more than one substring, that
// means the whole text did not fit into remaining line width, with text
// after |susbtring[0]| spilling into next line. If whole |substring[0]|
// was added to the current line (this may not be the case if part of the
// substring has different style), proceed to the next line.
if (substrings.size() > 1 && chunk.size() == substrings[0].size()) {
x = 0;
++line;
}
remaining_string = remaining_string.substr(chunk.size());
}
DCHECK_LE(used_width, width);
calculated_size_ = gfx::Size(used_width + GetInsets().width(), total_height);
return calculated_size_;
}
} // namespace views