blob: ec27ada6a66d57bd2dc30f684596dc022de41be5 [file] [log] [blame]
// Copyright 2020 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 "third_party/blink/renderer/core/layout/list_marker.h"
#include "third_party/blink/renderer/core/layout/layout_image_resource_style_image.h"
#include "third_party/blink/renderer/core/layout/layout_inside_list_marker.h"
#include "third_party/blink/renderer/core/layout/layout_list_item.h"
#include "third_party/blink/renderer/core/layout/layout_list_marker_image.h"
#include "third_party/blink/renderer/core/layout/layout_outside_list_marker.h"
#include "third_party/blink/renderer/core/layout/list_marker_text.h"
#include "third_party/blink/renderer/core/layout/ng/list/layout_ng_inside_list_marker.h"
#include "third_party/blink/renderer/core/layout/ng/list/layout_ng_list_item.h"
#include "third_party/blink/renderer/core/layout/ng/list/layout_ng_outside_list_marker.h"
namespace blink {
const int kCMarkerPaddingPx = 7;
// TODO(glebl): Move to core/html/resources/html.css after
// Blink starts to support ::marker crbug.com/457718
// Recommended UA margin for list markers.
const int kCUAMarkerMarginEm = 1;
ListMarker::ListMarker() : marker_text_type_(kNotText) {}
const ListMarker* ListMarker::Get(const LayoutObject* marker) {
if (auto* outside_marker = ToLayoutOutsideListMarkerOrNull(marker))
return &outside_marker->Marker();
if (auto* inside_marker = ToLayoutInsideListMarkerOrNull(marker))
return &inside_marker->Marker();
if (auto* ng_outside_marker = ToLayoutNGOutsideListMarkerOrNull(marker))
return &ng_outside_marker->Marker();
if (auto* ng_inside_marker = ToLayoutNGInsideListMarkerOrNull(marker))
return &ng_inside_marker->Marker();
return nullptr;
}
ListMarker* ListMarker::Get(LayoutObject* marker) {
return const_cast<ListMarker*>(
ListMarker::Get(static_cast<const LayoutObject*>(marker)));
}
LayoutObject* ListMarker::MarkerFromListItem(const LayoutObject* list_item) {
if (auto* legacy_list_item = ToLayoutListItemOrNull(list_item))
return legacy_list_item->Marker();
if (auto* ng_list_item = ToLayoutNGListItemOrNull(list_item))
return ng_list_item->Marker();
return nullptr;
}
LayoutObject* ListMarker::ListItem(const LayoutObject& marker) const {
DCHECK_EQ(Get(&marker), this);
LayoutObject* list_item = marker.GetNode()->parentNode()->GetLayoutObject();
DCHECK(list_item);
DCHECK(list_item->IsListItemIncludingNG());
return list_item;
}
LayoutBlockFlow* ListMarker::ListItemBlockFlow(
const LayoutObject& marker) const {
DCHECK_EQ(Get(&marker), this);
LayoutObject* list_item = ListItem(marker);
if (auto* legacy_list_item = ToLayoutListItemOrNull(list_item))
return legacy_list_item;
if (auto* ng_list_item = ToLayoutNGListItemOrNull(list_item))
return ng_list_item;
NOTREACHED();
return nullptr;
}
int ListMarker::ListItemValue(const LayoutObject& list_item) const {
if (auto* legacy_list_item = ToLayoutListItemOrNull(list_item))
return legacy_list_item->Value();
if (auto* ng_list_item = ToLayoutNGListItemOrNull(list_item))
return ng_list_item->Value();
NOTREACHED();
return 0;
}
// If the value of ListStyleType changed, we need to the marker text has been
// updated.
void ListMarker::ListStyleTypeChanged(LayoutObject& marker) {
DCHECK_EQ(Get(&marker), this);
if (marker_text_type_ == kNotText || marker_text_type_ == kUnresolved)
return;
marker_text_type_ = kUnresolved;
marker.SetNeedsLayoutAndIntrinsicWidthsRecalcAndFullPaintInvalidation(
layout_invalidation_reason::kListStyleTypeChange);
}
void ListMarker::OrdinalValueChanged(LayoutObject& marker) {
DCHECK_EQ(Get(&marker), this);
if (marker_text_type_ == kOrdinalValue) {
marker_text_type_ = kUnresolved;
marker.SetNeedsLayoutAndIntrinsicWidthsRecalcAndFullPaintInvalidation(
layout_invalidation_reason::kListValueChange);
}
}
void ListMarker::UpdateMarkerText(LayoutObject& marker, LayoutText* text) {
DCHECK_EQ(Get(&marker), this);
DCHECK(text);
DCHECK_EQ(marker_text_type_, kUnresolved);
StringBuilder marker_text_builder;
marker_text_type_ = MarkerText(marker, &marker_text_builder, kWithSuffix);
text->SetTextIfNeeded(marker_text_builder.ToString().ReleaseImpl());
DCHECK_NE(marker_text_type_, kNotText);
DCHECK_NE(marker_text_type_, kUnresolved);
}
void ListMarker::UpdateMarkerText(LayoutObject& marker) {
DCHECK_EQ(Get(&marker), this);
UpdateMarkerText(marker, ToLayoutText(marker.SlowFirstChild()));
}
ListMarker::MarkerTextType ListMarker::MarkerText(
const LayoutObject& marker,
StringBuilder* text,
MarkerTextFormat format) const {
DCHECK_EQ(Get(&marker), this);
if (!marker.StyleRef().ContentBehavesAsNormal())
return kNotText;
if (IsMarkerImage(marker)) {
if (format == kWithSuffix)
text->Append(' ');
return kNotText;
}
LayoutObject* list_item = ListItem(marker);
const ComputedStyle& style = list_item->StyleRef();
switch (GetListStyleCategory(style.ListStyleType())) {
case ListStyleCategory::kNone:
return kNotText;
case ListStyleCategory::kStaticString:
text->Append(style.ListStyleStringValue());
return kStatic;
case ListStyleCategory::kSymbol:
// value is ignored for these types
text->Append(list_marker_text::GetText(style.ListStyleType(), 0));
if (format == kWithSuffix)
text->Append(' ');
return kSymbolValue;
case ListStyleCategory::kLanguage: {
int value = ListItemValue(*list_item);
text->Append(list_marker_text::GetText(style.ListStyleType(), value));
if (format == kWithSuffix) {
text->Append(list_marker_text::Suffix(style.ListStyleType(), value));
text->Append(' ');
}
return kOrdinalValue;
}
}
NOTREACHED();
return kStatic;
}
String ListMarker::MarkerTextWithSuffix(const LayoutObject& marker) const {
DCHECK_EQ(Get(&marker), this);
StringBuilder text;
MarkerText(marker, &text, kWithSuffix);
return text.ToString();
}
String ListMarker::MarkerTextWithoutSuffix(const LayoutObject& marker) const {
DCHECK_EQ(Get(&marker), this);
StringBuilder text;
MarkerText(marker, &text, kWithoutSuffix);
return text.ToString();
}
String ListMarker::TextAlternative(const LayoutObject& marker) const {
DCHECK_EQ(Get(&marker), this);
// For accessibility, return the marker string in the logical order even in
// RTL, reflecting speech order.
return MarkerTextWithSuffix(marker);
}
void ListMarker::UpdateMarkerContentIfNeeded(LayoutObject& marker) {
DCHECK_EQ(Get(&marker), this);
if (!marker.StyleRef().ContentBehavesAsNormal()) {
marker_text_type_ = kNotText;
return;
}
// There should be at most one child.
LayoutObject* child = marker.SlowFirstChild();
DCHECK(!child || !child->NextSibling());
const ComputedStyle& style = ListItem(marker)->StyleRef();
if (IsMarkerImage(marker)) {
StyleImage* list_style_image = style.ListStyleImage();
if (child) {
// If the url of `list-style-image` changed, create a new LayoutImage.
if (!child->IsLayoutImage() ||
ToLayoutImage(child)->ImageResource()->ImagePtr() !=
list_style_image->Data()) {
child->Destroy();
child = nullptr;
}
}
if (!child) {
LayoutListMarkerImage* image =
LayoutListMarkerImage::CreateAnonymous(&marker.GetDocument());
if (marker.IsLayoutNGListMarker())
image->SetIsLayoutNGObjectForListMarkerImage(true);
scoped_refptr<ComputedStyle> image_style =
ComputedStyle::CreateAnonymousStyleWithDisplay(marker.StyleRef(),
EDisplay::kInline);
image->SetStyle(image_style);
image->SetImageResource(
MakeGarbageCollected<LayoutImageResourceStyleImage>(
list_style_image));
image->SetIsGeneratedContent();
marker.AddChild(image);
}
marker_text_type_ = kNotText;
return;
}
if (style.ListStyleType() == EListStyleType::kNone) {
marker_text_type_ = kNotText;
return;
}
// Create a LayoutText in it.
LayoutText* text = nullptr;
// |text_style| should be as same as style propagated in
// |LayoutObject::PropagateStyleToAnonymousChildren()| to avoid unexpected
// full layout due by style difference. See http://crbug.com/980399
scoped_refptr<ComputedStyle> text_style =
ComputedStyle::CreateAnonymousStyleWithDisplay(
marker.StyleRef(), marker.StyleRef().Display());
if (child) {
if (child->IsText()) {
text = ToLayoutText(child);
text->SetStyle(text_style);
} else {
child->Destroy();
child = nullptr;
}
}
if (!child) {
text = LayoutText::CreateEmptyAnonymous(marker.GetDocument(), text_style,
LegacyLayout::kAuto);
marker.AddChild(text);
marker_text_type_ = kUnresolved;
}
}
LayoutObject* ListMarker::SymbolMarkerLayoutText(
const LayoutObject& marker) const {
DCHECK_EQ(Get(&marker), this);
if (marker_text_type_ != kSymbolValue)
return nullptr;
return marker.SlowFirstChild();
}
bool ListMarker::IsMarkerImage(const LayoutObject& marker) const {
DCHECK_EQ(Get(&marker), this);
return marker.StyleRef().ContentBehavesAsNormal() &&
ListItem(marker)->StyleRef().GeneratesMarkerImage();
}
LayoutUnit ListMarker::WidthOfSymbol(const ComputedStyle& style) {
const Font& font = style.GetFont();
const SimpleFontData* font_data = font.PrimaryFont();
DCHECK(font_data);
if (!font_data)
return LayoutUnit();
return LayoutUnit((font_data->GetFontMetrics().Ascent() * 2 / 3 + 1) / 2 + 2);
}
std::pair<LayoutUnit, LayoutUnit> ListMarker::InlineMarginsForInside(
const ComputedStyle& marker_style,
const ComputedStyle& list_item_style) {
if (!marker_style.ContentBehavesAsNormal())
return {};
if (list_item_style.GeneratesMarkerImage())
return {LayoutUnit(), LayoutUnit(kCMarkerPaddingPx)};
switch (GetListStyleCategory(list_item_style.ListStyleType())) {
case ListStyleCategory::kSymbol:
return {LayoutUnit(-1),
LayoutUnit(kCUAMarkerMarginEm * marker_style.ComputedFontSize())};
default:
break;
}
return {};
}
std::pair<LayoutUnit, LayoutUnit> ListMarker::InlineMarginsForOutside(
const ComputedStyle& marker_style,
const ComputedStyle& list_item_style,
LayoutUnit marker_inline_size) {
LayoutUnit margin_start;
LayoutUnit margin_end;
if (!marker_style.ContentBehavesAsNormal()) {
margin_start = -marker_inline_size;
} else if (list_item_style.GeneratesMarkerImage()) {
margin_start = -marker_inline_size - kCMarkerPaddingPx;
margin_end = LayoutUnit(kCMarkerPaddingPx);
} else {
switch (GetListStyleCategory(list_item_style.ListStyleType())) {
case ListStyleCategory::kNone:
break;
case ListStyleCategory::kSymbol: {
const SimpleFontData* font_data = marker_style.GetFont().PrimaryFont();
DCHECK(font_data);
if (!font_data)
return {};
const FontMetrics& font_metrics = font_data->GetFontMetrics();
int offset = font_metrics.Ascent() * 2 / 3;
margin_start = LayoutUnit(-offset - kCMarkerPaddingPx - 1);
margin_end = offset + kCMarkerPaddingPx + 1 - marker_inline_size;
break;
}
default:
margin_start = -marker_inline_size;
}
}
DCHECK_EQ(margin_start + margin_end, -marker_inline_size);
return {margin_start, margin_end};
}
LayoutRect ListMarker::RelativeSymbolMarkerRect(const ComputedStyle& style,
LayoutUnit width) {
LayoutRect relative_rect;
const SimpleFontData* font_data = style.GetFont().PrimaryFont();
DCHECK(font_data);
if (!font_data)
return LayoutRect();
// TODO(wkorman): Review and clean up/document the calculations below.
// http://crbug.com/543193
const FontMetrics& font_metrics = font_data->GetFontMetrics();
int ascent = font_metrics.Ascent();
int bullet_width = (ascent * 2 / 3 + 1) / 2;
relative_rect = LayoutRect(1, 3 * (ascent - ascent * 2 / 3) / 2, bullet_width,
bullet_width);
if (!style.IsHorizontalWritingMode()) {
relative_rect = relative_rect.TransposedRect();
relative_rect.SetX(width - relative_rect.X() - relative_rect.Width());
}
return relative_rect;
}
ListMarker::ListStyleCategory ListMarker::GetListStyleCategory(
EListStyleType type) {
switch (type) {
case EListStyleType::kNone:
return ListStyleCategory::kNone;
case EListStyleType::kString:
return ListStyleCategory::kStaticString;
case EListStyleType::kDisc:
case EListStyleType::kCircle:
case EListStyleType::kSquare:
return ListStyleCategory::kSymbol;
case EListStyleType::kArabicIndic:
case EListStyleType::kArmenian:
case EListStyleType::kBengali:
case EListStyleType::kCambodian:
case EListStyleType::kCjkIdeographic:
case EListStyleType::kCjkEarthlyBranch:
case EListStyleType::kCjkHeavenlyStem:
case EListStyleType::kDecimalLeadingZero:
case EListStyleType::kDecimal:
case EListStyleType::kDevanagari:
case EListStyleType::kEthiopicHalehame:
case EListStyleType::kEthiopicHalehameAm:
case EListStyleType::kEthiopicHalehameTiEr:
case EListStyleType::kEthiopicHalehameTiEt:
case EListStyleType::kGeorgian:
case EListStyleType::kGujarati:
case EListStyleType::kGurmukhi:
case EListStyleType::kHangul:
case EListStyleType::kHangulConsonant:
case EListStyleType::kHebrew:
case EListStyleType::kHiragana:
case EListStyleType::kHiraganaIroha:
case EListStyleType::kKannada:
case EListStyleType::kKatakana:
case EListStyleType::kKatakanaIroha:
case EListStyleType::kKhmer:
case EListStyleType::kKoreanHangulFormal:
case EListStyleType::kKoreanHanjaFormal:
case EListStyleType::kKoreanHanjaInformal:
case EListStyleType::kLao:
case EListStyleType::kLowerAlpha:
case EListStyleType::kLowerArmenian:
case EListStyleType::kLowerGreek:
case EListStyleType::kLowerLatin:
case EListStyleType::kLowerRoman:
case EListStyleType::kMalayalam:
case EListStyleType::kMongolian:
case EListStyleType::kMyanmar:
case EListStyleType::kOriya:
case EListStyleType::kPersian:
case EListStyleType::kSimpChineseFormal:
case EListStyleType::kSimpChineseInformal:
case EListStyleType::kTelugu:
case EListStyleType::kThai:
case EListStyleType::kTibetan:
case EListStyleType::kTradChineseFormal:
case EListStyleType::kTradChineseInformal:
case EListStyleType::kUpperAlpha:
case EListStyleType::kUpperArmenian:
case EListStyleType::kUpperLatin:
case EListStyleType::kUpperRoman:
case EListStyleType::kUrdu:
return ListStyleCategory::kLanguage;
default:
NOTREACHED();
return ListStyleCategory::kLanguage;
}
}
} // namespace blink