diff options
Diffstat (limited to 'layout/generic/nsHTMLCanvasFrame.cpp')
-rw-r--r-- | layout/generic/nsHTMLCanvasFrame.cpp | 534 |
1 files changed, 534 insertions, 0 deletions
diff --git a/layout/generic/nsHTMLCanvasFrame.cpp b/layout/generic/nsHTMLCanvasFrame.cpp new file mode 100644 index 0000000000..2ccb6f0b20 --- /dev/null +++ b/layout/generic/nsHTMLCanvasFrame.cpp @@ -0,0 +1,534 @@ +/* -*- 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/. */ + +/* rendering object for the HTML <canvas> element */ + +#include "nsHTMLCanvasFrame.h" + +#include "nsGkAtoms.h" +#include "mozilla/Assertions.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/HTMLCanvasElement.h" +#include "mozilla/layers/ImageDataSerializer.h" +#include "mozilla/layers/WebRenderBridgeChild.h" +#include "mozilla/layers/WebRenderCanvasRenderer.h" +#include "mozilla/layers/RenderRootStateManager.h" +#include "mozilla/webgpu/CanvasContext.h" +#include "nsDisplayList.h" +#include "nsLayoutUtils.h" +#include "nsStyleUtil.h" +#include "ActiveLayerTracker.h" + +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::layers; +using namespace mozilla::gfx; + +/* Helper for our nsIFrame::GetIntrinsicSize() impl. Takes the result of + * "GetCanvasSize()" as a parameter, which may help avoid redundant + * indirect calls to GetCanvasSize(). + * + * @param aCanvasSizeInPx The canvas's size in CSS pixels, as returned + * by GetCanvasSize(). + * @return The canvas's intrinsic size, as an IntrinsicSize object. + */ +static IntrinsicSize IntrinsicSizeFromCanvasSize( + const nsIntSize& aCanvasSizeInPx) { + return IntrinsicSize( + nsPresContext::CSSPixelsToAppUnits(aCanvasSizeInPx.width), + nsPresContext::CSSPixelsToAppUnits(aCanvasSizeInPx.height)); +} + +/* Helper for our nsIFrame::GetIntrinsicRatio() impl. Takes the result of + * "GetCanvasSize()" as a parameter, which may help avoid redundant + * indirect calls to GetCanvasSize(). + * + * @return The canvas's intrinsic ratio. + */ +static AspectRatio IntrinsicRatioFromCanvasSize( + const nsIntSize& aCanvasSizeInPx) { + return AspectRatio::FromSize(aCanvasSizeInPx.width, aCanvasSizeInPx.height); +} + +class nsDisplayCanvas final : public nsPaintedDisplayItem { + public: + nsDisplayCanvas(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayCanvas); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayCanvas) + + NS_DISPLAY_DECL_NAME("nsDisplayCanvas", TYPE_CANVAS) + + virtual nsRegion GetOpaqueRegion(nsDisplayListBuilder* aBuilder, + bool* aSnap) const override { + *aSnap = false; + nsHTMLCanvasFrame* f = static_cast<nsHTMLCanvasFrame*>(Frame()); + HTMLCanvasElement* canvas = HTMLCanvasElement::FromNode(f->GetContent()); + nsRegion result; + if (canvas->GetIsOpaque()) { + // OK, the entire region painted by the canvas is opaque. But what is + // that region? It's the canvas's "dest rect" (controlled by the + // object-fit/object-position CSS properties), clipped to the container's + // content box (which is what GetBounds() returns). So, we grab those + // rects and intersect them. + nsRect constraintRect = GetBounds(aBuilder, aSnap); + + // Need intrinsic size & ratio, for ComputeObjectDestRect: + nsIntSize canvasSize = f->GetCanvasSize(); + IntrinsicSize intrinsicSize = IntrinsicSizeFromCanvasSize(canvasSize); + AspectRatio intrinsicRatio = IntrinsicRatioFromCanvasSize(canvasSize); + + const nsRect destRect = nsLayoutUtils::ComputeObjectDestRect( + constraintRect, intrinsicSize, intrinsicRatio, f->StylePosition()); + return nsRegion(destRect.Intersect(constraintRect)); + } + return result; + } + + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const override { + *aSnap = true; + return Frame()->GetContentRectRelativeToSelf() + ToReferenceFrame(); + } + + virtual bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + wr::IpcResourceUpdateQueue& aResources, const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override { + HTMLCanvasElement* element = + static_cast<HTMLCanvasElement*>(mFrame->GetContent()); + element->HandlePrintCallback(mFrame->PresContext()); + + if (element->IsOffscreen()) { + // If we are offscreen, then we either display via an ImageContainer + // which is updated asynchronously, likely from a worker thread, or a + // CompositableHandle managed inside the compositor process. There is + // nothing to paint until the owner attaches it. + + element->FlushOffscreenCanvas(); + + nsHTMLCanvasFrame* canvasFrame = static_cast<nsHTMLCanvasFrame*>(mFrame); + nsIntSize canvasSizeInPx = canvasFrame->GetCanvasSize(); + IntrinsicSize intrinsicSize = IntrinsicSizeFromCanvasSize(canvasSizeInPx); + AspectRatio intrinsicRatio = IntrinsicRatioFromCanvasSize(canvasSizeInPx); + nsRect area = mFrame->GetContentRectRelativeToSelf() + ToReferenceFrame(); + nsRect dest = nsLayoutUtils::ComputeObjectDestRect( + area, intrinsicSize, intrinsicRatio, mFrame->StylePosition()); + LayoutDeviceRect bounds = LayoutDeviceRect::FromAppUnits( + dest, mFrame->PresContext()->AppUnitsPerDevPixel()); + + RefPtr<ImageContainer> container = element->GetImageContainer(); + if (container) { + MOZ_ASSERT(container->IsAsync()); + aManager->CommandBuilder().PushImage(this, container, aBuilder, + aResources, aSc, bounds, bounds); + return true; + } + + return true; + } + + switch (element->GetCurrentContextType()) { + case CanvasContextType::Canvas2D: + case CanvasContextType::WebGL1: + case CanvasContextType::WebGL2: + case CanvasContextType::WebGPU: { + bool isRecycled; + RefPtr<WebRenderCanvasData> canvasData = + aManager->CommandBuilder() + .CreateOrRecycleWebRenderUserData<WebRenderCanvasData>( + this, &isRecycled); + nsHTMLCanvasFrame* canvasFrame = + static_cast<nsHTMLCanvasFrame*>(mFrame); + if (!canvasFrame->UpdateWebRenderCanvasData(aDisplayListBuilder, + canvasData)) { + return true; + } + WebRenderCanvasRendererAsync* data = canvasData->GetCanvasRenderer(); + MOZ_ASSERT(data); + data->UpdateCompositableClient(); + + // Push IFrame for async image pipeline. + // XXX Remove this once partial display list update is supported. + + nsIntSize canvasSizeInPx = data->GetSize(); + IntrinsicSize intrinsicSize = + IntrinsicSizeFromCanvasSize(canvasSizeInPx); + AspectRatio intrinsicRatio = + IntrinsicRatioFromCanvasSize(canvasSizeInPx); + + nsRect area = + mFrame->GetContentRectRelativeToSelf() + ToReferenceFrame(); + nsRect dest = nsLayoutUtils::ComputeObjectDestRect( + area, intrinsicSize, intrinsicRatio, mFrame->StylePosition()); + + LayoutDeviceRect bounds = LayoutDeviceRect::FromAppUnits( + dest, mFrame->PresContext()->AppUnitsPerDevPixel()); + + // We don't push a stacking context for this async image pipeline here. + // Instead, we do it inside the iframe that hosts the image. As a + // result, a bunch of the calculations normally done as part of that + // stacking context need to be done manually and pushed over to the + // parent side, where it will be done when we build the display list for + // the iframe. That happens in WebRenderCompositableHolder.s2); + aBuilder.PushIFrame(bounds, !BackfaceIsHidden(), + data->GetPipelineId().ref(), + /*ignoreMissingPipelines*/ false); + + LayoutDeviceRect scBounds(LayoutDevicePoint(0, 0), bounds.Size()); + auto filter = wr::ToImageRendering(mFrame->UsedImageRendering()); + auto mixBlendMode = wr::MixBlendMode::Normal; + aManager->WrBridge()->AddWebRenderParentCommand( + OpUpdateAsyncImagePipeline(data->GetPipelineId().value(), scBounds, + VideoInfo::Rotation::kDegree_0, filter, + mixBlendMode)); + break; + } + case CanvasContextType::ImageBitmap: { + nsHTMLCanvasFrame* canvasFrame = + static_cast<nsHTMLCanvasFrame*>(mFrame); + nsIntSize canvasSizeInPx = canvasFrame->GetCanvasSize(); + if (canvasSizeInPx.width <= 0 || canvasSizeInPx.height <= 0) { + return true; + } + bool isRecycled; + RefPtr<WebRenderCanvasData> canvasData = + aManager->CommandBuilder() + .CreateOrRecycleWebRenderUserData<WebRenderCanvasData>( + this, &isRecycled); + if (!canvasFrame->UpdateWebRenderCanvasData(aDisplayListBuilder, + canvasData)) { + canvasData->ClearImageContainer(); + return true; + } + + IntrinsicSize intrinsicSize = + IntrinsicSizeFromCanvasSize(canvasSizeInPx); + AspectRatio intrinsicRatio = + IntrinsicRatioFromCanvasSize(canvasSizeInPx); + + nsRect area = + mFrame->GetContentRectRelativeToSelf() + ToReferenceFrame(); + nsRect dest = nsLayoutUtils::ComputeObjectDestRect( + area, intrinsicSize, intrinsicRatio, mFrame->StylePosition()); + + LayoutDeviceRect bounds = LayoutDeviceRect::FromAppUnits( + dest, mFrame->PresContext()->AppUnitsPerDevPixel()); + + aManager->CommandBuilder().PushImage( + this, canvasData->GetImageContainer(), aBuilder, aResources, aSc, + bounds, bounds); + break; + } + case CanvasContextType::NoContext: + break; + default: + MOZ_ASSERT_UNREACHABLE("unknown canvas context type"); + } + return true; + } + + // FirstContentfulPaint is supposed to ignore "white" canvases. We use + // MaybeModified (if GetContext() was called on the canvas) as a standin for + // "white" + virtual bool IsContentful() const override { + nsHTMLCanvasFrame* f = static_cast<nsHTMLCanvasFrame*>(Frame()); + HTMLCanvasElement* canvas = HTMLCanvasElement::FromNode(f->GetContent()); + return canvas->MaybeModified(); + } + + virtual void Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) override { + nsHTMLCanvasFrame* f = static_cast<nsHTMLCanvasFrame*>(Frame()); + HTMLCanvasElement* canvas = HTMLCanvasElement::FromNode(f->GetContent()); + + nsRect area = f->GetContentRectRelativeToSelf() + ToReferenceFrame(); + nsIntSize canvasSizeInPx = f->GetCanvasSize(); + + nsPresContext* presContext = f->PresContext(); + canvas->HandlePrintCallback(presContext); + + if (canvasSizeInPx.width <= 0 || canvasSizeInPx.height <= 0 || + area.IsEmpty()) { + return; + } + + IntrinsicSize intrinsicSize = IntrinsicSizeFromCanvasSize(canvasSizeInPx); + AspectRatio intrinsicRatio = IntrinsicRatioFromCanvasSize(canvasSizeInPx); + + nsRect dest = nsLayoutUtils::ComputeObjectDestRect( + area, intrinsicSize, intrinsicRatio, f->StylePosition()); + + gfxContextMatrixAutoSaveRestore saveMatrix(aCtx); + + if (RefPtr<layers::Image> image = canvas->GetAsImage()) { + gfxRect destGFXRect = presContext->AppUnitsToGfxUnits(dest); + + // Transform the canvas into the right place + gfxPoint p = destGFXRect.TopLeft(); + Matrix transform = Matrix::Translation(p.x, p.y); + transform.PreScale(destGFXRect.Width() / canvasSizeInPx.width, + destGFXRect.Height() / canvasSizeInPx.height); + + aCtx->SetMatrix( + gfxUtils::SnapTransformTranslation(aCtx->CurrentMatrix(), nullptr)); + + RefPtr<gfx::SourceSurface> surface = image->GetAsSourceSurface(); + if (!surface || !surface->IsValid()) { + return; + } + gfx::IntSize size = surface->GetSize(); + + transform = gfxUtils::SnapTransform( + transform, gfxRect(0, 0, size.width, size.height), nullptr); + aCtx->Multiply(transform); + + aCtx->GetDrawTarget()->FillRect( + Rect(0, 0, size.width, size.height), + SurfacePattern(surface, ExtendMode::CLAMP, Matrix(), + nsLayoutUtils::GetSamplingFilterForFrame(f))); + return; + } + + if (canvas->IsOffscreen()) { + return; + } + + RefPtr<CanvasRenderer> renderer = new CanvasRenderer(); + if (!canvas->InitializeCanvasRenderer(aBuilder, renderer)) { + return; + } + renderer->FirePreTransactionCallback(); + const auto snapshot = renderer->BorrowSnapshot(); + if (!snapshot) { + return; + } + const auto& surface = snapshot->mSurf; + DrawTarget& dt = *aCtx->GetDrawTarget(); + gfx::Rect destRect = + NSRectToSnappedRect(dest, presContext->AppUnitsPerDevPixel(), dt); + + if (!renderer->YIsDown()) { + // Calculate y-coord that is as far below the bottom of destGFXRect as + // the origin was above the top, then reflect about that. + float y = destRect.Y() + destRect.YMost(); + Matrix transform = Matrix::Translation(0.0f, y).PreScale(1.0f, -1.0f); + aCtx->Multiply(transform); + } + + const auto& srcRect = surface->GetRect(); + dt.DrawSurface( + surface, destRect, + Rect(float(srcRect.X()), float(srcRect.Y()), float(srcRect.Width()), + float(srcRect.Height())), + DrawSurfaceOptions(nsLayoutUtils::GetSamplingFilterForFrame(f))); + + renderer->FireDidTransactionCallback(); + renderer->ResetDirty(); + } +}; + +nsIFrame* NS_NewHTMLCanvasFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) + nsHTMLCanvasFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_QUERYFRAME_HEAD(nsHTMLCanvasFrame) + NS_QUERYFRAME_ENTRY(nsHTMLCanvasFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +NS_IMPL_FRAMEARENA_HELPERS(nsHTMLCanvasFrame) + +void nsHTMLCanvasFrame::Destroy(DestroyContext& aContext) { + if (IsPrimaryFrame()) { + HTMLCanvasElement::FromNode(*mContent)->ResetPrintCallback(); + } + nsContainerFrame::Destroy(aContext); +} + +nsHTMLCanvasFrame::~nsHTMLCanvasFrame() = default; + +nsIntSize nsHTMLCanvasFrame::GetCanvasSize() const { + nsIntSize size(0, 0); + HTMLCanvasElement* canvas = HTMLCanvasElement::FromNodeOrNull(GetContent()); + if (canvas) { + size = canvas->GetSize(); + MOZ_ASSERT(size.width >= 0 && size.height >= 0, + "we should've required <canvas> width/height attrs to be " + "unsigned (non-negative) values"); + } else { + MOZ_ASSERT_UNREACHABLE("couldn't get canvas size"); + } + + return size; +} + +/* virtual */ +nscoord nsHTMLCanvasFrame::GetMinISize(gfxContext* aRenderingContext) { + // XXX The caller doesn't account for constraints of the height, + // min-height, and max-height properties. + nscoord result; + if (Maybe<nscoord> containISize = ContainIntrinsicISize()) { + result = *containISize; + } else { + bool vertical = GetWritingMode().IsVertical(); + result = nsPresContext::CSSPixelsToAppUnits( + vertical ? GetCanvasSize().height : GetCanvasSize().width); + } + DISPLAY_MIN_INLINE_SIZE(this, result); + return result; +} + +/* virtual */ +nscoord nsHTMLCanvasFrame::GetPrefISize(gfxContext* aRenderingContext) { + // XXX The caller doesn't account for constraints of the height, + // min-height, and max-height properties. + nscoord result; + if (Maybe<nscoord> containISize = ContainIntrinsicISize()) { + result = *containISize; + } else { + bool vertical = GetWritingMode().IsVertical(); + result = nsPresContext::CSSPixelsToAppUnits( + vertical ? GetCanvasSize().height : GetCanvasSize().width); + } + DISPLAY_PREF_INLINE_SIZE(this, result); + return result; +} + +/* virtual */ +IntrinsicSize nsHTMLCanvasFrame::GetIntrinsicSize() { + const auto containAxes = GetContainSizeAxes(); + IntrinsicSize size = containAxes.IsBoth() + ? IntrinsicSize(0, 0) + : IntrinsicSizeFromCanvasSize(GetCanvasSize()); + return containAxes.ContainIntrinsicSize(size, *this); +} + +/* virtual */ +AspectRatio nsHTMLCanvasFrame::GetIntrinsicRatio() const { + if (GetContainSizeAxes().IsAny()) { + return AspectRatio(); + } + + return IntrinsicRatioFromCanvasSize(GetCanvasSize()); +} + +/* virtual */ +nsIFrame::SizeComputationResult nsHTMLCanvasFrame::ComputeSize( + gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize, + nscoord aAvailableISize, const LogicalSize& aMargin, + const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides, + ComputeSizeFlags aFlags) { + return {ComputeSizeWithIntrinsicDimensions( + aRenderingContext, aWM, GetIntrinsicSize(), GetAspectRatio(), + aCBSize, aMargin, aBorderPadding, aSizeOverrides, aFlags), + AspectRatioUsage::None}; +} + +void nsHTMLCanvasFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aMetrics, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsHTMLCanvasFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aMetrics, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + NS_FRAME_TRACE( + NS_FRAME_TRACE_CALLS, + ("enter nsHTMLCanvasFrame::Reflow: availSize=%d,%d", + aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight())); + + MOZ_ASSERT(HasAnyStateBits(NS_FRAME_IN_REFLOW), "frame is not in reflow"); + + WritingMode wm = aReflowInput.GetWritingMode(); + const LogicalSize finalSize = aReflowInput.ComputedSizeWithBorderPadding(wm); + + aMetrics.SetSize(wm, finalSize); + aMetrics.SetOverflowAreasToDesiredBounds(); + FinishAndStoreOverflow(&aMetrics); + + // Reflow the single anon block child. + nsReflowStatus childStatus; + nsIFrame* childFrame = mFrames.FirstChild(); + WritingMode childWM = childFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(childWM); + availSize.BSize(childWM) = NS_UNCONSTRAINEDSIZE; + NS_ASSERTION(!childFrame->GetNextSibling(), "HTML canvas should have 1 kid"); + ReflowOutput childDesiredSize(aReflowInput.GetWritingMode()); + ReflowInput childReflowInput(aPresContext, aReflowInput, childFrame, + availSize); + ReflowChild(childFrame, aPresContext, childDesiredSize, childReflowInput, 0, + 0, ReflowChildFlags::Default, childStatus, nullptr); + FinishReflowChild(childFrame, aPresContext, childDesiredSize, + &childReflowInput, 0, 0, ReflowChildFlags::Default); + + NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS, + ("exit nsHTMLCanvasFrame::Reflow: size=%d,%d", + aMetrics.ISize(wm), aMetrics.BSize(wm))); +} + +bool nsHTMLCanvasFrame::UpdateWebRenderCanvasData( + nsDisplayListBuilder* aBuilder, WebRenderCanvasData* aCanvasData) { + HTMLCanvasElement* element = static_cast<HTMLCanvasElement*>(GetContent()); + return element->UpdateWebRenderCanvasData(aBuilder, aCanvasData); +} + +void nsHTMLCanvasFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + if (!IsVisibleForPainting()) return; + + DisplayBorderBackgroundOutline(aBuilder, aLists); + + if (HidesContent()) { + DisplaySelectionOverlay(aBuilder, aLists.Content(), + nsISelectionDisplay::DISPLAY_IMAGES); + return; + } + + uint32_t clipFlags = + nsStyleUtil::ObjectPropsMightCauseOverflow(StylePosition()) + ? 0 + : DisplayListClipState::ASSUME_DRAWING_RESTRICTED_TO_CONTENT_RECT; + + DisplayListClipState::AutoClipContainingBlockDescendantsToContentBox clip( + aBuilder, this, clipFlags); + + aLists.Content()->AppendNewToTop<nsDisplayCanvas>(aBuilder, this); + + DisplaySelectionOverlay(aBuilder, aLists.Content(), + nsISelectionDisplay::DISPLAY_IMAGES); +} + +void nsHTMLCanvasFrame::AppendDirectlyOwnedAnonBoxes( + nsTArray<OwnedAnonBox>& aResult) { + MOZ_ASSERT(mFrames.FirstChild(), "Must have our canvas content anon box"); + MOZ_ASSERT(!mFrames.FirstChild()->GetNextSibling(), + "Must only have our canvas content anon box"); + aResult.AppendElement(OwnedAnonBox(mFrames.FirstChild())); +} + +void nsHTMLCanvasFrame::UnionChildOverflow( + mozilla::OverflowAreas& aOverflowAreas) { + // Our one child (the canvas content anon box) is unpainted and isn't relevant + // for child-overflow purposes. So we need to provide our own trivial impl to + // avoid receiving the child-considering impl that we would otherwise inherit. +} + +#ifdef ACCESSIBILITY +a11y::AccType nsHTMLCanvasFrame::AccessibleType() { + return a11y::eHTMLCanvasType; +} +#endif + +#ifdef DEBUG_FRAME_DUMP +nsresult nsHTMLCanvasFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"HTMLCanvas"_ns, aResult); +} +#endif |