| // 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 <algorithm> |
| #include <limits> |
| #include <memory> |
| #include <vector> |
| |
| #include "base/i18n/rtl.h" |
| #include "base/strings/string_util.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/gfx/font_list.h" |
| #include "ui/gfx/text_elider.h" |
| #include "ui/gfx/text_utils.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 { |
| |
| 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 = std::make_unique<Label>( |
| text, Label::CustomFont{style_info.custom_font.value()}); |
| } else { |
| result = std::make_unique<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; |
| } |
| |
| // Returns the horizontal offset to align views in a line. |
| int HorizontalAdjustment(int used_width, |
| int width, |
| gfx::HorizontalAlignment alignment) { |
| const int space = width - used_width; |
| return alignment == gfx::ALIGN_LEFT |
| ? 0 |
| : alignment == gfx::ALIGN_CENTER ? space / 2 : space; |
| } |
| |
| } // namespace |
| |
| // StyledLabel::TestApi ------------------------------------------------ |
| |
| StyledLabel::TestApi::TestApi(StyledLabel* view) : view_(view) {} |
| |
| StyledLabel::TestApi::~TestApi() = default; |
| |
| const std::map<View*, gfx::Range>& StyledLabel::TestApi::link_targets() { |
| return view_->link_targets_; |
| } |
| |
| // StyledLabel::RangeStyleInfo ------------------------------------------------ |
| |
| StyledLabel::RangeStyleInfo::RangeStyleInfo() = default; |
| StyledLabel::RangeStyleInfo::RangeStyleInfo(const RangeStyleInfo& copy) = |
| default; |
| |
| StyledLabel::RangeStyleInfo::~RangeStyleInfo() = default; |
| |
| // 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 ---------------------------------------------------------------- |
| |
| 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() = default; |
| |
| 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::AddCustomView(std::unique_ptr<View> custom_view) { |
| DCHECK(custom_view->owned_by_client()); |
| custom_views_.insert(std::move(custom_view)); |
| } |
| |
| 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 (View* child : children()) { |
| DCHECK((child->GetClassName() == Label::kViewClassName) || |
| (child->GetClassName() == Link::kViewClassName)); |
| static_cast<Label*>(child)->SetBackgroundColor(color); |
| } |
| } |
| |
| void StyledLabel::SizeToFit(int max_width) { |
| if (max_width == 0) |
| max_width = std::numeric_limits<int>::max(); |
| |
| SetSize(CalculateAndDoLayout(max_width, true)); |
| } |
| |
| void StyledLabel::GetAccessibleNodeData(ui::AXNodeData* node_data) { |
| if (text_context_ == style::CONTEXT_DIALOG_TITLE) |
| node_data->role = ax::mojom::Role::kTitleBar; |
| else |
| node_data->role = ax::mojom::Role::kStaticText; |
| |
| node_data->SetName(text()); |
| } |
| |
| 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); |
| } |
| |
| // TODO(wutao): support gfx::ALIGN_TO_HEAD alignment. |
| void StyledLabel::SetHorizontalAlignment(gfx::HorizontalAlignment alignment) { |
| DCHECK_NE(gfx::ALIGN_TO_HEAD, alignment); |
| alignment = gfx::MaybeFlipForRTL(alignment); |
| |
| if (horizontal_alignment_ == alignment) |
| return; |
| horizontal_alignment_ = alignment; |
| PreferredSizeChanged(); |
| } |
| |
| void StyledLabel::ClearStyleRanges() { |
| style_ranges_.clear(); |
| PreferredSizeChanged(); |
| } |
| |
| 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 default_line_height = GetDefaultLineHeight(); |
| |
| // The index of the line we're on. |
| int line = 0; |
| const gfx::Insets insets = GetInsets(); |
| // The current child view's position, relative to content bounds, in pixels. |
| gfx::Point offset(0, insets.top()); |
| 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; |
| |
| // Max height of the views in a line. |
| int max_line_height = default_line_height; |
| |
| // Temporary references to the views in a line, used for alignment. |
| std::vector<View*> views_in_a_line; |
| |
| // Iterate over the text, creating a bunch of labels and links and laying them |
| // out in the appropriate positions. |
| while (!remaining_string.empty()) { |
| if (offset.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(); |
| std::vector<base::string16> substrings; |
| // If the current range is not a custom_view, then we use ElideRectangleText |
| // to determine the line wrapping. Note: if it is a custom_view, then the |
| // |position| should equal |range.start()| because the custom_view is |
| // treated as one unit. |
| if (position != range.start() || (current_range != style_ranges_.end() && |
| !current_range->style_info.custom_view)) { |
| const gfx::Rect chunk_bounds(offset.x(), 0, width - offset.x(), |
| default_line_height); |
| // 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(); |
| int elide_result = 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. Since wrapping is enabled, this |
| // should only occur if there is insufficient vertical space remaining. |
| // ElideRectangleText always adds a single character, even if there is |
| // no room horizontally. |
| DCHECK_NE(0, elide_result & gfx::INSUFFICIENT_SPACE_VERTICAL); |
| break; |
| } |
| |
| // Views are aligned to integer coordinates, but typesetting is not. This |
| // means that it's possible for an ElideRectangleText on a prior iteration |
| // to fit a word on the current line, which does not fit after that word |
| // is wrapped in a View for its chunk at the end of the line. In most |
| // cases, this will just wrap more words on to the next line. However, if |
| // the remaining chunk width is insufficient for the very _first_ word, |
| // that word will be incorrectly split. In this case, start a new line |
| // instead. |
| bool truncated_chunk = |
| offset.x() != 0 && |
| (elide_result & gfx::INSUFFICIENT_SPACE_FOR_FIRST_WORD) != 0; |
| if (substrings[0].empty() || truncated_chunk) { |
| // The entire line is \n, or 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. |
| AdvanceOneLine(&line, &offset, &max_line_height, width, |
| &views_in_a_line, |
| offset.x() != 0 || line > 0 /* new_line */); |
| continue; |
| } |
| } |
| |
| base::string16 chunk; |
| View* custom_view = nullptr; |
| std::unique_ptr<Label> label; |
| if (position >= range.start()) { |
| const RangeStyleInfo& style_info = current_range->style_info; |
| |
| if (style_info.custom_view) { |
| custom_view = style_info.custom_view; |
| // Ownership of the custom view must be passed to StyledLabel. |
| DCHECK( |
| std::find_if(custom_views_.cbegin(), custom_views_.cend(), |
| [custom_view](const std::unique_ptr<View>& view_ptr) { |
| return view_ptr.get() == custom_view; |
| }) != custom_views_.cend()); |
| // Do not allow wrap in custom view. |
| DCHECK_EQ(position, range.start()); |
| chunk = remaining_string.substr(0, range.end() - position); |
| } else { |
| chunk = substrings[0]; |
| } |
| |
| if (((custom_view && |
| offset.x() + custom_view->GetPreferredSize().width() > width) || |
| (style_info.disable_line_wrapping && |
| chunk.size() < range.length())) && |
| position == range.start() && offset.x() != 0) { |
| // If the chunk should not be wrapped, try to fit it entirely on the |
| // next line. |
| AdvanceOneLine(&line, &offset, &max_line_height, width, |
| &views_in_a_line); |
| continue; |
| } |
| |
| if (chunk.size() > range.end() - position) |
| chunk = chunk.substr(0, range.end() - position); |
| |
| if (!custom_view) { |
| 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 { |
| chunk = substrings[0]; |
| |
| // 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 (label) { |
| if (displayed_on_background_color_set_) |
| label->SetBackgroundColor(displayed_on_background_color_); |
| label->SetAutoColorReadabilityEnabled(auto_color_readability_enabled_); |
| } |
| |
| View* child_view = custom_view ? custom_view : label.get(); |
| gfx::Size view_size = child_view->GetPreferredSize(); |
| // |offset.y()| already contains |insets.top()|. |
| gfx::Point view_origin(insets.left() + offset.x(), offset.y()); |
| // The custom view could be wider than the available width; clamp as needed. |
| if (custom_view) |
| view_size.set_width(std::min(view_size.width(), width - offset.x())); |
| |
| child_view->SetBoundsRect(gfx::Rect(view_origin, view_size)); |
| offset.set_x(offset.x() + view_size.width()); |
| total_height = |
| std::max(total_height, std::max(child_view->bounds().bottom(), |
| offset.y() + default_line_height) + |
| insets.bottom()); |
| used_width = std::max(used_width, offset.x()); |
| max_line_height = std::max(max_line_height, view_size.height()); |
| |
| if (!dry_run) { |
| views_in_a_line.push_back(child_view); |
| if (label) |
| AddChildView(label.release()); |
| else |
| AddChildView(child_view); |
| } |
| |
| // 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 (!custom_view && substrings.size() > 1 && |
| chunk.size() == substrings[0].size()) { |
| AdvanceOneLine(&line, &offset, &max_line_height, width, &views_in_a_line); |
| } |
| |
| remaining_string = remaining_string.substr(chunk.size()); |
| } |
| AdvanceOneLine(&line, &offset, &max_line_height, width, &views_in_a_line, |
| false); |
| DCHECK_LE(used_width, width); |
| calculated_size_ = gfx::Size(used_width + GetInsets().width(), total_height); |
| return calculated_size_; |
| } |
| |
| void StyledLabel::AdvanceOneLine(int* line_number, |
| gfx::Point* offset, |
| int* max_line_height, |
| int width, |
| std::vector<View*>* views_in_a_line, |
| bool new_line) { |
| const int x_delta = |
| HorizontalAdjustment(offset->x(), width, horizontal_alignment_); |
| for (auto* view : *views_in_a_line) { |
| gfx::Rect bounds = view->bounds(); |
| bounds.set_x(bounds.x() + x_delta); |
| bounds.set_y(offset->y() + (*max_line_height - bounds.height()) / 2.0f); |
| view->SetBoundsRect(bounds); |
| } |
| views_in_a_line->clear(); |
| |
| if (new_line) { |
| ++(*line_number); |
| offset->set_y(offset->y() + *max_line_height); |
| *max_line_height = GetDefaultLineHeight(); |
| } |
| offset->set_x(0); |
| } |
| |
| BEGIN_METADATA(StyledLabel) |
| METADATA_PARENT_CLASS(View) |
| END_METADATA() |
| |
| } // namespace views |