blob: 5e94a811c18d6979aac3333e8d47e9e4d8328dab [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/mathml/ng_math_under_over_layout_algorithm.h"
#include "third_party/blink/renderer/core/layout/ng/mathml/ng_math_layout_utils.h"
#include "third_party/blink/renderer/core/layout/ng/ng_block_break_token.h"
#include "third_party/blink/renderer/core/layout/ng/ng_box_fragment.h"
#include "third_party/blink/renderer/core/layout/ng/ng_length_utils.h"
#include "third_party/blink/renderer/core/layout/ng/ng_out_of_flow_layout_part.h"
#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h"
#include "third_party/blink/renderer/core/mathml/mathml_operator_element.h"
#include "third_party/blink/renderer/core/mathml/mathml_under_over_element.h"
namespace blink {
namespace {
// Describes the amount to shift to apply to the under/over boxes.
// Data is populated from the OpenType MATH table.
// If the OpenType MATH table is not present fallback values are used.
// https://mathml-refresh.github.io/mathml-core/#base-with-underscript
// https://mathml-refresh.github.io/mathml-core/#base-with-overscript
struct UnderOverVerticalParameters {
bool use_under_over_bar_fallback;
LayoutUnit under_gap_min;
LayoutUnit over_gap_min;
LayoutUnit under_shift_min;
LayoutUnit over_shift_min;
LayoutUnit under_extra_descender;
LayoutUnit over_extra_ascender;
LayoutUnit accent_base_height;
};
UnderOverVerticalParameters GetUnderOverVerticalParameters(
const ComputedStyle& style,
bool is_base_large_operator,
bool is_base_stretchy_in_inline_axis) {
UnderOverVerticalParameters parameters;
const SimpleFontData* font_data = style.GetFont().PrimaryFont();
if (!font_data)
return parameters;
// https://mathml-refresh.github.io/mathml-core/#dfn-default-fallback-constant
const float default_fallback_constant = 0;
if (is_base_large_operator) {
parameters.under_gap_min = LayoutUnit(
MathConstant(style,
OpenTypeMathSupport::MathConstants::kLowerLimitGapMin)
.value_or(default_fallback_constant));
parameters.over_gap_min = LayoutUnit(
MathConstant(style,
OpenTypeMathSupport::MathConstants::kUpperLimitGapMin)
.value_or(default_fallback_constant));
parameters.under_shift_min = LayoutUnit(
MathConstant(
style,
OpenTypeMathSupport::MathConstants::kLowerLimitBaselineDropMin)
.value_or(default_fallback_constant));
parameters.over_shift_min = LayoutUnit(
MathConstant(
style,
OpenTypeMathSupport::MathConstants::kUpperLimitBaselineRiseMin)
.value_or(default_fallback_constant));
parameters.under_extra_descender = LayoutUnit();
parameters.over_extra_ascender = LayoutUnit();
parameters.accent_base_height = LayoutUnit();
parameters.use_under_over_bar_fallback = false;
return parameters;
}
if (is_base_stretchy_in_inline_axis) {
parameters.under_gap_min = LayoutUnit(
MathConstant(
style, OpenTypeMathSupport::MathConstants::kStretchStackGapBelowMin)
.value_or(default_fallback_constant));
parameters.over_gap_min = LayoutUnit(
MathConstant(
style, OpenTypeMathSupport::MathConstants::kStretchStackGapAboveMin)
.value_or(default_fallback_constant));
parameters.under_shift_min = LayoutUnit(
MathConstant(
style,
OpenTypeMathSupport::MathConstants::kStretchStackBottomShiftDown)
.value_or(default_fallback_constant));
parameters.over_shift_min = LayoutUnit(
MathConstant(
style, OpenTypeMathSupport::MathConstants::kStretchStackTopShiftUp)
.value_or(default_fallback_constant));
parameters.under_extra_descender = LayoutUnit();
parameters.over_extra_ascender = LayoutUnit();
parameters.accent_base_height = LayoutUnit();
parameters.use_under_over_bar_fallback = false;
return parameters;
}
const float default_rule_thickness = RuleThicknessFallback(style);
parameters.under_gap_min = LayoutUnit(
MathConstant(style,
OpenTypeMathSupport::MathConstants::kUnderbarVerticalGap)
.value_or(3 * default_rule_thickness));
parameters.over_gap_min = LayoutUnit(
MathConstant(style,
OpenTypeMathSupport::MathConstants::kOverbarVerticalGap)
.value_or(3 * default_rule_thickness));
parameters.under_shift_min = LayoutUnit();
parameters.over_shift_min = LayoutUnit();
parameters.under_extra_descender = LayoutUnit(
MathConstant(style,
OpenTypeMathSupport::MathConstants::kUnderbarExtraDescender)
.value_or(default_rule_thickness));
parameters.over_extra_ascender = LayoutUnit(
MathConstant(style,
OpenTypeMathSupport::MathConstants::kOverbarExtraAscender)
.value_or(default_rule_thickness));
parameters.accent_base_height = LayoutUnit(
MathConstant(style, OpenTypeMathSupport::MathConstants::kAccentBaseHeight)
.value_or(font_data->GetFontMetrics().XHeight() / 2));
parameters.use_under_over_bar_fallback = true;
return parameters;
}
// https://mathml-refresh.github.io/mathml-core/#underscripts-and-overscripts-munder-mover-munderover
bool HasAccent(const NGBlockNode& node, bool accent_under) {
DCHECK(node);
auto* underover = To<MathMLUnderOverElement>(node.GetDOMNode());
auto script_type = underover->GetScriptType();
DCHECK(script_type == MathScriptType::kUnderOver ||
(accent_under && script_type == MathScriptType::kUnder) ||
(!accent_under && script_type == MathScriptType::kOver));
base::Optional<bool> attribute_value =
accent_under ? underover->AccentUnder() : underover->Accent();
return attribute_value && *attribute_value;
}
} // namespace
NGMathUnderOverLayoutAlgorithm::NGMathUnderOverLayoutAlgorithm(
const NGLayoutAlgorithmParams& params)
: NGLayoutAlgorithm(params) {
DCHECK(params.space.IsNewFormattingContext());
}
void NGMathUnderOverLayoutAlgorithm::GatherChildren(NGBlockNode* base,
NGBlockNode* over,
NGBlockNode* under) {
auto script_type = Node().ScriptType();
for (NGLayoutInputNode child = Node().FirstChild(); child;
child = child.NextSibling()) {
NGBlockNode block_child = To<NGBlockNode>(child);
if (child.IsOutOfFlowPositioned()) {
container_builder_.AddOutOfFlowChildCandidate(
block_child, BorderScrollbarPadding().StartOffset());
continue;
}
if (!*base) {
*base = block_child;
continue;
}
switch (script_type) {
case MathScriptType::kUnder:
DCHECK(!*under);
*under = block_child;
break;
case MathScriptType::kOver:
DCHECK(!*over);
*over = block_child;
break;
case MathScriptType::kUnderOver:
if (!*under) {
*under = block_child;
continue;
}
DCHECK(!*over);
*over = block_child;
break;
default:
NOTREACHED();
}
}
}
scoped_refptr<const NGLayoutResult> NGMathUnderOverLayoutAlgorithm::Layout() {
DCHECK(!BreakToken());
DCHECK(IsValidMathMLScript(Node()));
NGBlockNode base = nullptr;
NGBlockNode over = nullptr;
NGBlockNode under = nullptr;
GatherChildren(&base, &over, &under);
const LogicalSize border_box_size = container_builder_.InitialBorderBoxSize();
const LogicalOffset content_start_offset =
BorderScrollbarPadding().StartOffset();
LayoutUnit block_offset = content_start_offset.block_offset;
bool is_base_large_operator = false;
bool is_base_stretchy_in_inline_axis = false;
if (auto* core_operator =
DynamicTo<MathMLOperatorElement>(base.GetDOMNode())) {
// TODO(crbug.com/1124298): Implement embellished operators.
is_base_large_operator =
core_operator->HasBooleanProperty(MathMLOperatorElement::kLargeOp);
is_base_stretchy_in_inline_axis =
core_operator->HasBooleanProperty(MathMLOperatorElement::kStretchy) &&
!core_operator->GetOperatorContent().is_vertical;
}
UnderOverVerticalParameters parameters = GetUnderOverVerticalParameters(
Style(), is_base_large_operator, is_base_stretchy_in_inline_axis);
// TODO(crbug.com/1124301): handle stretchy operators.
// TODO(crbug.com/1125136): take into account italic correction.
auto base_space = CreateConstraintSpaceForMathChild(
Node(), ChildAvailableSize(), ConstraintSpace(), base);
auto base_layout_result = base.Layout(base_space);
auto base_margins =
ComputeMarginsFor(base_space, base.Style(), ConstraintSpace());
NGBoxFragment base_fragment(
ConstraintSpace().GetWritingDirection(),
To<NGPhysicalBoxFragment>(base_layout_result->PhysicalFragment()));
LayoutUnit base_ascent = base_fragment.BaselineOrSynthesize();
// All children are positioned centered relative to the container (and
// therefore centered relative to themselves).
if (over) {
auto over_space = CreateConstraintSpaceForMathChild(
Node(), ChildAvailableSize(), ConstraintSpace(), over);
scoped_refptr<const NGLayoutResult> over_layout_result =
over.Layout(over_space);
NGBoxStrut over_margins =
ComputeMarginsFor(over_space, over.Style(), ConstraintSpace());
NGBoxFragment over_fragment(
ConstraintSpace().GetWritingDirection(),
To<NGPhysicalBoxFragment>(over_layout_result->PhysicalFragment()));
block_offset += parameters.over_extra_ascender + over_margins.block_start;
LogicalOffset over_offset = {
content_start_offset.inline_offset + over_margins.inline_start +
(ChildAvailableSize().inline_size -
(over_fragment.InlineSize() + over_margins.InlineSum())) /
2,
block_offset};
container_builder_.AddChild(over_layout_result->PhysicalFragment(),
over_offset);
over.StoreMargins(ConstraintSpace(), over_margins);
if (parameters.use_under_over_bar_fallback) {
block_offset += over_fragment.BlockSize();
if (HasAccent(Node(), false)) {
if (base_ascent < parameters.accent_base_height)
block_offset += parameters.accent_base_height - base_ascent;
} else {
block_offset += parameters.over_gap_min;
}
} else {
LayoutUnit over_ascent = over_fragment.BaselineOrSynthesize();
block_offset +=
std::max(over_fragment.BlockSize() + parameters.over_gap_min,
over_ascent + parameters.over_shift_min);
}
block_offset += over_margins.block_end;
}
block_offset += base_margins.block_start;
LogicalOffset base_offset = {
content_start_offset.inline_offset + base_margins.inline_start +
(ChildAvailableSize().inline_size -
(base_fragment.InlineSize() + base_margins.InlineSum())) /
2,
block_offset};
container_builder_.AddChild(base_layout_result->PhysicalFragment(),
base_offset);
base.StoreMargins(ConstraintSpace(), base_margins);
block_offset += base_fragment.BlockSize() + base_margins.block_end;
if (under) {
auto under_space = CreateConstraintSpaceForMathChild(
Node(), ChildAvailableSize(), ConstraintSpace(), under);
scoped_refptr<const NGLayoutResult> under_layout_result =
under.Layout(under_space);
NGBoxStrut under_margins =
ComputeMarginsFor(under_space, under.Style(), ConstraintSpace());
NGBoxFragment under_fragment(
ConstraintSpace().GetWritingDirection(),
To<NGPhysicalBoxFragment>(under_layout_result->PhysicalFragment()));
block_offset += under_margins.block_start;
if (parameters.use_under_over_bar_fallback) {
if (!HasAccent(Node(), true))
block_offset += parameters.under_gap_min;
} else {
LayoutUnit under_ascent = under_fragment.BaselineOrSynthesize();
block_offset += std::max(parameters.under_gap_min,
parameters.under_shift_min - under_ascent);
}
LogicalOffset under_offset = {
content_start_offset.inline_offset + under_margins.inline_start +
(ChildAvailableSize().inline_size -
(under_fragment.InlineSize() + under_margins.InlineSum())) /
2,
block_offset};
block_offset += under_fragment.BlockSize();
block_offset += parameters.under_extra_descender;
container_builder_.AddChild(under_layout_result->PhysicalFragment(),
under_offset);
under.StoreMargins(ConstraintSpace(), under_margins);
block_offset += under_margins.block_end;
}
container_builder_.SetBaseline(base_offset.block_offset + base_ascent);
block_offset += BorderScrollbarPadding().block_end;
LayoutUnit block_size = ComputeBlockSizeForFragment(
ConstraintSpace(), Style(), BorderPadding(), block_offset,
border_box_size.inline_size, Node().ShouldBeConsideredAsReplaced());
container_builder_.SetIntrinsicBlockSize(block_offset);
container_builder_.SetFragmentsTotalBlockSize(block_size);
NGOutOfFlowLayoutPart(Node(), ConstraintSpace(), &container_builder_).Run();
return container_builder_.ToBoxFragment();
}
MinMaxSizesResult NGMathUnderOverLayoutAlgorithm::ComputeMinMaxSizes(
const MinMaxSizesInput& child_input) const {
DCHECK(IsValidMathMLScript(Node()));
if (auto result = CalculateMinMaxSizesIgnoringChildren(
Node(), BorderScrollbarPadding()))
return *result;
MinMaxSizes sizes;
bool depends_on_percentage_block_size = false;
for (NGLayoutInputNode child = Node().FirstChild(); child;
child = child.NextSibling()) {
if (child.IsOutOfFlowPositioned())
continue;
// TODO(crbug.com/1125136): take into account italic correction.
auto child_result = ComputeMinAndMaxContentContribution(
Style(), To<NGBlockNode>(child), child_input);
NGBoxStrut margins = ComputeMinMaxMargins(Style(), child);
child_result.sizes += margins.InlineSum();
sizes.Encompass(child_result.sizes);
depends_on_percentage_block_size |=
child_result.depends_on_percentage_block_size;
}
sizes += BorderScrollbarPadding().InlineSum();
return MinMaxSizesResult(sizes, depends_on_percentage_block_size);
}
} // namespace blink