Resources, images, fonts & icons

Images, fonts, icons, and data files are bundled with a Day app from its resource/ directory. The build prepares each file for the selected platform and generates typed names for use in Rust. The subdirectories distinguish how each resource is used:

myapp/
  resource/
    assets/    # data files: JSON, databases — anything you open as bytes
    images/    # raster UI images, with @2x/@3x density variants
    vectors/   # SVG glyphs, staged natively per backend
    fonts/     # custom fonts (.ttf/.otf), referenced by family name
    icons/     # one app-icon master; day icon build generates the renditions

All five subdirectories use each platform’s native resource system. On Android your images become res/drawable-* entries crunched by aapt2; on iOS they join an asset catalog; on GTK they compile into a GResource bundle; on Qt, a Qt resource file. day build does the staging automatically, per target, before the platform build runs.

Typed names, generated at build

You reference bundled resources through generated constants, not bare strings. The scaffold’s build.rs calls day_build::prebuild_project(), which writes a typed constant per file:

image(res::images::wave)                  // ← resource/images/wave.png
day::resource(res::assets::stations_json) // ← resource/assets/stations.json
vector(res::vectors::home)                // ← resource/vectors/home.svg

A typo, or a file that was renamed or deleted, is a compile error, and the names autocomplete. Dropping a file into resource/ makes its constant appear on the next build. For a name known only at runtime, ImageName::dynamic(…) / AssetName::dynamic(…) opt out of the presence guarantee explicitly.

Data files: resource/assets/

Anything in resource/assets/ is packaged and readable at runtime through one call:

let bytes: day::Resource = day::resource(res::assets::stations_json).expect("packaged asset");
let parsed: Stations = serde_json::from_slice(bytes.as_slice())?;

Resource is a zero-copy view: on Android it borrows straight from the AAssetManager buffer, on GTK from the GResource, on desktop from an mmap (no copy into a Vec unless you make one). read_at(offset, buf) gives random access for large files. During development the same call resolves against your project directory, so editing an asset and relaunching picks it up without a packaging step.

Images: resource/images/

Drop PNGs (with optional @2x/@3x density variants) into resource/images/ and reference them by name:

image(res::images::wave)   // finds wave.png / wave@2x.png / wave@3x.png
    .frame(240.0, 120.0)

At build time each toolkit gets the format it expects: density buckets on Android (drawable-xhdpi/…), an asset catalog imageset on iOS, resource bundles on GTK/Qt. The platform picks the right density at runtime the same way it does for any native app. The resources reference documents the exact per-platform staging.

  • resource/images/ is raster. Photos and artwork belong here, with @2x/@3x density variants; SVG glyphs belong in resource/vectors/ (next section), which ships them as vectors.
  • Remote images (URL-loaded, cached) are a separate piece, day-piece-remote-image, because they involve networking and cache policy that the core leaves to the piece.

Vector glyphs: resource/vectors/

SVG glyphs (nav and toolbar icons, symbols) go in resource/vectors/ and render resolution-independent through the vector piece:

vector(res::vectors::home).tint(accent).frame(24.0, 24.0)

Vector names share the image namespace, so nav items, tab icons, and toolbar buttons accept them unchanged. Source art can be a plain .svg, an SF Symbols template export, or a .symbolset bundle; text has to be outlined, since Day compiles in no text shaping.

What each target ships

day build stages every glyph into the form its toolkit loads natively, and prints the split as it goes; Vectors xaml: 81/81 glyph(s) vector means every glyph converted and none fell back.

TargetShips asTint
ios-uikitthe SVG in an asset catalog with preserves-vector-representation, so UIImage renders at display sizetintColor on a template image
macos-appkitthe SVG itself — NSImage renders SVG files at display size (macOS 11+)contentTintColor on a template image
android-mdca VectorDrawable in res/drawable/setImageTintList
windows-xamlXAML geometry — a Path in a scaling Viewbox, a PathIcon in the nav pane, redrawn at every sizea brush on the shapes
web-domthe SVG, rendered by the browsera CSS mask painted with the tint
harmony-arkuithe SVG in rawfile, rendered by ArkUI’s ImageSVG fill color
linux-gtkthe SVG for icons, rendered at icon size by librsvg through gdk-pixbuf; the raster cache for the vector piecepixel recolor
linux-qtthe SVG, rendered at the size asked for by Qt’s SVG icon enginea SourceIn fill over the rendered glyph

Where a vector degrades to a raster

Two toolkits draw the art themselves rather than handing an SVG to the platform, and both accept the same subset: solid fills and strokes, in either fill rule. Art outside it (gradients, clipping paths, masks, filters) stages no geometry, and that glyph alone ships as a raster instead:

  • Android falls back at xxxhdpi. day lint reports it as day::lint::vector-raster-fallback whenever android-mdc is a declared target, so you hear about a gradient in an icon at lint time, before it reaches a device.
  • Windows falls back the same way, and the tint degrades with it: a monochrome BitmapIcon over the raster rather than a brush on geometry.

The fallback is per glyph, not per app. Day ships the raster only for art that could not convert, because bundling one for every glyph would double the payload and let a broken vector path hide behind a raster that still looks right.

Weights

.weight(VectorWeight::Light | Bold) selects per-weight art on every backend. Template sources (SF Symbols exports, .symbolset bundles) carry real Light and Bold variants; a plain SVG aliases all three to the same glyph, so the call degrades to Regular rather than to a missing asset.

The vectors reference covers the staging in full, including how packed apps carry these forms without the launch environment.

Custom fonts: resource/fonts/

Drop .ttf or .otf files into resource/fonts/ and reference them through the generated constant, which carries the family name baked into the font file itself (what Font Book or fontconfig report), not the file name:

label("Welcome aboard").font(Font::custom(res::fonts::pacifico, 24.0))

day build stages each font where the platform wants it: res/font/ on Android (with the resource-naming rules handled for you), the app bundle plus a UIAppFonts Info.plist entry on iOS, a fonts directory registered with CoreText / fontconfig / the QFontDatabase on the desktops, rawfile plus an ArkTS registerFont manifest on HarmonyOS. Each backend registers everything at startup. The point size scales with the platform’s accessibility text size, exactly like Font::System(pt).

The following restrictions are enforced as hard errors at build time, because each would otherwise surface as a confusing runtime-only failure on one platform:

  • .ttf and .otf only: Android’s res/font/ accepts nothing else, so Day holds every platform to the same rule. Convert collections (.ttc) and variable fonts to single static faces before bundling.
  • One face per family: Staged file names are derived from the family name (lowercased, [a-z0-9_]), so a second face of the same family would collide. Ship the regular face; bold and italic are synthesized where the platform can.
  • File names don’t matter; family names do. resource/fonts/SpecialElite-Regular.ttf whose embedded family is “Special Elite” generates res::fonts::special_elite, used as Font::custom(res::fonts::special_elite, 20.0). (Font::Custom("Special Elite", 20.0) is the unchecked alternative for a family name only known at runtime.)

Outside those rules, an unknown family never breaks the app. The label renders in the system font and the log names the family that didn’t resolve. .weight(...) and .italic() still apply, but a single-face family only gets what the platform can synthesize (a heavier stroke, a slant), not true bold or italic cuts.

The app icon: resource/icons/

resource/icons/ holds one master (icon.svg, day-icon.svg, or icon.png), and that is the only icon file in the repository. day prepare renders every platform’s set from it (.icns, .ico, Android’s adaptive and themed icons, the HarmonyOS layered icon, the Xcode asset catalogs, an Icon Composer package) under build/day/host/, which the host projects reference by path and every build refreshes when the master changes. A per-platform override beside the master (macos.svg, android.svg, …) replaces it for that platform alone. The icons guide covers the layered master and the outputs.

Localized strings are resources too

resource/locales/<lang>/app.ftl files are compiled in via include_str! at the moment (localization guide), and OS-facing strings (the app’s display name) are conveyed into platform manifests at build time. Piece packages can carry their own locales/ and resources, which aggregate into your app without name collisions.

What happens at build

resource/images/wave@2x.png ─┐  resource/fonts/Pacifico-Regular.ttf ─┐  resource/assets/stations.json ─┐
                     ▼          day build -p <target>   ▼                            ▼
   ┌───────────────────────────────────────────────────────────────────────┐
   │ android  → res/drawable-xhdpi/wave.png · res/font/pacifico.ttf        │
   │ ios      → DayPieces asset catalog + fonts/ bundle dir + UIAppFonts   │
   │ gtk/qt   → app.gresource / app.rcc; fonts registered at startup       │
   │ arkui    → hap rawfile/ (+ day/fonts.json → registerFont)             │
   │ desktop dev-launch → read from project dirs directly                  │
   └───────────────────────────────────────────────────────────────────────┘

Staging is best-effort in development: if a resource compiler is missing (say rcc on an unusual Qt install), the build warns and the app falls back to loading loose files from the project directory instead of failing. Packaged builds via day pack bundle every resource.