// 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 #include #include #include #include "base/i18n/rtl.h" #include "base/ranges/algorithm.h" #include "base/strings/string_util.h" #include "ui/accessibility/ax_enums.mojom.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/metadata/metadata_impl_macros.h" #include "ui/views/view_class_properties.h" namespace views { DEFINE_UI_CLASS_PROPERTY_KEY(bool, kStyledLabelCustomViewKey, false) StyledLabel::RangeStyleInfo::RangeStyleInfo() = default; StyledLabel::RangeStyleInfo::RangeStyleInfo(const RangeStyleInfo&) = default; StyledLabel::RangeStyleInfo& StyledLabel::RangeStyleInfo::operator=( const RangeStyleInfo&) = default; StyledLabel::RangeStyleInfo::~RangeStyleInfo() = default; // static StyledLabel::RangeStyleInfo StyledLabel::RangeStyleInfo::CreateForLink( base::RepeatingClosure callback) { // Adapt this closure to a Link::ClickedCallback by discarding the extra arg. return CreateForLink(base::BindRepeating( [](base::RepeatingClosure closure, const ui::Event&) { closure.Run(); }, std::move(callback))); } // static StyledLabel::RangeStyleInfo StyledLabel::RangeStyleInfo::CreateForLink( Link::ClickedCallback callback) { RangeStyleInfo result; result.callback = std::move(callback); result.disable_line_wrapping = true; result.text_style = style::STYLE_LINK; return result; } StyledLabel::LayoutSizeInfo::LayoutSizeInfo(int max_valid_width) : max_valid_width(max_valid_width) {} StyledLabel::LayoutSizeInfo::LayoutSizeInfo(const LayoutSizeInfo&) = default; StyledLabel::LayoutSizeInfo& StyledLabel::LayoutSizeInfo::operator=( const LayoutSizeInfo&) = default; StyledLabel::LayoutSizeInfo::~LayoutSizeInfo() = default; bool StyledLabel::StyleRange::operator<( const StyledLabel::StyleRange& other) const { return range.start() < other.range.start(); } struct StyledLabel::LayoutViews { // All views to be added as children, line by line. std::vector> views_per_line; // The subset of |views| that are created by StyledLabel itself. Basically, // this is all non-custom views; These appear in the same order as |views|. std::vector> owned_views; }; StyledLabel::StyledLabel() = default; StyledLabel::~StyledLabel() = default; const base::string16& StyledLabel::GetText() const { return text_; } void StyledLabel::SetText(base::string16 text) { // Failing to trim trailing whitespace will cause later confusion when the // text elider tries to do so internally. There's no obvious reason to // preserve trailing whitespace anyway. base::TrimWhitespace(std::move(text), base::TRIM_TRAILING, &text); if (text_ == text) return; text_ = text; style_ranges_.clear(); RemoveOrDeleteAllChildViews(); OnPropertyChanged(&text_, kPropertyEffectsPreferredSizeChanged); } gfx::FontList StyledLabel::GetFontList(const RangeStyleInfo& style_info) const { return style_info.custom_font.value_or(style::GetFont( text_context_, style_info.text_style.value_or(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 custom_view) { DCHECK(!custom_view->owned_by_client()); custom_view->SetProperty(kStyledLabelCustomViewKey, true); custom_views_.push_back(std::move(custom_view)); } int StyledLabel::GetTextContext() const { return text_context_; } void StyledLabel::SetTextContext(int text_context) { if (text_context_ == text_context) return; text_context_ = text_context; OnPropertyChanged(&text_context_, kPropertyEffectsPreferredSizeChanged); } int StyledLabel::GetDefaultTextStyle() const { return default_text_style_; } void StyledLabel::SetDefaultTextStyle(int text_style) { if (default_text_style_ == text_style) return; default_text_style_ = text_style; OnPropertyChanged(&default_text_style_, kPropertyEffectsPreferredSizeChanged); } int StyledLabel::GetLineHeight() const { return line_height_.value_or( style::GetLineHeight(text_context_, default_text_style_)); } void StyledLabel::SetLineHeight(int line_height) { if (line_height_ == line_height) return; line_height_ = line_height; OnPropertyChanged(&line_height_, kPropertyEffectsPreferredSizeChanged); } base::Optional StyledLabel::GetDisplayedOnBackgroundColor() const { return displayed_on_background_color_; } void StyledLabel::SetDisplayedOnBackgroundColor( const base::Optional& color) { if (displayed_on_background_color_ == color) return; displayed_on_background_color_ = color; if (GetNativeTheme()) UpdateLabelBackgroundColor(); OnPropertyChanged(&displayed_on_background_color_, kPropertyEffectsPaint); } bool StyledLabel::GetAutoColorReadabilityEnabled() const { return auto_color_readability_enabled_; } void StyledLabel::SetAutoColorReadabilityEnabled(bool auto_color_readability) { if (auto_color_readability_enabled_ == auto_color_readability) return; auto_color_readability_enabled_ = auto_color_readability; OnPropertyChanged(&auto_color_readability_enabled_, kPropertyEffectsPaint); } const StyledLabel::LayoutSizeInfo& StyledLabel::GetLayoutSizeInfoForWidth( int w) const { CalculateLayout(w); return layout_size_info_; } void StyledLabel::SizeToFit(int fixed_width) { CalculateLayout(fixed_width == 0 ? std::numeric_limits::max() : fixed_width); gfx::Size size = layout_size_info_.total_size; size.set_width(std::max(size.width(), fixed_width)); SetSize(size); } void StyledLabel::GetAccessibleNodeData(ui::AXNodeData* node_data) { node_data->role = (text_context_ == style::CONTEXT_DIALOG_TITLE) ? ax::mojom::Role::kTitleBar : ax::mojom::Role::kStaticText; node_data->SetName(GetText()); } gfx::Size StyledLabel::CalculatePreferredSize() const { // Respect any existing size. If there is none, default to a single line. CalculateLayout((width() == 0) ? std::numeric_limits::max() : width()); return layout_size_info_.total_size; } int StyledLabel::GetHeightForWidth(int w) const { return GetLayoutSizeInfoForWidth(w).total_size.height(); } void StyledLabel::Layout() { CalculateLayout(width()); // If the layout has been recalculated, add and position all views. if (layout_views_) { // Delete all non-custom views on removal; custom views are temporarily // moved to |custom_views_|. RemoveOrDeleteAllChildViews(); DCHECK_EQ(layout_size_info_.line_sizes.size(), layout_views_->views_per_line.size()); int line_y = GetInsets().top(); auto next_owned_view = layout_views_->owned_views.begin(); for (size_t line = 0; line < layout_views_->views_per_line.size(); ++line) { const auto& line_size = layout_size_info_.line_sizes[line]; int x = StartX(width() - line_size.width()); for (auto* view : layout_views_->views_per_line[line]) { gfx::Size size = view->GetPreferredSize(); size.set_width(std::min(size.width(), width() - x)); // Compute the view y such that the view center y and the line center y // match. Because of added rounding errors, this is not the same as // doing (line_size.height() - size.height()) / 2. const int y = line_size.height() / 2 - size.height() / 2; view->SetBoundsRect({{x, line_y + y}, size}); x += size.width(); // Transfer ownership for any views in layout_views_->owned_views or // custom_views_. The actual pointer is the same in both arms below. if (view->GetProperty(kStyledLabelCustomViewKey)) { auto custom_view = std::find_if(custom_views_.begin(), custom_views_.end(), [view](const auto& current_custom_view) { return current_custom_view.get() == view; }); DCHECK(custom_view != custom_views_.end()); AddChildView(std::move(*custom_view)); custom_views_.erase(custom_view); } else { DCHECK(next_owned_view != layout_views_->owned_views.end()); DCHECK(view == next_owned_view->get()); AddChildView(std::move(*next_owned_view)); ++next_owned_view; } } line_y += line_size.height(); } DCHECK(next_owned_view == layout_views_->owned_views.end()); layout_views_.reset(); } else if (horizontal_alignment_ != gfx::ALIGN_LEFT) { // Recompute all child X coordinates in case the width has shifted, which // will move the children if the label is center/right-aligned. If the // width hasn't changed, all the SetX() calls below will no-op, so this // won't have side effects. int line_bottom = GetInsets().top(); auto i = children().begin(); for (const auto& line_size : layout_size_info_.line_sizes) { DCHECK(i != children().end()); // Should not have an empty trailing line. int x = StartX(width() - line_size.width()); line_bottom += line_size.height(); for (; (i != children().end()) && ((*i)->y() < line_bottom); ++i) { (*i)->SetX(x); x += (*i)->GetPreferredSize().width(); } } DCHECK(i == children().end()); // Should not be short any lines. } } void StyledLabel::PreferredSizeChanged() { layout_size_info_ = LayoutSizeInfo(0); layout_views_.reset(); View::PreferredSizeChanged(); } void StyledLabel::OnThemeChanged() { View::OnThemeChanged(); UpdateLabelBackgroundColor(); } // 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(); } void StyledLabel::ClickLinkForTesting() { const auto it = base::ranges::find(children(), Link::kViewClassName, &View::GetClassName); DCHECK(it != children().cend()); (*it)->OnKeyPressed( ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_SPACE, ui::EF_NONE)); } int StyledLabel::StartX(int excess_space) const { int x = GetInsets().left(); if (horizontal_alignment_ == gfx::ALIGN_LEFT) return x; return x + ((horizontal_alignment_ == gfx::ALIGN_CENTER) ? (excess_space / 2) : excess_space); } void StyledLabel::CalculateLayout(int width) const { const gfx::Insets insets = GetInsets(); width = std::max(width, insets.width()); if (width >= layout_size_info_.total_size.width() && width <= layout_size_info_.max_valid_width) return; layout_size_info_ = LayoutSizeInfo(width); layout_views_ = std::make_unique(); const int content_width = width - insets.width(); const int line_height = GetLineHeight(); RangeStyleInfo default_style; default_style.text_style = default_text_style_; int max_width = 0, total_height = 0; // Try to preserve leading whitespace on the first line. bool can_trim_leading_whitespace = false; StyleRanges::const_iterator current_range = style_ranges_.begin(); for (base::string16 remaining_string = text_; content_width > 0 && !remaining_string.empty();) { layout_size_info_.line_sizes.emplace_back(0, line_height); auto& line_size = layout_size_info_.line_sizes.back(); layout_views_->views_per_line.emplace_back(); auto& views = layout_views_->views_per_line.back(); while (!remaining_string.empty()) { if (views.empty() && can_trim_leading_whitespace) { if (remaining_string.front() == '\n') { // Wrapped to the next line on \n, remove it. Other whitespace, // e.g. spaces to indent the 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); } } 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 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(line_size.width(), 0, content_width - line_size.width(), 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 = GetFontList((position >= range.start()) ? current_range->style_info : RangeStyleInfo()); 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. 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); // There's no way to continue processing; clear |remaining_string| so // the outer loop will terminate after this iteration completes. remaining_string.clear(); 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 = line_size.width() != 0 && (elide_result & gfx::INSUFFICIENT_SPACE_FOR_FIRST_WORD) != 0; if (substrings[0].empty() || truncated_chunk) { // The entire line is \n, or nothing else fits on this line. Wrap, // unless this is the first line, in which case we strip leading // whitespace and try again. if ((line_size.width() != 0) || (layout_views_->views_per_line.size() > 1)) break; can_trim_leading_whitespace = true; continue; } } base::string16 chunk; View* custom_view = nullptr; std::unique_ptr