From 1a4da9ff2ac464ebaafe80add3821ea65fc8a4f5 Mon Sep 17 00:00:00 2001 From: chinboy Date: Sun, 23 Feb 2025 15:24:07 +0800 Subject: [PATCH] Add ScopeAwareAdornerLayer --- .../Primitives/ScopeAwareAdornerLayer.cs | 428 ++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 src/AtomUI.Controls/Primitives/ScopeAwareAdornerLayer.cs diff --git a/src/AtomUI.Controls/Primitives/ScopeAwareAdornerLayer.cs b/src/AtomUI.Controls/Primitives/ScopeAwareAdornerLayer.cs new file mode 100644 index 000000000..01cd28dc0 --- /dev/null +++ b/src/AtomUI.Controls/Primitives/ScopeAwareAdornerLayer.cs @@ -0,0 +1,428 @@ +using System.Collections.Specialized; +using System.Diagnostics; +using System.Reflection; +using AtomUI.Controls.Utils; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace AtomUI.Controls.Primitives; + +public class ScopeAwareAdornerLayer : Canvas +{ + private const int ScopeAwareAdornerLayerZIndex = int.MaxValue - 1000; + + #region 公共属性 + + public static readonly AttachedProperty AdornedElementProperty = + AvaloniaProperty.RegisterAttached("AdornedElement"); + + public static readonly AttachedProperty AdornerProperty = + AvaloniaProperty.RegisterAttached("Adorner"); + + public static readonly StyledProperty LayerHostProperty = AvaloniaProperty + .Register(nameof(LayerHost)); + + public Visual? LayerHost + { + get => GetValue(LayerHostProperty); + set => SetValue(LayerHostProperty, value); + } + + #endregion + + #region 内部属性定义 + + private static readonly AttachedProperty AdornedElementInfoProperty = + AvaloniaProperty.RegisterAttached("AdornedElementInfo"); + + private static readonly AttachedProperty SavedAdornerLayerProperty = + AvaloniaProperty.RegisterAttached("SavedAdornerLayer"); + + private static readonly MethodInfo AddLayerMethodInfo; + + #endregion + + static ScopeAwareAdornerLayer() + { + AdornedElementProperty.Changed.Subscribe(HandleAdornedElementChanged); + AdornerProperty.Changed.Subscribe(HandleAdornerChanged); + AddLayerMethodInfo = + typeof(VisualLayerManager).GetMethod("AddLayer", BindingFlags.Instance | BindingFlags.NonPublic)!; + } + + public ScopeAwareAdornerLayer() + { + Children.CollectionChanged += ChildrenCollectionChanged; + } + + public static Visual? GetAdornedElement(Visual control) + { + return control.GetValue(AdornedElementProperty); + } + + public static void SetAdornedElement(Visual control, Visual? adorned) + { + control.SetValue(AdornedElementProperty, adorned); + } + + public static ScopeAwareAdornerLayer? GetDrawerLayer(Visual visual) + { + Layoutable? layerHost = visual.FindAncestorOfType(true); + var adorned = GetAdornedElement(visual); + if (layerHost != null && adorned != null) + { + while (layerHost != null && layerHost.IsVisualAncestorOf(adorned) == false) + { + layerHost = layerHost.FindAncestorOfType(); + } + } + + layerHost ??= visual.FindAncestorOfType(); + layerHost ??= TopLevel.GetTopLevel(visual); + + if (layerHost == null) + { + return null; + } + + var layer = + layerHost.GetVisualChildren().FirstOrDefault(c => c is ScopeAwareAdornerLayer) as ScopeAwareAdornerLayer; + layer ??= InjectLayer(layerHost); + + return layer; + } + + public static Control? GetAdorner(Visual visual) + { + return visual.GetValue(AdornerProperty); + } + + public static void SetAdorner(Visual visual, Control? adorner) + { + visual.SetValue(AdornerProperty, adorner); + } + + protected override Size MeasureOverride(Size availableSize) + { + foreach (var child in Children) + { + if (child is AvaloniaObject ao) + { + var info = ao.GetValue(AdornedElementInfoProperty); + + if (info != null && info.Bounds.HasValue) + { + child.Measure(info.Bounds.Value.Bounds.Size); + } + else + { + child.Measure(availableSize); + } + } + } + + return default; + } + + protected override Size ArrangeOverride(Size finalSize) + { + foreach (var child in Children) + { + if (child is AvaloniaObject ao) + { + var info = ao.GetValue(AdornedElementInfoProperty); + + if (info != null && info.Bounds.HasValue) + { + child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform); + child.RenderTransformOrigin = new RelativePoint(new Point(0, 0), RelativeUnit.Absolute); + UpdateClip(child, info.Bounds.Value); + child.Arrange(info.Bounds.Value.Bounds); + } + else + { + ArrangeChild(child, finalSize); + } + } + } + + return finalSize; + } + + private void ChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (Visual i in e.NewItems!) + { + UpdateAdornedElement(i, i.GetValue(AdornedElementProperty)); + } + + break; + } + + InvalidateArrange(); + } + + private void UpdateAdornedElement(Visual adorner, Control? adorned) + { + var info = adorner.GetValue(AdornedElementInfoProperty); + + if (info != null) + { + info.Subscription!.Dispose(); + + if (adorned == null) + { + adorner.ClearValue(AdornedElementInfoProperty); + } + } + + if (adorned != null) + { + if (info == null) + { + info = new AdornedElementInfo(); + adorner.SetValue(AdornedElementInfoProperty, info); + } + + info.Subscription = adorned.GetObservable(BoundsProperty).Subscribe(x => + { + Debug.Assert(LayerHost != null); + double offsetX = 0d; + double offsetY = 0d; + var relateToHostPoint = adorned.TranslatePoint(new Point(0, 0), LayerHost) ?? new Point(0, 0); + if (LayerHost is ScrollContentPresenter scrollContentPresenter) + { + offsetX = scrollContentPresenter.Offset.X; + offsetY = scrollContentPresenter.Offset.Y; + } + + offsetX += relateToHostPoint.X; + offsetY += relateToHostPoint.Y; + var translationMatrix = LayerHost != null + ? Matrix.CreateTranslation(offsetX, offsetY) + : Matrix.Identity; + info.Bounds = new TransformedBounds(new Rect(adorned.DesiredSize), + new Rect(adorned.DesiredSize), + translationMatrix); + InvalidateMeasure(); + }); + } + } + + private void UpdateClip(Control control, TransformedBounds bounds) + { + if (!(control.Clip is RectangleGeometry clip)) + { + clip = new RectangleGeometry(); + control.Clip = clip; + } + + var clipBounds = bounds.Bounds; + + clip.Rect = clipBounds; + } + + private static void HandleAdornedElementChanged(AvaloniaPropertyChangedEventArgs e) + { + var adorner = (Visual)e.Sender; + var adorned = e.NewValue.GetValueOrDefault(); + var layer = adorner.GetVisualParent(); + layer?.UpdateAdornedElement(adorner, adorned); + } + + private static void HandleAdornerChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is Visual visual) + { + var oldAdorner = e.OldValue.GetValueOrDefault(); + var newAdorner = e.NewValue.GetValueOrDefault(); + + if (Equals(oldAdorner, newAdorner)) + { + return; + } + + if (oldAdorner is { }) + { + visual.AttachedToVisualTree -= VisualOnAttachedToVisualTree; + visual.DetachedFromVisualTree -= VisualOnDetachedFromVisualTree; + Detach(visual, oldAdorner); + } + + if (newAdorner is { }) + { + visual.AttachedToVisualTree += VisualOnAttachedToVisualTree; + visual.DetachedFromVisualTree += VisualOnDetachedFromVisualTree; + Attach(visual, newAdorner); + } + } + } + + private static void Attach(Visual visual, Control adorner) + { + var layer = ScopeAwareAdornerLayer.GetDrawerLayer(visual); + AddVisualAdorner(visual, adorner, layer); + visual.SetValue(SavedAdornerLayerProperty, layer); + } + + private static void Detach(Visual visual, Control adorner) + { + var layer = visual.GetValue(SavedAdornerLayerProperty); + RemoveVisualAdorner(visual, adorner, layer); + visual.ClearValue(SavedAdornerLayerProperty); + } + + private static void AddVisualAdorner(Visual visual, Control? adorner, ScopeAwareAdornerLayer? layer) + { + if (adorner is null || layer == null || layer.Children.Contains(adorner)) + { + return; + } + + SetAdornedElement(adorner, visual); + + ((ISetLogicalParent)adorner).SetParent(visual); + layer.Children.Add(adorner); + } + + private static void RemoveVisualAdorner(Visual visual, Control? adorner, ScopeAwareAdornerLayer? layer) + { + if (adorner is null || layer is null || !layer.Children.Contains(adorner)) + { + return; + } + + layer.Children.Remove(adorner); + ((ISetLogicalParent)adorner).SetParent(null); + } + + private static void VisualOnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + if (sender is Visual visual) + { + var adorner = GetAdorner(visual); + if (adorner is { }) + { + Attach(visual, adorner); + } + } + } + + private static void VisualOnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) + { + if (sender is Visual visual) + { + var adorner = GetAdorner(visual); + if (adorner is { }) + { + Detach(visual, adorner); + } + } + } + + private static void AddLayerToVisualLayerManager(VisualLayerManager visualLayerManager, Control layer, int zindex) + { + AddLayerMethodInfo.Invoke(visualLayerManager, [layer, zindex]); + } + + private static ScopeAwareAdornerLayer InjectLayer(Layoutable layerHost) + { + var layer = FindAdornerLayer(layerHost); + if (layer != null) + { + return layer; + } + + layer = new ScopeAwareAdornerLayer + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + ZIndex = ScopeAwareAdornerLayerZIndex + }; + + layer.LayerHost = layerHost; + if (layerHost is VisualLayerManager visualLayerManager) + { + AddLayerToVisualLayerManager(visualLayerManager, layer, visualLayerManager.ZIndex); + } + else if (layerHost is ScrollContentPresenter scrollContentPresenter) + { + if (scrollContentPresenter.Content is Control controlContent) + { + var oldOffset = scrollContentPresenter.Offset; + // 直接内容控件 + scrollContentPresenter.Content = null; + scrollContentPresenter.UpdateChild(); + var panel = new Panel + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + }; + panel.Children.Add(controlContent); + panel.Children.Add(layer); + scrollContentPresenter.Content = panel; + var scrollViewer = scrollContentPresenter.FindAncestorOfType(); + if (scrollViewer != null) + { + scrollViewer.Offset = oldOffset; + } + } + else if (scrollContentPresenter.Content != null && scrollContentPresenter.ContentTemplate != null) + { + // 模版处理 + var injectTemplate = new FuncDataTemplate((o, scope) => + { + var originControl = scrollContentPresenter.ContentTemplate.Build(o); + var panel = new Panel + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + }; + Debug.Assert(originControl != null); + panel.Children.Add(originControl); + panel.Children.Add(layer); + return panel; + }); + scrollContentPresenter.ContentTemplate = injectTemplate; + } + } + + return layer; + } + + private static ScopeAwareAdornerLayer? FindAdornerLayer(Layoutable layerHost) + { + if (layerHost is ScrollContentPresenter scrollContentPresenter) + { + // 在 Panel 下面 + var panel = scrollContentPresenter.FindChildOfType(); + if (panel is not null) + { + return panel.FindChildOfType(); + } + } + else if (layerHost is VisualLayerManager visualLayerManager) + { + // 直接就在下面 + visualLayerManager.FindChildOfType(); + } + + return null; + } + + private class AdornedElementInfo + { + public IDisposable? Subscription { get; set; } + + public TransformedBounds? Bounds { get; set; } + } +} \ No newline at end of file