Render SF Symbol based art at any requested size on macOS (PR #26909)

39 views
Skip to first unread message

Daniel Kulp

unread,
Aug 24, 2026, 4:27:52 PM (4 days ago) Aug 24
to wx-...@googlegroups.com, Subscribed

SF Symbols returned by +[NSImage imageWithSystemSymbolName:] have a tiny intrinsic size (roughly the system font point size), so bitmap bundles created from them render as a small glyph scaled up, or centered, in whatever size the consumer requests — giving blurry or undersized icons for wxArtProvider art and other SF Symbol based bitmaps.

This adds a dedicated wxBitmapBundleImpl for SF Symbols that keeps the symbol name and regenerates the underlying NSImage at each requested size using NSImageSymbolConfiguration (so the stroke weight matches the rendered size). Since the symbols are effectively vector images, this produces crisp output at every size and display scale. The template flag is preserved so AppKit continues to tint the symbols correctly for light/dark appearance, and the native image cache is pre-populated so code paths retrieving the bundle's NSImage directly still get a proper template image.

wxMacArtProvider now also picks a reasonable default size from the art client hint instead of the symbol's intrinsic size, so SF Symbol based art reports sizes comparable to the other art provider icons.

Only active on macOS 11+ where the SF Symbol APIs exist; older systems fall through to the existing named-image path.

🤖 Generated with Claude Code


You can view, comment on, or merge this pull request online at:

  https://github.com/wxWidgets/wxWidgets/pull/26909

Commit Summary

  • 7a5ac7c Render SF Symbol based art at any requested size on macOS

File Changes

(4 files)

Patch Links:

—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909@github.com>

Copilot

unread,
Aug 24, 2026, 4:44:40 PM (4 days ago) Aug 24
to wx-...@googlegroups.com, Subscribed

@Copilot commented on this pull request.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds a macOS 11+ SF Symbols–backed wxBitmapBundleImpl that regenerates NSImage instances at the requested size to avoid blurry/undersized bitmap bundles, and updates the Mac art provider to choose a better default size than the symbol’s intrinsic size.

Changes:

  • Introduce wxOSXSFSymbolBundleImpl to regenerate SF Symbol NSImages per requested size using NSImageSymbolConfiguration.
  • Update wxOSXCreateSystemBitmapBundle() to prefer SF Symbol bundles on macOS 11+ and fall back to legacy named-image behavior otherwise.
  • Adjust wxMacArtProvider to derive a default size from the art client hint when no explicit size is requested.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/osx/core/bmpbndl.mm Adds SF Symbols bundle impl and factory to generate crisp bitmaps per requested size on macOS 11+.
src/osx/carbon/utilscocoa.mm Routes system bitmap bundle creation through the new SF Symbols bundle when available.
src/osx/artmac.cpp Uses client-based size hints so SF Symbol art reports/uses reasonable default sizes.
include/wx/private/bmpbndl.h Declares the new SF Symbols bundle factory API used by platform code.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.


In src/osx/core/bmpbndl.mm:

> +    virtual wxSize GetPreferredBitmapSizeAtScale(double scale) const override
+    {
+        return m_defaultSize * scale;
+    }

wxSize stores integer dimensions, but scale is a double. Returning m_defaultSize * scale risks truncation/rounding issues at non-integer scales (e.g., 1.5/2.0 with fractional DIP-to-pixel conversions), and may even be ambiguous depending on available operator overloads. Prefer explicitly computing the scaled width/height using a consistent rounding strategy (e.g., round-to-nearest) to avoid off-by-1 sizing on HiDPI.


In src/osx/core/bmpbndl.mm:

> +                // Configure the symbol at a point size matching the
+                // requested dimension so the stroke weight is appropriate
+                // for the rendered size.
+                NSImageSymbolConfiguration* config =
+                    [NSImageSymbolConfiguration
+                        configurationWithPointSize:size.GetHeight()

The configuration’s point size is derived only from size.GetHeight(), but the rendered image size is later set with both (size.x, size.y). For non-square requests this can produce an unintuitive stroke weight relative to the overall target size. Consider basing point size on a single, explicit policy (e.g., max(width, height) or min(width, height)) and document why, so sizing behavior is predictable.


In src/osx/core/bmpbndl.mm:

> +        wxCFStringRef cfname(name);
+        NSImage* probe =
+            [NSImage imageWithSystemSymbolName:cfname.AsNSString()
+                      accessibilityDescription:nil];
+        if ( probe )
+        {
+            wxSize sz = defaultSize;
+            if ( sz == wxDefaultSize )
+                sz = wxSize(32, 32);
+            return wxBitmapBundle::FromImpl(
+                new wxOSXSFSymbolBundleImpl(name, sz));
+        }

This does a symbol lookup (imageWithSystemSymbolName:) to probe availability, and then wxOSXSFSymbolBundleImpl immediately performs another lookup when pre-populating the cache (and again on each bitmap request). To avoid the redundant initial lookup, consider either (1) letting the bundle impl perform the first lookup and exposing a lightweight validity check, or (2) passing the already-created NSImage* into the impl for initial cache population (so the probe result is reused).

—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909/review/5012388042@github.com>

Maarten

unread,
Aug 25, 2026, 6:06:56 PM (3 days ago) Aug 25
to wx-...@googlegroups.com, Subscribed
MaartenBent left a comment (wxWidgets/wxWidgets#26909)

Nice. This seems to fix/improve image sizes in the artprov example. But it exposes a problem with image sizes in a wxListCtrl. See the before and after screenshots with the applied patch.

Like you said, before images get a size similar to the font point size. But after, wxDrawImageBitmap draws the image too big. It looks like a 64x64 bitmap, while I would expect 32x32 on a Retina display when specifying 16x16.

Image: Screenshot 2026-08-25 at 23 42 18 (view on web)Image: Screenshot 2026-08-25 at 23 43 48 (view on web)

diff --git "a/samples/listctrl/listtest.cpp" "b/samples/listctrl/listtest.cpp"
index 62ce41daf6c..86b8c3db0f5 100644
--- "a/samples/listctrl/listtest.cpp"
+++ "b/samples/listctrl/listtest.cpp"
@@ -41,6 +41,7 @@
 #include "wx/sysopt.h"
 #include "wx/numdlg.h"
 #include "wx/selstore.h"
+#include "wx/artprov.h"
 
 #include "listtest.h"
 
@@ -191,9 +192,6 @@ MyFrame::MyFrame(const wxString& title)
     m_imagesNormal.push_back( wxIcon("icon7", wxBITMAP_TYPE_ICO_RESOURCE) );
     m_imagesNormal.push_back( wxIcon("icon8", wxBITMAP_TYPE_ICO_RESOURCE) );
     m_imagesNormal.push_back( wxIcon("icon9", wxBITMAP_TYPE_ICO_RESOURCE) );
-
-    m_imagesSmall.push_back( wxIcon("iconsmall", wxBITMAP_TYPE_ICO_RESOURCE, 16, 16) );
-
 #else
     m_imagesNormal.push_back( wxIcon( toolbrai_xpm ) );
     m_imagesNormal.push_back( wxIcon( toolchar_xpm ) );
@@ -204,9 +202,8 @@ MyFrame::MyFrame(const wxString& title)
     m_imagesNormal.push_back( wxIcon( toolgame_xpm ) );
     m_imagesNormal.push_back( wxIcon( tooltime_xpm ) );
     m_imagesNormal.push_back( wxIcon( toolword_xpm ) );
-
-    m_imagesSmall.push_back( wxIcon( small1_xpm) );
 #endif
+    m_imagesSmall.push_back(wxArtProvider::GetBitmapBundle(wxART_ERROR, wxART_MENU, wxSize(16, 16)));
 
     // Make a menubar
     wxMenu *menuFile = new wxMenu;

—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909/c5417461158@github.com>

VZ

unread,
Aug 26, 2026, 10:56:38 AM (2 days ago) Aug 26
to wx-...@googlegroups.com, Subscribed
vadz left a comment (wxWidgets/wxWidgets#26909)

Thanks Maarten for testing this!

This problem needs to be fixed before this can be merged but I'm not qualified to do it.

—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909/c5427170222@github.com>

Daniel Kulp

unread,
Aug 26, 2026, 2:52:21 PM (2 days ago) Aug 26
to wx-...@googlegroups.com, Push

@dkulp pushed 2 commits.

  • 09205b8 Return SF symbol bitmaps at the requested size in pixels
  • 1bd712c Map wxART_TICK_MARK and wxART_CROSS_MARK to SF symbols on macOS

—
View it on GitHub or unsubscribe.


Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909/before/7a5ac7c791fda96f4cab807ffc3e835ce1e01ede/after/1bd712cbced014198207526b424887feb8eb57d4@github.com>

Daniel Kulp

unread,
Aug 26, 2026, 2:52:34 PM (2 days ago) Aug 26
to wx-...@googlegroups.com, Subscribed
dkulp left a comment (wxWidgets/wxWidgets#26909)

@MaartenBent thanks for catching this — root cause found and fixed in 09205b8.

wxBitmapBundleImpl::GetBitmap() receives the requested size in pixels and must return a bitmap of exactly that pixel size (wxBitmapBundle::GetBitmap() then adjusts its scale factor). The SF symbol implementation instead created an NSImage of that size in points and wrapped it in wxBitmap, which rasterizes using the main screen scale factor — so on a Retina display a 16x16 request produced a 32x32 pixel bitmap that was then drawn twice as big, exactly as in your wxListCtrl screenshot.

It now rasterizes the symbol directly at the requested pixel size, the same way the SVG-based bundle implementation does (including its last-bitmap cache). Verified with your listctrl change: GetBitmap(16,16) returns a 16x16 pixel bitmap, and the preferred-size-at-2x path returns 32x32 pixels with scale factor 2 (16 logical). The native NSImage cache still holds the point-sized template image, so code paths handing the image to AppKit controls (buttons, toolbars, menus) render byte-identically to before.

The Copilot comments are also addressed: no redundant symbol lookup when creating the bundle, rounding instead of truncation in GetPreferredBitmapSizeAtScale(), and a comment explaining the height-based symbol configuration.

While testing with the artprov sample we also noticed wxART_TICK_MARK/wxART_CROSS_MARK were the only standard art ids still stuck at a fixed 16x16 (they resolve to the default provider's XPMs), so 1bd712c maps them to the "checkmark"/"xmark" SF symbols too, with the XPM fallback intact pre-macOS 11.

🤖 Generated with Claude Code

—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909/c5429699569@github.com>

Daniel Kulp

unread,
Aug 26, 2026, 2:52:46 PM (2 days ago) Aug 26
to wx-...@googlegroups.com, Subscribed

@dkulp commented on this pull request.


In src/osx/core/bmpbndl.mm:

> +                // Configure the symbol at a point size matching the
+                // requested dimension so the stroke weight is appropriate
+                // for the rendered size.
+                NSImageSymbolConfiguration* config =
+                    [NSImageSymbolConfiguration
+                        configurationWithPointSize:size.GetHeight()

Kept the height-based configuration (symbols are laid out relative to the cap height, so the height determines the appropriate stroke weight) and documented it in a comment.

—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909/review/5034028324@github.com>

Daniel Kulp

unread,
Aug 26, 2026, 2:52:47 PM (2 days ago) Aug 26
to wx-...@googlegroups.com, Subscribed

@dkulp commented on this pull request.


In src/osx/core/bmpbndl.mm:

> +    virtual wxSize GetPreferredBitmapSizeAtScale(double scale) const override
+    {
+        return m_defaultSize * scale;
+    }

Done — GetPreferredBitmapSizeAtScale() now uses wxRound for both dimensions.

—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909/review/5034028146@github.com>

Daniel Kulp

unread,
Aug 26, 2026, 2:52:48 PM (2 days ago) Aug 26
to wx-...@googlegroups.com, Subscribed

@dkulp commented on this pull request.


In src/osx/core/bmpbndl.mm:

> +        wxCFStringRef cfname(name);
+        NSImage* probe =
+            [NSImage imageWithSystemSymbolName:cfname.AsNSString()
+                      accessibilityDescription:nil];
+        if ( probe )
+        {
+            wxSize sz = defaultSize;
+            if ( sz == wxDefaultSize )
+                sz = wxSize(32, 32);
+            return wxBitmapBundle::FromImpl(
+                new wxOSXSFSymbolBundleImpl(name, sz));
+        }

Restructured: the factory no longer probes separately — the ctor's cache pre-population doubles as the existence check (IsOk()), so there's exactly one lookup at bundle creation.

—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909/review/5034028492@github.com>

Daniel Kulp

unread,
Aug 26, 2026, 3:04:16 PM (2 days ago) Aug 26
to wx-...@googlegroups.com, Subscribed
dkulp left a comment (wxWidgets/wxWidgets#26909)
Image: wxlistctrl-sfsymbol-16px-fixed (view on web)

—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909/c5429739795@github.com>

Maarten

unread,
Aug 27, 2026, 5:44:49 PM (13 hours ago) Aug 27
to wx-...@googlegroups.com, Subscribed
MaartenBent left a comment (wxWidgets/wxWidgets#26909)

I can confirm the image size is fixed.
In your original PR the images get a different (better) colour. Is this caused by [symbol setTemplate:YES];?.
But with the latest commits this does not happen anymore. Can rasterizing the symbol handle this?

Image: Screenshot 2026-08-27 at 23 30 44 (view on web)Image: Screenshot 2026-08-27 at 23 29 44 (view on web)

—
Reply to this email directly, view it on GitHub, or unsubscribe.
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909/c5445582799@github.com>

Daniel Kulp

unread,
Aug 27, 2026, 10:38:50 PM (9 hours ago) Aug 27
to wx-...@googlegroups.com, Push

@dkulp pushed 1 commit.

  • 02a9c2b Preserve template rendering for rasterized SF symbol bitmaps

—
View it on GitHub or unsubscribe.


Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909/before/1bd712cbced014198207526b424887feb8eb57d4/after/02a9c2b7cd787f0b9adf848f975a83a8ab176263@github.com>

Daniel Kulp

unread,
12:07 AM (7 hours ago) 12:07 AM
to wx-...@googlegroups.com, Push

@dkulp pushed 1 commit.

  • 359885e Restore appearance tinting for rasterized SF symbol bitmaps

—
View it on GitHub or unsubscribe.


Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!

You are receiving this because you are subscribed to this thread.Message ID: <wxWidgets/wxWidgets/pull/26909/before/02a9c2b7cd787f0b9adf848f975a83a8ab176263/after/359885e11210f6ae57c972a2adca0234b85dabb4@github.com>

Reply all
Reply to author
Forward
0 new messages