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): #Symbols holding Weight-Scale variant 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:

BackendNative form
android-mdcVectorDrawable 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-gtkretained 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-qtretained 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-appkitthe 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-uikitthe SVG in a DayPieces asset-catalog imageset with preserves-vector-representation (Xcode 12+), so UIImage(named:) renders at display size
web-domthe 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-arkuithe 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-winuiXAML 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).