using System;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Metadata;
using Avalonia.Reactive;
using Avalonia.Xaml.Interactivity;
namespace Avalonia.Xaml.Interactions.Responsive;
///
/// Observes control or control property changes and if triggered sets or removes style classes when conditions from are met.
///
public class AdaptiveBehavior : StyledElementBehavior
{
private IDisposable? _disposable;
private AvaloniaList? _setters;
///
/// Identifies the avalonia property.
///
public static readonly StyledProperty SourceControlProperty =
AvaloniaProperty.Register(nameof(SourceControl));
///
/// Identifies the avalonia property.
///
public static readonly StyledProperty TargetControlProperty =
AvaloniaProperty.Register(nameof(TargetControl));
///
/// Identifies the avalonia property.
///
public static readonly DirectProperty> SettersProperty =
AvaloniaProperty.RegisterDirect>(nameof(Setters), t => t.Setters);
///
/// Gets or sets the the source control that property are observed from, if not set is used. This is a avalonia property.
///
[ResolveByName]
public Control? SourceControl
{
get => GetValue(SourceControlProperty);
set => SetValue(SourceControlProperty, value);
}
///
/// Gets or sets the target control that class name that should be added or removed when triggered, if not set is used or from . This is a avalonia property.
///
[ResolveByName]
public Control? TargetControl
{
get => GetValue(TargetControlProperty);
set => SetValue(TargetControlProperty, value);
}
///
/// Gets adaptive class setters collection. This is a avalonia property.
///
[Content]
public AvaloniaList Setters => _setters ??= [];
///
protected override void OnAttachedToVisualTree()
{
base.OnAttachedToVisualTree();
StopObserving();
StartObserving();
}
///
protected override void OnDetachedFromVisualTree()
{
base.OnDetachedFromVisualTree();
StopObserving();
}
private void StartObserving()
{
var sourceControl = GetValue(SourceControlProperty) is not null
? SourceControl
: AssociatedObject;
if (sourceControl is not null)
{
_disposable = ObserveBounds(sourceControl);
}
}
private void StopObserving()
{
_disposable?.Dispose();
}
private IDisposable ObserveBounds(Control sourceControl)
{
if (sourceControl is null)
{
throw new ArgumentNullException(nameof(sourceControl));
}
Execute(sourceControl, Setters, sourceControl.GetValue(Visual.BoundsProperty));
return sourceControl.GetObservable(Visual.BoundsProperty)
.Subscribe(new AnonymousObserver(bounds =>
{
Execute(sourceControl, Setters, bounds);
}));
}
private void Execute(Control? sourceControl, AvaloniaList? setters, Rect bounds)
{
if (sourceControl is null || setters is null)
{
return;
}
foreach (var setter in setters)
{
var isMinOrMaxWidthSet = setter.IsSet(AdaptiveClassSetter.MinWidthProperty)
|| setter.IsSet(AdaptiveClassSetter.MaxWidthProperty);
var widthConditionTriggered = GetResult(setter.MinWidthOperator, bounds.Width, setter.MinWidth)
&& GetResult(setter.MaxWidthOperator, bounds.Width, setter.MaxWidth);
var isMinOrMaxHeightSet = setter.IsSet(AdaptiveClassSetter.MinHeightProperty)
|| setter.IsSet(AdaptiveClassSetter.MaxHeightProperty);
var heightConditionTriggered = GetResult(setter.MinHeightOperator, bounds.Height, setter.MinHeight)
&& GetResult(setter.MaxHeightOperator, bounds.Height, setter.MaxHeight);
var isAddClassTriggered = isMinOrMaxWidthSet switch
{
true when !isMinOrMaxHeightSet => widthConditionTriggered,
false when isMinOrMaxHeightSet => heightConditionTriggered,
true when isMinOrMaxHeightSet => widthConditionTriggered && heightConditionTriggered,
_ => false
};
var targetControl = setter.GetValue(AdaptiveClassSetter.TargetControlProperty) is not null
? setter.TargetControl
: GetValue(TargetControlProperty) is not null
? TargetControl
: AssociatedObject;
if (targetControl is not null)
{
var className = setter.ClassName;
var isPseudoClass = setter.IsPseudoClass;
if (isAddClassTriggered)
{
Add(targetControl, className, isPseudoClass);
}
else
{
Remove(targetControl, className, isPseudoClass);
}
}
else
{
throw new ArgumentNullException(nameof(targetControl));
}
}
}
private bool GetResult(ComparisonConditionType comparisonConditionType, double property, double value)
{
return comparisonConditionType switch
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
ComparisonConditionType.Equal => property == value,
// ReSharper disable once CompareOfFloatsByEqualityOperator
ComparisonConditionType.NotEqual => property != value,
ComparisonConditionType.LessThan => property < value,
ComparisonConditionType.LessThanOrEqual => property <= value,
ComparisonConditionType.GreaterThan => property > value,
ComparisonConditionType.GreaterThanOrEqual => property >= value,
_ => throw new ArgumentOutOfRangeException()
};
}
private static void Add(Control targetControl, string? className, bool isPseudoClass)
{
if (className is null || string.IsNullOrEmpty(className) || targetControl.Classes.Contains(className))
{
return;
}
if (isPseudoClass)
{
((IPseudoClasses) targetControl.Classes).Add(className);
}
else
{
targetControl.Classes.Add(className);
}
}
private static void Remove(Control targetControl, string? className, bool isPseudoClass)
{
if (className is null || string.IsNullOrEmpty(className) || !targetControl.Classes.Contains(className))
{
return;
}
if (isPseudoClass)
{
((IPseudoClasses) targetControl.Classes).Remove(className);
}
else
{
targetControl.Classes.Remove(className);
}
}
}