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); } } }