Browse Source

Merge 2d73868200 into 5f47b4be53

pull/20315/merge
Nikita Tsukanov 1 day ago
committed by GitHub
parent
commit
9170bfd2b0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 36
      samples/ControlCatalog/Pages/AdornerLayerPage.xaml
  2. 2
      src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
  3. 26
      src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs
  4. 13
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  5. 9
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  6. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
  7. 26
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  8. 2
      src/Avalonia.Base/composition-schema.xml
  9. 180
      src/Avalonia.Controls/Primitives/AdornerHelper.cs
  10. 89
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  11. 2
      src/Avalonia.Controls/Primitives/Popup.cs
  12. 31
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

36
samples/ControlCatalog/Pages/AdornerLayerPage.xaml

@ -44,21 +44,31 @@
VerticalContentAlignment="Center" VerticalAlignment="Stretch"
Width="200" Height="42">
<AdornerLayer.Adorner>
<Canvas x:Name="AdornerCanvas"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Cyan"
IsHitTestVisible="False"
Opacity="0.3"
IsVisible="True"
AdornerLayer.IsClipEnabled="False">
<Line StartPoint="-10000,0" EndPoint="10000,0" Stroke="Cyan" StrokeThickness="1" />
<Line StartPoint="-10000,42" EndPoint="10000,42" Stroke="Cyan" StrokeThickness="1" />
<Line StartPoint="0,-10000" EndPoint="0,10000" Stroke="Cyan" StrokeThickness="1" />
<Line StartPoint="200,-10000" EndPoint="200,10000" Stroke="Cyan" StrokeThickness="1" />
</Canvas>
<Border AdornerLayer.IsClipEnabled="False" ClipToBounds="False" Background="Red" Opacity="0.3" Margin="-5" Padding="5"
IsHitTestVisible="False">
<Canvas
x:Name="AdornerCanvas"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Cyan"
IsHitTestVisible="False"
IsVisible="True"
AdornerLayer.IsClipEnabled="False">
<Line StartPoint="-10000,0" EndPoint="10000,0" Stroke="Cyan" StrokeThickness="1" />
<Line StartPoint="-10000,42" EndPoint="10000,42" Stroke="Cyan" StrokeThickness="1" />
<Line StartPoint="0,-10000" EndPoint="0,10000" Stroke="Cyan" StrokeThickness="1" />
<Line StartPoint="200,-10000" EndPoint="200,10000" Stroke="Cyan" StrokeThickness="1" />
</Canvas>
</Border>
</AdornerLayer.Adorner>
<Button.ContextMenu>
<ContextMenu PlacementTarget="{ResolveByName AdornerCanvas}" Placement="TopEdgeAlignedLeft">
<MenuItem Header="Menu 1"/>
<MenuItem Header="Menu 2"/>
</ContextMenu>
</Button.ContextMenu>
</Button>
</LayoutTransformControl>
</Grid>

2
src/Avalonia.Base/Input/TextInput/InputMethodManager.cs

@ -10,7 +10,7 @@ namespace Avalonia.Input.TextInput
private IInputElement? _focusedElement;
private Interactive? _visualRoot;
private TextInputMethodClient? _client;
private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper();
private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper(true);
public TextInputMethodManager()
{

26
src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.VisualTree;
@ -7,13 +9,15 @@ namespace Avalonia.Input.TextInput
{
class TransformTrackingHelper : IDisposable
{
private readonly bool _deferAfterRenderPass;
private Visual? _visual;
private bool _queuedForUpdate;
private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler;
private readonly List<Visual> _propertyChangedSubscriptions = new List<Visual>();
public TransformTrackingHelper()
public TransformTrackingHelper(bool deferAfterRenderPass)
{
_deferAfterRenderPass = deferAfterRenderPass;
_propertyChangedHandler = PropertyChangedHandler;
}
@ -91,7 +95,10 @@ namespace Avalonia.Input.TextInput
if(_queuedForUpdate)
return;
_queuedForUpdate = true;
Dispatcher.UIThread.Post(UpdateMatrix, DispatcherPriority.AfterRender);
if (_deferAfterRenderPass)
Dispatcher.UIThread.Post(UpdateMatrix, DispatcherPriority.AfterRender);
else
MediaContext.Instance.BeginInvokeOnRender(UpdateMatrix);
}
private void PropertyChangedHandler(object? sender, AvaloniaPropertyChangedEventArgs e)
@ -106,12 +113,23 @@ namespace Avalonia.Input.TextInput
UpdateMatrix();
}
public static IDisposable Track(Visual visual, Action<Visual, Matrix?> cb)
public static IDisposable Track(Visual visual, bool deferAfterRenderPass, Action<Visual, Matrix?> cb)
{
var rv = new TransformTrackingHelper();
var rv = new TransformTrackingHelper(deferAfterRenderPass);
rv.MatrixChanged += () => cb(visual, rv.Matrix);
rv.SetVisual(visual);
return rv;
}
public static IObservable<Matrix?> Observe(Visual visual, bool deferAfterRenderPass)
{
return Observable.Create<Matrix?>(observer =>
{
var rv = new TransformTrackingHelper(deferAfterRenderPass);
rv.MatrixChanged += () => observer.OnNext(rv.Matrix);
rv.SetVisual(visual);
return rv;
});
}
}
}

13
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs

@ -34,15 +34,10 @@ namespace Avalonia.Rendering.Composition.Server
var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root, parentCombinedTransform);
foreach (var child in Children)
{
if (child.AdornedVisual != null)
root.EnqueueAdornerUpdate(child);
else
{
var res = child.Update(root, GlobalTransformMatrix);
oldInvalidated |= res.InvalidatedOld;
newInvalidated |= res.InvalidatedNew;
combinedBounds = LtrbRect.FullUnion(combinedBounds, res.Bounds);
}
var res = child.Update(root, GlobalTransformMatrix);
oldInvalidated |= res.InvalidatedOld;
newInvalidated |= res.InvalidatedNew;
combinedBounds = LtrbRect.FullUnion(combinedBounds, res.Bounds);
}
// If effect is changed, we need to clean both old and new bounds

9
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -31,7 +31,6 @@ namespace Avalonia.Rendering.Composition.Server
private bool _fullRedrawRequested;
private bool _disposed;
private readonly HashSet<ServerCompositionVisual> _attachedVisuals = new();
private readonly Queue<ServerCompositionVisual> _adornerUpdateQueue = new();
public long Id { get; }
public ulong Revision { get; private set; }
@ -129,12 +128,6 @@ namespace Avalonia.Rendering.Composition.Server
// Update happens in a separate phase to extend dirty rect if needed
Root.Update(this, transform);
while (_adornerUpdateQueue.Count > 0)
{
var adorner = _adornerUpdateQueue.Dequeue();
adorner.Update(this, transform);
}
_updateRequested = false;
Readback.CompleteWrite(Revision);
@ -263,7 +256,5 @@ namespace Avalonia.Rendering.Composition.Server
if (visual.IsVisibleInFrame)
AddDirtyRect(visual.TransformedOwnContentBounds);
}
public void EnqueueAdornerUpdate(ServerCompositionVisual visual) => _adornerUpdateQueue.Enqueue(visual);
}
}

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs

@ -24,7 +24,6 @@ partial class ServerCompositionVisual
| CompositionVisualChangedFields.AnchorPointAnimated
| CompositionVisualChangedFields.CenterPoint
| CompositionVisualChangedFields.CenterPointAnimated
| CompositionVisualChangedFields.AdornedVisual
| CompositionVisualChangedFields.TransformMatrix
| CompositionVisualChangedFields.Scale
| CompositionVisualChangedFields.ScaleAnimated
@ -63,7 +62,6 @@ partial class ServerCompositionVisual
if (offset == s_IdOfSizeProperty
|| offset == s_IdOfAnchorPointProperty
|| offset == s_IdOfCenterPointProperty
|| offset == s_IdOfAdornedVisualProperty
|| offset == s_IdOfTransformMatrixProperty
|| offset == s_IdOfScaleProperty
|| offset == s_IdOfRotationAngleProperty

26
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -44,18 +44,7 @@ namespace Avalonia.Rendering.Composition.Server
Root!.DebugEvents?.IncrementRenderedVisuals();
var boundsRect = new Rect(new Size(Size.X, Size.Y));
if (AdornedVisual != null)
{
// Adorners are currently not supported in detached rendering mode
if(context.DetachedRendering)
return;
canvas.Transform = Matrix.Identity;
if (AdornerIsClipped)
canvas.PushClip(AdornedVisual._combinedTransformedClipBounds.ToRect());
}
using var _ = context.SetOrPushTransform(this);
var applyRenderOptions = RenderOptions != default;
@ -87,8 +76,7 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopGeometryClip();
if (ClipToBounds && !HandlesClipToBounds)
canvas.PopClip();
if (AdornedVisual != null && AdornerIsClipped)
canvas.PopClip();
if (Opacity != 1)
canvas.PopOpacity();
@ -144,7 +132,7 @@ namespace Avalonia.Rendering.Composition.Server
}
}
public virtual UpdateResult Update(ServerCompositionTarget root, Matrix parentVisualTransform)
public virtual UpdateResult Update(ServerCompositionTarget root, Matrix parentTransform)
{
if (Parent == null && Root == null)
return default;
@ -155,14 +143,11 @@ namespace Avalonia.Rendering.Composition.Server
if (_combinedTransformDirty)
{
CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint,
// HACK: Ignore RenderTransform set by the adorner layer
AdornedVisual != null ? Matrix.Identity : TransformMatrix,
TransformMatrix,
Scale, RotationAngle, Orientation, Offset);
_combinedTransformDirty = false;
}
var parentTransform = AdornedVisual?.GlobalTransformMatrix ?? parentVisualTransform;
var newTransform = CombinedTransformMatrix * parentTransform;
// Check if visual was moved and recalculate face orientation
@ -229,8 +214,7 @@ namespace Avalonia.Rendering.Composition.Server
}
_combinedTransformedClipBounds =
(AdornerIsClipped ? AdornedVisual?._combinedTransformedClipBounds : null)
?? (Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null)
(Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null)
?? new LtrbRect(0, 0, Root!.PixelSize.Width, Root!.PixelSize.Height);
if (_transformedClipBounds != null)

2
src/Avalonia.Base/composition-schema.xml

@ -29,8 +29,6 @@
<Property Name="Orientation" Type="Quaternion" DefaultValue="Quaternion.Identity" Animated="true"/>
<Property Name="Scale" Type="Vector3D" DefaultValue="new Avalonia.Vector3D(1, 1, 1)" Animated="true"/>
<Property Name="TransformMatrix" Type="Avalonia.Matrix" DefaultValue="Avalonia.Matrix.Identity" Animated="true" Internal="true"/>
<Property Name="AdornedVisual" Type="CompositionVisual?" Internal="true" />
<Property Name="AdornerIsClipped" Type="bool" Internal="true" />
<Property Name="OpacityMaskBrush" ClientName="OpacityMaskBrushTransportField" Type="Avalonia.Media.IBrush?" Private="true" />
<Property Name="Effect" Type="Avalonia.Media.IImmutableEffect?" Internal="true" />
<Property Name="RenderOptions" Type="Avalonia.Media.RenderOptions" />

180
src/Avalonia.Controls/Primitives/AdornerHelper.cs

@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives;
class AdornerHelper
{
public static IDisposable SubscribeToAncestorPropertyChanges(Visual visual,
bool includeClip, Action changed)
{
return new AncestorPropertyChangesSubscription(visual, includeClip, changed);
}
private class AncestorPropertyChangesSubscription : IDisposable
{
private readonly Visual _visual;
private readonly bool _includeClip;
private readonly Action _changed;
private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler;
private readonly List<Visual> _subscriptions = new List<Visual>();
private bool _isDisposed;
public AncestorPropertyChangesSubscription(Visual visual, bool includeClip, Action changed)
{
_visual = visual;
_includeClip = includeClip;
_changed = changed;
_propertyChangedHandler = OnPropertyChanged;
_visual.AttachedToVisualTree += OnAttachedToVisualTree;
_visual.DetachedFromVisualTree += OnDetachedFromVisualTree;
if (_visual.IsAttachedToVisualTree)
{
SubscribeToAncestors();
}
}
private void SubscribeToAncestors()
{
UnsubscribeFromAncestors();
// Subscribe to the visual's own Bounds property
_visual.PropertyChanged += _propertyChangedHandler;
_subscriptions.Add(_visual);
// Walk up the ancestor chain
var ancestor = _visual.VisualParent;
while (ancestor != null)
{
if (ancestor is Visual visualAncestor)
{
visualAncestor.PropertyChanged += _propertyChangedHandler;
_subscriptions.Add(visualAncestor);
}
ancestor = ancestor.VisualParent;
}
}
private void UnsubscribeFromAncestors()
{
foreach (var subscription in _subscriptions)
{
subscription.PropertyChanged -= _propertyChangedHandler;
}
_subscriptions.Clear();
}
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (!e.IsEffectiveValueChange)
return;
bool shouldNotify = false;
if (e.Property == Visual.RenderTransformProperty || e.Property == Visual.BoundsProperty)
{
shouldNotify = true;
}
else if (_includeClip)
{
if (e.Property == Visual.ClipToBoundsProperty ||
e.Property == Visual.ClipProperty) shouldNotify = true;
}
if (shouldNotify)
{
_changed();
}
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
SubscribeToAncestors();
_changed();
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
UnsubscribeFromAncestors();
_changed();
}
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
UnsubscribeFromAncestors();
_visual.AttachedToVisualTree -= OnAttachedToVisualTree;
_visual.DetachedFromVisualTree -= OnDetachedFromVisualTree;
}
}
public static Geometry? CalculateAdornerClip(Visual adornedElement)
{
// Walk ancestor stack and calculate clip geometry relative to the current visual.
// If ClipToBounds = true, add extra RectangleGeometry for Bounds.Size
Geometry? result = null;
var ancestor = adornedElement.VisualParent;
while (ancestor != null)
{
if (ancestor is Visual visualAncestor)
{
Geometry? ancestorClip = null;
// Check if ancestor has ClipToBounds enabled
if (visualAncestor.ClipToBounds)
{
ancestorClip = new RectangleGeometry(new Rect(visualAncestor.Bounds.Size));
}
// Check if ancestor has explicit Clip geometry
if (visualAncestor.Clip != null)
{
if (ancestorClip != null)
{
ancestorClip = new CombinedGeometry(GeometryCombineMode.Intersect, ancestorClip, visualAncestor.Clip);
}
else
{
ancestorClip = visualAncestor.Clip;
}
}
// Transform the clip geometry to adorned element's coordinate space
if (ancestorClip != null)
{
var transform = visualAncestor.TransformToVisual(adornedElement);
if (transform.HasValue && !transform.Value.IsIdentity)
{
ancestorClip = ancestorClip.Clone();
ancestorClip.Transform = new MatrixTransform(transform.Value);
}
// Combine with existing result
if (result != null)
{
result = new CombinedGeometry(GeometryCombineMode.Intersect, result, ancestorClip);
}
else
{
result = ancestorClip;
}
}
}
ancestor = ancestor.VisualParent;
}
return result;
}
}

89
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Specialized;
using Avalonia.Input.TextInput;
using Avalonia.Media;
using Avalonia.Reactive;
using Avalonia.VisualTree;
@ -45,15 +46,20 @@ namespace Avalonia.Controls.Primitives
private static readonly AttachedProperty<AdornerLayer?> s_savedAdornerLayerProperty =
AvaloniaProperty.RegisterAttached<Visual, Visual, AdornerLayer?>("SavedAdornerLayer");
private TransformTrackingHelper _trackingHelper = new TransformTrackingHelper(false);
static AdornerLayer()
{
AdornedElementProperty.Changed.Subscribe(AdornedElementChanged);
AdornerProperty.Changed.Subscribe(AdornerChanged);
IsClipEnabledProperty.Changed.Subscribe(AdornerIsClipEnabledChanged);
}
public AdornerLayer()
{
Children.CollectionChanged += ChildrenCollectionChanged;
_trackingHelper.SetVisual(this);
_trackingHelper.MatrixChanged += delegate { InvalidateMeasure(); };
}
public static Visual? GetAdornedElement(Visual adorner)
@ -199,9 +205,9 @@ namespace Avalonia.Controls.Primitives
{
var info = ao.GetValue(s_adornedElementInfoProperty);
if (info != null && info.Bounds.HasValue)
if (info is { AdornedElement: not null })
{
child.Measure(info.Bounds.Value.Bounds.Size);
child.Measure(info.AdornedElement.Bounds.Size);
}
else
{
@ -223,12 +229,22 @@ namespace Avalonia.Controls.Primitives
var info = ao.GetValue(s_adornedElementInfoProperty);
var isClipEnabled = ao.GetValue(IsClipEnabledProperty);
if (info != null && info.Bounds.HasValue)
var adorned = info?.AdornedElement;
if (adorned != null)
{
child.RenderTransform = new MatrixTransform(info.Bounds.Value.Transform);
child.Arrange(new(adorned.Bounds.Size));
var transform = adorned.TransformToVisual(this);
// If somebody decides that having Margin on an adorner is a good idea,
// we need to compensate for element being positioned at non-(0,0) coords.
if (transform != null && child.Bounds.Position != default)
{
transform = Matrix.CreateTranslation(child.Bounds.Position) * transform.Value *
Matrix.CreateTranslation(-child.Bounds.Position);
}
child.RenderTransform = new MatrixTransform(transform ?? default);
child.RenderTransformOrigin = new RelativePoint(new Point(0, 0), RelativeUnit.Absolute);
UpdateClip(child, info.Bounds.Value, isClipEnabled);
child.Arrange(info.Bounds.Value.Bounds);
UpdateClip(child, adorned, isClipEnabled);
}
else
{
@ -248,29 +264,22 @@ namespace Avalonia.Controls.Primitives
layer?.UpdateAdornedElement(adorner, adorned);
}
private void UpdateClip(Control control, TransformedBounds bounds, bool isEnabled)
private static void AdornerIsClipEnabledChanged(AvaloniaPropertyChangedEventArgs<bool> e)
{
var info = ((Visual)e.Sender).GetValue(s_adornedElementInfoProperty);
info?.UpdateSubscription();
info?.Layer?.InvalidateMeasure();
}
private void UpdateClip(Control control, Visual adorned, bool isEnabled)
{
if (!isEnabled)
{
control.Clip = null;
return;
}
if (!(control.Clip is RectangleGeometry clip))
{
clip = new RectangleGeometry();
control.Clip = clip;
}
var clipBounds = bounds.Bounds;
if (bounds.Transform.HasInverse)
{
clipBounds = bounds.Clip.TransformToAABB(bounds.Transform.Invert());
}
clip.Rect = clipBounds;
control.Clip = AdornerHelper.CalculateAdornerClip(adorned);
}
private void ChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
@ -291,17 +300,12 @@ namespace Avalonia.Controls.Primitives
private void UpdateAdornedElement(Visual adorner, Visual? adorned)
{
if (adorner.CompositionVisual != null)
{
adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual;
adorner.CompositionVisual.AdornerIsClipped = GetIsClipEnabled(adorner);
}
var info = adorner.GetValue(s_adornedElementInfoProperty);
if (info != null)
{
info.Subscription!.Dispose();
info.Subscription = null;
if (adorned == null)
{
@ -313,24 +317,35 @@ namespace Avalonia.Controls.Primitives
{
if (info == null)
{
info = new AdornedElementInfo();
info = new AdornedElementInfo(adorner);
adorner.SetValue(s_adornedElementInfoProperty, info);
}
if (adorner.CompositionVisual != null)
info.Subscription = adorned.GetObservable(BoundsProperty).Subscribe(x =>
{
info.Bounds = new TransformedBounds(new Rect(adorned.Bounds.Size), new Rect(adorned.Bounds.Size), Matrix.Identity);
InvalidateMeasure();
});
info.Layer = this;
info.AdornedElement = adorned;
info.UpdateSubscription();
}
}
private class AdornedElementInfo
private class AdornedElementInfo(Visual adorner)
{
public AdornerLayer? Layer { get; set; }
public IDisposable? Subscription { get; set; }
public Visual? AdornedElement { get; set; }
public TransformedBounds? Bounds { get; set; }
public void UpdateSubscription()
{
Subscription?.Dispose();
Subscription = null;
if (AdornedElement != null)
{
Subscription = AdornerHelper.SubscribeToAncestorPropertyChanges(AdornedElement,
GetIsClipEnabled(adorner), () =>
{
Layer?.InvalidateMeasure();
});
}
}
}
}
}

2
src/Avalonia.Controls/Primitives/Popup.cs

@ -470,7 +470,7 @@ namespace Avalonia.Controls.Primitives
if (InheritsTransform)
{
TransformTrackingHelper.Track(placementTarget, PlacementTargetTransformChanged)
TransformTrackingHelper.Track(placementTarget, true, PlacementTargetTransformChanged)
.DisposeWith(handlerCleanup);
}
else

31
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@ -570,16 +570,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
var target = positionRequest.Target;
if (target == null)
throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null");
Matrix? matrix;
if (TryGetAdorner(target, out var adorned, out var adornerLayer))
{
matrix = adorned!.TransformToVisual(topLevel) * target.TransformToVisual(adornerLayer!);
}
else
{
matrix = target.TransformToVisual(topLevel);
}
Matrix? matrix = target.TransformToVisual(topLevel);
if (matrix == null)
{
if (target.GetVisualRoot() == null)
@ -591,25 +583,6 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
var anchorRect = positionRequest.AnchorRect ?? bounds;
return anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
}
private static bool TryGetAdorner(Visual target, out Visual? adorned, out Visual? adornerLayer)
{
var element = target;
while (element != null)
{
if (AdornerLayer.GetAdornedElement(element) is { } adornedElement)
{
adorned = adornedElement;
adornerLayer = AdornerLayer.GetAdornerLayer(adorned);
return true;
}
element = element.VisualParent;
}
adorned = null;
adornerLayer = null;
return false;
}
}
}

Loading…
Cancel
Save