Skip to content

feat: SF Symbols variable-value rendering#813

Open
enieuwy wants to merge 1 commit into
FelixKratz:masterfrom
enieuwy:feat/sf-symbols-variable-rendering
Open

feat: SF Symbols variable-value rendering#813
enieuwy wants to merge 1 commit into
FelixKratz:masterfrom
enieuwy:feat/sf-symbols-variable-rendering

Conversation

@enieuwy

@enieuwy enieuwy commented May 13, 2026

Copy link
Copy Markdown

Why

Adds variable-value SF Symbol rendering as a new image source. The use case is progress-style indicators (battery fill, wifi/speaker level, audio waveform, custom dials) driven by a single symbol name + percentage, without managing N pre-rendered PNGs or N font glyphs.

This is different from #63 (closed in 2021): that discussion was about static SF Symbols, which already work fine by pasting the glyph into icon/label text. Variable-value rendering cannot be expressed as a Unicode glyph — Apple generates a per-frame Core Graphics image from a symbol name + a [0, 1] value, and that image is what changes as the value changes.

Issue links

I did not find an open issue that this should close. I searched existing issues for SF Symbols, variable/value symbols, symbol images, battery/progress indicators, image tinting, symbol color, and palette support. The closest historical issue is #43 (SF Symbols), but it is already closed and was about static SF Symbol support; this PR adds variable-value image rendering instead. Related discussion context: #63.

What

Four additions to the existing image sub-domain. No breaking changes, no new sub-domain, no new frameworks linked.

# Use an SF Symbol as the image source.
sketchybar --set battery background.image=sf.battery.100percent.circle

# Drive the variable value. Accepts ints in 0..100 or floats in 0.0..1.0.
sketchybar --set battery background.image.percentage=75
sketchybar --set battery background.image.percentage=0.75

# Optional rendering mode (macOS 26+; accepted but ignored on older systems).
sketchybar --set battery background.image.variable_value_mode=draw

# Optional monochrome symbol tint, useful on dark/transparent bars.
sketchybar --set battery background.image.symbol_color=0xff25be6a

Works with icon.background.image=, label.background.image=, and background.image= — anywhere image already accepts a source.

--query round-trips the new fields when the image source is an SF Symbol:

"image": {
  "value": "sf.battery.100percent.circle",
  "drawing": "on",
  "scale": 1.000000,
  "symbol": "battery.100percent.circle",
  "percentage": 0.750000,
  "variable_value_mode": "automatic",
  "symbol_color": "0xff25be6a"
}

symbol_color is emitted only when explicitly set, so existing configs and query output for untinted SF Symbols remain unchanged.

How

  • New src/symbol.{h,m} wraps +[NSImage imageWithSystemSymbolName:variableValue:accessibilityDescription:] behind a runtime respondsToSelector: check. The compile-time SDK already ships NSImageSymbolVariableValueMode (macOS 26), so the mode branch is guarded with @available(macOS 26.0, *) rather than NSInvocation tricks.
  • image_load adds an sf.* branch alongside app.* and space.*. The existing get_key_value_pair splits on the first ., so sf.battery.100percent.circle correctly extracts battery.100percent.circle as the symbol name.
  • Percentage uses the existing ANIMATE_FLOAT track, so --animate sin 60 --set foo background.image.percentage=… animates for free. Verified locally — 0.42 → 1.0 over 60 ticks interpolates correctly.
  • symbol_color uses NSImageSymbolConfiguration configurationWithHierarchicalColor: when set. This is intentionally narrow: one ARGB tint for monochrome symbols. Palette, multicolor, and symbol-rendering-style configuration are still left out.
  • SF Symbol images are rasterized at 2x the logical symbol size and drawn with high interpolation. Other image sources keep the existing global kCGInterpolationNone behavior.
  • image_set_image's data-memcmp dedup short-circuits the redraw when the re-rendered symbol pixels are identical to the previous frame, so no churn at steady state. No extra cache layer needed.
  • Dual-unit percentage parse: a literal . in the token routes through token_to_float (Apple's native units); otherwise the token parses as an int and is divided by 100 (matches slider.percentage semantics).
  • Switching the image source away from sf.* clears the symbol state cleanly (verified via --query round-trip). Symbol name is duplicated into image->symbol_name and freed in image_destroy/image_clear_pointers.

Compatibility

  • Build targets unchanged: x86_64-apple-macos10.13 and arm64-apple-macos11.
  • make x86, make arm64, make universal all build with zero new warnings on Xcode 26.5 / SDK MacOSX26.5.
  • Variable rendering itself requires macOS 13 or later (Apple API gate). On earlier macOS releases the sf. prefix is rejected with a clear error message; everything else (file paths, app.*, space.*, media.artwork) continues to work unchanged.
  • No new linker flags. AppKit is already linked.

Out of scope (intentional — happy to follow up if useful)

  • Palette / multicolor tinting via NSImageSymbolConfiguration palette colors or explicit rendering-style controls. This PR only adds the minimal monochrome symbol_color tint needed to make SF Symbol images usable across light, dark, and transparent bars.
  • Native SF Symbol animations (bounce, pulse, scale, replace, variableColor cumulative). These need an offscreen NSImageView cache / per-item renderer change and felt too large for this PR.
  • Symbol weight / scale axes beyond the implicit 32pt regular baseline, matching workspace_icon_for_app's sizing.

Verification done locally

  • All three build flavors (x86, arm64, universal) build clean.
  • Unit-level tests (separate small program linking symbol.o) confirm: invalid names return NULL; valid names return distinct pixel data for different values; same params produce byte-identical output (dedup property); out-of-range values clamp.
  • Integration via the built binary: percentage as int and as float, mode round-trip, symbol tint round-trip, animation interpolation, switching source from sf.* to a file path and back — all verified through --query JSON.
  • Visual smoke with sf.circle + symbol_color=0xff78a9ff confirms tinted variable-draw output renders without a PNG fallback.

@enieuwy enieuwy force-pushed the feat/sf-symbols-variable-rendering branch 3 times, most recently from 7ebf7c2 to 248ba21 Compare May 13, 2026 09:06
Adds a new `sf.<name>` image source that renders SF Symbols with
Apple's variable-value API. Lets configs drive progress-style indicators
(battery fill, wifi/speaker level, audio waveform, custom dials) from a
single symbol name + percentage without managing pre-rendered PNGs.

API additions (no breaking changes):
  * `background.image=sf.<symbol-name>` (also under `icon.` and `label.`)
  * `background.image.percentage` accepts `0..100` integers or `0.0..1.0`
    floats; both are clamped to `[0,1]` internally
  * `background.image.variable_value_mode={automatic|color|draw}`
    (macOS 26+; parsed and stored on older systems for forward compat)
  * `--query` round-trips the new `symbol`, `percentage`, and
    `variable_value_mode` fields when the image is an SF Symbol

Implementation notes:
  * New `src/symbol.{h,m}` wraps
    `+[NSImage imageWithSystemSymbolName:variableValue:accessibilityDescription:]`
    behind a runtime `respondsToSelector:` check; gracefully errors on
    macOS <13.
  * Image dedup via existing `image_set_image` data `memcmp` path —
    identical pixel data short-circuits the redraw, so no churn at
    steady state.
  * Percentage uses the existing `ANIMATE_FLOAT` track, so
    `--animate <fn> <ticks> --set foo background.image.percentage=…`
    works for free.
  * Deployment targets (`x86_64-apple-macos10.13`,
    `arm64-apple-macos11`) and linked frameworks are unchanged.
@enieuwy enieuwy force-pushed the feat/sf-symbols-variable-rendering branch from 248ba21 to 652139a Compare May 13, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant