diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml
index 7501c80940..e9a245a8e1 100644
--- a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml
+++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml
@@ -44,21 +44,31 @@
VerticalContentAlignment="Center" VerticalAlignment="Stretch"
Width="200" Height="42">
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
index ae3a8bc6a1..005a015644 100644
--- a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
+++ b/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()
{
diff --git a/src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs b/src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs
index ef8ae00c79..c5954bc600 100644
--- a/src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs
+++ b/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 _propertyChangedHandler;
private readonly List _propertyChangedSubscriptions = new List();
- public TransformTrackingHelper()
+ public TransformTrackingHelper(bool deferAfterRenderPass)
{
+ _deferAfterRenderPass = deferAfterRenderPass;
_propertyChangedHandler = PropertyChangedHandler;
}
@@ -70,6 +74,7 @@ namespace Avalonia.Input.TextInput
void UpdateMatrix()
{
+ _queuedForUpdate = false;
Matrix? matrix = null;
if (_visual != null && _visual.VisualRoot != null)
matrix = _visual.TransformToVisual((Visual)_visual.VisualRoot);
@@ -91,7 +96,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 +114,23 @@ namespace Avalonia.Input.TextInput
UpdateMatrix();
}
- public static IDisposable Track(Visual visual, Action cb)
+ public static IDisposable Track(Visual visual, bool deferAfterRenderPass, Action cb)
{
- var rv = new TransformTrackingHelper();
+ var rv = new TransformTrackingHelper(deferAfterRenderPass);
rv.MatrixChanged += () => cb(visual, rv.Matrix);
rv.SetVisual(visual);
return rv;
}
+
+ public static IObservable Observe(Visual visual, bool deferAfterRenderPass)
+ {
+ return Observable.Create(observer =>
+ {
+ var rv = new TransformTrackingHelper(deferAfterRenderPass);
+ rv.MatrixChanged += () => observer.OnNext(rv.Matrix);
+ rv.SetVisual(visual);
+ return rv;
+ });
+ }
}
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs
index 6f5f7b7572..fe6effbbd4 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs
@@ -46,20 +46,25 @@ partial class ServerCompositionVisual
public void UpdateAdorner()
{
GetAttHelper().EnqueuedForAdornerUpdate = false;
- var ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale,
- RotationAngle, Orientation, Offset);
-
+
if (AdornedVisual != null && Parent != null)
{
+ // We ignore Visual's RenderTransform completely since it's set by AdornerLayer and can be out of sync
+ // with compositor-driver animations
+ var ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, Matrix.Identity, Scale,
+ RotationAngle, Orientation, Offset);
if (
AdornerLayer_GetExpectedSharedAncestor(this) is {} sharedAncestor
&& ComputeTransformFromAncestor(AdornedVisual, sharedAncestor, out var adornerLayerToAdornedVisual))
- ownTransform = (ownTransform ?? Matrix.Identity) * adornerLayerToAdornedVisual;
+ _ownTransform = (ownTransform ?? Matrix.Identity) * adornerLayerToAdornedVisual;
else
- ownTransform = default(Matrix); // Don't render, something is broken
+ _ownTransform = default(Matrix); // Don't render, something is broken
}
- _ownTransform = ownTransform;
+ else
+ _ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale,
+ RotationAngle, Orientation, Offset);
+
PropagateFlags(true, true);
}
diff --git a/src/Avalonia.Controls/Primitives/AdornerHelper.cs b/src/Avalonia.Controls/Primitives/AdornerHelper.cs
new file mode 100644
index 0000000000..c3f967da5c
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/AdornerHelper.cs
@@ -0,0 +1,183 @@
+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 _propertyChangedHandler;
+ private readonly List _subscriptions = new List();
+ 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;
+
+ 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();
+ var matrix = ancestorClip.Transform is { Value.IsIdentity: false }
+ ? transform.Value * ancestorClip.Transform.Value
+ : transform.Value;
+ ancestorClip.Transform = new MatrixTransform(matrix);
+ }
+
+ // Combine with existing result
+ if (result != null)
+ {
+ result = new CombinedGeometry(GeometryCombineMode.Intersect, result, ancestorClip);
+ }
+ else
+ {
+ result = ancestorClip;
+ }
+ }
+ }
+
+ ancestor = ancestor.VisualParent;
+ }
+
+ return result;
+ }
+
+}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs
index 412dd236ff..6271f16784 100644
--- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs
+++ b/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 s_savedAdornerLayerProperty =
AvaloniaProperty.RegisterAttached("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 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)
@@ -313,24 +322,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();
+ });
+ }
+ }
}
}
}
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index 76fa73f4c3..5294df72d5 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -456,7 +456,7 @@ namespace Avalonia.Controls.Primitives
if (InheritsTransform)
{
- TransformTrackingHelper.Track(placementTarget, PlacementTargetTransformChanged)
+ TransformTrackingHelper.Track(placementTarget, true, PlacementTargetTransformChanged)
.DisposeWith(handlerCleanup);
}
else
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
index 82dead1ed5..2f735ef691 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
+++ b/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;
- }
}
}