Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Add new macOS backend app icon
  • Loading branch information
iccir committed Sep 6, 2026
commit d75a64a277ded39ef41aa0032f765d26fded0acd
4 changes: 3 additions & 1 deletion lib/matplotlib/backends/backend_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ def _allow_interrupt_macos():
@functools.lru_cache
def _init_macos():
data_path = cbook._get_data_path("images")
_macos._init({"matplotlib": str(data_path / "matplotlib.pdf")})
images = {path.stem: str(path) for path in data_path.glob("macos_*.png")}
use_dark_appicon = mpl.rcParams["macos.app_icon"] == "dark"
_macos._init(images, use_dark_appicon)


class FigureCanvasMac(_macos.FigureCanvas, FigureCanvasBase):
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions lib/matplotlib/mpl-data/matplotlibrc
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,7 @@
#savefig.orientation: portrait # orientation of saved figure, for PostScript output only

### macos backend params
#macos.app_icon: light # The appearance of the app icon (light, dark)
#macos.appearance: system # The appearance of windows and controls (system, light, dark)
#macos.window_mode: system # How to open new figures (system, tab, window)
# 'system' uses 'Prefer tabs...' from System Settings
Expand Down
8 changes: 8 additions & 0 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,7 @@ def _convert_validator_spec(key, conv):
"savefig.directory": _validate_pathlike,
"savefig.transparent": validate_bool,

"macos.app_icon": ["light", "dark"],
"macos.appearance": ["system", "light", "dark"],
"macos.window_mode": ["system", "tab", "window"],
"macosx.window_mode": ["system", "tab", "window"],
Expand Down Expand Up @@ -3429,6 +3430,13 @@ class _Subsection:
description="orientation of saved figure, for PostScript output only"
),
_Subsection("macOS backend parameters"),
_Param(
"macos.app_icon",
default="light",
type=Literal["light", "dark"],
validator=["light", "dark"],
description="Whether the application icon uses a light or dark style"
),
_Param(
"macos.appearance",
default="system",
Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@
"lines.scale_dashes",
"lines.solid_capstyle",
"lines.solid_joinstyle",
"macos.app_icon",
"macos.appearance",
"macos.window_mode",
"macosx.window_mode",
Expand Down
4 changes: 3 additions & 1 deletion src/macos/MPLAppDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ NS_ASSUME_NONNULL_BEGIN

@interface MPLAppDelegate : NSObject <NSApplicationDelegate>

- (instancetype) initWithImageDictionary:(MPLStringDictionary *)imageDictionary;
- (instancetype) initWithImageDictionary: (MPLStringDictionary *) imageDictionary
useDarkIcon: (BOOL) useDarkIcon;

@property (nonatomic, readonly) MPLStringDictionary *imageDictionary;
@property (nonatomic, readonly) BOOL useDarkIcon;

@end

Expand Down
95 changes: 89 additions & 6 deletions src/macos/MPLAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ @implementation MPLAppDelegate

#pragma mark - Lifecycle

- (instancetype) initWithImageDictionary:(MPLStringDictionary *)imageDictionary
- (instancetype) initWithImageDictionary: (MPLStringDictionary *) imageDictionary
useDarkIcon: (BOOL) useDarkIcon
{
if ((self = [super init])) {
_imageDictionary = imageDictionary;
_useDarkIcon = useDarkIcon;

MPLLog("[Lifecycle] MPLAppDelegate<%p> init", self);
}

Expand Down Expand Up @@ -133,12 +136,92 @@ - (void) _buildMainMenu

- (void) _buildAppIcon
{
NSString *imagePath = [_imageDictionary objectForKey:@"matplotlib"];
NSURL *imageURL = imagePath ? [NSURL fileURLWithPath:imagePath] : nil;
MPLStringDictionary *imageDictionary = _imageDictionary;
NSImage *shadowImage;
NSImage *maskImage;
NSImage *darkBackgroundImage;

BOOL useDarkIcon = _useDarkIcon;

__auto_type getImage = ^(NSString *key) {
NSString *path = [imageDictionary objectForKey:key];
return path ? [[NSImage alloc] initWithContentsOfFile:path] : nil;
};

if (@available(macOS 27.0, *)) {
darkBackgroundImage = getImage(@"macos_appicon_bgdark27");
} else if (@available(macOS 26.0, *)) {
darkBackgroundImage = getImage(@"macos_appicon_bgdark26");
}

if (@available(macOS 26.0, *)) {
shadowImage = getImage(@"macos_appicon_shadow26");
maskImage = getImage(@"macos_appicon_mask26");
} else {
shadowImage = getImage(@"macos_appicon_shadow11");
maskImage = getImage(@"macos_appicon_mask11");
}

if (imageURL) {
NSImage *image = [[NSImage alloc] initWithContentsOfURL:imageURL];
[NSApp setApplicationIconImage:image];
NSImage *contentImage = useDarkIcon ?
getImage(@"macos_appicon_dark") :
getImage(@"macos_appicon_light");

if (!maskImage || !shadowImage || !contentImage) return;

// Use standard size of 256 physical pixels (128x128 @2x)
CGSize iconSize = CGSizeMake(256, 256);
CGImageRef iconCGImage = MPLCreateImage(iconSize, 1, ^(CGContextRef context) {
CGRect iconBounds = CGRectMake(0, 0, iconSize.width, iconSize.height);

__auto_type getCGImage = ^(NSImage *nsImage) {
CGRect proposedRect = iconBounds;
return [nsImage CGImageForProposedRect: &proposedRect
context: [NSGraphicsContext currentContext]
hints: nil];
};

__auto_type getFrame = ^(CGImageRef image) {
CGSize size = CGSizeMake(CGImageGetWidth(image), CGImageGetHeight(image));
return MPLGetCenteredRect(iconBounds, size);
};

CGImageRef maskCGImage = MPLCopyGrayscaleNonAlphaImage(getCGImage(maskImage));
CGImageRef shadowCGImage = getCGImage(shadowImage);
CGImageRef contentCGImage = getCGImage(contentImage);
CGImageRef darkBGCGImage = getCGImage(darkBackgroundImage);

// MPLCreateImage() uses an upper-left-origin to match other graphics libraries.
// Reset to lower-left-origin coordinate system since we are drawing images,
// as CGContextDrawImage() translates the CTM by the image height and flips
// prior to drawing.
CGContextTranslateCTM(context, 0, iconSize.height);
CGContextScaleCTM(context, 1, -1);

CGContextDrawImage(context, getFrame(shadowCGImage), shadowCGImage);
CGContextClipToMask(context, getFrame(maskCGImage), maskCGImage);

if (useDarkIcon) {
if (darkBackgroundImage) {
CGContextDrawImage(context, getFrame(darkBGCGImage), darkBGCGImage);
} else {
NSGradient *gradient = [[NSGradient alloc] initWithColors:@[
MPLGetRGBColor(0x303030, 1.0),
MPLGetRGBColor(0x181818, 1.0)
]];

[gradient drawInRect:CGRectMake(0, 0, 256, 256) angle:-90];
}
}

CGContextDrawImage(context, getFrame(contentCGImage), contentCGImage);

CGImageRelease(maskCGImage);
});

if (iconCGImage) {
NSImage *iconImage = [[NSImage alloc] initWithCGImage:iconCGImage size:iconSize];
[NSApp setApplicationIconImage:iconImage];
CGImageRelease(iconCGImage);
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/macos/MPLUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,10 @@ extern CGImageRef _Nullable MPLCreateImage(
);


/*
Copy a grayscale non-alpha version of inImage to use with CGContextClipToMask()
*/
extern CGImageRef _Nullable MPLCopyGrayscaleNonAlphaImage(CGImageRef _Nullable inImage);


NS_ASSUME_NONNULL_END
16 changes: 15 additions & 1 deletion src/macos/MPLUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,22 @@ static CGImageRef _Nullable sCreateImage(
}


CGImageRef MPLCreateImage(CGSize size, CGFloat scale, void (^callback)(CGContextRef))
CGImageRef _Nullable MPLCreateImage(CGSize size, CGFloat scale, void (^callback)(CGContextRef))
{
CGBitmapInfo bitmapInfo = 0 | kCGImageAlphaPremultipliedFirst | kCGImageByteOrder32Little;
return sCreateImage(size, scale, YES, kCGColorSpaceSRGB, 4, bitmapInfo, callback);
}


_Nullable CGImageRef MPLCopyGrayscaleNonAlphaImage(_Nullable CGImageRef inImage)
{
if (!inImage) return NULL;

CGSize size = CGSizeMake(CGImageGetWidth(inImage), CGImageGetHeight(inImage));
CFStringRef colorSpaceName = kCGColorSpaceGenericGrayGamma2_2;
CGBitmapInfo bitmapInfo = 0 | kCGImageAlphaNone;

return sCreateImage(size, 1, NO, colorSpaceName, 1, bitmapInfo, ^(CGContextRef context) {
CGContextDrawImage(context, CGRectMake(0, 0, size.width, size.height), inImage);
});
}
7 changes: 5 additions & 2 deletions src/macos/_macos.m
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,8 @@ static int sInputHook(void)
BEGIN_OBJC_ENTRY

PyObject *imagesPyDict;
if (!PyArg_ParseTuple(args, "O!", &PyDict_Type, &imagesPyDict)) { return NULL; }
int useDarkIcon;
if (!PyArg_ParseTuple(args, "O!i", &PyDict_Type, &imagesPyDict, &useDarkIcon)) { return NULL; }

NSDictionary *imagesDictionary = MPLGetStringDictionaryWithPyDict(imagesPyDict);
if (!imagesDictionary) { return NULL; }
Expand All @@ -948,7 +949,9 @@ static int sInputHook(void)
}

if (![NSApp delegate]) {
sAppDelegate = [[MPLAppDelegate alloc] initWithImageDictionary:imagesDictionary];
sAppDelegate = [[MPLAppDelegate alloc] initWithImageDictionary: imagesDictionary
useDarkIcon: (useDarkIcon > 0)];

[NSApp setDelegate:sAppDelegate];
}

Expand Down
38 changes: 31 additions & 7 deletions tools/make_icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import argparse
import subprocess
import shutil
from fnmatch import fnmatch
from pathlib import Path
import xml.etree.ElementTree as ElementTree
from xml.etree.ElementTree import Element
Expand Down Expand Up @@ -216,6 +217,20 @@ def process_toolbar_icon(
converter.export_png(png_large_path, width=48, mode="GrayAlpha_8")


def process_macos_icon(icon: Path, dest_dir: Path, converter: ImageConverter) -> None:
if (fnmatch(icon.stem, "macos_appicon_mask*")):
mode = "Gray_8"
elif (fnmatch(icon.stem, "macos_appicon_shadow*")):
mode = "GrayAlpha_8"
elif (fnmatch(icon.stem, "macos_appicon_light")):
mode = "RGB_8"
else:
mode = "RGBA_8"

converter.open_svg(icon)
converter.export_png(dest_dir / f"{icon.stem}.png", mode=mode)


def make_icons() -> None:
parser = argparse.ArgumentParser(
description="Validate and convert SVG icons to PNG/PDF.")
Expand All @@ -230,6 +245,11 @@ def make_icons() -> None:
type=Path,
default=Path(__file__).parent / DEFAULT_IMAGES_PATH,
help="Directory where to write the PNG/PDF files.")
parser.add_argument(
"--macos",
action="store_true",
help="Process SVG files from make_macos_svgs.py tool")

args = parser.parse_args()

source_dir = args.source_dir
Expand All @@ -247,13 +267,17 @@ def make_icons() -> None:

converter = ImageConverter()

for name in TOOLBAR_ICON_NAMES:
process_toolbar_icon(name, source_dir, dest_dir, converter)

for name in ("matplotlib", "matplotlib_small"):
converter.open_svg(source_dir / f"{name}.svg")
converter.export_pdf(dest_dir / f"{name}.pdf")
converter.export_png(dest_dir / f"{name}.png")
if args.macos:
for icon in source_dir.glob("macos_*.svg"):
process_macos_icon(icon, dest_dir, converter)
else:
for name in TOOLBAR_ICON_NAMES:
process_toolbar_icon(name, source_dir, dest_dir, converter)

for name in ("matplotlib", "matplotlib_small"):
converter.open_svg(source_dir / f"{name}.svg")
converter.export_pdf(dest_dir / f"{name}.pdf")
converter.export_png(dest_dir / f"{name}.png")

converter.run()

Expand Down
Loading
Loading