Utility variants​
A utility variant is a markup extension that controls whether a prefixed AKCSS utility is currently active.
using Akbura.Markup;
<Border p-1
${sm}:p-2
${md}:p-3
${lg}:p-4 />
In this example, ${sm}, ${md}, and ${lg} are utility variants.
A variant extension must return one of these types from ProvideValue:
boolIObservable<bool>
A candidate participates in the AKCSS cascade only while the returned value is
true.
When an observable has not produced a value yet, or when its latest value is
false, the candidate is excluded. Another utility can then provide the
property value.
Declaring a variant​
Mark the markup extension class with UtilityVariantAttribute:
using Akbura.Markup;
[UtilityVariant(
10d,
ConflictGroup = "WindowBreakpoints",
UnprefixedPrecedence = UnprefixedUtilityPrecedence.Above)]
public sealed class WideExtension
{
public IObservable<bool> ProvideValue(IServiceProvider services)
{
// Return whether the variant is currently active.
}
}
The Extension suffix is omitted in markup:
using Demo.Markup;
<Border p-2
${Wide}:p-6 />
Variant names follow the normal markup extension lookup rules. The namespace containing the extension must be imported explicitly.
Conflict resolution is property-based​
Utilities do not conflict simply because they have similar names, use the same variant, or belong to the same conflict group.
Akbura expands every utility into individual property-writing operations and resolves each target property independently.
For example, suppose one utility writes:
Width
Background
Padding
and another utility writes:
Width
Height
Only the two Width operations compete.
The following operations remain active independently:
Background
Padding
Height
This also means that assigning two variants to the same ConflictGroup does
not make unrelated utilities conflict.
Resolution algorithm​
For each target property, Akbura resolves active utility operations in the following order.
1. Exclude inactive candidates​
A prefixed candidate is excluded when its variant returns false or when its
observable has not produced a value yet.
2. Respect AKCSS operation priority​
Variant ordering is considered only when candidates write the same target property and have the same AKCSS operation priority.
For example, a regular style operation and an active style-trigger operation are not made equal merely because their variants share a conflict group.
3. Resolve candidates inside each conflict group​
Active prefixed candidates with the same non-empty ConflictGroup are ordered
by Order.
The candidate with the greater value wins:
Order 20 > Order 10 > Order 1
When two candidates have the same Order, the candidate written later in
markup wins.
4. Compare different groups by source order​
Order is not a global priority.
Winners from different conflict groups are compared by their position in markup.
Candidates without a conflict group are also compared by source order.
For example, an Order of 1000 in one group does not automatically beat an
Order of 1 from another group.
5. Compare the prefixed winner with the unprefixed candidate​
After Akbura selects the winning prefixed candidate, it compares that candidate with the last unprefixed candidate writing the same property.
This comparison is controlled by UnprefixedPrecedence.
UtilityVariantAttribute​
[AttributeUsage(
AttributeTargets.Class,
Inherited = false,
AllowMultiple = false)]
public sealed class UtilityVariantAttribute : Attribute
The attribute can be applied once to a markup extension class.
Order​
public double Order { get; }
Order controls priority between active prefixed candidates only when all of
the following conditions are true:
- The candidates write the same target property.
- The operations have the same AKCSS operation priority.
- Both variants have the same non-empty
ConflictGroup.
The greater value wins.
When values are equal, source order is used.
Order is ignored:
- between different conflict groups;
- when one or both variants have no group;
- when comparing a prefixed candidate with an unprefixed candidate.
Example:
[UtilityVariant(
10d,
ConflictGroup = "Breakpoints")]
public sealed class MediumExtension
{
}
[UtilityVariant(
20d,
ConflictGroup = "Breakpoints")]
public sealed class LargeExtension
{
}
While both variants are active, LargeExtension wins conflicting property
operations because 20 is greater than 10.
ConflictGroup​
public string? ConflictGroup { get; init; }
ConflictGroup identifies variants whose active candidates can be ordered by
Order.
[UtilityVariant(
10d,
ConflictGroup = "Breakpoints")]
The group only affects operations that already conflict.
It does not create a conflict between unrelated properties.
A null, empty, or whitespace value means that the variant has no ordered
group. Such candidates are compared with other groups by source order.
Use the same group for variants that represent ordered alternatives, such as:
- minimum-width breakpoints;
- maximum-width breakpoints;
- mutually ordered interaction modes;
- ordered accessibility modes.
Use different groups when the variants represent independent conditions whose relative priority should remain controlled by markup order.
UnprefixedPrecedence​
public UnprefixedUtilityPrecedence UnprefixedPrecedence
{
get;
init;
}
This setting controls how the winning active prefixed candidate competes with the last unprefixed candidate that writes the same property.
The default is:
UnprefixedUtilityPrecedence.SourceOrder
The setting is applied only after the prefixed winner has been selected.
UnprefixedUtilityPrecedence​
Below​
UnprefixedUtilityPrecedence.Below
The unprefixed candidate always wins while both candidates are active.
Markup order does not matter.
<Border ${custom}:p-6
p-2 />
<Border p-2
${custom}:p-6 />
In both cases, p-2 wins.
SourceOrder​
UnprefixedUtilityPrecedence.SourceOrder
The candidate written later in markup wins.
<Border ${custom}:p-6
p-2 />
Here, p-2 wins.
<Border p-2
${custom}:p-6 />
Here, ${custom}:p-6 wins.
Above​
UnprefixedUtilityPrecedence.Above
The active prefixed candidate always wins.
Markup order does not matter.
<Border ${md}:p-6
p-2 />
<Border p-2
${md}:p-6 />
While ${md} is active, p-6 wins in both cases.
This behavior is used by the built-in breakpoint variants.
Built-in breakpoint implementation​
The built-in ${sm}, ${md}, ${lg}, ${xl}, and ${xxl} variants observe
the current TopLevel.ClientSize.
All variants belong to the same conflict group.
Their increasing Order values ensure that the greatest active breakpoint
wins. They also use UnprefixedUtilityPrecedence.Above, so an unprefixed
utility cannot override an active breakpoint.
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Xaml;
using System;
using System.Collections.Generic;
using System.Text;
namespace Akbura.Markup;
using static BreakpointsGroupKey;
file static class BreakpointsGroupKey
{
public const string BreakpointsGroup = nameof(BreakpointsGroup);
}
public abstract class BreakpointMarkupExtension
{
protected BreakpointPredicate IsActivatedPredicate
{
get;
init;
}
public IObservable<bool>? ProvideValue(IServiceProvider? serviceProvider)
{
if (serviceProvider == null)
{
return null;
}
if (serviceProvider.GetService(typeof(IProvideValueTarget))
is not IProvideValueTarget provideValueTarget)
{
return null;
}
if (provideValueTarget.TargetObject is not Visual target)
{
return null;
}
var topLevel = TopLevel.GetTopLevel(target);
if (topLevel == null &&
serviceProvider.GetService(typeof(IRootObjectProvider))
is IRootObjectProvider rootObjectProvider)
{
topLevel = rootObjectProvider.IntermediateRootObject as TopLevel
?? rootObjectProvider.RootObject as TopLevel;
}
if (topLevel == null)
{
return null;
}
return new BreakpointObservable(
topLevel.GetObservable(TopLevel.ClientSizeProperty),
IsActivatedPredicate);
}
protected readonly unsafe struct BreakpointPredicate
{
private readonly delegate*<double, bool> _pointer;
public BreakpointPredicate(delegate*<double, bool> pointer)
{
if (pointer == null)
{
throw new ArgumentNullException(nameof(pointer));
}
_pointer = pointer;
}
public bool Invoke(double width)
{
return _pointer(width);
}
}
private sealed class BreakpointObservable : IObservable<bool>
{
private readonly IObservable<Size> _source;
private readonly BreakpointPredicate _isActivated;
public BreakpointObservable(
IObservable<Size> source,
BreakpointPredicate isActivated)
{
_source = source;
_isActivated = isActivated;
}
public IDisposable Subscribe(IObserver<bool> observer)
{
ArgumentNullException.ThrowIfNull(observer);
return _source.Subscribe(
new BreakpointObserver(observer, _isActivated));
}
}
private sealed class BreakpointObserver : IObserver<Size>
{
private readonly IObserver<bool> _observer;
private readonly BreakpointPredicate _isActivated;
private bool _hasValue;
private bool _lastValue;
private bool _isStopped;
public BreakpointObserver(
IObserver<bool> observer,
BreakpointPredicate isActivated)
{
_observer = observer;
_isActivated = isActivated;
}
public void OnNext(Size size)
{
if (_isStopped)
{
return;
}
bool currentValue;
try
{
currentValue = _isActivated.Invoke(size.Width);
}
catch (Exception exception)
{
_isStopped = true;
_observer.OnError(exception);
return;
}
if (_hasValue && currentValue == _lastValue)
{
return;
}
_hasValue = true;
_lastValue = currentValue;
_observer.OnNext(currentValue);
}
public void OnError(Exception error)
{
if (_isStopped)
{
return;
}
_isStopped = true;
_observer.OnError(error);
}
public void OnCompleted()
{
if (_isStopped)
{
return;
}
_isStopped = true;
_observer.OnCompleted();
}
}
}
#pragma warning disable IDE1006 // Naming Styles
[UtilityVariant(
1,
ConflictGroup = BreakpointsGroup,
UnprefixedPrecedence = UnprefixedUtilityPrecedence.Above)]
public sealed class smExtension : BreakpointMarkupExtension
{
public smExtension()
{
unsafe
{
IsActivatedPredicate =
new BreakpointPredicate(&IsActivated);
}
}
private static bool IsActivated(double width)
{
return width >= 640d;
}
}
[UtilityVariant(
10,
ConflictGroup = BreakpointsGroup,
UnprefixedPrecedence = UnprefixedUtilityPrecedence.Above)]
public sealed class mdExtension : BreakpointMarkupExtension
{
public mdExtension()
{
unsafe
{
IsActivatedPredicate =
new BreakpointPredicate(&IsActivated);
}
}
private static bool IsActivated(double width)
{
return width >= 768d;
}
}
[UtilityVariant(
20,
ConflictGroup = BreakpointsGroup,
UnprefixedPrecedence = UnprefixedUtilityPrecedence.Above)]
public sealed class lgExtension : BreakpointMarkupExtension
{
public lgExtension()
{
unsafe
{
IsActivatedPredicate =
new BreakpointPredicate(&IsActivated);
}
}
private static bool IsActivated(double width)
{
return width >= 1024d;
}
}
[UtilityVariant(
30,
ConflictGroup = BreakpointsGroup,
UnprefixedPrecedence = UnprefixedUtilityPrecedence.Above)]
public sealed class xlExtension : BreakpointMarkupExtension
{
public xlExtension()
{
unsafe
{
IsActivatedPredicate =
new BreakpointPredicate(&IsActivated);
}
}
private static bool IsActivated(double width)
{
return width >= 1280d;
}
}
[UtilityVariant(
40,
ConflictGroup = BreakpointsGroup,
UnprefixedPrecedence = UnprefixedUtilityPrecedence.Above)]
public sealed class xxlExtension : BreakpointMarkupExtension
{
public xxlExtension()
{
unsafe
{
IsActivatedPredicate =
new BreakpointPredicate(&IsActivated);
}
}
private static bool IsActivated(double width)
{
return width >= 1536d;
}
}
#pragma warning restore IDE1006 // Naming Styles
Breakpoint orders​
| Variant | Active width | Order |
|---|---|---|
${sm} |
>= 640 |
1 |
${md} |
>= 768 |
10 |
${lg} |
>= 1024 |
20 |
${xl} |
>= 1280 |
30 |
${xxl} |
>= 1536 |
40 |
Consider this markup:
<Border ${lg}:w-10
w-5
${md}:w-7 />
At a width of 1100:
${md}is active;${lg}is active;${lg}has the greaterOrder;${lg}:w-10wins even though it appears beforew-5;w-5cannot override it because breakpoint variants useUnprefixedUtilityPrecedence.Above.
At a width of 800:
${md}is active;${lg}is inactive;${md}:w-7wins.
Below 768:
- both
${md}and${lg}are inactive; w-5wins.
Multiple properties​
Variant priority is resolved independently for every property-writing operation.
<Border p-2
bg-slate-800
${md}:p-6
${lg}:bg-blue-600 />
At the md breakpoint:
Padding = p-6
Background = bg-slate-800
At the lg breakpoint:
Padding = p-6
Background = bg-blue-600
${lg}:bg-blue-600 does not disable ${md}:p-6, because the candidates write
different properties.
Equal order​
When two active variants in the same group have equal Order, the candidate
written later wins.
[UtilityVariant(
10d,
ConflictGroup = "Modes")]
public sealed class FirstExtension
{
}
[UtilityVariant(
10d,
ConflictGroup = "Modes")]
public sealed class SecondExtension
{
}
<Border ${First}:p-2
${Second}:p-6 />
When both variants are active, ${Second}:p-6 wins.
Reversing the markup reverses the result:
<Border ${Second}:p-6
${First}:p-2 />
Now ${First}:p-2 wins.
Practical guidelines​
Use a shared ConflictGroup when variants represent an ordered scale.
sm < md < lg < xl < xxl
Use increasing Order values to represent that scale.
Use UnprefixedUtilityPrecedence.Above when an active variant must override
the default value regardless of markup order.
Use SourceOrder when the author should be able to override a variant by
placing another utility later.
Use Below when an unprefixed value must always remain authoritative.
Do not use Order as a global priority. It is intentionally local to one
non-empty conflict group and one conflicting property operation.
See also: