910e62b5创建于 1月15日历史提交
// Copyright 2013 The Chromium Authors
// 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 <optional>
#include <utility>
#include <variant>

#include "base/functional/callback_helpers.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/events/event.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/text_constants.h"
#include "ui/gfx/text_elider.h"
#include "ui/gfx/text_utils.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/link_fragment.h"
#include "ui/views/style/typography.h"
#include "ui/views/style/typography_provider.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.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::IgnoreArgs<const ui::Event&>(std::move(callback)));
}

// static
StyledLabel::RangeStyleInfo StyledLabel::RangeStyleInfo::CreateForLink(
    Link::ClickedCallback callback) {
  RangeStyleInfo result;
  result.callback = std::move(callback);
  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;

auto StyledLabel::StyleRange::operator<=>(
    const StyledLabel::StyleRange& other) const {
  return range <=> other.range;
}

struct StyledLabel::LayoutViews {
  // All views to be added as children, line by line.
  std::vector<std::vector<raw_ptr<View, VectorExperimental>>> 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<std::unique_ptr<View>> owned_views;
};

StyledLabel::StyledLabel() {
  GetViewAccessibility().SetRole(text_context_ == style::CONTEXT_DIALOG_TITLE
                                     ? ax::mojom::Role::kTitleBar
                                     : ax::mojom::Role::kStaticText);
}

StyledLabel::~StyledLabel() = default;

const std::u16string& StyledLabel::GetText() const {
  return text_;
}

void StyledLabel::SetText(std::u16string 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;
  GetViewAccessibility().SetName(text_);
  style_ranges_.clear();
  RemoveOrDeleteAllChildViews();
  OnPropertyChanged(&text_, PropertyEffects::kPreferredSizeChanged);
}

gfx::FontList StyledLabel::GetFontList(const RangeStyleInfo& style_info) const {
  return style_info.custom_font.value_or(TypographyProvider::Get().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.emplace_front(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_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;
  GetViewAccessibility().SetRole(text_context_ == style::CONTEXT_DIALOG_TITLE
                                     ? ax::mojom::Role::kTitleBar
                                     : ax::mojom::Role::kStaticText);
  OnPropertyChanged(&text_context_, PropertyEffects::kPreferredSizeChanged);
}

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_,
                    PropertyEffects::kPreferredSizeChanged);
}

std::optional<ui::ColorId> StyledLabel::GetDefaultEnabledColorId() const {
  return default_enabled_color_id_;
}

void StyledLabel::SetDefaultEnabledColorId(
    std::optional<ui::ColorId> enabled_color_id) {
  if (default_enabled_color_id_ == enabled_color_id) {
    return;
  }

  default_enabled_color_id_ = enabled_color_id;
  OnPropertyChanged(&default_enabled_color_id_, PropertyEffects::kPaint);
}

int StyledLabel::GetLineHeight() const {
  return line_height_.value_or(TypographyProvider::Get().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_, PropertyEffects::kPreferredSizeChanged);
}

std::optional<ui::ColorVariant> StyledLabel::GetDisplayedOnBackgroundColor()
    const {
  return displayed_on_background_color_;
}

void StyledLabel::SetDisplayedOnBackgroundColor(ui::ColorVariant color) {
  if (color == displayed_on_background_color_) {
    return;
  }

  displayed_on_background_color_ = color;

  if (GetWidget()) {
    UpdateLabelBackgroundColor();
  }

  OnPropertyChanged(&displayed_on_background_color_, PropertyEffects::kPaint);
}

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_, PropertyEffects::kPaint);
}

bool StyledLabel::GetSubpixelRenderingEnabled() const {
  return subpixel_rendering_enabled_;
}

void StyledLabel::SetSubpixelRenderingEnabled(bool subpixel_rendering_enabled) {
  if (subpixel_rendering_enabled_ == subpixel_rendering_enabled) {
    return;
  }

  subpixel_rendering_enabled_ = subpixel_rendering_enabled;
  OnPropertyChanged(&subpixel_rendering_enabled_, PropertyEffects::kPaint);
}

const StyledLabel::LayoutSizeInfo& StyledLabel::GetLayoutSizeInfoForWidth(
    int w) const {
  if (auto it = layout_size_info_cache_.Get(w);
      it != layout_size_info_cache_.end()) {
    return it->second;
  }
  CalculateLayout(w);
  layout_size_info_cache_.Put(w, layout_size_info_);
  return layout_size_info_;
}

void StyledLabel::SizeToFit(int fixed_width) {
  DCHECK_LE(0, fixed_width);
  fixed_width_ = fixed_width;
  gfx::Size size = CalculatePreferredSize(
      SizeBounds(fixed_width_ == 0 ? SizeBound() : SizeBound(width()), {}));
  size.set_width(std::max(size.width(), fixed_width));
  SetSize(size);
}

base::CallbackListSubscription StyledLabel::AddTextChangedCallback(
    views::PropertyChangedCallback callback) {
  return AddPropertyChangedCallback(&text_, std::move(callback));
}

gfx::Size StyledLabel::GetMinimumSize() const {
  // Overload it otherwise BubbleDialogDelegateViewTest.StyledLabelTitle will
  // fail.
  return CalculatePreferredSize(
      SizeBounds(width() == 0 ? SizeBound() : SizeBound(width()), {}));
}

gfx::Size StyledLabel::CalculatePreferredSize(
    const SizeBounds& available_size) const {
  int width = 0;
  if (fixed_width_) {
    width = fixed_width_;
  } else if (available_size.width().is_bounded()) {
    width = available_size.width().value();
  }

  // Respect any existing size.  If there is none, default to a single line.
  return GetLayoutSizeInfoForWidth(width == 0 ? std::numeric_limits<int>::max()
                                              : width)
      .total_size;
}

void StyledLabel::OnBoundsChanged(const gfx::Rect& previous_bounds) {
  if (previous_bounds.width() == width()) {
    return;
  }

  need_recreate_child_ = true;
}

void StyledLabel::Layout(PassKey) {
  if (!need_recreate_child_) {
    return;
  }

  RecreateChildViews();
}

void StyledLabel::PreferredSizeChanged() {
  need_recreate_child_ = true;
  layout_size_info_ = LayoutSizeInfo(0);
  layout_size_info_cache_.Clear();
  layout_views_.reset();
  View::PreferredSizeChanged();
}

// 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::ClickFirstLinkForTesting() {
  GetFirstLinkForTesting()->OnKeyPressed(  // IN-TEST
      ui::KeyEvent(ui::EventType::kKeyPressed, ui::VKEY_SPACE, ui::EF_NONE));
}

views::Link* StyledLabel::GetFirstLinkForTesting() {
  const auto it = std::ranges::find_if(children(), &IsViewClass<LinkFragment>);
  return (it == children().cend()) ? nullptr : static_cast<views::Link*>(*it);
}

int StyledLabel::StartX(int excess_space) const {
  int x = GetInsets().left();
  // If the element should be aligned to the leading side (left in LTR, or right
  // in RTL), position it at the leading side Insets (left).
  if (horizontal_alignment_ ==
      (base::i18n::IsRTL() ? gfx::ALIGN_RIGHT : 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<LayoutViews>();

  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();

  // A pointer to the previous link fragment if a logical link consists of
  // multiple `LinkFragment` elements.
  LinkFragment* previous_link_fragment = nullptr;

  for (std::u16string 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<std::u16string> 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;
        }
      }

      std::u16string 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;
          // Custom views must be marked as such.
          DCHECK(custom_view->GetProperty(kStyledLabelCustomViewKey));
          // 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 && position == range.start() &&
            line_size.width() != 0) {
          SizeBounds chunk_size(content_width - line_size.width(), {});
          int custom_view_width =
              custom_view->GetPreferredSize(chunk_size).width();
          if (line_size.width() + custom_view_width > content_width) {
            // If the chunk should not be wrapped, try to fit it entirely on the
            // next line.
            break;
          }
        }

        if (chunk.size() > range.end() - position) {
          chunk = chunk.substr(0, range.end() - position);
        }

        if (!custom_view) {
          label =
              CreateLabel(chunk, style_info, range, &previous_link_fragment);
        } else {
          previous_link_fragment = nullptr;
        }

        if (position + chunk.size() >= range.end()) {
          ++current_range;
          // Links do not connect across separate style ranges.
          previous_link_fragment = nullptr;
        }
      } else {
        chunk = substrings[0];
        if (position + chunk.size() > range.start()) {
          chunk = chunk.substr(0, range.start() - position);
        }

        // This chunk is normal text.
        label =
            CreateLabel(chunk, default_style, range, &previous_link_fragment);
      }

      View* child_view = custom_view ? custom_view : label.get();
      const gfx::Size child_size = child_view->GetPreferredSize(
          SizeBounds(content_width - line_size.width(), {}));
      // A custom view could be wider than the available width.
      line_size.SetSize(
          std::min(line_size.width() + child_size.width(), content_width),
          std::max(line_size.height(), child_size.height()));

      views.push_back(child_view);
      if (label) {
        layout_views_->owned_views.push_back(std::move(label));
      }

      remaining_string = remaining_string.substr(chunk.size());

      // 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()) {
        break;
      }
    }

    if (views.empty() && remaining_string.empty()) {
      // Remove an empty last line.
      layout_size_info_.line_sizes.pop_back();
      layout_views_->views_per_line.pop_back();
    } else {
      max_width = std::max(max_width, line_size.width());
      total_height += line_size.height();

      // Trim whitespace at the start of the next line.
      can_trim_leading_whitespace = true;
    }
  }

  layout_size_info_.total_size.SetSize(max_width + insets.width(),
                                       total_height + insets.height());
}

std::unique_ptr<Label> StyledLabel::CreateLabel(
    const std::u16string& text,
    const RangeStyleInfo& style_info,
    const gfx::Range& range,
    LinkFragment** previous_link_fragment) const {
  std::unique_ptr<Label> result;
  if (style_info.text_style == style::STYLE_LINK ||
      style_info.text_style == style::STYLE_LINK_2 ||
      style_info.text_style == style::STYLE_LINK_3 ||
      style_info.text_style == style::STYLE_LINK_4 ||
      style_info.text_style == style::STYLE_LINK_5) {
    // Nothing should (and nothing does) use a custom font for links.
    DCHECK(!style_info.custom_font);

    // Note this ignores |default_text_style_|, in favor of `style::STYLE_LINK`.
    auto link = std::make_unique<LinkFragment>(
        text, text_context_, *style_info.text_style, *previous_link_fragment);
    *previous_link_fragment = link.get();
    link->SetCallback(style_info.callback);
    if (!style_info.accessible_name.empty()) {
      link->GetViewAccessibility().SetName(style_info.accessible_name);
    }

    result = std::move(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_text_style_));
  }

  if (style_info.override_color_id) {
    result->SetEnabledColor(style_info.override_color_id.value());
  } else if (style_info.override_color) {
    result->SetEnabledColor(style_info.override_color.value());
  } else if (default_enabled_color_id_) {
    result->SetEnabledColor(default_enabled_color_id_.value());
  }
  if (!style_info.tooltip.empty()) {
    result->SetCustomTooltipText(style_info.tooltip);
  }
  if (!style_info.accessible_name.empty()) {
    result->GetViewAccessibility().SetName(style_info.accessible_name);
  }
  if (displayed_on_background_color_) {
    result->SetBackgroundColor(*displayed_on_background_color_);
  }

  result->SetAutoColorReadabilityEnabled(auto_color_readability_enabled_);
  result->SetSubpixelRenderingEnabled(subpixel_rendering_enabled_);
  return result;
}

void StyledLabel::UpdateLabelBackgroundColor() {
  for (View* child : children()) {
    if (!child->GetProperty(kStyledLabelCustomViewKey) &&
        displayed_on_background_color_) {
      // TODO(kylixrd): Should updating the label background color even be
      // allowed if there are custom views?
      DCHECK(IsViewClass<Label>(child) || IsViewClass<LinkFragment>(child));
      static_cast<Label*>(child)->SetBackgroundColor(
          *displayed_on_background_color_);
    }
  }
}

void StyledLabel::RemoveOrDeleteAllChildViews() {
  pending_delete_views_.clear();
  while (children().size() > 0) {
    std::unique_ptr<View> view = RemoveChildViewT(children()[0]);
    if (view->GetProperty(kStyledLabelCustomViewKey)) {
      custom_views_.push_back(std::move(view));
    } else {
      pending_delete_views_.push_back(std::move(view));
    }
  }
}

void StyledLabel::RecreateChildViews() {
  need_recreate_child_ = false;

  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 (views::View* view : layout_views_->views_per_line[line]) {
        gfx::Size size = view->GetPreferredSize(SizeBounds(line_size));
        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::ranges::find(custom_views_, view,
                                               &std::unique_ptr<View>::get);
          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(SizeBounds(line_size)).width();
      }
    }
    DCHECK(i == children().end());  // Should not be short any lines.
  }
}

BEGIN_METADATA(StyledLabel)
ADD_PROPERTY_METADATA(std::u16string, Text)
ADD_PROPERTY_METADATA(int, TextContext)
ADD_PROPERTY_METADATA(int, DefaultTextStyle)
ADD_PROPERTY_METADATA(int, LineHeight)
ADD_PROPERTY_METADATA(bool, AutoColorReadabilityEnabled)
ADD_READONLY_PROPERTY_METADATA(std::optional<ui::ColorVariant>,
                               DisplayedOnBackgroundColor)
ADD_PROPERTY_METADATA(std::optional<ui::ColorId>, DefaultEnabledColorId)
END_METADATA

}  // namespace views