Files
firefox/layout/mathml/nsMathMLmfracFrame.cpp
T
Frédéric Wang ad6375be7c Bug 1908069 - Add border/padding/margin support to <mfrac>. r=emilio
This is the first patch of the series that actually take
border/padding/margin into account for MathML layout. One subtility is
that Gecko distinguishes the ink nsBoundingMetrics box and the
ReflowOutput box for accurate positioning of math components contrary
to the current version of MathML Core [1].

For MathML Core and the WPT tests, it is assumed that border/padding are
used to inflate the content box [2] [3] and it does not really matter
whether we inflate the ink bounding box, or simply adjust its offsets
inside the normal bounding box. We choose the former and introduce a
helper function InflateReflowAndBoundingMetrics for that purpose, which
will be reused in follow-up patches. After inflating these boxes, the
offsets of children and painted items must also be adjusted by the
left/top border+padding.

Regarding margins, MathML Core says that we should use "margin boxes"
and WPT tests simply check that the layout of MathML constructions
containing mspace elements with margins is not changed if we move
these margins inside the mspace's width/height/depth attributes. To
pass these tests, we similarly need to inflate both the ink and
non-ink bounding boxes by the margin.

However, the margins can be negative possibly leading to negative
dimensions of "margin boxes". To make the code more generic, we
simplify adjust all usages of the children's nsBoundingMetrics and
ReflowOutput to include these margins. When calling FinishReflowChild
at the end, the offsets of children must be adjusted by these margins.

This patch also reimplements the extra one-pixel padding around a
fraction using rules from MathML Core's UA stylesheet at
https://w3c.github.io/mathml-core/#user-agent-stylesheet

[1] https://github.com/w3c/mathml-core/issues/78
[2] https://github.com/web-platform-tests/wpt/blob/27c8d234ee83a4b15f36ad10cc26c951b1782fb6/mathml/relations/css-styling/padding-border-margin/border-002.html#L25
[4] https://github.com/web-platform-tests/wpt/blob/27c8d234ee83a4b15f36ad10cc26c951b1782fb6/mathml/relations/css-styling/padding-border-margin/padding-002.html#L26
[3] https://github.com/web-platform-tests/wpt/blob/27c8d234ee83a4b15f36ad10cc26c951b1782fb6/mathml/relations/css-styling/padding-border-margin/margin-003.html#L50

Differential Revision: https://phabricator.services.mozilla.com/D216670
2024-08-07 06:00:44 +00:00

400 lines
15 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 "nsMathMLmfracFrame.h"
#include "gfxUtils.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/PresShell.h"
#include "mozilla/RefPtr.h"
#include "mozilla/StaticPrefs_mathml.h"
#include "nsLayoutUtils.h"
#include "nsPresContext.h"
#include "nsDisplayList.h"
#include "gfxContext.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/MathMLElement.h"
#include <algorithm>
#include "gfxMathTable.h"
#include "gfxTextRun.h"
using namespace mozilla;
using namespace mozilla::gfx;
//
// <mfrac> -- form a fraction from two subexpressions - implementation
//
nsIFrame* NS_NewMathMLmfracFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
return new (aPresShell)
nsMathMLmfracFrame(aStyle, aPresShell->GetPresContext());
}
NS_IMPL_FRAMEARENA_HELPERS(nsMathMLmfracFrame)
nsMathMLmfracFrame::~nsMathMLmfracFrame() = default;
eMathMLFrameType nsMathMLmfracFrame::GetMathMLFrameType() {
// frac is "inner" in TeXBook, Appendix G, rule 15e. See also page 170.
return eMathMLFrameType_Inner;
}
uint8_t nsMathMLmfracFrame::ScriptIncrement(nsIFrame* aFrame) {
if (StyleFont()->mMathStyle == StyleMathStyle::Compact && aFrame &&
(mFrames.FirstChild() == aFrame || mFrames.LastChild() == aFrame)) {
return 1;
}
return 0;
}
NS_IMETHODIMP
nsMathMLmfracFrame::TransmitAutomaticData() {
// The TeXbook (Ch 17. p.141) says the numerator inherits the compression
// while the denominator is compressed
UpdatePresentationDataFromChildAt(1, 1, NS_MATHML_COMPRESSED,
NS_MATHML_COMPRESSED);
// If displaystyle is false, then scriptlevel is incremented, so notify the
// children of this.
if (StyleFont()->mMathStyle == StyleMathStyle::Compact) {
PropagateFrameFlagFor(mFrames.FirstChild(),
NS_FRAME_MATHML_SCRIPT_DESCENDANT);
PropagateFrameFlagFor(mFrames.LastChild(),
NS_FRAME_MATHML_SCRIPT_DESCENDANT);
}
// if our numerator is an embellished operator, let its state bubble to us
GetEmbellishDataFrom(mFrames.FirstChild(), mEmbellishData);
if (NS_MATHML_IS_EMBELLISH_OPERATOR(mEmbellishData.flags)) {
// even when embellished, we need to record that <mfrac> won't fire
// Stretch() on its embellished child
mEmbellishData.direction = NS_STRETCH_DIRECTION_UNSUPPORTED;
}
return NS_OK;
}
nscoord nsMathMLmfracFrame::CalcLineThickness(nsPresContext* aPresContext,
ComputedStyle* aComputedStyle,
nsString& aThicknessAttribute,
nscoord onePixel,
nscoord aDefaultRuleThickness,
float aFontSizeInflation) {
nscoord defaultThickness = aDefaultRuleThickness;
nscoord lineThickness = aDefaultRuleThickness;
nscoord minimumThickness = onePixel;
// linethickness
// https://w3c.github.io/mathml-core/#dfn-linethickness
if (!aThicknessAttribute.IsEmpty()) {
lineThickness = defaultThickness;
ParseNumericValue(aThicknessAttribute, &lineThickness,
dom::MathMLElement::PARSE_ALLOW_NEGATIVE, aPresContext,
aComputedStyle, aFontSizeInflation);
// MathML Core says a negative value is interpreted as 0.
if (lineThickness < 0) {
lineThickness = 0;
}
}
// use minimum if the lineThickness is a non-zero value less than minimun
if (lineThickness && lineThickness < minimumThickness)
lineThickness = minimumThickness;
return lineThickness;
}
void nsMathMLmfracFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
const nsDisplayListSet& aLists) {
/////////////
// paint the numerator and denominator
nsMathMLContainerFrame::BuildDisplayList(aBuilder, aLists);
/////////////
// paint the fraction line
DisplayBar(aBuilder, this, mLineRect, aLists);
}
nsresult nsMathMLmfracFrame::AttributeChanged(int32_t aNameSpaceID,
nsAtom* aAttribute,
int32_t aModType) {
if (nsGkAtoms::linethickness_ == aAttribute) {
InvalidateFrame();
}
return nsMathMLContainerFrame::AttributeChanged(aNameSpaceID, aAttribute,
aModType);
}
/* virtual */
nsresult nsMathMLmfracFrame::MeasureForWidth(DrawTarget* aDrawTarget,
ReflowOutput& aDesiredSize) {
PlaceFlags flags(PlaceFlag::IntrinsicSize, PlaceFlag::MeasureOnly);
return PlaceInternal(aDrawTarget, flags, aDesiredSize);
}
nscoord nsMathMLmfracFrame::FixInterFrameSpacing(ReflowOutput& aDesiredSize) {
nscoord gap = nsMathMLContainerFrame::FixInterFrameSpacing(aDesiredSize);
if (!gap) return 0;
mLineRect.MoveBy(gap, 0);
return gap;
}
/* virtual */
nsresult nsMathMLmfracFrame::Place(DrawTarget* aDrawTarget,
const PlaceFlags& aFlags,
ReflowOutput& aDesiredSize) {
return PlaceInternal(aDrawTarget, aFlags, aDesiredSize);
}
nsresult nsMathMLmfracFrame::PlaceInternal(DrawTarget* aDrawTarget,
const PlaceFlags& aFlags,
ReflowOutput& aDesiredSize) {
////////////////////////////////////
// Get the children's desired sizes
nsBoundingMetrics bmNum, bmDen;
ReflowOutput sizeNum(aDesiredSize.GetWritingMode());
ReflowOutput sizeDen(aDesiredSize.GetWritingMode());
nsIFrame* frameDen = nullptr;
nsIFrame* frameNum = mFrames.FirstChild();
if (frameNum) frameDen = frameNum->GetNextSibling();
if (!frameNum || !frameDen || frameDen->GetNextSibling()) {
// report an error, encourage people to get their markups in order
if (!aFlags.contains(PlaceFlag::MeasureOnly)) {
ReportChildCountError();
}
return PlaceAsMrow(aDrawTarget, aFlags, aDesiredSize);
}
GetReflowAndBoundingMetricsFor(frameNum, sizeNum, bmNum);
GetReflowAndBoundingMetricsFor(frameDen, sizeDen, bmDen);
nsMargin numMargin = GetMarginForPlace(aFlags, frameNum),
denMargin = GetMarginForPlace(aFlags, frameDen);
nsPresContext* presContext = PresContext();
nscoord onePixel = nsPresContext::CSSPixelsToAppUnits(1);
float fontSizeInflation = nsLayoutUtils::FontSizeInflationFor(this);
RefPtr<nsFontMetrics> fm =
nsLayoutUtils::GetFontMetricsForFrame(this, fontSizeInflation);
nscoord defaultRuleThickness, axisHeight;
nscoord oneDevPixel = fm->AppUnitsPerDevPixel();
RefPtr<gfxFont> mathFont = fm->GetThebesFontGroup()->GetFirstMathFont();
if (mathFont) {
defaultRuleThickness = mathFont->MathTable()->Constant(
gfxMathTable::FractionRuleThickness, oneDevPixel);
} else {
GetRuleThickness(aDrawTarget, fm, defaultRuleThickness);
}
GetAxisHeight(aDrawTarget, fm, axisHeight);
bool outermostEmbellished = false;
if (mEmbellishData.coreFrame) {
nsEmbellishData parentData;
GetEmbellishDataFrom(GetParent(), parentData);
outermostEmbellished = parentData.coreFrame != mEmbellishData.coreFrame;
}
// see if the linethickness attribute is there
nsAutoString value;
mContent->AsElement()->GetAttr(nsGkAtoms::linethickness_, value);
mLineThickness =
CalcLineThickness(presContext, mComputedStyle, value, onePixel,
defaultRuleThickness, fontSizeInflation);
bool displayStyle = StyleFont()->mMathStyle == StyleMathStyle::Normal;
mLineRect.height = mLineThickness;
// Add lspace & rspace that may come from <mo> if we are an outermost
// embellished container (we fetch values from the core since they may use
// units that depend on style data, and style changes could have occurred
// in the core since our last visit there)
nscoord leftSpace = 0;
nscoord rightSpace = 0;
if (outermostEmbellished) {
const bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
nsEmbellishData coreData;
GetEmbellishDataFrom(mEmbellishData.coreFrame, coreData);
leftSpace += isRTL ? coreData.trailingSpace : coreData.leadingSpace;
rightSpace += isRTL ? coreData.leadingSpace : coreData.trailingSpace;
}
nscoord actualRuleThickness = mLineThickness;
//////////////////
// Get shifts
nscoord numShift = 0;
nscoord denShift = 0;
// Rule 15b, App. G, TeXbook
nscoord numShift1, numShift2, numShift3;
nscoord denShift1, denShift2;
GetNumeratorShifts(fm, numShift1, numShift2, numShift3);
GetDenominatorShifts(fm, denShift1, denShift2);
if (0 == actualRuleThickness) {
numShift = displayStyle ? numShift1 : numShift3;
denShift = displayStyle ? denShift1 : denShift2;
if (mathFont) {
numShift = mathFont->MathTable()->Constant(
displayStyle ? gfxMathTable::StackTopDisplayStyleShiftUp
: gfxMathTable::StackTopShiftUp,
oneDevPixel);
denShift = mathFont->MathTable()->Constant(
displayStyle ? gfxMathTable::StackBottomDisplayStyleShiftDown
: gfxMathTable::StackBottomShiftDown,
oneDevPixel);
}
} else {
numShift = displayStyle ? numShift1 : numShift2;
denShift = displayStyle ? denShift1 : denShift2;
if (mathFont) {
numShift = mathFont->MathTable()->Constant(
displayStyle ? gfxMathTable::FractionNumeratorDisplayStyleShiftUp
: gfxMathTable::FractionNumeratorShiftUp,
oneDevPixel);
denShift = mathFont->MathTable()->Constant(
displayStyle ? gfxMathTable::FractionDenominatorDisplayStyleShiftDown
: gfxMathTable::FractionDenominatorShiftDown,
oneDevPixel);
}
}
if (0 == actualRuleThickness) {
// Rule 15c, App. G, TeXbook
// min clearance between numerator and denominator
nscoord minClearance =
displayStyle ? 7 * defaultRuleThickness : 3 * defaultRuleThickness;
if (mathFont) {
minClearance = mathFont->MathTable()->Constant(
displayStyle ? gfxMathTable::StackDisplayStyleGapMin
: gfxMathTable::StackGapMin,
oneDevPixel);
}
nscoord actualClearance = (numShift - bmNum.descent - numMargin.bottom) -
(bmDen.ascent + denMargin.top - denShift);
// actualClearance should be >= minClearance
if (actualClearance < minClearance) {
nscoord halfGap = (minClearance - actualClearance) / 2;
numShift += halfGap;
denShift += halfGap;
}
} else {
// Rule 15d, App. G, TeXbook
// min clearance between numerator or denominator and middle of bar
// TeX has a different interpretation of the thickness.
// Try $a \above10pt b$ to see. Here is what TeX does:
// minClearance = displayStyle ?
// 3 * actualRuleThickness : actualRuleThickness;
// we slightly depart from TeX here. We use the defaultRuleThickness
// instead of the value coming from the linethickness attribute, i.e., we
// recover what TeX does if the user hasn't set linethickness. But when
// the linethickness is set, we avoid the wide gap problem.
nscoord minClearanceNum = displayStyle ? 3 * defaultRuleThickness
: defaultRuleThickness + onePixel;
nscoord minClearanceDen = minClearanceNum;
if (mathFont) {
minClearanceNum = mathFont->MathTable()->Constant(
displayStyle ? gfxMathTable::FractionNumDisplayStyleGapMin
: gfxMathTable::FractionNumeratorGapMin,
oneDevPixel);
minClearanceDen = mathFont->MathTable()->Constant(
displayStyle ? gfxMathTable::FractionDenomDisplayStyleGapMin
: gfxMathTable::FractionDenominatorGapMin,
oneDevPixel);
}
// adjust numShift to maintain minClearanceNum if needed
nscoord actualClearanceNum = (numShift - bmNum.descent - numMargin.bottom) -
(axisHeight + actualRuleThickness / 2);
if (actualClearanceNum < minClearanceNum) {
numShift += (minClearanceNum - actualClearanceNum);
}
// adjust denShift to maintain minClearanceDen if needed
nscoord actualClearanceDen = (axisHeight - actualRuleThickness / 2) -
(bmDen.ascent + denMargin.top - denShift);
if (actualClearanceDen < minClearanceDen) {
denShift += (minClearanceDen - actualClearanceDen);
}
}
//////////////////
// Place Children
// XXX Need revisiting the width. TeX uses the exact width
// e.g. in $$\huge\frac{\displaystyle\int}{i}$$
nscoord width = std::max(bmNum.width + numMargin.LeftRight(),
bmDen.width + denMargin.LeftRight());
nscoord dxNum =
leftSpace + (width - sizeNum.Width() - numMargin.LeftRight()) / 2;
nscoord dxDen =
leftSpace + (width - sizeDen.Width() - denMargin.LeftRight()) / 2;
width += leftSpace + rightSpace;
mBoundingMetrics.rightBearing =
std::max(dxNum + bmNum.rightBearing + numMargin.LeftRight(),
dxDen + bmDen.rightBearing + denMargin.LeftRight());
if (mBoundingMetrics.rightBearing < width - rightSpace)
mBoundingMetrics.rightBearing = width - rightSpace;
mBoundingMetrics.leftBearing =
std::min(dxNum + bmNum.leftBearing, dxDen + bmDen.leftBearing);
if (mBoundingMetrics.leftBearing > leftSpace)
mBoundingMetrics.leftBearing = leftSpace;
mBoundingMetrics.ascent = bmNum.ascent + numShift + numMargin.top;
mBoundingMetrics.descent = bmDen.descent + denShift + denMargin.bottom;
mBoundingMetrics.width = width;
aDesiredSize.SetBlockStartAscent(numMargin.top + sizeNum.BlockStartAscent() +
numShift);
aDesiredSize.Height() = aDesiredSize.BlockStartAscent() + sizeDen.Height() +
denMargin.bottom - sizeDen.BlockStartAscent() +
denShift;
aDesiredSize.Width() = mBoundingMetrics.width;
aDesiredSize.mBoundingMetrics = mBoundingMetrics;
// Add padding+border.
auto borderPadding = GetBorderPaddingForPlace(aFlags);
InflateReflowAndBoundingMetrics(borderPadding, aDesiredSize,
mBoundingMetrics);
leftSpace += borderPadding.left;
rightSpace += borderPadding.right;
width += borderPadding.LeftRight();
dxNum += borderPadding.left;
dxDen += borderPadding.left;
mReference.x = 0;
mReference.y = aDesiredSize.BlockStartAscent();
if (!aFlags.contains(PlaceFlag::MeasureOnly)) {
nscoord dy;
// place numerator
dxNum += numMargin.left;
dy = borderPadding.top + numMargin.top;
FinishReflowChild(frameNum, presContext, sizeNum, nullptr, dxNum, dy,
ReflowChildFlags::Default);
// place denominator
dxDen += denMargin.left;
dy = aDesiredSize.Height() - sizeDen.Height() - denMargin.bottom -
borderPadding.bottom;
FinishReflowChild(frameDen, presContext, sizeDen, nullptr, dxDen, dy,
ReflowChildFlags::Default);
// place the fraction bar - dy is top of bar
dy = aDesiredSize.BlockStartAscent() -
(axisHeight + actualRuleThickness / 2);
mLineRect.SetRect(leftSpace, dy, width - (leftSpace + rightSpace),
actualRuleThickness);
}
return NS_OK;
}