tokens¶
Tokens is a lexically-scoped dynamic lookup for design values: colors, lengths, font sizes, anything. define as much or as little as you want. the framework defines no schema and mandates no keys.
the shape¶
you describe a blob tree inside a Tokens.Scope call. Container and Text are the two most common blob types. Length is the s&box unit type for pixel sizes. Tokens.Scope pushes a dictionary on an ambient stack, evaluates the subtree expression, and pops it in a finally block.
private static readonly IReadOnlyDictionary<string, object> Dark = new Dictionary<string, object>
{
["Bg"] = Color.FromBytes(0x11, 0x14, 0x1B, 255),
["Fg"] = Color.FromBytes(0xEC, 0xED, 0xED, 255),
["Accent"] = Color.FromBytes(0x47, 0x8A, 0xD1, 255),
["Pad"] = Px.Of(16),
};
protected override Container Build() => Tokens.Scope(Dark, () => new Container
{
BackgroundColor = Tokens.Get<Color>("Bg"),
Padding = Tokens.Get<Length>("Pad"),
Children = { new Text("tokens demo") { FontColor = Tokens.Get<Color>("Fg") } },
});
Tokens.Scope(dict, () => subtree) returns whatever the body returns. lookups inside the body resolve against the stack top-down, so a nested Tokens.Scope overrides outer values for matching keys and falls through for missing ones.
Tokens.Get<T>("key") returns the value cast to T. it throws KeyNotFoundException if no active scope contains the key. it throws InvalidCastException if the stored value cannot be cast to T. Tokens.TryGet<T>(key, out value) is the non-throwing variant.
the dict type is IReadOnlyDictionary<string, object>. pass a plain Dictionary<string, object>, your own record that exposes one, or a static field. one entry or two hundred. your keys, your types.
Length accepts float implicitly in pixels, so writing 16f in any Length-typed property gives you 16 pixels. use Px.Of(16) when you need an explicit named Length value, for example as a dict entry. Px.Of returns a non-nullable Length, which is required when storing into object.
dynamic swapping¶
hold the dict in a field, swap the reference, call Rebuild(). resolution happens at every Build() call, so the next frame paints with the new values. Rebuild() is covered in a later guide.
IReadOnlyDictionary<string, object> _theme = Dark;
protected override Container Build() => Tokens.Scope(_theme, () => /* tree */);
void ToggleTheme()
{
_theme = _theme == Dark ? Light : Dark;
Rebuild();
}
this is the "dynamic" in dynamic-resource: the framework stores nothing across builds. swap the dict reference, ask for a redraw, done. no reactive layer, no observer wiring.
caveats¶
Tokens.Getthrows on miss. there are no silent defaults: a missing key is a bug, and a visible throw is better than a transparent zero that hides it.- the
Scopewrapper must wrap the subtree expression lexically. it cannot be an init-property onContainer, because C# evaluates child expressions before the parent's init properties run, so children would not see a parent's tokens during construction. - storage is
[ThreadStatic]. aBuild()called on one thread cannot see scopes pushed on another. safe by construction for s&box (UI is single-thread) and for xUnit (each test thread has its own stack).