From 1d0b16895f98488f631e576f271203755c4bbb46 Mon Sep 17 00:00:00 2001 From: RMBGAME Date: Fri, 30 Aug 2024 18:52:21 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20UI=20Adaptive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Behaviors/AdaptiveBehavior.cs | 203 ++++++ .../Properties/AssemblyInfo.Xaml.cs | 1 + .../UI/Styling/Controls/PageBase.axaml | 4 +- .../Controls/AcceleratorServiceStatus.axaml | 176 ++--- .../UI/Views/Controls/NetworkCheck.axaml | 499 ++++++++----- .../UI/Views/Controls/NetworkCheck.axaml.cs | 1 - .../Views/Controls/ProxyServiceStatus.axaml | 86 +-- .../UI/Views/Pages/AcceleratorPage2.axaml | 664 ++++++++++-------- 8 files changed, 1010 insertions(+), 624 deletions(-) create mode 100644 src/BD.WTTS.Client.Avalonia/Behaviors/AdaptiveBehavior.cs diff --git a/src/BD.WTTS.Client.Avalonia/Behaviors/AdaptiveBehavior.cs b/src/BD.WTTS.Client.Avalonia/Behaviors/AdaptiveBehavior.cs new file mode 100644 index 00000000000..6815937f5d4 --- /dev/null +++ b/src/BD.WTTS.Client.Avalonia/Behaviors/AdaptiveBehavior.cs @@ -0,0 +1,203 @@ +using Avalonia.Reactive; +using Avalonia.Xaml.Interactions.Responsive; +using Avalonia.Xaml.Interactivity; + +namespace BD.WTTS.Behaviors; + +/// +/// Observes control or control property changes and if triggered sets or removes style classes when conditions from are met. +/// +public class AdaptiveBehavior : Behavior +{ + 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 ??= new AvaloniaList(); + + /// + 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)); + } + + return sourceControl.GetObservable(Visual.BoundsProperty) + .Subscribe(new AnonymousObserver(bounds => ValueChanged(sourceControl, Setters, bounds))); + } + + private void ValueChanged(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); + } + } + } + } + + 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); + } + } +} diff --git a/src/BD.WTTS.Client.Avalonia/Properties/AssemblyInfo.Xaml.cs b/src/BD.WTTS.Client.Avalonia/Properties/AssemblyInfo.Xaml.cs index e58c07f4ae4..d83315f75e9 100644 --- a/src/BD.WTTS.Client.Avalonia/Properties/AssemblyInfo.Xaml.cs +++ b/src/BD.WTTS.Client.Avalonia/Properties/AssemblyInfo.Xaml.cs @@ -11,6 +11,7 @@ [assembly: XmlnsDefinition("https://steampp.net/ui", "BD.WTTS.UI.Views.Windows")] [assembly: XmlnsDefinition("https://steampp.net/ui", "BD.WTTS.Converters")] [assembly: XmlnsDefinition("https://steampp.net/ui", "BD.WTTS.Markup")] +[assembly: XmlnsDefinition("https://steampp.net/ui", "BD.WTTS.Behaviors")] [assembly: XmlnsDefinition("https://steampp.net/services", "BD.WTTS.Services")] [assembly: XmlnsDefinition("https://steampp.net/services", "BD.WTTS.Plugins")] [assembly: XmlnsDefinition("https://steampp.net/services", "BD.WTTS.Plugins.Abstractions")] diff --git a/src/BD.WTTS.Client.Avalonia/UI/Styling/Controls/PageBase.axaml b/src/BD.WTTS.Client.Avalonia/UI/Styling/Controls/PageBase.axaml index 625c57e6687..d2b21f4896f 100644 --- a/src/BD.WTTS.Client.Avalonia/UI/Styling/Controls/PageBase.axaml +++ b/src/BD.WTTS.Client.Avalonia/UI/Styling/Controls/PageBase.axaml @@ -97,7 +97,7 @@ - 20 + 20,10,20,20 - - - - - - + + + + + + + - - - - + + - - - - + + + - - - + + - - - - - + + + - - - - - - - - + + + + + + + - - - + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + \ No newline at end of file