/* -*- 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 "nsMathMLmpaddedFrame.h" #include "mozilla/dom/MathMLElement.h" #include "mozilla/gfx/2D.h" #include "mozilla/PresShell.h" #include "mozilla/TextUtils.h" #include "nsLayoutUtils.h" #include using namespace mozilla; // // -- adjust space around content - implementation // nsIFrame* NS_NewMathMLmpaddedFrame(PresShell* aPresShell, ComputedStyle* aStyle) { return new (aPresShell) nsMathMLmpaddedFrame(aStyle, aPresShell->GetPresContext()); } NS_IMPL_FRAMEARENA_HELPERS(nsMathMLmpaddedFrame) nsMathMLmpaddedFrame::~nsMathMLmpaddedFrame() = default; NS_IMETHODIMP nsMathMLmpaddedFrame::InheritAutomaticData(nsIFrame* aParent) { // let the base class get the default from our parent nsMathMLContainerFrame::InheritAutomaticData(aParent); mPresentationData.flags |= NS_MATHML_STRETCH_ALL_CHILDREN_VERTICALLY; return NS_OK; } nsresult nsMathMLmpaddedFrame::AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType) { if (aNameSpaceID == kNameSpaceID_None) { bool hasDirtyAttributes = false; IntrinsicDirty intrinsicDirty = IntrinsicDirty::None; if (aAttribute == nsGkAtoms::width) { mWidth.mState = Attribute::ParsingState::Dirty; hasDirtyAttributes = true; intrinsicDirty = IntrinsicDirty::FrameAndAncestors; } else if (aAttribute == nsGkAtoms::height) { mHeight.mState = Attribute::ParsingState::Dirty; hasDirtyAttributes = true; } else if (aAttribute == nsGkAtoms::depth) { mDepth.mState = Attribute::ParsingState::Dirty; hasDirtyAttributes = true; } else if (aAttribute == nsGkAtoms::lspace) { mLeadingSpace.mState = Attribute::ParsingState::Dirty; hasDirtyAttributes = true; intrinsicDirty = IntrinsicDirty::FrameAndAncestors; } else if (aAttribute == nsGkAtoms::voffset) { mVerticalOffset.mState = Attribute::ParsingState::Dirty; hasDirtyAttributes = true; } if (hasDirtyAttributes) { PresShell()->FrameNeedsReflow(this, intrinsicDirty, NS_FRAME_IS_DIRTY); } return NS_OK; } return nsMathMLContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); } void nsMathMLmpaddedFrame::ParseAttribute(nsAtom* aAtom, Attribute& aAttribute) { if (aAttribute.mState != Attribute::ParsingState::Dirty) { return; } nsAutoString value; aAttribute.mState = Attribute::ParsingState::Invalid; mContent->AsElement()->GetAttr(aAtom, value); if (!value.IsEmpty()) { if (!ParseAttribute(value, aAttribute)) { ReportParseError(aAtom->GetUTF16String(), value.get()); } } } bool nsMathMLmpaddedFrame::ParseAttribute(nsString& aString, Attribute& aAttribute) { // See https://www.w3.org/TR/MathML3/chapter3.html#presm.mpaddedatt aAttribute.Reset(); aAttribute.mState = Attribute::ParsingState::Invalid; aString.CompressWhitespace(); // aString is not a const in this code int32_t stringLength = aString.Length(); if (!stringLength) { return false; } nsAutoString number, unit; ////////////////////// // see if the sign is there int32_t i = 0; if (aString[0] == '+') { aAttribute.mSign = Attribute::Sign::Plus; i++; } else if (aString[0] == '-') { aAttribute.mSign = Attribute::Sign::Minus; i++; } else { aAttribute.mSign = Attribute::Sign::Unspecified; } // get the number bool gotDot = false, gotPercent = false; for (; i < stringLength; i++) { char16_t c = aString[i]; if (gotDot && c == '.') { // error - two dots encountered return false; } if (c == '.') { gotDot = true; } else if (!IsAsciiDigit(c)) { break; } number.Append(c); } // catch error if we didn't enter the loop above... we could simply initialize // floatValue = 1, to cater for cases such as width="height", but that // wouldn't be in line with the spec which requires an explicit number if (number.IsEmpty()) { return false; } nsresult errorCode; float floatValue = number.ToFloat(&errorCode); if (NS_FAILED(errorCode)) { return false; } // see if this is a percentage-based value if (i < stringLength && aString[i] == '%') { i++; gotPercent = true; } // the remainder now should be a css-unit, or a pseudo-unit, or a named-space aString.Right(unit, stringLength - i); if (unit.IsEmpty()) { if (gotPercent) { // case ["+"|"-"] unsigned-number "%" aAttribute.mValue.SetPercentValue(floatValue / 100.0f); aAttribute.mPseudoUnit = Attribute::PseudoUnit::ItSelf; aAttribute.mState = Attribute::ParsingState::Valid; return true; } else { // case ["+"|"-"] unsigned-number // XXXfredw: should we allow non-zero unitless values? See bug 757703. if (!floatValue) { aAttribute.mValue.SetFloatValue(floatValue, eCSSUnit_Number); aAttribute.mPseudoUnit = Attribute::PseudoUnit::ItSelf; aAttribute.mState = Attribute::ParsingState::Valid; return true; } } } else if (unit.EqualsLiteral("width")) { aAttribute.mPseudoUnit = Attribute::PseudoUnit::Width; } else if (unit.EqualsLiteral("height")) { aAttribute.mPseudoUnit = Attribute::PseudoUnit::Height; } else if (unit.EqualsLiteral("depth")) { aAttribute.mPseudoUnit = Attribute::PseudoUnit::Depth; } else if (!gotPercent) { // percentage can only apply to a pseudo-unit // see if the unit is a named-space if (dom::MathMLElement::ParseNamedSpaceValue( unit, aAttribute.mValue, dom::MathMLElement::PARSE_ALLOW_NEGATIVE, *mContent->OwnerDoc())) { // re-scale properly, and we know that the unit of the named-space is 'em' floatValue *= aAttribute.mValue.GetFloatValue(); aAttribute.mValue.SetFloatValue(floatValue, eCSSUnit_EM); aAttribute.mPseudoUnit = Attribute::PseudoUnit::NamedSpace; aAttribute.mState = Attribute::ParsingState::Valid; return true; } // see if the input was just a CSS value // We are not supposed to have a unitless, percent, negative or namedspace // value here. number.Append(unit); // leave the sign out if it was there if (dom::MathMLElement::ParseNumericValue( number, aAttribute.mValue, dom::MathMLElement::PARSE_SUPPRESS_WARNINGS, nullptr)) { aAttribute.mState = Attribute::ParsingState::Valid; return true; } } // if we enter here, we have a number that will act as a multiplier on a // pseudo-unit if (aAttribute.mPseudoUnit != Attribute::PseudoUnit::Unspecified) { if (gotPercent) { aAttribute.mValue.SetPercentValue(floatValue / 100.0f); } else { aAttribute.mValue.SetFloatValue(floatValue, eCSSUnit_Number); } aAttribute.mState = Attribute::ParsingState::Valid; return true; } #ifdef DEBUG printf("mpadded: attribute with bad numeric value: %s\n", NS_LossyConvertUTF16toASCII(aString).get()); #endif // if we reach here, it means we encounter an unexpected input return false; } void nsMathMLmpaddedFrame::UpdateValue(const Attribute& aAttribute, Attribute::PseudoUnit aSelfUnit, const ReflowOutput& aDesiredSize, nscoord& aValueToUpdate, float aFontSizeInflation) { nsCSSUnit unit = aAttribute.mValue.GetUnit(); if (aAttribute.IsValid() && eCSSUnit_Null != unit) { nscoord scaler = 0, amount = 0; if (eCSSUnit_Percent == unit || eCSSUnit_Number == unit) { auto pseudoUnit = aAttribute.mPseudoUnit; if (pseudoUnit == Attribute::PseudoUnit::ItSelf) { pseudoUnit = aSelfUnit; } switch (pseudoUnit) { case Attribute::PseudoUnit::Width: scaler = aDesiredSize.Width(); break; case Attribute::PseudoUnit::Height: scaler = aDesiredSize.BlockStartAscent(); break; case Attribute::PseudoUnit::Depth: scaler = aDesiredSize.Height() - aDesiredSize.BlockStartAscent(); break; default: // if we ever reach here, it would mean something is wrong // somewhere with the setup and/or the caller NS_ERROR("Unexpected Pseudo Unit"); return; } } if (eCSSUnit_Number == unit) { amount = NSToCoordRound(float(scaler) * aAttribute.mValue.GetFloatValue()); } else if (eCSSUnit_Percent == unit) { amount = NSToCoordRound(float(scaler) * aAttribute.mValue.GetPercentValue()); } else { amount = CalcLength(aAttribute.mValue, aFontSizeInflation, this); } switch (aAttribute.mSign) { case Attribute::Sign::Plus: aValueToUpdate += amount; break; case Attribute::Sign::Minus: aValueToUpdate -= amount; break; case Attribute::Sign::Unspecified: aValueToUpdate = amount; break; } } } /* virtual */ nsresult nsMathMLmpaddedFrame::Place(DrawTarget* aDrawTarget, const PlaceFlags& aFlags, ReflowOutput& aDesiredSize) { // First perform normal row layout without border/padding. PlaceFlags flags = aFlags + PlaceFlag::MeasureOnly + PlaceFlag::IgnoreBorderPadding + PlaceFlag::DoNotAdjustForWidthAndHeight; nsresult rv = nsMathMLContainerFrame::Place(aDrawTarget, flags, aDesiredSize); if (NS_FAILED(rv)) { DidReflowChildren(PrincipalChildList().FirstChild()); return rv; } nscoord height = aDesiredSize.BlockStartAscent(); nscoord depth = aDesiredSize.Height() - aDesiredSize.BlockStartAscent(); // The REC says: // // "The lspace attribute ('leading' space) specifies the horizontal location // of the positioning point of the child content with respect to the // positioning point of the mpadded element. By default they coincide, and // therefore absolute values for lspace have the same effect as relative // values." // // "MathML renderers should ensure that, except for the effects of the // attributes, the relative spacing between the contents of the mpadded // element and surrounding MathML elements would not be modified by replacing // an mpadded element with an mrow element with the same content, even if // linebreaking occurs within the mpadded element." // // (http://www.w3.org/TR/MathML/chapter3.html#presm.mpadded) // // "In those discussions, the terms leading and trailing are used to specify // a side of an object when which side to use depends on the directionality; // ie. leading means left in LTR but right in RTL." // (http://www.w3.org/TR/MathML/chapter3.html#presm.bidi.math) nscoord lspace = 0; // In MathML3, "width" will be the bounding box width and "advancewidth" will // refer "to the horizontal distance between the positioning point of the // mpadded and the positioning point for the following content". MathML2 // doesn't make the distinction. nscoord width = aDesiredSize.Width(); nscoord voffset = 0; nscoord initialWidth = width; float fontSizeInflation = nsLayoutUtils::FontSizeInflationFor(this); // update width ParseAttribute(nsGkAtoms::width, mWidth); UpdateValue(mWidth, Attribute::PseudoUnit::Width, aDesiredSize, width, fontSizeInflation); width = std::max(0, width); // update "height" (this is the ascent in the terminology of the REC) ParseAttribute(nsGkAtoms::height, mHeight); UpdateValue(mHeight, Attribute::PseudoUnit::Height, aDesiredSize, height, fontSizeInflation); height = std::max(0, height); // update "depth" (this is the descent in the terminology of the REC) ParseAttribute(nsGkAtoms::depth, mDepth); UpdateValue(mDepth, Attribute::PseudoUnit::Depth, aDesiredSize, depth, fontSizeInflation); depth = std::max(0, depth); // update lspace ParseAttribute(nsGkAtoms::lspace, mLeadingSpace); if (mLeadingSpace.mPseudoUnit != Attribute::PseudoUnit::ItSelf) { UpdateValue(mLeadingSpace, Attribute::PseudoUnit::Unspecified, aDesiredSize, lspace, fontSizeInflation); } // update voffset ParseAttribute(nsGkAtoms::voffset, mVerticalOffset); if (mVerticalOffset.mPseudoUnit != Attribute::PseudoUnit::ItSelf) { UpdateValue(mVerticalOffset, Attribute::PseudoUnit::Unspecified, aDesiredSize, voffset, fontSizeInflation); } // do the padding now that we have everything // The idea here is to maintain the invariant that ... // (i.e., with no attributes) looks the same as .... But when // there are attributes, tweak our metrics and move children to achieve the // desired visual effects. const bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; if (isRTL ? mWidth.IsValid() : mLeadingSpace.IsValid()) { // there was padding on the left. dismiss the left italic correction now // (so that our parent won't correct us) mBoundingMetrics.leftBearing = 0; } if (isRTL ? mLeadingSpace.IsValid() : mWidth.IsValid()) { // there was padding on the right. dismiss the right italic correction now // (so that our parent won't correct us) mBoundingMetrics.width = width; mBoundingMetrics.rightBearing = mBoundingMetrics.width; } nscoord dx = (isRTL ? width - initialWidth - lspace : lspace); aDesiredSize.SetBlockStartAscent(height); aDesiredSize.Width() = mBoundingMetrics.width; aDesiredSize.Height() = depth + aDesiredSize.BlockStartAscent(); mBoundingMetrics.ascent = height; mBoundingMetrics.descent = depth; aDesiredSize.mBoundingMetrics = mBoundingMetrics; // Apply width/height to math content box. auto sizes = GetWidthAndHeightForPlaceAdjustment(aFlags); dx += ApplyAdjustmentForWidthAndHeight(aFlags, sizes, aDesiredSize, mBoundingMetrics); // Add padding+border. auto borderPadding = GetBorderPaddingForPlace(aFlags); InflateReflowAndBoundingMetrics(borderPadding, aDesiredSize, mBoundingMetrics); dx += borderPadding.left; mReference.x = 0; mReference.y = aDesiredSize.BlockStartAscent(); if (!aFlags.contains(PlaceFlag::MeasureOnly)) { // Finish reflowing child frames, positioning their origins. PositionRowChildFrames(dx, aDesiredSize.BlockStartAscent() - voffset); } return NS_OK; }