Vector images (resource/vectors/, the vector piece)
This works like resource/images/ but is resolution-independent: SVG glyphs are staged per
backend into whatever form its toolkit loads natively and surfaced through typed constants and
one piece:
vector(res::vectors::home).tint(color).frame(24.0, 24.0)
res::vectors::… constants are generated by day-build (a VectorName per file, so a typo is a
compile error and presence is guaranteed). The name may also be a signal or a closure:
vector(move || slot.kind().read().icon()) swaps the glyph in the view already on screen and keeps
its template and tint, which is how a recycled list row’s icon follows its row. Vector and image names share one per-backend resolution
namespace (From<VectorName> for ImageName), so the name-based channels (nav-item icons, tab
icons, toolbar_button(...).image(…)) accept vectors unchanged.
Source forms
- Plain
.svg— the whole document is the glyph (raw Material Symbols downloads work as-is). - SF Symbol template
.svg— the SF Symbols app’s export (and third-party generators of the same shape):#SymbolsholdingWeight-Scalevariant groups. Day reduces it to the canonical Regular variant for non-Apple targets (cut out textually, re-boxed to its measured content). .symbolset/bundle — an Xcode custom-symbol bundle; its inner template SVG routes through the same reduction.
Text must be outlined (<text> is a hard build error; shaping is not compiled in).
Per-backend staging (§18.3)
At build, every glyph gets a raster cache PNG (build/day/vectors/raster/, 256 px), a
staged glyph SVG (build/day/vectors/svg/), and, where the art converts, XAML geometry
(build/day/vectors/xaml/). day pack stages the same caches on the one path that does not
run a build first (iOS packs straight through xcodebuild), so a packed app carries its glyphs
whether or not the tree was built before.
The raster cache is a build input, not a shipping form. A target’s
build/day/vectors/fallback/<toolkit>/ contains only art its vector pipeline cannot express.
GTK and Qt render the staged SVGs and ship no automatic raster copies.
Bundling the rest would ship a second copy of every glyph and would let a broken
vector path go unnoticed behind a raster that still looks right (two XAML bugs survived their
first review that way). day build reports the split per target
(Vectors xaml: 81/81 glyph(s) vector). What ships on top:
| Backend | Native form |
|---|---|
| android-mdc | VectorDrawable in res/drawable/ (solid fills/strokes, both fill rules, and linear/radial gradients on fill and stroke via aapt:attr, API 24, which is the scaffold’s minSdk; clips/masks/filters, and the gradients VD’s model cannot state, fall back to the raster at xxxhdpi, loudly). Intrinsic dimensions retain the SVG’s authored size and proportions, so native fit/crop matches the viewport without introducing rounding from a reduced icon size. A VD gradient carries no transform: linear bands are perpendicular to start→end and a radial is a true circle, so a gradient sheared out of perpendicular, a radial stretched to an ellipse, or an SVG focal point rasterizes rather than drawing subtly wrong art. gradientUnits="objectBoundingBox" is not such a case; usvg resolves it to user space before the emitter sees it |
| linux-gtk, macos-gtk, windows-gtk | retained SVG through librsvg 2.52+ and Cairo in a GdkPaintable. GtkPicture supplies fit/fill/stretch; snapshots render the geometry at the requested size. Icon paintables declare logical dimensions and are centered in their controls |
| linux-qt, macos-qt, windows-qt | retained SVG through QSvgRenderer in the image widget and a QIconEngine for SVG icon slots. Geometry is rendered at paint time; tint layers use the painter’s device transform, including screenshot scale, instead of a fixed pixmap size |
| macos-appkit | the staged SVG via DAY_VECTOR_SVG_ROOT (NSImage renders SVG files at display size on macOS 11+); other desktop dev launches use the raster cache via DAY_VECTOR_RASTER_ROOT (probed by resolve_image_file) |
| ios-uikit | the SVG in a DayPieces asset-catalog imageset with preserves-vector-representation (Xcode 12+), so UIImage(named:) renders at display size |
| web-dom | the SVG beside assets/images/; day-dom asks for .svg for the names in the page’s window.__DAY_VECTORS list (via the shim’s vector: env keys) and the browser renders it; the raster stays beside it as the older-host fallback |
| harmony-arkui | the SVG in rawfile day/ (same stem as the raster); day-arkui probes day/<name>.svg via the resource manager and ArkUI’s Image renders it natively; raster names keep the png |
| windows-winui | XAML geometry via DAY_VECTOR_XAML_ROOT: a Path in a scaling Viewbox in page content, a PathIcon in the nav pane’s icon slot, so the glyph is redrawn at every size rather than scaled from a cache. Emitted by the CLI (day_vector::to_xaml_geometry) as absolute M/L/C/Z only: the grammar is SVG’s, but a command XAML’s parser rejects fails the whole geometry, so quadratics are elevated to cubics (exact) and arcs are flattened upstream by usvg. Same subset as the VectorDrawable emission — gradients/clips/masks/filters stage no geometry and fall back to the raster |
GTK’s tint is a render-node color matrix that preserves coverage alpha. Qt uses a fresh
alpha-compositing layer at the current paint resolution. Both preserve authored strokes,
fill rules, holes, and partial opacity; resizing does not enlarge a previously rendered
image. Parsed documents are retained across repaints and tint changes. Qt toolbar ink
normalization measures bounds once, but paints the original geometry at every size.
Native button icons follow the button’s foreground, including reactive tint changes.
GTK controls that accept only an icon name, such as AdwViewSwitcher and settings group
icons, still use the existing symbolic PNG cache at 4× icon size. Those constrained slots
are not covered by the retained-paintable path; making them resolution independent requires
separate work on the icon-theme integration. Ordinary GTK image widgets, button icons,
sidebar rows, and toolbar icons use the retained SVG path.
The symbolic cache explicitly selects the SVG decoder, so leading license comments do not
make a valid glyph fail image-format detection. Its decode preserves aspect ratio and alpha.
These backends require development libraries at build time: qt6-svg-dev for Qt and
librsvg2-dev (2.52+) for GTK on Debian/Ubuntu; Homebrew’s qt / librsvg, or MSYS2’s
qt6-svg / librsvg packages on the other desktops. day doctor checks them. SVG feature
coverage follows the native renderer version; this does not add browser scripting or SVG
animation to Day’s static vector contract.
Packed desktop apps carry the same forms without the launch env: a .app ships
Contents/Resources/vectors/{svg,raster} (probed exe-relative: the SVGs by
resolve_vector_svg, the rasters by resolve_image_file); an AppImage/flatpak ships
share/<name>/vectors/{raster,svg} with the launcher exporting the same
DAY_VECTOR_*_ROOT roots a dev launch would; the Windows payload merges the raster cache
into the exe-relative images/, where both the piece resolution and the nav rows’
ms-appx:///images/ loads already look, and ships vectors/xaml beside the exe; geometry
specs are not loadable images, so they cannot merge into images/. The glyph SVGs do not ship
to Windows at all: that backend draws the geometry and nothing there reads an SVG.
Weights
vector(...).weight(VectorWeight::Light | Bold) resolves through __light/__bold suffixed
names; Regular is the glyph’s own name.
Template-form sources (SF template SVGs, .symbolset bundles) contribute true per-weight art
(their Light/Bold variants, with the template’s own fallback ladder for sparse exports) and stage
all three.
A plain SVG has no weight axis, so it stages once: its suffixed names alias back to the base
glyph in every resolver (resolve_image_file, resolve_vector_svg, resolve_vector_xaml,
Android’s drawableByName, and web-dom’s URL builder), so .weight(…) degrades to Regular rather
than to a missing asset, on every backend. Aliasing costs far less than copying: staging the
variants instead cost 3× with no benefit, and in Day-Showcase 38 of 39 sources were byte-identical
across all three names (117 staged glyphs and 708 KB of rasters became 41 and 244 KB).
An exact variant always wins over an alias, so a template’s real __bold art is never shadowed by
its own Regular.
Tint
.tint(color) recolors a monochrome glyph on every backend: template rendering +
contentTintColor on AppKit, alwaysTemplate + tintColor on UIKit, setImageTintList on
Android, pixel recolor on GTK, a QPainter SourceIn fill over the rendered glyph on Qt, a CSS mask
painted with the tint on web (a tinted glyph is a masked element, not an <img>, because the
browser cannot recolor an image’s own pixels), native SourceIn compositing on ArkUI,
and a brush on the Path shapes on XAML, composed over the geometry when the glyph is realized.
One staged glyph therefore serves every tint at every size without a second asset.
None (and every raster image(…)) means “as authored”.
ArkUI applies its tint layer only to SVG sources: strokes, unfilled interiors and partial opacity survive tinting. Replacing the SVG with a bitmap hides the tint layer; replacing it with another SVG restores the requested tint. HarmonyOS packaging also writes explicit intrinsic SVG dimensions, resolved from the original document, so ArkUI’s native Image honors fit/fill/stretch for viewBox-only assets without rasterizing them.
A tint can follow a signal. .tint(…) takes a plain color or anything reactive; a change
repaints the realized view through ImagePatch::Tint rather than rebuilding it, so a glyph that
recolors with the selection or the theme keeps its native view (and its layout, and any animation
in flight). Implemented on AppKit, UIKit, Android, GTK, Qt, WinUI and web; ArkUI still takes the
tint at realize only, so a reactive tint there lands on the next rebuild.
WinUI retains a viewport while replacing its vector or bitmap content. Fit centers the art, fill crops at the viewport, and source swaps preserve the content mode and tint. Explicit vector tints multiply the authored fill/stroke alpha by the tint alpha.
A tint follows the art it was authored with: the color fills where the glyph filled and strokes
where it stroked, so an outline glyph stays an outline rather than becoming a silhouette. Where
XAML has no geometry to draw (art outside the convertible subset) the tint degrades to a
monochrome BitmapIcon over the raster (still tinted, but from the 256 px cache).
Windows navigation and toolbar slots use a filled PathIcon. Before assigning its geometry,
the backend widens SVG strokes into outlines and unions the painted shapes with Direct2D.
This preserves stroke widths, caps, joins, and holes without rasterizing; concatenating the
original centerlines would incorrectly fill outline icons into solid silhouettes.
Nav-menu rows have their own arm, item(…).icon_tint(color) (docs/navigation.md): per-row
recolor on AppKit (contentTintColor), UIKit (tintColor), GTK (a render-node color matrix), Qt
(QPainter SourceIn at the current paint resolution), Android (compound-drawable setTint, best-effort after
the nav host mounts), ArkUI (SVG SourceIn compositing), XAML (Foreground on the row’s PathIcon), and
web (the mask painted with the tint instead of currentColor). Untinted rows keep each
backend’s template default (theme foreground / secondary label); ArkUI’s untinted raster rows
draw as authored (Day applies the tint layer only to SVG sources).
Template
.template() on vector(…) or image(…) gives a glyph of your own that same template default:
untinted, it draws in the foreground of the surface it sits on and follows a theme switch, instead
of in its authored colors. A bundled glyph is black on transparent, so an untinted image() of one
disappears on a dark surface; a template is what a row icon in a list of your own wants, and what
the data-driven sidebars (collapsed_sections, reorder_items) use for theirs.
row((vector(res::vectors::folder).template().frame(20.0, 20.0), label(name)))
Per backend the foreground is AppKit’s neutral template tint (isTemplate with no
contentTintColor), the inherited tintColor on UIKit (a cell’s, a bar’s — the iOS idiom), the
theme’s primary text color on Android (textColorPrimary) and ArkUI (SVG only; a raster template
draws as authored), the palette text color on Qt and the theme foreground on GTK (both re-rendered
on a palette change), the island’s TextFillColorPrimaryBrush on XAML (re-resolved when the
theme changes), and currentColor on the web, so it also turns white on a selected row. A
.tint(…) still wins while it is set, and clearing it (ImagePatch::Tint(None)) returns a
template to the foreground, not to the authored art. A source swap on a template or tinted glyph
re-cuts the mask from the new name, so a recycled row shows the icon it was rebound to.
Conformance
The conformance app stages real SVG fixtures and runs
vector-* pixel assertions on every active platform-toolkit target. It checks explicit tint,
unpainted regions, stroke caps, both fill rules, multiple sizes, and a native button icon slot. It also exercises resizing an
existing view, reactive tint
with partial opacity, fit/fill/stretch on non-square SVGs, SVG/bitmap source replacement,
and native button foreground updates.
Qt’s native qt-vectors test additionally checks icon pixmaps at 1×, 1.5×, 2× and 3×,
reactive foreground changes, and theme/outline symbol resolution. It runs with the existing
CMake/CTest native suite in CI.
Checking both painted and transparent regions prevents a missing icon from passing as an outline. The icon-slot case also exercises WinUI’s stroke-to-filled-geometry conversion.
A separate fine-detail fixture contains a line that is opaque at display size but never opaque in the 256px cache. Its pixel check and raster negative control catch that fallback being enlarged instead of drawing the vector. They do not prove the absence of every possible raster implementation. In particular, the current GTK and Qt standalone raster paths are expected to fail this case; there is no target exclusion concealing that limitation.
Lint
day lint validates every vector source: unreadable/unparseable art, glyph-embedded <text>
(a template’s Notes/Guides documentation text is fine; only the extracted glyph matters), an
empty .symbolset, a template whose Regular variant fails extraction, and, when android-mdc
is a declared target, art outside the VectorDrawable subset (day::lint::vector-raster-fallback,
a heads-up that Android ships the raster).
Not yet
Apple-native symbol weights (the .symbolset catalog staging that would enable them; staged
weights are template-extracted everywhere today, Apple included), and the opt-in runtime
rasterizer for downloaded SVGs (day-piece-remote-image’s planned svg arm).