blob: 56a9d5efaf1d88eb7c173341fa5da654854dca31 [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_scripts_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"
namespace blink {
namespace {
using MathConstants = OpenTypeMathSupport::MathConstants;
static bool IsPrescriptDelimiter(const NGBlockNode& blockNode) {
auto* node = blockNode.GetDOMNode();
return node && IsA<MathMLElement>(node) &&
node->HasTagName(mathml_names::kMprescriptsTag);
}
LayoutUnit GetSpaceAfterScript(const ComputedStyle& style) {
return LayoutUnit(MathConstant(style, MathConstants::kSpaceAfterScript)
.value_or(style.FontSize() / 5));
}
// Describes the amount of shift to apply to the sub/sup 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-subscript
// https://mathml-refresh.github.io/mathml-core/#base-with-superscript
// https://mathml-refresh.github.io/mathml-core/#base-with-subscript-and-superscript
struct ScriptsVerticalParameters {
STACK_ALLOCATED();
public:
LayoutUnit subscript_shift_down;
LayoutUnit superscript_shift_up;
LayoutUnit superscript_shift_up_cramped;
LayoutUnit subscript_baseline_drop_min;
LayoutUnit superscript_baseline_drop_max;
LayoutUnit sub_superscript_gap_min;
LayoutUnit superscript_bottom_min;
LayoutUnit subscript_top_max;
LayoutUnit superscript_bottom_max_with_subscript;
};
ScriptsVerticalParameters GetScriptsVerticalParameters(
const ComputedStyle& style) {
ScriptsVerticalParameters parameters;
const SimpleFontData* font_data = style.GetFont().PrimaryFont();
if (!font_data)
return parameters;
auto x_height = font_data->GetFontMetrics().XHeight();
parameters.subscript_shift_down =
LayoutUnit(MathConstant(style, MathConstants::kSubscriptShiftDown)
.value_or(x_height / 3));
parameters.superscript_shift_up =
LayoutUnit(MathConstant(style, MathConstants::kSuperscriptShiftUp)
.value_or(x_height));
parameters.superscript_shift_up_cramped =
LayoutUnit(MathConstant(style, MathConstants::kSuperscriptShiftUpCramped)
.value_or(x_height));
parameters.subscript_baseline_drop_min =
LayoutUnit(MathConstant(style, MathConstants::kSubscriptBaselineDropMin)
.value_or(x_height / 2));
parameters.superscript_baseline_drop_max =
LayoutUnit(MathConstant(style, MathConstants::kSuperscriptBaselineDropMax)
.value_or(x_height / 2));
parameters.sub_superscript_gap_min =
LayoutUnit(MathConstant(style, MathConstants::kSubSuperscriptGapMin)
.value_or(style.FontSize() / 5));
parameters.superscript_bottom_min =
LayoutUnit(MathConstant(style, MathConstants::kSuperscriptBottomMin)
.value_or(x_height / 4));
parameters.subscript_top_max =
LayoutUnit(MathConstant(style, MathConstants::kSubscriptTopMax)
.value_or(4 * x_height / 5));
parameters.superscript_bottom_max_with_subscript = LayoutUnit(
MathConstant(style, MathConstants::kSuperscriptBottomMaxWithSubscript)
.value_or(4 * x_height / 5));
return parameters;
}
} // namespace
NGMathScriptsLayoutAlgorithm::NGMathScriptsLayoutAlgorithm(
const NGLayoutAlgorithmParams& params)
: NGLayoutAlgorithm(params) {
DCHECK(params.space.IsNewFormattingContext());
}
void NGMathScriptsLayoutAlgorithm::GatherChildren(
NGBlockNode* base,
Vector<SubSupPair>* sub_sup_pairs,
NGBlockNode* prescripts,
unsigned* first_prescript_index,
NGBoxFragmentBuilder* container_builder) const {
auto script_type = Node().ScriptType();
bool number_of_scripts_is_even = true;
sub_sup_pairs->resize(1);
for (NGLayoutInputNode child = Node().FirstChild(); child;
child = child.NextSibling()) {
NGBlockNode block_child = To<NGBlockNode>(child);
if (child.IsOutOfFlowPositioned()) {
if (container_builder) {
container_builder->AddOutOfFlowChildCandidate(
block_child, BorderScrollbarPadding().StartOffset());
}
continue;
}
if (!*base) {
// All scripted elements must have at least one child.
// The first child is the base.
*base = block_child;
continue;
}
switch (script_type) {
case MathScriptType::kSub:
case MathScriptType::kUnder:
// These elements must have exactly two children.
// The second child is a postscript and there are no prescripts.
// <msub> base subscript </msub>
// <msup> base superscript </msup>
DCHECK(!sub_sup_pairs->at(0).sub);
sub_sup_pairs->at(0).sub = block_child;
continue;
case MathScriptType::kSuper:
case MathScriptType::kOver:
DCHECK(!sub_sup_pairs->at(0).sup);
sub_sup_pairs->at(0).sup = block_child;
continue;
case MathScriptType::kUnderOver:
case MathScriptType::kSubSup:
// These elements must have exactly three children.
// The second and third children are postscripts and there are no
// prescripts. <msubsup> base subscript superscript </msubsup>
if (!sub_sup_pairs->at(0).sub) {
sub_sup_pairs->at(0).sub = block_child;
} else {
DCHECK(!sub_sup_pairs->at(0).sup);
sub_sup_pairs->at(0).sup = block_child;
}
continue;
case MathScriptType::kMultiscripts: {
// The structure of mmultiscripts is specified here:
// https://mathml-refresh.github.io/mathml-core/#prescripts-and-tensor-indices-mmultiscripts
if (IsPrescriptDelimiter(block_child)) {
if (!number_of_scripts_is_even || *first_prescript_index > 0) {
NOTREACHED();
return;
}
*first_prescript_index = sub_sup_pairs->size() - 1;
*prescripts = block_child;
continue;
}
if (!sub_sup_pairs->back().sub) {
sub_sup_pairs->back().sub = block_child;
} else {
DCHECK(!sub_sup_pairs->back().sup);
sub_sup_pairs->back().sup = block_child;
}
number_of_scripts_is_even = !number_of_scripts_is_even;
if (number_of_scripts_is_even)
sub_sup_pairs->resize(sub_sup_pairs->size() + 1);
continue;
}
default:
NOTREACHED();
}
}
DCHECK(number_of_scripts_is_even);
}
// Determines ascent/descent and shift metrics depending on script type.
NGMathScriptsLayoutAlgorithm::VerticalMetrics
NGMathScriptsLayoutAlgorithm::GetVerticalMetrics(
const ChildAndMetrics& base_metrics,
const ChildrenAndMetrics& sub_metrics,
const ChildrenAndMetrics& sup_metrics) const {
ScriptsVerticalParameters parameters = GetScriptsVerticalParameters(Style());
VerticalMetrics metrics;
MathScriptType type = Node().ScriptType();
if (type == MathScriptType::kSub || type == MathScriptType::kSubSup ||
type == MathScriptType::kMultiscripts || type == MathScriptType::kUnder ||
type == MathScriptType::kMultiscripts) {
metrics.sub_shift =
std::max(parameters.subscript_shift_down,
base_metrics.descent + parameters.subscript_baseline_drop_min);
}
LayoutUnit shift_up = parameters.superscript_shift_up;
if (type == MathScriptType::kSuper || type == MathScriptType::kSubSup ||
type == MathScriptType::kMultiscripts || type == MathScriptType::kOver ||
type == MathScriptType::kMultiscripts) {
if (Style().MathShift() == EMathShift::kCompact)
shift_up = parameters.superscript_shift_up_cramped;
metrics.sup_shift =
std::max(shift_up, base_metrics.ascent -
parameters.superscript_baseline_drop_max);
}
switch (type) {
case MathScriptType::kSub:
case MathScriptType::kUnder: {
metrics.descent = sub_metrics[0].descent;
metrics.sub_shift =
std::max(metrics.sub_shift,
sub_metrics[0].ascent - parameters.subscript_top_max);
} break;
case MathScriptType::kSuper:
case MathScriptType::kOver: {
metrics.ascent = sup_metrics[0].ascent;
metrics.sup_shift =
std::max(metrics.sup_shift,
parameters.superscript_bottom_min + sup_metrics[0].descent);
} break;
case MathScriptType::kMultiscripts:
case MathScriptType::kUnderOver:
case MathScriptType::kSubSup: {
for (wtf_size_t idx = 0; idx < sub_metrics.size(); ++idx) {
metrics.ascent = std::max(metrics.ascent, sup_metrics[idx].ascent);
metrics.descent = std::max(metrics.descent, sub_metrics[idx].descent);
LayoutUnit sub_script_shift = std::max(
parameters.subscript_shift_down,
base_metrics.descent + parameters.subscript_baseline_drop_min);
sub_script_shift =
std::max(sub_script_shift,
sub_metrics[idx].ascent - parameters.subscript_top_max);
LayoutUnit sup_script_shift =
std::max(shift_up, base_metrics.ascent -
parameters.superscript_baseline_drop_max);
sup_script_shift =
std::max(sup_script_shift, parameters.superscript_bottom_min +
sup_metrics[idx].descent);
LayoutUnit sub_super_script_gap =
(sub_script_shift - sub_metrics[idx].ascent) +
(sup_script_shift - sup_metrics[idx].descent);
if (sub_super_script_gap < parameters.sub_superscript_gap_min) {
// First, we try and push the superscript up.
LayoutUnit delta = parameters.superscript_bottom_max_with_subscript -
(sup_script_shift - sup_metrics[idx].descent);
if (delta > 0) {
delta = std::min(delta, parameters.sub_superscript_gap_min -
sub_super_script_gap);
sup_script_shift += delta;
sub_super_script_gap += delta;
}
// If that is not enough, we push the subscript down.
if (sub_super_script_gap < parameters.sub_superscript_gap_min) {
sub_script_shift +=
parameters.sub_superscript_gap_min - sub_super_script_gap;
}
}
metrics.sub_shift = std::max(metrics.sub_shift, sub_script_shift);
metrics.sup_shift = std::max(metrics.sup_shift, sup_script_shift);
}
} break;
}
return metrics;
}
NGMathScriptsLayoutAlgorithm::ChildAndMetrics
NGMathScriptsLayoutAlgorithm::LayoutAndGetMetrics(NGBlockNode child) const {
ChildAndMetrics child_and_metrics;
auto constraint_space = CreateConstraintSpaceForMathChild(
Node(), ChildAvailableSize(), ConstraintSpace(), child);
child_and_metrics.result =
child.Layout(constraint_space, nullptr /*break_token*/);
NGBoxFragment fragment(
ConstraintSpace().GetWritingDirection(),
To<NGPhysicalBoxFragment>(child_and_metrics.result->PhysicalFragment()));
child_and_metrics.inline_size = fragment.InlineSize();
child_and_metrics.margins =
ComputeMarginsFor(constraint_space, child.Style(), ConstraintSpace());
child_and_metrics.ascent = fragment.BaselineOrSynthesize();
child_and_metrics.descent = fragment.BlockSize() - child_and_metrics.ascent +
child_and_metrics.margins.block_end;
child_and_metrics.ascent += child_and_metrics.margins.block_start;
child_and_metrics.node = child;
return child_and_metrics;
}
scoped_refptr<const NGLayoutResult> NGMathScriptsLayoutAlgorithm::Layout() {
DCHECK(!BreakToken());
NGBlockNode base = nullptr;
NGBlockNode prescripts = nullptr;
Vector<SubSupPair> sub_sup_pairs;
wtf_size_t first_prescript_index = 0;
GatherChildren(&base, &sub_sup_pairs, &prescripts, &first_prescript_index,
&container_builder_);
ChildrenAndMetrics sub_metrics, sup_metrics;
if (prescripts)
LayoutAndGetMetrics(prescripts);
for (auto sub_sup_pair : sub_sup_pairs) {
if (sub_sup_pair.sub)
sub_metrics.emplace_back(LayoutAndGetMetrics(sub_sup_pair.sub));
if (sub_sup_pair.sup)
sup_metrics.emplace_back(LayoutAndGetMetrics(sub_sup_pair.sup));
}
ChildAndMetrics base_metrics = LayoutAndGetMetrics(base);
VerticalMetrics metrics =
GetVerticalMetrics(base_metrics, sub_metrics, sup_metrics);
const LogicalOffset content_start_offset =
BorderScrollbarPadding().StartOffset();
LayoutUnit ascent =
std::max(base_metrics.ascent, metrics.ascent + metrics.sup_shift) +
content_start_offset.block_offset;
LayoutUnit descent =
std::max(base_metrics.descent, metrics.descent + metrics.sub_shift);
LayoutUnit base_italic_correction = std::min(
base_metrics.inline_size, base_metrics.result->MathItalicCorrection());
LayoutUnit inline_offset = content_start_offset.inline_offset;
LayoutUnit space = GetSpaceAfterScript(Style());
// Position pre scripts if needed.
if (prescripts) {
for (wtf_size_t idx = first_prescript_index; idx < sub_metrics.size();
++idx) {
auto& sub_metric = sub_metrics[idx];
auto& sup_metric = sup_metrics[idx];
LayoutUnit sub_sup_pair_inline_size =
std::max(sub_metric.inline_size, sup_metric.inline_size);
inline_offset += space + sub_sup_pair_inline_size;
LogicalOffset sub_offset(inline_offset - sub_metric.inline_size +
sub_metric.margins.inline_start,
ascent + metrics.sub_shift - sub_metric.ascent +
sub_metric.margins.block_start);
container_builder_.AddChild(sub_metric.result->PhysicalFragment(),
sub_offset);
sub_metric.node.StoreMargins(ConstraintSpace(), sub_metric.margins);
LogicalOffset sup_offset(inline_offset - sup_metric.inline_size +
sup_metric.margins.inline_start,
ascent - metrics.sup_shift - sup_metric.ascent +
sup_metric.margins.block_start);
container_builder_.AddChild(sup_metric.result->PhysicalFragment(),
sup_offset);
sup_metric.node.StoreMargins(ConstraintSpace(), sup_metric.margins);
}
} else {
first_prescript_index = std::max(sub_metrics.size(), sup_metrics.size());
}
inline_offset += base_metrics.margins.inline_start;
LogicalOffset base_offset(
inline_offset,
ascent - base_metrics.ascent + base_metrics.margins.block_start);
container_builder_.AddChild(base_metrics.result->PhysicalFragment(),
base_offset);
base.StoreMargins(ConstraintSpace(), base_metrics.margins);
inline_offset += base_metrics.inline_size + base_metrics.margins.inline_end;
// Position post scripts if needed.
for (unsigned idx = 0; idx < first_prescript_index; ++idx) {
ChildAndMetrics sub_metric, sup_metric;
if (idx < sub_metrics.size())
sub_metric = sub_metrics[idx];
if (idx < sup_metrics.size())
sup_metric = sup_metrics[idx];
if (sub_metric.node) {
LogicalOffset sub_offset(
LayoutUnit(inline_offset + sub_metric.margins.inline_start -
base_italic_correction)
.ClampNegativeToZero(),
ascent + metrics.sub_shift - sub_metric.ascent +
sub_metric.margins.block_start);
container_builder_.AddChild(sub_metric.result->PhysicalFragment(),
sub_offset);
sub_metric.node.StoreMargins(ConstraintSpace(), sub_metric.margins);
}
if (sup_metric.node) {
LogicalOffset sup_offset(inline_offset + sup_metric.margins.inline_start,
ascent - metrics.sup_shift - sup_metric.ascent +
sup_metric.margins.block_start);
container_builder_.AddChild(sup_metric.result->PhysicalFragment(),
sup_offset);
sup_metric.node.StoreMargins(ConstraintSpace(), sup_metric.margins);
}
LayoutUnit sub_sup_pair_inline_size =
std::max(sub_metric.inline_size, sup_metric.inline_size);
inline_offset += space + sub_sup_pair_inline_size;
}
container_builder_.SetBaseline(ascent);
LayoutUnit intrinsic_block_size =
ascent + descent + BorderScrollbarPadding().block_end;
LayoutUnit block_size = ComputeBlockSizeForFragment(
ConstraintSpace(), Style(), BorderPadding(), intrinsic_block_size,
container_builder_.InitialBorderBoxSize().inline_size);
container_builder_.SetIntrinsicBlockSize(intrinsic_block_size);
container_builder_.SetFragmentsTotalBlockSize(block_size);
NGOutOfFlowLayoutPart(Node(), ConstraintSpace(), &container_builder_).Run();
return container_builder_.ToBoxFragment();
}
MinMaxSizesResult NGMathScriptsLayoutAlgorithm::ComputeMinMaxSizes(
const MinMaxSizesInput& child_input) const {
if (auto result = CalculateMinMaxSizesIgnoringChildren(
Node(), BorderScrollbarPadding()))
return *result;
NGBlockNode base = nullptr;
NGBlockNode prescripts = nullptr;
Vector<SubSupPair> sub_sup_pairs;
unsigned first_prescript_index = 0;
GatherChildren(&base, &sub_sup_pairs, &prescripts, &first_prescript_index);
DCHECK_GE(sub_sup_pairs.size(), 1ul);
MinMaxSizes sizes;
bool depends_on_percentage_block_size = false;
ChildAndMetrics base_metrics = LayoutAndGetMetrics(base);
LayoutUnit base_italic_correction = std::min(
base_metrics.inline_size, base_metrics.result->MathItalicCorrection());
MinMaxSizesResult base_result =
ComputeMinAndMaxContentContribution(Style(), base, child_input);
base_result.sizes += ComputeMinMaxMargins(Style(), base).InlineSum();
sizes = base_result.sizes;
depends_on_percentage_block_size |=
base_result.depends_on_percentage_block_size;
LayoutUnit space = GetSpaceAfterScript(Style());
switch (Node().ScriptType()) {
case MathScriptType::kSub:
case MathScriptType::kUnder:
case MathScriptType::kOver:
case MathScriptType::kSuper: {
NGBlockNode sub = sub_sup_pairs[0].sub;
NGBlockNode sup = sub_sup_pairs[0].sup;
auto first_post_script = sub ? sub : sup;
auto first_post_script_result = ComputeMinAndMaxContentContribution(
Style(), first_post_script, child_input);
first_post_script_result.sizes +=
ComputeMinMaxMargins(Style(), first_post_script).InlineSum();
sizes += first_post_script_result.sizes;
if (sub)
sizes -= base_italic_correction;
sizes += space;
depends_on_percentage_block_size |=
first_post_script_result.depends_on_percentage_block_size;
break;
}
case MathScriptType::kSubSup:
case MathScriptType::kUnderOver:
case MathScriptType::kMultiscripts: {
MinMaxSizes sub_sup_pair_size;
unsigned index = 0;
do {
auto sub = sub_sup_pairs[index].sub;
if (!sub)
continue;
auto sub_result =
ComputeMinAndMaxContentContribution(Style(), sub, child_input);
sub_result.sizes += ComputeMinMaxMargins(Style(), sub).InlineSum();
sub_result.sizes -= base_italic_correction;
sub_sup_pair_size.Encompass(sub_result.sizes);
auto sup = sub_sup_pairs[index].sup;
if (!sup)
continue;
auto sup_result =
ComputeMinAndMaxContentContribution(Style(), sup, child_input);
sup_result.sizes += ComputeMinMaxMargins(Style(), sup).InlineSum();
sub_sup_pair_size.Encompass(sup_result.sizes);
sizes += sub_sup_pair_size;
sizes += space;
depends_on_percentage_block_size |=
sub_result.depends_on_percentage_block_size;
depends_on_percentage_block_size |=
sup_result.depends_on_percentage_block_size;
} while (++index < sub_sup_pairs.size());
break;
}
}
sizes += BorderScrollbarPadding().InlineSum();
return {sizes, depends_on_percentage_block_size};
}
} // namespace blink