Bug 1920468: Support badging the dock icon with an arbritrary image. r=spohl,jwatt

This also adds a way to set the SVG context for the image as it is rendered outside
the DOM

Differential Revision: https://phabricator.services.mozilla.com/D223118
This commit is contained in:
Dave Townsend 2024-10-09 15:03:05 +00:00
parent 7f989caf84
commit 31cedc5d17
15 changed files with 252 additions and 76 deletions

View File

@ -23,6 +23,7 @@ XPIDL_SOURCES += [
"nsILayoutHistoryState.idl",
"nsIPreloadedStyleSheet.idl",
"nsIStyleSheetService.idl",
"nsISVGPaintContext.idl",
]
if CONFIG["MOZ_DEBUG"]:

View File

@ -0,0 +1,35 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
/*
* Used to set SVG context values when rendering SVG images outside of a DOM
* context.
*/
[scriptable, uuid(43966236-3146-4518-ab39-f938795cd1a1)]
interface nsISVGPaintContext : nsISupports
{
/**
* The fill color to use. Any CSS color value.
*/
readonly attribute ACString fillColor;
/**
* The stroke color to use. Any CSS color value.
*/
readonly attribute ACString strokeColor;
/**
* The fill opacity to use.
*/
readonly attribute float fillOpacity;
/**
* The stroke opacity to use.
*/
readonly attribute float strokeOpacity;
};

View File

@ -15,6 +15,8 @@
#include "nsIFrame.h"
#include "nsPresContext.h"
#include "nsStyleStruct.h"
#include "nsISVGPaintContext.h"
#include "mozilla/ServoCSSParser.h"
namespace mozilla {
@ -85,4 +87,50 @@ void SVGImageContext::MaybeStoreContextPaint(SVGImageContext& aContext,
}
}
/* static */
void SVGImageContext::MaybeStoreContextPaint(SVGImageContext& aContext,
nsISVGPaintContext* aPaintContext,
imgIContainer* aImgContainer) {
if (aImgContainer->GetType() != imgIContainer::TYPE_VECTOR ||
!aPaintContext) {
// Avoid this overhead for raster images.
return;
}
bool haveContextPaint = false;
auto contextPaint = MakeRefPtr<SVGEmbeddingContextPaint>();
nsCString value;
float opacity;
if (NS_SUCCEEDED(aPaintContext->GetStrokeColor(value)) && !value.IsEmpty()) {
nscolor color;
if (ServoCSSParser::ComputeColor(nullptr, NS_RGB(0, 0, 0), value, &color)) {
haveContextPaint = true;
contextPaint->SetStroke(color);
}
}
if (NS_SUCCEEDED(aPaintContext->GetFillColor(value)) && !value.IsEmpty()) {
nscolor color;
if (ServoCSSParser::ComputeColor(nullptr, NS_RGB(0, 0, 0), value, &color)) {
haveContextPaint = true;
contextPaint->SetFill(color);
}
}
if (NS_SUCCEEDED(aPaintContext->GetStrokeOpacity(&opacity))) {
haveContextPaint = true;
contextPaint->SetStrokeOpacity(opacity);
}
if (NS_SUCCEEDED(aPaintContext->GetFillOpacity(&opacity))) {
haveContextPaint = true;
contextPaint->SetFillOpacity(opacity);
}
if (haveContextPaint) {
aContext.mContextPaint = std::move(contextPaint);
}
}
} // namespace mozilla

View File

@ -13,6 +13,7 @@
#include "Units.h"
class nsIFrame;
class nsISVGPaintContext;
namespace mozilla {
@ -60,6 +61,10 @@ class SVGImageContext {
const nsPresContext&, const ComputedStyle&,
imgIContainer*);
static void MaybeStoreContextPaint(SVGImageContext& aContext,
nsISVGPaintContext* aPaintContext,
imgIContainer* aImgContainer);
const Maybe<CSSIntSize>& GetViewportSize() const { return mViewportSize; }
void SetViewportSize(const Maybe<CSSIntSize>& aSize) {

View File

@ -15,7 +15,8 @@ class nsPresContext;
namespace mozilla {
class ComputedStyle;
}
class SVGImageContext;
} // namespace mozilla
@interface MOZIconHelper : NSObject
@ -25,9 +26,8 @@ class ComputedStyle;
// Returns an autoreleased NSImage.
+ (NSImage*)iconImageFromImageContainer:(imgIContainer*)aImage
withSize:(NSSize)aSize
presContext:(const nsPresContext*)aPresContext
computedStyle:
(const mozilla::ComputedStyle*)aComputedStyle
svgContext:
(const mozilla::SVGImageContext*)aSVGContext
scaleFactor:(CGFloat)aScaleFactor;
@end

View File

@ -22,21 +22,20 @@
// Returns an autoreleased NSImage.
+ (NSImage*)iconImageFromImageContainer:(imgIContainer*)aImage
withSize:(NSSize)aSize
presContext:(const nsPresContext*)aPresContext
computedStyle:
(const mozilla::ComputedStyle*)aComputedStyle
svgContext:
(const mozilla::SVGImageContext*)aSVGContext
scaleFactor:(CGFloat)aScaleFactor {
bool isEntirelyBlack = false;
NSImage* retainedImage = nil;
nsresult rv;
if (aScaleFactor != 0.0f) {
rv = nsCocoaUtils::CreateNSImageFromImageContainer(
aImage, imgIContainer::FRAME_CURRENT, aPresContext, aComputedStyle,
aSize, &retainedImage, aScaleFactor, &isEntirelyBlack);
aImage, imgIContainer::FRAME_CURRENT, aSVGContext, aSize,
&retainedImage, aScaleFactor, &isEntirelyBlack);
} else {
rv = nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
aImage, imgIContainer::FRAME_CURRENT, aPresContext, aComputedStyle,
aSize, &retainedImage, &isEntirelyBlack);
aImage, imgIContainer::FRAME_CURRENT, aSVGContext, aSize,
&retainedImage, &isEntirelyBlack);
}
NSImage* image = [retainedImage autorelease];

View File

@ -547,7 +547,7 @@ OSXNotificationCenter::OnImageReady(nsISupports* aUserData,
// properties.
// TODO: Do we have a reasonable size to pass around here?
nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
image, imgIContainer::FRAME_FIRST, nullptr, nullptr, NSMakeSize(0, 0),
image, imgIContainer::FRAME_FIRST, nullptr, NSMakeSize(0, 0),
&cocoaImage);
(osxni->mPendingNotification).contentImage = cocoaImage;
[cocoaImage release];

View File

@ -286,8 +286,8 @@ class nsCocoaUtils {
the <code>NSImage</code>.
@param aImage the image to extract a frame from
@param aWhichFrame the frame to extract (see imgIContainer FRAME_*)
@param aComputedStyle the ComputedStyle of the element that the image is
for, to support SVG context paint properties, can be null
@param aSVGContext the SVG context paint properties for the image. Can be
null.
@param aResult the resulting NSImage
@param scaleFactor the desired scale factor of the NSImage (2 for a retina
display)
@ -298,18 +298,16 @@ class nsCocoaUtils {
*/
static nsresult CreateNSImageFromImageContainer(
imgIContainer* aImage, uint32_t aWhichFrame,
const nsPresContext* aPresContext,
const mozilla::ComputedStyle* aComputedStyle,
const NSSize& aPreferredSize, NSImage** aResult, CGFloat scaleFactor,
bool* aIsEntirelyBlack = nullptr);
const mozilla::SVGImageContext* aSVGContext, const NSSize& aPreferredSize,
NSImage** aResult, CGFloat scaleFactor, bool* aIsEntirelyBlack = nullptr);
/** Creates a Cocoa <code>NSImage</code> from a frame of an
<code>imgIContainer</code>. The new <code>NSImage</code> will have both a
regular and HiDPI representation. The caller owns the <code>NSImage</code>.
@param aImage the image to extract a frame from
@param aWhichFrame the frame to extract (see imgIContainer FRAME_*)
@param aComputedStyle the ComputedStyle of the element that the image is
for, to support SVG context paint properties, can be null
@param aSVGContext the SVG context paint properties for the image. Can be
null.
@param aResult the resulting NSImage
@param aIsEntirelyBlack an outparam that, if non-null, will be set to a
bool that indicates whether the RGB values on all
@ -318,10 +316,8 @@ class nsCocoaUtils {
*/
static nsresult CreateDualRepresentationNSImageFromImageContainer(
imgIContainer* aImage, uint32_t aWhichFrame,
const nsPresContext* aPresContext,
const mozilla::ComputedStyle* aComputedStyle,
const NSSize& aPreferredSize, NSImage** aResult,
bool* aIsEntirelyBlack = nullptr);
const mozilla::SVGImageContext* aSVGContext, const NSSize& aPreferredSize,
NSImage** aResult, bool* aIsEntirelyBlack = nullptr);
/**
* Returns nsAString for aSrc.

View File

@ -506,9 +506,8 @@ nsresult nsCocoaUtils::CreateNSImageFromCGImage(CGImageRef aInputImage,
nsresult nsCocoaUtils::CreateNSImageFromImageContainer(
imgIContainer* aImage, uint32_t aWhichFrame,
const nsPresContext* aPresContext, const ComputedStyle* aComputedStyle,
const NSSize& aPreferredSize, NSImage** aResult, CGFloat scaleFactor,
bool* aIsEntirelyBlack) {
const SVGImageContext* aSVGContext, const NSSize& aPreferredSize,
NSImage** aResult, CGFloat scaleFactor, bool* aIsEntirelyBlack) {
RefPtr<SourceSurface> surface;
int32_t width = 0;
int32_t height = 0;
@ -544,14 +543,15 @@ nsresult nsCocoaUtils::CreateNSImageFromImageContainer(
gfxContext context(drawTarget);
SVGImageContext svgContext;
if (aPresContext && aComputedStyle) {
SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext,
*aComputedStyle, aImage);
UniquePtr<SVGImageContext> svgContext;
if (!aSVGContext) {
svgContext = MakeUnique<SVGImageContext>();
aSVGContext = svgContext.get();
}
mozilla::image::ImgDrawResult res =
aImage->Draw(&context, scaledSize, ImageRegion::Create(scaledSize),
aWhichFrame, SamplingFilter::POINT, svgContext,
aWhichFrame, SamplingFilter::POINT, *aSVGContext,
imgIContainer::FLAG_SYNC_DECODE, 1.0);
if (res != mozilla::image::ImgDrawResult::SUCCESS) {
@ -589,12 +589,12 @@ nsresult nsCocoaUtils::CreateNSImageFromImageContainer(
nsresult nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
imgIContainer* aImage, uint32_t aWhichFrame,
const nsPresContext* aPresContext, const ComputedStyle* aComputedStyle,
const NSSize& aPreferredSize, NSImage** aResult, bool* aIsEntirelyBlack) {
const SVGImageContext* aSVGContext, const NSSize& aPreferredSize,
NSImage** aResult, bool* aIsEntirelyBlack) {
NSImage* newRepresentation = nil;
nsresult rv = CreateNSImageFromImageContainer(
aImage, aWhichFrame, aPresContext, aComputedStyle, aPreferredSize,
&newRepresentation, 1.0f, aIsEntirelyBlack);
aImage, aWhichFrame, aSVGContext, aPreferredSize, &newRepresentation,
1.0f, aIsEntirelyBlack);
if (NS_FAILED(rv) || !newRepresentation) {
return NS_ERROR_FAILURE;
}
@ -609,9 +609,9 @@ nsresult nsCocoaUtils::CreateDualRepresentationNSImageFromImageContainer(
[newRepresentation release];
newRepresentation = nil;
rv = CreateNSImageFromImageContainer(
aImage, aWhichFrame, aPresContext, aComputedStyle, aPreferredSize,
&newRepresentation, 2.0f, aIsEntirelyBlack);
rv = CreateNSImageFromImageContainer(aImage, aWhichFrame, aSVGContext,
aPreferredSize, &newRepresentation, 2.0f,
aIsEntirelyBlack);
if (NS_FAILED(rv) || !newRepresentation) {
return NS_ERROR_FAILURE;
}

View File

@ -307,8 +307,8 @@ static constexpr nsCursor kCustomCursor = eCursorCount;
const NSSize cocoaSize = NSMakeSize(size.width, size.height);
NSImage* cursorImage;
nsresult rv = nsCocoaUtils::CreateNSImageFromImageContainer(
aCursor.mContainer, imgIContainer::FRAME_FIRST, nullptr, nullptr,
cocoaSize, &cursorImage, scaleFactor);
aCursor.mContainer, imgIContainer::FRAME_FIRST, nullptr, cocoaSize,
&cursorImage, scaleFactor);
if (NS_FAILED(rv) || !cursorImage) {
return NS_ERROR_FAILURE;
}

View File

@ -24,12 +24,15 @@ class nsMacDockSupport : public nsIMacDockSupport, public nsITaskbarProgress {
nsCOMPtr<nsIStandaloneNativeMenu> mDockMenu;
nsString mBadgeText;
bool mHasBadgeImage;
NSView* mDockTileWrapperView;
NSImageView* mDockBadgeView;
MOZProgressDockOverlayView* mProgressDockOverlayView;
nsTaskbarProgressState mProgressState;
double mProgressFraction;
void BuildDockTile();
nsresult UpdateDockTile();
};

View File

@ -13,6 +13,10 @@
#include "nsObjCExceptions.h"
#include "nsNativeThemeColors.h"
#include "nsString.h"
#include "imgLoader.h"
#include "MOZIconHelper.h"
#include "mozilla/SVGImageContext.h"
#include "nsISVGPaintContext.h"
NS_IMPL_ISUPPORTS(nsMacDockSupport, nsIMacDockSupport, nsITaskbarProgress)
@ -71,7 +75,9 @@ NS_IMPL_ISUPPORTS(nsMacDockSupport, nsIMacDockSupport, nsITaskbarProgress)
@end
nsMacDockSupport::nsMacDockSupport()
: mDockTileWrapperView(nil),
: mHasBadgeImage(false),
mDockTileWrapperView(nil),
mDockBadgeView(nil),
mProgressDockOverlayView(nil),
mProgressState(STATE_NO_PROGRESS),
mProgressFraction(0.0) {}
@ -81,6 +87,10 @@ nsMacDockSupport::~nsMacDockSupport() {
[mDockTileWrapperView release];
mDockTileWrapperView = nil;
}
if (mDockBadgeView) {
[mDockBadgeView release];
mDockBadgeView = nil;
}
if (mProgressDockOverlayView) {
[mProgressDockOverlayView release];
mProgressDockOverlayView = nil;
@ -117,14 +127,18 @@ nsMacDockSupport::SetBadgeText(const nsAString& aBadgeText) {
NSDockTile* tile = [[NSApplication sharedApplication] dockTile];
mBadgeText = aBadgeText;
if (aBadgeText.IsEmpty())
if (aBadgeText.IsEmpty()) {
[tile setBadgeLabel:nil];
else
} else {
SetBadgeImage(nullptr, nullptr);
[tile
setBadgeLabel:[NSString
stringWithCharacters:reinterpret_cast<const unichar*>(
mBadgeText.get())
length:mBadgeText.Length()]];
}
return NS_OK;
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
@ -136,6 +150,45 @@ nsMacDockSupport::GetBadgeText(nsAString& aBadgeText) {
return NS_OK;
}
NS_IMETHODIMP
nsMacDockSupport::SetBadgeImage(imgIContainer* aImage,
nsISVGPaintContext* aPaintContext) {
if (!aImage) {
mHasBadgeImage = false;
if (mDockBadgeView) {
mDockBadgeView.image = nullptr;
}
return UpdateDockTile();
}
if (!mBadgeText.IsEmpty()) {
mBadgeText.Truncate();
NSDockTile* tile = [[NSApplication sharedApplication] dockTile];
[tile setBadgeLabel:nil];
}
NS_OBJC_BEGIN_TRY_BLOCK_RETURN
mHasBadgeImage = true;
BuildDockTile();
mozilla::SVGImageContext svgContext;
mozilla::SVGImageContext::MaybeStoreContextPaint(svgContext, aPaintContext,
aImage);
NSImage* image =
[MOZIconHelper iconImageFromImageContainer:aImage
withSize:NSMakeSize(256, 256)
svgContext:&svgContext
scaleFactor:0.0];
image.resizingMode = NSImageResizingModeStretch;
mDockBadgeView.image = image;
return UpdateDockTile();
NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE)
}
NS_IMETHODIMP
nsMacDockSupport::SetProgressState(nsTaskbarProgressState aState,
uint64_t aCurrentValue, uint64_t aMaxValue) {
@ -158,46 +211,67 @@ nsMacDockSupport::SetProgressState(nsTaskbarProgressState aState,
return UpdateDockTile();
}
void nsMacDockSupport::BuildDockTile() {
if (!mDockTileWrapperView) {
// Create the following NSView hierarchy:
// * mDockTileWrapperView (NSView)
// * imageView (NSImageView) <- has the application icon
// * mDockBadgeView (NSImageView) <- has the dock badge
// * mProgressDockOverlayView (MOZProgressDockOverlayView) <- draws the
// progress bar
mDockTileWrapperView =
[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 32, 32)];
mDockTileWrapperView.autoresizingMask =
NSViewWidthSizable | NSViewHeightSizable;
NSImageView* imageView =
[[NSImageView alloc] initWithFrame:[mDockTileWrapperView bounds]];
imageView.image = [NSImage imageNamed:@"NSApplicationIcon"];
imageView.imageScaling = NSImageScaleAxesIndependently;
imageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
[mDockTileWrapperView addSubview:imageView];
mDockBadgeView =
[[NSImageView alloc] initWithFrame:NSMakeRect(19.5, 19.5, 12, 12)];
mDockBadgeView.imageScaling = NSImageScaleProportionallyUpOrDown;
mDockBadgeView.autoresizingMask = NSViewMinXMargin | NSViewWidthSizable |
NSViewMaxXMargin | NSViewMinYMargin |
NSViewHeightSizable | NSViewMaxYMargin;
[mDockTileWrapperView addSubview:mDockBadgeView];
mProgressDockOverlayView = [[MOZProgressDockOverlayView alloc]
initWithFrame:NSMakeRect(1, 3, 30, 4)];
mProgressDockOverlayView.autoresizingMask =
NSViewMinXMargin | NSViewWidthSizable | NSViewMaxXMargin |
NSViewMinYMargin | NSViewHeightSizable | NSViewMaxYMargin;
[mDockTileWrapperView addSubview:mProgressDockOverlayView];
}
}
nsresult nsMacDockSupport::UpdateDockTile() {
NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
if (mProgressState == STATE_NORMAL || mProgressState == STATE_INDETERMINATE) {
if (!mDockTileWrapperView) {
// Create the following NSView hierarchy:
// * mDockTileWrapperView (NSView)
// * imageView (NSImageView) <- has the application icon
// * mProgressDockOverlayView (MOZProgressDockOverlayView) <- draws the
// progress bar
if (mProgressState == STATE_NORMAL || mProgressState == STATE_INDETERMINATE ||
mHasBadgeImage) {
BuildDockTile();
mDockTileWrapperView =
[[NSView alloc] initWithFrame:NSMakeRect(0, 0, 32, 32)];
mDockTileWrapperView.autoresizingMask =
NSViewWidthSizable | NSViewHeightSizable;
NSImageView* imageView =
[[NSImageView alloc] initWithFrame:[mDockTileWrapperView bounds]];
imageView.image = [NSImage imageNamed:@"NSApplicationIcon"];
imageView.imageScaling = NSImageScaleAxesIndependently;
imageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
[mDockTileWrapperView addSubview:imageView];
mProgressDockOverlayView = [[MOZProgressDockOverlayView alloc]
initWithFrame:NSMakeRect(1, 3, 30, 4)];
mProgressDockOverlayView.autoresizingMask =
NSViewMinXMargin | NSViewWidthSizable | NSViewMaxXMargin |
NSViewMinYMargin | NSViewHeightSizable | NSViewMaxYMargin;
[mDockTileWrapperView addSubview:mProgressDockOverlayView];
}
if (NSApp.dockTile.contentView != mDockTileWrapperView) {
NSApp.dockTile.contentView = mDockTileWrapperView;
}
mDockBadgeView.hidden = !mHasBadgeImage;
if (mProgressState == STATE_NORMAL) {
mProgressDockOverlayView.fractionValue = mProgressFraction;
} else {
mProgressDockOverlayView.hidden = false;
} else if (mProgressState == STATE_INDETERMINATE) {
// Indeterminate states are rare. Just fill the entire progress bar in
// that case.
mProgressDockOverlayView.fractionValue = 1.0;
mProgressDockOverlayView.hidden = false;
} else {
mProgressDockOverlayView.hidden = true;
}
[NSApp.dockTile display];
} else if (NSApp.dockTile.contentView) {

View File

@ -24,6 +24,7 @@
#include "MOZIconHelper.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/SVGImageContext.h"
#include "nsCocoaUtils.h"
#include "nsComputedDOMStyle.h"
#include "nsContentUtils.h"
@ -152,11 +153,17 @@ nsresult nsMenuItemIconX::OnComplete(imgIContainer* aImage) {
mIconImage = nil;
}
RefPtr<nsPresContext> pc = mPresContext.get();
UniquePtr<SVGImageContext> svgContext;
if (pc && mComputedStyle) {
svgContext = MakeUnique<SVGImageContext>();
SVGImageContext::MaybeStoreContextPaint(*svgContext, *pc, *mComputedStyle,
aImage);
}
mIconImage = [[MOZIconHelper
iconImageFromImageContainer:aImage
withSize:NSMakeSize(kIconSize, kIconSize)
presContext:pc
computedStyle:mComputedStyle
svgContext:svgContext.get()
scaleFactor:0.0f] retain];
mComputedStyle = nullptr;
mPresContext = nullptr;

View File

@ -127,8 +127,7 @@ nsresult nsTouchBarInputIcon::OnComplete(imgIContainer* aImage) {
NSImage* image = [MOZIconHelper
iconImageFromImageContainer:aImage
withSize:NSMakeSize(kIconHeight, kIconHeight)
presContext:nullptr
computedStyle:nullptr
svgContext:nullptr
scaleFactor:kHiDPIScalingFactor];
[mButton setImage:image];
[mShareScrubber setButtonImage:image];

View File

@ -5,6 +5,8 @@
#include "nsISupports.idl"
interface nsIStandaloneNativeMenu;
interface nsISVGPaintContext;
interface imgIContainer;
/**
* Allow applications to interface with the Mac OS X Dock.
@ -33,10 +35,17 @@ interface nsIMacDockSupport : nsISupports
void activateApplication(in boolean aIgnoreOtherApplications);
/**
* Text used to badge the dock tile.
* Text used to badge the dock tile. Setting this will remove any badge image.
*/
attribute AString badgeText;
/**
* An image to add to the dock icon as a badge. Setting this will remove any
* badgeText. If an SVG image is passed the given paint context is used to
* set the stroke and fill properties.
*/
void setBadgeImage(in imgIContainer aBadgeImage, [optional] in nsISVGPaintContext aPaintContext);
/**
* True if this app is in the list of apps that are persisted to the macOS
* Dock (as if the user selected "Keep in Dock").