Pre-alphaFoundry is in development. Tools, APIs, and sample content may change.

Browse documentation
Shared infrastructurecom.zoa.ui.theming · v0.4.0

ZOA UI Theming

Give your editor tools and game UI a shared visual style.

ZOA Foundry is the default theme: dark ink surfaces, violet controls, and copper accents. ZOA Swarms remains available as a separate preset. Choose a theme in Presentation → UI Theming, or edit a theme asset to suit your game.

A ZOAUiTheme asset holds the colors, fonts, spacing, and visual assets used by themed controls. Foundry generates a USS stylesheet from those settings and refreshes attached UI Toolkit panels when the theme changes.

Tooling

Editor tools

Regenerate Active Theme Stylesheet

Tools > ZOA > Advanced > Generate > UI > Themes > Regenerate Active Theme Stylesheet

Forces ZOAThemeStyleSheetGenerator to rewrite the active theme's variables sheet. The postprocessor normally does this on save; reach for the menu after an out-of-band edit.

Ensure Default Theme / Ensure Theme Settings

Tools > ZOA > Advanced > Generate > UI > Themes

Creates the Foundry default, the selectable Swarms preset, and the theme settings at their expected paths. This also runs automatically on editor startup.

Reset Default Theme to Canonical Values

Tools > ZOA > Advanced > Generate > UI > Themes > Reset Default Theme to Canonical Values

Restores the Foundry default asset’s palette, metrics, and visual settings from ZOAFoundryThemePreset.

ZOAThemeAssetPostprocessor

Runs on every import. When a ZOAUiTheme or ZOAUiThemeSettings asset is imported, moved, or deleted, it regenerates the affected stylesheet and refreshes attached roots. No menu entry, no manual step.

Declared ZOA dependencies (0)

No ZOA dependencies declared in the package manifest.

How it works

Concepts

A shared theme package without ZOA dependencies

UI Theming has no ZOA package dependencies. This keeps shared theme contracts available to gameplay and editor packages without introducing a reverse dependency on either.

The package does not depend on Messaging. A consuming bootstrap registers the theme service against IZOAThemeService and IHudThemeProvider. In the editor, ZOAUiThemeEditorServiceBootstrap installs the resolver used by Workbench windows and wizards.

HUD components can use IHudTheme for the shared palette. They do not need a reference to the package that created the theme asset.

Choose the contract your UI needs

IHudThemeProvider is the narrow surface: ActiveTheme plus a ThemeChanged event, where the theme is six colours with no UI Toolkit types anywhere in the signature. A HUD bridge can compile against it without pulling UnityEngine.UIElements into its assembly, and it may legitimately see a null ActiveTheme before a theme loads.

IZOAThemeService supports UI Toolkit panels. Attach adds the generated variables stylesheet to a VisualElement root, while ResolvedTheme supplies fallback-resolved assets and metrics. HUD consumers can use the narrower theme contracts when they do not need panel operations.

Attach is idempotent per root, tracks roots through weak references so it never keeps a dead visual tree alive, and re-attaches automatically when the active theme changes. Callers do not need their own bookkeeping for hot reload.

Tokens are generated from the asset

When a ZOAUiTheme asset is imported, ZOAThemeAssetPostprocessor asks the stylesheet generator to update its --zoa-* variables. Attached panels then resolve their existing USS rules against the new values.

The postprocessor ignores imports that contain only the generated stylesheet, preventing a recursive generation loop. EnsureGenerated compares the new content with the existing file and writes only when it has changed.

ZOAUiTheme implements IHudTheme directly rather than through an adapter. WindowBackgroundColor maps to SurfaceBase, PanelBackgroundColor to SurfacePanel, SlotBackgroundColor to SurfaceEmbedded, BorderColor to BorderDefault, TextColor to TextPrimary, and AccentColor to Yellow, the canonical primary accent. A ZOAUiTheme reference can be handed to a HUD bridge as-is.

A panel needs two separate things

A runtime UIDocument paints nothing without a PanelSettings asset, and a PanelSettings without a base ThemeStyleSheet renders Labels with no glyphs at all. That is the panel-level concern, and ZOAUiPanelSettings.Ensure owns it: it loads the canonical PanelSettings from the Resources path UI/ZOA UI Panel Settings and guarantees it carries the runtime theme sheet at UI/ZOADefaultRuntimeTheme.

Attaching the design tokens is the element-level concern, and that is root.ApplyZOATheme(). Both are required and neither substitutes for the other. Skipping the first gives an invisible panel; skipping the second gives a visible panel painted in fallback colours.

The canonical panel assets live in another package's Resources folder and are loaded here by global Resources path, because Resources folders merge across packages at runtime. That is how the theming package supplies them without taking an asmdef dependency on their owner.

Players can pick a theme too

ZOAUiThemeSettings is the project-level answer to which theme is active, loaded from Resources/ZOA/ThemeSettings so any package can resolve it with a single Resources.Load. ZOAUiThemeRuntimeSelection layers a player-facing choice on top, persisting a theme id under the PlayerPrefs key ZOA.UI.Theme.ActiveThemeId and scanning the ZOA/ThemeS catalog folders for what is available.

ZOAUiThemeSelectorWidget builds the picker as a plain VisualElement, independent of the frontend options descriptor model. An options screen wants a rich preset picker with live apply; the scalar-valued options service is the wrong shape for that, so the widget owns the interaction and writes back through the theme service itself.

Setup

Workflow

  1. 01

    Let the editor bootstrap seed the project

    On editor startup, ZOAUiThemeBootstrap creates the Foundry default theme and project settings when needed. A one-time migration updates the old Swarms default while preserving custom themes. After migration, an explicit Swarms selection is kept. The theme menus can recreate missing assets.

  2. 02

    Author or import a theme

    Create a ZOAUiTheme through ZOA > UI > UI Theme, or import the UI Themes sample, which ships more than thirty pre-configured themes spanning Modern Warfare, Sci Fi, Cyberpunk, Dieselpunk, Fantasy, Wild West, Post-Apocalypse, Ancient World, and Cosmic Horror aesthetics, one folder per theme, each with a ZOAUiTheme asset. Point the settings asset's ActiveTheme at the one you want.

  3. 03

    Save, and let the generator run

    Saving the theme triggers ZOAThemeAssetPostprocessor, which regenerates the theme's variables stylesheet and prompts the service to re-attach every tracked root. There is no manual regenerate step in the normal loop.

  4. 04

    Register a provider from your own bootstrap

    The theming package cannot reach FoundryServiceRegistry. Construct a ZOAThemeService in a consuming package's bootstrap and register it against both IZOAThemeService and IHudThemeProvider, then install the resolver with VisualElementExtensions.SetThemeServiceResolver so ApplyZOATheme routes to the same instance.

  5. 05

    Make each panel render-ready, then theme its root

    In a UIDocument host's Awake or OnEnable, call document.EnsureZOAPanelSettings() for the panel-level requirement, then root.ApplyZOATheme() for the element-level one. Call both, in that order, on every panel.

  6. 06

    Consume tokens from USS, not from literals

    Write rules against var(--zoa-surface-panel), var(--zoa-border-default) and friends. The token-coverage guardrail fails the build on raw colour literals in rule bodies, so a theme swap changes the whole surface.

Reference

Key types

IHudTheme

interface

Six colours and an id. The single cross-package contract a HUD component consumes so it never imports the package that authored the theme.

  • string ThemeId { get; }
  • Color WindowBackgroundColor { get; }
  • Color PanelBackgroundColor { get; }
  • Color SlotBackgroundColor { get; }
  • Color BorderColor { get; }
  • Color TextColor { get; }
  • Color AccentColor { get; }

IHudThemeProvider

interface

Publishes the active IHudTheme and raises an event when it changes. Resolved from the service registry by consumers; ActiveTheme may be null before a theme loads.

  • IHudTheme ActiveTheme { get; }
  • event Action<IHudTheme> ThemeChanged

IZOAThemeService

interface

The UI Toolkit side of theming. Attach the generated variables sheet to a panel root, read the fallback-resolved theme view, and react to theme changes.

  • ZOAUiTheme ActiveTheme { get; }
  • ZOAUiThemeResolvedView ResolvedTheme { get; }
  • event Action<ZOAUiTheme> ThemeChanged
  • void Attach(VisualElement root)
  • void Detach(VisualElement root)
  • void RefreshAttachedRoots()
  • void ReloadFromSettings()

ZOAThemeService

service

The default implementation of both interfaces. Resolves the active theme from settings, loads the generated stylesheet from Resources, and re-attaches every tracked root on change. It never self-registers.

  • ZOAThemeService()
  • ZOAThemeService(ZOAUiTheme fallbackTheme)
  • void SetActiveTheme(ZOAUiTheme theme)
  • void ReloadFromSettings()

VisualElementExtensions

class

The extension methods most call sites use. ApplyZOATheme resolves the service through an installed resolver, falling back to an in-process instance so an unbootstrapped editor panel still renders themed.

  • static void ApplyZOATheme(this VisualElement root)
  • static void RemoveZOATheme(this VisualElement root)
  • static void SetThemeServiceResolver(Func<IZOAThemeService> resolver)

ZOAUiPanelSettings

class

Makes a runtime UIDocument render-ready: canonical PanelSettings plus the base ThemeStyleSheet that UI Toolkit needs before a Label draws any glyphs.

  • const string PanelSettingsResourcePath = "UI/ZOA UI Panel Settings"
  • const string RuntimeThemeResourcePath = "UI/ZOADefaultRuntimeTheme"
  • static PanelSettings LoadCanonical()
  • static PanelSettings Ensure(UIDocument document)
  • static void EnsureRuntimeTheme(PanelSettings settings)
  • static PanelSettings EnsureZOAPanelSettings(this UIDocument document)

ZOAUiThemeResolvedView

class

Active theme projected over a fallback theme. Reach for this instead of reading ActiveTheme directly when you need sprites, textures, cursors, HUD metrics, or scalars, so every UI stack shares one set of fallback rules.

  • ZOAUiTheme ActiveTheme / FallbackTheme / EffectiveTheme { get; }
  • static ZOAUiThemeResolvedView FromActiveTheme(ZOAUiTheme activeTheme)
  • static ZOAUiTheme LoadDefaultTheme()
  • Sprite CrosshairSprite { get; } float CrosshairSize { get; }
  • float HudBarHeight { get; } float HudBarSpacing { get; }
  • Sprite ResolveArchetypeIcon(string archetypeId)
  • bool TryResolveSlotLayout(string slotId, out Rect position)

ZOAUiThemeRuntimeSelection

class

Player-facing theme selection. Enumerates the theme catalog out of Resources and persists the chosen theme id in PlayerPrefs, separate from the project default in the settings asset.

  • const string PlayerPrefsThemeIdKey = "ZOA.UI.Theme.ActiveThemeId"
  • static List<ZOAUiTheme> LoadAvailableThemes()
  • static ZOAUiTheme ResolveThemeForService(ZOAUiTheme projectDefault)
  • static void PersistThemeSelection(ZOAUiTheme theme)
  • static bool TryFindThemeById(string themeId, out ZOAUiTheme theme)
  • static void ClearPersistedThemeSelection()

ZOAUiThemeSelectorWidget

class

Builds a runtime-safe theme picker as a VisualElement. Options control whether a pick is persisted, written back to the settings asset, and applied immediately.

  • static VisualElement Build(Options options = null)
  • class Options { Themes, ActiveTheme, ThemeService, PersistSelection, WriteThemeSettings, ApplyOnSelection, ThemeSelected }

Reference

Authoring assets

ZOAUiTheme

asset

The theme asset. One ScriptableObject carrying every --zoa-* token the project renders through: palette and tints, typography, spacing, radii, borders, surfaces, text roles, controls, transitions, shadows, focus, sprites, cursors, accessibility values, and HUD bar and notification values. It implements IHudTheme directly. Created via ZOA > UI > UI Theme.

  • string ThemeId / DisplayName / Description, int Version, string CacheKey
  • Color Yellow, Black, Dark, Slate, Red, Cyan, White, Dim (the nine canonical palette tokens)
  • Color SurfaceBase / SurfacePanel / SurfaceEmbedded / SurfaceElevated / SurfaceHover
  • Color TextPrimary / TextSecondary / TextAccent, BorderSubtle / BorderDefault / BorderStrong
  • float UiScale, bool UseColorBlindMode, Color ColorBlindAccent
  • Sprite CrosshairSprite, float CrosshairSize, Texture2D CursorDefault / CursorInteract
  • Sprite GetArchetypeIcon(string archetypeId)
  • bool TryGetSlotLayout(string slotId, out Rect position)

ZOAUiThemeSettings

asset

Project-level active-theme selector, resolved through a static Instance that loads from Resources/ZOA/ThemeSettings. In the editor it falls back to a project-wide asset search and then to the editor bootstrap, so a fresh clone is never themeless. Created via ZOA > UI > Theme Settings.

  • static ZOAUiThemeSettings Instance { get; }
  • ZOAUiTheme ActiveTheme { get; set; }
  • bool RequireThemeInResources { get; }
  • float WorkbenchThemeBackgroundOpacity / ChromeOpacity / ContentOpacity
  • ZOAUiThemePaths.DefaultSettingsAssetPath = "Assets/Resources/ZOA/ThemeSettings.asset"

Usage

Examples

A HUD component consuming the narrow contractcsharp
using System;
using UnityEngine;
using UnityEngine.UIElements;
using ZOA.Messaging;
using ZOA.UI.Theming;

public sealed class VitalsPanel : MonoBehaviour
{
    private IHudThemeProvider _themes;
    private VisualElement _panel;

    private void OnEnable()
    {
        if (!FoundryServiceRegistry.TryResolve<IHudThemeProvider>(out _themes))
            return;

        _themes.ThemeChanged += Repaint;
        // ActiveTheme is legitimately null before a theme loads.
        Repaint(_themes.ActiveTheme);
    }

    private void OnDisable()
    {
        if (_themes != null) _themes.ThemeChanged -= Repaint;
    }

    private void Repaint(IHudTheme theme)
    {
        if (theme == null || _panel == null) return;
        _panel.style.backgroundColor = theme.PanelBackgroundColor;
        _panel.style.borderBottomColor = theme.BorderColor;
        _panel.style.color = theme.TextColor;
    }
}
This component reads the shared palette through IHudTheme, without referencing the package that created the theme.
Making a runtime panel render-readycsharp
using UnityEngine;
using UnityEngine.UIElements;
using ZOA.UI.Theming;

[RequireComponent(typeof(UIDocument))]
public sealed class ObjectiveHud : MonoBehaviour
{
    private void Awake()
    {
        var document = GetComponent<UIDocument>();

        // 1. Panel-level: PanelSettings + base ThemeStyleSheet, or the
        //    panel paints nothing and Labels render no glyphs.
        document.EnsureZOAPanelSettings();
    }

    private void OnEnable()
    {
        var root = GetComponent<UIDocument>().rootVisualElement;

        // 2. Element-level: attach the generated --zoa-* variables sheet
        //    so this tree's USS resolves against the active theme.
        root.ApplyZOATheme();
    }
}
EnsureZOAPanelSettings configures the panel; ApplyZOATheme attaches the active design tokens.
Registering the service from a consuming bootstrapcsharp
using ZOA.Messaging;
using ZOA.UI.Theming;

public static class ProjectThemeBootstrap
{
    public static void Install()
    {
        var service = new ZOAThemeService();
        service.ReloadFromSettings();

        // Register against both faces: HUD bridges resolve the narrow
        // provider, panel hosts resolve the UI Toolkit service.
        FoundryServiceRegistry.Register<IZOAThemeService>(service);
        FoundryServiceRegistry.Register<IHudThemeProvider>(service);

        // Route ApplyZOATheme at the same instance instead of the
        // in-process fallback.
        VisualElementExtensions.SetThemeServiceResolver(
            () => FoundryServiceRegistry.TryResolve<IZOAThemeService>(out var s) ? s : null);
    }
}
The theming package cannot do this itself: a package with no dependencies cannot reach the service registry. Registration belongs to the consumer.
Dropping a theme picker into a settings screencsharp
using UnityEngine.UIElements;
using ZOA.UI.Theming;

VisualElement BuildThemeSection(IZOAThemeService themeService)
{
    return ZOAUiThemeSelectorWidget.Build(new ZOAUiThemeSelectorWidget.Options
    {
        Themes = ZOAUiThemeRuntimeSelection.LoadAvailableThemes(),
        ActiveTheme = themeService.ActiveTheme,
        ThemeService = themeService,
        PersistSelection = true,   // writes ZOA.UI.Theme.ActiveThemeId
        WriteThemeSettings = false, // leave the project default alone
        ApplyOnSelection = true,
        ThemeSelected = theme => Debug.Log("Theme now " + theme.DisplayName),
    });
}
PersistSelection stores a player choice; WriteThemeSettings changes the project default. A shipping options screen usually wants the first and not the second.

Integration

Behavior to account for

See also