gecko-dev/layout/svg/SVGForeignObjectFrame.cpp
Robert Longson a1b7878e33 Bug 1833616 - Use RAII to manage Push/Pop of gfxContext Clip r=emilio,gfx-reviewers,lsalzman
Take care to restore only the parts of the gfxContext that we need to because saving and restoring the complete gfxContext is expensive.

Differential Revision: https://phabricator.services.mozilla.com/D178304
2023-05-17 15:08:28 +00:00

452 lines
16 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/. */
// Main header first:
#include "SVGForeignObjectFrame.h"
// Keep others in (case-insensitive) order:
#include "ImgDrawResult.h"
#include "gfxContext.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/PresShell.h"
#include "mozilla/SVGContainerFrame.h"
#include "mozilla/SVGObserverUtils.h"
#include "mozilla/SVGUtils.h"
#include "mozilla/dom/SVGForeignObjectElement.h"
#include "nsDisplayList.h"
#include "nsGkAtoms.h"
#include "nsNameSpaceManager.h"
#include "nsLayoutUtils.h"
#include "nsRegion.h"
#include "SVGGeometryProperty.h"
using namespace mozilla::dom;
using namespace mozilla::image;
namespace SVGT = SVGGeometryProperty::Tags;
//----------------------------------------------------------------------
// Implementation
nsContainerFrame* NS_NewSVGForeignObjectFrame(mozilla::PresShell* aPresShell,
mozilla::ComputedStyle* aStyle) {
return new (aPresShell)
mozilla::SVGForeignObjectFrame(aStyle, aPresShell->GetPresContext());
}
namespace mozilla {
NS_IMPL_FRAMEARENA_HELPERS(SVGForeignObjectFrame)
SVGForeignObjectFrame::SVGForeignObjectFrame(ComputedStyle* aStyle,
nsPresContext* aPresContext)
: nsContainerFrame(aStyle, aPresContext, kClassID), mInReflow(false) {
AddStateBits(NS_FRAME_REFLOW_ROOT | NS_FRAME_MAY_BE_TRANSFORMED |
NS_FRAME_SVG_LAYOUT | NS_FRAME_FONT_INFLATION_CONTAINER |
NS_FRAME_FONT_INFLATION_FLOW_ROOT);
}
//----------------------------------------------------------------------
// nsIFrame methods
NS_QUERYFRAME_HEAD(SVGForeignObjectFrame)
NS_QUERYFRAME_ENTRY(ISVGDisplayableFrame)
NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
void SVGForeignObjectFrame::Init(nsIContent* aContent,
nsContainerFrame* aParent,
nsIFrame* aPrevInFlow) {
NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::foreignObject),
"Content is not an SVG foreignObject!");
nsContainerFrame::Init(aContent, aParent, aPrevInFlow);
AddStateBits(aParent->GetStateBits() & NS_STATE_SVG_CLIPPATH_CHILD);
}
nsresult SVGForeignObjectFrame::AttributeChanged(int32_t aNameSpaceID,
nsAtom* aAttribute,
int32_t aModType) {
if (aNameSpaceID == kNameSpaceID_None) {
if (aAttribute == nsGkAtoms::transform) {
// We don't invalidate for transform changes (the layers code does that).
// Also note that SVGTransformableElement::GetAttributeChangeHint will
// return nsChangeHint_UpdateOverflow for "transform" attribute changes
// and cause DoApplyRenderingChangeToTree to make the SchedulePaint call.
mCanvasTM = nullptr;
} else if (aAttribute == nsGkAtoms::viewBox ||
aAttribute == nsGkAtoms::preserveAspectRatio) {
nsLayoutUtils::PostRestyleEvent(
mContent->AsElement(), RestyleHint{0},
nsChangeHint_InvalidateRenderingObservers);
}
}
return NS_OK;
}
void SVGForeignObjectFrame::DidSetComputedStyle(
ComputedStyle* aOldComputedStyle) {
nsContainerFrame::DidSetComputedStyle(aOldComputedStyle);
if (aOldComputedStyle) {
if (StyleSVGReset()->mX != aOldComputedStyle->StyleSVGReset()->mX ||
StyleSVGReset()->mY != aOldComputedStyle->StyleSVGReset()->mY) {
// Invalidate cached transform matrix.
mCanvasTM = nullptr;
SVGUtils::ScheduleReflowSVG(this);
}
}
}
void SVGForeignObjectFrame::Reflow(nsPresContext* aPresContext,
ReflowOutput& aDesiredSize,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IS_NONDISPLAY),
"Should not have been called");
// Only InvalidateAndScheduleBoundsUpdate marks us with NS_FRAME_IS_DIRTY,
// so if that bit is still set we still have a resize pending. If we hit
// this assertion, then we should get the presShell to skip reflow roots
// that have a dirty parent since a reflow is going to come via the
// reflow root's parent anyway.
NS_ASSERTION(!HasAnyStateBits(NS_FRAME_IS_DIRTY),
"Reflowing while a resize is pending is wasteful");
// ReflowSVG makes sure mRect is up to date before we're called.
NS_ASSERTION(!aReflowInput.mParentReflowInput,
"should only get reflow from being reflow root");
NS_ASSERTION(aReflowInput.ComputedWidth() == GetSize().width &&
aReflowInput.ComputedHeight() == GetSize().height,
"reflow roots should be reflowed at existing size and "
"svg.css should ensure we have no padding/border/margin");
DoReflow();
WritingMode wm = aReflowInput.GetWritingMode();
LogicalSize finalSize(wm, aReflowInput.ComputedISize(),
aReflowInput.ComputedBSize());
aDesiredSize.SetSize(wm, finalSize);
aDesiredSize.SetOverflowAreasToDesiredBounds();
}
void SVGForeignObjectFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
const nsDisplayListSet& aLists) {
if (!static_cast<const SVGElement*>(GetContent())->HasValidDimensions()) {
return;
}
nsDisplayList newList(aBuilder);
nsDisplayListSet set(&newList, &newList, &newList, &newList, &newList,
&newList);
DisplayOutline(aBuilder, set);
BuildDisplayListForNonBlockChildren(aBuilder, set);
aLists.Content()->AppendNewToTop<nsDisplayForeignObject>(aBuilder, this,
&newList);
}
bool SVGForeignObjectFrame::IsSVGTransformed(
Matrix* aOwnTransform, Matrix* aFromParentTransform) const {
return SVGUtils::IsSVGTransformed(this, aOwnTransform, aFromParentTransform);
}
void SVGForeignObjectFrame::PaintSVG(gfxContext& aContext,
const gfxMatrix& aTransform,
imgDrawingParams& aImgParams) {
NS_ASSERTION(HasAnyStateBits(NS_FRAME_IS_NONDISPLAY),
"Only painting of non-display SVG should take this code path");
if (IsDisabled()) {
return;
}
nsIFrame* kid = PrincipalChildList().FirstChild();
if (!kid) {
return;
}
if (aTransform.IsSingular()) {
NS_WARNING("Can't render foreignObject element!");
return;
}
gfxClipAutoSaveRestore autoSaveClip(&aContext);
if (StyleDisplay()->IsScrollableOverflow()) {
float x, y, width, height;
SVGGeometryProperty::ResolveAll<SVGT::X, SVGT::Y, SVGT::Width,
SVGT::Height>(
static_cast<SVGElement*>(GetContent()), &x, &y, &width, &height);
gfxRect clipRect =
SVGUtils::GetClipRectForFrame(this, 0.0f, 0.0f, width, height);
autoSaveClip.TransformedClip(aTransform, clipRect);
}
// SVG paints in CSS px, but normally frames paint in dev pixels. Here we
// multiply a CSS-px-to-dev-pixel factor onto aTransform so our children
// paint correctly.
float cssPxPerDevPx = nsPresContext::AppUnitsToFloatCSSPixels(
PresContext()->AppUnitsPerDevPixel());
gfxMatrix canvasTMForChildren = aTransform;
canvasTMForChildren.PreScale(cssPxPerDevPx, cssPxPerDevPx);
aContext.Multiply(canvasTMForChildren);
using PaintFrameFlags = nsLayoutUtils::PaintFrameFlags;
PaintFrameFlags flags = PaintFrameFlags::InTransform;
if (SVGAutoRenderState::IsPaintingToWindow(aContext.GetDrawTarget())) {
flags |= PaintFrameFlags::ToWindow;
}
if (aImgParams.imageFlags & imgIContainer::FLAG_SYNC_DECODE) {
flags |= PaintFrameFlags::SyncDecodeImages;
}
if (aImgParams.imageFlags & imgIContainer::FLAG_HIGH_QUALITY_SCALING) {
flags |= PaintFrameFlags::UseHighQualityScaling;
}
nsLayoutUtils::PaintFrame(&aContext, kid, nsRegion(kid->InkOverflowRect()),
NS_RGBA(0, 0, 0, 0),
nsDisplayListBuilderMode::Painting, flags);
}
nsIFrame* SVGForeignObjectFrame::GetFrameForPoint(const gfxPoint& aPoint) {
MOZ_ASSERT_UNREACHABLE(
"A clipPath cannot contain an SVGForeignObject element");
return nullptr;
}
void SVGForeignObjectFrame::ReflowSVG() {
NS_ASSERTION(SVGUtils::OuterSVGIsCallingReflowSVG(this),
"This call is probably a wasteful mistake");
MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IS_NONDISPLAY),
"ReflowSVG mechanism not designed for this");
if (!SVGUtils::NeedsReflowSVG(this)) {
return;
}
// We update mRect before the DoReflow call so that DoReflow uses the
// correct dimensions:
float x, y, w, h;
SVGGeometryProperty::ResolveAll<SVGT::X, SVGT::Y, SVGT::Width, SVGT::Height>(
static_cast<SVGElement*>(GetContent()), &x, &y, &w, &h);
// If mRect's width or height are negative, reflow blows up! We must clamp!
if (w < 0.0f) w = 0.0f;
if (h < 0.0f) h = 0.0f;
mRect = nsLayoutUtils::RoundGfxRectToAppRect(gfxRect(x, y, w, h),
AppUnitsPerCSSPixel());
// Fully mark our kid dirty so that it gets resized if necessary
// (NS_FRAME_HAS_DIRTY_CHILDREN isn't enough in that case):
nsIFrame* kid = PrincipalChildList().FirstChild();
kid->MarkSubtreeDirty();
// Make sure to not allow interrupts if we're not being reflown as a root:
nsPresContext::InterruptPreventer noInterrupts(PresContext());
DoReflow();
if (HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) {
// Make sure we have our filter property (if any) before calling
// FinishAndStoreOverflow (subsequent filter changes are handled off
// nsChangeHint_UpdateEffects):
SVGObserverUtils::UpdateEffects(this);
}
// If we have a filter, we need to invalidate ourselves because filter
// output can change even if none of our descendants need repainting.
if (StyleEffects()->HasFilters()) {
InvalidateFrame();
}
auto* anonKid = PrincipalChildList().FirstChild();
nsRect overflow = anonKid->InkOverflowRect();
OverflowAreas overflowAreas(overflow, overflow);
FinishAndStoreOverflow(overflowAreas, mRect.Size());
// Now unset the various reflow bits:
RemoveStateBits(NS_FRAME_FIRST_REFLOW | NS_FRAME_IS_DIRTY |
NS_FRAME_HAS_DIRTY_CHILDREN);
}
void SVGForeignObjectFrame::NotifySVGChanged(uint32_t aFlags) {
MOZ_ASSERT(aFlags & (TRANSFORM_CHANGED | COORD_CONTEXT_CHANGED),
"Invalidation logic may need adjusting");
bool needNewBounds = false; // i.e. mRect or ink overflow rect
bool needReflow = false;
bool needNewCanvasTM = false;
if (aFlags & COORD_CONTEXT_CHANGED) {
// Coordinate context changes affect mCanvasTM if we have a
// percentage 'x' or 'y'
if (StyleSVGReset()->mX.HasPercent() || StyleSVGReset()->mY.HasPercent()) {
needNewBounds = true;
needNewCanvasTM = true;
}
// Our coordinate context's width/height has changed. If we have a
// percentage width/height our dimensions will change so we must reflow.
if (StylePosition()->mWidth.HasPercent() ||
StylePosition()->mHeight.HasPercent()) {
needNewBounds = true;
needReflow = true;
}
}
if (aFlags & TRANSFORM_CHANGED) {
if (mCanvasTM && mCanvasTM->IsSingular()) {
needNewBounds = true; // old bounds are bogus
}
needNewCanvasTM = true;
// In an ideal world we would reflow when our CTM changes. This is because
// glyph metrics do not necessarily scale uniformly with change in scale
// and, as a result, CTM changes may require text to break at different
// points. The problem would be how to keep performance acceptable when
// e.g. the transform of an ancestor is animated.
// We also seem to get some sort of infinite loop post bug 421584 if we
// reflow.
}
if (needNewBounds) {
// Ancestor changes can't affect how we render from the perspective of
// any rendering observers that we may have, so we don't need to
// invalidate them. We also don't need to invalidate ourself, since our
// changed ancestor will have invalidated its entire area, which includes
// our area.
SVGUtils::ScheduleReflowSVG(this);
}
// If we're called while the PresShell is handling reflow events then we
// must have been called as a result of the NotifyViewportChange() call in
// our SVGOuterSVGFrame's Reflow() method. We must not call RequestReflow
// at this point (i.e. during reflow) because it could confuse the
// PresShell and prevent it from reflowing us properly in future. Besides
// that, SVGOuterSVGFrame::DidReflow will take care of reflowing us
// synchronously, so there's no need.
if (needReflow && !PresShell()->IsReflowLocked()) {
RequestReflow(IntrinsicDirty::None);
}
if (needNewCanvasTM) {
// Do this after calling InvalidateAndScheduleBoundsUpdate in case we
// change the code and it needs to use it.
mCanvasTM = nullptr;
}
}
SVGBBox SVGForeignObjectFrame::GetBBoxContribution(
const Matrix& aToBBoxUserspace, uint32_t aFlags) {
SVGForeignObjectElement* content =
static_cast<SVGForeignObjectElement*>(GetContent());
float x, y, w, h;
SVGGeometryProperty::ResolveAll<SVGT::X, SVGT::Y, SVGT::Width, SVGT::Height>(
content, &x, &y, &w, &h);
if (w < 0.0f) w = 0.0f;
if (h < 0.0f) h = 0.0f;
if (aToBBoxUserspace.IsSingular()) {
// XXX ReportToConsole
return SVGBBox();
}
return aToBBoxUserspace.TransformBounds(gfx::Rect(0.0, 0.0, w, h));
}
//----------------------------------------------------------------------
gfxMatrix SVGForeignObjectFrame::GetCanvasTM() {
if (!mCanvasTM) {
NS_ASSERTION(GetParent(), "null parent");
auto* parent = static_cast<SVGContainerFrame*>(GetParent());
auto* content = static_cast<SVGForeignObjectElement*>(GetContent());
gfxMatrix tm = content->PrependLocalTransformsTo(parent->GetCanvasTM());
mCanvasTM = MakeUnique<gfxMatrix>(tm);
}
return *mCanvasTM;
}
//----------------------------------------------------------------------
// Implementation helpers
void SVGForeignObjectFrame::RequestReflow(IntrinsicDirty aType) {
if (HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) {
// If we haven't had a ReflowSVG() yet, nothing to do.
return;
}
nsIFrame* kid = PrincipalChildList().FirstChild();
if (!kid) {
return;
}
PresShell()->FrameNeedsReflow(kid, aType, NS_FRAME_IS_DIRTY);
}
void SVGForeignObjectFrame::DoReflow() {
MarkInReflow();
// Skip reflow if we're zero-sized, unless this is our first reflow.
if (IsDisabled() && !HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) {
return;
}
nsPresContext* presContext = PresContext();
nsIFrame* kid = PrincipalChildList().FirstChild();
if (!kid) {
return;
}
// initiate a synchronous reflow here and now:
UniquePtr<gfxContext> renderingContext =
presContext->PresShell()->CreateReferenceRenderingContext();
mInReflow = true;
WritingMode wm = kid->GetWritingMode();
ReflowInput reflowInput(presContext, kid, renderingContext.get(),
LogicalSize(wm, ISize(wm), NS_UNCONSTRAINEDSIZE));
ReflowOutput desiredSize(reflowInput);
nsReflowStatus status;
// We don't use mRect.height above because that tells the child to do
// page/column breaking at that height.
NS_ASSERTION(
reflowInput.ComputedPhysicalBorderPadding() == nsMargin(0, 0, 0, 0) &&
reflowInput.ComputedPhysicalMargin() == nsMargin(0, 0, 0, 0),
"style system should ensure that :-moz-svg-foreign-content "
"does not get styled");
NS_ASSERTION(reflowInput.ComputedISize() == ISize(wm),
"reflow input made child wrong size");
reflowInput.SetComputedBSize(BSize(wm));
ReflowChild(kid, presContext, desiredSize, reflowInput, 0, 0,
ReflowChildFlags::NoMoveFrame, status);
NS_ASSERTION(mRect.width == desiredSize.Width() &&
mRect.height == desiredSize.Height(),
"unexpected size");
FinishReflowChild(kid, presContext, desiredSize, &reflowInput, 0, 0,
ReflowChildFlags::NoMoveFrame);
mInReflow = false;
}
void SVGForeignObjectFrame::AppendDirectlyOwnedAnonBoxes(
nsTArray<OwnedAnonBox>& aResult) {
MOZ_ASSERT(PrincipalChildList().FirstChild(), "Must have our anon box");
aResult.AppendElement(OwnedAnonBox(PrincipalChildList().FirstChild()));
}
} // namespace mozilla