From d3c811acb5649d85189400b1aa3083130855723e Mon Sep 17 00:00:00 2001
From: Luke <55367595+luke-whos-here@users.noreply.github.com>
Date: Wed, 11 Mar 2026 10:26:01 +0000
Subject: [PATCH 1/4] Update summaries for gesture events in InputElement
(#20862)
Write new descriptions in tags to provide more precise definitions of gestures. This is intended to address https://github.com/AvaloniaUI/avalonia-docs/issues/896
---
.../Input/InputElement.Gestures.cs | 30 +++++++++----------
1 file changed, 15 insertions(+), 15 deletions(-)
diff --git a/src/Avalonia.Base/Input/InputElement.Gestures.cs b/src/Avalonia.Base/Input/InputElement.Gestures.cs
index 1ad1146282..83f350f0e7 100644
--- a/src/Avalonia.Base/Input/InputElement.Gestures.cs
+++ b/src/Avalonia.Base/Input/InputElement.Gestures.cs
@@ -149,7 +149,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a pinch gesture occurs on the control.
+ /// Occurs when the user moves two contact points closer together.
///
public event EventHandler? Pinch
{
@@ -158,7 +158,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a pinch gesture ends on the control.
+ /// Occurs when the user releases both contact points used in a pinch gesture.
///
public event EventHandler? PinchEnded
{
@@ -167,7 +167,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a pull gesture occurs on the control.
+ /// Occurs when the user drags from the edge of a control.
///
public event EventHandler? PullGesture
{
@@ -176,7 +176,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a pull gesture ends on the control.
+ /// Occurs when the user releases the pointer after a pull gesture.
///
public event EventHandler? PullGestureEnded
{
@@ -185,7 +185,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a scroll gesture occurs on the control.
+ /// Occurs when the user continuously moves the pointer in the same direction within the control’s boundaries.
///
public event EventHandler? ScrollGesture
{
@@ -194,7 +194,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a scroll gesture inertia starts on the control.
+ /// Occurs within a scroll gesture, when the user releases the pointer, and scrolling continues by transitioning to momentum-based gliding movement.
///
public event EventHandler? ScrollGestureInertiaStarting
{
@@ -203,7 +203,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a scroll gesture ends on the control.
+ /// Occurs when a scroll gesture has fully stopped, taking into account any inertial movement that continues the scroll after the user has released the pointer.
///
public event EventHandler? ScrollGestureEnded
{
@@ -212,7 +212,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a touchpad magnify gesture occurs on the control.
+ /// Occurs when the user moves two contact points away from each other on a touchpad.
///
public event EventHandler? PointerTouchPadGestureMagnify
{
@@ -221,7 +221,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a touchpad rotate gesture occurs on the control.
+ /// Occurs when the user places two contact points and moves them in a circular motion on a touchpad.
///
public event EventHandler? PointerTouchPadGestureRotate
{
@@ -230,7 +230,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a swipe gesture occurs on the control.
+ /// Occurs when the user rapidly drags the pointer in a single direction across the control.
///
public event EventHandler? SwipeGesture
{
@@ -239,7 +239,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a touchpad swipe gesture occurs on the control.
+ /// Occurs when the user performs a rapid dragging motion in a single direction on a touchpad.
///
public event EventHandler? PointerTouchPadGestureSwipe
{
@@ -248,7 +248,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a tap gesture occurs on the control.
+ /// Occurs when the user briefly contacts and releases a single point, without significant movement.
///
public event EventHandler? Tapped
{
@@ -257,7 +257,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a right tap gesture occurs on the control.
+ /// Occurs when the user briefly contacts and releases a single point, without significant movement, using a mechanism on the input device recognized as a right button or equivalent.
///
public event EventHandler? RightTapped
{
@@ -266,7 +266,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a hold gesture occurs on the control.
+ /// Occurs when the user makes a single contact, then maintains contact beyond a given time threshold without releasing or making another contact.
///
public event EventHandler? Holding
{
@@ -275,7 +275,7 @@ namespace Avalonia.Input
}
///
- /// Occurs when a double-tap gesture occurs on the control.
+ /// Occurs when the user briefly contacts and releases twice on a single point, without significant movement.
///
public event EventHandler? DoubleTapped
{
From e7543155ec6d6b03a34590ffbe03706f9ee45827 Mon Sep 17 00:00:00 2001
From: Jumar Macato <16554748+jmacato@users.noreply.github.com>
Date: Wed, 11 Mar 2026 18:42:06 +0800
Subject: [PATCH 2/4] Disable Animations processing when Visual's not visible
redux (#20820)
* Add failing tests for animations visibility behavior
* Dont trigger InternalStep when IsEffectivelyVisible is false.
* Fix a 6 year old mistake.
Dont spawn a new DoubleAnimator, instead just parse the target Transform object and do a SetValue call for the interpolated animation value.
* add comment and remove redundant else
* use LightweightSubject to avoid hammering the binding system with extra value frames from SetValue as @grokys suggested.
* whitespace
* remove unused namespace
* remove extra fields
* Implement animation pause on invisible and dispose on visual tree detach
* Implement animation pause on invisible and dispose on visual tree detach
* add tests
* check pause state and combo with IEV
* fix review comment
---
.../Animation/AnimationInstance`1.cs | 63 ++++-
src/Avalonia.Base/Visual.cs | 6 +
.../Animation/AnimationIterationTests.cs | 250 ++++++++++++++++++
3 files changed, 316 insertions(+), 3 deletions(-)
diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs
index 3be646d66c..6358dd2c6b 100644
--- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs
+++ b/src/Avalonia.Base/Animation/AnimationInstance`1.cs
@@ -7,7 +7,7 @@ using Avalonia.Data;
namespace Avalonia.Animation
{
///
- /// Handles interpolation and time-related functions
+ /// Handles interpolation and time-related functions
/// for keyframe animations.
///
internal class AnimationInstance : SingleSubscriberObservableBase
@@ -35,6 +35,8 @@ namespace Avalonia.Animation
private readonly IClock _baseClock;
private IClock? _clock;
private EventHandler? _propertyChangedDelegate;
+ private EventHandler? _visibilityChangedHandler;
+ private EventHandler? _detachedHandler;
public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action? OnComplete, Func Interpolator)
{
@@ -80,11 +82,34 @@ namespace Avalonia.Animation
protected override void Unsubscribed()
{
+ // Guard against reentrancy: DoComplete() can trigger Unsubscribed() via the
+ // _onCompleteAction disposal chain, and then PublishCompleted() calls it again.
+ var timerSub = _timerSub;
+ _timerSub = null;
+ if (timerSub is null)
+ return;
+
// Animation may have been stopped before it has finished.
ApplyFinalFill();
_targetControl.PropertyChanged -= _propertyChangedDelegate;
- _timerSub?.Dispose();
+ timerSub.Dispose();
+
+ if (_targetControl is Visual visual)
+ {
+ if (_visibilityChangedHandler is not null)
+ {
+ visual.IsEffectivelyVisibleChanged -= _visibilityChangedHandler;
+ _visibilityChangedHandler = null;
+ }
+
+ if (_detachedHandler is not null)
+ {
+ visual.DetachedFromVisualTree -= _detachedHandler;
+ _detachedHandler = null;
+ }
+ }
+
_clock!.PlayState = PlayState.Stop;
}
@@ -92,6 +117,35 @@ namespace Avalonia.Animation
{
_clock = new Clock(_baseClock);
_timerSub = _clock.Subscribe(Step);
+
+ if (_targetControl is Visual visual)
+ {
+ _visibilityChangedHandler = (_, _) =>
+ {
+ if (_clock is null || _clock.PlayState == PlayState.Stop)
+ return;
+ if (visual.IsEffectivelyVisible)
+ {
+ if (_clock.PlayState == PlayState.Pause)
+ _clock.PlayState = PlayState.Run;
+ }
+ else
+ {
+ if (_clock.PlayState == PlayState.Run)
+ _clock.PlayState = PlayState.Pause;
+ }
+ };
+ visual.IsEffectivelyVisibleChanged += _visibilityChangedHandler;
+
+ // If already invisible when animation starts, pause immediately.
+ if (!visual.IsEffectivelyVisible)
+ _clock.PlayState = PlayState.Pause;
+
+ // Stop and dispose the animation when detached from the visual tree.
+ _detachedHandler = (_, _) => DoComplete();
+ visual.DetachedFromVisualTree += _detachedHandler;
+ }
+
_propertyChangedDelegate ??= ControlPropertyChanged;
_targetControl.PropertyChanged += _propertyChangedDelegate;
UpdateNeutralValue();
@@ -101,7 +155,10 @@ namespace Avalonia.Animation
{
try
{
- InternalStep(frameTick);
+ if (_clock?.PlayState == PlayState.Pause)
+ return;
+
+ InternalStep(frameTick);
}
catch (Exception e)
{
diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs
index 95d55754d3..4bd359fa78 100644
--- a/src/Avalonia.Base/Visual.cs
+++ b/src/Avalonia.Base/Visual.cs
@@ -208,6 +208,11 @@ namespace Avalonia
///
public bool IsEffectivelyVisible { get; private set; } = true;
+ ///
+ /// Raised when changes.
+ ///
+ internal event EventHandler? IsEffectivelyVisibleChanged;
+
///
/// Updates the property based on the parent's
/// .
@@ -221,6 +226,7 @@ namespace Avalonia
return;
IsEffectivelyVisible = isEffectivelyVisible;
+ IsEffectivelyVisibleChanged?.Invoke(this, EventArgs.Empty);
// PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ
// will cause extra allocations and overhead.
diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
index 4a3d67d0c7..752c1b166b 100644
--- a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
@@ -93,6 +93,256 @@ namespace Avalonia.Base.UnitTests.Animation
Assert.True(animationRun.Status == TaskStatus.RanToCompletion);
Assert.Equal(border.Width, 100d);
}
+
+ [Fact]
+ public void Pause_Animation_When_IsEffectivelyVisible_Is_False()
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 200d), }, Cue = new Cue(1d)
+ };
+ var keyframe2 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 150d), }, Cue = new Cue(0.5d)
+ };
+
+ var keyframe3 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 100d), }, Cue = new Cue(0d)
+ };
+
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(3),
+ Delay = TimeSpan.FromSeconds(3),
+ DelayBetweenIterations = TimeSpan.FromSeconds(3),
+ IterationCount = new IterationCount(2),
+ Children = { keyframe1, keyframe2, keyframe3 }
+ };
+
+ var border = new Border() { Height = 100d, Width = 100d };
+
+ var clock = new TestClock();
+ var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken);
+
+ border.Measure(Size.Infinity);
+ border.Arrange(new Rect(border.DesiredSize));
+
+ clock.Step(TimeSpan.Zero);
+
+ clock.Step(TimeSpan.FromSeconds(0));
+ Assert.Equal(100d, border.Width);
+
+ // Hide the border — this should pause the animation clock.
+ border.IsVisible = false;
+
+ clock.Step(TimeSpan.FromSeconds(4.5));
+
+ // Width should not change while invisible (animation is paused).
+ Assert.Equal(100d, border.Width);
+
+ // Show the border — animation resumes from where it left off.
+ border.IsVisible = true;
+
+ // The pause absorbed 4.5s of wall-clock time, so internal time = wall - 4.5.
+ // To reach internal time 6s (end of iteration 1, cue 1.0 -> width 200):
+ // wall = 4.5 + 6 = 10.5
+ clock.Step(TimeSpan.FromSeconds(10.5));
+ Assert.Equal(200d, border.Width);
+
+ // To complete the animation (internal time 14s triggers trailing delay of iter 2):
+ // wall = 4.5 + 14 = 18.5
+ clock.Step(TimeSpan.FromSeconds(18.5));
+ Assert.True(animationRun.Status == TaskStatus.RanToCompletion);
+ Assert.Equal(100d, border.Width);
+ }
+
+ [Fact]
+ public void Pause_Animation_When_IsEffectivelyVisible_Is_False_Nested()
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 200d), }, Cue = new Cue(1d)
+ };
+ var keyframe2 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 150d), }, Cue = new Cue(0.5d)
+ };
+
+ var keyframe3 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 100d), }, Cue = new Cue(0d)
+ };
+
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(3),
+ Delay = TimeSpan.FromSeconds(3),
+ DelayBetweenIterations = TimeSpan.FromSeconds(3),
+ IterationCount = new IterationCount(2),
+ Children = { keyframe1, keyframe2, keyframe3 }
+ };
+
+ var border = new Border() { Height = 100d, Width = 100d };
+
+ var borderParent = new Border { Child = border };
+
+ var clock = new TestClock();
+ var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken);
+
+ border.Measure(Size.Infinity);
+ border.Arrange(new Rect(border.DesiredSize));
+
+ clock.Step(TimeSpan.Zero);
+
+ clock.Step(TimeSpan.FromSeconds(0));
+ Assert.Equal(100d, border.Width);
+
+ // Hide the parent — this makes border.IsEffectivelyVisible false,
+ // which should pause the animation clock.
+ borderParent.IsVisible = false;
+
+ clock.Step(TimeSpan.FromSeconds(4.5));
+
+ // Width should not change while parent is invisible (animation is paused).
+ Assert.Equal(100d, border.Width);
+
+ // Show the parent — animation resumes from where it left off.
+ borderParent.IsVisible = true;
+
+ // The pause absorbed 4.5s of wall-clock time, so internal time = wall - 4.5.
+ // To reach internal time 6s (end of iteration 1, cue 1.0 -> width 200):
+ // wall = 4.5 + 6 = 10.5
+ clock.Step(TimeSpan.FromSeconds(10.5));
+ Assert.Equal(200d, border.Width);
+
+ // To complete the animation (internal time 14s triggers trailing delay of iter 2):
+ // wall = 4.5 + 14 = 18.5
+ clock.Step(TimeSpan.FromSeconds(18.5));
+ Assert.True(animationRun.Status == TaskStatus.RanToCompletion);
+ Assert.Equal(100d, border.Width);
+ }
+
+ [Fact]
+ public void Stop_And_Dispose_Animation_When_Detached_From_Visual_Tree()
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 200d), }, Cue = new Cue(1d)
+ };
+ var keyframe2 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 100d), }, Cue = new Cue(0d)
+ };
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(10),
+ IterationCount = new IterationCount(1),
+ Children = { keyframe2, keyframe1 }
+ };
+ var border = new Border() { Height = 100d, Width = 50d };
+ var root = new TestRoot(border);
+ var clock = new TestClock();
+ var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken);
+ clock.Step(TimeSpan.Zero);
+ clock.Step(TimeSpan.FromSeconds(0));
+ Assert.False(animationRun.IsCompleted);
+
+ // Detach from visual tree
+ root.Child = null;
+
+ // Animation should be completed/disposed
+ Assert.True(animationRun.IsCompleted);
+
+ // Further clock ticks should not affect the border
+ var widthAfterDetach = border.Width;
+ clock.Step(TimeSpan.FromSeconds(5));
+ clock.Step(TimeSpan.FromSeconds(10));
+ Assert.Equal(widthAfterDetach, border.Width);
+ }
+
+ [Fact]
+ public void Pause_Animation_When_Control_Starts_Invisible()
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 200d), }, Cue = new Cue(1d)
+ };
+ var keyframe2 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 100d), }, Cue = new Cue(0d)
+ };
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(3),
+ IterationCount = new IterationCount(1),
+ Children = { keyframe2, keyframe1 }
+ };
+
+ var border = new Border() { Height = 100d, Width = 100d, IsVisible = false };
+ var clock = new TestClock();
+ var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken);
+
+ // Clock ticks while invisible should not advance the animation.
+ clock.Step(TimeSpan.Zero);
+ clock.Step(TimeSpan.FromSeconds(1));
+ clock.Step(TimeSpan.FromSeconds(2));
+ Assert.Equal(100d, border.Width);
+ Assert.False(animationRun.IsCompleted);
+
+ // Make visible — animation starts from the beginning.
+ border.IsVisible = true;
+
+ // The pause absorbed 2s of wall-clock time, so to reach internal time 3s:
+ // wall = 2 + 3 = 5
+ clock.Step(TimeSpan.FromSeconds(5));
+ Assert.True(animationRun.IsCompleted);
+ Assert.Equal(100d, border.Width);
+ }
+
+ [Fact]
+ public void Animation_Plays_Correctly_After_Reattach()
+ {
+ var keyframe1 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 200d), }, Cue = new Cue(1d)
+ };
+ var keyframe2 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 100d), }, Cue = new Cue(0d)
+ };
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(5),
+ IterationCount = new IterationCount(1),
+ FillMode = FillMode.Forward,
+ Children = { keyframe2, keyframe1 }
+ };
+
+ var border = new Border() { Height = 100d, Width = 50d };
+ var root = new TestRoot(border);
+ var clock = new TestClock();
+ var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken);
+
+ clock.Step(TimeSpan.Zero);
+ Assert.False(animationRun.IsCompleted);
+
+ // Detach — animation completes.
+ root.Child = null;
+ Assert.True(animationRun.IsCompleted);
+
+ // Reattach and start a fresh animation.
+ root.Child = border;
+ var clock2 = new TestClock();
+ var animationRun2 = animation.RunAsync(border, clock2, TestContext.Current.CancellationToken);
+
+ clock2.Step(TimeSpan.Zero);
+ Assert.False(animationRun2.IsCompleted);
+
+ clock2.Step(TimeSpan.FromSeconds(5));
+ Assert.True(animationRun2.IsCompleted);
+ Assert.Equal(200d, border.Width);
+ }
[Fact]
public void Check_FillModes_Start_and_End_Values_if_Retained()
From 99b071bf2225cdef828eca1da3a1afc7e77834ac Mon Sep 17 00:00:00 2001
From: Betta_Fish <96322503+zxbmmmmmmmmm@users.noreply.github.com>
Date: Wed, 11 Mar 2026 19:27:06 +0800
Subject: [PATCH 3/4] feat: translation (#20836)
---
.../ServerCompositionVisual.Adorners.cs | 6 +++---
.../ServerCompositionVisual.ComputedProperties.cs | 4 ++--
.../ServerCompositionVisual.DirtyInputs.cs | 7 +++++--
src/Avalonia.Base/composition-schema.xml | 1 +
4 files changed, 11 insertions(+), 7 deletions(-)
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 fe6effbbd4..6f78ca312a 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.Adorners.cs
@@ -52,7 +52,7 @@ partial class ServerCompositionVisual
// 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);
+ RotationAngle, Orientation, Offset + Translation);
if (
AdornerLayer_GetExpectedSharedAncestor(this) is {} sharedAncestor
&& ComputeTransformFromAncestor(AdornedVisual, sharedAncestor, out var adornerLayerToAdornedVisual))
@@ -63,7 +63,7 @@ partial class ServerCompositionVisual
}
else
_ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale,
- RotationAngle, Orientation, Offset);
+ RotationAngle, Orientation, Offset + Translation);
PropagateFlags(true, true);
@@ -170,4 +170,4 @@ partial class ServerCompositionVisual
_pools.IntStackPool.Return(ref _adornerPushedClipStack!);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs
index 4b98b0f80e..ed8860e04a 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.ComputedProperties.cs
@@ -148,7 +148,7 @@ partial class ServerCompositionVisual
if (_combinedTransformDirty)
{
_ownTransform = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, TransformMatrix, Scale,
- RotationAngle, Orientation, Offset);
+ RotationAngle, Orientation, Offset + Translation);
setDirtyForRender = setDirtyBounds = true;
@@ -161,4 +161,4 @@ partial class ServerCompositionVisual
_ownBoundsDirty = _clipSizeDirty = _combinedTransformDirty = _compositionFieldsDirty = false;
PropagateFlags(setDirtyBounds, setDirtyForRender, setHasExtraDirtyRect);
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs
index fa8c6047fc..8352fc70e2 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual/ServerCompositionVisual.DirtyInputs.cs
@@ -43,7 +43,9 @@ partial class ServerCompositionVisual
| CompositionVisualChangedFields.Orientation
| CompositionVisualChangedFields.OrientationAnimated
| CompositionVisualChangedFields.Offset
- | CompositionVisualChangedFields.OffsetAnimated;
+ | CompositionVisualChangedFields.OffsetAnimated
+ | CompositionVisualChangedFields.Translation
+ | CompositionVisualChangedFields.TranslationAnimated;
private const CompositionVisualChangedFields ClipSizeDirtyMask =
CompositionVisualChangedFields.Size
@@ -100,7 +102,8 @@ partial class ServerCompositionVisual
|| property == s_IdOfScaleProperty
|| property == s_IdOfRotationAngleProperty
|| property == s_IdOfOrientationProperty
- || property == s_IdOfOffsetProperty)
+ || property == s_IdOfOffsetProperty
+ || property == s_IdOfTranslationProperty)
TriggerCombinedTransformDirty();
if (property == s_IdOfClipToBoundsProperty
diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml
index febf465042..defe02769c 100644
--- a/src/Avalonia.Base/composition-schema.xml
+++ b/src/Avalonia.Base/composition-schema.xml
@@ -22,6 +22,7 @@
+
From b9995d179d4585ab513f46fbad07ffcddbb80537 Mon Sep 17 00:00:00 2001
From: Nikita Tsukanov
Date: Thu, 12 Mar 2026 03:48:51 +0300
Subject: [PATCH 4/4] Add .serena to .gitignore
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index 826b4d8a5a..23b8efffa0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -219,3 +219,4 @@ src/Browser/Avalonia.Browser.Blazor/wwwroot
src/Browser/Avalonia.Browser/wwwroot
api/diff
src/Browser/Avalonia.Browser/staticwebassets
+.serena