fune/layout/generic/nsImageFrame.cpp
Emilio Cobos Álvarez 7875df041f Bug 1560702 - Improve hit boxes of titlebar buttons. r=stransky,desktop-theme-reviewers,dao
In order to do it, expose the button padding to CSS via env(), and make
the buttons just use the regular drawing.

This slightly changes the padding to the end of the titlebar to match
one half of the inter-button spacing, rather that however much padding
the headerbar has.

We could improve on this slightly by also exposing the headerbar padding
and applying that to the last button, but that's not terribly easy to do
due to us supporting re-ordering of the titlebar buttons, and reversing
their placement, so it'd involve some rather hacky CSS. Not impossible,
but not trivial, and this looks good enough IMO.

Differential Revision: https://phabricator.services.mozilla.com/D202616
2024-02-27 10:28:56 +00:00

2908 lines
104 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/. */
/* rendering object for replaced elements with image data */
#include "nsImageFrame.h"
#include "TextDrawTarget.h"
#include "gfx2DGlue.h"
#include "gfxContext.h"
#include "gfxUtils.h"
#include "mozilla/dom/NameSpaceConstants.h"
#include "mozilla/intl/BidiEmbeddingLevel.h"
#include "mozilla/ComputedStyle.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/Encoding.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/dom/FetchPriority.h"
#include "mozilla/dom/ImageTracker.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/gfx/Helpers.h"
#include "mozilla/gfx/PathHelpers.h"
#include "mozilla/dom/GeneratedImageContent.h"
#include "mozilla/dom/HTMLAreaElement.h"
#include "mozilla/dom/HTMLImageElement.h"
#include "mozilla/dom/ReferrerInfo.h"
#include "mozilla/dom/ResponsiveImageSelector.h"
#include "mozilla/dom/LargestContentfulPaint.h"
#include "mozilla/image/WebRenderImageProvider.h"
#include "mozilla/layers/RenderRootStateManager.h"
#include "mozilla/layers/WebRenderLayerManager.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/PresShell.h"
#include "mozilla/PresShellInlines.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/StaticPrefs_image.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/SVGImageContext.h"
#include "mozilla/Unused.h"
#include "nsCOMPtr.h"
#include "nsFontMetrics.h"
#include "nsIFrameInlines.h"
#include "nsIImageLoadingContent.h"
#include "nsImageLoadingContent.h"
#include "nsImageRenderer.h"
#include "nsObjectLoadingContent.h"
#include "nsString.h"
#include "nsPrintfCString.h"
#include "nsPresContext.h"
#include "nsGkAtoms.h"
#include "mozilla/dom/Document.h"
#include "nsContentUtils.h"
#include "nsCSSAnonBoxes.h"
#include "nsStyleConsts.h"
#include "nsStyleUtil.h"
#include "nsTransform2D.h"
#include "nsImageMap.h"
#include "nsILoadGroup.h"
#include "nsNetUtil.h"
#include "nsNetCID.h"
#include "nsCSSRendering.h"
#include "nsNameSpaceManager.h"
#include <algorithm>
#ifdef ACCESSIBILITY
# include "nsAccessibilityService.h"
#endif
#include "nsLayoutUtils.h"
#include "nsDisplayList.h"
#include "nsIContent.h"
#include "mozilla/dom/Selection.h"
#include "nsIURIMutator.h"
#include "imgIContainer.h"
#include "imgLoader.h"
#include "imgRequestProxy.h"
#include "nsCSSFrameConstructor.h"
#include "nsRange.h"
#include "nsError.h"
#include "nsBidiUtils.h"
#include "nsBidiPresUtils.h"
#include "gfxRect.h"
#include "ImageRegion.h"
#include "ImageContainer.h"
#include "mozilla/ServoStyleSet.h"
#include "nsBlockFrame.h"
#include "nsStyleStructInlines.h"
#include "mozilla/Preferences.h"
#include "mozilla/dom/Link.h"
#include "mozilla/dom/HTMLAnchorElement.h"
#include "mozilla/dom/BrowserChild.h"
using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::gfx;
using namespace mozilla::image;
using namespace mozilla::layers;
using mozilla::layout::TextDrawTarget;
class nsDisplayGradient final : public nsPaintedDisplayItem {
public:
nsDisplayGradient(nsDisplayListBuilder* aBuilder, nsImageFrame* aFrame)
: nsPaintedDisplayItem(aBuilder, aFrame) {
MOZ_COUNT_CTOR(nsDisplayGradient);
}
~nsDisplayGradient() final { MOZ_COUNT_DTOR(nsDisplayGradient); }
nsRect GetBounds(bool* aSnap) const {
*aSnap = true;
return Frame()->GetContentRectRelativeToSelf() + ToReferenceFrame();
}
nsRect GetBounds(nsDisplayListBuilder*, bool* aSnap) const final {
return GetBounds(aSnap);
}
void Paint(nsDisplayListBuilder*, gfxContext* aCtx) final;
bool CreateWebRenderCommands(mozilla::wr::DisplayListBuilder&,
mozilla::wr::IpcResourceUpdateQueue&,
const StackingContextHelper&,
mozilla::layers::RenderRootStateManager*,
nsDisplayListBuilder*) final;
NS_DISPLAY_DECL_NAME("Gradient", TYPE_GRADIENT)
};
void nsDisplayGradient::Paint(nsDisplayListBuilder* aBuilder,
gfxContext* aCtx) {
auto* frame = static_cast<nsImageFrame*>(Frame());
nsImageRenderer imageRenderer(frame, frame->GetImageFromStyle(),
aBuilder->GetImageRendererFlags());
nsSize size = frame->GetSize();
imageRenderer.SetPreferredSize({}, size);
ImgDrawResult result;
if (!imageRenderer.PrepareImage()) {
result = imageRenderer.PrepareResult();
} else {
nsRect dest(ToReferenceFrame(), size);
result = imageRenderer.DrawLayer(
frame->PresContext(), *aCtx, dest, dest, dest.TopLeft(),
GetPaintRect(aBuilder, aCtx), dest.Size(), /* aOpacity = */ 1.0f);
}
Unused << result;
}
bool nsDisplayGradient::CreateWebRenderCommands(
wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources,
const StackingContextHelper& aSc,
mozilla::layers::RenderRootStateManager* aManager,
nsDisplayListBuilder* aDisplayListBuilder) {
auto* frame = static_cast<nsImageFrame*>(Frame());
nsImageRenderer imageRenderer(frame, frame->GetImageFromStyle(),
aDisplayListBuilder->GetImageRendererFlags());
nsSize size = frame->GetSize();
imageRenderer.SetPreferredSize({}, size);
ImgDrawResult result;
if (!imageRenderer.PrepareImage()) {
result = imageRenderer.PrepareResult();
} else {
nsRect dest(ToReferenceFrame(), size);
result = imageRenderer.BuildWebRenderDisplayItemsForLayer(
frame->PresContext(), aBuilder, aResources, aSc, aManager, this, dest,
dest, dest.TopLeft(), dest, dest.Size(),
/* aOpacity = */ 1.0f);
if (result == ImgDrawResult::NOT_SUPPORTED) {
return false;
}
}
return true;
}
// sizes (pixels) for image icon, padding and border frame
#define ICON_SIZE (16)
#define ICON_PADDING (3)
#define ALT_BORDER_WIDTH (1)
// Default alignment value (so we can tell an unset value from a set value)
#define ALIGN_UNSET uint8_t(-1)
class BrokenImageIcon final : public imgINotificationObserver {
// private class that wraps the data and logic needed for
// broken image and loading image icons
public:
explicit BrokenImageIcon(const nsImageFrame& aFrame);
static void Shutdown();
NS_DECL_ISUPPORTS
NS_DECL_IMGINOTIFICATIONOBSERVER
static imgRequestProxy* GetImage(nsImageFrame* aFrame) {
return Get(*aFrame).mImage.get();
}
static void AddObserver(nsImageFrame* aFrame) {
auto& instance = Get(*aFrame);
MOZ_ASSERT(!instance.mObservers.Contains(aFrame),
"Observer shouldn't aleady be in array");
instance.mObservers.AppendElement(aFrame);
}
static void RemoveObserver(nsImageFrame* aFrame) {
auto& instance = Get(*aFrame);
DebugOnly<bool> didRemove = instance.mObservers.RemoveElement(aFrame);
MOZ_ASSERT(didRemove, "Observer not in array");
}
private:
static BrokenImageIcon& Get(const nsImageFrame& aFrame) {
if (!gSingleton) {
gSingleton = new BrokenImageIcon(aFrame);
}
return *gSingleton;
}
~BrokenImageIcon() = default;
nsTObserverArray<nsImageFrame*> mObservers;
RefPtr<imgRequestProxy> mImage;
static StaticRefPtr<BrokenImageIcon> gSingleton;
};
StaticRefPtr<BrokenImageIcon> BrokenImageIcon::gSingleton;
NS_IMPL_ISUPPORTS(BrokenImageIcon, imgINotificationObserver)
BrokenImageIcon::BrokenImageIcon(const nsImageFrame& aFrame) {
constexpr auto brokenSrc = u"resource://gre-resources/broken-image.png"_ns;
nsCOMPtr<nsIURI> realURI;
NS_NewURI(getter_AddRefs(realURI), brokenSrc);
MOZ_ASSERT(realURI, "how?");
if (NS_WARN_IF(!realURI)) {
return;
}
nsPresContext* pc = aFrame.PresContext();
Document* doc = pc->Document();
RefPtr<imgLoader> il = nsContentUtils::GetImgLoaderForDocument(doc);
nsCOMPtr<nsILoadGroup> loadGroup = doc->GetDocumentLoadGroup();
// For icon loads, we don't need to merge with the loadgroup flags
const nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL;
const nsContentPolicyType contentPolicyType =
nsIContentPolicy::TYPE_INTERNAL_IMAGE;
nsresult rv =
il->LoadImage(realURI, /* icon URI */
nullptr, /* initial document URI; this is only
relevant for cookies, so does not
apply to icons. */
nullptr, /* referrer (not relevant for icons) */
nullptr, /* principal (not relevant for icons) */
0, loadGroup, this, nullptr, /* No context */
nullptr, /* Not associated with any particular document */
loadFlags, nullptr, contentPolicyType, u""_ns,
false, /* aUseUrgentStartForChannel */
false, /* aLinkPreload */
0, FetchPriority::Auto, getter_AddRefs(mImage));
Unused << NS_WARN_IF(NS_FAILED(rv));
}
void BrokenImageIcon::Shutdown() {
if (!gSingleton) {
return;
}
if (gSingleton->mImage) {
gSingleton->mImage->CancelAndForgetObserver(NS_ERROR_FAILURE);
gSingleton->mImage = nullptr;
}
gSingleton = nullptr;
}
void BrokenImageIcon::Notify(imgIRequest* aRequest, int32_t aType,
const nsIntRect* aData) {
MOZ_ASSERT(aRequest);
if (aType != imgINotificationObserver::LOAD_COMPLETE &&
aType != imgINotificationObserver::FRAME_UPDATE) {
return;
}
if (aType == imgINotificationObserver::LOAD_COMPLETE) {
nsCOMPtr<imgIContainer> image;
aRequest->GetImage(getter_AddRefs(image));
if (!image) {
return;
}
// Retrieve the image's intrinsic size.
int32_t width = 0;
int32_t height = 0;
image->GetWidth(&width);
image->GetHeight(&height);
// Request a decode at that size.
image->RequestDecodeForSize(IntSize(width, height),
imgIContainer::DECODE_FLAGS_DEFAULT |
imgIContainer::FLAG_HIGH_QUALITY_SCALING);
}
for (nsImageFrame* frame : mObservers.ForwardRange()) {
frame->InvalidateFrame();
}
}
// test if the width and height are fixed, looking at the style data
// This is used by nsImageFrame::ImageFrameTypeFor and should not be used for
// layout decisions.
static bool HaveSpecifiedSize(const nsStylePosition* aStylePosition) {
// check the width and height values in the reflow input's style struct
// - if width and height are specified as either coord or percentage, then
// the size of the image frame is constrained
return aStylePosition->mWidth.IsLengthPercentage() &&
aStylePosition->mHeight.IsLengthPercentage();
}
template <typename SizeOrMaxSize>
static bool DependsOnIntrinsicSize(const SizeOrMaxSize& aMinOrMaxSize) {
auto length = nsIFrame::ToExtremumLength(aMinOrMaxSize);
if (!length) {
return false;
}
switch (*length) {
case nsIFrame::ExtremumLength::MinContent:
case nsIFrame::ExtremumLength::MaxContent:
case nsIFrame::ExtremumLength::FitContent:
case nsIFrame::ExtremumLength::FitContentFunction:
return true;
case nsIFrame::ExtremumLength::MozAvailable:
return false;
}
MOZ_ASSERT_UNREACHABLE("Unknown sizing keyword?");
return false;
}
// Decide whether we can optimize away reflows that result from the
// image's intrinsic size changing.
static bool SizeDependsOnIntrinsicSize(const ReflowInput& aReflowInput) {
const auto& position = *aReflowInput.mStylePosition;
WritingMode wm = aReflowInput.GetWritingMode();
// Don't try to make this optimization when an image has percentages
// in its 'width' or 'height'. The percentages might be treated like
// auto (especially for intrinsic width calculations and for heights).
//
// min-width: min-content and such can also affect our intrinsic size.
// but note that those keywords on the block axis behave like auto, so we
// don't need to check them.
//
// Flex item's min-[width|height]:auto resolution depends on intrinsic size.
return !position.mHeight.ConvertsToLength() ||
!position.mWidth.ConvertsToLength() ||
DependsOnIntrinsicSize(position.MinISize(wm)) ||
DependsOnIntrinsicSize(position.MaxISize(wm)) ||
aReflowInput.mFrame->IsFlexItem();
}
nsIFrame* NS_NewImageFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
return new (aPresShell) nsImageFrame(aStyle, aPresShell->GetPresContext(),
nsImageFrame::Kind::ImageLoadingContent);
}
nsIFrame* NS_NewXULImageFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
return new (aPresShell) nsImageFrame(aStyle, aPresShell->GetPresContext(),
nsImageFrame::Kind::XULImage);
}
nsIFrame* NS_NewImageFrameForContentProperty(PresShell* aPresShell,
ComputedStyle* aStyle) {
return new (aPresShell) nsImageFrame(aStyle, aPresShell->GetPresContext(),
nsImageFrame::Kind::ContentProperty);
}
nsIFrame* NS_NewImageFrameForGeneratedContentIndex(PresShell* aPresShell,
ComputedStyle* aStyle) {
return new (aPresShell)
nsImageFrame(aStyle, aPresShell->GetPresContext(),
nsImageFrame::Kind::ContentPropertyAtIndex);
}
nsIFrame* NS_NewImageFrameForListStyleImage(PresShell* aPresShell,
ComputedStyle* aStyle) {
return new (aPresShell) nsImageFrame(aStyle, aPresShell->GetPresContext(),
nsImageFrame::Kind::ListStyleImage);
}
bool nsImageFrame::ShouldShowBrokenImageIcon() const {
// NOTE(emilio, https://github.com/w3c/csswg-drafts/issues/2832): WebKit and
// Blink behave differently here for content: url(..), for now adapt to
// Blink's behavior.
if (mKind != Kind::ImageLoadingContent) {
return false;
}
if (!StaticPrefs::browser_display_show_image_placeholders()) {
return false;
}
// <img alt=""> is special, and it shouldn't draw the broken image icon,
// unlike the no-alt attribute or non-empty-alt-attribute case.
if (auto* image = HTMLImageElement::FromNode(mContent)) {
const nsAttrValue* alt = image->GetParsedAttr(nsGkAtoms::alt);
if (alt && alt->IsEmptyString()) {
return false;
}
}
// check for broken images. valid null images (eg. img src="") are
// not considered broken because they have no image requests
if (nsCOMPtr<imgIRequest> currentRequest = GetCurrentRequest()) {
uint32_t imageStatus;
return NS_SUCCEEDED(currentRequest->GetImageStatus(&imageStatus)) &&
(imageStatus & imgIRequest::STATUS_ERROR);
}
nsCOMPtr<nsIImageLoadingContent> imageLoader = do_QueryInterface(mContent);
MOZ_ASSERT(imageLoader);
// Show the broken image icon only if we've tried to perform a load at all
// (that is, if we have a current uri).
nsCOMPtr<nsIURI> currentURI = imageLoader->GetCurrentURI();
return !!currentURI;
}
nsImageFrame* nsImageFrame::CreateContinuingFrame(
mozilla::PresShell* aPresShell, ComputedStyle* aStyle) const {
return new (aPresShell)
nsImageFrame(aStyle, aPresShell->GetPresContext(), mKind);
}
NS_IMPL_FRAMEARENA_HELPERS(nsImageFrame)
nsImageFrame::nsImageFrame(ComputedStyle* aStyle, nsPresContext* aPresContext,
ClassID aID, Kind aKind)
: nsAtomicContainerFrame(aStyle, aPresContext, aID),
mIntrinsicSize(0, 0),
mKind(aKind) {
EnableVisibilityTracking();
}
nsImageFrame::~nsImageFrame() = default;
NS_QUERYFRAME_HEAD(nsImageFrame)
NS_QUERYFRAME_ENTRY(nsImageFrame)
NS_QUERYFRAME_TAIL_INHERITING(nsAtomicContainerFrame)
#ifdef ACCESSIBILITY
a11y::AccType nsImageFrame::AccessibleType() {
if (mKind == Kind::ListStyleImage) {
// This is an HTMLListBulletAccessible.
return a11y::eNoType;
}
// Don't use GetImageMap() to avoid reentrancy into accessibility.
if (HasImageMap()) {
return a11y::eHTMLImageMapType;
}
return a11y::eImageType;
}
#endif
void nsImageFrame::DisconnectMap() {
if (!mImageMap) {
return;
}
mImageMap->Destroy();
mImageMap = nullptr;
#ifdef ACCESSIBILITY
if (nsAccessibilityService* accService = GetAccService()) {
accService->RecreateAccessible(PresShell(), mContent);
}
#endif
}
void nsImageFrame::Destroy(DestroyContext& aContext) {
MaybeSendIntrinsicSizeAndRatioToEmbedder(Nothing(), Nothing());
if (mReflowCallbackPosted) {
PresShell()->CancelReflowCallback(this);
mReflowCallbackPosted = false;
}
// Tell our image map, if there is one, to clean up
// This causes the nsImageMap to unregister itself as
// a DOM listener.
DisconnectMap();
MOZ_ASSERT(mListener);
if (mKind == Kind::ImageLoadingContent) {
MOZ_ASSERT(!mOwnedRequest);
MOZ_ASSERT(!mOwnedRequestRegistered);
nsCOMPtr<nsIImageLoadingContent> imageLoader = do_QueryInterface(mContent);
MOZ_ASSERT(imageLoader);
// Notify our image loading content that we are going away so it can
// deregister with our refresh driver.
imageLoader->FrameDestroyed(this);
imageLoader->RemoveNativeObserver(mListener);
} else {
DeinitOwnedRequest();
}
// set the frame to null so we don't send messages to a dead object.
mListener->SetFrame(nullptr);
mListener = nullptr;
// If we were displaying an icon, take ourselves off the list
if (mDisplayingIcon) {
BrokenImageIcon::RemoveObserver(this);
}
nsAtomicContainerFrame::Destroy(aContext);
}
void nsImageFrame::DeinitOwnedRequest() {
MOZ_ASSERT(mKind != Kind::ImageLoadingContent);
if (!mOwnedRequest) {
return;
}
PresContext()->Document()->ImageTracker()->Remove(mOwnedRequest);
nsLayoutUtils::DeregisterImageRequest(PresContext(), mOwnedRequest,
&mOwnedRequestRegistered);
mOwnedRequest->CancelAndForgetObserver(NS_BINDING_ABORTED);
mOwnedRequest = nullptr;
}
void nsImageFrame::DidSetComputedStyle(ComputedStyle* aOldStyle) {
nsAtomicContainerFrame::DidSetComputedStyle(aOldStyle);
// A ::marker's default size is calculated from the font's em-size.
if (IsForMarkerPseudo()) {
mIntrinsicSize = IntrinsicSize(0, 0);
UpdateIntrinsicSize();
}
// Normal "owned" images reframe when `content` or `list-style-image` change,
// but XUL images don't (and we don't really need to). So deal with the
// dynamic list-style-image change in that case.
//
// TODO(emilio): We might want to do the same for regular list-style-image or
// even simple content: url() changes.
if (mKind == Kind::XULImage && aOldStyle) {
if (!mContent->AsElement()->HasNonEmptyAttr(nsGkAtoms::src) &&
aOldStyle->StyleList()->mListStyleImage !=
StyleList()->mListStyleImage) {
UpdateXULImage();
}
// If we have no image our intrinsic size might be themed. We need to
// update the size even if the effective appearance hasn't changed to
// deal correctly with theme changes.
if (!mOwnedRequest) {
UpdateIntrinsicSize();
}
}
// We need to update our orientation either if we had no ComputedStyle before
// because this is the first time it's been set, or if the image-orientation
// property changed from its previous value.
bool shouldUpdateOrientation = false;
nsCOMPtr<imgIRequest> currentRequest = GetCurrentRequest();
const auto newOrientation =
StyleVisibility()->UsedImageOrientation(currentRequest);
if (mImage) {
if (aOldStyle) {
auto oldOrientation =
aOldStyle->StyleVisibility()->UsedImageOrientation(currentRequest);
shouldUpdateOrientation = oldOrientation != newOrientation;
} else {
shouldUpdateOrientation = true;
}
}
if (shouldUpdateOrientation) {
nsCOMPtr<imgIContainer> image(mImage->Unwrap());
mImage = nsLayoutUtils::OrientImage(image, newOrientation);
UpdateIntrinsicSize();
UpdateIntrinsicRatio();
} else if (!aOldStyle || aOldStyle->StylePosition()->mAspectRatio !=
StylePosition()->mAspectRatio) {
UpdateIntrinsicRatio();
}
}
static bool SizeIsAvailable(imgIRequest* aRequest) {
if (!aRequest) {
return false;
}
uint32_t imageStatus = 0;
nsresult rv = aRequest->GetImageStatus(&imageStatus);
return NS_SUCCEEDED(rv) && (imageStatus & imgIRequest::STATUS_SIZE_AVAILABLE);
}
const StyleImage* nsImageFrame::GetImageFromStyle() const {
switch (mKind) {
case Kind::ImageLoadingContent:
break;
case Kind::ListStyleImage:
MOZ_ASSERT(
GetParent()->GetContent()->IsGeneratedContentContainerForMarker());
MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::mozgeneratedcontentimage));
return &StyleList()->mListStyleImage;
case Kind::XULImage:
MOZ_ASSERT(!mContent->AsElement()->HasNonEmptyAttr(nsGkAtoms::src));
return &StyleList()->mListStyleImage;
case Kind::ContentProperty:
case Kind::ContentPropertyAtIndex: {
uint32_t contentIndex = 0;
const nsStyleContent* styleContent = StyleContent();
if (mKind == Kind::ContentPropertyAtIndex) {
MOZ_RELEASE_ASSERT(
mContent->IsHTMLElement(nsGkAtoms::mozgeneratedcontentimage));
contentIndex =
static_cast<GeneratedImageContent*>(mContent.get())->Index();
// TODO(emilio): Consider inheriting the `content` property instead of
// doing this parent traversal?
nsIFrame* parent = GetParent();
MOZ_DIAGNOSTIC_ASSERT(
parent->GetContent()->IsGeneratedContentContainerForMarker() ||
parent->GetContent()->IsGeneratedContentContainerForAfter() ||
parent->GetContent()->IsGeneratedContentContainerForBefore());
nsIFrame* nonAnonymousParent = parent;
while (nonAnonymousParent->Style()->IsAnonBox()) {
nonAnonymousParent = nonAnonymousParent->GetParent();
}
MOZ_DIAGNOSTIC_ASSERT(parent->GetContent() ==
nonAnonymousParent->GetContent());
styleContent = nonAnonymousParent->StyleContent();
}
MOZ_RELEASE_ASSERT(contentIndex < styleContent->ContentCount());
auto& contentItem = styleContent->ContentAt(contentIndex);
MOZ_RELEASE_ASSERT(contentItem.IsImage());
return &contentItem.AsImage();
}
}
MOZ_ASSERT_UNREACHABLE("Don't call me");
return nullptr;
}
void nsImageFrame::UpdateXULImage() {
MOZ_ASSERT(mKind == Kind::XULImage);
DeinitOwnedRequest();
nsAutoString src;
nsPresContext* pc = PresContext();
if (mContent->AsElement()->GetAttr(nsGkAtoms::src, src) && !src.IsEmpty()) {
nsContentPolicyType contentPolicyType;
nsCOMPtr<nsIPrincipal> triggeringPrincipal;
uint64_t requestContextID = 0;
nsContentUtils::GetContentPolicyTypeForUIImageLoading(
mContent, getter_AddRefs(triggeringPrincipal), contentPolicyType,
&requestContextID);
nsCOMPtr<nsIURI> uri;
nsContentUtils::NewURIWithDocumentCharset(
getter_AddRefs(uri), src, pc->Document(), mContent->GetBaseURI());
if (uri) {
auto referrerInfo = MakeRefPtr<ReferrerInfo>(*mContent->AsElement());
nsContentUtils::LoadImage(
uri, mContent, pc->Document(), triggeringPrincipal, requestContextID,
referrerInfo, mListener, nsIRequest::LOAD_NORMAL, u""_ns,
getter_AddRefs(mOwnedRequest), contentPolicyType);
SetupOwnedRequest();
}
} else {
const auto* image = GetImageFromStyle();
if (image->IsImageRequestType()) {
if (imgRequestProxy* proxy = image->GetImageRequest()) {
proxy->Clone(mListener, pc->Document(), getter_AddRefs(mOwnedRequest));
SetupOwnedRequest();
}
}
}
if (!mOwnedRequest) {
UpdateImage(nullptr, nullptr);
}
}
void nsImageFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
nsIFrame* aPrevInFlow) {
MOZ_ASSERT_IF(aPrevInFlow,
aPrevInFlow->Type() == Type() &&
static_cast<nsImageFrame*>(aPrevInFlow)->mKind == mKind);
nsAtomicContainerFrame::Init(aContent, aParent, aPrevInFlow);
mListener = new nsImageListener(this);
GetImageMap(); // Ensure to init the image map asap. This is important to
// make <area> elements focusable.
if (StaticPrefs::layout_image_eager_broken_image_icon()) {
Unused << BrokenImageIcon::GetImage(this);
}
nsPresContext* pc = PresContext();
if (mKind == Kind::ImageLoadingContent) {
nsCOMPtr<nsIImageLoadingContent> imageLoader = do_QueryInterface(aContent);
MOZ_ASSERT(imageLoader);
imageLoader->AddNativeObserver(mListener);
// We have a PresContext now, so we need to notify the image content node
// that it can register images.
imageLoader->FrameCreated(this);
AssertSyncDecodingHintIsInSync();
if (nsIDocShell* docShell = pc->GetDocShell()) {
RefPtr<BrowsingContext> bc = docShell->GetBrowsingContext();
mIsInObjectOrEmbed = bc->IsEmbedderTypeObjectOrEmbed() &&
pc->Document()->IsImageDocument();
}
} else if (mKind == Kind::XULImage) {
UpdateXULImage();
} else {
const StyleImage* image = GetImageFromStyle();
if (image->IsImageRequestType()) {
if (imgRequestProxy* proxy = image->GetImageRequest()) {
proxy->Clone(mListener, pc->Document(), getter_AddRefs(mOwnedRequest));
SetupOwnedRequest();
}
}
}
// Give image loads associated with an image frame a small priority boost.
if (nsCOMPtr<imgIRequest> currentRequest = GetCurrentRequest()) {
uint32_t categoryToBoostPriority = imgIRequest::CATEGORY_FRAME_INIT;
// Increase load priority further if intrinsic size might be important for
// layout.
if (!HaveSpecifiedSize(StylePosition())) {
categoryToBoostPriority |= imgIRequest::CATEGORY_SIZE_QUERY;
}
currentRequest->BoostPriority(categoryToBoostPriority);
}
MaybeSendIntrinsicSizeAndRatioToEmbedder();
}
void nsImageFrame::SetupOwnedRequest() {
MOZ_ASSERT(mKind != Kind::ImageLoadingContent);
if (!mOwnedRequest) {
return;
}
// We're not using AssociateRequestToFrame for the content property, so we
// need to add it to the image tracker manually.
PresContext()->Document()->ImageTracker()->Add(mOwnedRequest);
uint32_t status = 0;
nsresult rv = mOwnedRequest->GetImageStatus(&status);
if (NS_FAILED(rv)) {
return;
}
if (status & imgIRequest::STATUS_SIZE_AVAILABLE) {
nsCOMPtr<imgIContainer> image;
mOwnedRequest->GetImage(getter_AddRefs(image));
OnSizeAvailable(mOwnedRequest, image);
}
if (status & imgIRequest::STATUS_FRAME_COMPLETE) {
mFirstFrameComplete = true;
}
if (status & imgIRequest::STATUS_IS_ANIMATED) {
nsLayoutUtils::RegisterImageRequest(PresContext(), mOwnedRequest,
&mOwnedRequestRegistered);
}
}
static void ScaleIntrinsicSizeForDensity(IntrinsicSize& aSize,
const ImageResolution& aResolution) {
if (aSize.width) {
aResolution.ApplyXTo(aSize.width.ref());
}
if (aSize.height) {
aResolution.ApplyYTo(aSize.height.ref());
}
}
static void ScaleIntrinsicSizeForDensity(imgIContainer* aImage,
nsIContent& aContent,
IntrinsicSize& aSize) {
ImageResolution resolution = aImage->GetResolution();
if (auto* image = HTMLImageElement::FromNode(aContent)) {
if (auto* selector = image->GetResponsiveImageSelector()) {
resolution.ScaleBy(selector->GetSelectedImageDensity());
}
}
ScaleIntrinsicSizeForDensity(aSize, resolution);
}
static nscoord ListImageDefaultLength(const nsImageFrame& aFrame) {
// https://drafts.csswg.org/css-lists-3/#image-markers
// The spec says we should use 1em x 1em, but that seems too large.
// See disussion in https://github.com/w3c/csswg-drafts/issues/4207
auto* pc = aFrame.PresContext();
RefPtr<nsFontMetrics> fm =
nsLayoutUtils::GetFontMetricsForComputedStyle(aFrame.Style(), pc);
RefPtr<gfxFont> font = fm->GetThebesFontGroup()->GetFirstValidFont();
auto emAU =
font->GetMetrics(fm->Orientation()).emHeight * pc->AppUnitsPerDevPixel();
return std::max(NSToCoordRound(0.4f * emAU),
nsPresContext::CSSPixelsToAppUnits(1));
}
static IntrinsicSize ComputeIntrinsicSize(imgIContainer* aImage,
bool aUseMappedRatio,
nsImageFrame::Kind aKind,
const nsImageFrame& aFrame) {
const auto containAxes = aFrame.GetContainSizeAxes();
if (containAxes.IsBoth()) {
return aFrame.FinishIntrinsicSize(containAxes, IntrinsicSize(0, 0));
}
nsSize size;
if (aImage && NS_SUCCEEDED(aImage->GetIntrinsicSize(&size))) {
IntrinsicSize intrinsicSize;
intrinsicSize.width = size.width == -1 ? Nothing() : Some(size.width);
intrinsicSize.height = size.height == -1 ? Nothing() : Some(size.height);
if (aKind == nsImageFrame::Kind::ListStyleImage) {
if (intrinsicSize.width.isNothing() || intrinsicSize.height.isNothing()) {
nscoord defaultLength = ListImageDefaultLength(aFrame);
if (intrinsicSize.width.isNothing()) {
intrinsicSize.width = Some(defaultLength);
}
if (intrinsicSize.height.isNothing()) {
intrinsicSize.height = Some(defaultLength);
}
}
}
if (aKind == nsImageFrame::Kind::ImageLoadingContent ||
(aKind == nsImageFrame::Kind::XULImage &&
aFrame.GetContent()->AsElement()->HasNonEmptyAttr(nsGkAtoms::src))) {
ScaleIntrinsicSizeForDensity(aImage, *aFrame.GetContent(), intrinsicSize);
} else {
ScaleIntrinsicSizeForDensity(
intrinsicSize,
aFrame.GetImageFromStyle()->GetResolution(*aFrame.Style()));
}
return aFrame.FinishIntrinsicSize(containAxes, intrinsicSize);
}
if (aKind == nsImageFrame::Kind::ListStyleImage) {
// Note: images are handled above, this handles gradients etc.
const nscoord defaultLength = ListImageDefaultLength(aFrame);
return aFrame.FinishIntrinsicSize(
containAxes, IntrinsicSize(defaultLength, defaultLength));
}
if (aKind == nsImageFrame::Kind::XULImage && aFrame.IsThemed()) {
nsPresContext* pc = aFrame.PresContext();
// FIXME: const_cast here is a bit evil but IsThemed and so does the same.
const auto widgetSize = pc->Theme()->GetMinimumWidgetSize(
pc, const_cast<nsImageFrame*>(&aFrame),
aFrame.StyleDisplay()->EffectiveAppearance());
const IntrinsicSize intrinsicSize(
LayoutDeviceIntSize::ToAppUnits(widgetSize, pc->AppUnitsPerDevPixel()));
return aFrame.FinishIntrinsicSize(containAxes, intrinsicSize);
}
if (aFrame.ShouldShowBrokenImageIcon()) {
nscoord edgeLengthToUse = nsPresContext::CSSPixelsToAppUnits(
ICON_SIZE + (2 * (ICON_PADDING + ALT_BORDER_WIDTH)));
return aFrame.FinishIntrinsicSize(
containAxes, IntrinsicSize(edgeLengthToUse, edgeLengthToUse));
}
if (aUseMappedRatio && aFrame.StylePosition()->mAspectRatio.HasRatio()) {
return IntrinsicSize();
}
// XXX: No FinishIntrinsicSize?
return IntrinsicSize(0, 0);
}
// For compat reasons, see bug 1602047, we don't use the intrinsic ratio from
// width="" and height="" for images with no src attribute (no request).
//
// But we shouldn't get fooled by <img loading=lazy>. We do want to apply the
// ratio then...
bool nsImageFrame::ShouldUseMappedAspectRatio() const {
if (mKind != Kind::ImageLoadingContent) {
return true;
}
nsCOMPtr<imgIRequest> currentRequest = GetCurrentRequest();
if (currentRequest) {
return true;
}
// TODO(emilio): Investigate the compat situation of the above check, maybe we
// can just check for empty src attribute or something...
auto* image = HTMLImageElement::FromNode(mContent);
return image && image->IsAwaitingLoadOrLazyLoading();
}
bool nsImageFrame::UpdateIntrinsicSize() {
IntrinsicSize oldIntrinsicSize = mIntrinsicSize;
mIntrinsicSize =
ComputeIntrinsicSize(mImage, ShouldUseMappedAspectRatio(), mKind, *this);
return mIntrinsicSize != oldIntrinsicSize;
}
static AspectRatio ComputeIntrinsicRatio(imgIContainer* aImage,
bool aUseMappedRatio,
const nsImageFrame& aFrame) {
if (aFrame.GetContainSizeAxes().IsAny()) {
return AspectRatio();
}
if (aImage) {
if (Maybe<AspectRatio> fromImage = aImage->GetIntrinsicRatio()) {
return *fromImage;
}
}
if (aUseMappedRatio) {
const StyleAspectRatio& ratio = aFrame.StylePosition()->mAspectRatio;
if (ratio.auto_ && ratio.HasRatio()) {
// Return the mapped intrinsic aspect ratio stored in
// nsStylePosition::mAspectRatio.
return ratio.ratio.AsRatio().ToLayoutRatio(UseBoxSizing::Yes);
}
}
if (aFrame.ShouldShowBrokenImageIcon()) {
return AspectRatio(1.0f);
}
return AspectRatio();
}
bool nsImageFrame::UpdateIntrinsicRatio() {
AspectRatio oldIntrinsicRatio = mIntrinsicRatio;
mIntrinsicRatio =
ComputeIntrinsicRatio(mImage, ShouldUseMappedAspectRatio(), *this);
return mIntrinsicRatio != oldIntrinsicRatio;
}
bool nsImageFrame::GetSourceToDestTransform(nsTransform2D& aTransform) {
// First, figure out destRect (the rect we're rendering into).
// NOTE: We use mComputedSize instead of just GetContentRectRelativeToSelf()'s
// own size here, because GetContentRectRelativeToSelf() might be smaller if
// we're fragmented, whereas mComputedSize has our full content-box size
// (which we need for ComputeObjectDestRect to work correctly).
nsRect constraintRect(GetContentRectRelativeToSelf().TopLeft(),
mComputedSize);
constraintRect.y -= GetContinuationOffset();
nsRect destRect = nsLayoutUtils::ComputeObjectDestRect(
constraintRect, mIntrinsicSize, mIntrinsicRatio, StylePosition());
// Set the translation components, based on destRect
// XXXbz does this introduce rounding errors because of the cast to
// float? Should we just manually add that stuff in every time
// instead?
aTransform.SetToTranslate(float(destRect.x), float(destRect.y));
// NOTE(emilio): This intrinsicSize is not the same as the layout intrinsic
// size (mIntrinsicSize), which can be scaled due to ResponsiveImageSelector,
// see ScaleIntrinsicSizeForDensity.
nsSize intrinsicSize;
if (!mImage || !NS_SUCCEEDED(mImage->GetIntrinsicSize(&intrinsicSize)) ||
intrinsicSize.IsEmpty()) {
return false;
}
aTransform.SetScale(float(destRect.width) / float(intrinsicSize.width),
float(destRect.height) / float(intrinsicSize.height));
return true;
}
// This function checks whether the given request is the current request for our
// mContent.
bool nsImageFrame::IsPendingLoad(imgIRequest* aRequest) const {
// Default to pending load in case of errors
if (mKind != Kind::ImageLoadingContent) {
MOZ_ASSERT(aRequest == mOwnedRequest);
return false;
}
nsCOMPtr<nsIImageLoadingContent> imageLoader(do_QueryInterface(mContent));
MOZ_ASSERT(imageLoader);
int32_t requestType = nsIImageLoadingContent::UNKNOWN_REQUEST;
imageLoader->GetRequestType(aRequest, &requestType);
return requestType != nsIImageLoadingContent::CURRENT_REQUEST;
}
nsRect nsImageFrame::SourceRectToDest(const nsIntRect& aRect) {
// When scaling the image, row N of the source image may (depending on
// the scaling function) be used to draw any row in the destination image
// between floor(F * (N-1)) and ceil(F * (N+1)), where F is the
// floating-point scaling factor. The same holds true for columns.
// So, we start by computing that bound without the floor and ceiling.
nsRect r(nsPresContext::CSSPixelsToAppUnits(aRect.x - 1),
nsPresContext::CSSPixelsToAppUnits(aRect.y - 1),
nsPresContext::CSSPixelsToAppUnits(aRect.width + 2),
nsPresContext::CSSPixelsToAppUnits(aRect.height + 2));
nsTransform2D sourceToDest;
if (!GetSourceToDestTransform(sourceToDest)) {
// Failed to generate transform matrix. Return our whole content area,
// to be on the safe side (since this method is used for generating
// invalidation rects).
return GetContentRectRelativeToSelf();
}
sourceToDest.TransformCoord(&r.x, &r.y, &r.width, &r.height);
// Now, round the edges out to the pixel boundary.
nscoord scale = nsPresContext::CSSPixelsToAppUnits(1);
nscoord right = r.x + r.width;
nscoord bottom = r.y + r.height;
r.x -= (scale + (r.x % scale)) % scale;
r.y -= (scale + (r.y % scale)) % scale;
r.width = right + ((scale - (right % scale)) % scale) - r.x;
r.height = bottom + ((scale - (bottom % scale)) % scale) - r.y;
return r;
}
static bool ImageOk(ElementState aState) {
return !aState.HasState(ElementState::BROKEN);
}
static bool HasAltText(const Element& aElement) {
// We always return some alternate text for <input>, see
// nsCSSFrameConstructor::GetAlternateTextFor.
if (aElement.IsHTMLElement(nsGkAtoms::input)) {
return true;
}
MOZ_ASSERT(aElement.IsHTMLElement(nsGkAtoms::img));
return aElement.HasNonEmptyAttr(nsGkAtoms::alt);
}
bool nsImageFrame::ShouldCreateImageFrameForContentProperty(
const Element& aElement, const ComputedStyle& aStyle) {
if (aElement.IsRootOfNativeAnonymousSubtree()) {
return false;
}
const auto& content = aStyle.StyleContent()->mContent;
if (!content.IsItems()) {
return false;
}
Span<const StyleContentItem> items = content.AsItems().AsSpan();
return items.Length() == 1 && items[0].IsImage();
}
// Check if we want to use an image frame or just let the frame constructor make
// us into an inline, and if so, which kind of image frame should we create.
/* static */
auto nsImageFrame::ImageFrameTypeFor(const Element& aElement,
const ComputedStyle& aStyle)
-> ImageFrameType {
if (ShouldCreateImageFrameForContentProperty(aElement, aStyle)) {
// Prefer the content property, for compat reasons, see bug 1484928.
return ImageFrameType::ForContentProperty;
}
if (ImageOk(aElement.State())) {
// Image is fine or loading; do the image frame thing
return ImageFrameType::ForElementRequest;
}
if (aStyle.StyleUIReset()->mMozForceBrokenImageIcon) {
return ImageFrameType::ForElementRequest;
}
if (!HasAltText(aElement)) {
return ImageFrameType::ForElementRequest;
}
// FIXME(emilio, bug 1788767): We definitely don't reframe when
// HaveSpecifiedSize changes...
if (aElement.OwnerDoc()->GetCompatibilityMode() == eCompatibility_NavQuirks &&
HaveSpecifiedSize(aStyle.StylePosition())) {
return ImageFrameType::ForElementRequest;
}
return ImageFrameType::None;
}
void nsImageFrame::Notify(imgIRequest* aRequest, int32_t aType,
const nsIntRect* aRect) {
if (aType == imgINotificationObserver::SIZE_AVAILABLE) {
nsCOMPtr<imgIContainer> image;
aRequest->GetImage(getter_AddRefs(image));
return OnSizeAvailable(aRequest, image);
}
if (aType == imgINotificationObserver::FRAME_UPDATE) {
return OnFrameUpdate(aRequest, aRect);
}
if (aType == imgINotificationObserver::FRAME_COMPLETE) {
mFirstFrameComplete = true;
}
if (aType == imgINotificationObserver::IS_ANIMATED &&
mKind != Kind::ImageLoadingContent) {
nsLayoutUtils::RegisterImageRequest(PresContext(), mOwnedRequest,
&mOwnedRequestRegistered);
}
if (aType == imgINotificationObserver::LOAD_COMPLETE) {
LargestContentfulPaint::MaybeProcessImageForElementTiming(
static_cast<imgRequestProxy*>(aRequest), GetContent()->AsElement());
uint32_t imgStatus;
aRequest->GetImageStatus(&imgStatus);
nsresult status =
imgStatus & imgIRequest::STATUS_ERROR ? NS_ERROR_FAILURE : NS_OK;
return OnLoadComplete(aRequest, status);
}
}
void nsImageFrame::OnSizeAvailable(imgIRequest* aRequest,
imgIContainer* aImage) {
if (!aImage) {
return;
}
/* Get requested animation policy from the pres context:
* normal = 0
* one frame = 1
* one loop = 2
*/
aImage->SetAnimationMode(PresContext()->ImageAnimationMode());
if (IsPendingLoad(aRequest)) {
// We don't care
return;
}
UpdateImage(aRequest, aImage);
}
void nsImageFrame::UpdateImage(imgIRequest* aRequest, imgIContainer* aImage) {
if (SizeIsAvailable(aRequest)) {
StyleImageOrientation orientation =
StyleVisibility()->UsedImageOrientation(aRequest);
// This is valid and for the current request, so update our stored image
// container, orienting according to our style.
mImage = nsLayoutUtils::OrientImage(aImage, orientation);
MOZ_ASSERT(mImage);
} else {
// We no longer have a valid image, so release our stored image container.
mImage = mPrevImage = nullptr;
if (mKind == Kind::ListStyleImage) {
auto* genContent = static_cast<GeneratedImageContent*>(GetContent());
genContent->NotifyLoadFailed();
// No need to continue below since the above state change will destroy
// this frame.
return;
}
}
UpdateIntrinsicSizeAndRatio();
if (!GotInitialReflow()) {
return;
}
// We're going to need to repaint now either way.
InvalidateFrame();
}
void nsImageFrame::OnFrameUpdate(imgIRequest* aRequest,
const nsIntRect* aRect) {
if (NS_WARN_IF(!aRect)) {
return;
}
if (!GotInitialReflow()) {
// Don't bother to do anything; we have a reflow coming up!
return;
}
if (mFirstFrameComplete && !StyleVisibility()->IsVisible()) {
return;
}
if (IsPendingLoad(aRequest)) {
// We don't care
return;
}
nsIntRect layerInvalidRect =
mImage ? mImage->GetImageSpaceInvalidationRect(*aRect) : *aRect;
if (layerInvalidRect.IsEqualInterior(GetMaxSizedIntRect())) {
// Invalidate our entire area.
InvalidateSelf(nullptr, nullptr);
return;
}
nsRect frameInvalidRect = SourceRectToDest(layerInvalidRect);
InvalidateSelf(&layerInvalidRect, &frameInvalidRect);
}
void nsImageFrame::InvalidateSelf(const nsIntRect* aLayerInvalidRect,
const nsRect* aFrameInvalidRect) {
// Check if WebRender has interacted with this frame. If it has
// we need to let it know that things have changed.
const auto type = DisplayItemType::TYPE_IMAGE;
const auto providerId = mImage ? mImage->GetProviderId() : 0;
if (WebRenderUserData::ProcessInvalidateForImage(this, type, providerId)) {
return;
}
InvalidateLayer(type, aLayerInvalidRect, aFrameInvalidRect);
if (!mFirstFrameComplete) {
InvalidateLayer(DisplayItemType::TYPE_ALT_FEEDBACK, aLayerInvalidRect,
aFrameInvalidRect);
}
}
void nsImageFrame::MaybeSendIntrinsicSizeAndRatioToEmbedder() {
MaybeSendIntrinsicSizeAndRatioToEmbedder(Some(GetIntrinsicSize()),
Some(GetAspectRatio()));
}
void nsImageFrame::MaybeSendIntrinsicSizeAndRatioToEmbedder(
Maybe<IntrinsicSize> aIntrinsicSize, Maybe<AspectRatio> aIntrinsicRatio) {
if (!mIsInObjectOrEmbed || !mImage) {
return;
}
nsCOMPtr<nsIDocShell> docShell = PresContext()->GetDocShell();
if (!docShell) {
return;
}
BrowsingContext* bc = docShell->GetBrowsingContext();
if (!bc) {
return;
}
MOZ_ASSERT(bc->IsContentSubframe());
if (bc->GetParent()->IsInProcess()) {
if (Element* embedder = bc->GetEmbedderElement()) {
if (nsCOMPtr<nsIObjectLoadingContent> olc = do_QueryInterface(embedder)) {
static_cast<nsObjectLoadingContent*>(olc.get())
->SubdocumentIntrinsicSizeOrRatioChanged(aIntrinsicSize,
aIntrinsicRatio);
} else {
MOZ_ASSERT_UNREACHABLE("Got out of sync?");
}
return;
}
}
if (BrowserChild* browserChild = BrowserChild::GetFrom(docShell)) {
Unused << browserChild->SendIntrinsicSizeOrRatioChanged(aIntrinsicSize,
aIntrinsicRatio);
}
}
void nsImageFrame::OnLoadComplete(imgIRequest* aRequest, nsresult aStatus) {
NotifyNewCurrentRequest(aRequest, aStatus);
}
void nsImageFrame::ElementStateChanged(ElementState aStates) {
if (!(aStates & ElementState::BROKEN)) {
return;
}
if (mKind != Kind::ImageLoadingContent) {
return;
}
if (!ImageOk(mContent->AsElement()->State())) {
UpdateImage(nullptr, nullptr);
}
}
void nsImageFrame::ResponsiveContentDensityChanged() {
UpdateIntrinsicSizeAndRatio();
}
void nsImageFrame::UpdateIntrinsicSizeAndRatio() {
bool intrinsicSizeOrRatioChanged = [&] {
// NOTE(emilio): We intentionally want to call both functions and avoid
// short-circuiting.
bool intrinsicSizeChanged = UpdateIntrinsicSize();
bool intrinsicRatioChanged = UpdateIntrinsicRatio();
return intrinsicSizeChanged || intrinsicRatioChanged;
}();
if (!intrinsicSizeOrRatioChanged) {
return;
}
// Our aspect-ratio property value changed, and an embedding <object> or
// <embed> might care about that.
MaybeSendIntrinsicSizeAndRatioToEmbedder();
if (!GotInitialReflow()) {
return;
}
// Now we need to reflow if we have an unconstrained size and have
// already gotten the initial reflow.
if (!HasAnyStateBits(IMAGE_SIZECONSTRAINED)) {
PresShell()->FrameNeedsReflow(
this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
} else if (PresShell()->IsActive()) {
// We've already gotten the initial reflow, and our size hasn't changed,
// so we're ready to request a decode.
MaybeDecodeForPredictedSize();
}
}
void nsImageFrame::NotifyNewCurrentRequest(imgIRequest* aRequest,
nsresult aStatus) {
nsCOMPtr<imgIContainer> image;
aRequest->GetImage(getter_AddRefs(image));
NS_ASSERTION(image || NS_FAILED(aStatus),
"Successful load with no container?");
UpdateImage(aRequest, image);
}
void nsImageFrame::MaybeDecodeForPredictedSize() {
// Check that we're ready to decode.
if (!mImage) {
return; // Nothing to do yet.
}
if (mComputedSize.IsEmpty()) {
return; // We won't draw anything, so no point in decoding.
}
if (GetVisibility() != Visibility::ApproximatelyVisible) {
return; // We're not visible, so don't decode.
}
// OK, we're ready to decode. Compute the scale to the screen...
mozilla::PresShell* presShell = PresShell();
MatrixScales scale =
ScaleFactor<UnknownUnits, UnknownUnits>(
presShell->GetCumulativeResolution()) *
nsLayoutUtils::GetTransformToAncestorScaleExcludingAnimated(this);
auto resolutionToScreen = ViewAs<LayoutDeviceToScreenScale2D>(scale);
// If we are in a remote browser, then apply scaling from ancestor browsers
if (BrowserChild* browserChild = BrowserChild::GetFrom(presShell)) {
resolutionToScreen =
resolutionToScreen * ViewAs<ScreenToScreenScale2D>(
browserChild->GetEffectsInfo().mRasterScale);
}
// ...and this frame's content box...
const nsPoint offset =
GetOffsetToCrossDoc(nsLayoutUtils::GetReferenceFrame(this));
const nsRect frameContentBox = GetContentRectRelativeToSelf() + offset;
// ...and our predicted dest rect...
const int32_t factor = PresContext()->AppUnitsPerDevPixel();
const LayoutDeviceRect destRect = LayoutDeviceRect::FromAppUnits(
PredictedDestRect(frameContentBox), factor);
// ...and use them to compute our predicted size in screen pixels.
const ScreenSize predictedScreenSize = destRect.Size() * resolutionToScreen;
const ScreenIntSize predictedScreenIntSize =
RoundedToInt(predictedScreenSize);
if (predictedScreenIntSize.IsEmpty()) {
return;
}
// Determine the optimal image size to use.
uint32_t flags = imgIContainer::FLAG_HIGH_QUALITY_SCALING |
imgIContainer::FLAG_ASYNC_NOTIFY;
SamplingFilter samplingFilter =
nsLayoutUtils::GetSamplingFilterForFrame(this);
gfxSize gfxPredictedScreenSize =
gfxSize(predictedScreenIntSize.width, predictedScreenIntSize.height);
nsIntSize predictedImageSize = mImage->OptimalImageSizeForDest(
gfxPredictedScreenSize, imgIContainer::FRAME_CURRENT, samplingFilter,
flags);
// Request a decode.
mImage->RequestDecodeForSize(predictedImageSize, flags);
}
nsRect nsImageFrame::PredictedDestRect(const nsRect& aFrameContentBox) {
// Note: To get the "dest rect", we have to provide the "constraint rect"
// (which is the content-box, with the effects of fragmentation undone).
nsRect constraintRect(aFrameContentBox.TopLeft(), mComputedSize);
constraintRect.y -= GetContinuationOffset();
return nsLayoutUtils::ComputeObjectDestRect(constraintRect, mIntrinsicSize,
mIntrinsicRatio, StylePosition());
}
bool nsImageFrame::IsForMarkerPseudo() const {
if (mKind == Kind::ImageLoadingContent) {
return false;
}
auto* subtreeRoot = GetContent()->GetClosestNativeAnonymousSubtreeRoot();
return subtreeRoot && subtreeRoot->IsGeneratedContentContainerForMarker();
}
void nsImageFrame::EnsureIntrinsicSizeAndRatio() {
const auto containAxes = GetContainSizeAxes();
if (containAxes.IsBoth()) {
// If we have 'contain:size', then we have no intrinsic aspect ratio,
// and the intrinsic size is determined by contain-intrinsic-size,
// regardless of what our underlying image may think.
mIntrinsicSize = FinishIntrinsicSize(containAxes, IntrinsicSize(0, 0));
mIntrinsicRatio = AspectRatio();
return;
}
// If mIntrinsicSize.width and height are 0, then we need to update from the
// image container. Note that we handle ::marker intrinsic size/ratio in
// DidSetComputedStyle.
if (mIntrinsicSize != IntrinsicSize(0, 0) && !IsForMarkerPseudo()) {
return;
}
bool intrinsicSizeOrRatioChanged = UpdateIntrinsicSize();
intrinsicSizeOrRatioChanged =
UpdateIntrinsicRatio() || intrinsicSizeOrRatioChanged;
if (intrinsicSizeOrRatioChanged) {
// Our aspect-ratio property value changed, and an embedding <object> or
// <embed> might care about that.
MaybeSendIntrinsicSizeAndRatioToEmbedder();
}
}
nsIFrame::SizeComputationResult nsImageFrame::ComputeSize(
gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize,
nscoord aAvailableISize, const LogicalSize& aMargin,
const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides,
ComputeSizeFlags aFlags) {
EnsureIntrinsicSizeAndRatio();
return {ComputeSizeWithIntrinsicDimensions(
aRenderingContext, aWM, mIntrinsicSize, GetAspectRatio(), aCBSize,
aMargin, aBorderPadding, aSizeOverrides, aFlags),
AspectRatioUsage::None};
}
Element* nsImageFrame::GetMapElement() const {
return IsForImageLoadingContent()
? nsImageLoadingContent::FindImageMap(mContent->AsElement())
: nullptr;
}
// get the offset into the content area of the image where aImg starts if it is
// a continuation.
nscoord nsImageFrame::GetContinuationOffset() const {
nscoord offset = 0;
for (nsIFrame* f = GetPrevInFlow(); f; f = f->GetPrevInFlow()) {
offset += f->GetContentRect().height;
}
NS_ASSERTION(offset >= 0, "bogus GetContentRect");
return offset;
}
nscoord nsImageFrame::GetMinISize(gfxContext* aRenderingContext) {
// XXX The caller doesn't account for constraints of the block-size,
// min-block-size, and max-block-size properties.
DebugOnly<nscoord> result;
DISPLAY_MIN_INLINE_SIZE(this, result);
EnsureIntrinsicSizeAndRatio();
const auto& iSize = GetWritingMode().IsVertical() ? mIntrinsicSize.height
: mIntrinsicSize.width;
return iSize.valueOr(0);
}
nscoord nsImageFrame::GetPrefISize(gfxContext* aRenderingContext) {
// XXX The caller doesn't account for constraints of the block-size,
// min-block-size, and max-block-size properties.
DebugOnly<nscoord> result;
DISPLAY_PREF_INLINE_SIZE(this, result);
EnsureIntrinsicSizeAndRatio();
const auto& iSize = GetWritingMode().IsVertical() ? mIntrinsicSize.height
: mIntrinsicSize.width;
// convert from normal twips to scaled twips (printing...)
return iSize.valueOr(0);
}
void nsImageFrame::ReflowChildren(nsPresContext* aPresContext,
const ReflowInput& aReflowInput,
const LogicalSize& aImageSize) {
for (nsIFrame* child : mFrames) {
ReflowOutput childDesiredSize(aReflowInput);
WritingMode wm = GetWritingMode();
// Shouldn't be hard to support if we want, but why bother.
MOZ_ASSERT(
wm == child->GetWritingMode(),
"We don't expect mismatched writing-modes in content we control");
nsReflowStatus childStatus;
LogicalPoint childOffset(wm);
ReflowInput childReflowInput(aPresContext, aReflowInput, child, aImageSize);
const nsSize containerSize = aImageSize.GetPhysicalSize(wm);
ReflowChild(child, aPresContext, childDesiredSize, childReflowInput, wm,
childOffset, containerSize, ReflowChildFlags::Default,
childStatus);
FinishReflowChild(child, aPresContext, childDesiredSize, &childReflowInput,
wm, childOffset, containerSize,
ReflowChildFlags::Default);
}
}
void nsImageFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
MarkInReflow();
DO_GLOBAL_REFLOW_COUNT("nsImageFrame");
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 nsImageFrame::Reflow: availSize=%d,%d",
aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight()));
MOZ_ASSERT(HasAnyStateBits(NS_FRAME_IN_REFLOW), "frame is not in reflow");
// see if we have a frozen size (i.e. a fixed width and height)
if (!SizeDependsOnIntrinsicSize(aReflowInput)) {
AddStateBits(IMAGE_SIZECONSTRAINED);
} else {
RemoveStateBits(IMAGE_SIZECONSTRAINED);
}
mComputedSize = aReflowInput.ComputedPhysicalSize();
const auto wm = GetWritingMode();
aMetrics.SetSize(wm, aReflowInput.ComputedSizeWithBorderPadding(wm));
if (GetPrevInFlow()) {
aMetrics.Width() = GetPrevInFlow()->GetSize().width;
nscoord y = GetContinuationOffset();
aMetrics.Height() -= y + aReflowInput.ComputedPhysicalBorderPadding().top;
aMetrics.Height() = std::max(0, aMetrics.Height());
}
// we have to split images if we are:
// in Paginated mode, we need to have a constrained height, and have a height
// larger than our available height
uint32_t loadStatus = imgIRequest::STATUS_NONE;
if (nsCOMPtr<imgIRequest> currentRequest = GetCurrentRequest()) {
currentRequest->GetImageStatus(&loadStatus);
}
if (aPresContext->IsPaginated() &&
((loadStatus & imgIRequest::STATUS_SIZE_AVAILABLE) ||
HasAnyStateBits(IMAGE_SIZECONSTRAINED)) &&
NS_UNCONSTRAINEDSIZE != aReflowInput.AvailableHeight() &&
aMetrics.Height() > aReflowInput.AvailableHeight()) {
// our desired height was greater than 0, so to avoid infinite
// splitting, use 1 pixel as the min
aMetrics.Height() = std::max(nsPresContext::CSSPixelsToAppUnits(1),
aReflowInput.AvailableHeight());
aStatus.SetIncomplete();
}
aMetrics.SetOverflowAreasToDesiredBounds();
bool imageOK = mKind != Kind::ImageLoadingContent ||
ImageOk(mContent->AsElement()->State());
// Determine if the size is available
bool haveSize = false;
if (loadStatus & imgIRequest::STATUS_SIZE_AVAILABLE) {
haveSize = true;
}
if (!imageOK || !haveSize) {
nsRect altFeedbackSize(
0, 0,
nsPresContext::CSSPixelsToAppUnits(
ICON_SIZE + 2 * (ICON_PADDING + ALT_BORDER_WIDTH)),
nsPresContext::CSSPixelsToAppUnits(
ICON_SIZE + 2 * (ICON_PADDING + ALT_BORDER_WIDTH)));
// We include the altFeedbackSize in our ink overflow, but not in our
// scrollable overflow, since it doesn't really need to be scrolled to
// outside the image.
nsRect& inkOverflow = aMetrics.InkOverflow();
inkOverflow.UnionRect(inkOverflow, altFeedbackSize);
} else if (PresShell()->IsActive()) {
// We've just reflowed and we should have an accurate size, so we're ready
// to request a decode.
MaybeDecodeForPredictedSize();
}
FinishAndStoreOverflow(&aMetrics, aReflowInput.mStyleDisplay);
// Reflow the child frames. Our children can't affect our size in any way.
ReflowChildren(aPresContext, aReflowInput, aMetrics.Size(GetWritingMode()));
if (HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && !mReflowCallbackPosted) {
mReflowCallbackPosted = true;
PresShell()->PostReflowCallback(this);
}
NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS, ("exit nsImageFrame::Reflow: size=%d,%d",
aMetrics.Width(), aMetrics.Height()));
}
bool nsImageFrame::ReflowFinished() {
mReflowCallbackPosted = false;
// XXX(seth): We don't need this. The purpose of updating visibility
// synchronously is to ensure that animated images start animating
// immediately. In the short term, however,
// nsImageLoadingContent::OnUnlockedDraw() is enough to ensure that
// animations start as soon as the image is painted for the first time, and in
// the long term we want to update visibility information from the display
// list whenever we paint, so we don't actually need to do this. However, to
// avoid behavior changes during the transition from the old image visibility
// code, we'll leave it in for now.
UpdateVisibilitySynchronously();
return false;
}
void nsImageFrame::ReflowCallbackCanceled() { mReflowCallbackPosted = false; }
// Computes the width of the specified string. aMaxWidth specifies the maximum
// width available. Once this limit is reached no more characters are measured.
// The number of characters that fit within the maximum width are returned in
// aMaxFit. NOTE: it is assumed that the fontmetrics have already been selected
// into the rendering context before this is called (for performance). MMP
nscoord nsImageFrame::MeasureString(const char16_t* aString, int32_t aLength,
nscoord aMaxWidth, uint32_t& aMaxFit,
gfxContext& aContext,
nsFontMetrics& aFontMetrics) {
nscoord totalWidth = 0;
aFontMetrics.SetTextRunRTL(false);
nscoord spaceWidth = aFontMetrics.SpaceWidth();
aMaxFit = 0;
while (aLength > 0) {
// Find the next place we can line break
uint32_t len = aLength;
bool trailingSpace = false;
for (int32_t i = 0; i < aLength; i++) {
if (dom::IsSpaceCharacter(aString[i]) && (i > 0)) {
len = i; // don't include the space when measuring
trailingSpace = true;
break;
}
}
// Measure this chunk of text, and see if it fits
nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi(
aString, len, this, aFontMetrics, aContext);
bool fits = (totalWidth + width) <= aMaxWidth;
// If it fits on the line, or it's the first word we've processed then
// include it
if (fits || (0 == totalWidth)) {
// New piece fits
totalWidth += width;
// If there's a trailing space then see if it fits as well
if (trailingSpace) {
if ((totalWidth + spaceWidth) <= aMaxWidth) {
totalWidth += spaceWidth;
} else {
// Space won't fit. Leave it at the end but don't include it in
// the width
fits = false;
}
len++;
}
aMaxFit += len;
aString += len;
aLength -= len;
}
if (!fits) {
break;
}
}
return totalWidth;
}
// Formats the alt-text to fit within the specified rectangle. Breaks lines
// between words if a word would extend past the edge of the rectangle
void nsImageFrame::DisplayAltText(nsPresContext* aPresContext,
gfxContext& aRenderingContext,
const nsString& aAltText,
const nsRect& aRect) {
// Set font and color
aRenderingContext.SetColor(
sRGBColor::FromABGR(StyleText()->mColor.ToColor()));
RefPtr<nsFontMetrics> fm =
nsLayoutUtils::GetInflatedFontMetricsForFrame(this);
// Format the text to display within the formatting rect
nscoord maxAscent = fm->MaxAscent();
nscoord maxDescent = fm->MaxDescent();
nscoord lineHeight = fm->MaxHeight(); // line-relative, so an x-coordinate
// length if writing mode is vertical
WritingMode wm = GetWritingMode();
bool isVertical = wm.IsVertical();
fm->SetVertical(isVertical);
fm->SetTextOrientation(StyleVisibility()->mTextOrientation);
// XXX It would be nice if there was a way to have the font metrics tell
// use where to break the text given a maximum width. At a minimum we need
// to be able to get the break character...
const char16_t* str = aAltText.get();
int32_t strLen = aAltText.Length();
nsPoint pt = wm.IsVerticalRL() ? aRect.TopRight() - nsPoint(lineHeight, 0)
: aRect.TopLeft();
nscoord iSize = isVertical ? aRect.height : aRect.width;
if (!aPresContext->BidiEnabled() && HasRTLChars(aAltText)) {
aPresContext->SetBidiEnabled();
}
// Always show the first line, even if we have to clip it below
bool firstLine = true;
while (strLen > 0) {
if (!firstLine) {
// If we've run out of space, break out of the loop
if ((!isVertical && (pt.y + maxDescent) >= aRect.YMost()) ||
(wm.IsVerticalRL() && (pt.x + maxDescent < aRect.x)) ||
(wm.IsVerticalLR() && (pt.x + maxDescent >= aRect.XMost()))) {
break;
}
}
// Determine how much of the text to display on this line
uint32_t maxFit; // number of characters that fit
nscoord strWidth =
MeasureString(str, strLen, iSize, maxFit, aRenderingContext, *fm);
// Display the text
nsresult rv = NS_ERROR_FAILURE;
if (aPresContext->BidiEnabled()) {
mozilla::intl::BidiEmbeddingLevel level;
nscoord x, y;
if (isVertical) {
x = pt.x + maxDescent;
if (wm.IsBidiLTR()) {
y = aRect.y;
level = mozilla::intl::BidiEmbeddingLevel::LTR();
} else {
y = aRect.YMost() - strWidth;
level = mozilla::intl::BidiEmbeddingLevel::RTL();
}
} else {
y = pt.y + maxAscent;
if (wm.IsBidiLTR()) {
x = aRect.x;
level = mozilla::intl::BidiEmbeddingLevel::LTR();
} else {
x = aRect.XMost() - strWidth;
level = mozilla::intl::BidiEmbeddingLevel::RTL();
}
}
rv = nsBidiPresUtils::RenderText(
str, maxFit, level, aPresContext, aRenderingContext,
aRenderingContext.GetDrawTarget(), *fm, x, y);
}
if (NS_FAILED(rv)) {
nsLayoutUtils::DrawUniDirString(str, maxFit,
isVertical
? nsPoint(pt.x + maxDescent, pt.y)
: nsPoint(pt.x, pt.y + maxAscent),
*fm, aRenderingContext);
}
// Move to the next line
str += maxFit;
strLen -= maxFit;
if (wm.IsVerticalRL()) {
pt.x -= lineHeight;
} else if (wm.IsVerticalLR()) {
pt.x += lineHeight;
} else {
pt.y += lineHeight;
}
firstLine = false;
}
}
struct nsRecessedBorder : public nsStyleBorder {
explicit nsRecessedBorder(nscoord aBorderWidth) {
for (const auto side : AllPhysicalSides()) {
BorderColorFor(side) = StyleColor::Black();
mBorder.Side(side) = aBorderWidth;
// Note: use SetBorderStyle here because we want to affect
// mComputedBorder
SetBorderStyle(side, StyleBorderStyle::Inset);
}
}
};
class nsDisplayAltFeedback final : public nsPaintedDisplayItem {
public:
nsDisplayAltFeedback(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame)
: nsPaintedDisplayItem(aBuilder, aFrame) {}
nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) const final {
*aSnap = false;
return mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame();
}
void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) final {
// Always sync decode, because these icons are UI, and since they're not
// discardable we'll pay the price of sync decoding at most once.
uint32_t flags = imgIContainer::FLAG_SYNC_DECODE;
nsImageFrame* f = static_cast<nsImageFrame*>(mFrame);
Unused << f->DisplayAltFeedback(*aCtx, GetPaintRect(aBuilder, aCtx),
ToReferenceFrame(), flags);
}
bool CreateWebRenderCommands(
mozilla::wr::DisplayListBuilder& aBuilder,
mozilla::wr::IpcResourceUpdateQueue& aResources,
const StackingContextHelper& aSc,
mozilla::layers::RenderRootStateManager* aManager,
nsDisplayListBuilder* aDisplayListBuilder) final {
// Always sync decode, because these icons are UI, and since they're not
// discardable we'll pay the price of sync decoding at most once.
uint32_t flags =
imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY;
nsImageFrame* f = static_cast<nsImageFrame*>(mFrame);
ImgDrawResult result = f->DisplayAltFeedbackWithoutLayer(
this, aBuilder, aResources, aSc, aManager, aDisplayListBuilder,
ToReferenceFrame(), flags);
return result == ImgDrawResult::SUCCESS;
}
NS_DISPLAY_DECL_NAME("AltFeedback", TYPE_ALT_FEEDBACK)
};
ImgDrawResult nsImageFrame::DisplayAltFeedback(gfxContext& aRenderingContext,
const nsRect& aDirtyRect,
nsPoint aPt, uint32_t aFlags) {
// Whether we draw the broken or loading icon.
bool isLoading = mKind != Kind::ImageLoadingContent ||
ImageOk(mContent->AsElement()->State());
// Calculate the content area.
nsRect inner = GetContentRectRelativeToSelf() + aPt;
// Display a recessed one pixel border
nscoord borderEdgeWidth =
nsPresContext::CSSPixelsToAppUnits(ALT_BORDER_WIDTH);
// if inner area is empty, then make it big enough for at least the icon
if (inner.IsEmpty()) {
inner.SizeTo(2 * (nsPresContext::CSSPixelsToAppUnits(
ICON_SIZE + ICON_PADDING + ALT_BORDER_WIDTH)),
2 * (nsPresContext::CSSPixelsToAppUnits(
ICON_SIZE + ICON_PADDING + ALT_BORDER_WIDTH)));
}
// Make sure we have enough room to actually render the border within
// our frame bounds
if ((inner.width < 2 * borderEdgeWidth) ||
(inner.height < 2 * borderEdgeWidth)) {
return ImgDrawResult::SUCCESS;
}
// Paint the border
if (!isLoading) {
nsRecessedBorder recessedBorder(borderEdgeWidth);
// Assert that we're not drawing a border-image here; if we were, we
// couldn't ignore the ImgDrawResult that PaintBorderWithStyleBorder
// returns.
MOZ_ASSERT(recessedBorder.mBorderImageSource.IsNone());
Unused << nsCSSRendering::PaintBorderWithStyleBorder(
PresContext(), aRenderingContext, this, inner, inner, recessedBorder,
mComputedStyle, PaintBorderFlags::SyncDecodeImages);
}
// Adjust the inner rect to account for the one pixel recessed border,
// and a six pixel padding on each edge
inner.Deflate(
nsPresContext::CSSPixelsToAppUnits(ICON_PADDING + ALT_BORDER_WIDTH),
nsPresContext::CSSPixelsToAppUnits(ICON_PADDING + ALT_BORDER_WIDTH));
if (inner.IsEmpty()) {
return ImgDrawResult::SUCCESS;
}
DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
// Clip so we don't render outside the inner rect
aRenderingContext.Save();
aRenderingContext.Clip(NSRectToSnappedRect(
inner, PresContext()->AppUnitsPerDevPixel(), *drawTarget));
ImgDrawResult result = ImgDrawResult::SUCCESS;
// Check if we should display image placeholders
if (ShouldShowBrokenImageIcon()) {
result = ImgDrawResult::NOT_READY;
nscoord size = nsPresContext::CSSPixelsToAppUnits(ICON_SIZE);
imgIRequest* request = BrokenImageIcon::GetImage(this);
// If we weren't previously displaying an icon, register ourselves
// as an observer for load and animation updates and flag that we're
// doing so now.
if (request && !mDisplayingIcon) {
BrokenImageIcon::AddObserver(this);
mDisplayingIcon = true;
}
WritingMode wm = GetWritingMode();
bool flushRight = wm.IsPhysicalRTL();
// If the icon in question is loaded, draw it.
uint32_t imageStatus = 0;
if (request) request->GetImageStatus(&imageStatus);
if (imageStatus & imgIRequest::STATUS_LOAD_COMPLETE &&
!(imageStatus & imgIRequest::STATUS_ERROR)) {
nsCOMPtr<imgIContainer> imgCon;
request->GetImage(getter_AddRefs(imgCon));
MOZ_ASSERT(imgCon, "Load complete, but no image container?");
nsRect dest(flushRight ? inner.XMost() - size : inner.x, inner.y, size,
size);
result = nsLayoutUtils::DrawSingleImage(
aRenderingContext, PresContext(), imgCon,
nsLayoutUtils::GetSamplingFilterForFrame(this), dest, aDirtyRect,
SVGImageContext(), aFlags);
}
// If we could not draw the icon, just draw some graffiti in the mean time.
if (result == ImgDrawResult::NOT_READY) {
ColorPattern color(ToDeviceColor(sRGBColor(1.f, 0.f, 0.f, 1.f)));
nscoord iconXPos = flushRight ? inner.XMost() - size : inner.x;
// stroked rect:
nsRect rect(iconXPos, inner.y, size, size);
Rect devPxRect = ToRect(nsLayoutUtils::RectToGfxRect(
rect, PresContext()->AppUnitsPerDevPixel()));
drawTarget->StrokeRect(devPxRect, color);
// filled circle in bottom right quadrant of stroked rect:
nscoord twoPX = nsPresContext::CSSPixelsToAppUnits(2);
rect = nsRect(iconXPos + size / 2, inner.y + size / 2, size / 2 - twoPX,
size / 2 - twoPX);
devPxRect = ToRect(nsLayoutUtils::RectToGfxRect(
rect, PresContext()->AppUnitsPerDevPixel()));
RefPtr<PathBuilder> builder = drawTarget->CreatePathBuilder();
AppendEllipseToPath(builder, devPxRect.Center(), devPxRect.Size());
RefPtr<Path> ellipse = builder->Finish();
drawTarget->Fill(ellipse, color);
}
// Reduce the inner rect by the width of the icon, and leave an
// additional ICON_PADDING pixels for padding
int32_t paddedIconSize =
nsPresContext::CSSPixelsToAppUnits(ICON_SIZE + ICON_PADDING);
if (wm.IsVertical()) {
inner.y += paddedIconSize;
inner.height -= paddedIconSize;
} else {
if (!flushRight) {
inner.x += paddedIconSize;
}
inner.width -= paddedIconSize;
}
}
// If there's still room, display the alt-text
if (!inner.IsEmpty()) {
nsAutoString altText;
nsCSSFrameConstructor::GetAlternateTextFor(*mContent->AsElement(), altText);
DisplayAltText(PresContext(), aRenderingContext, altText, inner);
}
aRenderingContext.Restore();
return result;
}
ImgDrawResult nsImageFrame::DisplayAltFeedbackWithoutLayer(
nsDisplayItem* aItem, mozilla::wr::DisplayListBuilder& aBuilder,
mozilla::wr::IpcResourceUpdateQueue& aResources,
const StackingContextHelper& aSc,
mozilla::layers::RenderRootStateManager* aManager,
nsDisplayListBuilder* aDisplayListBuilder, nsPoint aPt, uint32_t aFlags) {
// Whether we draw the broken or loading icon.
bool isLoading = mKind != Kind::ImageLoadingContent ||
ImageOk(mContent->AsElement()->State());
// Calculate the content area.
nsRect inner = GetContentRectRelativeToSelf() + aPt;
// Display a recessed one pixel border
nscoord borderEdgeWidth =
nsPresContext::CSSPixelsToAppUnits(ALT_BORDER_WIDTH);
// if inner area is empty, then make it big enough for at least the icon
if (inner.IsEmpty()) {
inner.SizeTo(2 * (nsPresContext::CSSPixelsToAppUnits(
ICON_SIZE + ICON_PADDING + ALT_BORDER_WIDTH)),
2 * (nsPresContext::CSSPixelsToAppUnits(
ICON_SIZE + ICON_PADDING + ALT_BORDER_WIDTH)));
}
// Make sure we have enough room to actually render the border within
// our frame bounds
if ((inner.width < 2 * borderEdgeWidth) ||
(inner.height < 2 * borderEdgeWidth)) {
return ImgDrawResult::SUCCESS;
}
// If the TextDrawTarget requires fallback we need to rollback everything we
// may have added to the display list, but we don't find that out until the
// end.
bool textDrawResult = true;
class AutoSaveRestore {
public:
explicit AutoSaveRestore(mozilla::wr::DisplayListBuilder& aBuilder,
bool& aTextDrawResult)
: mBuilder(aBuilder), mTextDrawResult(aTextDrawResult) {
mBuilder.Save();
}
~AutoSaveRestore() {
// If we have to use fallback for the text restore the builder and remove
// anything else we added to the display list, we need to use fallback.
if (mTextDrawResult) {
mBuilder.ClearSave();
} else {
mBuilder.Restore();
}
}
private:
mozilla::wr::DisplayListBuilder& mBuilder;
bool& mTextDrawResult;
};
AutoSaveRestore autoSaveRestore(aBuilder, textDrawResult);
// Paint the border
if (!isLoading) {
nsRecessedBorder recessedBorder(borderEdgeWidth);
// Assert that we're not drawing a border-image here; if we were, we
// couldn't ignore the ImgDrawResult that PaintBorderWithStyleBorder
// returns.
MOZ_ASSERT(recessedBorder.mBorderImageSource.IsNone());
nsRect rect = nsRect(aPt, GetSize());
Unused << nsCSSRendering::CreateWebRenderCommandsForBorderWithStyleBorder(
aItem, this, rect, aBuilder, aResources, aSc, aManager,
aDisplayListBuilder, recessedBorder);
}
// Adjust the inner rect to account for the one pixel recessed border,
// and a six pixel padding on each edge
inner.Deflate(
nsPresContext::CSSPixelsToAppUnits(ICON_PADDING + ALT_BORDER_WIDTH),
nsPresContext::CSSPixelsToAppUnits(ICON_PADDING + ALT_BORDER_WIDTH));
if (inner.IsEmpty()) {
return ImgDrawResult::SUCCESS;
}
// Clip to this rect so we don't render outside the inner rect
const auto bounds = LayoutDeviceRect::FromAppUnits(
inner, PresContext()->AppUnitsPerDevPixel());
auto wrBounds = wr::ToLayoutRect(bounds);
// Check if we should display image placeholders
if (ShouldShowBrokenImageIcon()) {
ImgDrawResult result = ImgDrawResult::NOT_READY;
nscoord size = nsPresContext::CSSPixelsToAppUnits(ICON_SIZE);
imgIRequest* request = BrokenImageIcon::GetImage(this);
// If we weren't previously displaying an icon, register ourselves
// as an observer for load and animation updates and flag that we're
// doing so now.
if (request && !mDisplayingIcon) {
BrokenImageIcon::AddObserver(this);
mDisplayingIcon = true;
}
WritingMode wm = GetWritingMode();
const bool flushRight = wm.IsPhysicalRTL();
// If the icon in question is loaded, draw it.
uint32_t imageStatus = 0;
if (request) request->GetImageStatus(&imageStatus);
if (imageStatus & imgIRequest::STATUS_LOAD_COMPLETE &&
!(imageStatus & imgIRequest::STATUS_ERROR)) {
nsCOMPtr<imgIContainer> imgCon;
request->GetImage(getter_AddRefs(imgCon));
MOZ_ASSERT(imgCon, "Load complete, but no image container?");
nsRect dest(flushRight ? inner.XMost() - size : inner.x, inner.y, size,
size);
const int32_t factor = PresContext()->AppUnitsPerDevPixel();
const auto destRect = LayoutDeviceRect::FromAppUnits(dest, factor);
SVGImageContext svgContext;
Maybe<ImageIntRegion> region;
IntSize decodeSize =
nsLayoutUtils::ComputeImageContainerDrawingParameters(
imgCon, this, destRect, destRect, aSc, aFlags, svgContext,
region);
RefPtr<image::WebRenderImageProvider> provider;
result = imgCon->GetImageProvider(aManager->LayerManager(), decodeSize,
svgContext, region, aFlags,
getter_AddRefs(provider));
if (provider) {
bool wrResult = aManager->CommandBuilder().PushImageProvider(
aItem, provider, result, aBuilder, aResources, destRect, bounds);
result &= wrResult ? ImgDrawResult::SUCCESS : ImgDrawResult::NOT_READY;
} else {
// We don't use &= here because we want the result to be NOT_READY so
// the next block executes.
result = ImgDrawResult::NOT_READY;
}
}
// If we could not draw the icon, just draw some graffiti in the mean time.
if (result == ImgDrawResult::NOT_READY) {
auto color = wr::ColorF{1.0f, 0.0f, 0.0f, 1.0f};
bool isBackfaceVisible = !aItem->BackfaceIsHidden();
nscoord iconXPos = flushRight ? inner.XMost() - size : inner.x;
// stroked rect:
nsRect rect(iconXPos, inner.y, size, size);
auto devPxRect = LayoutDeviceRect::FromAppUnits(
rect, PresContext()->AppUnitsPerDevPixel());
auto dest = wr::ToLayoutRect(devPxRect);
auto borderWidths = wr::ToBorderWidths(1.0, 1.0, 1.0, 1.0);
wr::BorderSide side = {color, wr::BorderStyle::Solid};
wr::BorderSide sides[4] = {side, side, side, side};
Range<const wr::BorderSide> sidesRange(sides, 4);
aBuilder.PushBorder(dest, wrBounds, isBackfaceVisible, borderWidths,
sidesRange, wr::EmptyBorderRadius());
// filled circle in bottom right quadrant of stroked rect:
nscoord twoPX = nsPresContext::CSSPixelsToAppUnits(2);
rect = nsRect(iconXPos + size / 2, inner.y + size / 2, size / 2 - twoPX,
size / 2 - twoPX);
devPxRect = LayoutDeviceRect::FromAppUnits(
rect, PresContext()->AppUnitsPerDevPixel());
dest = wr::ToLayoutRect(devPxRect);
aBuilder.PushRoundedRect(dest, wrBounds, isBackfaceVisible, color);
}
// Reduce the inner rect by the width of the icon, and leave an
// additional ICON_PADDING pixels for padding
int32_t paddedIconSize =
nsPresContext::CSSPixelsToAppUnits(ICON_SIZE + ICON_PADDING);
if (wm.IsVertical()) {
inner.y += paddedIconSize;
inner.height -= paddedIconSize;
} else {
if (!flushRight) {
inner.x += paddedIconSize;
}
inner.width -= paddedIconSize;
}
}
// Draw text
if (!inner.IsEmpty()) {
RefPtr<TextDrawTarget> textDrawer =
new TextDrawTarget(aBuilder, aResources, aSc, aManager, aItem, inner,
/* aCallerDoesSaveRestore = */ true);
MOZ_ASSERT(textDrawer->IsValid());
if (textDrawer->IsValid()) {
gfxContext captureCtx(textDrawer);
nsAutoString altText;
nsCSSFrameConstructor::GetAlternateTextFor(*mContent->AsElement(),
altText);
DisplayAltText(PresContext(), captureCtx, altText, inner);
textDrawer->TerminateShadows();
textDrawResult = !textDrawer->CheckHasUnsupportedFeatures();
}
}
// Purposely ignore local DrawResult because we handled it not being success
// already.
return textDrawResult ? ImgDrawResult::SUCCESS : ImgDrawResult::NOT_READY;
}
// We want to sync-decode in this case, as otherwise we either need to flash
// white while waiting to decode the new image, or paint the old image with a
// different aspect-ratio, which would be bad as it'd be stretched.
//
// See bug 1589955.
static bool OldImageHasDifferentRatio(const nsImageFrame& aFrame,
imgIContainer& aImage,
imgIContainer* aPrevImage) {
if (!aPrevImage || aPrevImage == &aImage) {
return false;
}
// If we don't depend on our intrinsic image size / ratio, we're good.
//
// FIXME(emilio): There's the case of the old image being painted
// intrinsically, and src and styles changing at the same time... Maybe we
// should keep track of the old GetPaintRect()'s ratio and the image's ratio,
// instead of checking this bit?
if (aFrame.HasAnyStateBits(IMAGE_SIZECONSTRAINED)) {
return false;
}
auto currentRatio = aFrame.GetIntrinsicRatio();
// If we have an image, we need to have a current request.
// Same if we had an image.
const bool hasRequest = true;
#ifdef DEBUG
auto currentRatioRecomputed =
ComputeIntrinsicRatio(&aImage, hasRequest, aFrame);
// If the image encounters an error after decoding the size (and we run
// UpdateIntrinsicRatio) then the image will return the empty AspectRatio and
// the aspect ratio we compute here will be different from what was computed
// and stored before the image went into error state. It would be better to
// check that the image has an error here but we need an imgIRequest for that,
// not an imgIContainer. In lieu of that we check that
// aImage.GetIntrinsicRatio() returns Nothing() as it does when the image is
// in the error state and that the recomputed ratio is the zero ratio.
MOZ_ASSERT(
(!currentRatioRecomputed && aImage.GetIntrinsicRatio() == Nothing()) ||
currentRatio == currentRatioRecomputed,
"aspect-ratio got out of sync during paint? How?");
#endif
auto oldRatio = ComputeIntrinsicRatio(aPrevImage, hasRequest, aFrame);
return oldRatio != currentRatio;
}
#ifdef DEBUG
void nsImageFrame::AssertSyncDecodingHintIsInSync() const {
if (!IsForImageLoadingContent()) {
return;
}
nsCOMPtr<nsIImageLoadingContent> imageLoader = do_QueryInterface(mContent);
MOZ_ASSERT(imageLoader);
// The opposite is not true, we might have some other heuristics which force
// sync-decoding of images.
MOZ_ASSERT_IF(imageLoader->GetSyncDecodingHint(), mForceSyncDecoding);
}
#endif
void nsDisplayImage::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) {
MOZ_ASSERT(mImage);
auto* frame = static_cast<nsImageFrame*>(mFrame);
frame->AssertSyncDecodingHintIsInSync();
const bool oldImageIsDifferent =
OldImageHasDifferentRatio(*frame, *mImage, mPrevImage);
uint32_t flags = aBuilder->GetImageDecodeFlags();
if (aBuilder->ShouldSyncDecodeImages() || oldImageIsDifferent ||
frame->mForceSyncDecoding) {
flags |= imgIContainer::FLAG_SYNC_DECODE;
}
ImgDrawResult result = frame->PaintImage(
*aCtx, ToReferenceFrame(), GetPaintRect(aBuilder, aCtx), mImage, flags);
if (result == ImgDrawResult::NOT_READY ||
result == ImgDrawResult::INCOMPLETE ||
result == ImgDrawResult::TEMPORARY_ERROR) {
// If the current image failed to paint because it's still loading or
// decoding, try painting the previous image.
if (mPrevImage) {
result =
frame->PaintImage(*aCtx, ToReferenceFrame(),
GetPaintRect(aBuilder, aCtx), mPrevImage, flags);
}
}
}
nsRect nsDisplayImage::GetDestRect() const {
bool snap = true;
const nsRect frameContentBox = GetBounds(&snap);
nsImageFrame* imageFrame = static_cast<nsImageFrame*>(mFrame);
return imageFrame->PredictedDestRect(frameContentBox);
}
nsRegion nsDisplayImage::GetOpaqueRegion(nsDisplayListBuilder* aBuilder,
bool* aSnap) const {
*aSnap = false;
if (mImage && mImage->WillDrawOpaqueNow()) {
const nsRect frameContentBox = GetBounds(aSnap);
return GetDestRect().Intersect(frameContentBox);
}
return nsRegion();
}
bool nsDisplayImage::CreateWebRenderCommands(
mozilla::wr::DisplayListBuilder& aBuilder,
mozilla::wr::IpcResourceUpdateQueue& aResources,
const StackingContextHelper& aSc, RenderRootStateManager* aManager,
nsDisplayListBuilder* aDisplayListBuilder) {
if (!mImage) {
return false;
}
MOZ_ASSERT(mFrame->IsImageFrame() || mFrame->IsImageControlFrame());
// Image layer doesn't support draw focus ring for image map.
auto* frame = static_cast<nsImageFrame*>(mFrame);
if (frame->HasImageMap()) {
return false;
}
frame->AssertSyncDecodingHintIsInSync();
const bool oldImageIsDifferent =
OldImageHasDifferentRatio(*frame, *mImage, mPrevImage);
uint32_t flags = aDisplayListBuilder->GetImageDecodeFlags();
if (aDisplayListBuilder->ShouldSyncDecodeImages() || oldImageIsDifferent ||
frame->mForceSyncDecoding) {
flags |= imgIContainer::FLAG_SYNC_DECODE;
}
if (StaticPrefs::image_svg_blob_image() &&
mImage->GetType() == imgIContainer::TYPE_VECTOR) {
flags |= imgIContainer::FLAG_RECORD_BLOB;
}
const int32_t factor = mFrame->PresContext()->AppUnitsPerDevPixel();
LayoutDeviceRect destRect(
LayoutDeviceRect::FromAppUnits(GetDestRect(), factor));
SVGImageContext svgContext;
Maybe<ImageIntRegion> region;
IntSize decodeSize = nsLayoutUtils::ComputeImageContainerDrawingParameters(
mImage, mFrame, destRect, destRect, aSc, flags, svgContext, region);
RefPtr<image::WebRenderImageProvider> provider;
ImgDrawResult drawResult =
mImage->GetImageProvider(aManager->LayerManager(), decodeSize, svgContext,
region, flags, getter_AddRefs(provider));
if (nsCOMPtr<imgIRequest> currentRequest = frame->GetCurrentRequest()) {
LCPHelpers::FinalizeLCPEntryForImage(
frame->GetContent()->AsElement(),
static_cast<imgRequestProxy*>(currentRequest.get()),
GetDestRect() - ToReferenceFrame());
}
// While we got a container, it may not contain a fully decoded surface. If
// that is the case, and we have an image we were previously displaying which
// has a fully decoded surface, then we should prefer the previous image.
bool updatePrevImage = false;
switch (drawResult) {
case ImgDrawResult::NOT_READY:
case ImgDrawResult::INCOMPLETE:
case ImgDrawResult::TEMPORARY_ERROR:
if (mPrevImage && mPrevImage != mImage) {
// The current image and the previous image might be switching between
// rasterized surfaces and blob recordings, so we need to update the
// flags appropriately.
uint32_t prevFlags = flags;
if (StaticPrefs::image_svg_blob_image() &&
mPrevImage->GetType() == imgIContainer::TYPE_VECTOR) {
prevFlags |= imgIContainer::FLAG_RECORD_BLOB;
} else {
prevFlags &= ~imgIContainer::FLAG_RECORD_BLOB;
}
RefPtr<image::WebRenderImageProvider> prevProvider;
ImgDrawResult prevDrawResult = mPrevImage->GetImageProvider(
aManager->LayerManager(), decodeSize, svgContext, region, prevFlags,
getter_AddRefs(prevProvider));
if (prevProvider && (prevDrawResult == ImgDrawResult::SUCCESS ||
prevDrawResult == ImgDrawResult::WRONG_SIZE)) {
// We use WRONG_SIZE here to ensure that when the frame next tries to
// invalidate due to a frame update from the current image, we don't
// consider the result from the previous image to be a valid result to
// avoid redrawing.
drawResult = ImgDrawResult::WRONG_SIZE;
provider = std::move(prevProvider);
flags = prevFlags;
break;
}
// Previous image was unusable; we can forget about it.
updatePrevImage = true;
}
break;
case ImgDrawResult::NOT_SUPPORTED:
return false;
default:
updatePrevImage = mPrevImage != mImage;
break;
}
// The previous image was not used, and is different from the current image.
// We should forget about it. We need to update the frame as well because the
// display item may get recreated.
if (updatePrevImage) {
mPrevImage = mImage;
frame->mPrevImage = frame->mImage;
}
// If the image provider is null, we don't want to fallback. Any other
// failure will be due to resource constraints and fallback is unlikely to
// help us. Hence we can ignore the return value from PushImage.
aManager->CommandBuilder().PushImageProvider(
this, provider, drawResult, aBuilder, aResources, destRect, destRect);
return true;
}
ImgDrawResult nsImageFrame::PaintImage(gfxContext& aRenderingContext,
nsPoint aPt, const nsRect& aDirtyRect,
imgIContainer* aImage, uint32_t aFlags) {
DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
// Render the image into our content area (the area inside
// the borders and padding)
NS_ASSERTION(GetContentRectRelativeToSelf().width == mComputedSize.width,
"bad width");
// NOTE: We use mComputedSize instead of just GetContentRectRelativeToSelf()'s
// own size here, because GetContentRectRelativeToSelf() might be smaller if
// we're fragmented, whereas mComputedSize has our full content-box size
// (which we need for ComputeObjectDestRect to work correctly).
nsRect constraintRect(aPt + GetContentRectRelativeToSelf().TopLeft(),
mComputedSize);
constraintRect.y -= GetContinuationOffset();
nsPoint anchorPoint;
nsRect dest = nsLayoutUtils::ComputeObjectDestRect(
constraintRect, mIntrinsicSize, mIntrinsicRatio, StylePosition(),
&anchorPoint);
SVGImageContext svgContext;
SVGImageContext::MaybeStoreContextPaint(svgContext, this, aImage);
ImgDrawResult result = nsLayoutUtils::DrawSingleImage(
aRenderingContext, PresContext(), aImage,
nsLayoutUtils::GetSamplingFilterForFrame(this), dest, aDirtyRect,
svgContext, aFlags, &anchorPoint);
if (nsImageMap* map = GetImageMap()) {
gfxPoint devPixelOffset = nsLayoutUtils::PointToGfxPoint(
dest.TopLeft(), PresContext()->AppUnitsPerDevPixel());
AutoRestoreTransform autoRestoreTransform(drawTarget);
drawTarget->SetTransform(
drawTarget->GetTransform().PreTranslate(ToPoint(devPixelOffset)));
// solid white stroke:
ColorPattern white(ToDeviceColor(sRGBColor::OpaqueWhite()));
map->Draw(this, *drawTarget, white);
// then dashed black stroke over the top:
ColorPattern black(ToDeviceColor(sRGBColor::OpaqueBlack()));
StrokeOptions strokeOptions;
nsLayoutUtils::InitDashPattern(strokeOptions, StyleBorderStyle::Dotted);
map->Draw(this, *drawTarget, black, strokeOptions);
}
if (result == ImgDrawResult::SUCCESS) {
mPrevImage = aImage;
} else if (result == ImgDrawResult::BAD_IMAGE) {
mPrevImage = nullptr;
}
return result;
}
already_AddRefed<imgIRequest> nsImageFrame::GetCurrentRequest() const {
if (mKind != Kind::ImageLoadingContent) {
return do_AddRef(mOwnedRequest);
}
MOZ_ASSERT(!mOwnedRequest);
nsCOMPtr<imgIRequest> request;
nsCOMPtr<nsIImageLoadingContent> imageLoader = do_QueryInterface(mContent);
MOZ_ASSERT(imageLoader);
imageLoader->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
getter_AddRefs(request));
return request.forget();
}
void nsImageFrame::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);
if (mComputedSize.width != 0 && mComputedSize.height != 0) {
const bool imageOK = mKind != Kind::ImageLoadingContent ||
ImageOk(mContent->AsElement()->State());
nsCOMPtr<imgIRequest> currentRequest = GetCurrentRequest();
const bool isImageFromStyle =
mKind != Kind::ImageLoadingContent && mKind != Kind::XULImage;
const bool drawAltFeedback = [&] {
if (!imageOK) {
return true;
}
// If we're a gradient, we don't need to draw alt feedback.
if (isImageFromStyle && !GetImageFromStyle()->IsImageRequestType()) {
return false;
}
// XXX(seth): The SizeIsAvailable check here should not be necessary - the
// intention is that a non-null mImage means we have a size, but there is
// currently some code that violates this invariant.
return !mImage || !SizeIsAvailable(currentRequest);
}();
if (drawAltFeedback) {
// No image yet, or image load failed. Draw the alt-text and an icon
// indicating the status
aLists.Content()->AppendNewToTop<nsDisplayAltFeedback>(aBuilder, this);
// This image is visible (we are being asked to paint it) but it's not
// decoded yet. And we are not going to ask the image to draw, so this
// may be the only chance to tell it that it should decode.
if (currentRequest) {
uint32_t status = 0;
currentRequest->GetImageStatus(&status);
if (!(status & imgIRequest::STATUS_DECODE_COMPLETE)) {
MaybeDecodeForPredictedSize();
}
// Increase loading priority if the image is ready to be displayed.
if (!(status & imgIRequest::STATUS_LOAD_COMPLETE)) {
currentRequest->BoostPriority(imgIRequest::CATEGORY_DISPLAY);
}
}
} else {
if (mImage) {
aLists.Content()->AppendNewToTop<nsDisplayImage>(aBuilder, this, mImage,
mPrevImage);
} else if (isImageFromStyle) {
aLists.Content()->AppendNewToTop<nsDisplayGradient>(aBuilder, this);
}
// If we were previously displaying an icon, we're not anymore
if (mDisplayingIcon) {
BrokenImageIcon::RemoveObserver(this);
mDisplayingIcon = false;
}
}
}
if (ShouldDisplaySelection()) {
DisplaySelectionOverlay(aBuilder, aLists.Content(),
nsISelectionDisplay::DISPLAY_IMAGES);
}
BuildDisplayListForNonBlockChildren(aBuilder, aLists);
}
bool nsImageFrame::ShouldDisplaySelection() {
int16_t displaySelection = PresShell()->GetSelectionFlags();
if (!(displaySelection & nsISelectionDisplay::DISPLAY_IMAGES)) {
// no need to check the blue border, we cannot be drawn selected.
return false;
}
if (displaySelection != nsISelectionDisplay::DISPLAY_ALL) {
return true;
}
// If the image is currently resize target of the editor, don't draw the
// selection overlay.
HTMLEditor* htmlEditor = nsContentUtils::GetHTMLEditor(PresContext());
if (!htmlEditor) {
return true;
}
return htmlEditor->GetResizerTarget() != mContent;
}
nsImageMap* nsImageFrame::GetImageMap() {
if (!mImageMap) {
if (nsIContent* map = GetMapElement()) {
mImageMap = new nsImageMap();
mImageMap->Init(this, map);
}
}
return mImageMap;
}
bool nsImageFrame::IsServerImageMap() {
return mContent->AsElement()->HasAttr(nsGkAtoms::ismap);
}
CSSIntPoint nsImageFrame::TranslateEventCoords(const nsPoint& aPoint) {
const nsRect contentRect = GetContentRectRelativeToSelf();
// Subtract out border and padding here so that the coordinates are
// now relative to the content area of this frame.
return CSSPixel::FromAppUnitsRounded(aPoint - contentRect.TopLeft());
}
bool nsImageFrame::GetAnchorHREFTargetAndNode(nsIURI** aHref, nsString& aTarget,
nsIContent** aNode) {
aTarget.Truncate();
*aHref = nullptr;
*aNode = nullptr;
// Walk up the content tree, looking for an nsIDOMAnchorElement
for (nsIContent* content = mContent->GetParent(); content;
content = content->GetParent()) {
nsCOMPtr<dom::Link> link = do_QueryInterface(content);
if (!link) {
continue;
}
if (nsCOMPtr<nsIURI> href = link->GetURI()) {
href.forget(aHref);
}
if (auto* anchor = HTMLAnchorElement::FromNode(content)) {
anchor->GetTarget(aTarget);
}
NS_ADDREF(*aNode = content);
return *aHref != nullptr;
}
return false;
}
bool nsImageFrame::IsLeafDynamic() const {
if (mKind != Kind::ImageLoadingContent) {
// Image frames created for "content: url()" could have an author-controlled
// shadow root, we want to be a regular leaf for those.
return true;
}
// For elements that create image frames, calling attachShadow() will throw,
// so the only ShadowRoot we could ever have is a UA widget.
const auto* shadow = mContent->AsElement()->GetShadowRoot();
MOZ_ASSERT_IF(shadow, shadow->IsUAWidget());
return !shadow;
}
nsresult nsImageFrame::GetContentForEvent(const WidgetEvent* aEvent,
nsIContent** aContent) {
NS_ENSURE_ARG_POINTER(aContent);
nsIFrame* f = nsLayoutUtils::GetNonGeneratedAncestor(this);
if (f != this) {
return f->GetContentForEvent(aEvent, aContent);
}
// XXX We need to make this special check for area element's capturing the
// mouse due to bug 135040. Remove it once that's fixed.
nsIContent* capturingContent = aEvent->HasMouseEventMessage()
? PresShell::GetCapturingContent()
: nullptr;
if (capturingContent && capturingContent->GetPrimaryFrame() == this) {
*aContent = capturingContent;
NS_IF_ADDREF(*aContent);
return NS_OK;
}
if (nsImageMap* map = GetImageMap()) {
const CSSIntPoint p = TranslateEventCoords(
nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this}));
nsCOMPtr<nsIContent> area = map->GetArea(p);
if (area) {
area.forget(aContent);
return NS_OK;
}
}
*aContent = GetContent();
NS_IF_ADDREF(*aContent);
return NS_OK;
}
// XXX what should clicks on transparent pixels do?
nsresult nsImageFrame::HandleEvent(nsPresContext* aPresContext,
WidgetGUIEvent* aEvent,
nsEventStatus* aEventStatus) {
NS_ENSURE_ARG_POINTER(aEventStatus);
if ((aEvent->mMessage == eMouseClick &&
aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary) ||
aEvent->mMessage == eMouseMove) {
nsImageMap* map = GetImageMap();
bool isServerMap = IsServerImageMap();
if (map || isServerMap) {
CSSIntPoint p =
TranslateEventCoords(nsLayoutUtils::GetEventCoordinatesRelativeTo(
aEvent, RelativeTo{this}));
// Even though client-side image map triggering happens
// through content, we need to make sure we're not inside
// (in case we deal with a case of both client-side and
// sever-side on the same image - it happens!)
const bool inside = map && map->GetArea(p);
if (!inside && isServerMap) {
// Server side image maps use the href in a containing anchor
// element to provide the basis for the destination url.
nsCOMPtr<nsIURI> uri;
nsAutoString target;
nsCOMPtr<nsIContent> anchorNode;
if (GetAnchorHREFTargetAndNode(getter_AddRefs(uri), target,
getter_AddRefs(anchorNode))) {
// XXX if the mouse is over/clicked in the border/padding area
// we should probably just pretend nothing happened. Nav4
// keeps the x,y coordinates positive as we do; IE doesn't
// bother. Both of them send the click through even when the
// mouse is over the border.
if (p.x < 0) p.x = 0;
if (p.y < 0) p.y = 0;
nsAutoCString spec;
nsresult rv = uri->GetSpec(spec);
NS_ENSURE_SUCCESS(rv, rv);
spec += nsPrintfCString("?%d,%d", p.x.value, p.y.value);
rv = NS_MutateURI(uri).SetSpec(spec).Finalize(uri);
NS_ENSURE_SUCCESS(rv, rv);
bool clicked = false;
if (aEvent->mMessage == eMouseClick && !aEvent->DefaultPrevented()) {
*aEventStatus = nsEventStatus_eConsumeDoDefault;
clicked = true;
}
nsContentUtils::TriggerLink(anchorNode, uri, target, clicked,
/* isTrusted */ true);
}
}
}
}
return nsAtomicContainerFrame::HandleEvent(aPresContext, aEvent,
aEventStatus);
}
nsIFrame::Cursor nsImageFrame::GetCursor(const nsPoint& aPoint) {
nsImageMap* map = GetImageMap();
if (!map) {
return nsIFrame::GetCursor(aPoint);
}
const CSSIntPoint p = TranslateEventCoords(aPoint);
HTMLAreaElement* area = map->GetArea(p);
if (!area) {
return nsIFrame::GetCursor(aPoint);
}
// Use the cursor from the style of the *area* element.
RefPtr<ComputedStyle> areaStyle =
PresShell()->StyleSet()->ResolveStyleLazily(*area);
// This is one of the cases, like the <xul:tree> pseudo-style stuff, where we
// get styles out of the blue and expect to trigger image loads for those.
areaStyle->StartImageLoads(*PresContext()->Document());
StyleCursorKind kind = areaStyle->StyleUI()->Cursor().keyword;
if (kind == StyleCursorKind::Auto) {
kind = StyleCursorKind::Default;
}
return Cursor{kind, AllowCustomCursorImage::Yes, std::move(areaStyle)};
}
nsresult nsImageFrame::AttributeChanged(int32_t aNameSpaceID,
nsAtom* aAttribute, int32_t aModType) {
nsresult rv = nsAtomicContainerFrame::AttributeChanged(aNameSpaceID,
aAttribute, aModType);
if (NS_FAILED(rv)) {
return rv;
}
if (nsGkAtoms::alt == aAttribute) {
PresShell()->FrameNeedsReflow(
this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
}
if (mKind == Kind::XULImage && aAttribute == nsGkAtoms::src &&
aNameSpaceID == kNameSpaceID_None) {
UpdateXULImage();
}
return NS_OK;
}
void nsImageFrame::OnVisibilityChange(
Visibility aNewVisibility, const Maybe<OnNonvisible>& aNonvisibleAction) {
if (mKind == Kind::ImageLoadingContent) {
nsCOMPtr<nsIImageLoadingContent> imageLoader = do_QueryInterface(mContent);
imageLoader->OnVisibilityChange(aNewVisibility, aNonvisibleAction);
}
if (aNewVisibility == Visibility::ApproximatelyVisible &&
PresShell()->IsActive()) {
MaybeDecodeForPredictedSize();
}
nsAtomicContainerFrame::OnVisibilityChange(aNewVisibility, aNonvisibleAction);
}
#ifdef DEBUG_FRAME_DUMP
nsresult nsImageFrame::GetFrameName(nsAString& aResult) const {
return MakeFrameName(u"ImageFrame"_ns, aResult);
}
void nsImageFrame::List(FILE* out, const char* aPrefix,
ListFlags aFlags) const {
nsCString str;
ListGeneric(str, aPrefix, aFlags);
// output the img src url
if (nsCOMPtr<imgIRequest> currentRequest = GetCurrentRequest()) {
nsCOMPtr<nsIURI> uri = currentRequest->GetURI();
nsAutoCString uristr;
uri->GetAsciiSpec(uristr);
str += nsPrintfCString(" [src=%s]", uristr.get());
}
fprintf_stderr(out, "%s\n", str.get());
}
#endif
LogicalSides nsImageFrame::GetLogicalSkipSides() const {
LogicalSides skip(mWritingMode);
if (MOZ_UNLIKELY(StyleBorder()->mBoxDecorationBreak ==
StyleBoxDecorationBreak::Clone)) {
return skip;
}
if (GetPrevInFlow()) {
skip |= eLogicalSideBitsBStart;
}
if (GetNextInFlow()) {
skip |= eLogicalSideBitsBEnd;
}
return skip;
}
NS_IMPL_ISUPPORTS(nsImageListener, imgINotificationObserver)
nsImageListener::nsImageListener(nsImageFrame* aFrame) : mFrame(aFrame) {}
nsImageListener::~nsImageListener() = default;
void nsImageListener::Notify(imgIRequest* aRequest, int32_t aType,
const nsIntRect* aData) {
if (!mFrame) {
return;
}
return mFrame->Notify(aRequest, aType, aData);
}
static bool IsInAutoWidthTableCellForQuirk(nsIFrame* aFrame) {
if (eCompatibility_NavQuirks != aFrame->PresContext()->CompatibilityMode())
return false;
// Check if the parent of the closest nsBlockFrame has auto width.
nsBlockFrame* ancestor = nsLayoutUtils::FindNearestBlockAncestor(aFrame);
if (ancestor->Style()->GetPseudoType() == PseudoStyleType::cellContent) {
// Assume direct parent is a table cell frame.
nsIFrame* grandAncestor = static_cast<nsIFrame*>(ancestor->GetParent());
return grandAncestor && grandAncestor->StylePosition()->mWidth.IsAuto();
}
return false;
}
void nsImageFrame::AddInlineMinISize(gfxContext* aRenderingContext,
nsIFrame::InlineMinISizeData* aData) {
nscoord isize = nsLayoutUtils::IntrinsicForContainer(
aRenderingContext, this, IntrinsicISizeType::MinISize);
bool canBreak = !IsInAutoWidthTableCellForQuirk(this);
aData->DefaultAddInlineMinISize(this, isize, canBreak);
}
void nsImageFrame::ReleaseGlobals() { BrokenImageIcon::Shutdown(); }