mirror of
https://github.com/HeyPuter/firefox.git
synced 2026-11-13 20:14:57 -10:00
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
400 lines
15 KiB
C++
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;
|
|
}
|