gecko-dev/layout/mathml/nsMathMLTokenFrame.cpp
Frédéric Wang 656a36187f Bug 1916988 - Support CSS width/height properties on MathML elements. r=emilio
This patch implements support for the width/height properties on
MathML elements [1]. The general algorithm from the spec is as
follows:

(1) The outcome of the math layout is a "math content box".
(2) The content box sets its size from computed width/height values. If
  auto, it's the one of the "math content box". This patch ignores
  percentage values for now [2] [3].
(3) math content box is shifted so that its inline-start and top edges
  aligns with the ones of the content box. There are exceptions
  elements like mfrac and munder/mover/munderover which instead
  horizontally center the math content box within the content box.
  For baseline adjustment, we follow what Chromium does, see [4].
(4) Padding+border are added around the content box. Note that we
  ignore the box-sizing property for now [5].

The patch essentially tweaks the various MathML layout algorithms to
perform steps (3) and (4) before the calls to
GetBorderPaddingForPlace and InflateReflowAndBoundingMetrics.

[1] https://w3c.github.io/mathml-core/#layout-algorithms
[2] https://github.com/w3c/mathml-core/issues/76
[3] https://github.com/w3c/mathml-core/issues/77
[4] https://github.com/w3c/mathml-core/issues/259
[5] https://github.com/w3c/mathml-core/issues/257

Below is more information about test coverage:

- width-height-001: Verify that width, height, inline-size and block-size
  properties sets the size of the content box. This test used to verify
  they are ignored, this patch fixes the `<meta name="assert">` tag.
  It also adds a test for the case the specified size is smaller than the
  content (we force non empty descendants to make sure this content is
  large enough) and to verify the width is used for the preferred width.

- width-height-002, width-height-003: These are reftests visually checking
  offsets of the math content box within a larger content box (specified
  by width/height) for the mtext, mrow, mpadded, mfrac, msqrt, mroot,
  in LTR/RTL modes. In particular they allow to verify some painted
  elements like fraction bar and radical symbols.

- width-height-004: This test more directly checks that the math content
  box is horizontally centered within a larger content box for munder,
  mover, munderover and mfrac. This patch extends the test to cover the
  case when the math content box is wider (i.e. overflowing outside the
  content box) and removes unnecessary specified height.

- width-height-005: New test for other layout algorithm that don't
  center the math content box, checking inline-start edges of children
  when a width is specified. We check both LTR/RTL modes and
  wider/narrower content boxes.

- width-height-006: Same but checking the top edges for larger/smaller
  height and verifying that baseline is perserved.

Differential Revision: https://phabricator.services.mozilla.com/D221436
2024-09-30 12:18:39 +00:00

215 lines
8.2 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsMathMLTokenFrame.h"
#include "mozilla/PresShell.h"
#include "nsLayoutUtils.h"
#include "nsPresContext.h"
#include "nsContentUtils.h"
#include "nsTextFrame.h"
#include "gfxContext.h"
#include <algorithm>
using namespace mozilla;
nsIFrame* NS_NewMathMLTokenFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
return new (aPresShell)
nsMathMLTokenFrame(aStyle, aPresShell->GetPresContext());
}
NS_IMPL_FRAMEARENA_HELPERS(nsMathMLTokenFrame)
nsMathMLTokenFrame::~nsMathMLTokenFrame() = default;
NS_IMETHODIMP
nsMathMLTokenFrame::InheritAutomaticData(nsIFrame* aParent) {
// let the base class get the default from our parent
nsMathMLContainerFrame::InheritAutomaticData(aParent);
return NS_OK;
}
eMathMLFrameType nsMathMLTokenFrame::GetMathMLFrameType() {
// treat everything other than <mi> as ordinary...
if (!mContent->IsMathMLElement(nsGkAtoms::mi_)) {
return eMathMLFrameType_Ordinary;
}
StyleMathVariant mathVariant = StyleFont()->mMathVariant;
if ((mathVariant == StyleMathVariant::None &&
(StyleFont()->mFont.style.IsItalic() ||
HasAnyStateBits(NS_FRAME_IS_IN_SINGLE_CHAR_MI))) ||
mathVariant == StyleMathVariant::Italic ||
mathVariant == StyleMathVariant::BoldItalic ||
mathVariant == StyleMathVariant::SansSerifItalic ||
mathVariant == StyleMathVariant::SansSerifBoldItalic) {
return eMathMLFrameType_ItalicIdentifier;
}
return eMathMLFrameType_UprightIdentifier;
}
void nsMathMLTokenFrame::MarkTextFramesAsTokenMathML() {
nsIFrame* child = nullptr;
uint32_t childCount = 0;
// Set flags on child text frames
// - to force them to trim their leading and trailing whitespaces.
// - Indicate which frames are suitable for mathvariant
// - flag single character <mi> frames for special italic treatment
for (nsIFrame* childFrame = PrincipalChildList().FirstChild(); childFrame;
childFrame = childFrame->GetNextSibling()) {
for (nsIFrame* childFrame2 = childFrame->PrincipalChildList().FirstChild();
childFrame2; childFrame2 = childFrame2->GetNextSibling()) {
if (childFrame2->IsTextFrame()) {
childFrame2->AddStateBits(TEXT_IS_IN_TOKEN_MATHML);
child = childFrame2;
childCount++;
}
}
}
if (mContent->IsMathMLElement(nsGkAtoms::mi_) && childCount == 1) {
nsAutoString data;
nsContentUtils::GetNodeTextContent(mContent, false, data);
data.CompressWhitespace();
int32_t length = data.Length();
bool isSingleCharacter =
length == 1 || (length == 2 && NS_IS_HIGH_SURROGATE(data[0]));
if (isSingleCharacter) {
child->AddStateBits(NS_FRAME_IS_IN_SINGLE_CHAR_MI);
AddStateBits(NS_FRAME_IS_IN_SINGLE_CHAR_MI);
}
}
}
void nsMathMLTokenFrame::SetInitialChildList(ChildListID aListID,
nsFrameList&& aChildList) {
// First, let the base class do its work
nsMathMLContainerFrame::SetInitialChildList(aListID, std::move(aChildList));
MarkTextFramesAsTokenMathML();
}
void nsMathMLTokenFrame::AppendFrames(ChildListID aListID,
nsFrameList&& aChildList) {
nsMathMLContainerFrame::AppendFrames(aListID, std::move(aChildList));
MarkTextFramesAsTokenMathML();
}
void nsMathMLTokenFrame::InsertFrames(
ChildListID aListID, nsIFrame* aPrevFrame,
const nsLineList::iterator* aPrevFrameLine, nsFrameList&& aChildList) {
nsMathMLContainerFrame::InsertFrames(aListID, aPrevFrame, aPrevFrameLine,
std::move(aChildList));
MarkTextFramesAsTokenMathML();
}
void nsMathMLTokenFrame::Reflow(nsPresContext* aPresContext,
ReflowOutput& aDesiredSize,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
MarkInReflow();
MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
// initializations needed for empty markup like <mtag></mtag>
aDesiredSize.ClearSize();
aDesiredSize.SetBlockStartAscent(0);
aDesiredSize.mBoundingMetrics = nsBoundingMetrics();
for (nsIFrame* childFrame : PrincipalChildList()) {
// ask our children to compute their bounding metrics
ReflowOutput childDesiredSize(aReflowInput.GetWritingMode());
WritingMode wm = childFrame->GetWritingMode();
LogicalSize availSize = aReflowInput.ComputedSize(wm);
availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
ReflowInput childReflowInput(aPresContext, aReflowInput, childFrame,
availSize);
nsReflowStatus childStatus;
ReflowChild(childFrame, aPresContext, childDesiredSize, childReflowInput,
childStatus);
NS_ASSERTION(childStatus.IsComplete(),
"We gave the child unconstrained available block-size, so its "
"status should be complete!");
SaveReflowAndBoundingMetricsFor(childFrame, childDesiredSize,
childDesiredSize.mBoundingMetrics);
}
// place and size children
FinalizeReflow(aReflowInput.mRenderingContext->GetDrawTarget(), aDesiredSize);
aStatus.Reset(); // This type of frame can't be split.
}
// For token elements, mBoundingMetrics is computed at the ReflowToken
// pass, it is not computed here because our children may be text frames
// that do not implement the GetBoundingMetrics() interface.
/* virtual */
nsresult nsMathMLTokenFrame::Place(DrawTarget* aDrawTarget,
const PlaceFlags& aFlags,
ReflowOutput& aDesiredSize) {
mBoundingMetrics = nsBoundingMetrics();
for (nsIFrame* childFrame : PrincipalChildList()) {
ReflowOutput childSize(aDesiredSize.GetWritingMode());
nsBoundingMetrics bmChild;
GetReflowAndBoundingMetricsFor(childFrame, childSize, bmChild, nullptr);
auto childMargin = GetMarginForPlace(aFlags, childFrame);
bmChild.ascent += childMargin.top;
bmChild.descent += childMargin.bottom;
bmChild.rightBearing += childMargin.LeftRight();
bmChild.width += childMargin.LeftRight();
// compute and cache the bounding metrics
mBoundingMetrics += bmChild;
}
RefPtr<nsFontMetrics> fm =
nsLayoutUtils::GetInflatedFontMetricsForFrame(this);
nscoord ascent = fm->MaxAscent();
nscoord descent = fm->MaxDescent();
aDesiredSize.mBoundingMetrics = mBoundingMetrics;
aDesiredSize.Width() = mBoundingMetrics.width;
aDesiredSize.SetBlockStartAscent(std::max(mBoundingMetrics.ascent, ascent));
aDesiredSize.Height() = aDesiredSize.BlockStartAscent() +
std::max(mBoundingMetrics.descent, descent);
// Apply width/height to math content box.
auto sizes = GetWidthAndHeightForPlaceAdjustment(aFlags);
auto shiftX = ApplyAdjustmentForWidthAndHeight(aFlags, sizes, aDesiredSize,
mBoundingMetrics);
// Add padding+border.
auto borderPadding = GetBorderPaddingForPlace(aFlags);
InflateReflowAndBoundingMetrics(borderPadding, aDesiredSize,
mBoundingMetrics);
if (!aFlags.contains(PlaceFlag::MeasureOnly)) {
nscoord dx = borderPadding.left;
dx += shiftX;
for (nsIFrame* childFrame : PrincipalChildList()) {
ReflowOutput childSize(aDesiredSize.GetWritingMode());
GetReflowAndBoundingMetricsFor(childFrame, childSize,
childSize.mBoundingMetrics);
auto childMargin = GetMarginForPlace(aFlags, childFrame);
// place and size the child; (dx,0) makes the caret happy - bug 188146
nscoord dy = childSize.Height() == 0
? 0
: aDesiredSize.BlockStartAscent() -
childSize.BlockStartAscent() + childMargin.top;
FinishReflowChild(childFrame, PresContext(), childSize, nullptr, dx, dy,
ReflowChildFlags::Default);
dx += childSize.Width();
}
}
SetReference(nsPoint(0, aDesiredSize.BlockStartAscent()));
return NS_OK;
}