blob: 910ad2efea260370f582a597c31da18c649747ea [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/ng/table/layout_ng_table.h"
#include "third_party/blink/renderer/core/layout/layout_analyzer.h"
#include "third_party/blink/renderer/core/layout/layout_object_factory.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/layout/ng/ng_block_node.h"
#include "third_party/blink/renderer/core/layout/ng/ng_constraint_space.h"
#include "third_party/blink/renderer/core/layout/ng/ng_layout_result.h"
#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h"
#include "third_party/blink/renderer/core/layout/ng/table/layout_ng_table_caption.h"
#include "third_party/blink/renderer/core/layout/ng/table/layout_ng_table_column.h"
#include "third_party/blink/renderer/core/layout/ng/table/layout_ng_table_section.h"
#include "third_party/blink/renderer/core/layout/ng/table/ng_table_borders.h"
#include "third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm_helpers.h"
#include "third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm_utils.h"
#include "third_party/blink/renderer/core/paint/ng/ng_box_fragment_painter.h"
#include "third_party/blink/renderer/core/paint/ng/ng_table_painters.h"
namespace blink {
namespace {
inline bool NeedsTableSection(const LayoutObject& object) {
// Return true if 'object' can't exist in an anonymous table without being
// wrapped in a table section box.
EDisplay display = object.StyleRef().Display();
return display != EDisplay::kTableCaption &&
display != EDisplay::kTableColumnGroup &&
display != EDisplay::kTableColumn;
}
} // namespace
LayoutNGTable::LayoutNGTable(Element* element)
: LayoutNGMixin<LayoutBlock>(element) {}
wtf_size_t LayoutNGTable::ColumnCount() const {
NOT_DESTROYED();
const NGLayoutResult* cached_layout_result = GetCachedLayoutResult();
if (!cached_layout_result)
return 0;
return cached_layout_result->TableColumnCount();
}
bool LayoutNGTable::HasCollapsedBorders() const {
NOT_DESTROYED();
return cached_table_borders_ && cached_table_borders_->IsCollapsed();
}
void LayoutNGTable::SetCachedTableBorders(
scoped_refptr<const NGTableBorders> table_borders) {
NOT_DESTROYED();
cached_table_borders_ = std::move(table_borders);
}
void LayoutNGTable::InvalidateCachedTableBorders() {
NOT_DESTROYED();
// TODO(layout-dev) When cached borders are invalidated, we could do a
// special kind of relayout where fragments can replace only TableBorders,
// keep the geometry, and repaint.
cached_table_borders_.reset();
}
const NGTableTypes::Columns* LayoutNGTable::GetCachedTableColumnConstraints() {
NOT_DESTROYED();
if (IsTableColumnsConstraintsDirty())
cached_table_columns_.reset();
return cached_table_columns_.get();
}
void LayoutNGTable::SetCachedTableColumnConstraints(
scoped_refptr<const NGTableTypes::Columns> columns) {
NOT_DESTROYED();
cached_table_columns_ = std::move(columns);
SetTableColumnConstraintDirty(false);
}
void LayoutNGTable::GridBordersChanged() {
NOT_DESTROYED();
InvalidateCachedTableBorders();
if (StyleRef().BorderCollapse() == EBorderCollapse::kCollapse) {
SetShouldDoFullPaintInvalidationWithoutGeometryChange(
PaintInvalidationReason::kStyle);
// If borders change, table fragment must be regenerated.
SetNeedsLayout(layout_invalidation_reason::kTableChanged);
}
}
void LayoutNGTable::TableGridStructureChanged() {
NOT_DESTROYED();
// Callers must ensure table layout gets invalidated.
InvalidateCachedTableBorders();
}
bool LayoutNGTable::HasBackgroundForPaint() const {
NOT_DESTROYED();
if (StyleRef().HasBackground())
return true;
DCHECK_GT(PhysicalFragmentCount(), 0u);
const NGTableFragmentData::ColumnGeometries* column_geometries =
GetPhysicalFragment(0)->TableColumnGeometries();
if (column_geometries) {
for (const auto& column_geometry : *column_geometries) {
if (column_geometry.node.Style().HasBackground())
return true;
}
}
return false;
}
void LayoutNGTable::UpdateBlockLayout(bool relayout_children) {
NOT_DESTROYED();
LayoutAnalyzer::BlockScope analyzer(*this);
if (IsOutOfFlowPositioned()) {
UpdateOutOfFlowBlockLayout();
return;
}
UpdateInFlowBlockLayout();
}
void LayoutNGTable::AddChild(LayoutObject* child, LayoutObject* before_child) {
NOT_DESTROYED();
TableGridStructureChanged();
// Only TablesNG table parts are allowed.
DCHECK(child->IsLayoutNGObject() ||
(!child->IsTableCaption() && !child->IsLayoutTableCol() &&
!child->IsTableSection()));
bool wrap_in_anonymous_section = !child->IsTableCaption() &&
!child->IsLayoutTableCol() &&
!child->IsTableSection();
if (!wrap_in_anonymous_section) {
if (before_child && before_child->Parent() != this)
before_child = SplitAnonymousBoxesAroundChild(before_child);
LayoutBox::AddChild(child, before_child);
return;
}
if (!before_child && LastChild() && LastChild()->IsTableSection() &&
LastChild()->IsAnonymous() && !LastChild()->IsBeforeContent()) {
LastChild()->AddChild(child);
return;
}
if (before_child && !before_child->IsAnonymous() &&
before_child->Parent() == this) {
LayoutNGTableSection* section =
DynamicTo<LayoutNGTableSection>(before_child->PreviousSibling());
if (section && section->IsAnonymous()) {
section->AddChild(child);
return;
}
}
LayoutObject* last_box = before_child;
while (last_box && last_box->Parent()->IsAnonymous() &&
!last_box->IsTableSection() && NeedsTableSection(*last_box))
last_box = last_box->Parent();
if (last_box && last_box->IsAnonymous() && last_box->IsTablePart() &&
!IsAfterContent(last_box)) {
if (before_child == last_box)
before_child = last_box->SlowFirstChild();
last_box->AddChild(child, before_child);
return;
}
if (before_child && !before_child->IsTableSection() &&
NeedsTableSection(*before_child))
before_child = nullptr;
LayoutBox* section =
LayoutObjectFactory::CreateAnonymousTableSectionWithParent(*this);
AddChild(section, before_child);
section->AddChild(child);
}
void LayoutNGTable::RemoveChild(LayoutObject* child) {
NOT_DESTROYED();
TableGridStructureChanged();
LayoutNGMixin<LayoutBlock>::RemoveChild(child);
}
void LayoutNGTable::StyleDidChange(StyleDifference diff,
const ComputedStyle* old_style) {
NOT_DESTROYED();
// StyleDifference handles changes in table-layout, border-spacing.
if (old_style) {
bool borders_changed = !old_style->BorderVisuallyEqual(StyleRef()) ||
(diff.TextDecorationOrColorChanged() &&
StyleRef().HasBorderColorReferencingCurrentColor());
bool collapse_changed =
StyleRef().BorderCollapse() != old_style->BorderCollapse();
if (borders_changed || collapse_changed)
GridBordersChanged();
}
LayoutNGMixin<LayoutBlock>::StyleDidChange(diff, old_style);
}
LayoutBox* LayoutNGTable::CreateAnonymousBoxWithSameTypeAs(
const LayoutObject* parent) const {
NOT_DESTROYED();
return LayoutObjectFactory::CreateAnonymousTableWithParent(*parent);
}
PhysicalRect LayoutNGTable::OverflowClipRect(
const PhysicalOffset& location,
OverlayScrollbarClipBehavior overlay_scrollbar_clip_behavior) const {
NOT_DESTROYED();
PhysicalRect clip_rect;
if (StyleRef().BorderCollapse() == EBorderCollapse::kCollapse) {
clip_rect = PhysicalRect(location, Size());
const auto overflow_clip = GetOverflowClipAxes();
IntRect infinite_rect = PhysicalRect::InfiniteIntRect();
if ((overflow_clip & kOverflowClipX) == kNoOverflowClip) {
clip_rect.offset.left = LayoutUnit(infinite_rect.X());
clip_rect.size.width = LayoutUnit(infinite_rect.Width());
}
if ((overflow_clip & kOverflowClipY) == kNoOverflowClip) {
clip_rect.offset.top = LayoutUnit(infinite_rect.Y());
clip_rect.size.height = LayoutUnit(infinite_rect.Height());
}
} else {
clip_rect = LayoutNGMixin<LayoutBlock>::OverflowClipRect(
location, overlay_scrollbar_clip_behavior);
}
// TODO(1142929)
// We cannot handle table hidden overflow with captions correctly.
// Correct handling would clip table grid content to grid content rect,
// but not clip the captions.
// Since we are not generating table's grid fragment, this is not
// possible.
// The current solution is to not clip if we have captions.
// Maybe a fix is to do an additional clip in table painter?
const LayoutBox* child = FirstChildBox();
while (child) {
if (child->IsTableCaption()) {
// If there are captions, we cannot clip to content box.
clip_rect.Unite(PhysicalRect(location, Size()));
break;
}
child = child->NextSiblingBox();
}
return clip_rect;
}
void LayoutNGTable::AddVisualEffectOverflow() {
NOT_DESTROYED();
// TODO(1061423) Fragment painting: need a correct fragment.
if (const NGPhysicalBoxFragment* fragment = GetPhysicalFragment(0)) {
DCHECK_EQ(PhysicalFragmentCount(), 1u);
// Table's collapsed borders contribute to visual overflow.
// In the inline direction, table's border box does not include
// visual border width (largest border), but does include
// layout border width (border of first cell).
// Expands border box to include visual border width.
if (const NGTableBorders* collapsed_borders =
fragment->TableCollapsedBorders()) {
PhysicalRect borders_overflow = PhysicalBorderBoxRect();
NGBoxStrut table_borders = collapsed_borders->TableBorder();
auto visual_inline_strut =
collapsed_borders->GetCollapsedBorderVisualInlineStrut();
// Expand by difference between visual and layout border width.
table_borders.inline_start =
visual_inline_strut.first - table_borders.inline_start;
table_borders.inline_end =
visual_inline_strut.second - table_borders.inline_end;
table_borders.block_start = LayoutUnit();
table_borders.block_end = LayoutUnit();
borders_overflow.Expand(
table_borders.ConvertToPhysical(StyleRef().GetWritingDirection()));
AddSelfVisualOverflow(borders_overflow);
}
}
LayoutNGMixin<LayoutBlock>::AddVisualEffectOverflow();
}
void LayoutNGTable::Paint(const PaintInfo& paint_info) const {
NOT_DESTROYED();
DCHECK_EQ(PhysicalFragmentCount(), 1u);
NGBoxFragmentPainter(*LayoutNGMixin<LayoutBlock>::GetPhysicalFragment(0))
.Paint(paint_info);
}
LayoutUnit LayoutNGTable::BorderLeft() const {
NOT_DESTROYED();
// DCHECK(cached_table_borders_.get())
// ScrollAnchoring fails this DCHECK.
if (ShouldCollapseBorders() && cached_table_borders_.get()) {
return cached_table_borders_->TableBorder()
.ConvertToPhysical(Style()->GetWritingDirection())
.left;
}
return LayoutNGMixin<LayoutBlock>::BorderLeft();
}
LayoutUnit LayoutNGTable::BorderRight() const {
NOT_DESTROYED();
// DCHECK(cached_table_borders_.get())
// ScrollAnchoring fails this DCHECK.
if (ShouldCollapseBorders() && cached_table_borders_.get()) {
return cached_table_borders_->TableBorder()
.ConvertToPhysical(Style()->GetWritingDirection())
.right;
}
return LayoutNGMixin<LayoutBlock>::BorderRight();
}
LayoutUnit LayoutNGTable::BorderTop() const {
NOT_DESTROYED();
// DCHECK(cached_table_borders_.get())
// ScrollAnchoring fails this DCHECK.
if (ShouldCollapseBorders() && cached_table_borders_.get()) {
return cached_table_borders_->TableBorder()
.ConvertToPhysical(Style()->GetWritingDirection())
.top;
}
return LayoutNGMixin<LayoutBlock>::BorderTop();
}
LayoutUnit LayoutNGTable::BorderBottom() const {
NOT_DESTROYED();
// DCHECK(cached_table_borders_.get())
// ScrollAnchoring fails this DCHECK.
if (ShouldCollapseBorders() && cached_table_borders_.get()) {
return cached_table_borders_->TableBorder()
.ConvertToPhysical(Style()->GetWritingDirection())
.bottom;
}
return LayoutNGMixin<LayoutBlock>::BorderBottom();
}
LayoutUnit LayoutNGTable::PaddingTop() const {
NOT_DESTROYED();
if (ShouldCollapseBorders())
return LayoutUnit();
return LayoutNGMixin<LayoutBlock>::PaddingTop();
}
LayoutUnit LayoutNGTable::PaddingBottom() const {
NOT_DESTROYED();
if (ShouldCollapseBorders())
return LayoutUnit();
return LayoutNGMixin<LayoutBlock>::PaddingBottom();
}
LayoutUnit LayoutNGTable::PaddingLeft() const {
NOT_DESTROYED();
if (ShouldCollapseBorders())
return LayoutUnit();
return LayoutNGMixin<LayoutBlock>::PaddingLeft();
}
LayoutUnit LayoutNGTable::PaddingRight() const {
NOT_DESTROYED();
if (ShouldCollapseBorders())
return LayoutUnit();
return LayoutNGMixin<LayoutBlock>::PaddingRight();
}
LayoutRectOutsets LayoutNGTable::BorderBoxOutsets() const {
NOT_DESTROYED();
// DCHECK(cached_table_borders_.get())
// ScrollAnchoring fails this DCHECK.
if (PhysicalFragmentCount() > 0) {
return GetPhysicalFragment(0)->Borders().ToLayoutRectOutsets();
}
NOTREACHED();
return LayoutRectOutsets();
}
// Effective column index is index of columns with mergeable
// columns skipped. Used in a11y.
unsigned LayoutNGTable::AbsoluteColumnToEffectiveColumn(
unsigned absolute_column_index) const {
NOT_DESTROYED();
if (!cached_table_columns_) {
NOTREACHED();
return absolute_column_index;
}
unsigned effective_column_index = 0;
unsigned column_count = cached_table_columns_.get()->data.size();
for (unsigned current_column_index = 0; current_column_index < column_count;
++current_column_index) {
if (current_column_index != 0 &&
!cached_table_columns_.get()->data[current_column_index].is_mergeable)
++effective_column_index;
if (current_column_index == absolute_column_index)
return effective_column_index;
}
return effective_column_index;
}
bool LayoutNGTable::IsFirstCell(const LayoutNGTableCellInterface& cell) const {
NOT_DESTROYED();
const LayoutNGTableRowInterface* row = cell.RowInterface();
if (row->FirstCellInterface() != &cell)
return false;
const LayoutNGTableSectionInterface* section = row->SectionInterface();
if (section->FirstRowInterface() != row)
return false;
NGTableGroupedChildren grouped_children(
NGBlockNode(const_cast<LayoutNGTable*>(this)));
auto first_section = grouped_children.begin();
return first_section != grouped_children.end() &&
ToInterface<LayoutNGTableSectionInterface>(
(*first_section).GetLayoutBox()) == section;
}
// Only called from AXLayoutObject::IsDataTable()
LayoutNGTableSectionInterface* LayoutNGTable::FirstBodyInterface() const {
NOT_DESTROYED();
for (LayoutObject* child = FirstChild(); child;
child = child->NextSibling()) {
if (child->StyleRef().Display() == EDisplay::kTableRowGroup)
return ToInterface<LayoutNGTableSectionInterface>(child);
}
return nullptr;
}
// Called from many AXLayoutObject methods.
LayoutNGTableSectionInterface* LayoutNGTable::TopSectionInterface() const {
NOT_DESTROYED();
NGTableGroupedChildren grouped_children(
NGBlockNode(const_cast<LayoutNGTable*>(this)));
auto first_section = grouped_children.begin();
if (first_section != grouped_children.end()) {
return ToInterface<LayoutNGTableSectionInterface>(
(*first_section).GetLayoutBox());
}
return nullptr;
}
// Called from many AXLayoutObject methods.
LayoutNGTableSectionInterface* LayoutNGTable::SectionBelowInterface(
const LayoutNGTableSectionInterface* target,
SkipEmptySectionsValue skip) const {
NOT_DESTROYED();
NGTableGroupedChildren grouped_children(
NGBlockNode(const_cast<LayoutNGTable*>(this)));
bool found = false;
for (NGBlockNode section : grouped_children) {
if (found &&
((skip == kDoNotSkipEmptySections) || (!section.IsEmptyTableSection())))
return To<LayoutNGTableSection>(section.GetLayoutBox());
if (target == To<LayoutNGTableSection>(section.GetLayoutBox())
->ToLayoutNGTableSectionInterface())
found = true;
}
return nullptr;
}
} // namespace blink