From 658afb87173081b9444d30dd70ae47a04e519e4e Mon Sep 17 00:00:00 2001
From: Jumar Macato <16554748+jmacato@users.noreply.github.com>
Date: Mon, 23 Mar 2026 21:00:46 +0800
Subject: [PATCH 01/57] Animation PlaybackBehavior Implementation and Tests.
(#20966)
* Animation PlaybackBehavior Implementation and Tests.
* fix review
* Fix review comments.
* Add isManuallyStarted to IAnimation interface
---
src/Avalonia.Base/Animation/Animation.cs | 60 +++-
.../Animation/AnimationInstance`1.cs | 44 +--
.../Animation/Animators/Animator`1.cs | 9 +-
.../Animation/Animators/BaseBrushAnimator.cs | 6 +-
.../Animation/Animators/TransformAnimator.cs | 9 +-
.../DisposeAnimationInstanceSubject.cs | 24 +-
src/Avalonia.Base/Animation/IAnimation.cs | 3 +-
src/Avalonia.Base/Animation/IAnimator.cs | 5 +-
.../Animation/PlaybackBehavior.cs | 29 ++
.../Media/Effects/EffectAnimator.cs | 6 +-
src/Avalonia.Base/Styling/StyleInstance.cs | 3 +-
.../Animation/AnimationIterationTests.cs | 310 +++++++++++++++++-
12 files changed, 444 insertions(+), 64 deletions(-)
create mode 100644 src/Avalonia.Base/Animation/PlaybackBehavior.cs
diff --git a/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs
index 0391280ede..5f15f14534 100644
--- a/src/Avalonia.Base/Animation/Animation.cs
+++ b/src/Avalonia.Base/Animation/Animation.cs
@@ -42,6 +42,15 @@ namespace Avalonia.Animation
o => o._playbackDirection,
(o, v) => o._playbackDirection = v);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty PlaybackBehaviorProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(PlaybackBehavior),
+ o => o._playbackBehavior,
+ (o, v) => o._playbackBehavior = v);
+
///
/// Defines the property.
///
@@ -91,6 +100,7 @@ namespace Avalonia.Animation
private TimeSpan _duration;
private IterationCount _iterationCount = new IterationCount(1);
private PlaybackDirection _playbackDirection;
+ private PlaybackBehavior _playbackBehavior;
private FillMode _fillMode;
private Easing _easing = new LinearEasing();
private TimeSpan _delay = TimeSpan.Zero;
@@ -124,6 +134,19 @@ namespace Avalonia.Animation
set { SetAndRaise(PlaybackDirectionProperty, ref _playbackDirection, value); }
}
+ ///
+ /// Gets or sets the playback behavior for this animation.
+ /// When set to , manually started animations and
+ /// animations targeting always play,
+ /// while style-applied animations pause when the control is not effectively visible
+ /// (see ).
+ ///
+ public PlaybackBehavior PlaybackBehavior
+ {
+ get { return _playbackBehavior; }
+ set { SetAndRaise(PlaybackBehaviorProperty, ref _playbackBehavior, value); }
+ }
+
///
/// Gets or sets the value fill mode for this animation.
///
@@ -192,11 +215,12 @@ namespace Avalonia.Animation
return null;
}
- private (IList Animators, IList subscriptions) InterpretKeyframes(Animatable control)
+ private (IList Animators, IList subscriptions, bool animatesVisibility) InterpretKeyframes(Animatable control)
{
var handlerList = new Dictionary<(Type type, AvaloniaProperty Property), Func>();
var animatorKeyFrames = new List();
var subscriptions = new List();
+ var animatesVisibility = false;
foreach (var keyframe in Children)
{
@@ -207,6 +231,9 @@ namespace Avalonia.Animation
throw new InvalidOperationException("No Setter property assigned.");
}
+ if (setter.Property == Visual.IsVisibleProperty)
+ animatesVisibility = true;
+
var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property);
if (handler == null)
@@ -265,19 +292,31 @@ namespace Avalonia.Animation
}
}
- return (newAnimatorInstances, subscriptions);
+ return (newAnimatorInstances, subscriptions, animatesVisibility);
}
- IDisposable IAnimation.Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete)
- => Apply(control, clock, match, onComplete);
-
+ IDisposable IAnimation.Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete,
+ bool isManuallyStarted)
+ => Apply(control, clock, match, onComplete, isManuallyStarted);
+
///
- internal IDisposable Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete)
+ internal IDisposable Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete,
+ bool isManuallyStarted = false)
{
- var (animators, subscriptions) = InterpretKeyframes(control);
+ var (animators, subscriptions, animatesVisibility) = InterpretKeyframes(control);
+
+ var shouldPauseOnInvisible = _playbackBehavior switch
+ {
+ PlaybackBehavior.Auto => !(animatesVisibility || isManuallyStarted),
+ PlaybackBehavior.Always => false,
+ PlaybackBehavior.OnlyIfVisible => true,
+ _ => throw new InvalidOperationException($"Unknown PlaybackBehavior value: {_playbackBehavior}"),
+ };
+
if (animators.Count == 1)
{
- var subscription = animators[0].Apply(this, control, clock, match, onComplete);
+ var subscription = animators[0].Apply(this, control, clock, match,
+ onComplete, shouldPauseOnInvisible);
if (subscription is not null)
{
@@ -297,7 +336,8 @@ namespace Avalonia.Animation
completionTasks!.Add(tcs.Task);
}
- var subscription = animator.Apply(this, control, clock, match, animatorOnComplete);
+ var subscription = animator.Apply(this, control, clock, match,
+ animatorOnComplete, shouldPauseOnInvisible);
if (subscription is not null)
{
@@ -348,7 +388,7 @@ namespace Avalonia.Animation
run.TrySetResult(null);
subscriptions?.Dispose();
cancellation?.Dispose();
- });
+ }, isManuallyStarted: true);
cancellation = cancellationToken.Register(() =>
{
diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs
index 6358dd2c6b..390a4a10b4 100644
--- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs
+++ b/src/Avalonia.Base/Animation/AnimationInstance`1.cs
@@ -38,7 +38,9 @@ namespace Avalonia.Animation
private EventHandler? _visibilityChangedHandler;
private EventHandler? _detachedHandler;
- public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action? OnComplete, Func Interpolator)
+ private readonly bool _shouldPauseOnInvisible;
+
+ public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action? OnComplete, Func Interpolator, bool shouldPauseOnInvisible)
{
_animator = animator;
_animation = animation;
@@ -49,6 +51,7 @@ namespace Avalonia.Animation
_lastInterpValue = default!;
_firstKFValue = default!;
_neutralValue = default!;
+ _shouldPauseOnInvisible = shouldPauseOnInvisible;
FetchProperties();
}
@@ -120,26 +123,29 @@ namespace Avalonia.Animation
if (_targetControl is Visual visual)
{
- _visibilityChangedHandler = (_, _) =>
+ if (_shouldPauseOnInvisible)
{
- if (_clock is null || _clock.PlayState == PlayState.Stop)
- return;
- if (visual.IsEffectivelyVisible)
- {
- if (_clock.PlayState == PlayState.Pause)
- _clock.PlayState = PlayState.Run;
- }
- else
+ _visibilityChangedHandler = (_, _) =>
{
- 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;
+ 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();
diff --git a/src/Avalonia.Base/Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs
index 954b62f9bc..20311c8389 100644
--- a/src/Avalonia.Base/Animation/Animators/Animator`1.cs
+++ b/src/Avalonia.Base/Animation/Animators/Animator`1.cs
@@ -17,9 +17,9 @@ namespace Avalonia.Animation.Animators
public AvaloniaProperty? Property { get; set; }
///
- public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete)
+ public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete, bool shouldPauseOnInvisible)
{
- var subject = new DisposeAnimationInstanceSubject(this, animation, control, clock, onComplete);
+ var subject = new DisposeAnimationInstanceSubject(this, animation, control, clock, onComplete, shouldPauseOnInvisible);
return new CompositeDisposable(match.Subscribe(subject), subject);
}
@@ -103,7 +103,7 @@ namespace Avalonia.Animation.Animators
///
/// Runs the KeyFrames Animation.
///
- internal IDisposable Run(Animation animation, Animatable control, IClock? clock, Action? onComplete)
+ internal IDisposable Run(Animation animation, Animatable control, IClock? clock, Action? onComplete, bool shouldPauseOnInvisible)
{
var instance = new AnimationInstance(
animation,
@@ -111,7 +111,8 @@ namespace Avalonia.Animation.Animators
this,
clock ?? control.Clock ?? Clock.GlobalClock,
onComplete,
- InterpolationHandler);
+ InterpolationHandler,
+ shouldPauseOnInvisible);
return BindAnimation(control, instance);
}
diff --git a/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs
index c81be67060..e18b98ad26 100644
--- a/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs
+++ b/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs
@@ -38,20 +38,20 @@ namespace Avalonia.Animation.Animators
///
public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock,
- IObservable match, Action? onComplete)
+ IObservable match, Action? onComplete, bool shouldPauseOnInvisible)
{
if (TryCreateCustomRegisteredAnimator(out var animator)
|| TryCreateGradientAnimator(out animator)
|| TryCreateSolidColorBrushAnimator(out animator))
{
- return animator.Apply(animation, control, clock, match, onComplete);
+ return animator.Apply(animation, control, clock, match, onComplete, shouldPauseOnInvisible);
}
Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log(
this,
"The animation's keyframe value types set is not supported.");
- return base.Apply(animation, control, clock, match, onComplete);
+ return base.Apply(animation, control, clock, match, onComplete, shouldPauseOnInvisible);
}
///
diff --git a/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs b/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs
index b1fc720e6a..aef5c78f95 100644
--- a/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs
+++ b/src/Avalonia.Base/Animation/Animators/TransformAnimator.cs
@@ -14,7 +14,8 @@ namespace Avalonia.Animation.Animators
DoubleAnimator? _doubleAnimator;
///
- public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable obsMatch, Action? onComplete)
+ public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock,
+ IObservable obsMatch, Action? onComplete, bool shouldPauseOnInvisible)
{
var ctrl = (Visual)control;
@@ -65,7 +66,8 @@ namespace Avalonia.Animation.Animators
// It's a transform object so let's target that.
if (renderTransformType == Property.OwnerType)
{
- return _doubleAnimator.Apply(animation, (Transform) ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete);
+ return _doubleAnimator.Apply(animation, (Transform) ctrl.RenderTransform, clock ?? control.Clock,
+ obsMatch, onComplete, shouldPauseOnInvisible);
}
// It's a TransformGroup and try finding the target there.
else if (renderTransformType == typeof(TransformGroup))
@@ -74,7 +76,8 @@ namespace Avalonia.Animation.Animators
{
if (transform.GetType() == Property.OwnerType)
{
- return _doubleAnimator.Apply(animation, transform, clock ?? control.Clock, obsMatch, onComplete);
+ return _doubleAnimator.Apply(animation, transform, clock ?? control.Clock,
+ obsMatch, onComplete, shouldPauseOnInvisible);
}
}
}
diff --git a/src/Avalonia.Base/Animation/DisposeAnimationInstanceSubject.cs b/src/Avalonia.Base/Animation/DisposeAnimationInstanceSubject.cs
index af25766289..7d355cc77c 100644
--- a/src/Avalonia.Base/Animation/DisposeAnimationInstanceSubject.cs
+++ b/src/Avalonia.Base/Animation/DisposeAnimationInstanceSubject.cs
@@ -6,24 +6,18 @@ namespace Avalonia.Animation
///
/// Manages the lifetime of animation instances as determined by its selector state.
///
- internal class DisposeAnimationInstanceSubject : IObserver, IDisposable
+ internal class DisposeAnimationInstanceSubject(
+ Animator animator,
+ Animation animation,
+ Animatable control,
+ IClock? clock,
+ Action? onComplete,
+ bool shouldPauseOnInvisible)
+ : IObserver, IDisposable
{
private IDisposable? _lastInstance;
private bool _lastMatch;
- private readonly Animator _animator;
- private readonly Animation _animation;
- private readonly Animatable _control;
- private readonly Action? _onComplete;
- private readonly IClock? _clock;
- public DisposeAnimationInstanceSubject(Animator animator, Animation animation, Animatable control, IClock? clock, Action? onComplete)
- {
- this._animator = animator;
- this._animation = animation;
- this._control = control;
- this._onComplete = onComplete;
- this._clock = clock;
- }
public void Dispose()
{
_lastInstance?.Dispose();
@@ -47,7 +41,7 @@ namespace Avalonia.Animation
if (matchVal)
{
- _lastInstance = _animator.Run(_animation, _control, _clock, _onComplete);
+ _lastInstance = animator.Run(animation, control, clock, onComplete, shouldPauseOnInvisible);
}
else
{
diff --git a/src/Avalonia.Base/Animation/IAnimation.cs b/src/Avalonia.Base/Animation/IAnimation.cs
index a5b8b75b12..53146a3edd 100644
--- a/src/Avalonia.Base/Animation/IAnimation.cs
+++ b/src/Avalonia.Base/Animation/IAnimation.cs
@@ -14,7 +14,8 @@ namespace Avalonia.Animation
///
/// Apply the animation to the specified control and run it when produces true.
///
- internal IDisposable Apply(Animatable control, IClock? clock, IObservable match, Action? onComplete = null);
+ internal IDisposable Apply(Animatable control, IClock? clock, IObservable match,
+ Action? onComplete = null, bool isManuallyStarted = false);
///
/// Run the animation on the specified control.
diff --git a/src/Avalonia.Base/Animation/IAnimator.cs b/src/Avalonia.Base/Animation/IAnimator.cs
index 9fed7be9be..f065ad3730 100644
--- a/src/Avalonia.Base/Animation/IAnimator.cs
+++ b/src/Avalonia.Base/Animation/IAnimator.cs
@@ -12,11 +12,12 @@ namespace Avalonia.Animation
///
/// The target property.
///
- AvaloniaProperty? Property {get; set;}
+ AvaloniaProperty? Property { get; set; }
///
/// Applies the current KeyFrame group to the specified control.
///
- IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete);
+ IDisposable? Apply(Animation animation, Animatable control, IClock? clock,
+ IObservable match, Action? onComplete, bool shouldPauseOnInvisible);
}
}
diff --git a/src/Avalonia.Base/Animation/PlaybackBehavior.cs b/src/Avalonia.Base/Animation/PlaybackBehavior.cs
new file mode 100644
index 0000000000..4dcfd5c784
--- /dev/null
+++ b/src/Avalonia.Base/Animation/PlaybackBehavior.cs
@@ -0,0 +1,29 @@
+namespace Avalonia.Animation
+{
+ ///
+ /// Determines whether an animation pauses when its target control is not effectively visible
+ /// (see ).
+ ///
+ public enum PlaybackBehavior
+ {
+ ///
+ /// The system decides based on context. Manually started animations
+ /// (via )
+ /// and animations that target always play.
+ /// Style-applied animations pause when the control is not effectively visible
+ /// (see ).
+ ///
+ Auto,
+
+ ///
+ /// The animation always plays regardless of the control's effective visibility state.
+ ///
+ Always,
+
+ ///
+ /// The animation pauses when the control is not effectively visible
+ /// (see ).
+ ///
+ OnlyIfVisible,
+ }
+}
diff --git a/src/Avalonia.Base/Media/Effects/EffectAnimator.cs b/src/Avalonia.Base/Media/Effects/EffectAnimator.cs
index e2c4cc096c..a81e58f2d0 100644
--- a/src/Avalonia.Base/Media/Effects/EffectAnimator.cs
+++ b/src/Avalonia.Base/Media/Effects/EffectAnimator.cs
@@ -10,17 +10,17 @@ namespace Avalonia.Animation.Animators;
internal class EffectAnimator : Animator
{
public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock,
- IObservable match, Action? onComplete)
+ IObservable match, Action? onComplete, bool shouldPauseOnInvisible)
{
if (TryCreateAnimator(out var animator)
|| TryCreateAnimator(out animator))
- return animator.Apply(animation, control, clock, match, onComplete);
+ return animator.Apply(animation, control, clock, match, onComplete, shouldPauseOnInvisible);
Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log(
this,
"The animation's keyframe value types set is not supported.");
- return base.Apply(animation, control, clock, match, onComplete);
+ return base.Apply(animation, control, clock, match, onComplete, shouldPauseOnInvisible);
}
private bool TryCreateAnimator([NotNullWhen(true)] out IAnimator? animator)
diff --git a/src/Avalonia.Base/Styling/StyleInstance.cs b/src/Avalonia.Base/Styling/StyleInstance.cs
index c397aef8c6..1ecaf3734a 100644
--- a/src/Avalonia.Base/Styling/StyleInstance.cs
+++ b/src/Avalonia.Base/Styling/StyleInstance.cs
@@ -72,7 +72,8 @@ namespace Avalonia.Styling
_animationTrigger ??= new LightweightSubject();
_animationApplyDisposables ??= new List();
foreach (var animation in _animations)
- _animationApplyDisposables.Add(animation.Apply(animatable, null, _animationTrigger));
+ _animationApplyDisposables.Add(animation.Apply(animatable, null, _animationTrigger,
+ onComplete: null, isManuallyStarted: false));
if (_activator is null)
_animationTrigger.OnNext(true);
diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
index 752c1b166b..0ca5a3be6a 100644
--- a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
@@ -95,7 +95,7 @@ namespace Avalonia.Base.UnitTests.Animation
}
[Fact]
- public void Pause_Animation_When_IsEffectivelyVisible_Is_False()
+ public void OnlyIfVisible_Pauses_Animation_When_IsEffectivelyVisible_Is_False()
{
var keyframe1 = new KeyFrame()
{
@@ -117,6 +117,9 @@ namespace Avalonia.Base.UnitTests.Animation
Delay = TimeSpan.FromSeconds(3),
DelayBetweenIterations = TimeSpan.FromSeconds(3),
IterationCount = new IterationCount(2),
+ // Explicit opt-in: RunAsync (manual) with Auto resolves to Always,
+ // but this test specifically exercises the pause-on-invisible feature.
+ PlaybackBehavior = PlaybackBehavior.OnlyIfVisible,
Children = { keyframe1, keyframe2, keyframe3 }
};
@@ -158,7 +161,7 @@ namespace Avalonia.Base.UnitTests.Animation
}
[Fact]
- public void Pause_Animation_When_IsEffectivelyVisible_Is_False_Nested()
+ public void OnlyIfVisible_Pauses_Animation_When_IsEffectivelyVisible_Is_False_Nested()
{
var keyframe1 = new KeyFrame()
{
@@ -180,6 +183,9 @@ namespace Avalonia.Base.UnitTests.Animation
Delay = TimeSpan.FromSeconds(3),
DelayBetweenIterations = TimeSpan.FromSeconds(3),
IterationCount = new IterationCount(2),
+ // Explicit opt-in: RunAsync (manual) with Auto resolves to Always,
+ // but this test specifically exercises the pause-on-invisible feature.
+ PlaybackBehavior = PlaybackBehavior.OnlyIfVisible,
Children = { keyframe1, keyframe2, keyframe3 }
};
@@ -262,7 +268,7 @@ namespace Avalonia.Base.UnitTests.Animation
}
[Fact]
- public void Pause_Animation_When_Control_Starts_Invisible()
+ public void OnlyIfVisible_Pauses_Animation_When_Control_Starts_Invisible()
{
var keyframe1 = new KeyFrame()
{
@@ -276,6 +282,9 @@ namespace Avalonia.Base.UnitTests.Animation
{
Duration = TimeSpan.FromSeconds(3),
IterationCount = new IterationCount(1),
+ // Explicit opt-in: RunAsync (manual) with Auto resolves to Always,
+ // but this test specifically exercises the pause-on-invisible feature.
+ PlaybackBehavior = PlaybackBehavior.OnlyIfVisible,
Children = { keyframe2, keyframe1 }
};
@@ -960,6 +969,301 @@ namespace Avalonia.Base.UnitTests.Animation
}
}
+ [Fact]
+ public void Animation_Can_Set_IsVisible_True_On_Invisible_Control()
+ {
+ // Reproduces a bug where an expand animation tries to make a collapsed
+ // (invisible) control visible at Cue 0.0, but the animation system pauses
+ // animations on invisible controls, creating a deadlock where the animation
+ // can't run to set IsVisible=true because the control is already invisible.
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(0.3),
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ new KeyFrame()
+ {
+ Cue = new Cue(0.0),
+ Setters = { new Setter(Visual.IsVisibleProperty, true) }
+ },
+ new KeyFrame()
+ {
+ Cue = new Cue(1.0),
+ Setters = { new Setter(Visual.IsVisibleProperty, true) }
+ }
+ }
+ };
+
+ // Control starts invisible (collapsed state).
+ var border = new Border() { IsVisible = false };
+
+ var clock = new TestClock();
+ var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken);
+
+ // Kick off the animation.
+ clock.Step(TimeSpan.Zero);
+
+ // The Cue 0.0 keyframe should have set IsVisible = true,
+ // even though the control started invisible.
+ Assert.True(border.IsVisible);
+
+ // Animation should progress to completion.
+ clock.Step(TimeSpan.FromSeconds(0.3));
+ Assert.True(animationRun.IsCompleted);
+ }
+
+ [Fact]
+ public void Width_Animation_Resumes_After_IsVisible_Set_True_On_Invisible_Control()
+ {
+ // Tests the expand scenario with OnlyIfVisible: the control starts invisible
+ // and the animation is paused. Once IsVisible is set to true externally,
+ // the animation resumes and completes.
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(0.3),
+ Easing = new LinearEasing(),
+ FillMode = FillMode.Forward,
+ PlaybackBehavior = PlaybackBehavior.OnlyIfVisible,
+ Children =
+ {
+ new KeyFrame()
+ {
+ Cue = new Cue(0.0),
+ Setters = { new Setter(Layoutable.WidthProperty, 0d) }
+ },
+ new KeyFrame()
+ {
+ Cue = new Cue(1.0),
+ Setters = { new Setter(Layoutable.WidthProperty, 100d) }
+ }
+ }
+ };
+
+ // Control starts invisible (collapsed state).
+ var border = new Border() { Width = 0d, IsVisible = false };
+
+ var clock = new TestClock();
+ var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken);
+
+ // Animation is paused because control is invisible.
+ clock.Step(TimeSpan.Zero);
+ Assert.Equal(0d, border.Width);
+ Assert.False(animationRun.IsCompleted);
+
+ // Simulate what the expand handler does: set IsVisible = true externally.
+ border.IsVisible = true;
+
+ // The animation should now resume and complete.
+ clock.Step(TimeSpan.FromSeconds(0.3));
+ Assert.True(animationRun.IsCompleted);
+ Assert.Equal(100d, border.Width);
+ }
+
+ [Fact]
+ public void Animation_Can_Set_IsVisible_False_At_End_Without_Pausing_Itself()
+ {
+ // An animation that sets IsVisible=false at Cue 1.0 should complete normally.
+ // The visibility change at the final keyframe should not cause the animation
+ // to pause before it can report completion.
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(0.3),
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ new KeyFrame()
+ {
+ Cue = new Cue(0.0),
+ Setters = { new Setter(Visual.IsVisibleProperty, true) }
+ },
+ new KeyFrame()
+ {
+ Cue = new Cue(1.0),
+ Setters = { new Setter(Visual.IsVisibleProperty, false) }
+ }
+ }
+ };
+
+ // Control starts visible (expanded state).
+ var border = new Border() { IsVisible = true };
+
+ var clock = new TestClock();
+ var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken);
+
+ clock.Step(TimeSpan.Zero);
+ Assert.True(border.IsVisible);
+
+ // Step to the end: animation sets IsVisible=false.
+ clock.Step(TimeSpan.FromSeconds(0.3));
+
+ // Animation should have completed and the final value should hold.
+ Assert.True(animationRun.IsCompleted);
+ Assert.False(border.IsVisible);
+ }
+
+ [Fact]
+ public async Task Cancelling_Expand_Animation_Mid_Flight_Then_Collapsing_Works()
+ {
+ // Reproduces the scenario where a user rapidly toggles expand/collapse:
+ // the first animation is cancelled and a new one starts in the opposite direction.
+ // Uses single-property animations to isolate the visibility behavior.
+ var expandAnimation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(0.3),
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ new KeyFrame()
+ {
+ Cue = new Cue(0.0),
+ Setters = { new Setter(Visual.IsVisibleProperty, true) }
+ },
+ new KeyFrame()
+ {
+ Cue = new Cue(1.0),
+ Setters = { new Setter(Visual.IsVisibleProperty, true) }
+ }
+ }
+ };
+
+ var collapseAnimation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(0.3),
+ FillMode = FillMode.Forward,
+ Children =
+ {
+ new KeyFrame()
+ {
+ Cue = new Cue(0.0),
+ Setters = { new Setter(Visual.IsVisibleProperty, true) }
+ },
+ new KeyFrame()
+ {
+ Cue = new Cue(1.0),
+ Setters = { new Setter(Visual.IsVisibleProperty, false) }
+ }
+ }
+ };
+
+ var border = new Border() { IsVisible = false };
+
+ // Start expand.
+ var cts1 = new CancellationTokenSource();
+ var clock1 = new TestClock();
+ var expandRun = expandAnimation.RunAsync(border, clock1, cts1.Token);
+
+ clock1.Step(TimeSpan.Zero);
+ Assert.True(border.IsVisible);
+
+ // Partially through expand, cancel and start collapse.
+ clock1.Step(TimeSpan.FromSeconds(0.15));
+ cts1.Cancel();
+ await expandRun;
+
+ var cts2 = new CancellationTokenSource();
+ var clock2 = new TestClock();
+ var collapseRun = collapseAnimation.RunAsync(border, clock2, cts2.Token);
+
+ clock2.Step(TimeSpan.Zero);
+ clock2.Step(TimeSpan.FromSeconds(0.3));
+
+ Assert.True(collapseRun.IsCompleted);
+ Assert.False(border.IsVisible);
+ }
+
+ [Fact]
+ public void Auto_Pauses_On_Invisible_When_Started_From_Style()
+ {
+ // When started via Apply (the style path), Auto resolves to OnlyIfVisible.
+ // The animation should pause when the control becomes invisible.
+ var keyframe1 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 100d) }, Cue = new Cue(0d)
+ };
+ var keyframe2 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 200d) }, Cue = new Cue(1d)
+ };
+
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(3),
+ IterationCount = new IterationCount(1),
+ Children = { keyframe1, keyframe2 }
+ };
+
+ var border = new Border() { Height = 100d, Width = 50d };
+
+ var clock = new TestClock();
+ var completed = false;
+
+ // Apply (not RunAsync), this is the style-applied path.
+ var disposable = animation.Apply(border, clock, Observable.Return(true), () => completed = true);
+
+ clock.Step(TimeSpan.Zero);
+ Assert.Equal(100d, border.Width);
+
+ // Hide the control, animation should pause under Auto.
+ border.IsVisible = false;
+
+ clock.Step(TimeSpan.FromSeconds(1.5));
+ // Width should not have advanced while invisible.
+ Assert.Equal(100d, border.Width);
+
+ // Show the control, animation resumes.
+ border.IsVisible = true;
+
+ clock.Step(TimeSpan.FromSeconds(4.5));
+ Assert.True(completed);
+
+ disposable.Dispose();
+ }
+
+ [Fact]
+ public void Auto_Does_Not_Pause_On_Invisible_When_Started_Manually()
+ {
+ // When started via RunAsync (manual), Auto resolves to Always.
+ // The animation should NOT pause when the control becomes invisible.
+ var keyframe1 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 100d) }, Cue = new Cue(0d)
+ };
+ var keyframe2 = new KeyFrame()
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 200d) }, Cue = new Cue(1d)
+ };
+
+ var animation = new Animation()
+ {
+ Duration = TimeSpan.FromSeconds(3),
+ IterationCount = new IterationCount(1),
+ Easing = new LinearEasing(),
+ FillMode = FillMode.Forward,
+ Children = { keyframe1, keyframe2 }
+ };
+
+ var border = new Border() { Height = 100d, Width = 50d };
+
+ var clock = new TestClock();
+ var animationRun = animation.RunAsync(border, clock, TestContext.Current.CancellationToken);
+
+ clock.Step(TimeSpan.Zero);
+ Assert.Equal(100d, border.Width);
+
+ // Hide the control, animation should keep running under Auto + manual.
+ border.IsVisible = false;
+
+ // Width should advance while invisible (not paused).
+ clock.Step(TimeSpan.FromSeconds(1.5));
+ Assert.Equal(150d, border.Width);
+ Assert.False(animationRun.IsCompleted);
+
+ clock.Step(TimeSpan.FromSeconds(3));
+ Assert.True(animationRun.IsCompleted);
+ Assert.Equal(200d, border.Width);
+ }
+
private sealed class FakeAnimator : InterpolatingAnimator
{
public double LastProgress { get; set; } = double.NaN;
From d30779c5a6625a2a5833a1acac31b8fba7a6f64d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Javier=20Su=C3=A1rez?=
Date: Mon, 23 Mar 2026 17:52:16 +0100
Subject: [PATCH 02/57] Rename PipsPager button theme APIs (#20954)
* Rename PipsPager button theme
* Updated PipsPager render tests
* Updated tests
* Updated Avalonia.nupkg.xml
---
api/Avalonia.nupkg.xml | 96 ++++++++++++++++++
...l => PipsPagerCustomButtonThemesPage.xaml} | 26 ++---
.../PipsPagerCustomButtonThemesPage.xaml.cs | 11 ++
.../PipsPagerCustomButtonsPage.xaml.cs | 11 --
.../Pages/PipsPagerPage.xaml.cs | 6 +-
src/Avalonia.Controls/Page/NavigationPage.cs | 17 +++-
src/Avalonia.Controls/PipsPager/PipsPager.cs | 28 ++---
.../Controls/PipsPager.xaml | 61 +++++++++--
.../Controls/PipsPager.xaml | 4 +
.../DrawerPageTests.cs | 2 +-
.../NavigationPageTests.cs | 28 ++++-
.../PipsPagerTests.cs | 40 ++++++++
.../Controls/PipsPagerTests.cs | 45 ++++----
.../PipsPager/PipsPager_Default.expected.png | Bin 1498 -> 1087 bytes
.../PipsPager_Preselected_Index.expected.png | Bin 1401 -> 1082 bytes
15 files changed, 291 insertions(+), 84 deletions(-)
rename samples/ControlCatalog/Pages/PipsPager/{PipsPagerCustomButtonsPage.xaml => PipsPagerCustomButtonThemesPage.xaml} (83%)
create mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml.cs
delete mode 100644 samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs
diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index b2a81dd55d..86fd0cdd75 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -5641,4 +5641,100 @@
baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+ CP0002
+ F:Avalonia.Controls.NavigationPage.IsBackButtonEffectivelyVisibleProperty
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.NavigationPage.get_IsBackButtonEffectivelyVisible
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ F:Avalonia.Controls.PipsPager.PreviousButtonStyleProperty
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ F:Avalonia.Controls.PipsPager.NextButtonStyleProperty
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.get_PreviousButtonStyle
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.set_PreviousButtonStyle(Avalonia.Styling.ControlTheme)
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.get_NextButtonStyle
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.set_NextButtonStyle(Avalonia.Styling.ControlTheme)
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ F:Avalonia.Controls.NavigationPage.IsBackButtonEffectivelyVisibleProperty
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.NavigationPage.get_IsBackButtonEffectivelyVisible
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0002
+ F:Avalonia.Controls.PipsPager.PreviousButtonStyleProperty
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0002
+ F:Avalonia.Controls.PipsPager.NextButtonStyleProperty
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.get_PreviousButtonStyle
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.set_PreviousButtonStyle(Avalonia.Styling.ControlTheme)
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.get_NextButtonStyle
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.set_NextButtonStyle(Avalonia.Styling.ControlTheme)
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml
similarity index 83%
rename from samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml
rename to samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml
index 8b9856424d..3e32f253f5 100644
--- a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml
+++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml
@@ -1,17 +1,17 @@
+ x:Class="ControlCatalog.Pages.PipsPagerCustomButtonThemesPage">
-
+ Text="Replace the default chevron navigation buttons with custom button themes using PreviousButtonTheme and NextButtonTheme." />
-
-
+
+
@@ -27,7 +27,7 @@
-
+
@@ -41,7 +41,7 @@
-
+
@@ -56,12 +56,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml.cs
new file mode 100644
index 0000000000..26682b9d61
--- /dev/null
+++ b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonThemesPage.xaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace ControlCatalog.Pages;
+
+public partial class PipsPagerCustomButtonThemesPage : UserControl
+{
+ public PipsPagerCustomButtonThemesPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs
deleted file mode 100644
index 4fc74995bc..0000000000
--- a/samples/ControlCatalog/Pages/PipsPager/PipsPagerCustomButtonsPage.xaml.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Avalonia.Controls;
-
-namespace ControlCatalog.Pages;
-
-public partial class PipsPagerCustomButtonsPage : UserControl
-{
- public PipsPagerCustomButtonsPage()
- {
- InitializeComponent();
- }
-}
diff --git a/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs
index 33cc6d0fdf..0559a265e4 100644
--- a/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs
@@ -25,9 +25,9 @@ namespace ControlCatalog.Pages
("Appearance", "Custom Colors",
"Override pip indicator colors using resource keys for normal, selected, and hover states.",
() => new PipsPagerCustomColorsPage()),
- ("Appearance", "Custom Buttons",
- "Replace the default chevron navigation buttons with custom styled buttons.",
- () => new PipsPagerCustomButtonsPage()),
+ ("Appearance", "Custom Button Themes",
+ "Replace the default chevron navigation buttons with custom button themes.",
+ () => new PipsPagerCustomButtonThemesPage()),
("Appearance", "Custom Templates",
"Override pip item templates to create squares, pills, numbers, or any custom shape.",
() => new PipsPagerCustomTemplatesPage()),
diff --git a/src/Avalonia.Controls/Page/NavigationPage.cs b/src/Avalonia.Controls/Page/NavigationPage.cs
index 8e38fbbdbc..e736535e75 100644
--- a/src/Avalonia.Controls/Page/NavigationPage.cs
+++ b/src/Avalonia.Controls/Page/NavigationPage.cs
@@ -63,13 +63,14 @@ namespace Avalonia.Controls
private ContentPresenter? _modalPresenter;
private ContentPresenter? _topCommandBarPresenter;
private IDisposable? _hasNavigationBarSub;
+ private IDisposable? _hasBackButtonSub;
private IDisposable? _isBackButtonEnabledSub;
private IDisposable? _barLayoutBehaviorSub;
private IDisposable? _barHeightSub;
private IDisposable? _backButtonContentSub;
private bool _isNavigating;
private bool _canGoBack;
- private bool? _isBackButtonEffectivelyVisible;
+ private bool _isBackButtonEffectivelyVisible;
private bool _isNavBarEffectivelyVisible;
private double _effectiveBarHeight;
private bool _isBackButtonEffectivelyEnabled;
@@ -110,8 +111,8 @@ namespace Avalonia.Controls
///
/// Defines the property.
///
- public static readonly DirectProperty IsBackButtonEffectivelyVisibleProperty =
- AvaloniaProperty.RegisterDirect(nameof(IsBackButtonEffectivelyVisible), o => o.IsBackButtonEffectivelyVisible);
+ public static readonly DirectProperty IsBackButtonEffectivelyVisibleProperty =
+ AvaloniaProperty.RegisterDirect(nameof(IsBackButtonEffectivelyVisible), o => o.IsBackButtonEffectivelyVisible);
///
/// Defines the property.
@@ -330,7 +331,7 @@ namespace Avalonia.Controls
///
/// Gets the effective back-button visibility.
///
- public bool? IsBackButtonEffectivelyVisible
+ public bool IsBackButtonEffectivelyVisible
{
get => _isBackButtonEffectivelyVisible;
private set => SetAndRaise(IsBackButtonEffectivelyVisibleProperty, ref _isBackButtonEffectivelyVisible, value);
@@ -730,6 +731,8 @@ namespace Avalonia.Controls
_hasNavigationBarSub?.Dispose();
_hasNavigationBarSub = null;
+ _hasBackButtonSub?.Dispose();
+ _hasBackButtonSub = null;
_isBackButtonEnabledSub?.Dispose();
_isBackButtonEnabledSub = null;
_barLayoutBehaviorSub?.Dispose();
@@ -1840,6 +1843,9 @@ namespace Avalonia.Controls
_hasNavigationBarSub?.Dispose();
_hasNavigationBarSub = null;
+ _hasBackButtonSub?.Dispose();
+ _hasBackButtonSub = null;
+
_isBackButtonEnabledSub?.Dispose();
_isBackButtonEnabledSub = null;
@@ -1857,6 +1863,9 @@ namespace Avalonia.Controls
_hasNavigationBarSub = page.GetObservable(HasNavigationBarProperty)
.Subscribe(new AnonymousObserver(_ => UpdateIsNavBarEffectivelyVisible()));
+ _hasBackButtonSub = page.GetObservable(HasBackButtonProperty)
+ .Subscribe(new AnonymousObserver(_ => UpdateIsBackButtonEffectivelyVisible()));
+
_isBackButtonEnabledSub = page.GetObservable(IsBackButtonEnabledProperty)
.Subscribe(new AnonymousObserver(_ => UpdateIsBackButtonEffectivelyEnabled()));
diff --git a/src/Avalonia.Controls/PipsPager/PipsPager.cs b/src/Avalonia.Controls/PipsPager/PipsPager.cs
index b976df4826..a83c4ade0d 100644
--- a/src/Avalonia.Controls/PipsPager/PipsPager.cs
+++ b/src/Avalonia.Controls/PipsPager/PipsPager.cs
@@ -87,16 +87,16 @@ namespace Avalonia.Controls
x => x.TemplateSettings);
///
- /// Defines the property.
+ /// Defines the property.
///
- public static readonly StyledProperty PreviousButtonStyleProperty =
- AvaloniaProperty.Register(nameof(PreviousButtonStyle));
+ public static readonly StyledProperty PreviousButtonThemeProperty =
+ AvaloniaProperty.Register(nameof(PreviousButtonTheme));
///
- /// Defines the property.
+ /// Defines the property.
///
- public static readonly StyledProperty NextButtonStyleProperty =
- AvaloniaProperty.Register(nameof(NextButtonStyle));
+ public static readonly StyledProperty NextButtonThemeProperty =
+ AvaloniaProperty.Register(nameof(NextButtonTheme));
///
/// Defines the event.
@@ -195,21 +195,21 @@ namespace Avalonia.Controls
}
///
- /// Gets or sets the style for the previous button.
+ /// Gets or sets the theme for the previous button.
///
- public ControlTheme? PreviousButtonStyle
+ public ControlTheme? PreviousButtonTheme
{
- get => GetValue(PreviousButtonStyleProperty);
- set => SetValue(PreviousButtonStyleProperty, value);
+ get => GetValue(PreviousButtonThemeProperty);
+ set => SetValue(PreviousButtonThemeProperty, value);
}
///
- /// Gets or sets the style for the next button.
+ /// Gets or sets the theme for the next button.
///
- public ControlTheme? NextButtonStyle
+ public ControlTheme? NextButtonTheme
{
- get => GetValue(NextButtonStyleProperty);
- set => SetValue(NextButtonStyleProperty, value);
+ get => GetValue(NextButtonThemeProperty);
+ set => SetValue(NextButtonThemeProperty, value);
}
///
diff --git a/src/Avalonia.Themes.Fluent/Controls/PipsPager.xaml b/src/Avalonia.Themes.Fluent/Controls/PipsPager.xaml
index 6d20216a48..a331b63977 100644
--- a/src/Avalonia.Themes.Fluent/Controls/PipsPager.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/PipsPager.xaml
@@ -13,12 +13,12 @@
M 2.29,8.12 L 7.00,3.41 C 7.27,3.14 7.71,3.14 7.98,3.41 L 12.69,8.12 C 13.14,8.57 12.82,9.33 12.19,9.33 L 2.79,9.33 C 2.16,9.33 1.84,8.57 2.29,8.12 Z
M 2.29,3.88 L 7.00,8.59 C 7.27,8.86 7.71,8.86 7.98,8.59 L 12.69,3.88 C 13.14,3.43 12.82,2.67 12.19,2.67 L 2.79,2.67 C 2.16,2.67 1.84,3.43 2.29,3.88 Z
-
+
-
+
-
-
+
+
@@ -41,16 +41,20 @@
@@ -73,6 +77,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -122,8 +161,8 @@
-
-
+
+
-public partial class DispatcherTimer
+public class DispatcherTimer
{
internal static int ActiveTimersCount { get; private set; }
///
- /// Creates a timer that uses theUI thread's Dispatcher2 to
- /// process the timer event at background priority.
+ /// Creates a timer that uses to
+ /// process the timer event at background priority.
///
- public DispatcherTimer() : this(DispatcherPriority.Background)
+ public DispatcherTimer()
+ : this(TimeSpan.Zero, DispatcherPriority.Background, Dispatcher.CurrentDispatcher)
{
}
///
- /// Creates a timer that uses the UI thread's Dispatcher2 to
- /// process the timer event at the specified priority.
+ /// Creates a timer that uses to
+ /// process the timer event at the specified priority.
///
- ///
- /// The priority to process the timer at.
- ///
- public DispatcherTimer(DispatcherPriority priority) : this(Threading.Dispatcher.UIThread, priority,
- TimeSpan.FromMilliseconds(0))
+ /// The priority to process the timer at.
+ public DispatcherTimer(DispatcherPriority priority)
+ : this(TimeSpan.Zero, priority, Dispatcher.CurrentDispatcher)
{
}
///
- /// Creates a timer that uses the specified Dispatcher2 to
- /// process the timer event at the specified priority.
+ /// Creates a timer that uses the specified to
+ /// process the timer event at the specified priority.
///
- ///
- /// The priority to process the timer at.
- ///
- ///
- /// The dispatcher to use to process the timer.
- ///
- internal DispatcherTimer(DispatcherPriority priority, Dispatcher dispatcher) : this(dispatcher, priority,
- TimeSpan.FromMilliseconds(0))
+ /// The priority to process the timer at.
+ /// The dispatcher to use to process the timer.
+ public DispatcherTimer(DispatcherPriority priority, Dispatcher dispatcher)
+ : this(TimeSpan.Zero, priority, dispatcher)
{
}
///
- /// Creates a timer that uses the UI thread's Dispatcher2 to
- /// process the timer event at the specified priority after the specified timeout.
+ /// Creates a timer that uses the specified to
+ /// process the timer event at the specified priority after the specified timeout.
///
- ///
- /// The interval to tick the timer after.
- ///
- ///
- /// The priority to process the timer at.
- ///
- ///
- /// The callback to call when the timer ticks.
- ///
- public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback)
- : this(Threading.Dispatcher.UIThread, priority, interval)
+ /// The interval to tick the timer after.
+ /// The priority to process the timer at.
+ /// The dispatcher to use to process the timer.
+ public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, Dispatcher dispatcher)
{
- if (callback == null)
+ ArgumentNullException.ThrowIfNull(dispatcher);
+
+ DispatcherPriority.Validate(priority, "priority");
+ if (priority == DispatcherPriority.Inactive)
+ {
+ throw new ArgumentException("Specified priority is not valid.", nameof(priority));
+ }
+
+ var ms = interval.TotalMilliseconds;
+ if (ms < 0)
{
- throw new ArgumentNullException(nameof(callback));
+ throw new ArgumentOutOfRangeException(nameof(interval),
+ "TimeSpan period must be greater than or equal to zero.");
}
+ if (ms > int.MaxValue)
+ {
+ throw new ArgumentOutOfRangeException(nameof(interval),
+ "TimeSpan period must be less than or equal to Int32.MaxValue.");
+ }
+
+ _dispatcher = dispatcher;
+ _priority = priority;
+ _interval = interval;
+ }
+
+ ///
+ /// Creates a timer that uses to
+ /// process the timer event at the specified priority after the specified timeout and with
+ /// the specified handler.
+ ///
+ /// The interval to tick the timer after.
+ /// The priority to process the timer at.
+ /// The callback to call when the timer ticks.
+ /// This constructor immediately starts the timer.
+ public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, EventHandler callback)
+ : this(interval, priority, Dispatcher.CurrentDispatcher, callback)
+ {
+ }
+
+ ///
+ /// Creates a timer that uses the specified to
+ /// process the timer event at the specified priority after the specified timeout and with
+ /// the specified handler.
+ ///
+ /// The interval to tick the timer after.
+ /// The priority to process the timer at.
+ /// The dispatcher to use to process the timer.
+ /// The callback to call when the timer ticks.
+ /// This constructor immediately starts the timer.
+ public DispatcherTimer(TimeSpan interval, DispatcherPriority priority, Dispatcher dispatcher, EventHandler callback)
+ : this(interval, priority, dispatcher)
+ {
+ ArgumentNullException.ThrowIfNull(callback);
Tick += callback;
Start();
@@ -252,33 +289,6 @@ public partial class DispatcherTimer
///
public object? Tag { get; set; }
-
- internal DispatcherTimer(Dispatcher dispatcher, DispatcherPriority priority, TimeSpan interval)
- {
- if (dispatcher == null)
- {
- throw new ArgumentNullException(nameof(dispatcher));
- }
-
- DispatcherPriority.Validate(priority, "priority");
- if (priority == DispatcherPriority.Inactive)
- {
- throw new ArgumentException("Specified priority is not valid.", nameof(priority));
- }
-
- if (interval.TotalMilliseconds < 0)
- throw new ArgumentOutOfRangeException(nameof(interval), "TimeSpan period must be greater than or equal to zero.");
-
- if (interval.TotalMilliseconds > Int32.MaxValue)
- throw new ArgumentOutOfRangeException(nameof(interval),
- "TimeSpan period must be less than or equal to Int32.MaxValue.");
-
-
- _dispatcher = dispatcher;
- _priority = priority;
- _interval = interval;
- }
-
private void Restart()
{
lock (_instanceLock)
From cdc722264659ef5d43d6a84d780e0a14351a49ed Mon Sep 17 00:00:00 2001
From: Nikita Tsukanov
Date: Tue, 24 Mar 2026 21:43:00 +0500
Subject: [PATCH 06/57] Make Window.WindowState a direct property with (on some
platforms) reliable values (#20973)
* Make Window.WindowState a direct property with (on some platforms) reliable values
* Use reported window state from the callback
* compile
* Actually use the cached value in WindowState getter
* api diff
* Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Tests for our erratic WindowState behavior.
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
api/Avalonia.nupkg.xml | 24 ++++
src/Avalonia.Controls/Platform/IWindowImpl.cs | 8 ++
src/Avalonia.Controls/Window.cs | 55 ++++++--
.../Remote/PreviewerWindowImpl.cs | 1 +
src/Avalonia.DesignerSupport/Remote/Stubs.cs | 1 +
src/Avalonia.Native/WindowImpl.cs | 1 +
src/Avalonia.X11/X11Window.cs | 47 +++----
.../Avalonia.Headless/HeadlessWindowImpl.cs | 1 +
src/Windows/Avalonia.Win32/WindowImpl.cs | 1 +
.../WindowTests.cs | 122 ++++++++++++++++++
10 files changed, 230 insertions(+), 31 deletions(-)
diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index 86fd0cdd75..0e767b5ef3 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -1939,6 +1939,12 @@
baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+ CP0002
+ F:Avalonia.Controls.Window.WindowStateProperty
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
CP0002
M:Avalonia.AppBuilder.get_LifetimeOverride
@@ -3601,6 +3607,12 @@
baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+ CP0002
+ F:Avalonia.Controls.Window.WindowStateProperty
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
CP0002
M:Avalonia.AppBuilder.get_LifetimeOverride
@@ -4561,6 +4573,12 @@
baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+ CP0006
+ P:Avalonia.Platform.IWindowImpl.WindowStateGetterIsUsable
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
CP0006
M:Avalonia.OpenGL.IGlPlatformSurfaceRenderTargetFactory.CanRenderToSurface(Avalonia.OpenGL.IGlContext,Avalonia.Platform.Surfaces.IPlatformRenderSurface)
@@ -4915,6 +4933,12 @@
baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+ CP0006
+ P:Avalonia.Platform.IWindowImpl.WindowStateGetterIsUsable
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
CP0006
M:Avalonia.OpenGL.IGlExternalSemaphore.SignalTimelineSemaphore(Avalonia.OpenGL.IGlExternalImageTexture,System.UInt64)
diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs
index 7a55a0386b..42be775f0d 100644
--- a/src/Avalonia.Controls/Platform/IWindowImpl.cs
+++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs
@@ -16,6 +16,14 @@ namespace Avalonia.Platform
/// Gets or sets the minimized/maximized state of the window.
///
WindowState WindowState { get; set; }
+
+ ///
+ /// Indicates if platform implementation has a working getter for that produces
+ /// consistent results with WindowStateChanged callback.
+ /// If false, Window will not call the getter and will only use the setter and
+ /// callback to track window state.
+ ///
+ bool WindowStateGetterIsUsable { get; }
///
/// Gets or sets a method called when the minimized/maximized state of the window changes.
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index e7a4ce953e..80349634b7 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
@@ -157,10 +158,12 @@ namespace Avalonia.Controls
AvaloniaProperty.Register(nameof(ClosingBehavior));
///
- /// Represents the current window state (normal, minimized, maximized)
+ /// Represents the currently effective window state (normal, minimized, maximized)
///
- public static readonly StyledProperty WindowStateProperty =
- AvaloniaProperty.Register(nameof(WindowState));
+ public static readonly DirectProperty WindowStateProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(WindowState), o => o.WindowState,
+ (o, v) => o.WindowState = v);
///
/// Defines the property.
@@ -254,8 +257,7 @@ namespace Avalonia.Controls
CreatePlatformImplBinding(CanMinimizeProperty, canMinimize => PlatformImpl!.SetCanMinimize(canMinimize));
CreatePlatformImplBinding(CanMaximizeProperty, canMaximize => PlatformImpl!.SetCanMaximize(canMaximize));
CreatePlatformImplBinding(ShowInTaskbarProperty, show => PlatformImpl!.ShowTaskbarIcon(show));
-
- CreatePlatformImplBinding(WindowStateProperty, state => PlatformImpl!.WindowState = state);
+
CreatePlatformImplBinding(ExtendClientAreaToDecorationsHintProperty, hint => PlatformImpl!.SetExtendClientAreaToDecorationsHint(hint));
CreatePlatformImplBinding(ExtendClientAreaTitleBarHeightHintProperty, height => PlatformImpl!.SetExtendClientAreaTitleBarHeightHint(height));
@@ -402,13 +404,45 @@ namespace Avalonia.Controls
set => SetValue(ClosingBehaviorProperty, value);
}
+ private WindowState _lastWindowState;
///
- /// Gets or sets the minimized/maximized state of the window.
+ /// Represents the currently effective window state (normal, minimized, maximized)
///
public WindowState WindowState
{
- get => GetValue(WindowStateProperty);
- set => SetValue(WindowStateProperty, value);
+ get => PlatformImpl?.WindowStateGetterIsUsable == true ?
+ PlatformImpl.WindowState :
+ _lastWindowState;
+ set
+ {
+ if (PlatformImpl != null)
+ {
+ if (PlatformImpl.WindowStateGetterIsUsable)
+ {
+ // Attempt to set the window state to desired value, if it succeeds the platform will
+ // trigger WindowStateChanged callback which will trigger SetAndRaise for the WindowState property
+ PlatformImpl.WindowState = value;
+
+ // If the request was refused - trigger a synthetic property change notification
+ // for data bindings and user state to fix itself.
+ if (PlatformImpl.WindowState != value)
+ {
+ // Since it's a force notify, we aren't checking for the old value and sometimes
+ // trigger notification with oldValue = newValue.
+ var oldValue = _lastWindowState;
+ _lastWindowState = PlatformImpl.WindowState;
+ RaisePropertyChanged(WindowStateProperty, oldValue, _lastWindowState);
+ }
+ }
+ else
+ {
+ // Legacy behavior - update the property and hope for the best that the platform
+ // will update it back to match the actual window state
+ SetAndRaise(WindowStateProperty, ref _lastWindowState, value);
+ PlatformImpl.WindowState = _lastWindowState;
+ }
+ }
+ }
}
///
@@ -616,7 +650,10 @@ namespace Avalonia.Controls
private void HandleWindowStateChanged(WindowState state)
{
- WindowState = state;
+ // Check if platform impl doesn't lie about get_WindowState being usable
+ Debug.Assert(PlatformImpl is not { WindowStateGetterIsUsable: true } || PlatformImpl.WindowState == state);
+
+ SetAndRaise(WindowStateProperty, ref _lastWindowState, state);
if (state == WindowState.Minimized)
{
diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
index e714b62511..35d0a0cb80 100644
--- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
+++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
@@ -44,6 +44,7 @@ namespace Avalonia.DesignerSupport.Remote
public Action Activated { get; set; }
public Func Closing { get; set; }
public WindowState WindowState { get; set; }
+ public bool WindowStateGetterIsUsable => false;
public Action WindowStateChanged { get; set; }
public Size MaxAutoSizeHint { get; } = new Size(4096, 4096);
diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs
index d9c8e333cb..8149e5ad71 100644
--- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs
+++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs
@@ -44,6 +44,7 @@ namespace Avalonia.DesignerSupport.Remote
public PixelPoint Position { get; set; }
public Action? PositionChanged { get; set; }
public WindowState WindowState { get; set; }
+ public bool WindowStateGetterIsUsable => false;
public Action? WindowStateChanged { get; set; }
public Action? TransparencyLevelChanged { get; set; }
diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs
index f23b6cbc62..76f47150db 100644
--- a/src/Avalonia.Native/WindowImpl.cs
+++ b/src/Avalonia.Native/WindowImpl.cs
@@ -114,6 +114,7 @@ namespace Avalonia.Native
set => _native.SetWindowState((AvnWindowState)value);
}
+ public bool WindowStateGetterIsUsable => false;
public Action? WindowStateChanged { get; set; }
public Action? ExtendClientAreaToDecorationsChanged { get; set; }
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index bf20600a18..0cfa226163 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -228,6 +228,7 @@ namespace Avalonia.X11
surfaces.Add(new SurfacePlatformHandle(this));
Surfaces = surfaces.ToArray();
+ UpdateEffectiveSystemDecorations();
UpdateMotifHints();
UpdateSizeHints(null);
@@ -744,6 +745,8 @@ namespace Avalonia.X11
return false;
}
+ public bool WindowStateGetterIsUsable => true;
+
private WindowState _lastWindowState;
public WindowState WindowState
{
@@ -752,7 +755,6 @@ namespace Avalonia.X11
{
if(_lastWindowState == value)
return;
- _lastWindowState = value;
if (value == WindowState.Minimized)
{
XIconifyWindow(_x11.Display, _handle, _x11.DefaultScreen);
@@ -780,7 +782,6 @@ namespace Avalonia.X11
SendNetWMMessage(_x11.Atoms._NET_ACTIVE_WINDOW, (IntPtr)1, _x11.LastActivityTimestamp,
IntPtr.Zero);
}
- WindowStateChanged?.Invoke(value);
}
}
@@ -795,42 +796,44 @@ namespace Avalonia.X11
if (property == _x11.Atoms._NET_WM_STATE)
{
- WindowState state = WindowState.Normal;
var atoms = hasValue
? XGetWindowPropertyAsIntPtrArray(_x11.Display, _handle, _x11.Atoms._NET_WM_STATE,
(IntPtr)Atom.XA_ATOM)
?? []
: [];
int maximized = 0;
+ bool hasMinimized = false, hasFullscreen = false;
foreach (var atom in atoms)
{
- if (atom == _x11.Atoms._NET_WM_STATE_HIDDEN)
- {
- state = WindowState.Minimized;
- break;
- }
-
- if(atom == _x11.Atoms._NET_WM_STATE_FULLSCREEN)
- {
- state = WindowState.FullScreen;
- break;
- }
+ if (atom == _x11.Atoms._NET_WM_STATE_HIDDEN)
+ hasMinimized = true;
if (atom == _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ ||
- atom == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT)
- {
+ atom == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT)
maximized++;
- if (maximized == 2)
- {
- state = WindowState.Maximized;
- break;
- }
- }
+
+ if(atom == _x11.Atoms._NET_WM_STATE_FULLSCREEN)
+ hasFullscreen = true;
}
+
+ var state = hasMinimized ? WindowState.Minimized
+ : hasFullscreen ? WindowState.FullScreen
+ : maximized == 2 ? WindowState.Maximized
+ : WindowState.Normal;
+
if (_lastWindowState != state)
{
_lastWindowState = state;
WindowStateChanged?.Invoke(state);
+
+ XGetGeometry(_x11.Display, _handle, out var _, out var _, out var _, out var width, out var height,
+ out var _, out var _);
+ var newSize = new PixelSize(width, height);
+ if (newSize != _realSize)
+ {
+ _realSize = newSize;
+ Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified);
+ }
}
_activationTracker?.OnNetWmStateChanged(atoms);
diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
index 999a20644f..f2a7bf3575 100644
--- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
+++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
@@ -141,6 +141,7 @@ namespace Avalonia.Headless
}
public WindowState WindowState { get; set; }
+ public bool WindowStateGetterIsUsable => false;
public Action? WindowStateChanged { get; set; }
public void SetTitle(string? title)
{
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 92564d296d..0882516f57 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -208,6 +208,7 @@ namespace Avalonia.Win32
public Action? PositionChanged { get; set; }
+ public bool WindowStateGetterIsUsable => false;
public Action? WindowStateChanged { get; set; }
public Action? LostFocus { get; set; }
diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
index 59a84462ef..5347acbc33 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Media;
using Avalonia.Platform;
@@ -1208,6 +1209,127 @@ namespace Avalonia.Controls.UnitTests
}
}
+ [Fact]
+ public void WindowState_UsableGetter_Setter_Updates_Only_After_Platform_Callback()
+ {
+ var windowImpl = MockWindowingPlatform.CreateWindowMock();
+
+ // Simulate a platform where the getter is usable and the setter is accepted
+ var platformState = WindowState.Normal;
+ windowImpl.Setup(x => x.WindowStateGetterIsUsable).Returns(true);
+ windowImpl.Setup(x => x.WindowState).Returns(() => platformState);
+ windowImpl.SetupSet(x => x.WindowState = It.IsAny())
+ .Callback(v =>
+ {
+ platformState = v;
+ // Platform accepts the state and fires the callback
+ windowImpl.Object.WindowStateChanged?.Invoke(v);
+ });
+
+ var windowingPlatform = new MockWindowingPlatform(() => windowImpl.Object);
+ using (UnitTestApplication.Start(new TestServices(windowingPlatform: windowingPlatform)))
+ {
+ var target = new Window();
+ target.Show();
+
+ var raised = new List();
+ target.GetObservable(Window.WindowStateProperty).Skip(1).Subscribe(s => raised.Add(s));
+
+ // Set to Maximized - platform accepts and fires callback
+ target.WindowState = WindowState.Maximized;
+ Assert.Equal(WindowState.Maximized, target.WindowState);
+ Assert.Contains(WindowState.Maximized, raised);
+
+ // Set to FullScreen - platform accepts and fires callback
+ raised.Clear();
+ target.WindowState = WindowState.FullScreen;
+ Assert.Equal(WindowState.FullScreen, target.WindowState);
+ Assert.Contains(WindowState.FullScreen, raised);
+ }
+ }
+
+ [Fact]
+ public void WindowState_UsableGetter_Setter_Raises_Synthetic_Notification_When_Platform_Refuses()
+ {
+ var windowImpl = MockWindowingPlatform.CreateWindowMock();
+
+ // Simulate a platform where the getter is usable but refuses state change requests.
+ // Start in Maximized state, then refuse a request to go Normal.
+ var platformState = WindowState.Normal;
+ windowImpl.Setup(x => x.WindowStateGetterIsUsable).Returns(true);
+ windowImpl.Setup(x => x.WindowState).Returns(() => platformState);
+ windowImpl.SetupSet(x => x.WindowState = It.IsAny())
+ .Callback(v =>
+ {
+ // Platform accepts Maximized but refuses everything else
+ if (v == WindowState.Maximized)
+ {
+ platformState = v;
+ windowImpl.Object.WindowStateChanged?.Invoke(v);
+ }
+ // else: platform refuses, does not change state
+ });
+
+ var windowingPlatform = new MockWindowingPlatform(() => windowImpl.Object);
+ using (UnitTestApplication.Start(new TestServices(windowingPlatform: windowingPlatform)))
+ {
+ var target = new Window();
+ target.Show();
+
+ // First, go to Maximized (accepted by platform)
+ target.WindowState = WindowState.Maximized;
+ Assert.Equal(WindowState.Maximized, target.WindowState);
+
+ var raised = new List();
+ target.PropertyChanged += (_, e) =>
+ {
+ if (e.Property == Window.WindowStateProperty)
+ raised.Add(e);
+ };
+
+ // Now try to go to FullScreen - platform refuses, stays Maximized
+ target.WindowState = WindowState.FullScreen;
+
+ // The getter should still return Maximized because the platform refused
+ Assert.Equal(WindowState.Maximized, target.WindowState);
+
+ // A synthetic notification should have been raised so data bindings can recover
+ Assert.NotEmpty(raised);
+ Assert.Equal(WindowState.Maximized, raised[^1].GetNewValue());
+ }
+ }
+
+ [Fact]
+ public void WindowState_NonUsableGetter_Setter_Updates_Immediately()
+ {
+ var windowImpl = MockWindowingPlatform.CreateWindowMock();
+
+ // Legacy behavior: WindowStateGetterIsUsable = false
+ windowImpl.Setup(x => x.WindowStateGetterIsUsable).Returns(false);
+
+ var windowingPlatform = new MockWindowingPlatform(() => windowImpl.Object);
+ using (UnitTestApplication.Start(new TestServices(windowingPlatform: windowingPlatform)))
+ {
+ var target = new Window();
+ target.Show();
+
+ var raised = new List();
+ target.GetObservable(Window.WindowStateProperty).Skip(1).Subscribe(s => raised.Add(s));
+
+ // Set to Maximized - should update immediately regardless of platform behavior
+ target.WindowState = WindowState.Maximized;
+
+ Assert.Equal(WindowState.Maximized, target.WindowState);
+ Assert.Contains(WindowState.Maximized, raised);
+
+ // Verify the setter was forwarded to the platform impl
+ windowImpl.VerifySet(x => x.WindowState = WindowState.Maximized);
+
+ // Platform getter should never be called in legacy mode
+ windowImpl.VerifyGet(x => x.WindowState, Times.Never());
+ }
+ }
+
private class TopmostWindow : Window
{
static TopmostWindow()
From 9e4e454ad0a3b98326bfda6c438957217b734715 Mon Sep 17 00:00:00 2001
From: Emmanuel Hansen
Date: Wed, 25 Mar 2026 08:58:45 +0000
Subject: [PATCH 07/57] add StyleKeyOverride for ContentPage (#20977)
---
src/Avalonia.Controls/Page/ContentPage.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/Avalonia.Controls/Page/ContentPage.cs b/src/Avalonia.Controls/Page/ContentPage.cs
index b7523b4688..41fec8d1fc 100644
--- a/src/Avalonia.Controls/Page/ContentPage.cs
+++ b/src/Avalonia.Controls/Page/ContentPage.cs
@@ -151,6 +151,8 @@ namespace Avalonia.Controls
protected override AutomationPeer OnCreateAutomationPeer() => new ContentPageAutomationPeer(this);
+ protected override Type StyleKeyOverride => typeof(ContentPage);
+
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
From 3a316845e843a34b46a4449390716a06530d4ad8 Mon Sep 17 00:00:00 2001
From: Emmanuel Hansen
Date: Wed, 25 Mar 2026 10:16:13 +0000
Subject: [PATCH 08/57] add page header template (#20982)
---
src/Avalonia.Controls/Page/Page.cs | 15 +++++++++++++++
.../Controls/NavigationPage.xaml | 1 +
.../Controls/NavigationPage.xaml | 1 +
3 files changed, 17 insertions(+)
diff --git a/src/Avalonia.Controls/Page/Page.cs b/src/Avalonia.Controls/Page/Page.cs
index a12ce76ddf..4dcb84312d 100644
--- a/src/Avalonia.Controls/Page/Page.cs
+++ b/src/Avalonia.Controls/Page/Page.cs
@@ -26,6 +26,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty
public static readonly StyledProperty TextProperty =
- TextBlock.TextProperty.AddOwner(new(string.Empty, BindingMode.TwoWay));
+ TextBlock.TextProperty.AddOwner(new(string.Empty, BindingMode.TwoWay,
+ enableDataValidation: true));
///
/// Defines the property.
diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
index ca1fa66d91..578484557f 100644
--- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
@@ -392,6 +392,28 @@ namespace Avalonia.Controls.UnitTests
}
+ [Fact]
+ public void Text_Validation()
+ {
+ using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+ {
+ var target = new ComboBox
+ {
+ Template = GetTemplate(),
+ };
+
+ target.ApplyTemplate();
+ target.Presenter!.ApplyTemplate();
+
+ var exception = new System.InvalidCastException("failed validation");
+ var textObservable = new BehaviorSubject(new BindingNotification(exception, BindingErrorType.DataValidationError));
+ target.Bind(ComboBox.TextProperty, textObservable);
+
+ Assert.True(DataValidationErrors.GetHasErrors(target));
+ Assert.Equal([exception], DataValidationErrors.GetErrors(target));
+ }
+ }
+
[Fact]
public void Close_Window_On_Alt_F4_When_ComboBox_Is_Focus()
{
From a01245a78411816663c4b8ab53011ed04a69a433 Mon Sep 17 00:00:00 2001
From: Nikita Tsukanov
Date: Thu, 26 Mar 2026 00:25:21 +0500
Subject: [PATCH 15/57] QoL for ControlCatalog: add sidebar search, auto-sort,
and default page selection (#20987)
* feat(ControlCatalog): add sidebar search, auto-sort, and default page selection
- Add search TextBox (PART_SearchBox) to HamburgerMenu pane template
that filters sidebar items by header text (case-insensitive)
- Auto-sort TabItems alphabetically by Header on first load
- Add IsDefaultPage attached property on HamburgerMenu to preselect
a specific TabItem (set on Buttons page in MainView)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: replace IsDefaultPage attached property with simple IsSelected
IsSelected on TabItem is sufficient for preselecting a page after
auto-sort. Removes the custom IsDefaultPage attached property.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
samples/ControlCatalog/MainView.xaml | 2 +-
.../HamburgerMenu/HamburgerMenu.cs | 68 ++++++++++++++++++-
.../HamburgerMenu/HamburgerMenu.xaml | 17 ++++-
3 files changed, 82 insertions(+), 5 deletions(-)
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index adea1b90fc..3a6d57801a 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -16,7 +16,7 @@
-
+
diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs b/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
index 57f8a138af..0e45b28a78 100644
--- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
+++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
@@ -1,4 +1,6 @@
-using Avalonia;
+using System;
+using System.Linq;
+using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
@@ -8,6 +10,8 @@ namespace ControlSamples
public class HamburgerMenu : TabControl
{
private SplitView? _splitView;
+ private TextBox? _searchBox;
+ private bool _initialized;
public static readonly StyledProperty PaneBackgroundProperty =
SplitView.PaneBackgroundProperty.AddOwner();
@@ -41,6 +45,68 @@ namespace ControlSamples
base.OnApplyTemplate(e);
_splitView = e.NameScope.Find("PART_NavigationPane");
+ _searchBox = e.NameScope.Find("PART_SearchBox");
+
+ if (_searchBox is not null)
+ {
+ _searchBox.TextChanged += OnSearchTextChanged;
+ }
+ }
+
+ protected override void OnLoaded(Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ base.OnLoaded(e);
+
+ if (!_initialized)
+ {
+ _initialized = true;
+ SortItems();
+ }
+ }
+
+ private void SortItems()
+ {
+ var items = Items.OfType().ToList();
+ var sorted = items.OrderBy(t => t.Header?.ToString() ?? "", StringComparer.OrdinalIgnoreCase).ToList();
+
+ // Only reorder if needed
+ bool needsSort = false;
+ for (int i = 0; i < items.Count; i++)
+ {
+ if (!ReferenceEquals(items[i], sorted[i]))
+ {
+ needsSort = true;
+ break;
+ }
+ }
+
+ if (needsSort)
+ {
+ Items.Clear();
+ foreach (var item in sorted)
+ {
+ Items.Add(item);
+ }
+ }
+ }
+
+ private void OnSearchTextChanged(object? sender, TextChangedEventArgs e)
+ {
+ var searchText = _searchBox?.Text;
+ var hasFilter = !string.IsNullOrWhiteSpace(searchText);
+
+ foreach (var item in Items.OfType())
+ {
+ if (hasFilter)
+ {
+ var header = item.Header?.ToString() ?? "";
+ item.IsVisible = header.Contains(searchText!, StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ item.IsVisible = true;
+ }
+ }
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
index 49972bdc64..d9b3e3c73f 100644
--- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
+++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
@@ -175,10 +175,21 @@
OpenPaneLength="{StaticResource PaneExpandWidth}"
PaneBackground="Transparent">
-
+
+
+
+
+
+
@@ -195,7 +206,7 @@
Date: Thu, 26 Mar 2026 18:19:17 +0500
Subject: [PATCH 16/57] Introduced "forced" CSD mode without app opting in
(#20976)
* Introduced "forced" CSD mode without app opting in
* C is for Consistency
* api diff
* [X11] Better handling of forced-vs-app-triggeed CSD
* Round WindowDrawnDecorations sizes to be pixel-aligned
---
api/Avalonia.nupkg.xml | 12 +
.../Chrome/WindowDrawnDecorations.cs | 27 +-
.../PresentationSource.RenderRoot.cs | 5 +-
.../PresentationSource/PresentationSource.cs | 4 +-
src/Avalonia.Controls/TopLevel.cs | 2 +-
src/Avalonia.Controls/TopLevelHost.cs | 87 +++++++
src/Avalonia.Controls/Window.cs | 99 +++++++-
src/Avalonia.Controls/WindowBase.cs | 2 +-
src/Avalonia.X11/X11Platform.cs | 31 ++-
src/Avalonia.X11/X11Window.cs | 19 +-
src/Windows/Avalonia.Win32/WindowImpl.cs | 1 +
.../WindowTests.cs | 231 ++++++++++++++++++
12 files changed, 488 insertions(+), 32 deletions(-)
diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index 0e767b5ef3..71bdf3714d 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -2395,6 +2395,12 @@
baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+ CP0002
+ M:Avalonia.Controls.Window.ExtendClientAreaToDecorationsChanged(System.Boolean)
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
CP0002
M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints
@@ -4063,6 +4069,12 @@
baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+ CP0002
+ M:Avalonia.Controls.Window.ExtendClientAreaToDecorationsChanged(System.Boolean)
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
CP0002
M:Avalonia.Controls.Window.get_ExtendClientAreaChromeHints
diff --git a/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs b/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
index 48847b5f59..7a282bf306 100644
--- a/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
+++ b/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
@@ -2,6 +2,7 @@ using System;
using Avalonia.Automation;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
using Avalonia.Styling;
@@ -138,6 +139,7 @@ public class WindowDrawnDecorations : StyledElement
private IDisposable? _windowSubscriptions;
private Window? _hostWindow;
private double _titleBarHeightOverride = -1;
+ private double _renderScaling = 1.0;
///
/// Raised when any property affecting the effective geometry changes
@@ -145,6 +147,22 @@ public class WindowDrawnDecorations : StyledElement
///
internal event Action? EffectiveGeometryChanged;
+ ///
+ /// Gets or sets the current render scaling factor used for pixel-aligning
+ /// decoration geometry (title bar height, frame/shadow thickness).
+ ///
+ internal double RenderScaling
+ {
+ get => _renderScaling;
+ set
+ {
+ if (_renderScaling == value)
+ return;
+ _renderScaling = value;
+ UpdateEffectiveGeometry();
+ }
+ }
+
///
/// Gets or sets the decorations template.
///
@@ -559,16 +577,19 @@ public class WindowDrawnDecorations : StyledElement
private void UpdateEffectiveGeometry()
{
+ var scale = _renderScaling;
+
TitleBarHeight = EnabledParts.HasFlag(DrawnWindowDecorationParts.TitleBar)
- ? (TitleBarHeightOverride == -1 ? DefaultTitleBarHeight : TitleBarHeightOverride)
+ ? LayoutHelper.RoundLayoutValue(
+ TitleBarHeightOverride == -1 ? DefaultTitleBarHeight : TitleBarHeightOverride, scale)
: 0;
FrameThickness = EnabledParts.HasFlag(DrawnWindowDecorationParts.Border)
- ? (FrameThicknessOverride ?? DefaultFrameThickness)
+ ? LayoutHelper.RoundLayoutThickness(FrameThicknessOverride ?? DefaultFrameThickness, scale)
: default;
ShadowThickness = EnabledParts.HasFlag(DrawnWindowDecorationParts.Shadow)
- ? (ShadowThicknessOverride ?? DefaultShadowThickness)
+ ? LayoutHelper.RoundLayoutThickness(ShadowThicknessOverride ?? DefaultShadowThickness, scale)
: default;
EffectiveGeometryChanged?.Invoke();
diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs
index 7c129742b8..2a9e833a48 100644
--- a/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs
+++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.RenderRoot.cs
@@ -1,3 +1,4 @@
+using Avalonia.Input;
using System;
using Avalonia.Layout;
using Avalonia.Rendering;
@@ -7,7 +8,6 @@ namespace Avalonia.Controls;
internal partial class PresentationSource
{
- private readonly Func _clientSizeProvider;
public CompositingRenderer Renderer { get; }
IRenderer IPresentationSource.Renderer => Renderer;
Visual IPresentationSource.RootVisual => RootVisual;
@@ -16,8 +16,7 @@ internal partial class PresentationSource
public IHitTester? HitTesterOverride { get; set; }
public double RenderScaling { get; private set; } = 1.0;
-
- public Size ClientSize => _clientSizeProvider();
+ public Size ClientSize => PlatformImpl?.ClientSize ?? default;
public void SceneInvalidated(object? sender, SceneInvalidatedEventArgs sceneInvalidatedEventArgs)
{
diff --git a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs
index aad1bc1003..8f298d2e30 100644
--- a/src/Avalonia.Controls/PresentationSource/PresentationSource.cs
+++ b/src/Avalonia.Controls/PresentationSource/PresentationSource.cs
@@ -23,10 +23,8 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi
public PresentationSource(InputElement rootVisual, InputElement defaultFocusVisual,
ITopLevelImpl platformImpl,
- IAvaloniaDependencyResolver dependencyResolver, Func clientSizeProvider)
+ IAvaloniaDependencyResolver dependencyResolver)
{
- _clientSizeProvider = clientSizeProvider;
-
PlatformImpl = platformImpl;
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 69d32d2b56..44b82c886f 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -212,7 +212,7 @@ namespace Avalonia.Controls
LogicalChildren.Add(hostVisual);
_source = new PresentationSource(hostVisual, this,
- impl, dependencyResolver, () => ClientSize);
+ impl, dependencyResolver);
_source.Renderer.SceneInvalidated += SceneInvalidated;
_scaling = LayoutHelper.ValidateScaling(impl.RenderScaling);
diff --git a/src/Avalonia.Controls/TopLevelHost.cs b/src/Avalonia.Controls/TopLevelHost.cs
index d2d3ddf8fa..9592dd8221 100644
--- a/src/Avalonia.Controls/TopLevelHost.cs
+++ b/src/Avalonia.Controls/TopLevelHost.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Chrome;
using Avalonia.Input;
+using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
@@ -14,6 +15,8 @@ namespace Avalonia.Controls;
///
internal partial class TopLevelHost : Control
{
+ private Thickness _decorationInset;
+
static TopLevelHost()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle);
@@ -25,5 +28,89 @@ internal partial class TopLevelHost : Control
VisualChildren.Add(tl);
}
+ ///
+ /// Gets or sets the decoration inset applied to the TopLevel child in forced decoration mode.
+ /// When non-zero, the TopLevel is measured and arranged within the inset area while
+ /// decoration layers use the full available size.
+ ///
+ internal Thickness DecorationInset
+ {
+ get => _decorationInset;
+ set
+ {
+ if (_decorationInset == value)
+ return;
+ _decorationInset = value;
+ InvalidateMeasure();
+ }
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ var inset = _decorationInset;
+ var hasInset = inset != default;
+ var desiredSize = default(Size);
+
+ foreach (var child in VisualChildren)
+ {
+ if (child is Layoutable l)
+ {
+ if (hasInset && ReferenceEquals(child, _topLevel))
+ {
+ // In forced mode, measure the TopLevel with reduced size
+ var contentSize = new Size(
+ Math.Max(0, availableSize.Width - inset.Left - inset.Right),
+ Math.Max(0, availableSize.Height - inset.Top - inset.Bottom));
+ l.Measure(contentSize);
+
+ // Add inset back so TopLevelHost's desired size represents the full frame.
+ // This ensures ArrangeOverride receives the full frame size and can correctly
+ // position the TopLevel within the inset area.
+ desiredSize = new Size(
+ Math.Max(desiredSize.Width, l.DesiredSize.Width + inset.Left + inset.Right),
+ Math.Max(desiredSize.Height, l.DesiredSize.Height + inset.Top + inset.Bottom));
+ }
+ else
+ {
+ l.Measure(availableSize);
+
+ desiredSize = new Size(
+ Math.Max(desiredSize.Width, l.DesiredSize.Width),
+ Math.Max(desiredSize.Height, l.DesiredSize.Height));
+ }
+ }
+ }
+
+ return desiredSize;
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ var inset = _decorationInset;
+ var hasInset = inset != default;
+
+ foreach (var child in VisualChildren)
+ {
+ if (child is Layoutable l)
+ {
+ if (hasInset && ReferenceEquals(child, _topLevel))
+ {
+ // In forced mode, arrange the TopLevel within the inset area
+ var contentSize = new Size(
+ Math.Max(0, finalSize.Width - inset.Left - inset.Right),
+ Math.Max(0, finalSize.Height - inset.Top - inset.Bottom));
+
+ l.Arrange(new Rect(inset.Left, inset.Top, contentSize.Width, contentSize.Height));
+ }
+ else
+ {
+ l.Arrange(new Rect(finalSize));
+ }
+ }
+ }
+
+ return finalSize;
+ }
+
protected override bool BypassFlowDirectionPolicies => true;
}
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index 80349634b7..4dab3574eb 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -98,6 +98,7 @@ namespace Avalonia.Controls
private Thickness _offScreenMargin;
private bool _canHandleResized = false;
private Size _arrangeBounds;
+ private bool _isForcedDecorationMode;
///
/// Defines the property.
@@ -249,7 +250,11 @@ namespace Avalonia.Controls
impl.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;
- this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, WindowResizeReason.Application));
+ this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x =>
+ {
+ ResizePlatformImpl(x, WindowResizeReason.Application);
+ });
+ ScalingChanged += OnScalingChangedUpdateDecorations;
CreatePlatformImplBinding(TitleProperty, title => PlatformImpl!.SetTitle(title));
CreatePlatformImplBinding(IconProperty, SetEffectiveIcon);
@@ -668,7 +673,7 @@ namespace Avalonia.Controls
UpdateDrawnDecorationParts();
}
- protected virtual void ExtendClientAreaToDecorationsChanged(bool isExtended)
+ private void ExtendClientAreaToDecorationsChanged(bool isExtended)
{
IsExtendedIntoWindowDecorations = isExtended;
OffScreenMargin = PlatformImpl?.OffScreenMargin ?? default;
@@ -679,6 +684,10 @@ namespace Avalonia.Controls
private void UpdateDrawnDecorations()
{
var parts = ComputeDecorationParts();
+
+ // Detect forced mode: platform needs managed decorations but app hasn't opted in
+ _isForcedDecorationMode = parts != null && !IsExtendedIntoWindowDecorations;
+
TopLevelHost.UpdateDrawnDecorations(parts, WindowState);
if (parts != null)
@@ -687,6 +696,8 @@ namespace Avalonia.Controls
var decorations = TopLevelHost.Decorations;
if (decorations != null)
{
+ decorations.RenderScaling = RenderScaling;
+
var hint = ExtendClientAreaTitleBarHeightHint;
if (hint >= 0)
decorations.TitleBarHeightOverride = hint;
@@ -696,6 +707,13 @@ namespace Avalonia.Controls
UpdateDrawnDecorationMargins();
}
+ private void OnScalingChangedUpdateDecorations(object? sender, EventArgs e)
+ {
+ var decorations = TopLevelHost.Decorations;
+ if (decorations != null)
+ decorations.RenderScaling = RenderScaling;
+ }
+
///
/// Updates decoration parts based on current window state without
/// re-creating the decorations instance.
@@ -753,7 +771,9 @@ namespace Avalonia.Controls
var decorations = TopLevelHost.Decorations;
if (decorations == null)
{
+ // Only use platform margins if drawn decorations are not active
WindowDecorationMargin = PlatformImpl?.ExtendedMargins ?? default;
+ TopLevelHost.DecorationInset = default;
return;
}
@@ -764,11 +784,25 @@ namespace Avalonia.Controls
? decorations.FrameThickness : default;
var shadow = parts.HasFlag(Chrome.DrawnWindowDecorationParts.Shadow)
? decorations.ShadowThickness : default;
- WindowDecorationMargin = new Thickness(
+ var margin = new Thickness(
frame.Left + shadow.Left,
titleBarHeight + frame.Top + shadow.Top,
frame.Right + shadow.Right,
frame.Bottom + shadow.Bottom);
+
+ if (_isForcedDecorationMode)
+ {
+ // In forced mode, app is unaware of decorations.
+ // TopLevelHost insets the Window child; WindowDecorationMargin stays zero.
+ WindowDecorationMargin = default;
+ TopLevelHost.DecorationInset = margin;
+ }
+ else
+ {
+ // In extended mode, app handles the margin itself.
+ WindowDecorationMargin = margin;
+ TopLevelHost.DecorationInset = default;
+ }
}
private void OnTitleBarHeightHintChanged()
@@ -933,6 +967,15 @@ namespace Avalonia.Controls
// Enable drawn decorations before layout so margins are computed
UpdateDrawnDecorations();
+ // In forced mode, adjust ClientSize to reflect usable content area
+ if (_isForcedDecorationMode)
+ {
+ var inset = TopLevelHost.DecorationInset;
+ ClientSize = new Size(
+ Math.Max(0, ClientSize.Width - inset.Left - inset.Right),
+ Math.Max(0, ClientSize.Height - inset.Top - inset.Bottom));
+ }
+
_shown = true;
IsVisible = true;
@@ -978,10 +1021,18 @@ namespace Avalonia.Controls
DesktopScalingOverride = null;
- if (clientSizeChanged || ClientSize != PlatformImpl?.ClientSize)
+ // In forced mode, compare against adjusted platform size
+ var platformClientSize = PlatformImpl?.ClientSize ?? default;
+ var comparableClientSize = _isForcedDecorationMode
+ ? new Size(
+ Math.Max(0, platformClientSize.Width - TopLevelHost.DecorationInset.Left - TopLevelHost.DecorationInset.Right),
+ Math.Max(0, platformClientSize.Height - TopLevelHost.DecorationInset.Top - TopLevelHost.DecorationInset.Bottom))
+ : platformClientSize;
+
+ if (clientSizeChanged || ClientSize != comparableClientSize)
{
// Previously it was called before ExecuteInitialLayoutPass
- PlatformImpl?.Resize(ClientSize, WindowResizeReason.Layout);
+ ResizePlatformImpl(ClientSize, WindowResizeReason.Layout);
// we do not want PlatformImpl?.Resize to trigger HandleResized yet because it will set Width and Height.
// So perform some important actions from HandleResized
@@ -1037,6 +1088,22 @@ namespace Avalonia.Controls
}
}
+ private void ResizePlatformImpl(Size size, WindowResizeReason reason)
+ {
+ // In forced mode, add decoration inset so platform gets full frame size
+ if (_isForcedDecorationMode)
+ {
+ var inset = TopLevelHost.DecorationInset;
+ size = new Size(
+ size.Width + inset.Left + inset.Right,
+ size.Height + inset.Top + inset.Bottom);
+ if (PlatformImpl?.ClientSize != size)
+ PlatformImpl?.Resize(size, reason);
+ }
+ else
+ PlatformImpl?.Resize(size, reason);
+ }
+
///
/// Shows the window as a dialog.
///
@@ -1272,6 +1339,14 @@ namespace Avalonia.Controls
{
var sizeToContent = SizeToContent;
var clientSize = ClientSize;
+ if (_isForcedDecorationMode)
+ {
+ clientSize = PlatformImpl?.ClientSize ?? clientSize;
+ var inset = TopLevelHost.DecorationInset;
+ clientSize = new Size(
+ Math.Max(0, clientSize.Width - inset.Left - inset.Right),
+ Math.Max(0, clientSize.Height - inset.Top - inset.Bottom));
+ }
var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity;
var useAutoWidth = sizeToContent.HasAllFlags(SizeToContent.Width);
var useAutoHeight = sizeToContent.HasAllFlags(SizeToContent.Height);
@@ -1332,7 +1407,9 @@ namespace Avalonia.Controls
{
_arrangeBounds = size;
if (_canHandleResized)
- PlatformImpl?.Resize(size, WindowResizeReason.Layout);
+ {
+ ResizePlatformImpl(size, WindowResizeReason.Layout);
+ }
return ClientSize;
}
@@ -1350,6 +1427,16 @@ namespace Avalonia.Controls
///
internal override void HandleResized(Size clientSize, WindowResizeReason reason)
{
+ // In forced decoration mode, the platform's clientSize includes decoration area.
+ // Subtract the decoration inset so Window.ClientSize reflects the usable content area.
+ if (_isForcedDecorationMode)
+ {
+ var inset = TopLevelHost.DecorationInset;
+ clientSize = new Size(
+ Math.Max(0, clientSize.Width - inset.Left - inset.Right),
+ Math.Max(0, clientSize.Height - inset.Top - inset.Bottom));
+ }
+
if (_canHandleResized && (ClientSize != clientSize || double.IsNaN(Width) || double.IsNaN(Height)))
{
var sizeToContent = SizeToContent;
diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs
index 07966600e6..894b7ca6e6 100644
--- a/src/Avalonia.Controls/WindowBase.cs
+++ b/src/Avalonia.Controls/WindowBase.cs
@@ -304,7 +304,7 @@ namespace Avalonia.Controls
{
var constraint = ArrangeSetBounds(finalRect.Size);
var arrangeSize = ArrangeOverride(constraint);
- Bounds = new Rect(arrangeSize);
+ Bounds = new Rect(finalRect.Position, arrangeSize);
}
///
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index 566b0d907a..a3f98265f4 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -482,13 +482,32 @@ namespace Avalonia
, Message = "Experimental, used mostly for testing"
#endif
)]
- public bool? EnableDrawnDecorations
- {
- get => EnableDrawnDecorationsInternal;
- set => EnableDrawnDecorationsInternal = value;
- }
+ public bool? EnableDrawnDecorations { get; set; }
+
+ internal bool EnableDrawnDecorationsInternal =>
+#pragma warning disable AVALONIA_X11_CSD
+ EnableDrawnDecorations == true || ForceDrawnDecorationsInternal;
+#pragma warning restore AVALONIA_X11_CSD
+
+
+ ///
+ /// Forces client-side drawn window decorations on X11 for all windows,
+ /// even when the app has not opted in via ExtendClientAreaToDecorationsHint.
+ /// In this mode, Window.ClientSize reflects the usable content area
+ /// (platform client size minus decoration margins) and the app is unaware
+ /// of the decorations.
+ /// Implies EnableDrawnDecorations = true.
+ ///
+ [Experimental("AVALONIA_X11_FORCE_CSD"
+ #if NET10_0_OR_GREATER
+ , Message = "Experimental, used mostly for testing"
+ #endif
+ )]
+ public bool ForceDrawnDecorations { get; set; }
- internal bool? EnableDrawnDecorationsInternal { get; set; }
+#pragma warning disable AVALONIA_X11_FORCE_CSD
+ internal bool ForceDrawnDecorationsInternal => ForceDrawnDecorations;
+#pragma warning restore AVALONIA_X11_FORCE_CSD
///
/// If Avalonia is in control of a run loop, we propagate exceptions by stopping the run loop frame
diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs
index 0cfa226163..23c5cc18db 100644
--- a/src/Avalonia.X11/X11Window.cs
+++ b/src/Avalonia.X11/X11Window.cs
@@ -926,7 +926,7 @@ namespace Avalonia.X11
mouse.Position = mouse.Position / RenderScaling;
// Chrome hit-test for drawn decorations
- if (_extendClientAreaToDecorations
+ if (UseManagedDecorations
&& mouse.Type == RawPointerEventType.LeftButtonDown
&& _inputRoot is { } inputRoot)
{
@@ -1187,8 +1187,8 @@ namespace Avalonia.X11
private void UpdateEffectiveSystemDecorations()
{
- // When extending client area, always hide WM decorations (we draw our own)
- var effective = _extendClientAreaToDecorations
+ // When extending client area or forcing drawn decorations, always hide WM decorations (we draw our own)
+ var effective = UseManagedDecorations
? WindowDecorations.None
: (_requestedWindowDecorations == WindowDecorations.Full
? WindowDecorations.Full
@@ -1509,17 +1509,18 @@ namespace Avalonia.X11
}
}
- private bool _extendClientAreaToDecorations;
+ private bool _extendingClientAreaToDecorations;
+ private bool UseManagedDecorations => _extendingClientAreaToDecorations || _platform.Options.ForceDrawnDecorationsInternal;
public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint)
{
- if (_platform.Options.EnableDrawnDecorationsInternal != true)
+ if (!_platform.Options.EnableDrawnDecorationsInternal)
return;
- if (_extendClientAreaToDecorations == extendIntoClientAreaHint)
+ if (_extendingClientAreaToDecorations == extendIntoClientAreaHint)
return;
- _extendClientAreaToDecorations = extendIntoClientAreaHint;
+ _extendingClientAreaToDecorations = extendIntoClientAreaHint;
UpdateEffectiveSystemDecorations();
IsClientAreaExtendedToDecorations = extendIntoClientAreaHint;
@@ -1606,10 +1607,10 @@ namespace Avalonia.X11
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8);
- public bool NeedsManagedDecorations => _extendClientAreaToDecorations;
+ public bool NeedsManagedDecorations => UseManagedDecorations;
public PlatformRequestedDrawnDecoration RequestedDrawnDecorations =>
- _extendClientAreaToDecorations
+ UseManagedDecorations
? PlatformRequestedDrawnDecoration.Border
| PlatformRequestedDrawnDecoration.ResizeGrips
| PlatformRequestedDrawnDecoration.TitleBar
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 0882516f57..9233cc2809 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -1188,6 +1188,7 @@ namespace Avalonia.Win32
{
if (!_shown)
{
+ ExtendClientAreaToDecorationsChanged?.Invoke(_isClientAreaExtended);
return;
}
diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
index 5347acbc33..63cc2db193 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Threading.Tasks;
+using Avalonia.Controls.Platform;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
@@ -1164,6 +1165,236 @@ namespace Avalonia.Controls.UnitTests
}
}
+ public class ForcedDecorationSizingTests : ScopedTestBase
+ {
+ ///
+ /// Creates a mock IWindowImpl that simulates forced CSD mode:
+ /// NeedsManagedDecorations = true, RequestedDrawnDecorations includes TitleBar + Border,
+ /// but IsClientAreaExtendedToDecorations = false.
+ ///
+ private static Mock CreateForcedCsdWindowMock(
+ double initialWidth = 800, double initialHeight = 600)
+ {
+ var windowImpl = MockWindowingPlatform.CreateWindowMock(initialWidth, initialHeight);
+
+ windowImpl.Setup(x => x.NeedsManagedDecorations).Returns(true);
+ windowImpl.Setup(x => x.RequestedDrawnDecorations).Returns(
+ PlatformRequestedDrawnDecoration.TitleBar | PlatformRequestedDrawnDecoration.Border);
+
+ return windowImpl;
+ }
+
+ [Fact]
+ public void ClientSize_Should_Exclude_Decoration_Inset()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.Manual,
+ };
+
+ // Verify mock setup
+ Assert.True(windowImpl.Object.NeedsManagedDecorations);
+
+ target.Show();
+
+ var host = target.TopLevelHost;
+ var decorations = host.Decorations;
+
+ // Debug: verify decorations were created
+ Assert.NotNull(decorations);
+ Assert.True(decorations!.TitleBarHeight > 0,
+ $"TitleBarHeight was {decorations.TitleBarHeight}");
+
+ var inset = host.DecorationInset;
+ Assert.NotEqual(default, inset);
+
+ var expectedClientSize = new Size(
+ 800 - inset.Left - inset.Right,
+ 600 - inset.Top - inset.Bottom);
+ Assert.Equal(expectedClientSize, target.ClientSize);
+ }
+ }
+
+ [Fact]
+ public void WindowDecorationMargin_Should_Be_Zero_In_Forced_Mode()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.Manual,
+ };
+
+ target.Show();
+
+ Assert.Equal(default(Thickness), target.WindowDecorationMargin);
+ }
+ }
+
+ [Fact]
+ public void HandleResized_Should_Subtract_Inset_From_Platform_Size()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.Manual,
+ };
+
+ target.Show();
+
+ var inset = target.TopLevelHost.DecorationInset;
+
+ // Simulate a platform resize (e.g. user resize)
+ target.PlatformImpl!.Resized!.Invoke(new Size(1000, 700), WindowResizeReason.User);
+
+ var expectedClientSize = new Size(
+ 1000 - inset.Left - inset.Right,
+ 700 - inset.Top - inset.Bottom);
+ Assert.Equal(expectedClientSize, target.ClientSize);
+ }
+ }
+
+ [Fact]
+ public void Setting_Width_Should_Resize_WindowImpl_With_Inset_Added()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var target = new Window(windowImpl.Object)
+ {
+ Width = 400,
+ Height = 300,
+ SizeToContent = SizeToContent.Manual,
+ };
+
+ target.Show();
+
+ var inset = target.TopLevelHost.DecorationInset;
+
+ target.Width = 500;
+ target.LayoutManager.ExecuteLayoutPass();
+
+ // Platform should receive full frame size (content + inset)
+ var expectedPlatformSize = new Size(
+ 500 + inset.Left + inset.Right,
+ 300 + inset.Top + inset.Bottom);
+ windowImpl.Verify(x => x.Resize(expectedPlatformSize, WindowResizeReason.Layout));
+ }
+ }
+
+ [Fact]
+ public void Child_Should_Be_Measured_With_Content_Size()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var child = new ChildControl();
+ var target = new Window(windowImpl.Object)
+ {
+ Width = 400,
+ Height = 300,
+ SizeToContent = SizeToContent.Manual,
+ Content = child,
+ };
+
+ target.Show();
+
+ Assert.Equal(1, child.MeasureSizes.Count);
+ Assert.Equal(new Size(400, 300), child.MeasureSizes[0]);
+ }
+ }
+
+ [Fact]
+ public void Width_Height_Should_Not_Be_NaN_After_Show()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.Manual,
+ };
+
+ target.Show();
+
+ Assert.False(double.IsNaN(target.Width));
+ Assert.False(double.IsNaN(target.Height));
+
+ var inset = target.TopLevelHost.DecorationInset;
+ Assert.Equal(800 - inset.Left - inset.Right, target.Width);
+ Assert.Equal(600 - inset.Top - inset.Bottom, target.Height);
+ }
+ }
+
+ [Fact]
+ public void SizeToContent_Should_Work_In_Forced_Mode()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var child = new Canvas
+ {
+ Width = 400,
+ Height = 300,
+ };
+
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.WidthAndHeight,
+ Content = child,
+ };
+
+ target.Show();
+
+ Assert.Equal(400, target.Width);
+ Assert.Equal(300, target.Height);
+ Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+ }
+ }
+
+ [Fact]
+ public void User_Resize_Should_Reset_SizeToContent()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var windowImpl = CreateForcedCsdWindowMock();
+ var child = new Canvas
+ {
+ Width = 400,
+ Height = 300,
+ };
+
+ var target = new Window(windowImpl.Object)
+ {
+ SizeToContent = SizeToContent.WidthAndHeight,
+ Content = child,
+ };
+
+ target.Show();
+ Assert.Equal(400, target.Width);
+ Assert.Equal(300, target.Height);
+
+ var inset = target.TopLevelHost.DecorationInset;
+ // Platform fires resize with full frame size
+ var newPlatformWidth = 500 + inset.Left + inset.Right;
+ var newPlatformHeight = 300 + inset.Top + inset.Bottom;
+ windowImpl.Object.Resized?.Invoke(
+ new Size(newPlatformWidth, newPlatformHeight),
+ WindowResizeReason.User);
+
+ Assert.Equal(500, target.Width);
+ Assert.Equal(300, target.Height);
+ Assert.Equal(SizeToContent.Height, target.SizeToContent);
+ }
+ }
+ }
+
private static Mock CreateImpl()
{
var screen1 = new MockScreen(1.75, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 966)), true);
From 2dfd8515a74fac33842b9025f1d666988b6d4413 Mon Sep 17 00:00:00 2001
From: Julien Lebosquain
Date: Thu, 26 Mar 2026 16:25:25 +0100
Subject: [PATCH 17/57] Use the correct value for animations stopped during a
visual tree detach (#20995)
* Add failing test for FillMode on visual tree detach
* Update animation fill value when detached from visual tree
---
.../Animation/AnimationInstance`1.cs | 15 ++++++--
.../Animation/AnimationIterationTests.cs | 38 +++++++++++++++++++
2 files changed, 50 insertions(+), 3 deletions(-)
diff --git a/src/Avalonia.Base/Animation/AnimationInstance`1.cs b/src/Avalonia.Base/Animation/AnimationInstance`1.cs
index 390a4a10b4..757e5f8987 100644
--- a/src/Avalonia.Base/Animation/AnimationInstance`1.cs
+++ b/src/Avalonia.Base/Animation/AnimationInstance`1.cs
@@ -148,7 +148,11 @@ namespace Avalonia.Animation
}
// Stop and dispose the animation when detached from the visual tree.
- _detachedHandler = (_, _) => DoComplete();
+ _detachedHandler = (_, _) =>
+ {
+ SetFinalValue();
+ DoComplete();
+ };
visual.DetachedFromVisualTree += _detachedHandler;
}
@@ -172,6 +176,12 @@ namespace Avalonia.Animation
}
}
+ private void SetFinalValue()
+ {
+ var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0);
+ _lastInterpValue = _interpolator(easedTime, _neutralValue);
+ }
+
private void ApplyFinalFill()
{
if (_animator.Property is null)
@@ -237,8 +247,7 @@ namespace Avalonia.Animation
// when the duration is set to zero while animating and snap to the last iterated value.
if (_currentIteration + 1 > _iterationCount || _duration == TimeSpan.Zero)
{
- var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0);
- _lastInterpValue = _interpolator(easedTime, _neutralValue);
+ SetFinalValue();
DoComplete();
return;
}
diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
index 0ca5a3be6a..b32cb5a7d1 100644
--- a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
@@ -1264,6 +1264,44 @@ namespace Avalonia.Base.UnitTests.Animation
Assert.Equal(200d, border.Width);
}
+ [Fact]
+ public void FillMode_Applies_Final_Value_When_Visual_Detached_During_Animation()
+ {
+ var keyframe1 = new KeyFrame
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 100d) },
+ Cue = new Cue(0d)
+ };
+ var keyframe2 = new KeyFrame
+ {
+ Setters = { new Setter(Layoutable.WidthProperty, 300d) },
+ Cue = new Cue(1d)
+ };
+
+ var animation = new Animation
+ {
+ Duration = TimeSpan.FromSeconds(5),
+ IterationCount = new IterationCount(1),
+ FillMode = FillMode.Forward,
+ Children = { keyframe1, keyframe2 }
+ };
+
+ 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.Equal(100d, border.Width);
+
+ // Detach from visual tree immediately
+ root.Child = null;
+
+ // The final value should be applied
+ Assert.True(animationRun.IsCompleted);
+ Assert.Equal(300d, border.Width);
+ }
+
private sealed class FakeAnimator : InterpolatingAnimator
{
public double LastProgress { get; set; } = double.NaN;
From edb89bd16651fd799ecce6d5bbd39c8b397f318d Mon Sep 17 00:00:00 2001
From: Julien Lebosquain
Date: Fri, 27 Mar 2026 09:21:51 +0100
Subject: [PATCH 18/57] Remove LambdaExpression.Compile from
CompiledBinding.Create (#20996)
---
.../Core/Parsers/BindingExpressionVisitor.cs | 21 +++----------------
1 file changed, 3 insertions(+), 18 deletions(-)
diff --git a/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs b/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs
index 8525dd8493..841bcb19a7 100644
--- a/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs
+++ b/src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs
@@ -303,29 +303,14 @@ internal class BindingExpressionVisitor(LambdaExpression expression) : Expr
}
}
- private static Func
+
+
+
+
+
+
+
@@ -82,6 +90,14 @@
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs
index 243bc5868b..0a81133bae 100644
--- a/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DrawerPage/DrawerPageCustomizationPage.xaml.cs
@@ -1,8 +1,10 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
+using Avalonia.Layout;
using Avalonia.Media;
namespace ControlCatalog.Pages
@@ -164,7 +166,7 @@ namespace ControlCatalog.Pages
if (!_isLoaded)
return;
if (ShowHeaderCheck.IsChecked == true)
- DemoDrawer.DrawerHeader = DrawerHeaderBorder;
+ DemoDrawer.DrawerHeader = HeaderTemplateCombo.SelectedIndex == 0 ? DrawerHeaderBorder : (object)"My Application";
else
DemoDrawer.DrawerHeader = null;
}
@@ -174,9 +176,128 @@ namespace ControlCatalog.Pages
if (!_isLoaded)
return;
if (ShowFooterCheck.IsChecked == true)
- DemoDrawer.DrawerFooter = DrawerFooterBorder;
+ {
+ DemoDrawer.DrawerFooter = FooterTemplateCombo.SelectedIndex switch
+ {
+ 1 => (object)"v12.0",
+ 2 => (object)"Avalonia",
+ _ => DrawerFooterBorder
+ };
+ }
else
+ {
DemoDrawer.DrawerFooter = null;
+ }
+ }
+
+ private void OnHeaderTemplateChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (!_isLoaded)
+ return;
+
+ switch (HeaderTemplateCombo.SelectedIndex)
+ {
+ case 1:
+ DemoDrawer.DrawerHeader = "My Application";
+ DemoDrawer.DrawerHeaderTemplate = new FuncDataTemplate((data, _) =>
+ new Border
+ {
+ Padding = new Avalonia.Thickness(16),
+ Child = new StackPanel
+ {
+ Spacing = 2,
+ Children =
+ {
+ new TextBlock { Text = data, FontSize = 18, FontWeight = FontWeight.SemiBold, Foreground = Brushes.White },
+ new TextBlock { Text = "Navigation", FontSize = 12, Foreground = Brushes.White, Opacity = 0.7 }
+ }
+ }
+ });
+ break;
+
+ case 2:
+ DemoDrawer.DrawerHeader = "My Application";
+ DemoDrawer.DrawerHeaderTemplate = new FuncDataTemplate((data, _) =>
+ {
+ var initial = data?.Length > 0 ? data[0].ToString().ToUpperInvariant() : "?";
+ var avatar = new Border
+ {
+ Width = 40,
+ Height = 40,
+ CornerRadius = new Avalonia.CornerRadius(20),
+ Background = new SolidColorBrush(Color.Parse("#1976D2")),
+ Child = new TextBlock
+ {
+ Text = initial,
+ FontSize = 18,
+ FontWeight = FontWeight.Bold,
+ Foreground = Brushes.White,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ }
+ };
+ var label = new TextBlock { Text = data, FontSize = 14, FontWeight = FontWeight.SemiBold, VerticalAlignment = VerticalAlignment.Center };
+ var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 10 };
+ row.Children.Add(avatar);
+ row.Children.Add(label);
+ return new Border { Padding = new Avalonia.Thickness(12), Child = row };
+ });
+ break;
+
+ default:
+ DemoDrawer.DrawerHeader = DrawerHeaderBorder;
+ DemoDrawer.DrawerHeaderTemplate = null;
+ break;
+ }
+ }
+
+ private void OnFooterTemplateChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (!_isLoaded)
+ return;
+
+ switch (FooterTemplateCombo.SelectedIndex)
+ {
+ case 1:
+ DemoDrawer.DrawerFooter = "v12.0";
+ DemoDrawer.DrawerFooterTemplate = new FuncDataTemplate((data, _) =>
+ new Border
+ {
+ Padding = new Avalonia.Thickness(12, 8),
+ Child = new Border
+ {
+ Padding = new Avalonia.Thickness(8, 4),
+ CornerRadius = new Avalonia.CornerRadius(4),
+ Background = new SolidColorBrush(Color.Parse("#1976D2")),
+ Child = new TextBlock { Text = data, FontSize = 11, Foreground = Brushes.White, FontWeight = FontWeight.SemiBold }
+ }
+ });
+ break;
+
+ case 2:
+ DemoDrawer.DrawerFooter = "Avalonia";
+ DemoDrawer.DrawerFooterTemplate = new FuncDataTemplate((data, _) =>
+ {
+ var icon = new PathIcon
+ {
+ Width = 14,
+ Height = 14,
+ Data = Geometry.Parse("M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"),
+ Opacity = 0.5
+ };
+ var label = new TextBlock { Text = data, FontSize = 12, Opacity = 0.6, VerticalAlignment = VerticalAlignment.Center };
+ var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 6 };
+ row.Children.Add(icon);
+ row.Children.Add(label);
+ return new Border { Padding = new Avalonia.Thickness(14, 10), Child = row };
+ });
+ break;
+
+ default:
+ DemoDrawer.DrawerFooter = DrawerFooterBorder;
+ DemoDrawer.DrawerFooterTemplate = null;
+ break;
+ }
}
private void OnMenuItemClick(object? sender, RoutedEventArgs e)
diff --git a/src/Avalonia.Controls/Page/DrawerPage.cs b/src/Avalonia.Controls/Page/DrawerPage.cs
index f8e039f8cd..e2cefeb5d9 100644
--- a/src/Avalonia.Controls/Page/DrawerPage.cs
+++ b/src/Avalonia.Controls/Page/DrawerPage.cs
@@ -124,6 +124,18 @@ namespace Avalonia.Controls
public static readonly StyledProperty DrawerFooterProperty =
AvaloniaProperty.Register(nameof(DrawerFooter));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty DrawerHeaderTemplateProperty =
+ AvaloniaProperty.Register(nameof(DrawerHeaderTemplate));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty DrawerFooterTemplateProperty =
+ AvaloniaProperty.Register(nameof(DrawerFooterTemplate));
+
///
/// Defines the property.
///
@@ -403,6 +415,7 @@ namespace Avalonia.Controls
///
/// Gets or sets the header content displayed at the top of the drawer pane.
///
+ [DependsOn(nameof(DrawerHeaderTemplate))]
public object? DrawerHeader
{
get => GetValue(DrawerHeaderProperty);
@@ -412,12 +425,31 @@ namespace Avalonia.Controls
///
/// Gets or sets the footer content displayed at the bottom of the drawer pane.
///
+ [DependsOn(nameof(DrawerFooterTemplate))]
public object? DrawerFooter
{
get => GetValue(DrawerFooterProperty);
set => SetValue(DrawerFooterProperty, value);
}
+ ///
+ /// Gets or sets the data template used to display .
+ ///
+ public IDataTemplate? DrawerHeaderTemplate
+ {
+ get => GetValue(DrawerHeaderTemplateProperty);
+ set => SetValue(DrawerHeaderTemplateProperty, value);
+ }
+
+ ///
+ /// Gets or sets the data template used to display .
+ ///
+ public IDataTemplate? DrawerFooterTemplate
+ {
+ get => GetValue(DrawerFooterTemplateProperty);
+ set => SetValue(DrawerFooterTemplateProperty, value);
+ }
+
///
/// Gets or sets the icon displayed in the drawer toggle button.
///
diff --git a/src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml b/src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml
index 985814c967..2a5a672d6b 100644
--- a/src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/DrawerPage.xaml
@@ -57,11 +57,13 @@
+ new FuncControlTemplate((dp, scope) =>
+ {
+ var header = new ContentPresenter { Name = "PART_DrawerHeader" }.RegisterInNameScope(scope);
+ header.Bind(ContentPresenter.ContentProperty, dp.GetObservable(DrawerPage.DrawerHeaderProperty));
+ header.Bind(ContentPresenter.ContentTemplateProperty, dp.GetObservable(DrawerPage.DrawerHeaderTemplateProperty));
+
+ var footer = new ContentPresenter { Name = "PART_DrawerFooter" }.RegisterInNameScope(scope);
+ footer.Bind(ContentPresenter.ContentProperty, dp.GetObservable(DrawerPage.DrawerFooterProperty));
+ footer.Bind(ContentPresenter.ContentTemplateProperty, dp.GetObservable(DrawerPage.DrawerFooterTemplateProperty));
+
+ return new StackPanel { Children = { header, footer } };
+ });
+
+ private static (DrawerPage dp, ContentPresenter header, ContentPresenter footer, TestRoot root) Create(
+ object? drawerHeader = null,
+ IDataTemplate? headerTemplate = null,
+ object? drawerFooter = null,
+ IDataTemplate? footerTemplate = null)
+ {
+ var dp = new DrawerPage
+ {
+ Template = MinimalPaneTemplate(),
+ DrawerHeader = drawerHeader,
+ DrawerHeaderTemplate = headerTemplate,
+ DrawerFooter = drawerFooter,
+ DrawerFooterTemplate = footerTemplate,
+ };
+ var root = new TestRoot { Child = dp };
+ dp.ApplyTemplate();
+
+ var header = dp.GetVisualDescendants().OfType().First(x => x.Name == "PART_DrawerHeader");
+ var footer = dp.GetVisualDescendants().OfType().First(x => x.Name == "PART_DrawerFooter");
+
+ return (dp, header, footer, root);
+ }
+
+ [Fact]
+ public void DrawerHeaderTemplate_IsForwardedToContentPresenter()
+ {
+ var template = new FuncDataTemplate((_, _) => new TextBlock());
+ var (_, header, _, _) = Create(drawerHeader: "App", headerTemplate: template);
+
+ Assert.Same(template, header.ContentTemplate);
+ }
+
+ [Fact]
+ public void DrawerFooterTemplate_IsForwardedToContentPresenter()
+ {
+ var template = new FuncDataTemplate((_, _) => new TextBlock());
+ var (_, _, footer, _) = Create(drawerFooter: "v1.0", footerTemplate: template);
+
+ Assert.Same(template, footer.ContentTemplate);
+ }
+
+ [Fact]
+ public void DrawerHeaderTemplate_RendersControlProducedByFactory()
+ {
+ var (_, header, _, _) = Create(
+ drawerHeader: "App",
+ headerTemplate: new FuncDataTemplate((_, _) => new Canvas()));
+
+ header.UpdateChild();
+
+ Assert.IsType
internal static FocusManager? GetFocusManager(IInputElement? element)
{
-
+
// Element might not be a visual, and not attached to the root.
// But IFocusManager is always expected to be a FocusManager.
return (FocusManager?)(element as Visual)?.GetInputRoot()?.FocusManager
@@ -188,8 +188,12 @@ namespace Avalonia.Input
{
if (CanFocus(e))
{
- if (ev.Pointer.Type == PointerType.Mouse || ev is PointerReleasedEventArgs)
- return true;
+ return ev switch
+ {
+ PointerReleasedEventArgs releasedEventArgs when releasedEventArgs.Pointer.Type != PointerType.Mouse => true,
+ PointerPressedEventArgs pressedEventArgs when pressedEventArgs.Pointer.Type == PointerType.Mouse => true,
+ _ => false,
+ };
}
return false;
@@ -229,7 +233,7 @@ namespace Avalonia.Input
var root = v.PresentationSource?.InputRoot.FocusRoot as Visual;
while (root is IHostedVisualTreeRoot hosted &&
- hosted.Host?.PresentationSource?.InputRoot.FocusRoot is {} parentRoot)
+ hosted.Host?.PresentationSource?.InputRoot.FocusRoot is { } parentRoot)
{
root = parentRoot;
}
diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
index a09eccc1a3..45d665b591 100644
--- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
@@ -24,7 +24,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.Same(target, root.FocusManager.GetFocusedElement());
}
}
-
+
[Fact]
public void Invisible_Controls_Should_Not_Receive_Focus()
{
@@ -34,20 +34,20 @@ namespace Avalonia.Base.UnitTests.Input
{
var root = new TestRoot
{
- Child = target = new Button() { IsVisible = false}
+ Child = target = new Button() { IsVisible = false }
};
-
+
Assert.Null(root.FocusManager.GetFocusedElement());
target.Focus();
-
+
Assert.False(target.IsFocused);
Assert.False(target.IsKeyboardFocusWithin);
Assert.Null(root.FocusManager.GetFocusedElement());
}
}
-
+
[Fact]
public void Effectively_Invisible_Controls_Should_Not_Receive_Focus()
{
@@ -64,11 +64,11 @@ namespace Avalonia.Base.UnitTests.Input
Children = { target }
}
};
-
+
Assert.Null(root.FocusManager.GetFocusedElement());
target.Focus();
-
+
Assert.False(target.IsFocused);
Assert.False(target.IsKeyboardFocusWithin);
@@ -87,7 +87,7 @@ namespace Avalonia.Base.UnitTests.Input
var root = new TestRoot
{
Child = new StackPanel
- {
+ {
Children =
{
(first = new Button()),
@@ -365,7 +365,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.False(target2.Classes.Contains(":focus-visible"));
}
}
-
+
[Fact]
public void Control_FocusWithin_PseudoClass_Should_Be_Applied()
{
@@ -398,7 +398,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.True(root.IsKeyboardFocusWithin);
}
}
-
+
[Fact]
public void Control_FocusWithin_PseudoClass_Should_Be_Applied_and_Removed()
{
@@ -419,7 +419,7 @@ namespace Avalonia.Base.UnitTests.Input
}
}
};
-
+
target1.ApplyTemplate();
target2.ApplyTemplate();
@@ -433,9 +433,9 @@ namespace Avalonia.Base.UnitTests.Input
Assert.True(root.Child.IsKeyboardFocusWithin);
Assert.True(root.Classes.Contains(":focus-within"));
Assert.True(root.IsKeyboardFocusWithin);
-
+
target2.Focus();
-
+
Assert.False(target1.IsFocused);
Assert.False(target1.Classes.Contains(":focus-within"));
Assert.False(target1.IsKeyboardFocusWithin);
@@ -445,7 +445,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.True(root.Child.IsKeyboardFocusWithin);
Assert.True(root.Classes.Contains(":focus-within"));
Assert.True(root.IsKeyboardFocusWithin);
-
+
Assert.True(target2.IsFocused);
Assert.True(target2.Classes.Contains(":focus-within"));
Assert.True(target2.IsKeyboardFocusWithin);
@@ -453,7 +453,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.True(panel2.IsKeyboardFocusWithin);
}
}
-
+
[Fact]
public void Control_FocusWithin_Pseudoclass_Should_Be_Removed_When_Removed_From_Tree()
{
@@ -487,11 +487,11 @@ namespace Avalonia.Base.UnitTests.Input
var keyboardDevice = KeyboardDevice.Instance!;
Assert.Equal(keyboardDevice.FocusedElement, target1);
-
+
root.Child = null;
-
+
Assert.Null(keyboardDevice.FocusedElement);
-
+
Assert.False(target1.IsFocused);
Assert.False(target1.Classes.Contains(":focus-within"));
Assert.False(target1.IsKeyboardFocusWithin);
@@ -499,7 +499,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.False(root.IsKeyboardFocusWithin);
}
}
-
+
[Fact]
public void Control_FocusWithin_Pseudoclass_Should_Be_Removed_Focus_Moves_To_Different_Root()
{
@@ -507,7 +507,7 @@ namespace Avalonia.Base.UnitTests.Input
{
var target1 = new Decorator { Focusable = true };
var target2 = new Decorator { Focusable = true };
-
+
var root1 = new TestRoot
{
Child = new StackPanel
@@ -518,7 +518,7 @@ namespace Avalonia.Base.UnitTests.Input
}
}
};
-
+
var root2 = new TestRoot
{
Child = new StackPanel
@@ -543,9 +543,9 @@ namespace Avalonia.Base.UnitTests.Input
Assert.True(root1.IsKeyboardFocusWithin);
Assert.Equal(KeyboardDevice.Instance!.FocusedElement, target1);
-
+
target2.Focus();
-
+
Assert.False(target1.IsFocused);
Assert.False(target1.Classes.Contains(":focus-within"));
Assert.False(target1.IsKeyboardFocusWithin);
@@ -553,7 +553,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.False(root1.Child.IsKeyboardFocusWithin);
Assert.False(root1.Classes.Contains(":focus-within"));
Assert.False(root1.IsKeyboardFocusWithin);
-
+
Assert.True(target2.IsFocused);
Assert.True(target2.Classes.Contains(":focus-within"));
Assert.True(target2.IsKeyboardFocusWithin);
@@ -1031,7 +1031,7 @@ namespace Avalonia.Base.UnitTests.Input
[XYFocus.UpProperty] = target3,
[XYFocus.DownProperty] = target4,
};
- var container = new Canvas
+ var container = new Canvas
{
Children =
{
diff --git a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
index 6380f71935..1ec6ec2be5 100644
--- a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
@@ -4,7 +4,6 @@ using Avalonia.Input.Raw;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
-using Avalonia.Threading;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@@ -18,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var scope = AvaloniaLocator.EnterScope();
var settingsMock = new Mock();
-
+
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind().ToConstant(settingsMock.Object);
@@ -32,7 +31,7 @@ namespace Avalonia.Base.UnitTests.Input
var control = new Control();
var root = CreateInputRoot(impl.Object, control, renderer.Object);
-
+
MouseButton button = default;
root.PointerReleased += (s, e) => button = e.InitialPressMouseButton;
@@ -50,10 +49,10 @@ namespace Avalonia.Base.UnitTests.Input
impl.Object.Input!(up);
Assert.Equal(MouseButton.Left, button);
-
+
impl.Object.Input!(up);
- Assert.Equal(MouseButton.None, button);
+ Assert.Equal(MouseButton.None, button);
}
[Fact]
@@ -85,7 +84,7 @@ namespace Avalonia.Base.UnitTests.Input
impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
Assert.NotNull(result);
-
+
result.Capture(control);
Assert.Same(control, result.Captured);
@@ -115,8 +114,8 @@ namespace Avalonia.Base.UnitTests.Input
})
}
}, renderer.Object);
-
-
+
+
Point? result = null;
root.PointerMoved += (_, a) =>
{
@@ -128,5 +127,93 @@ namespace Avalonia.Base.UnitTests.Input
Assert.Equal(new Point(1, 11), result);
}
+
+ [Fact]
+ public void Mouse_Pointer_Should_Set_Focus_On_Pointer_Pressed()
+ {
+ using var scope = AvaloniaLocator.EnterScope();
+ var settingsMock = new Mock();
+
+ AvaloniaLocator.CurrentMutable.BindToSelf(this)
+ .Bind().ToConstant(settingsMock.Object);
+
+ using var app = UnitTestApplication.Start(
+ TestServices.RealFocus);
+
+ var renderer = new Mock();
+ var impl = CreateTopLevelImplMock();
+
+ var control = new Button()
+ {
+ Focusable = true
+ };
+ var root = CreateInputRoot(impl.Object, control, renderer.Object);
+
+ var device = new MouseDevice();
+
+ var down = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonDown);
+ var up = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonUp);
+
+ SetHit(renderer, control);
+
+ Assert.False(control.IsFocused);
+
+ impl.Object.Input!(down);
+
+ Assert.True(control.IsFocused);
+ impl.Object.Input!(up);
+
+ Assert.True(control.IsFocused);
+ }
+
+ [Fact]
+ public void Control_Should_Not_Gain_Focus_On_Mouse_Release()
+ {
+ using var scope = AvaloniaLocator.EnterScope();
+ var settingsMock = new Mock();
+
+ AvaloniaLocator.CurrentMutable.BindToSelf(this)
+ .Bind().ToConstant(settingsMock.Object);
+
+ using var app = UnitTestApplication.Start(
+ TestServices.RealFocus);
+
+ var renderer = new Mock();
+ var impl = CreateTopLevelImplMock();
+
+ var control1 = new Button()
+ {
+ Focusable = true
+ };
+
+ var control2 = new Button()
+ {
+ Focusable = true
+ };
+ var stack = new StackPanel()
+ {
+ Children = { control1, control2 }
+ };
+ var root = CreateInputRoot(impl.Object, stack, renderer.Object);
+
+ var device = new MouseDevice();
+
+ var down = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonDown);
+ var up = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonUp);
+
+ SetHit(renderer, control1);
+
+ Assert.False(control1.IsFocused);
+
+ impl.Object.Input!(down);
+
+ Assert.True(control1.IsFocused);
+
+ control2.Focus();
+
+ impl.Object.Input!(up);
+
+ Assert.False(control1.IsFocused);
+ }
}
}
diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs b/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs
index c75ff56d82..1675dc9e62 100644
--- a/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs
@@ -7,9 +7,7 @@ using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Rendering;
-using Avalonia.Rendering.Composition;
using Avalonia.UnitTests;
-using Avalonia.VisualTree;
using Moq;
namespace Avalonia.Base.UnitTests.Input;
diff --git a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs
index 97f83b1a69..600856c643 100644
--- a/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs
@@ -1,14 +1,16 @@
using System;
+using Avalonia.Base.UnitTests.Input;
+using Avalonia.Controls;
using Avalonia.Input.Raw;
using Avalonia.Platform;
-using Avalonia.Threading;
+using Avalonia.Rendering;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Input.UnitTests
{
- public class TouchDeviceTests
+ public class TouchDeviceTests : PointerTestsBase
{
[Fact]
public void Tapped_Event_Is_Fired_With_Touch()
@@ -141,6 +143,36 @@ namespace Avalonia.Input.UnitTests
Assert.Equal(0, doubleTappedExecutedTimes);
}
+ [Fact]
+ public void Touch_Pointer_Should_Set_Focus_On_Pointer_Released()
+ {
+ using var scope = AvaloniaLocator.EnterScope();
+ using var app = UnitTestApplication.Start(
+ TestServices.RealFocus);
+
+ var impl = CreateTopLevelImplMock();
+
+ var renderer = new Mock();
+ var root = new TestTopLevel(impl.Object)
+ {
+ HitTesterOverride = renderer.Object,
+ };
+ var host = root.TopLevelHost;
+
+ host.Focusable = true;
+ var touchDevice = new TouchDevice();
+ var inputManager = InputManager.Instance!;
+
+ Assert.False(host.IsFocused);
+
+ Press(InputManager.Instance!, touchDevice, root.InputRoot);
+
+ Assert.False(host.IsFocused);
+ Release(InputManager.Instance!, touchDevice, root.InputRoot);
+
+ Assert.True(host.IsFocused);
+ }
+
[Fact]
public void Click_Counting_Should_Work_Correctly_With_Few_Touch_Contacts()
{
@@ -241,14 +273,12 @@ namespace Avalonia.Input.UnitTests
private static void TapOnce(IInputManager inputManager, TouchDevice device, IInputRoot root, ulong timestamp = 0, long touchPointId = 0)
{
- inputManager.ProcessInput(new RawPointerEventArgs(device, timestamp,
- root,
- RawPointerEventType.TouchBegin,
- new Point(0, 0),
- RawInputModifiers.None)
- {
- RawPointerId = touchPointId
- });
+ Press(inputManager, device, root, timestamp, touchPointId);
+ Release(inputManager, device, root, timestamp, touchPointId);
+ }
+
+ private static void Release(IInputManager inputManager, TouchDevice device, IInputRoot root, ulong timestamp = 0, long touchPointId = 0)
+ {
inputManager.ProcessInput(new RawPointerEventArgs(device, timestamp,
root,
RawPointerEventType.TouchEnd,
@@ -258,5 +288,22 @@ namespace Avalonia.Input.UnitTests
RawPointerId = touchPointId
});
}
+
+ private static void Press(IInputManager inputManager, TouchDevice device, IInputRoot root, ulong timestamp = 0, long touchPointId = 0)
+ {
+ inputManager.ProcessInput(new RawPointerEventArgs(device, timestamp,
+ root,
+ RawPointerEventType.TouchBegin,
+ new Point(0, 0),
+ RawInputModifiers.None)
+ {
+ RawPointerId = touchPointId
+ });
+ }
+
+ private class TestTopLevel(ITopLevelImpl impl) : TopLevel(impl)
+ {
+
+ }
}
}
From 5ba86721dedc565f88037fbcec79ee2bfa32403e Mon Sep 17 00:00:00 2001
From: Julien Lebosquain
Date: Mon, 30 Mar 2026 23:04:58 +0200
Subject: [PATCH 33/57] Change DoDragDropAsync trigger event to
PointerPressedEventArgs (#20988)
* Change DnD trigger event to PointerPressedEventArgs
* Update API suppressions
* Switch IPlatformDragSource to a private API
* Update API suppressions
---
api/Avalonia.nupkg.xml | 300 ++++++++++--------
src/Avalonia.Base/Input/DragDrop.cs | 2 +-
.../Input/Platform/IPlatformDragSource.cs | 4 +-
.../Platform/InProcessDragSource.cs | 2 +-
.../AvaloniaNativeDragSource.cs | 2 +-
src/Windows/Avalonia.Win32/DragSource.cs | 2 +-
6 files changed, 174 insertions(+), 138 deletions(-)
diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index a63ac3baae..cbaf782d24 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -247,6 +247,24 @@
baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+ CP0001
+ T:Avalonia.Controls.AppBarButton
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0001
+ T:Avalonia.Controls.AppBarSeparator
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0001
+ T:Avalonia.Controls.AppBarToggleButton
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
CP0001
T:Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetimeOptions
@@ -727,6 +745,24 @@
baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+ CP0001
+ T:Avalonia.Controls.AppBarButton
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0001
+ T:Avalonia.Controls.AppBarSeparator
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0001
+ T:Avalonia.Controls.AppBarToggleButton
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
CP0001
T:Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetimeOptions
@@ -1117,6 +1153,12 @@
baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Input.DragDrop.DoDragDropAsync(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects)
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Input.DragEventArgs.#ctor(Avalonia.Interactivity.RoutedEvent{Avalonia.Input.DragEventArgs},Avalonia.Input.IDataObject,Avalonia.Interactivity.Interactive,Avalonia.Point,Avalonia.Input.KeyModifiers)
@@ -1417,6 +1459,12 @@
baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDropAsync(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects)
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Input.Raw.RawDragEvent.#ctor(Avalonia.Input.Raw.IDragDropDevice,Avalonia.Input.Raw.RawDragEventType,Avalonia.Input.IInputRoot,Avalonia.Point,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects,Avalonia.Input.RawInputModifiers)
@@ -1885,6 +1933,24 @@
baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+ CP0002
+ F:Avalonia.Controls.NavigationPage.IsBackButtonEffectivelyVisibleProperty
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ F:Avalonia.Controls.PipsPager.NextButtonStyleProperty
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ F:Avalonia.Controls.PipsPager.PreviousButtonStyleProperty
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
CP0002
F:Avalonia.Controls.Primitives.FlyoutBase.IsOpenProperty
@@ -2119,12 +2185,42 @@
baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+ CP0002
+ M:Avalonia.Controls.NavigationPage.get_IsBackButtonEffectivelyVisible
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
CP0002
M:Avalonia.Controls.PageSelectionChangedEventArgs.#ctor(Avalonia.Controls.Page,Avalonia.Controls.Page)
baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.get_NextButtonStyle
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.get_PreviousButtonStyle
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.set_NextButtonStyle(Avalonia.Styling.ControlTheme)
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.set_PreviousButtonStyle(Avalonia.Styling.ControlTheme)
+ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
CP0002
M:Avalonia.Controls.Platform.DefaultMenuInteractionHandler.GotFocus(System.Object,Avalonia.Input.GotFocusEventArgs)
@@ -2815,6 +2911,12 @@
baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Input.DragDrop.DoDragDropAsync(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Input.DragEventArgs.#ctor(Avalonia.Interactivity.RoutedEvent{Avalonia.Input.DragEventArgs},Avalonia.Input.IDataObject,Avalonia.Interactivity.Interactive,Avalonia.Point,Avalonia.Input.KeyModifiers)
@@ -3115,6 +3217,12 @@
baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDropAsync(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Input.Raw.RawDragEvent.#ctor(Avalonia.Input.Raw.IDragDropDevice,Avalonia.Input.Raw.RawDragEventType,Avalonia.Input.IInputRoot,Avalonia.Point,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects,Avalonia.Input.RawInputModifiers)
@@ -3583,6 +3691,24 @@
baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+ CP0002
+ F:Avalonia.Controls.NavigationPage.IsBackButtonEffectivelyVisibleProperty
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0002
+ F:Avalonia.Controls.PipsPager.NextButtonStyleProperty
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0002
+ F:Avalonia.Controls.PipsPager.PreviousButtonStyleProperty
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
CP0002
F:Avalonia.Controls.Primitives.FlyoutBase.IsOpenProperty
@@ -3817,12 +3943,42 @@
baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+ CP0002
+ M:Avalonia.Controls.NavigationPage.get_IsBackButtonEffectivelyVisible
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
CP0002
M:Avalonia.Controls.PageSelectionChangedEventArgs.#ctor(Avalonia.Controls.Page,Avalonia.Controls.Page)
baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.get_NextButtonStyle
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.get_PreviousButtonStyle
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.set_NextButtonStyle(Avalonia.Styling.ControlTheme)
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
+
+ CP0002
+ M:Avalonia.Controls.PipsPager.set_PreviousButtonStyle(Avalonia.Styling.ControlTheme)
+ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
+ current/Avalonia/lib/net8.0/Avalonia.Controls.dll
+
CP0002
M:Avalonia.Controls.Platform.DefaultMenuInteractionHandler.GotFocus(System.Object,Avalonia.Input.GotFocusEventArgs)
@@ -4471,6 +4627,12 @@
baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+ CP0006
+ M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDropAsync(Avalonia.Input.PointerPressedEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects)
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
CP0006
M:Avalonia.Platform.ICursorFactory.CreateCursor(Avalonia.Media.Imaging.Bitmap,Avalonia.PixelPoint)
@@ -4819,6 +4981,12 @@
baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+ CP0006
+ M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDropAsync(Avalonia.Input.PointerPressedEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
CP0006
M:Avalonia.Platform.ICursorFactory.CreateCursor(Avalonia.Media.Imaging.Bitmap,Avalonia.PixelPoint)
@@ -5725,136 +5893,4 @@
baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
-
- CP0002
- F:Avalonia.Controls.NavigationPage.IsBackButtonEffectivelyVisibleProperty
- baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
- current/Avalonia/lib/net10.0/Avalonia.Controls.dll
-
-
- CP0002
- M:Avalonia.Controls.NavigationPage.get_IsBackButtonEffectivelyVisible
- baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
- current/Avalonia/lib/net10.0/Avalonia.Controls.dll
-
-
- CP0002
- F:Avalonia.Controls.PipsPager.PreviousButtonStyleProperty
- baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
- current/Avalonia/lib/net10.0/Avalonia.Controls.dll
-
-
- CP0002
- F:Avalonia.Controls.PipsPager.NextButtonStyleProperty
- baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
- current/Avalonia/lib/net10.0/Avalonia.Controls.dll
-
-
- CP0002
- M:Avalonia.Controls.PipsPager.get_PreviousButtonStyle
- baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
- current/Avalonia/lib/net10.0/Avalonia.Controls.dll
-
-
- CP0002
- M:Avalonia.Controls.PipsPager.set_PreviousButtonStyle(Avalonia.Styling.ControlTheme)
- baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
- current/Avalonia/lib/net10.0/Avalonia.Controls.dll
-
-
- CP0002
- M:Avalonia.Controls.PipsPager.get_NextButtonStyle
- baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
- current/Avalonia/lib/net10.0/Avalonia.Controls.dll
-
-
- CP0002
- M:Avalonia.Controls.PipsPager.set_NextButtonStyle(Avalonia.Styling.ControlTheme)
- baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
- current/Avalonia/lib/net10.0/Avalonia.Controls.dll
-
-
- CP0002
- F:Avalonia.Controls.NavigationPage.IsBackButtonEffectivelyVisibleProperty
- baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
- current/Avalonia/lib/net8.0/Avalonia.Controls.dll
-
-
- CP0002
- M:Avalonia.Controls.NavigationPage.get_IsBackButtonEffectivelyVisible
- baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
- current/Avalonia/lib/net8.0/Avalonia.Controls.dll
-
-
- CP0002
- F:Avalonia.Controls.PipsPager.PreviousButtonStyleProperty
- baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
- current/Avalonia/lib/net8.0/Avalonia.Controls.dll
-
-
- CP0002
- F:Avalonia.Controls.PipsPager.NextButtonStyleProperty
- baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
- current/Avalonia/lib/net8.0/Avalonia.Controls.dll
-
-
- CP0002
- M:Avalonia.Controls.PipsPager.get_PreviousButtonStyle
- baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
- current/Avalonia/lib/net8.0/Avalonia.Controls.dll
-
-
- CP0002
- M:Avalonia.Controls.PipsPager.set_PreviousButtonStyle(Avalonia.Styling.ControlTheme)
- baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
- current/Avalonia/lib/net8.0/Avalonia.Controls.dll
-
-
- CP0002
- M:Avalonia.Controls.PipsPager.get_NextButtonStyle
- baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
- current/Avalonia/lib/net8.0/Avalonia.Controls.dll
-
-
- CP0002
- M:Avalonia.Controls.PipsPager.set_NextButtonStyle(Avalonia.Styling.ControlTheme)
- baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
- current/Avalonia/lib/net8.0/Avalonia.Controls.dll
-
-
- CP0001
- T:Avalonia.Controls.AppBarButton
- baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
- current/Avalonia/lib/net10.0/Avalonia.Controls.dll
-
-
- CP0001
- T:Avalonia.Controls.AppBarSeparator
- baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
- current/Avalonia/lib/net10.0/Avalonia.Controls.dll
-
-
- CP0001
- T:Avalonia.Controls.AppBarToggleButton
- baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
- current/Avalonia/lib/net10.0/Avalonia.Controls.dll
-
-
- CP0001
- T:Avalonia.Controls.AppBarButton
- baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
- current/Avalonia/lib/net8.0/Avalonia.Controls.dll
-
-
- CP0001
- T:Avalonia.Controls.AppBarSeparator
- baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
- current/Avalonia/lib/net8.0/Avalonia.Controls.dll
-
-
- CP0001
- T:Avalonia.Controls.AppBarToggleButton
- baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll
- current/Avalonia/lib/net8.0/Avalonia.Controls.dll
-
diff --git a/src/Avalonia.Base/Input/DragDrop.cs b/src/Avalonia.Base/Input/DragDrop.cs
index 33f538c443..fa94e37b8c 100644
--- a/src/Avalonia.Base/Input/DragDrop.cs
+++ b/src/Avalonia.Base/Input/DragDrop.cs
@@ -127,7 +127,7 @@ namespace Avalonia.Input
///
///
public static Task DoDragDropAsync(
- PointerEventArgs triggerEvent,
+ PointerPressedEventArgs triggerEvent,
IDataTransfer dataTransfer,
DragDropEffects allowedEffects)
{
diff --git a/src/Avalonia.Base/Input/Platform/IPlatformDragSource.cs b/src/Avalonia.Base/Input/Platform/IPlatformDragSource.cs
index 4ad36576ca..d5835d96b5 100644
--- a/src/Avalonia.Base/Input/Platform/IPlatformDragSource.cs
+++ b/src/Avalonia.Base/Input/Platform/IPlatformDragSource.cs
@@ -3,11 +3,11 @@ using Avalonia.Metadata;
namespace Avalonia.Input.Platform
{
- [NotClientImplementable]
+ [PrivateApi]
public interface IPlatformDragSource
{
Task DoDragDropAsync(
- PointerEventArgs triggerEvent,
+ PointerPressedEventArgs triggerEvent,
IDataTransfer dataTransfer,
DragDropEffects allowedEffects);
}
diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs
index 1a7b719499..6a8b79eab6 100644
--- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs
+++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs
@@ -31,7 +31,7 @@ namespace Avalonia.Platform
}
public async Task DoDragDropAsync(
- PointerEventArgs triggerEvent,
+ PointerPressedEventArgs triggerEvent,
IDataTransfer dataTransfer,
DragDropEffects allowedEffects)
{
diff --git a/src/Avalonia.Native/AvaloniaNativeDragSource.cs b/src/Avalonia.Native/AvaloniaNativeDragSource.cs
index 662d79d1b8..5927397ab5 100644
--- a/src/Avalonia.Native/AvaloniaNativeDragSource.cs
+++ b/src/Avalonia.Native/AvaloniaNativeDragSource.cs
@@ -33,7 +33,7 @@ namespace Avalonia.Native
}
public Task DoDragDropAsync(
- PointerEventArgs triggerEvent,
+ PointerPressedEventArgs triggerEvent,
IDataTransfer dataTransfer,
DragDropEffects allowedEffects)
{
diff --git a/src/Windows/Avalonia.Win32/DragSource.cs b/src/Windows/Avalonia.Win32/DragSource.cs
index f1cdfa440a..9ca295a904 100644
--- a/src/Windows/Avalonia.Win32/DragSource.cs
+++ b/src/Windows/Avalonia.Win32/DragSource.cs
@@ -10,7 +10,7 @@ namespace Avalonia.Win32
internal sealed class DragSource : IPlatformDragSource
{
public Task DoDragDropAsync(
- PointerEventArgs triggerEvent,
+ PointerPressedEventArgs triggerEvent,
IDataTransfer dataTransfer,
DragDropEffects allowedEffects)
{
From 02d17c78636decb76d5eb3058713657c0f305e39 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Javier=20Su=C3=A1rez?=
Date: Mon, 30 Mar 2026 22:32:22 +0200
Subject: [PATCH 34/57] Apply optimizations in Avalonia iOS (#20969)
* Apply optimizations on Avalonia iOS
* Updated benchmarks
* More changes
* Removed iOS Benchmarks project
---
src/iOS/Avalonia.iOS/DispatcherImpl.cs | 23 +++++++---------------
src/iOS/Avalonia.iOS/InputHandler.cs | 4 ++--
src/iOS/Avalonia.iOS/TextInputResponder.cs | 3 ++-
3 files changed, 11 insertions(+), 19 deletions(-)
diff --git a/src/iOS/Avalonia.iOS/DispatcherImpl.cs b/src/iOS/Avalonia.iOS/DispatcherImpl.cs
index b39ba1a85a..1bf4503af8 100644
--- a/src/iOS/Avalonia.iOS/DispatcherImpl.cs
+++ b/src/iOS/Avalonia.iOS/DispatcherImpl.cs
@@ -22,7 +22,8 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing
private readonly IntPtr _mainLoop;
private readonly IntPtr _mainQueue;
private Thread? _loopThread;
- private bool _backgroundProcessingRequested, _signaled;
+ private bool _backgroundProcessingRequested;
+ private int _signaled;
private unsafe DispatcherImpl()
{
@@ -60,15 +61,12 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing
public unsafe void Signal()
{
- lock (_sync)
+ if (Interlocked.CompareExchange(ref _signaled, 1, 0) != 0)
{
- if (_signaled)
- return;
- _signaled = true;
-
- Interop.dispatch_async_f(_mainQueue, IntPtr.Zero, &CheckSignaled);
- Interop.CFRunLoopWakeUp(_mainLoop);
+ return;
}
+ Interop.dispatch_async_f(_mainQueue, IntPtr.Zero, &CheckSignaled);
+ Interop.CFRunLoopWakeUp(_mainLoop);
}
public void UpdateTimer(long? dueTimeInMs)
@@ -91,14 +89,7 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing
private void CheckSignaled()
{
- bool signaled;
- lock (_sync)
- {
- signaled = _signaled;
- _signaled = false;
- }
-
- if (signaled)
+ if (Interlocked.Exchange(ref _signaled, 0) != 0)
{
Signaled?.Invoke();
}
diff --git a/src/iOS/Avalonia.iOS/InputHandler.cs b/src/iOS/Avalonia.iOS/InputHandler.cs
index bd66d5a51e..06542cbb0d 100644
--- a/src/iOS/Avalonia.iOS/InputHandler.cs
+++ b/src/iOS/Avalonia.iOS/InputHandler.cs
@@ -308,7 +308,7 @@ internal sealed class InputHandler
_tl.Input?.Invoke(new RawMouseWheelEventArgs(
_mouseDevice,
- (ulong)(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()),
+ (ulong)Environment.TickCount64,
Root,
_cachedScrollLocation ?? new Point(0, 0),
new Vector(deltaX, deltaY),
@@ -372,7 +372,7 @@ internal sealed class InputHandler
// until the inertia stops.
_tl.Input?.Invoke(new RawMouseWheelEventArgs(
_mouseDevice,
- (ulong)(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()),
+ (ulong)Environment.TickCount64,
Root,
_cachedScrollLocation.Value,
new Vector(_momentumVelocityX, _momentumVelocityY),
diff --git a/src/iOS/Avalonia.iOS/TextInputResponder.cs b/src/iOS/Avalonia.iOS/TextInputResponder.cs
index 678fac8766..b2c1d1700c 100644
--- a/src/iOS/Avalonia.iOS/TextInputResponder.cs
+++ b/src/iOS/Avalonia.iOS/TextInputResponder.cs
@@ -84,6 +84,7 @@ partial class AvaloniaView
private int _inSurroundingTextUpdateEvent;
private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0);
private readonly UITextInputStringTokenizer _tokenizer;
+ private readonly NSString _textInputContextIdentifier = new NSString(Guid.NewGuid().ToString());
private bool _isInUpdate;
public TextInputMethodClient? Client => _client;
@@ -95,7 +96,7 @@ partial class AvaloniaView
public override UIEditingInteractionConfiguration EditingInteractionConfiguration =>
UIEditingInteractionConfiguration.Default;
- public override NSString TextInputContextIdentifier => new NSString(Guid.NewGuid().ToString());
+ public override NSString TextInputContextIdentifier => _textInputContextIdentifier;
public override UITextInputMode TextInputMode
{
From 0842236630eff07498bf306655eb6c35d8eba761 Mon Sep 17 00:00:00 2001
From: Julien Lebosquain
Date: Tue, 31 Mar 2026 13:48:55 +0200
Subject: [PATCH 35/57] Do not use visual children enumerator in TopLevelHost
(#21048)
* Add failing test for ExtendClientAreaToDecorationsHint in AttachedToVisualTree
* Do not use visual children enumerator in TopLevelHost
---
src/Avalonia.Controls/TopLevelHost.cs | 18 ++++++++----
.../WindowTests.cs | 29 +++++++++++++++++++
2 files changed, 41 insertions(+), 6 deletions(-)
diff --git a/src/Avalonia.Controls/TopLevelHost.cs b/src/Avalonia.Controls/TopLevelHost.cs
index 9592dd8221..afc4d59e7b 100644
--- a/src/Avalonia.Controls/TopLevelHost.cs
+++ b/src/Avalonia.Controls/TopLevelHost.cs
@@ -51,11 +51,14 @@ internal partial class TopLevelHost : Control
var hasInset = inset != default;
var desiredSize = default(Size);
- foreach (var child in VisualChildren)
+ var children = VisualChildren;
+ var childrenCount = children.Count;
+
+ for (var i = 0; i < childrenCount; i++)
{
- if (child is Layoutable l)
+ if (children[i] is Layoutable l)
{
- if (hasInset && ReferenceEquals(child, _topLevel))
+ if (hasInset && ReferenceEquals(l, _topLevel))
{
// In forced mode, measure the TopLevel with reduced size
var contentSize = new Size(
@@ -89,11 +92,14 @@ internal partial class TopLevelHost : Control
var inset = _decorationInset;
var hasInset = inset != default;
- foreach (var child in VisualChildren)
+ var children = VisualChildren;
+ var childrenCount = children.Count;
+
+ for (var i = 0; i < childrenCount; i++)
{
- if (child is Layoutable l)
+ if (children[i] is Layoutable l)
{
- if (hasInset && ReferenceEquals(child, _topLevel))
+ if (hasInset && ReferenceEquals(l, _topLevel))
{
// In forced mode, arrange the TopLevel within the inset area
var contentSize = new Size(
diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
index 63cc2db193..3b2386b1d5 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
@@ -710,6 +710,35 @@ namespace Avalonia.Controls.UnitTests
Assert.False(visualRoot.HasMirrorTransform);
}
+ [Fact]
+ public void Extending_Client_Area_To_Decorations_When_Attached_To_Visual_Tree_Works()
+ {
+ var extended = false;
+
+ var windowImpl = MockWindowingPlatform.CreateWindowMock();
+ windowImpl.Setup(w => w.NeedsManagedDecorations).Returns(() => extended);
+ windowImpl.Setup(w => w.RequestedDrawnDecorations).Returns(PlatformRequestedDrawnDecoration.TitleBar);
+
+ using var app = UnitTestApplication.Start(TestServices.StyledWindow.With(
+ windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)));
+
+ var border = new Border();
+
+ var window = new Window
+ {
+ Content = border
+ };
+
+ border.AttachedToVisualTree +=
+ (_, _) =>
+ {
+ extended = true;
+ windowImpl.Object.ExtendClientAreaToDecorationsChanged?.Invoke(true);
+ };
+
+ window.Show();
+ }
+
public class SizingTests : ScopedTestBase
{
[Fact]
From 412dca3aaeff60515e003b2842b88070f98719bb Mon Sep 17 00:00:00 2001
From: Nikita Tsukanov
Date: Tue, 31 Mar 2026 16:49:29 +0500
Subject: [PATCH 36/57] Expose allowed titlebar button actions from the
platform implementation and respect them from drawn decorations (#21035)
* Expose WM window action capabilities from X11.
* Hide titlebar buttons for unsupported actions
* Simplify subscription
* Explose NetSupported property from X11Globals.cs, so it's easier to make checks from the rest of the codebase
* shifts
* comma
---
.../Chrome/WindowDrawnDecorations.cs | 53 +++++++++++++++----
src/Avalonia.Controls/Platform/IWindowImpl.cs | 10 ++++
.../Platform/PlatformAllowedWindowActions.cs | 33 ++++++++++++
src/Avalonia.Controls/Window.cs | 16 ++++++
.../Controls/WindowDrawnDecorations.xaml | 14 +++++
.../Controls/WindowDrawnDecorations.xaml | 14 +++++
src/Avalonia.X11/X11Atoms.cs | 4 ++
src/Avalonia.X11/X11Globals.cs | 25 +++++++--
src/Avalonia.X11/X11Window.cs | 30 +++++++++++
.../X11WindowModes/DefaultWindowMode.cs | 3 +-
10 files changed, 185 insertions(+), 17 deletions(-)
create mode 100644 src/Avalonia.Controls/Platform/PlatformAllowedWindowActions.cs
diff --git a/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs b/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
index 7a282bf306..26dffa8f5d 100644
--- a/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
+++ b/src/Avalonia.Controls/Chrome/WindowDrawnDecorations.cs
@@ -1,6 +1,7 @@
using System;
using Avalonia.Automation;
using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.LogicalTree;
@@ -15,7 +16,8 @@ namespace Avalonia.Controls.Chrome;
/// TopLevelHost extracts overlay/underlay/popover visuals from the template content
/// and inserts them into its own visual tree.
///
-[PseudoClasses(pcNormal, pcMaximized, pcFullscreen, pcHasShadow, pcHasBorder, pcHasTitlebar)]
+[PseudoClasses(pcNormal, pcMaximized, pcFullscreen, pcHasShadow, pcHasBorder, pcHasTitlebar,
+ pcHasMaximize, pcHasFullscreen, pcHasMinimize)]
[TemplatePart(PART_CloseButton, typeof(Button))]
[TemplatePart(PART_MinimizeButton, typeof(Button))]
[TemplatePart(PART_MaximizeButton, typeof(Button))]
@@ -31,6 +33,9 @@ public class WindowDrawnDecorations : StyledElement
internal const string pcHasShadow = ":has-shadow";
internal const string pcHasBorder = ":has-border";
internal const string pcHasTitlebar = ":has-titlebar";
+ internal const string pcHasMaximize = ":has-maximize";
+ internal const string pcHasFullscreen = ":has-fullscreen";
+ internal const string pcHasMinimize = ":has-minimize";
// Template part names for caption buttons
internal const string PART_CloseButton = "PART_CloseButton";
@@ -383,8 +388,11 @@ public class WindowDrawnDecorations : StyledElement
Detach();
_hostWindow = window;
+ window.AllowedWindowActionsChanged += OnAllowedWindowActionsChanged;
+
_windowSubscriptions = new CompositeDisposable
{
+ Disposable.Create(() => window.AllowedWindowActionsChanged -= OnAllowedWindowActionsChanged),
window.GetObservable(Window.TitleProperty)
.Subscribe(title => SetCurrentValue(TitleProperty, title)),
window.GetObservable(Window.CanMaximizeProperty)
@@ -407,6 +415,7 @@ public class WindowDrawnDecorations : StyledElement
}),
};
+ UpdateAllowedActionsPseudoClasses();
UpdateMaximizeButtonState();
UpdateMinimizeButtonState();
UpdateFullScreenButtonState();
@@ -547,32 +556,54 @@ public class WindowDrawnDecorations : StyledElement
e.Handled = true;
}
+ private PlatformAllowedWindowActions EffectiveAllowedActions =>
+ _hostWindow?.AllowedWindowActions ?? PlatformAllowedWindowActions.All;
+
private void UpdateMaximizeButtonState()
{
if (_maximizeButton == null)
return;
- _maximizeButton.IsEnabled = _hostWindow?.WindowState switch
- {
- WindowState.Maximized or WindowState.FullScreen => _hostWindow.CanResize,
- WindowState.Normal => _hostWindow.CanMaximize,
- _ => true
- };
+ _maximizeButton.IsEnabled = EffectiveAllowedActions.HasFlag(PlatformAllowedWindowActions.Maximize)
+ && (_hostWindow?.WindowState switch
+ {
+ WindowState.Maximized or WindowState.FullScreen => _hostWindow.CanResize,
+ WindowState.Normal => _hostWindow.CanMaximize,
+ _ => true
+ });
}
private void UpdateMinimizeButtonState()
{
if (_minimizeButton == null)
return;
- _minimizeButton.IsEnabled = _hostWindow?.CanMinimize ?? true;
+ _minimizeButton.IsEnabled = EffectiveAllowedActions.HasFlag(PlatformAllowedWindowActions.Minimize)
+ && (_hostWindow?.CanMinimize ?? true);
}
private void UpdateFullScreenButtonState()
{
if (_fullScreenButton == null)
return;
- _fullScreenButton.IsEnabled = _hostWindow?.WindowState == WindowState.FullScreen
- ? _hostWindow.CanResize
- : _hostWindow?.CanMaximize ?? true;
+ _fullScreenButton.IsEnabled = EffectiveAllowedActions.HasFlag(PlatformAllowedWindowActions.Fullscreen)
+ && (_hostWindow?.WindowState == WindowState.FullScreen
+ ? _hostWindow.CanResize
+ : _hostWindow?.CanMaximize ?? true);
+ }
+
+ private void OnAllowedWindowActionsChanged(PlatformAllowedWindowActions actions)
+ {
+ UpdateAllowedActionsPseudoClasses();
+ UpdateMaximizeButtonState();
+ UpdateMinimizeButtonState();
+ UpdateFullScreenButtonState();
+ }
+
+ private void UpdateAllowedActionsPseudoClasses()
+ {
+ var actions = EffectiveAllowedActions;
+ PseudoClasses.Set(pcHasMaximize, actions.HasFlag(PlatformAllowedWindowActions.Maximize));
+ PseudoClasses.Set(pcHasFullscreen, actions.HasFlag(PlatformAllowedWindowActions.Fullscreen));
+ PseudoClasses.Set(pcHasMinimize, actions.HasFlag(PlatformAllowedWindowActions.Minimize));
}
private void UpdateEffectiveGeometry()
diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs
index 42be775f0d..d864ca35c3 100644
--- a/src/Avalonia.Controls/Platform/IWindowImpl.cs
+++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs
@@ -162,5 +162,15 @@ namespace Avalonia.Platform
///
/// -1 for platform default, otherwise the height in DIPs.
void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight);
+
+ ///
+ /// Gets the window actions that the underlying platform currently allows.
+ ///
+ PlatformAllowedWindowActions AllowedWindowActions => PlatformAllowedWindowActions.All;
+
+ ///
+ /// Gets or sets a callback invoked when changes.
+ ///
+ Action? AllowedWindowActionsChanged { get => null; set { } }
}
}
diff --git a/src/Avalonia.Controls/Platform/PlatformAllowedWindowActions.cs b/src/Avalonia.Controls/Platform/PlatformAllowedWindowActions.cs
new file mode 100644
index 0000000000..35871c0803
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/PlatformAllowedWindowActions.cs
@@ -0,0 +1,33 @@
+using System;
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls.Platform;
+
+///
+/// Flags indicating which window actions the underlying platform supports.
+///
+[Flags, PrivateApi]
+public enum PlatformAllowedWindowActions
+{
+ None = 0,
+
+ ///
+ /// The underlying platform supports maximizing/unmaximizing windows.
+ ///
+ Maximize = 1 << 0,
+
+ ///
+ /// The underlying platform supports fullscreen mode.
+ ///
+ Fullscreen = 1 << 1,
+
+ ///
+ /// The underlying platform supports minimizing windows.
+ ///
+ Minimize = 1 << 2,
+
+ ///
+ /// All actions are supported (default when the underlying platform does not report capabilities).
+ ///
+ All = Maximize | Fullscreen | Minimize,
+}
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index 4dab3574eb..0797f39733 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -220,6 +220,7 @@ namespace Avalonia.Controls
private bool _positionWasSet;
private bool _wasShownBefore;
private IDisposable? _modalSubscription;
+ private PlatformAllowedWindowActions _allowedWindowActions = PlatformAllowedWindowActions.All;
///
/// Initializes static members of the class.
@@ -250,6 +251,8 @@ namespace Avalonia.Controls
impl.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;
+ impl.AllowedWindowActionsChanged = OnAllowedWindowActionsChanged;
+ _allowedWindowActions = impl.AllowedWindowActions;
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x =>
{
ResizePlatformImpl(x, WindowResizeReason.Application);
@@ -485,6 +488,11 @@ namespace Avalonia.Controls
set => SetValue(CanMaximizeProperty, value);
}
+ ///
+ /// Gets the window actions currently allowed by the underlying platform.
+ ///
+ internal PlatformAllowedWindowActions AllowedWindowActions => _allowedWindowActions;
+
///
/// Gets or sets the icon of the window.
///
@@ -673,6 +681,14 @@ namespace Avalonia.Controls
UpdateDrawnDecorationParts();
}
+ internal event Action? AllowedWindowActionsChanged;
+
+ private void OnAllowedWindowActionsChanged(PlatformAllowedWindowActions actions)
+ {
+ _allowedWindowActions = actions;
+ AllowedWindowActionsChanged?.Invoke(actions);
+ }
+
private void ExtendClientAreaToDecorationsChanged(bool isExtended)
{
IsExtendedIntoWindowDecorations = isExtended;
diff --git a/src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml b/src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml
index b2d66fdbbd..3ccda42ce3 100644
--- a/src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/WindowDrawnDecorations.xaml
@@ -196,6 +196,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Simple/Controls/TextSelectionHandle.xaml b/src/Avalonia.Themes.Simple/Controls/TextSelectionHandle.xaml
index c9ca5ebc63..4d8c4b0d90 100644
--- a/src/Avalonia.Themes.Simple/Controls/TextSelectionHandle.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/TextSelectionHandle.xaml
@@ -1,6 +1,63 @@
- 32
+ 24
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
-
+
+
+
+
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index fa578a7afe..9d7b8a6118 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
@@ -1537,11 +1537,11 @@ namespace Avalonia.Controls.UnitTests
}
[Theory]
- [InlineData(2,4)]
- [InlineData(0,4)]
- [InlineData(2,6)]
- [InlineData(0,6)]
- [InlineData(3,4)]
+ [InlineData(2, 4)]
+ [InlineData(0, 4)]
+ [InlineData(2, 6)]
+ [InlineData(0, 6)]
+ [InlineData(3, 4)]
public void When_Selection_From_Left_To_Right_Pressing_Right_Should_Remove_Selection_Moving_Caret_To_End_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
@@ -1566,11 +1566,11 @@ namespace Avalonia.Controls.UnitTests
}
[Theory]
- [InlineData(2,4)]
- [InlineData(0,4)]
- [InlineData(2,6)]
- [InlineData(0,6)]
- [InlineData(3,4)]
+ [InlineData(2, 4)]
+ [InlineData(0, 4)]
+ [InlineData(2, 6)]
+ [InlineData(0, 6)]
+ [InlineData(3, 4)]
public void When_Selection_From_Left_To_Right_Pressing_Left_Should_Remove_Selection_Moving_Caret_To_Start_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
@@ -1595,11 +1595,11 @@ namespace Avalonia.Controls.UnitTests
}
[Theory]
- [InlineData(4,2)]
- [InlineData(4,0)]
- [InlineData(6,2)]
- [InlineData(6,0)]
- [InlineData(4,3)]
+ [InlineData(4, 2)]
+ [InlineData(4, 0)]
+ [InlineData(6, 2)]
+ [InlineData(6, 0)]
+ [InlineData(4, 3)]
public void When_Selection_From_Right_To_Left_Pressing_Right_Should_Remove_Selection_Moving_Caret_To_Start_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
@@ -1624,11 +1624,11 @@ namespace Avalonia.Controls.UnitTests
}
[Theory]
- [InlineData(4,2)]
- [InlineData(4,0)]
- [InlineData(6,2)]
- [InlineData(6,0)]
- [InlineData(4,3)]
+ [InlineData(4, 2)]
+ [InlineData(4, 0)]
+ [InlineData(6, 2)]
+ [InlineData(6, 0)]
+ [InlineData(4, 3)]
public void When_Selection_From_Right_To_Left_Pressing_Left_Should_Remove_Selection_Moving_Caret_To_End_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
@@ -1707,11 +1707,11 @@ namespace Avalonia.Controls.UnitTests
}
[Theory]
- [InlineData(2,4)]
- [InlineData(0,4)]
- [InlineData(2,6)]
- [InlineData(0,6)]
- [InlineData(3,4)]
+ [InlineData(2, 4)]
+ [InlineData(0, 4)]
+ [InlineData(2, 6)]
+ [InlineData(0, 6)]
+ [InlineData(3, 4)]
public void When_Selection_From_Left_To_Right_Pressing_Up_Should_Remove_Selection_Moving_Caret_To_Start_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
@@ -1736,11 +1736,11 @@ namespace Avalonia.Controls.UnitTests
}
[Theory]
- [InlineData(4,2)]
- [InlineData(4,0)]
- [InlineData(6,2)]
- [InlineData(6,0)]
- [InlineData(4,3)]
+ [InlineData(4, 2)]
+ [InlineData(4, 0)]
+ [InlineData(6, 2)]
+ [InlineData(6, 0)]
+ [InlineData(4, 3)]
public void When_Selection_From_Right_To_Left_Pressing_Up_Should_Remove_Selection_Moving_Caret_To_End_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
@@ -1792,11 +1792,11 @@ namespace Avalonia.Controls.UnitTests
}
[Theory]
- [InlineData(2,4)]
- [InlineData(0,4)]
- [InlineData(2,6)]
- [InlineData(0,6)]
- [InlineData(3,4)]
+ [InlineData(2, 4)]
+ [InlineData(0, 4)]
+ [InlineData(2, 6)]
+ [InlineData(0, 6)]
+ [InlineData(3, 4)]
public void When_Selection_From_Left_To_Right_Pressing_Down_Should_Remove_Selection_Moving_Caret_To_End_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
@@ -1821,11 +1821,11 @@ namespace Avalonia.Controls.UnitTests
}
[Theory]
- [InlineData(4,2)]
- [InlineData(4,0)]
- [InlineData(6,2)]
- [InlineData(6,0)]
- [InlineData(4,3)]
+ [InlineData(4, 2)]
+ [InlineData(4, 0)]
+ [InlineData(6, 2)]
+ [InlineData(6, 0)]
+ [InlineData(4, 3)]
public void When_Selection_From_Right_To_Left_Pressing_Down_Should_Remove_Selection_Moving_Caret_To_Start_Of_Previous_Selection(int selectionStart, int selectionEnd)
{
using (UnitTestApplication.Start(Services))
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_Input.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_Input.cs
new file mode 100644
index 0000000000..e19bc5ca9a
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_Input.cs
@@ -0,0 +1,278 @@
+#nullable enable
+
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
+using Avalonia.Harfbuzz;
+using Avalonia.Headless;
+using Avalonia.Input;
+using Avalonia.Input.Platform;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Avalonia.UnitTests;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+ public class TextBoxTests_Input : ScopedTestBase
+ {
+ [Fact]
+ public void Touch_Tap_Moves_Caret()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ var target = new TextBox
+ {
+ Template = CreateTemplate(),
+ Text = "12 12345678"
+ };
+
+ var root = new TestRoot()
+ {
+ Child = target
+ };
+
+ target.ApplyTemplate();
+
+ root.LayoutManager.ExecuteInitialLayoutPass();
+
+ var touch = new TouchTestHelper();
+
+ Assert.Equal(target.CaretIndex, 0);
+
+ // Move to index 8
+ touch.Down(target, new Point(50, 0));
+ touch.Up(target, new Point(50, 0));
+
+ Assert.Equal(target.CaretIndex, 8);
+ }
+ }
+
+ [Fact]
+ public void Touch_Double_Tap_Selects_Word()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ var target = new TextBox
+ {
+ Template = CreateTemplate(),
+ Text = "12 12345678"
+ };
+
+ var root = new TestRoot()
+ {
+ Child = target
+ };
+
+ target.ApplyTemplate();
+
+
+ root.LayoutManager.ExecuteInitialLayoutPass();
+
+ var touch = new TouchTestHelper();
+
+ Assert.Equal(target.CaretIndex, 0);
+
+ // Move to index 8
+ touch.Down(target, new Point(50, 0));
+ touch.Up(target, new Point(50, 0));
+
+ // Double tap
+ touch.Down(target, new Point(50, 0));
+ touch.Up(target, new Point(50, 0));
+
+ Assert.Equal(target.SelectionStart, 3);
+ Assert.Equal(target.SelectionEnd, 11);
+ Assert.Equal(target.CaretIndex, 8);
+ }
+ }
+
+ [Fact]
+ public void Touch_Hold_Selects_Word()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ var target = new TextBox
+ {
+ Template = CreateTemplate(),
+ Text = "12 12345678"
+ };
+
+ var root = new TestRoot()
+ {
+ Child = target
+ };
+
+ target.ApplyTemplate();
+
+ root.LayoutManager.ExecuteInitialLayoutPass();
+
+ var touch = new TouchTestHelper();
+
+ Assert.Equal(target.CaretIndex, 0);
+
+ // Move to index 8
+ touch.Down(target, new Point(50, 0));
+
+ var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests());
+ timer.ForceFire();
+ touch.Up(target, new Point(50, 0));
+
+ Assert.Equal(target.SelectionStart, 3);
+ Assert.Equal(target.SelectionEnd, 11);
+ Assert.Equal(target.CaretIndex, 8);
+ }
+ }
+
+ [Fact]
+ public void Touch_Hold_On_Selection_Requests_Context()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ var target = new TextBox
+ {
+ Template = CreateTemplate(),
+ Text = "12 12345678"
+ };
+
+ var root = new TestRoot()
+ {
+ Child = target
+ };
+
+ target.ApplyTemplate();
+ bool requested = false;
+
+ target.SelectionStart = 3;
+ target.SelectionEnd = 11;
+
+ target.ContextRequested += Target_ContextRequested;
+
+ root.LayoutManager.ExecuteInitialLayoutPass();
+
+ var touch = new TouchTestHelper();
+
+ Assert.Equal(target.CaretIndex, 0);
+
+ // Move to index 8
+ touch.Down(target, new Point(50, 0));
+
+ var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests());
+ timer.ForceFire();
+ touch.Up(target, new Point(50, 0));
+
+ Assert.True(requested);
+
+ void Target_ContextRequested(object? sender, ContextRequestedEventArgs e)
+ {
+ requested = true;
+ }
+ }
+ }
+
+ private static TestServices Services => TestServices.MockThreadingInterface.With(
+ standardCursorFactory: Mock.Of(),
+ renderInterface: new HeadlessPlatformRenderInterface(),
+ textShaperImpl: new HarfBuzzTextShaper(),
+ fontManagerImpl: new TestFontManager(),
+ assetLoader: new StandardAssetLoader());
+
+ internal static IControlTemplate CreateTemplate()
+ {
+ return new FuncControlTemplate((control, scope) =>
+ new ScrollViewer
+ {
+ Name = "PART_ScrollViewer",
+ Template = new FuncControlTemplate(ScrollViewerTests.CreateTemplate),
+ Content = new TextPresenter
+ {
+ Name = "PART_TextPresenter",
+ [!!TextPresenter.TextProperty] = new Binding
+ {
+ Path = nameof(TextPresenter.Text),
+ Mode = BindingMode.TwoWay,
+ Priority = BindingPriority.Template,
+ RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
+ },
+ [!!TextPresenter.CaretIndexProperty] = new Binding
+ {
+ Path = nameof(TextPresenter.CaretIndex),
+ Mode = BindingMode.TwoWay,
+ Priority = BindingPriority.Template,
+ RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
+ }
+ }.RegisterInNameScope(scope)
+ }.RegisterInNameScope(scope));
+ }
+
+ private static void RaiseKeyEvent(TextBox textBox, Key key, KeyModifiers inputModifiers)
+ {
+ textBox.RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = InputElement.KeyDownEvent,
+ KeyModifiers = inputModifiers,
+ Key = key
+ });
+ }
+
+ private static void RaiseTextEvent(TextBox textBox, string text)
+ {
+ textBox.RaiseEvent(new TextInputEventArgs
+ {
+ RoutedEvent = InputElement.TextInputEvent,
+ Text = text
+ });
+ }
+
+ private class Class1 : NotifyingBase
+ {
+ private int _foo;
+ private string? _bar;
+
+ public int Foo
+ {
+ get { return _foo; }
+ set { _foo = value; RaisePropertyChanged(); }
+ }
+
+ public string? Bar
+ {
+ get { return _bar; }
+ set { _bar = value; RaisePropertyChanged(); }
+ }
+ }
+
+ private class TestTopLevel(ITopLevelImpl impl) : TopLevel(impl)
+ {
+ }
+
+ private static Mock CreateMockTopLevelImpl()
+ {
+ var clipboard = new Mock();
+ clipboard.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
+ clipboard.Setup(r => r.TryGetFeature(typeof(IClipboard)))
+ .Returns(new Clipboard(new HeadlessClipboardImplStub()));
+ clipboard.SetupGet(x => x.RenderScaling).Returns(1);
+ return clipboard;
+ }
+
+ private static FuncControlTemplate CreateTopLevelTemplate()
+ {
+ return new FuncControlTemplate((x, scope) =>
+ new ContentPresenter
+ {
+ Name = "PART_ContentPresenter",
+ [!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty],
+ }.RegisterInNameScope(scope));
+ }
+
+ private class TestContextMenu : ContextMenu
+ {
+ public TestContextMenu()
+ {
+ IsOpen = true;
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/TouchTestHelper.cs b/tests/Avalonia.UnitTests/TouchTestHelper.cs
index 3ec49fa321..7a13425997 100644
--- a/tests/Avalonia.UnitTests/TouchTestHelper.cs
+++ b/tests/Avalonia.UnitTests/TouchTestHelper.cs
@@ -7,6 +7,7 @@ namespace Avalonia.UnitTests
{
private readonly Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Touch, true);
private ulong _nextStamp = 1;
+ private int _clickCount = 0;
private ulong Timestamp() => _nextStamp++;
public IInputElement? Captured => _pointer.Captured;
@@ -20,7 +21,7 @@ namespace Avalonia.UnitTests
_pointer.Capture((IInputElement)target);
source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (Visual)source, position, Timestamp(),
new(RawInputModifiers.LeftMouseButton, PointerUpdateKind.LeftButtonPressed),
- modifiers));
+ modifiers, ++_clickCount));
}
public void Move(Interactive target, in Point position, KeyModifiers modifiers = default) => Move(target, target, position, modifiers);
From 5d5fce8d9b1330a07cfd2ad0739180c403a91eba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Javier=20Su=C3=A1rez?=
Date: Tue, 31 Mar 2026 16:54:40 +0200
Subject: [PATCH 41/57] Add keyboard navigation for overflow CommandBar popup
(#21005)
* Fix keyboard navigation in CommandBar overflow
* More fixes
* More changes
* More changes, added more tests
---
.../CommandBar/CommandBarKeyboardPage.xaml | 121 ++++++++++
.../CommandBar/CommandBarKeyboardPage.xaml.cs | 93 ++++++++
.../Pages/CommandBarPage.xaml.cs | 1 +
.../CommandBar/CommandBar.cs | 128 +++++++++-
.../Controls/CommandBar.xaml | 5 +-
.../Controls/CommandBar.xaml | 5 +-
.../CommandBarTests.cs | 223 ++++++++++++++++++
7 files changed, 569 insertions(+), 7 deletions(-)
create mode 100644 samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml
create mode 100644 samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml.cs
diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml
new file mode 100644
index 0000000000..7b820b0183
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml
@@ -0,0 +1,121 @@
+
+
+ M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z
+ M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z
+ M15.6,10.79C17.04,10.07 18,8.64 18,7C18,4.79 16.21,3 14,3H7V21H14.73C16.78,21 18.5,19.37 18.5,17.32C18.5,15.82 17.72,14.53 16.5,13.77C16.2,13.59 15.9,13.44 15.6,13.32V10.79M10,6.5H13C13.83,6.5 14.5,7.17 14.5,8C14.5,8.83 13.83,9.5 13,9.5H10V6.5M13.5,17.5H10V14H13.5C14.33,14 15,14.67 15,15.5C15,16.33 14.33,17.5 13.5,17.5Z
+ M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z
+ M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Keyboard navigation in the overflow popup: Up/Down to move, Home/End to jump, Escape to close.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml.cs
new file mode 100644
index 0000000000..013614db7f
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace ControlCatalog.Pages
+{
+ public partial class CommandBarKeyboardPage : UserControl
+ {
+ private readonly List _log = new();
+
+ public CommandBarKeyboardPage()
+ {
+ InitializeComponent();
+ Loaded += OnLoaded;
+ Unloaded += OnUnloaded;
+ }
+
+ private void OnLoaded(object? sender, RoutedEventArgs e)
+ {
+ DemoBar.Opened += OnOpened;
+ DemoBar.Closed += OnClosed;
+
+ BtnCopy.GotFocus += OnItemFocused;
+ BtnPaste.GotFocus += OnItemFocused;
+ BtnBold.GotFocus += OnItemFocused;
+ BtnShare.GotFocus += OnItemFocused;
+ BtnDelete.GotFocus += OnItemFocused;
+ BtnExport.GotFocus += OnItemFocused;
+ }
+
+ private void OnUnloaded(object? sender, RoutedEventArgs e)
+ {
+ DemoBar.Opened -= OnOpened;
+ DemoBar.Closed -= OnClosed;
+
+ BtnCopy.GotFocus -= OnItemFocused;
+ BtnPaste.GotFocus -= OnItemFocused;
+ BtnBold.GotFocus -= OnItemFocused;
+ BtnShare.GotFocus -= OnItemFocused;
+ BtnDelete.GotFocus -= OnItemFocused;
+ BtnExport.GotFocus -= OnItemFocused;
+ }
+
+ private void OnOpened(object? sender, RoutedEventArgs e)
+ => AppendLog("Opened. Use arrow keys to navigate.");
+
+ private void OnClosed(object? sender, RoutedEventArgs e)
+ => AppendLog("Closed");
+
+ private void OnItemFocused(object? sender, FocusChangedEventArgs e)
+ {
+ var label = sender switch
+ {
+ CommandBarButton btn => btn.Label ?? "(unnamed)",
+ CommandBarToggleButton t => t.Label ?? "(unnamed)",
+ _ => sender?.GetType().Name ?? "?"
+ };
+
+ var method = e.NavigationMethod switch
+ {
+ NavigationMethod.Directional => "arrow key",
+ NavigationMethod.Tab => "Tab",
+ NavigationMethod.Pointer => "pointer",
+ _ => "unspecified"
+ };
+
+ AppendLog($"Focus: {label} ({method})");
+ }
+
+ private void OnOpenOverflow(object? sender, RoutedEventArgs e)
+ {
+ DemoBar.IsOpen = true;
+ }
+
+ private void OnClearLog(object? sender, RoutedEventArgs e)
+ {
+ _log.Clear();
+ FocusLogText.Text = "Log cleared.";
+ }
+
+ private void AppendLog(string message)
+ {
+ _log.Add(message);
+
+ if (_log.Count > 10)
+ _log.RemoveAt(0);
+
+ FocusLogText.Text = string.Join("\n", _log.Select((entry, i) => $"{i + 1,2}. {entry}"));
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/CommandBarPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBarPage.xaml.cs
index e76d605645..b759c7b605 100644
--- a/samples/ControlCatalog/Pages/CommandBarPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CommandBarPage.xaml.cs
@@ -23,6 +23,7 @@ namespace ControlCatalog.Pages
("Features", "Overflow Menu", "Secondary commands appear in an overflow popup. Configure visibility and sticky behavior.", () => new CommandBarOverflowPage()),
("Features", "Dynamic Overflow", "IsDynamicOverflowEnabled moves primary commands to overflow as space shrinks.", () => new CommandBarDynamicOverflowPage()),
("Features", "Events & State", "Observe Opening, Opened, Closing, and Closed while tracking IsOpen, HasSecondaryCommands, and IsOverflowButtonVisible.", () => new CommandBarEventsPage()),
+ ("Features", "Keyboard Navigation", "Up/Down to move between overflow items, Home/End to jump to first/last, Escape to close and return focus.", () => new CommandBarKeyboardPage()),
};
public CommandBarPage()
diff --git a/src/Avalonia.Controls/CommandBar/CommandBar.cs b/src/Avalonia.Controls/CommandBar/CommandBar.cs
index d493bce304..d8047f9d9e 100644
--- a/src/Avalonia.Controls/CommandBar/CommandBar.cs
+++ b/src/Avalonia.Controls/CommandBar/CommandBar.cs
@@ -7,6 +7,7 @@ using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Metadata;
+using Avalonia.Threading;
namespace Avalonia.Controls
{
@@ -14,9 +15,10 @@ namespace Avalonia.Controls
/// A command bar that provides primary commands displayed inline and secondary commands
/// accessible via an overflow menu.
///
- [TemplatePart("PART_OverflowButton", typeof(Button))]
+ [TemplatePart("PART_OverflowButton", typeof(Button))]
[TemplatePart("PART_OverflowPopup", typeof(Popup))]
- [TemplatePart("PART_ContentPresenter", typeof(Control))]
+ [TemplatePart("PART_OverflowPresenter", typeof(ItemsControl))]
+ [TemplatePart("PART_ContentPresenter", typeof(Control))]
public class CommandBar : TemplatedControl
{
///
@@ -129,12 +131,14 @@ namespace Avalonia.Controls
private Button? _overflowButton;
private Popup? _overflowPopup;
+ private ItemsControl? _overflowPresenter;
private Control? _contentPresenter;
private readonly ObservableCollection _visiblePrimaryCommands = new();
private readonly ObservableCollection _overflowItems = new();
private bool _isDynamicUpdateInProgress;
private double _constraintWidth = double.PositiveInfinity;
+ private bool _openedViaKeyboard;
public CommandBar()
{
@@ -348,14 +352,33 @@ namespace Avalonia.Controls
base.OnApplyTemplate(e);
if (_overflowButton != null)
+ {
_overflowButton.Click -= OnOverflowButtonClick;
+ _overflowButton.GotFocus -= OnOverflowButtonGotFocus;
+ _overflowButton.RemoveHandler(Input.InputElement.KeyDownEvent, OnOverflowButtonKeyDown);
+ _overflowButton.RemoveHandler(Input.InputElement.PointerPressedEvent, OnOverflowButtonPointerPressed);
+ }
+ if (_overflowPresenter != null)
+ _overflowPresenter.KeyDown -= OnOverflowPresenterKeyDown;
+ if (_overflowPopup != null)
+ _overflowPopup.Opened -= OnOverflowPopupOpened;
_overflowButton = e.NameScope.Find("PART_OverflowButton");
_overflowPopup = e.NameScope.Find("PART_OverflowPopup");
+ _overflowPresenter = e.NameScope.Find("PART_OverflowPresenter");
_contentPresenter = e.NameScope.Find("PART_ContentPresenter");
if (_overflowButton != null)
+ {
_overflowButton.Click += OnOverflowButtonClick;
+ _overflowButton.GotFocus += OnOverflowButtonGotFocus;
+ _overflowButton.AddHandler(Input.InputElement.KeyDownEvent, OnOverflowButtonKeyDown, handledEventsToo: true);
+ _overflowButton.AddHandler(Input.InputElement.PointerPressedEvent, OnOverflowButtonPointerPressed, handledEventsToo: true);
+ }
+ if (_overflowPresenter != null)
+ _overflowPresenter.KeyDown += OnOverflowPresenterKeyDown;
+ if (_overflowPopup != null)
+ _overflowPopup.Opened += OnOverflowPopupOpened;
ApplyLabelPositionToChildren();
UpdateOverflowButtonVisibility();
@@ -369,7 +392,7 @@ namespace Avalonia.Controls
if (change.Property == IsOpenProperty)
{
- var isOpen = (bool)change.NewValue!;
+ var isOpen = change.GetNewValue();
if (isOpen)
{
RaiseEvent(new RoutedEventArgs(OpeningEvent));
@@ -446,6 +469,105 @@ namespace Avalonia.Controls
SetCurrentValue(IsOpenProperty, !IsOpen);
}
+ private void OnOverflowButtonGotFocus(object? sender, Input.FocusChangedEventArgs e)
+ {
+ _openedViaKeyboard = e.NavigationMethod is Input.NavigationMethod.Directional
+ or Input.NavigationMethod.Tab;
+ }
+
+ private void OnOverflowButtonKeyDown(object? sender, Input.KeyEventArgs e)
+ {
+ if (e.Key is Input.Key.Enter or Input.Key.Space)
+ _openedViaKeyboard = true;
+ }
+
+ private void OnOverflowButtonPointerPressed(object? sender, Input.PointerPressedEventArgs e)
+ {
+ _openedViaKeyboard = false;
+ }
+
+ private void OnOverflowPresenterKeyDown(object? sender, Input.KeyEventArgs e)
+ {
+ switch (e.Key)
+ {
+ case Input.Key.Up:
+ NavigateOverflow(forward: false);
+ e.Handled = true;
+ break;
+ case Input.Key.Down:
+ NavigateOverflow(forward: true);
+ e.Handled = true;
+ break;
+ case Input.Key.Home:
+ FocusOverflowItem(first: true);
+ e.Handled = true;
+ break;
+ case Input.Key.End:
+ FocusOverflowItem(first: false);
+ e.Handled = true;
+ break;
+ case Input.Key.Escape:
+ SetCurrentValue(IsOpenProperty, false);
+ _overflowButton?.Focus(Input.NavigationMethod.Unspecified);
+ e.Handled = true;
+ break;
+ }
+ }
+
+ private void OnOverflowPopupOpened(object? sender, EventArgs e)
+ {
+ var method = _openedViaKeyboard
+ ? Input.NavigationMethod.Directional
+ : Input.NavigationMethod.Pointer;
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (IsOpen)
+ FocusOverflowItem(first: true, method);
+ }, DispatcherPriority.Loaded);
+ }
+
+ private void NavigateOverflow(bool forward)
+ {
+ var items = GetFocusableOverflowItems();
+ if (items.Count == 0)
+ return;
+
+ int current = -1;
+ for (int i = 0; i < items.Count; i++)
+ {
+ if (items[i].IsFocused || items[i].IsKeyboardFocusWithin)
+ {
+ current = i;
+ break;
+ }
+ }
+
+ int next = current < 0
+ ? (forward ? 0 : items.Count - 1)
+ : (forward ? (current + 1) % items.Count : (current - 1 + items.Count) % items.Count);
+
+ items[next].Focus(Input.NavigationMethod.Directional);
+ }
+
+ private void FocusOverflowItem(bool first, Input.NavigationMethod method = Input.NavigationMethod.Directional)
+ {
+ var items = GetFocusableOverflowItems();
+ if (items.Count > 0)
+ items[first ? 0 : items.Count - 1].Focus(method);
+ }
+
+ private List GetFocusableOverflowItems()
+ {
+ var result = new List();
+ foreach (var item in _overflowItems)
+ {
+ if (item is Control { IsEnabled: true, IsVisible: true, Focusable: true } control && item is not CommandBarSeparator)
+ result.Add(control);
+ }
+ return result;
+ }
+
private void OnPrimaryCommandsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
diff --git a/src/Avalonia.Themes.Fluent/Controls/CommandBar.xaml b/src/Avalonia.Themes.Fluent/Controls/CommandBar.xaml
index 8692a6153f..b76acab856 100644
--- a/src/Avalonia.Themes.Fluent/Controls/CommandBar.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/CommandBar.xaml
@@ -237,7 +237,7 @@
Padding="{TemplateBinding Padding}"
MinHeight="{TemplateBinding MinHeight}">
-
+
-
+
diff --git a/src/Avalonia.Themes.Simple/Controls/CommandBar.xaml b/src/Avalonia.Themes.Simple/Controls/CommandBar.xaml
index b489a20c4d..dd073c0f38 100644
--- a/src/Avalonia.Themes.Simple/Controls/CommandBar.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/CommandBar.xaml
@@ -234,7 +234,7 @@
Padding="{TemplateBinding Padding}"
MinHeight="{TemplateBinding MinHeight}">
-
+
-
+
diff --git a/tests/Avalonia.Controls.UnitTests/CommandBarTests.cs b/tests/Avalonia.Controls.UnitTests/CommandBarTests.cs
index eabb6e33c1..1950cff16c 100644
--- a/tests/Avalonia.Controls.UnitTests/CommandBarTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/CommandBarTests.cs
@@ -1,7 +1,14 @@
+using System;
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Linq;
using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.LogicalTree;
+using Avalonia.Threading;
using Avalonia.UnitTests;
+using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Controls.UnitTests;
@@ -973,6 +980,222 @@ public class CommandBarItemWidthTests : ScopedTestBase
}
}
+public class CommandBarOverflowKeyboardTests : ScopedTestBase
+{
+ private readonly IDisposable _app;
+
+ public CommandBarOverflowKeyboardTests()
+ {
+ _app = UnitTestApplication.Start(TestServices.FocusableWindow);
+ }
+
+ public override void Dispose()
+ {
+ _app.Dispose();
+ base.Dispose();
+ }
+
+ [Fact]
+ public void Escape_WhenOverflowOpen_ClosesOverflow()
+ {
+ var cb = new CommandBar();
+ cb.SecondaryCommands.Add(new CommandBarButton { Label = "Action" });
+ var root = new TestRoot(useGlobalStyles: true, child: cb);
+ root.LayoutManager.ExecuteInitialLayoutPass();
+ cb.IsOpen = true;
+
+ RaiseKeyOnOverflowPresenter(cb, Key.Escape);
+
+ Assert.False(cb.IsOpen);
+ }
+
+ [Fact]
+ public void Escape_WhenOverflowClosed_DoesNothing()
+ {
+ var cb = new CommandBar();
+ cb.SecondaryCommands.Add(new CommandBarButton { Label = "Action" });
+ var root = new TestRoot(useGlobalStyles: true, child: cb);
+ root.LayoutManager.ExecuteInitialLayoutPass();
+
+ RaiseKeyOnOverflowPresenter(cb, Key.Escape);
+
+ Assert.False(cb.IsOpen);
+ }
+
+ [Theory]
+ [InlineData(Key.Down)]
+ [InlineData(Key.Up)]
+ [InlineData(Key.Home)]
+ [InlineData(Key.End)]
+ public void NavigationKeys_AreHandled_WhenOverflowHasItems(Key key)
+ {
+ var cb = new CommandBar();
+ cb.SecondaryCommands.Add(new CommandBarButton { Label = "A" });
+ cb.SecondaryCommands.Add(new CommandBarButton { Label = "B" });
+ var root = new TestRoot(useGlobalStyles: true, child: cb);
+ root.LayoutManager.ExecuteInitialLayoutPass();
+ cb.IsOpen = true;
+
+ var e = new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = key };
+ GetOverflowPresenter(cb).RaiseEvent(e);
+
+ Assert.True(e.Handled);
+ }
+
+ [Fact]
+ public void NavigationKeys_AreHandled_WhenAllItemsAreSeparators()
+ {
+ var cb = new CommandBar();
+ cb.SecondaryCommands.Add(new CommandBarSeparator());
+ var root = new TestRoot(useGlobalStyles: true, child: cb);
+ root.LayoutManager.ExecuteInitialLayoutPass();
+ cb.IsOpen = true;
+
+ var e = new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.Down };
+ GetOverflowPresenter(cb).RaiseEvent(e);
+
+ Assert.True(e.Handled);
+ }
+
+ [Fact]
+ public void NavigationKeys_AreHandled_WhenAllItemsAreDisabled()
+ {
+ var cb = new CommandBar();
+ cb.SecondaryCommands.Add(new CommandBarButton { Label = "A", IsEnabled = false });
+ var root = new TestRoot(useGlobalStyles: true, child: cb);
+ root.LayoutManager.ExecuteInitialLayoutPass();
+ cb.IsOpen = true;
+
+ var e = new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.Down };
+ GetOverflowPresenter(cb).RaiseEvent(e);
+
+ Assert.True(e.Handled);
+ }
+
+ [Fact]
+ public void NavigationKeys_AreHandled_WhenAllItemsAreNonFocusable()
+ {
+ var cb = new CommandBar();
+ cb.SecondaryCommands.Add(new CommandBarButton { Label = "A", Focusable = false });
+ var root = new TestRoot(useGlobalStyles: true, child: cb);
+ root.LayoutManager.ExecuteInitialLayoutPass();
+ cb.IsOpen = true;
+
+ var e = new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.Down };
+ GetOverflowPresenter(cb).RaiseEvent(e);
+
+ Assert.True(e.Handled);
+ }
+
+ [Fact]
+ public void KeyboardOpen_FocusesFirstOverflowItem_AsFocusVisible()
+ {
+ var first = new CommandBarButton { Label = "A" };
+ var cb = new CommandBar();
+ cb.SecondaryCommands.Add(first);
+ var window = CreateWindow(cb);
+
+ Assert.True(GetOverflowButton(cb).Focus(NavigationMethod.Tab));
+
+ cb.IsOpen = true;
+ Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded, TestContext.Current.CancellationToken);
+
+ Assert.True(first.IsFocused);
+ Assert.True(first.Classes.Contains(":focus-visible"));
+ }
+
+ [Fact]
+ public void PointerOpen_AfterKeyboardFocus_DoesNotMakeFirstOverflowItemFocusVisible()
+ {
+ var first = new CommandBarButton { Label = "A" };
+ var cb = new CommandBar();
+ cb.SecondaryCommands.Add(first);
+ var window = CreateWindow(cb);
+
+ var overflowButton = GetOverflowButton(cb);
+ Assert.True(overflowButton.Focus(NavigationMethod.Tab));
+ RaisePointerPressed(overflowButton);
+
+ cb.IsOpen = true;
+ Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded, TestContext.Current.CancellationToken);
+
+ Assert.True(first.IsFocused);
+ Assert.False(first.Classes.Contains(":focus-visible"));
+ }
+
+ [Fact]
+ public void KeyboardOpen_AfterPointerFocus_MakesFirstOverflowItemFocusVisible()
+ {
+ var first = new CommandBarButton { Label = "A" };
+ var cb = new CommandBar();
+ cb.SecondaryCommands.Add(first);
+ var window = CreateWindow(cb);
+
+ var overflowButton = GetOverflowButton(cb);
+ Assert.True(overflowButton.Focus(NavigationMethod.Pointer));
+ overflowButton.RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = InputElement.KeyDownEvent,
+ Key = Key.Space,
+ });
+
+ cb.IsOpen = true;
+ Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded, TestContext.Current.CancellationToken);
+
+ Assert.True(first.IsFocused);
+ Assert.True(first.Classes.Contains(":focus-visible"));
+ }
+
+ private static ItemsControl GetOverflowPresenter(CommandBar cb)
+ {
+ var popup = cb.GetVisualDescendants()
+ .OfType()
+ .First(p => p.Name == "PART_OverflowPopup");
+
+ return popup.GetLogicalDescendants()
+ .OfType()
+ .First(x => x.Name == "PART_OverflowPresenter");
+ }
+
+ private static void RaiseKeyOnOverflowPresenter(CommandBar cb, Key key)
+ {
+ GetOverflowPresenter(cb).RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = InputElement.KeyDownEvent,
+ Key = key,
+ });
+ }
+
+ private static Button GetOverflowButton(CommandBar cb)
+ {
+ return cb.GetVisualDescendants()
+ .OfType()
+ .First(x => x.Name == "PART_OverflowButton");
+ }
+
+ private static Window CreateWindow(Control content)
+ {
+ var window = new Window { Content = content };
+ window.Show();
+ window.ApplyStyling();
+ window.ApplyTemplate();
+ return window;
+ }
+
+ private static void RaisePointerPressed(Button target)
+ {
+ var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
+ target.RaiseEvent(new PointerPressedEventArgs(
+ target,
+ pointer,
+ target,
+ default,
+ timestamp: 1,
+ new PointerPointProperties(RawInputModifiers.LeftMouseButton, PointerUpdateKind.LeftButtonPressed),
+ KeyModifiers.None));
+ }
+}
+
file sealed class DelegateCommand : System.Windows.Input.ICommand
{
private readonly System.Action _execute;
From 3a5b3e376e5e39d9da61c8e4342c186742e879a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Javier=20Su=C3=A1rez?=
Date: Tue, 31 Mar 2026 19:48:06 +0200
Subject: [PATCH 42/57] Improve CommandBar dynamic overflow separator handling
(#21039)
* Improve CommandBar dynamic overflow separator handling
* More changes
* Updated sample
---------
Co-authored-by: Julien Lebosquain
---
.../CommandBarDynamicOverflowPage.xaml | 28 +-
.../CommandBarDynamicOverflowPage.xaml.cs | 64 +++-
.../CommandBar/CommandBar.cs | 154 ++++++++-
.../CommandBarTests.cs | 311 +++++++++++++++++-
4 files changed, 538 insertions(+), 19 deletions(-)
diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml
index 6d66c19c96..f3098dee0f 100644
--- a/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml
+++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml
@@ -8,6 +8,7 @@
M15.6,10.79C17.04,10.07 18,8.64 18,7C18,4.79 16.21,3 14,3H7V21H14.73C16.78,21 18.5,19.37 18.5,17.32C18.5,15.82 17.72,14.53 16.5,13.77C16.2,13.59 15.9,13.44 15.6,13.32V10.79M10,6.5H13C13.83,6.5 14.5,7.17 14.5,8C14.5,8.83 13.83,9.5 13,9.5H10V6.5M13.5,17.5H10V14H13.5C14.33,14 15,14.67 15,15.5C15,16.33 14.33,17.5 13.5,17.5Z
M10,4V7H12.21L8.79,15H6V18H14V15H11.79L15.21,7H18V4H10Z
M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z
+ M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M16,11V18.1L13.9,16L11.1,18.8L8.3,16L11.1,13.2L9,11.1L16,11Z
@@ -33,6 +34,18 @@
IsChecked="True"
IsCheckedChanged="OnDynamicOverflowChanged" />
+
+
+
+
+
+
@@ -47,11 +60,11 @@
-
-
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs
index 45a1c95527..19a451b55c 100644
--- a/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs
@@ -9,7 +9,13 @@ namespace ControlCatalog.Pages
public CommandBarDynamicOverflowPage()
{
InitializeComponent();
+ if (SecondaryVisibleCheck.IsChecked != true)
+ {
+ DemoBar.SecondaryCommands?.Remove(DemoSecondaryCommand);
+ }
+
((INotifyCollectionChanged)DemoBar.OverflowItems).CollectionChanged += OnOverflowChanged;
+ ((INotifyCollectionChanged)DemoBar.VisiblePrimaryCommands).CollectionChanged += OnOverflowChanged;
UpdateStatus();
}
@@ -29,6 +35,26 @@ namespace ControlCatalog.Pages
DemoBar.IsDynamicOverflowEnabled = DynamicOverflowCheck.IsChecked == true;
}
+ private void OnSecondaryVisibilityChanged(object? sender, RoutedEventArgs e)
+ {
+ if (DemoBar?.SecondaryCommands == null || DemoSecondaryCommand == null)
+ return;
+
+ bool shouldInclude = SecondaryVisibleCheck.IsChecked == true;
+ bool isIncluded = DemoBar.SecondaryCommands.Contains(DemoSecondaryCommand);
+
+ if (shouldInclude && !isIncluded)
+ {
+ DemoBar.SecondaryCommands.Add(DemoSecondaryCommand);
+ }
+ else if (!shouldInclude && isIncluded)
+ {
+ DemoBar.SecondaryCommands.Remove(DemoSecondaryCommand);
+ }
+
+ UpdateStatus();
+ }
+
private void OnOverflowChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateStatus();
@@ -36,10 +62,40 @@ namespace ControlCatalog.Pages
private void UpdateStatus()
{
- var total = DemoBar.PrimaryCommands.Count;
- var overflow = DemoBar.OverflowItems.Count;
- var visible = total - overflow;
- StatusText.Text = $"Showing {visible} of {total} commands, {overflow} in overflow";
+ int visiblePrimaryCommandCount = 0;
+ int visiblePrimarySeparatorCount = 0;
+ foreach (var item in DemoBar.VisiblePrimaryCommands)
+ {
+ if (item is CommandBarSeparator)
+ visiblePrimarySeparatorCount++;
+ else
+ visiblePrimaryCommandCount++;
+ }
+
+ int overflowCommandCount = 0;
+ int overflowSeparatorCount = 0;
+ bool hasSyntheticOverflowDivider = false;
+ foreach (var item in DemoBar.OverflowItems)
+ {
+ if (item is CommandBarSeparator separator)
+ {
+ overflowSeparatorCount++;
+ if (!DemoBar.PrimaryCommands.Contains(separator) &&
+ !DemoBar.SecondaryCommands.Contains(separator))
+ {
+ hasSyntheticOverflowDivider = true;
+ }
+ }
+ else
+ {
+ overflowCommandCount++;
+ }
+ }
+
+ StatusText.Text =
+ $"Visible primary: {visiblePrimaryCommandCount} commands, {visiblePrimarySeparatorCount} separators\n" +
+ $"Overflow items: {overflowCommandCount} commands, {overflowSeparatorCount} separators\n" +
+ $"Synthetic overflow divider: {(hasSyntheticOverflowDivider ? "present" : "absent")}";
}
}
}
diff --git a/src/Avalonia.Controls/CommandBar/CommandBar.cs b/src/Avalonia.Controls/CommandBar/CommandBar.cs
index d8047f9d9e..6b92a935ad 100644
--- a/src/Avalonia.Controls/CommandBar/CommandBar.cs
+++ b/src/Avalonia.Controls/CommandBar/CommandBar.cs
@@ -7,6 +7,7 @@ using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Metadata;
+using Avalonia.Reactive;
using Avalonia.Threading;
namespace Avalonia.Controls
@@ -136,6 +137,8 @@ namespace Avalonia.Controls
private readonly ObservableCollection _visiblePrimaryCommands = new();
private readonly ObservableCollection _overflowItems = new();
+ private readonly CommandBarSeparator _overflowPrimarySecondarySeparator = new();
+ private readonly CompositeDisposable _secondaryCommandVisibilitySubscriptions = new();
private bool _isDynamicUpdateInProgress;
private double _constraintWidth = double.PositiveInfinity;
private bool _openedViaKeyboard;
@@ -151,6 +154,7 @@ namespace Avalonia.Controls
var secondaryCommands = new ObservableCollection();
SetCurrentValue(SecondaryCommandsProperty, (IList)secondaryCommands);
+ RebuildSecondaryCommandVisibilitySubscriptions();
SizeChanged += CommandBar_SizeChanged;
}
@@ -386,6 +390,18 @@ namespace Avalonia.Controls
UpdateDynamicOverflow();
}
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ RebuildSecondaryCommandVisibilitySubscriptions();
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ _secondaryCommandVisibilitySubscriptions.Clear();
+ base.OnDetachedFromVisualTree(e);
+ }
+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@@ -439,6 +455,7 @@ namespace Avalonia.Controls
oldSecondary.CollectionChanged -= OnSecondaryCommandsChanged;
if (change.NewValue is INotifyCollectionChanged newSecondary)
newSecondary.CollectionChanged += OnSecondaryCommandsChanged;
+ RebuildSecondaryCommandVisibilitySubscriptions();
UpdateDynamicOverflow();
}
}
@@ -583,6 +600,7 @@ namespace Avalonia.Controls
private void OnSecondaryCommandsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
+ RebuildSecondaryCommandVisibilitySubscriptions();
UpdateDynamicOverflow();
}
@@ -598,12 +616,9 @@ namespace Avalonia.Controls
{
_visiblePrimaryCommands.Clear();
_overflowItems.Clear();
+ SetOverflowMode(_overflowPrimarySecondarySeparator, false);
- foreach (var item in SecondaryCommands)
- {
- SetOverflowMode(item, true);
- _overflowItems.Add(item);
- }
+ var overflowedPrimaryCommands = new List();
var availableWidth = double.IsFinite(_constraintWidth) ? _constraintWidth : Bounds.Width;
@@ -669,8 +684,6 @@ namespace Avalonia.Controls
? a.Order.CompareTo(b.Order)
: a.Index.CompareTo(b.Index));
- // Separators stay in the primary bar but are not counted toward maxItems.
- // If no non-separator buttons fit, separators are moved to overflow too.
var visibleIndices = new HashSet();
int nonSeparatorCount = 0;
for (var i = 0; i < prioritized.Count; i++)
@@ -688,6 +701,8 @@ namespace Avalonia.Controls
if (nonSeparatorCount == 0)
visibleIndices.Clear();
+ TrimOrphanedSeparatorsFromVisibleCommands(PrimaryCommands, visibleIndices);
+
for (var i = 0; i < PrimaryCommands.Count; i++)
{
if (visibleIndices.Contains(i))
@@ -695,15 +710,20 @@ namespace Avalonia.Controls
SetOverflowMode(PrimaryCommands[i], false);
_visiblePrimaryCommands.Add(PrimaryCommands[i]);
}
+ else if (PrimaryCommands[i] is CommandBarSeparator)
+ {
+ SetOverflowMode(PrimaryCommands[i], false);
+ }
else
{
SetOverflowMode(PrimaryCommands[i], true);
- _overflowItems.Add(PrimaryCommands[i]);
+ overflowedPrimaryCommands.Add(PrimaryCommands[i]);
}
}
}
}
+ AddOverflowItems(overflowedPrimaryCommands);
HasSecondaryCommands = _overflowItems.Count > 0;
UpdateOverflowButtonVisibility();
}
@@ -731,6 +751,8 @@ namespace Avalonia.Controls
if (SecondaryCommands != null)
foreach (var cmd in SecondaryCommands)
ApplyLabelPositionToElement(cmd);
+
+ ApplyLabelPositionToElement(_overflowPrimarySecondarySeparator);
}
private void ApplyLabelPositionToElement(ICommandBarElement element)
@@ -758,5 +780,121 @@ namespace Avalonia.Controls
_ => HasSecondaryCommands // Auto
};
}
+
+ private void AddOverflowItems(IReadOnlyList overflowedPrimaryCommands)
+ {
+ for (var i = 0; i < overflowedPrimaryCommands.Count; i++)
+ _overflowItems.Add(overflowedPrimaryCommands[i]);
+
+ if (overflowedPrimaryCommands.Count > 0 && HasVisibleElements(SecondaryCommands))
+ {
+ SetOverflowMode(_overflowPrimarySecondarySeparator, true);
+ _overflowItems.Add(_overflowPrimarySecondarySeparator);
+ }
+
+ foreach (var item in SecondaryCommands)
+ {
+ SetOverflowMode(item, true);
+ _overflowItems.Add(item);
+ }
+ }
+
+ private void RebuildSecondaryCommandVisibilitySubscriptions()
+ {
+ _secondaryCommandVisibilitySubscriptions.Clear();
+
+ if (SecondaryCommands is null)
+ return;
+
+ for (var i = 0; i < SecondaryCommands.Count; i++)
+ {
+ if (SecondaryCommands[i] is Avalonia.Visual visual)
+ {
+ bool isInitialValue = true;
+ visual.GetObservable(Avalonia.Visual.IsVisibleProperty)
+ .Subscribe(_ =>
+ {
+ if (isInitialValue)
+ {
+ isInitialValue = false;
+ return;
+ }
+
+ UpdateDynamicOverflow();
+ })
+ .DisposeWith(_secondaryCommandVisibilitySubscriptions);
+ }
+ }
+ }
+
+ private static bool HasVisibleElements(IList commands)
+ {
+ for (var i = 0; i < commands.Count; i++)
+ {
+ if (commands[i] is Avalonia.Visual visual && visual.IsVisible)
+ return true;
+ }
+
+ return false;
+ }
+
+ private static void TrimOrphanedSeparatorsFromVisibleCommands(
+ IList commands, HashSet visibleIndices)
+ {
+ var toRemove = new List();
+ for (var i = 0; i < commands.Count; i++)
+ {
+ if (!visibleIndices.Contains(i) || commands[i] is not CommandBarSeparator)
+ continue;
+
+ bool hasNonSeparatorBefore = FindNonSeparatorInVisibleCommands(
+ commands, visibleIndices, forward: false, startIndex: i - 1, out _);
+ bool hasNonSeparatorAfter = FindNonSeparatorInVisibleCommands(
+ commands, visibleIndices, forward: true, startIndex: i + 1, out _);
+
+ if (!hasNonSeparatorBefore || !hasNonSeparatorAfter)
+ toRemove.Add(i);
+ }
+
+ foreach (var idx in toRemove)
+ visibleIndices.Remove(idx);
+
+ bool previousWasSeparator = false;
+ for (var i = 0; i < commands.Count; i++)
+ {
+ if (!visibleIndices.Contains(i))
+ continue;
+
+ if (commands[i] is CommandBarSeparator)
+ {
+ if (previousWasSeparator)
+ visibleIndices.Remove(i);
+ else
+ previousWasSeparator = true;
+ }
+ else
+ {
+ previousWasSeparator = false;
+ }
+ }
+ }
+
+ private static bool FindNonSeparatorInVisibleCommands(
+ IList commands, HashSet visibleIndices,
+ bool forward, int startIndex, out int foundIndex)
+ {
+ foundIndex = -1;
+ var i = startIndex;
+ while (forward ? i < commands.Count : i >= 0)
+ {
+ if (visibleIndices.Contains(i) && commands[i] is not CommandBarSeparator)
+ {
+ foundIndex = i;
+ return true;
+ }
+ i += forward ? 1 : -1;
+ }
+ return false;
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/CommandBarTests.cs b/tests/Avalonia.Controls.UnitTests/CommandBarTests.cs
index 1950cff16c..371cbe4d6d 100644
--- a/tests/Avalonia.Controls.UnitTests/CommandBarTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/CommandBarTests.cs
@@ -874,13 +874,17 @@ public class CommandBarItemWidthTests : ScopedTestBase
public void ItemWidthBottom_Controls_HowManyButtonsFit()
{
var cb = CreateWithWidth(300);
- cb.SecondaryCommands!.Add(new CommandBarButton()); // forces overflow button
+ var secondary = new CommandBarButton();
+ cb.SecondaryCommands!.Add(secondary); // forces overflow button
for (int i = 0; i < 4; i++)
cb.PrimaryCommands!.Add(new CommandBarButton());
cb.IsDynamicOverflowEnabled = true;
Assert.Equal(3, cb.VisiblePrimaryCommands.Count);
- Assert.Equal(1, cb.OverflowItems.Count - 1); // -1 for the secondary command
+ Assert.Equal(3, cb.OverflowItems.Count);
+ Assert.IsType(cb.OverflowItems[0]);
+ Assert.IsType(cb.OverflowItems[1]);
+ Assert.Same(secondary, cb.OverflowItems[2]);
}
[Fact]
@@ -1196,6 +1200,309 @@ public class CommandBarOverflowKeyboardTests : ScopedTestBase
}
}
+public class CommandBarSeparatorOverflowTests : ScopedTestBase
+{
+ private static CommandBar CreateWithWidth(double width)
+ {
+ var cb = new CommandBar();
+ cb.Measure(new Size(width, double.PositiveInfinity));
+ return cb;
+ }
+
+ [Fact]
+ public void TrailingSeparator_IsNotLastVisibleItem()
+ {
+ // [Btn, Btn, Sep, Btn] with room for 2 buttons: Sep should NOT trail.
+ var cb = CreateWithWidth(300);
+ cb.SecondaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.IsNotType(cb.VisiblePrimaryCommands[^1]);
+ }
+
+ [Fact]
+ public void TrailingSeparator_MovedToOverflow()
+ {
+ // [Btn, Sep, Btn, Btn] with room for 1 button: Sep after the single visible button should overflow.
+ var cb = CreateWithWidth(300);
+ cb.ItemWidthBottom = 260;
+ cb.SecondaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.Equal(1, cb.VisiblePrimaryCommands.Count);
+ Assert.IsType(cb.VisiblePrimaryCommands[0]);
+ }
+
+ [Fact]
+ public void MultipleSeparators_AllTrailingOnesStripped()
+ {
+ // [Btn, Sep, Sep, Btn] with room for 1: both trailing separators should be stripped.
+ var cb = CreateWithWidth(300);
+ cb.ItemWidthBottom = 260;
+ cb.SecondaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.Equal(1, cb.VisiblePrimaryCommands.Count);
+ Assert.IsType(cb.VisiblePrimaryCommands[0]);
+ }
+
+ [Fact]
+ public void MidSeparator_StaysVisible_WhenButtonsOnBothSides()
+ {
+ // [Btn, Sep, Btn] with room for all: separator stays.
+ var cb = CreateWithWidth(300);
+ cb.PrimaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.Equal(3, cb.VisiblePrimaryCommands.Count);
+ Assert.IsType(cb.VisiblePrimaryCommands[1]);
+ }
+
+ [Fact]
+ public void AllButtonsOverflow_SeparatorsAlsoOverflow()
+ {
+ // [Sep, Btn, Btn] with room for 0: everything overflows.
+ var cb = CreateWithWidth(50);
+ cb.SecondaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands!.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.Empty(cb.VisiblePrimaryCommands);
+ }
+
+ [Fact]
+ public void LeadingSeparator_IsStrippedFromVisible()
+ {
+ // [Sep, Btn, Btn, Btn] with room for 2: leading Sep should be stripped.
+ var cb = CreateWithWidth(300);
+ cb.SecondaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands!.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.IsNotType(cb.VisiblePrimaryCommands[0]);
+ }
+
+ [Fact]
+ public void ConsecutiveSeparators_CollapsedToOne()
+ {
+ // [Btn, Sep, Sep, Btn] all fit: only one separator should remain.
+ var cb = CreateWithWidth(300);
+ cb.PrimaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.IsDynamicOverflowEnabled = true;
+
+ int sepCount = CountSeparators(cb.VisiblePrimaryCommands);
+ Assert.Equal(1, sepCount);
+ }
+
+ [Fact]
+ public void OrphanedMidSeparator_RemovedWhenNeighborOverflows()
+ {
+ // [Btn1, Sep, Btn2, Sep, Btn3] with room for 2: Btn3 overflows,
+ // second Sep becomes trailing and is removed. First Sep stays.
+ var cb = CreateWithWidth(300);
+ cb.ItemWidthBottom = 100;
+ cb.SecondaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.IsNotType(cb.VisiblePrimaryCommands[^1]);
+ Assert.Equal(1, CountSeparators(cb.VisiblePrimaryCommands));
+ }
+
+ [Fact]
+ public void SeparatorBetweenOverflowedButtons_IsRemoved()
+ {
+ // [Btn1, Btn2, Sep, Btn3, Btn4] with room for 2: Btn3 and Btn4 overflow,
+ // Sep has no non-separator after it in visible set, so it is removed.
+ var cb = CreateWithWidth(300);
+ cb.ItemWidthBottom = 100;
+ cb.SecondaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.Equal(0, CountSeparators(cb.VisiblePrimaryCommands));
+ }
+
+ [Fact]
+ public void MultipleSeparatorGroups_OnlyValidOnesRemain()
+ {
+ // [Btn, Sep, Btn, Sep, Btn, Sep, Btn] with room for 3:
+ // last Btn overflows, last Sep becomes trailing, the rest stay.
+ var cb = CreateWithWidth(300);
+ cb.SecondaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarButton());
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.IsNotType(cb.VisiblePrimaryCommands[^1]);
+ Assert.IsNotType(cb.VisiblePrimaryCommands[0]);
+ }
+
+ [Fact]
+ public void OnlySeparators_AllOverflow()
+ {
+ // [Sep, Sep, Sep] with no buttons: all should overflow.
+ var cb = CreateWithWidth(300);
+ cb.SecondaryCommands!.Add(new CommandBarButton());
+ cb.PrimaryCommands!.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.PrimaryCommands.Add(new CommandBarSeparator());
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.Empty(cb.VisiblePrimaryCommands);
+ }
+
+ [Fact]
+ public void PrimarySeparator_IsRemovedInsteadOfBecomingFirstOverflowItem()
+ {
+ var cb = CreateWithWidth(300);
+ cb.ItemWidthBottom = 260;
+
+ var leadingSeparator = new CommandBarSeparator();
+ var firstButton = new CommandBarButton();
+ var overflowedButton = new CommandBarButton();
+
+ cb.PrimaryCommands!.Add(leadingSeparator);
+ cb.PrimaryCommands.Add(firstButton);
+ cb.PrimaryCommands.Add(overflowedButton);
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.Single(cb.OverflowItems);
+ Assert.Same(overflowedButton, cb.OverflowItems[0]);
+ Assert.DoesNotContain(leadingSeparator, cb.OverflowItems);
+ }
+
+ [Fact]
+ public void OverflowedPrimaryCommands_PrecedeSecondaryCommands_WithSyntheticSeparator()
+ {
+ var cb = CreateWithWidth(300);
+ cb.ItemWidthBottom = 260;
+
+ var visiblePrimary = new CommandBarButton();
+ var originalPrimarySeparator = new CommandBarSeparator();
+ var overflowedPrimaryOne = new CommandBarButton();
+ var overflowedPrimaryTwo = new CommandBarButton();
+ var secondary = new CommandBarButton();
+
+ cb.PrimaryCommands!.Add(visiblePrimary);
+ cb.PrimaryCommands.Add(originalPrimarySeparator);
+ cb.PrimaryCommands.Add(overflowedPrimaryOne);
+ cb.PrimaryCommands.Add(overflowedPrimaryTwo);
+ cb.SecondaryCommands!.Add(secondary);
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.Equal(4, cb.OverflowItems.Count);
+ Assert.Same(overflowedPrimaryOne, cb.OverflowItems[0]);
+ Assert.Same(overflowedPrimaryTwo, cb.OverflowItems[1]);
+ Assert.IsType(cb.OverflowItems[2]);
+ Assert.NotSame(originalPrimarySeparator, cb.OverflowItems[2]);
+ Assert.Same(secondary, cb.OverflowItems[3]);
+ Assert.DoesNotContain(originalPrimarySeparator, cb.OverflowItems);
+ }
+
+ [Fact]
+ public void HiddenSecondaryCommands_DoNotGetSyntheticOverflowSeparator()
+ {
+ var cb = CreateWithWidth(300);
+ cb.ItemWidthBottom = 260;
+
+ var visiblePrimary = new CommandBarButton();
+ var overflowedPrimary = new CommandBarButton();
+ var hiddenSecondary = new CommandBarButton { IsVisible = false };
+
+ cb.PrimaryCommands!.Add(visiblePrimary);
+ cb.PrimaryCommands.Add(overflowedPrimary);
+ cb.SecondaryCommands!.Add(hiddenSecondary);
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.Equal(2, cb.OverflowItems.Count);
+ Assert.Same(overflowedPrimary, cb.OverflowItems[0]);
+ Assert.Same(hiddenSecondary, cb.OverflowItems[1]);
+ Assert.DoesNotContain(cb.OverflowItems, x => x is CommandBarSeparator);
+ }
+
+ [Fact]
+ public void TogglingSecondaryVisibility_RebuildsSyntheticOverflowSeparator()
+ {
+ var cb = CreateWithWidth(300);
+ cb.ItemWidthBottom = 260;
+
+ var visiblePrimary = new CommandBarButton();
+ var overflowedPrimary = new CommandBarButton();
+ var secondary = new CommandBarButton();
+
+ cb.PrimaryCommands!.Add(visiblePrimary);
+ cb.PrimaryCommands.Add(overflowedPrimary);
+ cb.SecondaryCommands!.Add(secondary);
+ cb.IsDynamicOverflowEnabled = true;
+
+ Assert.Equal(3, cb.OverflowItems.Count);
+ Assert.Same(overflowedPrimary, cb.OverflowItems[0]);
+ Assert.IsType(cb.OverflowItems[1]);
+ Assert.Same(secondary, cb.OverflowItems[2]);
+
+ secondary.IsVisible = false;
+
+ Assert.Equal(2, cb.OverflowItems.Count);
+ Assert.Same(overflowedPrimary, cb.OverflowItems[0]);
+ Assert.Same(secondary, cb.OverflowItems[1]);
+ Assert.DoesNotContain(cb.OverflowItems, x => x is CommandBarSeparator);
+
+ secondary.IsVisible = true;
+
+ Assert.Equal(3, cb.OverflowItems.Count);
+ Assert.Same(overflowedPrimary, cb.OverflowItems[0]);
+ Assert.IsType(cb.OverflowItems[1]);
+ Assert.Same(secondary, cb.OverflowItems[2]);
+ }
+
+ private static int CountSeparators(IReadOnlyList items)
+ {
+ int count = 0;
+ for (var i = 0; i < items.Count; i++)
+ {
+ if (items[i] is CommandBarSeparator)
+ count++;
+ }
+ return count;
+ }
+}
+
file sealed class DelegateCommand : System.Windows.Input.ICommand
{
private readonly System.Action _execute;
From 7530b74cdb9007113ad136cbc859fb686d15e6b8 Mon Sep 17 00:00:00 2001
From: Julien Lebosquain
Date: Tue, 31 Mar 2026 20:39:46 +0200
Subject: [PATCH 43/57] Fix FocusManager.FocusedElement on canceled/redirected
focus (#21047)
* Add failing tests for FocusManager.FocusedElement
* Fix FocusManager.FocusedElement on canceled/redirected focus
* Fix IFocusManager documentation
* Add new focus restoration test
---
src/Avalonia.Base/Input/FocusManager.cs | 50 ++++---
src/Avalonia.Base/Input/IFocusManager.cs | 4 +-
.../Input/InputElement_Focus.cs | 123 ++++++++++++++++++
3 files changed, 157 insertions(+), 20 deletions(-)
diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs
index 651210fc2b..72a13d385d 100644
--- a/src/Avalonia.Base/Input/FocusManager.cs
+++ b/src/Avalonia.Base/Input/FocusManager.cs
@@ -75,30 +75,44 @@ namespace Avalonia.Input
if (element is not null)
{
- if (!CanFocus(element))
- return false;
+ return FocusCore(keyboardDevice, element, method, keyModifiers);
+ }
+
+ if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore && restore != Current)
+ {
+ return FocusCore(keyboardDevice, restore, method, keyModifiers);
+ }
+
+ _focusRoot = null;
+ keyboardDevice.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None, false);
+ return false;
+ }
+
+ private bool FocusCore(
+ KeyboardDevice keyboardDevice,
+ IInputElement element,
+ NavigationMethod method,
+ KeyModifiers keyModifiers)
+ {
+ if (!CanFocus(element))
+ return false;
+
+ keyboardDevice.SetFocusedElement(element, method, keyModifiers);
- if (GetFocusScope(element) is StyledElement scope)
+ if (keyboardDevice.FocusedElement is { } effectivelyFocusedElement)
+ {
+ if (GetFocusScope(effectivelyFocusedElement) is { } scope)
{
- scope.SetValue(FocusedElementProperty, element);
+ scope.SetValue(FocusedElementProperty, effectivelyFocusedElement);
_focusRoot = GetFocusRoot(scope);
}
- keyboardDevice.SetFocusedElement(element, method, keyModifiers);
- return true;
- }
- else if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore &&
- restore != Current &&
- Focus(restore))
- {
- return true;
- }
- else
- {
- _focusRoot = null;
- keyboardDevice.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None, false);
- return false;
+ return effectivelyFocusedElement == element;
}
+
+ _focusRoot = null;
+ keyboardDevice.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None, false);
+ return false;
}
internal void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent)
diff --git a/src/Avalonia.Base/Input/IFocusManager.cs b/src/Avalonia.Base/Input/IFocusManager.cs
index d9e8d36f8b..2cf51c7965 100644
--- a/src/Avalonia.Base/Input/IFocusManager.cs
+++ b/src/Avalonia.Base/Input/IFocusManager.cs
@@ -24,8 +24,8 @@ namespace Avalonia.Input
/// If is null, this method tries to clear the focus. However, it is not advised.
/// For a better user experience, focus should be moved to another element when possible.
///
- /// When this method return true, it is not guaranteed that the focus has been moved
- /// to . The focus might have been redirected to another element.
+ /// When this method returns true, the focus has been moved to .
+ /// When this method returns false, the focus may have been canceled or redirected to another element.
///
bool Focus(
IInputElement? element,
diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
index 45d665b591..9e835b2427 100644
--- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
@@ -1064,6 +1064,129 @@ namespace Avalonia.Base.UnitTests.Input
}
}
+ [Fact]
+ public void Focus_In_Scope_Should_Not_Change_When_Focus_Canceled()
+ {
+ using var app = UnitTestApplication.Start(TestServices.RealFocus);
+ var first = new Button { Name = "First" };
+ var second = new Button { Name = "Second" };
+
+ var root = new TestRoot
+ {
+ Child = new StackPanel
+ {
+ Children =
+ {
+ first,
+ second
+ }
+ }
+ };
+
+ var focusManager = (FocusManager)root.FocusManager;
+
+ // Focus the first element
+ first.Focus();
+ Assert.Same(first, focusManager.GetFocusedElement(root));
+
+ // Cancel focus change
+ second.GettingFocus += (_, e) => e.TryCancel();
+
+ // Move the focus to the second element: it should fail
+ var focusResult = focusManager.Focus(second);
+ Assert.False(focusResult);
+ Assert.Same(first, KeyboardDevice.Instance?.FocusedElement);
+
+ // FocusedElement for the scope should remain the same
+ var newFocusedElementInScope = focusManager.GetFocusedElement(root);
+ Assert.Same(first, newFocusedElementInScope);
+ }
+
+ [Fact]
+ public void Focus_In_Scope_Should_Match_Redirected_Element_When_Focus_Redirected()
+ {
+ using var app = UnitTestApplication.Start(TestServices.RealFocus);
+ var first = new Button { Name = "First" };
+ var second = new Button { Name = "Second" };
+ var third = new Button { Name = "Third" };
+
+ var root = new TestRoot
+ {
+ Child = new StackPanel
+ {
+ Children =
+ {
+ first,
+ second,
+ third
+ }
+ }
+ };
+
+ var focusManager = (FocusManager)root.FocusManager;
+
+ // Focus the first element
+ first.Focus();
+ Assert.Same(first, focusManager.GetFocusedElement(root));
+
+ // Redirect focus change
+ second.GettingFocus += (_, e) => e.TrySetNewFocusedElement(third);
+
+ // Move the focus to the second element: it should fail
+ var focusResult = focusManager.Focus(second);
+ Assert.False(focusResult);
+ Assert.Same(third, KeyboardDevice.Instance?.FocusedElement);
+
+ // FocusedElement for the scope should have moved to the redirected element
+ var newFocusedElementInScope = focusManager.GetFocusedElement(root);
+ Assert.Same(third, newFocusedElementInScope);
+ }
+
+ [Fact]
+ public void Focus_Should_Return_To_First_Window_When_Second_Is_Closed()
+ {
+ using var app = UnitTestApplication.Start(
+ TestServices.StyledWindow.With(keyboardDevice: () => new KeyboardDevice()));
+ var first = new Button { Name = "FirstButton" };
+ var second = new Button { Name = "SecondButton" };
+
+ var window1 = new Window
+ {
+ Content = first
+ };
+
+ var window2 = new Window
+ {
+ Content = second
+ };
+
+ window1.Show();
+
+ // Focus the first button in the first window
+ first.Focus();
+ Assert.Same(first, KeyboardDevice.Instance?.FocusedElement);
+ Assert.Same(first, window1.FocusManager.GetFocusedElement());
+
+ window2.Show();
+
+ // Focus the second button in the second window
+ second.Focus();
+ Assert.Same(second, KeyboardDevice.Instance?.FocusedElement);
+ Assert.Same(second, window2.FocusManager.GetFocusedElement());
+
+ // Close the second window, focus should be lost
+ window2.Close();
+ Assert.Null(KeyboardDevice.Instance?.FocusedElement);
+ Assert.Null(window2.FocusManager.GetFocusedElement());
+
+ // Activate the first window again
+ window1.PlatformImpl?.Activated?.Invoke();
+
+ // Focus should have moved back to the first button in the first window
+ Assert.Same(first, KeyboardDevice.Instance?.FocusedElement);
+ Assert.Same(first, window1.FocusManager.GetFocusedElement());
+ }
+
private class TestFocusScope : Panel, IFocusScope
{
}
From bb3f0f331ee242bc4fcb45e5f43240afd5cb5878 Mon Sep 17 00:00:00 2001
From: Betta_Fish <96322503+zxbmmmmmmmmm@users.noreply.github.com>
Date: Wed, 1 Apr 2026 12:48:22 +0800
Subject: [PATCH 44/57] CompositionAnimation fixes (#20936)
* fix: ExpressionVariant types
* fix: CompositionProperty is not correctly registered
* fix(ExpressionVariant): remove Scalar
* refactor: use switch expressions
* fix: "this.Target" is not tracked
* fix: unit tests
* fix: remove registry
* remove unused code
* fix
* fix: notify other animations when update
* add unit tests
* fix: IsValid
* fix
* fix: requeue target when another animation invalidated during evaluation
* fix: GetProperty fails in aot
* fix
---
.../Animations/AnimationInstanceBase.cs | 4 +-
.../Expressions/DelegateExpressionFfi.cs | 8 +-
.../Composition/Expressions/Expression.cs | 15 +
.../Expressions/ExpressionParser.cs | 14 +-
.../Expressions/ExpressionVariant.cs | 555 ++++++------------
.../Composition/Server/CompositionProperty.cs | 65 +-
.../Server/ServerCompositorAnimations.cs | 9 +-
.../Composition/Server/ServerObject.cs | 2 +-
.../Server/ServerObjectAnimations.cs | 11 +-
.../CompositionGenerator/Generator.cs | 2 +
.../CompositionAnimationParserTests.cs | 6 +-
.../Composition/CompositionAnimationTests.cs | 133 ++++-
12 files changed, 364 insertions(+), 460 deletions(-)
diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs
index 8a25f23e59..226331853a 100644
--- a/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs
@@ -30,7 +30,9 @@ internal abstract class AnimationInstanceBase : IAnimationInstance
_trackedObjects = new ();
foreach (var t in trackedObjects)
{
- var obj = Parameters.GetObjectParameter(t.name);
+ var obj = (t.name == ExpressionKeywords.Target)
+ ? TargetObject
+ : Parameters.GetObjectParameter(t.name);
if (obj is ServerObject tracked)
{
var off = tracked.GetCompositionProperty(t.member);
diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs
index c70056b5aa..042e7429c6 100644
--- a/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Expressions/DelegateExpressionFfi.cs
@@ -62,12 +62,10 @@ namespace Avalonia.Rendering.Composition.Expressions
var arg = arguments[c].Type;
if (parameter != arg)
{
- var canCast = (parameter == VariantType.Double && arg == VariantType.Scalar)
- || (parameter == VariantType.Vector3D && arg == VariantType.Vector3)
+ var canCast = (parameter == VariantType.Vector3D && arg == VariantType.Vector3)
|| (parameter == VariantType.Vector && arg == VariantType.Vector2)
|| (anyCast && (
- (arg == VariantType.Double && parameter == VariantType.Scalar)
- || (arg == VariantType.Vector3D && parameter == VariantType.Vector3)
+ (arg == VariantType.Vector3D && parameter == VariantType.Vector3)
|| (arg == VariantType.Vector && parameter == VariantType.Vector2)
));
if (!canCast)
@@ -112,7 +110,7 @@ namespace Avalonia.Rendering.Composition.Expressions
static readonly Dictionary TypeMap = new Dictionary
{
[typeof(bool)] = VariantType.Boolean,
- [typeof(float)] = VariantType.Scalar,
+ [typeof(float)] = VariantType.Double,
[typeof(double)] = VariantType.Double,
[typeof(Vector2)] = VariantType.Vector2,
[typeof(Vector)] = VariantType.Vector,
diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs
index d9f39d0ce0..7f5253f476 100644
--- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs
@@ -104,6 +104,17 @@ namespace Avalonia.Rendering.Composition.Expressions
False
}
+ internal static class ExpressionKeywords
+ {
+ public const string StartingValue = "this.startingvalue";
+ public const string CurrentValue = "this.currentvalue";
+ public const string FinalValue = "this.finalvalue";
+ public const string Pi = "pi";
+ public const string True = "true";
+ public const string False = "false";
+ public const string Target = "this.target";
+ }
+
internal class ConditionalExpression : Expression
{
public Expression Condition { get; }
@@ -202,6 +213,10 @@ namespace Avalonia.Rendering.Composition.Expressions
Target.CollectReferences(references);
if (Target is ParameterExpression pe)
references.Add((pe.Name, Member));
+ else if (Target is KeywordExpression { Keyword : ExpressionKeyword.Target })
+ {
+ references.Add((ExpressionKeywords.Target, Member));
+ }
}
public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context)
diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs
index 885499bc2c..45e4e164c1 100644
--- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionParser.cs
@@ -25,19 +25,19 @@ namespace Avalonia.Rendering.Composition.Expressions
{
// We can parse keywords, parameter names and constants
expr = null;
- if (parser.TryParseKeywordLowerCase("this.startingvalue"))
+ if (parser.TryParseKeywordLowerCase(ExpressionKeywords.StartingValue))
expr = new KeywordExpression(ExpressionKeyword.StartingValue);
- else if(parser.TryParseKeywordLowerCase("this.currentvalue"))
+ else if(parser.TryParseKeywordLowerCase(ExpressionKeywords.CurrentValue))
expr = new KeywordExpression(ExpressionKeyword.CurrentValue);
- else if(parser.TryParseKeywordLowerCase("this.finalvalue"))
+ else if(parser.TryParseKeywordLowerCase(ExpressionKeywords.FinalValue))
expr = new KeywordExpression(ExpressionKeyword.FinalValue);
- else if(parser.TryParseKeywordLowerCase("pi"))
+ else if(parser.TryParseKeywordLowerCase(ExpressionKeywords.Pi))
expr = new KeywordExpression(ExpressionKeyword.Pi);
- else if(parser.TryParseKeywordLowerCase("true"))
+ else if(parser.TryParseKeywordLowerCase(ExpressionKeywords.True))
expr = new KeywordExpression(ExpressionKeyword.True);
- else if(parser.TryParseKeywordLowerCase("false"))
+ else if(parser.TryParseKeywordLowerCase(ExpressionKeywords.False))
expr = new KeywordExpression(ExpressionKeyword.False);
- else if (parser.TryParseKeywordLowerCase("this.target"))
+ else if (parser.TryParseKeywordLowerCase(ExpressionKeywords.Target))
expr = new KeywordExpression(ExpressionKeyword.Target);
if (expr != null)
diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs
index 136ef69b55..bbaab572d5 100644
--- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs
@@ -10,7 +10,6 @@ namespace Avalonia.Rendering.Composition.Expressions
{
Invalid,
Boolean,
- Scalar,
Double,
Vector2,
Vector3,
@@ -33,7 +32,6 @@ namespace Avalonia.Rendering.Composition.Expressions
[FieldOffset(0)] public VariantType Type;
[FieldOffset(4)] public bool Boolean;
- [FieldOffset(4)] public float Scalar;
[FieldOffset(4)] public double Double;
[FieldOffset(4)] public Vector2 Vector2;
[FieldOffset(4)] public Vector3 Vector3;
@@ -45,191 +43,194 @@ namespace Avalonia.Rendering.Composition.Expressions
[FieldOffset(4)] public Matrix4x4 Matrix4x4;
[FieldOffset(4)] public Quaternion Quaternion;
[FieldOffset(4)] public Color Color;
-
+
public ExpressionVariant GetProperty(string property)
{
if (Type == VariantType.Vector2)
{
- if (ReferenceEquals(property, "X"))
+ if (IsMatch(property, "X"))
return Vector2.X;
- if (ReferenceEquals(property, "Y"))
+ if (IsMatch(property, "Y"))
return Vector2.Y;
return default;
}
-
+
if (Type == VariantType.Vector)
{
- if (ReferenceEquals(property, "X"))
+ if (IsMatch(property, "X"))
return Vector.X;
- if (ReferenceEquals(property, "Y"))
+ if (IsMatch(property, "Y"))
return Vector.Y;
return default;
}
if (Type == VariantType.Vector3)
{
- if (ReferenceEquals(property, "X"))
+ if (IsMatch(property, "X"))
return Vector3.X;
- if (ReferenceEquals(property, "Y"))
+ if (IsMatch(property, "Y"))
return Vector3.Y;
- if (ReferenceEquals(property, "Z"))
+ if (IsMatch(property, "Z"))
return Vector3.Z;
- if(ReferenceEquals(property, "XY"))
+ if (IsMatch(property, "XY"))
return new Vector2(Vector3.X, Vector3.Y);
- if(ReferenceEquals(property, "YX"))
+ if (IsMatch(property, "YX"))
return new Vector2(Vector3.Y, Vector3.X);
- if(ReferenceEquals(property, "XZ"))
+ if (IsMatch(property, "XZ"))
return new Vector2(Vector3.X, Vector3.Z);
- if(ReferenceEquals(property, "ZX"))
+ if (IsMatch(property, "ZX"))
return new Vector2(Vector3.Z, Vector3.X);
- if(ReferenceEquals(property, "YZ"))
+ if (IsMatch(property, "YZ"))
return new Vector2(Vector3.Y, Vector3.Z);
- if(ReferenceEquals(property, "ZY"))
+ if (IsMatch(property, "ZY"))
return new Vector2(Vector3.Z, Vector3.Y);
return default;
}
-
+
if (Type == VariantType.Vector3D)
{
- if (ReferenceEquals(property, "X"))
+ if (IsMatch(property, "X"))
return Vector3D.X;
- if (ReferenceEquals(property, "Y"))
+ if (IsMatch(property, "Y"))
return Vector3D.Y;
- if (ReferenceEquals(property, "Z"))
+ if (IsMatch(property, "Z"))
return Vector3D.Z;
- if(ReferenceEquals(property, "XY"))
+ if (IsMatch(property, "XY"))
return new Vector(Vector3D.X, Vector3D.Y);
- if(ReferenceEquals(property, "YX"))
+ if (IsMatch(property, "YX"))
return new Vector(Vector3D.Y, Vector3D.X);
- if(ReferenceEquals(property, "XZ"))
+ if (IsMatch(property, "XZ"))
return new Vector(Vector3D.X, Vector3D.Z);
- if(ReferenceEquals(property, "ZX"))
+ if (IsMatch(property, "ZX"))
return new Vector(Vector3D.Z, Vector3D.X);
- if(ReferenceEquals(property, "YZ"))
+ if (IsMatch(property, "YZ"))
return new Vector(Vector3D.Y, Vector3D.Z);
- if(ReferenceEquals(property, "ZY"))
+ if (IsMatch(property, "ZY"))
return new Vector(Vector3D.Z, Vector3D.Y);
return default;
}
if (Type == VariantType.Vector4)
{
- if (ReferenceEquals(property, "X"))
+ if (IsMatch(property, "X"))
return Vector4.X;
- if (ReferenceEquals(property, "Y"))
+ if (IsMatch(property, "Y"))
return Vector4.Y;
- if (ReferenceEquals(property, "Z"))
+ if (IsMatch(property, "Z"))
return Vector4.Z;
- if (ReferenceEquals(property, "W"))
+ if (IsMatch(property, "W"))
return Vector4.W;
return default;
}
if (Type == VariantType.Matrix3x2)
{
- if (ReferenceEquals(property, "M11"))
+ if (IsMatch(property, "M11"))
return Matrix3x2.M11;
- if (ReferenceEquals(property, "M12"))
+ if (IsMatch(property, "M12"))
return Matrix3x2.M12;
- if (ReferenceEquals(property, "M21"))
+ if (IsMatch(property, "M21"))
return Matrix3x2.M21;
- if (ReferenceEquals(property, "M22"))
+ if (IsMatch(property, "M22"))
return Matrix3x2.M22;
- if (ReferenceEquals(property, "M31"))
+ if (IsMatch(property, "M31"))
return Matrix3x2.M31;
- if (ReferenceEquals(property, "M32"))
+ if (IsMatch(property, "M32"))
return Matrix3x2.M32;
return default;
}
-
+
if (Type == VariantType.AvaloniaMatrix)
{
- if (ReferenceEquals(property, "M11"))
+ if (IsMatch(property, "M11"))
return AvaloniaMatrix.M11;
- if (ReferenceEquals(property, "M12"))
+ if (IsMatch(property, "M12"))
return AvaloniaMatrix.M12;
- if (ReferenceEquals(property, "M13"))
+ if (IsMatch(property, "M13"))
return AvaloniaMatrix.M13;
- if (ReferenceEquals(property, "M21"))
+ if (IsMatch(property, "M21"))
return AvaloniaMatrix.M21;
- if (ReferenceEquals(property, "M22"))
+ if (IsMatch(property, "M22"))
return AvaloniaMatrix.M22;
- if (ReferenceEquals(property, "M23"))
+ if (IsMatch(property, "M23"))
return AvaloniaMatrix.M23;
- if (ReferenceEquals(property, "M31"))
+ if (IsMatch(property, "M31"))
return AvaloniaMatrix.M31;
- if (ReferenceEquals(property, "M32"))
+ if (IsMatch(property, "M32"))
return AvaloniaMatrix.M32;
- if (ReferenceEquals(property, "M33"))
+ if (IsMatch(property, "M33"))
return AvaloniaMatrix.M33;
return default;
}
if (Type == VariantType.Matrix4x4)
{
- if (ReferenceEquals(property, "M11"))
+ if (IsMatch(property, "M11"))
return Matrix4x4.M11;
- if (ReferenceEquals(property, "M12"))
+ if (IsMatch(property, "M12"))
return Matrix4x4.M12;
- if (ReferenceEquals(property, "M13"))
+ if (IsMatch(property, "M13"))
return Matrix4x4.M13;
- if (ReferenceEquals(property, "M14"))
+ if (IsMatch(property, "M14"))
return Matrix4x4.M14;
- if (ReferenceEquals(property, "M21"))
+ if (IsMatch(property, "M21"))
return Matrix4x4.M21;
- if (ReferenceEquals(property, "M22"))
+ if (IsMatch(property, "M22"))
return Matrix4x4.M22;
- if (ReferenceEquals(property, "M23"))
+ if (IsMatch(property, "M23"))
return Matrix4x4.M23;
- if (ReferenceEquals(property, "M24"))
+ if (IsMatch(property, "M24"))
return Matrix4x4.M24;
- if (ReferenceEquals(property, "M31"))
+ if (IsMatch(property, "M31"))
return Matrix4x4.M31;
- if (ReferenceEquals(property, "M32"))
+ if (IsMatch(property, "M32"))
return Matrix4x4.M32;
- if (ReferenceEquals(property, "M33"))
+ if (IsMatch(property, "M33"))
return Matrix4x4.M33;
- if (ReferenceEquals(property, "M34"))
+ if (IsMatch(property, "M34"))
return Matrix4x4.M34;
- if (ReferenceEquals(property, "M41"))
+ if (IsMatch(property, "M41"))
return Matrix4x4.M41;
- if (ReferenceEquals(property, "M42"))
+ if (IsMatch(property, "M42"))
return Matrix4x4.M42;
- if (ReferenceEquals(property, "M43"))
+ if (IsMatch(property, "M43"))
return Matrix4x4.M43;
- if (ReferenceEquals(property, "M44"))
+ if (IsMatch(property, "M44"))
return Matrix4x4.M44;
return default;
}
if (Type == VariantType.Quaternion)
{
- if (ReferenceEquals(property, "X"))
+ if (IsMatch(property, "X"))
return Quaternion.X;
- if (ReferenceEquals(property, "Y"))
+ if (IsMatch(property, "Y"))
return Quaternion.Y;
- if (ReferenceEquals(property, "Z"))
+ if (IsMatch(property, "Z"))
return Quaternion.Z;
- if (ReferenceEquals(property, "W"))
+ if (IsMatch(property, "W"))
return Quaternion.W;
return default;
}
-
+
if (Type == VariantType.Color)
{
- if (ReferenceEquals(property, "A"))
+ if (IsMatch(property, "A"))
return Color.A;
- if (ReferenceEquals(property, "R"))
+ if (IsMatch(property, "R"))
return Color.R;
- if (ReferenceEquals(property, "G"))
+ if (IsMatch(property, "G"))
return Color.G;
- if (ReferenceEquals(property, "B"))
+ if (IsMatch(property, "B"))
return Color.B;
return default;
}
return default;
+
+ static bool IsMatch(string propertyName, string memberName) =>
+ string.Equals(propertyName, memberName, StringComparison.Ordinal);
}
public static implicit operator ExpressionVariant(bool value) =>
@@ -238,13 +239,6 @@ namespace Avalonia.Rendering.Composition.Expressions
Type = VariantType.Boolean,
Boolean = value
};
-
- public static implicit operator ExpressionVariant(float scalar) =>
- new ExpressionVariant
- {
- Type = VariantType.Scalar,
- Scalar = scalar
- };
public static implicit operator ExpressionVariant(double d) =>
new ExpressionVariant
@@ -300,7 +294,7 @@ namespace Avalonia.Rendering.Composition.Expressions
public static implicit operator ExpressionVariant(Matrix value) =>
new ExpressionVariant
{
- Type = VariantType.Matrix3x2,
+ Type = VariantType.AvaloniaMatrix,
AvaloniaMatrix = value
};
@@ -330,15 +324,12 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type != right.Type || left.Type == VariantType.Invalid)
return default;
- if (left.Type == VariantType.Scalar)
- return left.Scalar + right.Scalar;
-
if (left.Type == VariantType.Double)
return left.Double + right.Double;
if (left.Type == VariantType.Vector2)
return left.Vector2 + right.Vector2;
-
+
if (left.Type == VariantType.Vector)
return left.Vector + right.Vector;
@@ -350,16 +341,16 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type == VariantType.Vector4)
return left.Vector4 + right.Vector4;
-
+
if (left.Type == VariantType.Matrix3x2)
return left.Matrix3x2 + right.Matrix3x2;
-
+
if (left.Type == VariantType.Matrix4x4)
return left.Matrix4x4 + right.Matrix4x4;
-
+
if (left.Type == VariantType.Quaternion)
return left.Quaternion + right.Quaternion;
-
+
return default;
}
@@ -368,15 +359,12 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type != right.Type || left.Type == VariantType.Invalid)
return default;
- if (left.Type == VariantType.Scalar)
- return left.Scalar - right.Scalar;
-
if (left.Type == VariantType.Double)
return left.Double - right.Double;
if (left.Type == VariantType.Vector2)
return left.Vector2 - right.Vector2;
-
+
if (left.Type == VariantType.Vector)
return left.Vector - right.Vector;
@@ -388,13 +376,13 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type == VariantType.Vector4)
return left.Vector4 - right.Vector4;
-
+
if (left.Type == VariantType.Matrix3x2)
return left.Matrix3x2 - right.Matrix3x2;
-
+
if (left.Type == VariantType.Matrix4x4)
return left.Matrix4x4 - right.Matrix4x4;
-
+
if (left.Type == VariantType.Quaternion)
return left.Quaternion - right.Quaternion;
@@ -403,34 +391,31 @@ namespace Avalonia.Rendering.Composition.Expressions
public static ExpressionVariant operator -(ExpressionVariant left)
{
-
- if (left.Type == VariantType.Scalar)
- return -left.Scalar;
if (left.Type == VariantType.Double)
return -left.Double;
if (left.Type == VariantType.Vector2)
return -left.Vector2;
-
+
if (left.Type == VariantType.Vector)
return -left.Vector;
if (left.Type == VariantType.Vector3)
return -left.Vector3;
-
+
if (left.Type == VariantType.Vector3D)
return -left.Vector3D;
if (left.Type == VariantType.Vector4)
return -left.Vector4;
-
+
if (left.Type == VariantType.Matrix3x2)
return -left.Matrix3x2;
-
+
if (left.Type == VariantType.AvaloniaMatrix)
return -left.AvaloniaMatrix;
-
+
if (left.Type == VariantType.Matrix4x4)
return -left.Matrix4x4;
@@ -445,9 +430,6 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid)
return default;
- if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar)
- return left.Scalar * right.Scalar;
-
if (left.Type == VariantType.Double && right.Type == VariantType.Double)
return left.Double * right.Double;
@@ -457,53 +439,50 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type == VariantType.Vector && right.Type == VariantType.Vector)
return Vector.Multiply(left.Vector, right.Vector);
- if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar)
- return left.Vector2 * right.Scalar;
-
- if (left.Type == VariantType.Vector && right.Type == VariantType.Scalar)
- return left.Vector * right.Scalar;
-
+ if (left.Type == VariantType.Vector2 && right.Type == VariantType.Double)
+ return left.Vector2 * (float)right.Double;
+
if (left.Type == VariantType.Vector && right.Type == VariantType.Double)
return left.Vector * right.Double;
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3)
return left.Vector3 * right.Vector3;
-
+
if (left.Type == VariantType.Vector3D && right.Type == VariantType.Vector3D)
return Vector3D.Multiply(left.Vector3D, right.Vector3D);
- if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar)
- return left.Vector3 * right.Scalar;
-
- if (left.Type == VariantType.Vector3D && right.Type == VariantType.Scalar)
- return Vector3D.Multiply(left.Vector3D, right.Scalar);
+ if (left.Type == VariantType.Vector3 && right.Type == VariantType.Double)
+ return left.Vector3 * (float)right.Double;
+
+ if (left.Type == VariantType.Vector3D && right.Type == VariantType.Double)
+ return Vector3D.Multiply(left.Vector3D, right.Double);
if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4)
return left.Vector4 * right.Vector4;
- if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar)
- return left.Vector4 * right.Scalar;
-
+ if (left.Type == VariantType.Vector4 && right.Type == VariantType.Double)
+ return left.Vector4 * (float)right.Double;
+
if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Matrix3x2)
return left.Matrix3x2 * right.Matrix3x2;
- if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Scalar)
- return left.Matrix3x2 * right.Scalar;
-
+ if (left.Type == VariantType.Matrix3x2 && right.Type == VariantType.Double)
+ return left.Matrix3x2 * (float)right.Double;
+
if (left.Type == VariantType.AvaloniaMatrix && right.Type == VariantType.AvaloniaMatrix)
return left.AvaloniaMatrix * right.AvaloniaMatrix;
-
+
if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Matrix4x4)
return left.Matrix4x4 * right.Matrix4x4;
- if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Scalar)
- return left.Matrix4x4 * right.Scalar;
-
+ if (left.Type == VariantType.Matrix4x4 && right.Type == VariantType.Double)
+ return left.Matrix4x4 * (float)right.Double;
+
if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion)
return left.Quaternion * right.Quaternion;
- if (left.Type == VariantType.Quaternion && right.Type == VariantType.Scalar)
- return left.Quaternion * right.Scalar;
+ if (left.Type == VariantType.Quaternion && right.Type == VariantType.Double)
+ return left.Quaternion * (float)right.Double;
return default;
}
@@ -513,9 +492,6 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type == VariantType.Invalid || right.Type == VariantType.Invalid)
return default;
- if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar)
- return left.Scalar / right.Scalar;
-
if (left.Type == VariantType.Double && right.Type == VariantType.Double)
return left.Double / right.Double;
@@ -525,14 +501,11 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type == VariantType.Vector && right.Type == VariantType.Vector)
return Vector.Divide(left.Vector, right.Vector);
- if (left.Type == VariantType.Vector2 && right.Type == VariantType.Scalar)
- return left.Vector2 / right.Scalar;
-
- if (left.Type == VariantType.Vector && right.Type == VariantType.Scalar)
- return left.Vector / right.Scalar;
-
+ if (left.Type == VariantType.Vector2 && right.Type == VariantType.Double)
+ return left.Vector2 / (float)right.Double;
+
if (left.Type == VariantType.Vector && right.Type == VariantType.Double)
- return left.Vector / right.Scalar;
+ return left.Vector / right.Double;
if (left.Type == VariantType.Vector3 && right.Type == VariantType.Vector3)
return left.Vector3 / right.Vector3;
@@ -540,21 +513,18 @@ namespace Avalonia.Rendering.Composition.Expressions
if (left.Type == VariantType.Vector3D && right.Type == VariantType.Vector3D)
return Vector3D.Divide(left.Vector3D, right.Vector3D);
- if (left.Type == VariantType.Vector3 && right.Type == VariantType.Scalar)
- return left.Vector3 / right.Scalar;
-
- if (left.Type == VariantType.Vector3D && right.Type == VariantType.Scalar)
- return Avalonia.Vector3D.Divide(left.Vector3D, right.Scalar);
-
+ if (left.Type == VariantType.Vector3 && right.Type == VariantType.Double)
+ return left.Vector3 / (float)right.Double;
+
if (left.Type == VariantType.Vector3D && right.Type == VariantType.Double)
return Avalonia.Vector3D.Divide(left.Vector3D, right.Double);
if (left.Type == VariantType.Vector4 && right.Type == VariantType.Vector4)
return left.Vector4 / right.Vector4;
- if (left.Type == VariantType.Vector4 && right.Type == VariantType.Scalar)
- return left.Vector4 / right.Scalar;
-
+ if (left.Type == VariantType.Vector4 && right.Type == VariantType.Double)
+ return left.Vector4 / (float)right.Double;
+
if (left.Type == VariantType.Quaternion && right.Type == VariantType.Quaternion)
return left.Quaternion / right.Quaternion;
@@ -564,11 +534,7 @@ namespace Avalonia.Rendering.Composition.Expressions
public ExpressionVariant EqualsTo(ExpressionVariant right)
{
if (Type != right.Type || Type == VariantType.Invalid)
- return default;
-
- if (Type == VariantType.Scalar)
- return Scalar == right.Scalar;
-
+ return default;
if (Type == VariantType.Double)
return Double == right.Double;
@@ -623,8 +589,6 @@ namespace Avalonia.Rendering.Composition.Expressions
public static ExpressionVariant operator %(ExpressionVariant left, ExpressionVariant right)
{
- if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar)
- return left.Scalar % right.Scalar;
if (left.Type == VariantType.Double && right.Type == VariantType.Double)
return left.Double % right.Double;
return default;
@@ -632,18 +596,13 @@ namespace Avalonia.Rendering.Composition.Expressions
public static ExpressionVariant operator <(ExpressionVariant left, ExpressionVariant right)
{
- if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar)
- return left.Scalar < right.Scalar;
if (left.Type == VariantType.Double && right.Type == VariantType.Double)
return left.Double < right.Double;
return default;
}
public static ExpressionVariant operator >(ExpressionVariant left, ExpressionVariant right)
- {
- if (left.Type == VariantType.Scalar && right.Type == VariantType.Scalar)
- return left.Scalar > right.Scalar;
-
+ {
if (left.Type == VariantType.Double && right.Type == VariantType.Double)
return left.Double > right.Double;
return default;
@@ -659,207 +618,89 @@ namespace Avalonia.Rendering.Composition.Expressions
public ExpressionVariant Or(ExpressionVariant right)
{
if (Type == VariantType.Boolean && right.Type == VariantType.Boolean)
- return Boolean && right.Boolean;
+ return Boolean || right.Boolean;
return default;
}
public bool TryCast(out T res) where T : struct
{
- if (typeof(T) == typeof(bool))
- {
- if (Type == VariantType.Boolean)
- {
- res = (T) (object) Boolean;
- return true;
- }
- }
-
- if (typeof(T) == typeof(float))
+ switch (default(T))
{
- if (Type == VariantType.Scalar)
- {
- res = (T) (object) Scalar;
- return true;
- }
- if (Type == VariantType.Double)
- {
- res = (T)(object)Scalar;
+ case bool when Type is VariantType.Boolean:
+ res = (T)(object)Boolean;
return true;
- }
- }
-
- if (typeof(T) == typeof(double))
- {
- if (Type == VariantType.Double)
- {
- res = (T) (object) Double;
- return true;
- }
-
- if (Type == VariantType.Scalar)
- {
+ case float when Type is VariantType.Double:
res = (T)(object)(float)Double;
return true;
- }
- }
-
- if (typeof(T) == typeof(Vector2))
- {
- if (Type == VariantType.Vector2)
- {
- res = (T) (object) Vector2;
+ case double when Type is VariantType.Double:
+ res = (T)(object)Double;
return true;
- }
-
- if (Type == VariantType.Vector)
- {
- res = (T) (object) Vector.ToVector2();
+ case System.Numerics.Vector2 when Type is VariantType.Vector2:
+ res = (T)(object)Vector2;
return true;
- }
- }
-
- if (typeof(T) == typeof(Vector))
- {
- if (Type == VariantType.Vector)
- {
- res = (T) (object) Vector;
+ case System.Numerics.Vector2 when Type is VariantType.Vector:
+ res = (T)(object)Vector.ToVector2();
return true;
- }
-
- if (Type == VariantType.Vector2)
- {
+ case Avalonia.Vector when Type is VariantType.Vector:
+ res = (T)(object)Vector;
+ return true;
+ case Avalonia.Vector when Type is VariantType.Vector2:
res = (T)(object)new Vector(Vector2);
return true;
- }
- }
-
- if (typeof(T) == typeof(Vector3))
- {
- if (Type == VariantType.Vector3)
- {
- res = (T) (object) Vector3;
+ case System.Numerics.Vector3 when Type is VariantType.Vector3:
+ res = (T)(object)Vector3;
return true;
- }
- if (Type == VariantType.Vector3D)
- {
- res = (T) (object) Vector3D.ToVector3();
+ case System.Numerics.Vector3 when Type is VariantType.Vector3D:
+ res = (T)(object)Vector3D.ToVector3();
return true;
- }
- }
-
- if (typeof(T) == typeof(Vector3D))
- {
- if (Type == VariantType.Vector3D)
- {
- res = (T) (object) Vector3D;
+ case Avalonia.Vector3D when Type is VariantType.Vector3D:
+ res = (T)(object)Vector3D;
return true;
- }
-
- if (Type == VariantType.Vector3)
- {
+ case Avalonia.Vector3D when Type is VariantType.Vector3:
res = (T)(object)new Vector3D(Vector3);
return true;
- }
- }
-
- if (typeof(T) == typeof(Vector4))
- {
- if (Type == VariantType.Vector4)
- {
- res = (T) (object) Vector4;
+ case System.Numerics.Vector4 when Type is VariantType.Vector4:
+ res = (T)(object)Vector4;
return true;
- }
- }
-
- if (typeof(T) == typeof(Matrix3x2))
- {
- if (Type == VariantType.Matrix3x2)
- {
- res = (T) (object) Matrix3x2;
+ case System.Numerics.Matrix3x2 when Type is VariantType.Matrix3x2:
+ res = (T)(object)Matrix3x2;
return true;
- }
- }
-
- if (typeof(T) == typeof(Matrix))
- {
- if (Type == VariantType.AvaloniaMatrix)
- {
- res = (T) (object) Matrix3x2;
+ case Avalonia.Matrix when Type is VariantType.AvaloniaMatrix:
+ res = (T)(object)AvaloniaMatrix;
return true;
- }
- }
-
- if (typeof(T) == typeof(Matrix4x4))
- {
- if (Type == VariantType.Matrix4x4)
- {
- res = (T) (object) Matrix4x4;
+ case System.Numerics.Matrix4x4 when Type is VariantType.Matrix4x4:
+ res = (T)(object)Matrix4x4;
return true;
- }
- }
-
- if (typeof(T) == typeof(Quaternion))
- {
- if (Type == VariantType.Quaternion)
- {
- res = (T) (object) Quaternion;
+ case System.Numerics.Quaternion when Type is VariantType.Quaternion:
+ res = (T)(object)Quaternion;
return true;
- }
- }
-
- if (typeof(T) == typeof(Avalonia.Media.Color))
- {
- if (Type == VariantType.Color)
- {
- res = (T) (object) Color;
+ case Avalonia.Media.Color when Type is VariantType.Color:
+ res = (T)(object)Color;
return true;
- }
+ default:
+ res = default;
+ return false;
}
-
- res = default;
- return false;
}
public static ExpressionVariant Create(T v) where T : struct
- {
- if (typeof(T) == typeof(bool))
- return (bool) (object) v;
-
- if (typeof(T) == typeof(float))
- return (float) (object) v;
-
- if (typeof(T) == typeof(Vector2))
- return (Vector2) (object) v;
-
- if (typeof(T) == typeof(Vector))
- return (Vector) (object) v;
-
- if (typeof(T) == typeof(Vector3))
- return (Vector3) (object) v;
-
- if (typeof(T) == typeof(Vector3D))
- return (Vector3D) (object) v;
-
- if (typeof(T) == typeof(Vector4))
- return (Vector4) (object) v;
-
- if (typeof(T) == typeof(Matrix3x2))
- return (Matrix3x2) (object) v;
-
- if (typeof(T) == typeof(Matrix))
- return (Matrix) (object) v;
-
- if (typeof(T) == typeof(Matrix4x4))
- return (Matrix4x4) (object) v;
-
- if (typeof(T) == typeof(Quaternion))
- return (Quaternion) (object) v;
-
- if (typeof(T) == typeof(Avalonia.Media.Color))
- return (Avalonia.Media.Color) (object) v;
-
- throw new ArgumentException("Invalid variant type: " + typeof(T));
- }
+ => default(T) switch
+ {
+ bool => (bool)(object)v,
+ float => (float)(object)v,
+ double => (double)(object)v,
+ System.Numerics.Vector2 => (Vector2)(object)v,
+ Avalonia.Vector => (Vector)(object)v,
+ System.Numerics.Vector3 => (Vector3)(object)v,
+ Avalonia.Vector3D => (Vector3D)(object)v,
+ System.Numerics.Vector4 => (Vector4)(object)v,
+ System.Numerics.Matrix3x2 => (Matrix3x2)(object)v,
+ Avalonia.Matrix => (Matrix)(object)v,
+ System.Numerics.Matrix4x4 => (Matrix4x4)(object)v,
+ System.Numerics.Quaternion => (Quaternion)(object)v,
+ Avalonia.Media.Color => (Avalonia.Media.Color)(object)v,
+ _ => throw new ArgumentException("Invalid variant type: " + typeof(T))
+ };
public T CastOrDefault() where T : struct
{
@@ -869,35 +710,23 @@ namespace Avalonia.Rendering.Composition.Expressions
public override string ToString()
{
- if (Type == VariantType.Boolean)
- return Boolean.ToString();
- if (Type == VariantType.Scalar)
- return Scalar.ToString(CultureInfo.InvariantCulture);
- if (Type == VariantType.Double)
- return Double.ToString(CultureInfo.InvariantCulture);
- if (Type == VariantType.Vector2)
- return Vector2.ToString();
- if (Type == VariantType.Vector)
- return Vector.ToString();
- if (Type == VariantType.Vector3)
- return Vector3.ToString();
- if (Type == VariantType.Vector3D)
- return Vector3D.ToString();
- if (Type == VariantType.Vector4)
- return Vector4.ToString();
- if (Type == VariantType.Quaternion)
- return Quaternion.ToString();
- if (Type == VariantType.Matrix3x2)
- return Matrix3x2.ToString();
- if (Type == VariantType.AvaloniaMatrix)
- return AvaloniaMatrix.ToString();
- if (Type == VariantType.Matrix4x4)
- return Matrix4x4.ToString();
- if (Type == VariantType.Color)
- return Color.ToString();
- if (Type == VariantType.Invalid)
- return "Invalid";
- return "Unknown";
+ return Type switch
+ {
+ VariantType.Boolean => Boolean.ToString(),
+ VariantType.Double => Double.ToString(CultureInfo.InvariantCulture),
+ VariantType.Vector2 => Vector2.ToString(),
+ VariantType.Vector => Vector.ToString(),
+ VariantType.Vector3 => Vector3.ToString(),
+ VariantType.Vector3D => Vector3D.ToString(),
+ VariantType.Vector4 => Vector4.ToString(),
+ VariantType.Quaternion => Quaternion.ToString(),
+ VariantType.Matrix3x2 => Matrix3x2.ToString(),
+ VariantType.AvaloniaMatrix => AvaloniaMatrix.ToString(),
+ VariantType.Matrix4x4 => Matrix4x4.ToString(),
+ VariantType.Color => Color.ToString(),
+ VariantType.Invalid => "Invalid",
+ _ => "Unknown"
+ };
}
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs b/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs
index 180c717803..a0aea862de 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/CompositionProperty.cs
@@ -1,7 +1,4 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
using Avalonia.Rendering.Composition.Expressions;
namespace Avalonia.Rendering.Composition.Server;
@@ -11,15 +8,6 @@ internal class CompositionProperty
private static int s_nextId = 1;
private static readonly object _lock = new();
- private static Dictionary> s_dynamicRegistry = new();
-
- class ReadOnlyRegistry : Dictionary>
- {
-
- }
-
- private static volatile ReadOnlyRegistry? s_ReadOnlyRegistry;
-
public CompositionProperty(int id, string name, Type owner, Func? getVariant)
{
Id = id;
@@ -43,59 +31,8 @@ internal class CompositionProperty
prop = new CompositionProperty(id, name, typeof(TOwner), getField, setField, getVariant);
}
- s_ReadOnlyRegistry = null;
return prop;
}
-
- static void PopulatePropertiesForType(Type type, List l)
- {
- Type? t = type;
- while (t != null && t != typeof(object))
- {
- if (s_dynamicRegistry.TryGetValue(t, out var lst))
- l.AddRange(lst);
- t = t.BaseType;
- }
- }
-
- static ReadOnlyRegistry Build()
- {
- var reg = new ReadOnlyRegistry();
- foreach (var type in s_dynamicRegistry.Keys)
- {
- var lst = new List();
- PopulatePropertiesForType(type, lst);
- reg[type] = lst.ToDictionary(x => x.Name);
- }
-
- return reg;
- }
-
- public static IReadOnlyDictionary? TryGetPropertiesForType(Type t)
- {
- GetRegistry().TryGetValue(t, out var rv);
- return rv;
- }
-
- public static CompositionProperty? Find(Type owner, string name)
- {
- if (TryGetPropertiesForType(owner)?.TryGetValue(name, out var prop) == true)
- return prop;
- return null;
- }
-
- static ReadOnlyRegistry GetRegistry()
- {
- var reg = s_ReadOnlyRegistry;
- if (reg != null)
- return reg;
- lock (_lock)
- {
- // ReSharper disable once NonAtomicCompoundOperator
- // This is the only line ever that would set the field to a not-null value, and we are inside of a lock
- return s_ReadOnlyRegistry ??= Build();
- }
- }
}
internal class CompositionProperty : CompositionProperty
@@ -112,4 +49,4 @@ internal class CompositionProperty : CompositionProperty
GetField = getField;
SetField = setField;
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs
index 0e59cd8f03..525bafb8a1 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositorAnimations.cs
@@ -26,7 +26,12 @@ internal class ServerCompositorAnimations
_clockItemsToUpdate.Clear();
while (_dirtyAnimatedObjectQueue.Count > 0)
- _dirtyAnimatedObjectQueue.Dequeue().EvaluateAnimations();
+ {
+ var animation = _dirtyAnimatedObjectQueue.Dequeue();
+ _dirtyAnimatedObjects.Remove(animation);
+ animation.EvaluateAnimations();
+ }
+
_dirtyAnimatedObjects.Clear();
}
@@ -37,4 +42,4 @@ internal class ServerCompositorAnimations
if (_dirtyAnimatedObjects.Add(obj))
_dirtyAnimatedObjectQueue.Enqueue(obj);
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
index f975e8e726..ac022f5356 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
@@ -71,7 +71,7 @@ namespace Avalonia.Rendering.Composition.Server
ExpressionVariant IExpressionObject.GetProperty(string name)
{
if (_animations == null)
- return CompositionProperty.Find(this.GetType(), name)?.GetVariant?.Invoke(this) ?? default;
+ return GetCompositionProperty(name)?.GetVariant?.Invoke(this) ?? default;
return _animations.GetPropertyForAnimation(name);
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObjectAnimations.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObjectAnimations.cs
index 1d213781c7..c62c82ea5c 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObjectAnimations.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObjectAnimations.cs
@@ -12,18 +12,15 @@ class ServerObjectAnimations
private readonly ServerObject _owner;
private InlineDictionary _subscriptions;
private InlineDictionary _animations;
- private readonly IReadOnlyDictionary _properties;
public ServerObjectAnimations(ServerObject owner)
{
_owner = owner;
- _properties = CompositionProperty.TryGetPropertiesForType(owner.GetType()) ??
- new Dictionary();
}
private class ServerObjectSubscriptionStore
{
- public bool IsValid;
+ public bool IsValid = true;
public RefTrackingDictionary? Subscribers;
public void Invalidate()
@@ -84,6 +81,7 @@ class ServerObjectAnimations
NeedsUpdate = false;
_property.SetField(Owner._owner, GetVariant().CastOrDefault());
Owner._owner.NotifyAnimatedValueChanged(_property);
+ Owner.OnSetDirectValue(_property);
}
}
}
@@ -143,7 +141,8 @@ class ServerObjectAnimations
public ExpressionVariant GetPropertyForAnimation(string name)
{
- if (!_properties.TryGetValue(name, out var prop))
+ var prop = _owner.GetCompositionProperty(name);
+ if (prop is null)
return default;
if (_subscriptions.TryGetValue(prop, out var subs))
@@ -172,4 +171,4 @@ class ServerObjectAnimations
else
Debug.Assert(false);
}
-}
\ No newline at end of file
+}
diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs
index 4210dfc308..b6b7eccb78 100644
--- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs
+++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs
@@ -426,6 +426,7 @@ return;
{
"bool",
"float",
+ "double",
"Vector2",
"Vector3",
"Vector4",
@@ -435,6 +436,7 @@ return;
"Quaternion",
"Color",
"Avalonia.Media.Color",
+ "Vector",
"Vector3D"
};
diff --git a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationParserTests.cs b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationParserTests.cs
index fc6ea969d8..5457138406 100644
--- a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationParserTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationParserTests.cs
@@ -28,12 +28,10 @@ public class CompositionAnimationParserTests
};
var res = expr.Evaluate(ref ctx);
double doubleRes;
- if (res.Type == VariantType.Scalar)
- doubleRes = res.Scalar;
- else if (res.Type == VariantType.Double)
+ if (res.Type == VariantType.Double)
doubleRes = res.Double;
else
throw new Exception("Invalid result type: " + res.Type);
Assert.Equal(value, doubleRes);
}
-}
\ No newline at end of file
+}
diff --git a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs
index 21ac9c1ae1..9faca90841 100644
--- a/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationTests.cs
@@ -1,10 +1,9 @@
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Animation.Easings;
-using Avalonia.Base.UnitTests.Rendering;
+using Avalonia.Controls;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Expressions;
@@ -81,7 +80,7 @@ public class CompositionAnimationTests : ScopedTestBase
public void Post(Action action, DispatcherPriority priority = default) => throw new NotSupportedException();
}
-
+
[AnimationDataProvider]
[Theory]
public void GenericCheck(AnimationData data)
@@ -100,11 +99,11 @@ public class CompositionAnimationTests : ScopedTestBase
foreach (var check in data.Checks)
{
currentValue = instance.Evaluate(TimeSpan.FromSeconds(check.time), currentValue);
- Assert.Equal(check.value, currentValue.Scalar);
+ Assert.Equal(check.value, currentValue.Double);
}
-
+
}
-
+
public class AnimationData
{
public AnimationData(string name)
@@ -112,7 +111,7 @@ public class CompositionAnimationTests : ScopedTestBase
Name = name;
}
- public string Name { get; }
+ public string Name { get; }
public List<(float key, float value)> Frames { get; set; } = new();
public List<(float time, float value)> Checks { get; set; } = new();
public float StartingValue { get; set; }
@@ -122,4 +121,124 @@ public class CompositionAnimationTests : ScopedTestBase
return Name;
}
}
+
+ [Theory]
+ [InlineData("Color")]
+ [InlineData("Offset")]
+
+ public void GetCompositionProperty_ReturnsRegisteredProperties(string propName)
+ {
+ using var scope = AvaloniaLocator.EnterScope();
+ var compositor = new Compositor(RenderLoop.FromTimer(new CompositorTestServices.ManualRenderTimer()), null);
+ var target = compositor.CreateSolidColorVisual();
+
+ var property = target.Server.GetCompositionProperty(propName);
+
+ Assert.NotNull(property);
+ Assert.Equal(propName, property.Name);
+ Assert.NotNull(property.GetVariant);
+ }
+
+ [Fact]
+ public void ExpressionAnimation_Operations_WorksCorrectly()
+ {
+ using var scope = AvaloniaLocator.EnterScope();
+ var compositor = new Compositor(RenderLoop.FromTimer(new CompositorTestServices.ManualRenderTimer()), null);
+ var target = compositor.CreateSolidColorVisual();
+ target.Server.Offset = new Vector3D(100, 200, 0);
+
+ var ani = compositor.CreateExpressionAnimation("this.Target.Offset.X * 0.5 + 10");
+ var instance = ani.CreateInstance(target.Server, null);
+ instance.Initialize(TimeSpan.Zero, ExpressionVariant.Create(0f),
+ ServerCompositionVisual.s_IdOfRotationAngleProperty);
+
+ var result = instance.Evaluate(TimeSpan.Zero, ExpressionVariant.Create(0f));
+
+ Assert.Equal(VariantType.Double, result.Type);
+ Assert.Equal(60.0, result.Double);
+ }
+
+
+ [Fact]
+ public void ExpressionAnimation_Tracks_ReferenceParameter()
+ {
+ using var scope = AvaloniaLocator.EnterScope();
+ var compositor = new Compositor(RenderLoop.FromTimer(new CompositorTestServices.ManualRenderTimer()), null);
+ var target = compositor.CreateSolidColorVisual();
+ var obj = compositor.CreateSolidColorVisual();
+ obj.Server.Offset = new Vector3D(100, 200, 0);
+
+ var ani = compositor.CreateExpressionAnimation("obj.Offset.X * 0.5 + 10");
+ ani.SetReferenceParameter("obj", obj);
+ var instance = ani.CreateInstance(target.Server, null);
+
+ target.Server.Activate();
+
+ // Invoke OnSetAnimatedValue manually to create ServerObjectAnimationInstance.
+ target.Server.GetOrCreateAnimations();
+ var tmp = 0f;
+ target.Server.Animations!.OnSetAnimatedValue(ServerCompositionVisual.s_IdOfRotationAngleProperty, ref tmp, TimeSpan.Zero, instance);
+
+ var initialResult = instance.Evaluate(TimeSpan.Zero, ExpressionVariant.Create(0f));
+ Assert.Equal(60.0, initialResult.Double);
+
+ obj.Server.Offset = new Vector3D(200, 300, 0);
+ var updatedResult = instance.Evaluate(TimeSpan.Zero, ExpressionVariant.Create(0f));
+ Assert.Equal(110.0, updatedResult.Double);
+ }
+
+ [Fact]
+ public void ExpressionAnimation_Tracks_Target()
+ {
+ using var scope = AvaloniaLocator.EnterScope();
+ var compositor = new Compositor(RenderLoop.FromTimer(new CompositorTestServices.ManualRenderTimer()), null);
+ var target = compositor.CreateSolidColorVisual();
+
+ target.Server.Offset = new Vector3D(100, 200, 0);
+
+ var ani = compositor.CreateExpressionAnimation("this.Target.Offset.X * 0.5 + 10");
+ var instance = ani.CreateInstance(target.Server, null);
+
+ target.Server.Activate();
+
+ // Invoke OnSetAnimatedValue manually to create ServerObjectAnimationInstance.
+ target.Server.GetOrCreateAnimations();
+ var tmp = 0f;
+ target.Server.Animations!.OnSetAnimatedValue(ServerCompositionVisual.s_IdOfRotationAngleProperty, ref tmp, TimeSpan.Zero, instance);
+
+ var initialResult = instance.Evaluate(TimeSpan.Zero, ExpressionVariant.Create(0f));
+ Assert.Equal(60, initialResult.Double);
+
+ target.Server.Offset = new Vector3D(200, 300, 0);
+ var updatedResult = instance.Evaluate(TimeSpan.Zero, ExpressionVariant.Create(0f));
+ Assert.Equal(110.0, updatedResult.Double);
+ }
+
+ [Fact]
+ public void ExpressionAnimation_Requeues_Target_When_Another_Animation_Is_Invalidated_During_Evaluation()
+ {
+ using var services = new CompositorTestServices();
+ var border = new Border
+ {
+ Width = 10,
+ Height = 10
+ };
+
+ services.TopLevel.Content = border;
+ services.RunJobs();
+
+ var visual = ElementComposition.GetElementVisual(border)!;
+ var opacityAnimation = visual.Compositor.CreateExpressionAnimation("this.Target.RotationAngle * 0.1");
+ var rotationAnimation = visual.Compositor.CreateExpressionAnimation("this.Target.Offset.X * 0.5");
+
+ visual.StartAnimation("Opacity", opacityAnimation);
+ visual.StartAnimation("RotationAngle", rotationAnimation);
+
+ services.RunJobs();
+ visual.Offset = new Vector3D(100, 0, 0);
+ services.RunJobs();
+
+ Assert.Equal(50, visual.Server.RotationAngle);
+ Assert.Equal(5, visual.Server.Opacity);
+ }
}
From 4218e0ea7d330a017acd0433752885ff633836f7 Mon Sep 17 00:00:00 2001
From: Max Katz
Date: Wed, 1 Apr 2026 19:02:14 -0700
Subject: [PATCH 45/57] Add check for forked PRs in update-api workflow
---
.github/workflows/update-api.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/update-api.yml b/.github/workflows/update-api.yml
index 27a0598d3b..5e45fcd567 100644
--- a/.github/workflows/update-api.yml
+++ b/.github/workflows/update-api.yml
@@ -59,7 +59,9 @@ jobs:
repo: context.repo.repo,
pull_number: context.issue.number,
});
- if (pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) {
+ const isFork = pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`;
+ const isTrustedOverride = context.payload.comment.body.includes('/trusted');
+ if (isFork && !isTrustedOverride) {
core.setFailed('Cannot run /update-api on fork PRs — would execute untrusted code with write permissions.');
return;
}
From 853975ca7c786bb0d622c2934fb4087e9ace6f22 Mon Sep 17 00:00:00 2001
From: Julien Lebosquain
Date: Thu, 2 Apr 2026 09:17:18 +0200
Subject: [PATCH 46/57] Switch solution to SLNX (#21057)
* Switch solution to SLNX
* Address review
---
Avalonia.Desktop.slnf | 2 +-
Avalonia.sln | 765 ------------------
Avalonia.slnx | 197 +++++
...n.DotSettings => Avalonia.slnx.DotSettings | 0
docs/build.md | 2 +-
nukebuild/Build.cs | 2 +-
tests/BuildTests/BuildTests.sln | 75 --
tests/BuildTests/BuildTests.slnx | 16 +
8 files changed, 216 insertions(+), 843 deletions(-)
delete mode 100644 Avalonia.sln
create mode 100644 Avalonia.slnx
rename Avalonia.sln.DotSettings => Avalonia.slnx.DotSettings (100%)
delete mode 100644 tests/BuildTests/BuildTests.sln
create mode 100644 tests/BuildTests/BuildTests.slnx
diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf
index dfa0945890..3edf421eb4 100644
--- a/Avalonia.Desktop.slnf
+++ b/Avalonia.Desktop.slnf
@@ -1,6 +1,6 @@
{
"solution": {
- "path": "Avalonia.sln",
+ "path": "Avalonia.slnx",
"projects": [
"packages\\Avalonia\\Avalonia.csproj",
"samples\\AppWithoutLifetime\\AppWithoutLifetime.csproj",
diff --git a/Avalonia.sln b/Avalonia.sln
deleted file mode 100644
index 3b86c4df47..0000000000
--- a/Avalonia.sln
+++ /dev/null
@@ -1,765 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31903.59
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Windows", "Windows", "{B39A8919-9F95-48FE-AD7B-76E08B509888}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32", "src\Windows\Avalonia.Win32\Avalonia.Win32.csproj", "{811A76CF-1CF6-440F-963B-BBE31BD72A82}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls", "src\Avalonia.Controls\Avalonia.Controls.csproj", "{D2221C82-4A25-4583-9B43-D791E3F6820C}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Simple", "src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.UnitTests", "tests\Avalonia.Controls.UnitTests\Avalonia.Controls.UnitTests.csproj", "{5CCB5571-7C30-4E7D-967D-0E2158EBD91F}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base.UnitTests", "tests\Avalonia.Base.UnitTests\Avalonia.Base.UnitTests.csproj", "{2905FF23-53FB-45E6-AA49-6AF47A172056}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.UnitTests", "tests\Avalonia.Markup.Xaml.UnitTests\Avalonia.Markup.Xaml.UnitTests.csproj", "{99135EAB-653D-47E4-A378-C96E1278CA44}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Markup", "Markup", "{8B6A8209-894F-4BA1-B880-965FD453982C}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml", "src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj", "{3E53A01A-B331-47F3-B828-4A5717E77A24}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{9B9E3891-2366-4253-A952-D08BCEB71098}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}"
- ProjectSection(SolutionItems) = preProject
- .editorconfig = .editorconfig
- src\Shared\CallerArgumentExpressionAttribute.cs = src\Shared\CallerArgumentExpressionAttribute.cs
- src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
- src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
- src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
- src\Shared\StreamCompatibilityExtensions.cs = src\Shared\StreamCompatibilityExtensions.cs
- src\Shared\StringCompatibilityExtensions.cs = src\Shared\StringCompatibilityExtensions.cs
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup", "src\Markup\Avalonia.Markup\Avalonia.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.UnitTests", "tests\Avalonia.Markup.UnitTests\Avalonia.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingDemo", "samples\BindingDemo\BindingDemo.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skia", "Skia", "{3743B0F2-CC41-4F14-A8C8-267F579BF91E}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Android", "src\Android\Avalonia.Android\Avalonia.Android.csproj", "{7B92AF71-6287-4693-9DCB-BD5B6E927E23}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "iOS", "iOS", "{0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.iOS", "src\iOS\Avalonia.iOS\Avalonia.iOS.csproj", "{4488AD85-1495-4809-9AA4-DDFE0A48527E}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.LeakTests", "tests\Avalonia.LeakTests\Avalonia.LeakTests.csproj", "{E1AA3DBF-9056-4530-9376-18119A7A3FFE}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.UnitTests", "tests\Avalonia.UnitTests\Avalonia.UnitTests.csproj", "{88060192-33D5-4932-B0F9-8BD2763E857D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Benchmarks", "tests\Avalonia.Benchmarks\Avalonia.Benchmarks.csproj", "{410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport", "src\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj", "{799A7BB5-3C2C-48B6-85A7-406A12C420DA}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog", "samples\ControlCatalog\ControlCatalog.csproj", "{D0A739B9-3C68-4BA6-A328-41606954B6BD}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Desktop", "samples\ControlCatalog.Desktop\ControlCatalog.Desktop.csproj", "{2B888490-D14A-4BCA-AB4B-48676FA93C9B}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualizationDemo", "samples\VirtualizationDemo\VirtualizationDemo.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interop", "Interop", "{A0CC0258-D18C-4AB3-854F-7101680FC3F9}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RenderDemo", "samples\RenderDemo\RenderDemo.csproj", "{F1FDC5B0-4654-416F-AE69-E3E9BBD87801}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia", "src\Skia\Avalonia.Skia\Avalonia.Skia.csproj", "{7D2D3083-71DD-4CC9-8907-39A0D86FB322}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}"
- ProjectSection(SolutionItems) = preProject
- build\AnalyzerProject.targets = build\AnalyzerProject.targets
- build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props
- build\Base.props = build\Base.props
- build\CoreLibraries.props = build\CoreLibraries.props
- build\DevAnalyzers.props = build\DevAnalyzers.props
- build\EmbedXaml.props = build\EmbedXaml.props
- build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
- build\NetAnalyzers.props = build\NetAnalyzers.props
- build\NullableEnable.props = build\NullableEnable.props
- build\ReferenceCoreLibraries.props = build\ReferenceCoreLibraries.props
- build\SampleApp.props = build\SampleApp.props
- build\SharedVersion.props = build\SharedVersion.props
- build\SkiaSharp.props = build\SkiaSharp.props
- build\SourceGenerators.props = build\SourceGenerators.props
- build\SourceLink.props = build\SourceLink.props
- build\TargetFrameworks.props = build\TargetFrameworks.props
- build\TrimmingEnable.props = build\TrimmingEnable.props
- build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
- build\XUnit.props = build\XUnit.props
- build\MicroCOM.props = build\MicroCOM.props
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}"
- ProjectSection(SolutionItems) = preProject
- build\BuildTargets.targets = build\BuildTargets.targets
- build\DevSingleProject.targets = build\DevSingleProject.targets
- build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.LinuxFramebuffer", "src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj", "{854568D5-13D1-4B4F-B50D-534DC7EFD3C9}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32.Interoperability", "src\Windows\Avalonia.Win32.Interoperability\Avalonia.Win32.Interoperability.csproj", "{CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.RenderTests", "tests\Avalonia.Skia.RenderTests\Avalonia.Skia.RenderTests.csproj", "{E1582370-37B3-403C-917F-8209551B1634}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Remote.Protocol", "src\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj", "{D78A720C-C0C6-478B-8564-F167F9BDD01B}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{4ED8B739-6F4E-4CD4-B993-545E6B5CE637}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Designer.HostApp", "src\tools\Avalonia.Designer.HostApp\Avalonia.Designer.HostApp.csproj", "{050CC912-FF49-4A8B-B534-9544017446DD}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.UnitTests", "tests\Avalonia.Skia.UnitTests\Avalonia.Skia.UnitTests.csproj", "{E1240B49-7B4B-4371-A00E-068778C5CF0B}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.OpenGL", "src\Avalonia.OpenGL\Avalonia.OpenGL.csproj", "{7CCAEFC4-135D-401D-BDDD-896B9B7D3569}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Native", "src\Avalonia.Native\Avalonia.Native.csproj", "{12A91A62-C064-42CA-9A8C-A1272F354388}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Packages", "Packages", "{E870DCD7-F46A-498D-83FC-D0FD13E0A11C}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia", "packages\Avalonia\Avalonia.csproj", "{D49233F8-F29C-47DD-9975-C4C9E4502720}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Desktop", "src\Avalonia.Desktop\Avalonia.Desktop.csproj", "{3C471044-3640-45E3-B1B2-16D2FF8399EE}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Build.Tasks", "src\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj", "{BF28998D-072C-439A-AFBB-2FE5021241E0}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "nukebuild\_build.csproj", "{3F00BC43-5095-477F-93D8-E65B08179A00}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Headless\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "src\Headless\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.MicroCom", "src\Avalonia.MicroCom\Avalonia.MicroCom.csproj", "{FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MiniMvvm", "samples\MiniMvvm\MiniMvvm.csproj", "{BC594FD5-4AF2-409E-A1E6-04123F54D7C5}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestApp", "samples\IntegrationTestApp\IntegrationTestApp.csproj", "{676D6BFD-029D-4E43-BFC7-3892265CE251}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextTestApp", "samples\TextTestApp\TextTestApp.csproj", "{CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.IntegrationTests.Appium", "tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj", "{F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Browser", "Browser", "{86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsInteropTest", "samples\interop\WindowsInteropTest\WindowsInteropTest.csproj", "{26A98DA1-D89D-4A95-8152-349F404DA2E2}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\SampleControls\ControlSamples.csproj", "{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{7BF6C69D-FC14-43EB-9ED0-782C16F3D5D9}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SingleProjectSandbox", "samples\SingleProjectSandbox\SingleProjectSandbox.csproj", "{3B8519C1-2F51-4F12-A348-120AB91D4532}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Browser", "src\Browser\Avalonia.Browser\Avalonia.Browser.csproj", "{4A39637C-9338-4925-A4DB-D072E292EC78}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser", "samples\ControlCatalog.Browser\ControlCatalog.Browser.csproj", "{15B93A4C-1B46-43F6-B534-7B25B6E99932}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers.CSharp", "src\tools\Avalonia.Analyzers.CSharp\Avalonia.Analyzers.CSharp.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}"
- ProjectSection(SolutionItems) = preProject
- .editorconfig = .editorconfig
- azure-pipelines-integrationtests.yml = azure-pipelines-integrationtests.yml
- azure-pipelines.yml = azure-pipelines.yml
- CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md
- CONTRIBUTING.md = CONTRIBUTING.md
- Directory.Build.props = Directory.Build.props
- Directory.Build.targets = Directory.Build.targets
- dirs.proj = dirs.proj
- global.json = global.json
- licence.md = licence.md
- NOTICE.md = NOTICE.md
- NuGet.Config = NuGet.Config
- readme.md = readme.md
- Directory.Packages.props = Directory.Packages.props
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppWithoutLifetime", "samples\AppWithoutLifetime\AppWithoutLifetime.csproj", "{F8928267-688E-4A51-989C-612A72446D33}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo", "samples\SafeAreaDemo\SafeAreaDemo.csproj", "{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Android", "samples\SafeAreaDemo.Android\SafeAreaDemo.Android.csproj", "{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Desktop", "samples\SafeAreaDemo.Desktop\SafeAreaDemo.Desktop.csproj", "{4CDAD037-34A2-4CCF-A03A-C6C7B988A572}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.iOS", "samples\SafeAreaDemo.iOS\SafeAreaDemo.iOS.csproj", "{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF237916-7150-496B-89ED-6CA3292896E7}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.NUnit", "src\Headless\Avalonia.Headless.NUnit\Avalonia.Headless.NUnit.csproj", "{ED976634-B118-43F8-8B26-0279C7A7044F}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Metal", "src\Avalonia.Metal\Avalonia.Metal.csproj", "{60B4ED1F-ECFA-453B-8A70-1788261C8355}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Build.Tasks.UnitTest", "tests\Avalonia.Build.Tasks.UnitTest\Avalonia.Build.Tasks.UnitTest.csproj", "{B0FD6A48-FBAB-4676-B36A-DE76B0922B12}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestFiles", "TestFiles", "{9D6AEF22-221F-4F4B-B335-A4BA510F002C}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildTasks", "BuildTasks", "{5BF0C3B8-E595-4940-AB30-2DA206C2F085}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PInvoke", "tests\TestFiles\BuildTasks\PInvoke\PInvoke.csproj", "{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnloadableAssemblyLoadContext", "UnloadableAssemblyLoadContext", "{9CCA131B-DE95-4D44-8788-C3CAE28574CD}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnloadableAssemblyLoadContext", "samples\UnloadableAssemblyLoadContext\UnloadableAssemblyLoadContext\UnloadableAssemblyLoadContext.csproj", "{D7FE3E0F-3FE0-4F87-A2F5-24F1454D84C0}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnloadableAssemblyLoadContextPlug", "samples\UnloadableAssemblyLoadContext\UnloadableAssemblyLoadContextPlug\UnloadableAssemblyLoadContextPlug.csproj", "{DA5F1FF9-4259-4C54-B443-85CFA226EE6A}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Vulkan", "src\Avalonia.Vulkan\Avalonia.Vulkan.csproj", "{3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.RenderTests.WpfCompare", "tests\Avalonia.RenderTests.WpfCompare\Avalonia.RenderTests.WpfCompare.csproj", "{9AE1B827-21AC-4063-AB22-C8804B7F931E}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32.Automation", "src\Windows\Avalonia.Win32.Automation\Avalonia.Win32.Automation.csproj", "{0097673D-DBCE-476E-82FE-E78A56E58AA2}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XEmbedSample", "samples\XEmbedSample\XEmbedSample.csproj", "{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.MacCatalyst", "samples\ControlCatalog.MacCatalyst\ControlCatalog.MacCatalyst.csproj", "{DE3C28DD-B602-4750-831D-345102A54CA0}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.tvOS", "samples\ControlCatalog.tvOS\ControlCatalog.tvOS.csproj", "{14342787-B4EF-4076-8C91-BA6C523DE8DF}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HarfBuzz", "HarfBuzz", "{7670D720-6E84-4AFC-8331-A5C399481905}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.HarfBuzz", "src\HarfBuzz\Avalonia.HarfBuzz\Avalonia.HarfBuzz.csproj", "{E2BFA463-6402-4EF8-8945-FD9A10A914D1}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.PerAssembly.UnitTests", "tests\Avalonia.Headless.NUnit.PerAssembly.UnitTests\Avalonia.Headless.NUnit.PerAssembly.UnitTests.csproj", "{A175EFAE-476C-4DAA-87D5-742C18CFCC27}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.PerTest.UnitTests", "tests\Avalonia.Headless.NUnit.PerTest.UnitTests\Avalonia.Headless.NUnit.PerTest.UnitTests.csproj", "{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.PerAssembly.UnitTests", "tests\Avalonia.Headless.XUnit.PerAssembly.UnitTests\Avalonia.Headless.XUnit.PerAssembly.UnitTests.csproj", "{342D2657-2F84-493C-B74B-9D2CAE5D9DAB}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.PerTest.UnitTests", "tests\Avalonia.Headless.XUnit.PerTest.UnitTests\Avalonia.Headless.XUnit.PerTest.UnitTests.csproj", "{26918642-829D-4FA2-B60A-BE8D83F4E063}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.IntegrationTests.Win32", "tests\Avalonia.IntegrationTests.Win32\Avalonia.IntegrationTests.Win32.csproj", "{11522B0D-BF31-42D5-8FC5-41E58F319AF9}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Analyzers.CodeFixes.CSharp", "src\tools\Avalonia.Analyzers.CodeFixes.CSharp\Avalonia.Analyzers.CodeFixes.CSharp.csproj", "{FDFB9C25-552D-420B-9D4A-DB0BB6472239}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Analyzers.VisualBasic", "src\tools\Avalonia.Analyzers.VisualBasic\Avalonia.Analyzers.VisualBasic.csproj", "{A7644C3B-B843-44F1-9940-560D56CB0936}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop.AtSpi", "src\Avalonia.FreeDesktop.AtSpi\Avalonia.FreeDesktop.AtSpi.csproj", "{742C3613-514C-4D6B-804A-2A7925F278F3}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DBus.Generators", "src\tools\Avalonia.DBus.Generators\Avalonia.DBus.Generators.csproj", "{98A16FFD-0C99-4665-AC64-DC17E86879A2}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|Any CPU.Build.0 = Release|Any CPU
- {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|Any CPU.Build.0 = Release|Any CPU
- {D2221C82-4A25-4583-9B43-D791E3F6820C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D2221C82-4A25-4583-9B43-D791E3F6820C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|Any CPU.Build.0 = Release|Any CPU
- {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Release|Any CPU.Build.0 = Release|Any CPU
- {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|Any CPU.Build.0 = Release|Any CPU
- {2905FF23-53FB-45E6-AA49-6AF47A172056}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2905FF23-53FB-45E6-AA49-6AF47A172056}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|Any CPU.Build.0 = Release|Any CPU
- {99135EAB-653D-47E4-A378-C96E1278CA44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {99135EAB-653D-47E4-A378-C96E1278CA44}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {99135EAB-653D-47E4-A378-C96E1278CA44}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {99135EAB-653D-47E4-A378-C96E1278CA44}.Release|Any CPU.Build.0 = Release|Any CPU
- {3E53A01A-B331-47F3-B828-4A5717E77A24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3E53A01A-B331-47F3-B828-4A5717E77A24}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3E53A01A-B331-47F3-B828-4A5717E77A24}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3E53A01A-B331-47F3-B828-4A5717E77A24}.Release|Any CPU.Build.0 = Release|Any CPU
- {6417E941-21BC-467B-A771-0DE389353CE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6417E941-21BC-467B-A771-0DE389353CE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6417E941-21BC-467B-A771-0DE389353CE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6417E941-21BC-467B-A771-0DE389353CE6}.Release|Any CPU.Build.0 = Release|Any CPU
- {8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Release|Any CPU.Build.0 = Release|Any CPU
- {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}.Release|Any CPU.Build.0 = Release|Any CPU
- {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|Any CPU.Build.0 = Release|Any CPU
- {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Release|Any CPU.Build.0 = Release|Any CPU
- {E1AA3DBF-9056-4530-9376-18119A7A3FFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E1AA3DBF-9056-4530-9376-18119A7A3FFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E1AA3DBF-9056-4530-9376-18119A7A3FFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E1AA3DBF-9056-4530-9376-18119A7A3FFE}.Release|Any CPU.Build.0 = Release|Any CPU
- {88060192-33D5-4932-B0F9-8BD2763E857D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {88060192-33D5-4932-B0F9-8BD2763E857D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {88060192-33D5-4932-B0F9-8BD2763E857D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {88060192-33D5-4932-B0F9-8BD2763E857D}.Release|Any CPU.Build.0 = Release|Any CPU
- {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B}.Release|Any CPU.Build.0 = Release|Any CPU
- {799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Release|Any CPU.Build.0 = Release|Any CPU
- {D0A739B9-3C68-4BA6-A328-41606954B6BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D0A739B9-3C68-4BA6-A328-41606954B6BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D0A739B9-3C68-4BA6-A328-41606954B6BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D0A739B9-3C68-4BA6-A328-41606954B6BD}.Release|Any CPU.Build.0 = Release|Any CPU
- {2B888490-D14A-4BCA-AB4B-48676FA93C9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2B888490-D14A-4BCA-AB4B-48676FA93C9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2B888490-D14A-4BCA-AB4B-48676FA93C9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2B888490-D14A-4BCA-AB4B-48676FA93C9B}.Release|Any CPU.Build.0 = Release|Any CPU
- {F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|Any CPU.Build.0 = Release|Any CPU
- {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|Any CPU.Build.0 = Release|Any CPU
- {F1FDC5B0-4654-416F-AE69-E3E9BBD87801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F1FDC5B0-4654-416F-AE69-E3E9BBD87801}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F1FDC5B0-4654-416F-AE69-E3E9BBD87801}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F1FDC5B0-4654-416F-AE69-E3E9BBD87801}.Release|Any CPU.Build.0 = Release|Any CPU
- {29132311-1848-4FD6-AE0C-4FF841151BD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {29132311-1848-4FD6-AE0C-4FF841151BD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {29132311-1848-4FD6-AE0C-4FF841151BD3}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
- {29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|Any CPU.Build.0 = Release|Any CPU
- {29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|Any CPU.Deploy.0 = Release|Any CPU
- {7D2D3083-71DD-4CC9-8907-39A0D86FB322}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7D2D3083-71DD-4CC9-8907-39A0D86FB322}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7D2D3083-71DD-4CC9-8907-39A0D86FB322}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7D2D3083-71DD-4CC9-8907-39A0D86FB322}.Release|Any CPU.Build.0 = Release|Any CPU
- {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.Build.0 = Release|Any CPU
- {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|Any CPU.Build.0 = Release|Any CPU
- {E1582370-37B3-403C-917F-8209551B1634}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E1582370-37B3-403C-917F-8209551B1634}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E1582370-37B3-403C-917F-8209551B1634}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E1582370-37B3-403C-917F-8209551B1634}.Release|Any CPU.Build.0 = Release|Any CPU
- {D78A720C-C0C6-478B-8564-F167F9BDD01B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D78A720C-C0C6-478B-8564-F167F9BDD01B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D78A720C-C0C6-478B-8564-F167F9BDD01B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D78A720C-C0C6-478B-8564-F167F9BDD01B}.Release|Any CPU.Build.0 = Release|Any CPU
- {050CC912-FF49-4A8B-B534-9544017446DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {050CC912-FF49-4A8B-B534-9544017446DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {050CC912-FF49-4A8B-B534-9544017446DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {050CC912-FF49-4A8B-B534-9544017446DD}.Release|Any CPU.Build.0 = Release|Any CPU
- {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E1240B49-7B4B-4371-A00E-068778C5CF0B}.Release|Any CPU.Build.0 = Release|Any CPU
- {7CCAEFC4-135D-401D-BDDD-896B9B7D3569}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7CCAEFC4-135D-401D-BDDD-896B9B7D3569}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7CCAEFC4-135D-401D-BDDD-896B9B7D3569}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7CCAEFC4-135D-401D-BDDD-896B9B7D3569}.Release|Any CPU.Build.0 = Release|Any CPU
- {12A91A62-C064-42CA-9A8C-A1272F354388}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {12A91A62-C064-42CA-9A8C-A1272F354388}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {12A91A62-C064-42CA-9A8C-A1272F354388}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {12A91A62-C064-42CA-9A8C-A1272F354388}.Release|Any CPU.Build.0 = Release|Any CPU
- {D49233F8-F29C-47DD-9975-C4C9E4502720}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D49233F8-F29C-47DD-9975-C4C9E4502720}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D49233F8-F29C-47DD-9975-C4C9E4502720}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D49233F8-F29C-47DD-9975-C4C9E4502720}.Release|Any CPU.Build.0 = Release|Any CPU
- {3C471044-3640-45E3-B1B2-16D2FF8399EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3C471044-3640-45E3-B1B2-16D2FF8399EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3C471044-3640-45E3-B1B2-16D2FF8399EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3C471044-3640-45E3-B1B2-16D2FF8399EE}.Release|Any CPU.Build.0 = Release|Any CPU
- {BF28998D-072C-439A-AFBB-2FE5021241E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {BF28998D-072C-439A-AFBB-2FE5021241E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|Any CPU.Build.0 = Release|Any CPU
- {3F00BC43-5095-477F-93D8-E65B08179A00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {41B02319-965D-4945-8005-C1A3D1224165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {41B02319-965D-4945-8005-C1A3D1224165}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {41B02319-965D-4945-8005-C1A3D1224165}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {41B02319-965D-4945-8005-C1A3D1224165}.Release|Any CPU.Build.0 = Release|Any CPU
- {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|Any CPU.Build.0 = Release|Any CPU
- {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.Build.0 = Release|Any CPU
- {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.Build.0 = Release|Any CPU
- {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|Any CPU.Build.0 = Release|Any CPU
- {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.Build.0 = Release|Any CPU
- {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.Build.0 = Release|Any CPU
- {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|Any CPU.Build.0 = Release|Any CPU
- {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.Build.0 = Release|Any CPU
- {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FE2F3E5E-1E34-4972-8DC1-5C2C588E5ECE}.Release|Any CPU.Build.0 = Release|Any CPU
- {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {BC594FD5-4AF2-409E-A1E6-04123F54D7C5}.Release|Any CPU.Build.0 = Release|Any CPU
- {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {676D6BFD-029D-4E43-BFC7-3892265CE251}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {676D6BFD-029D-4E43-BFC7-3892265CE251}.Release|Any CPU.Build.0 = Release|Any CPU
- {CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CE728F96-A593-462C-B8D4-1D5AFFDB5B4F}.Release|Any CPU.Build.0 = Release|Any CPU
- {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A}.Release|Any CPU.Build.0 = Release|Any CPU
- {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {26A98DA1-D89D-4A95-8152-349F404DA2E2}.Release|Any CPU.Build.0 = Release|Any CPU
- {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|Any CPU.Build.0 = Release|Any CPU
- {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.Build.0 = Release|Any CPU
- {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.Build.0 = Release|Any CPU
- {7BF6C69D-FC14-43EB-9ED0-782C16F3D5D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7BF6C69D-FC14-43EB-9ED0-782C16F3D5D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7BF6C69D-FC14-43EB-9ED0-782C16F3D5D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7BF6C69D-FC14-43EB-9ED0-782C16F3D5D9}.Release|Any CPU.Build.0 = Release|Any CPU
- {EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.Build.0 = Release|Any CPU
- {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU
- {3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3B8519C1-2F51-4F12-A348-120AB91D4532}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3B8519C1-2F51-4F12-A348-120AB91D4532}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3B8519C1-2F51-4F12-A348-120AB91D4532}.Release|Any CPU.Build.0 = Release|Any CPU
- {4A39637C-9338-4925-A4DB-D072E292EC78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4A39637C-9338-4925-A4DB-D072E292EC78}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4A39637C-9338-4925-A4DB-D072E292EC78}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4A39637C-9338-4925-A4DB-D072E292EC78}.Release|Any CPU.Build.0 = Release|Any CPU
- {15B93A4C-1B46-43F6-B534-7B25B6E99932}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {15B93A4C-1B46-43F6-B534-7B25B6E99932}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {15B93A4C-1B46-43F6-B534-7B25B6E99932}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {15B93A4C-1B46-43F6-B534-7B25B6E99932}.Release|Any CPU.Build.0 = Release|Any CPU
- {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU
- {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU
- {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU
- {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU
- {DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.Build.0 = Release|Any CPU
- {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU
- {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU
- {F8928267-688E-4A51-989C-612A72446D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F8928267-688E-4A51-989C-612A72446D33}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F8928267-688E-4A51-989C-612A72446D33}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F8928267-688E-4A51-989C-612A72446D33}.Release|Any CPU.Build.0 = Release|Any CPU
- {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Release|Any CPU.Build.0 = Release|Any CPU
- {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
- {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Release|Any CPU.Build.0 = Release|Any CPU
- {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Release|Any CPU.Deploy.0 = Release|Any CPU
- {4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Release|Any CPU.Build.0 = Release|Any CPU
- {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
- {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Build.0 = Release|Any CPU
- {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Deploy.0 = Release|Any CPU
- {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU
- {ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.Build.0 = Release|Any CPU
- {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Release|Any CPU.Build.0 = Release|Any CPU
- {60B4ED1F-ECFA-453B-8A70-1788261C8355}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {60B4ED1F-ECFA-453B-8A70-1788261C8355}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {60B4ED1F-ECFA-453B-8A70-1788261C8355}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {60B4ED1F-ECFA-453B-8A70-1788261C8355}.Release|Any CPU.Build.0 = Release|Any CPU
- {B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Release|Any CPU.Build.0 = Release|Any CPU
- {0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Release|Any CPU.Build.0 = Release|Any CPU
- {D7FE3E0F-3FE0-4F87-A2F5-24F1454D84C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D7FE3E0F-3FE0-4F87-A2F5-24F1454D84C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D7FE3E0F-3FE0-4F87-A2F5-24F1454D84C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D7FE3E0F-3FE0-4F87-A2F5-24F1454D84C0}.Release|Any CPU.Build.0 = Release|Any CPU
- {DA5F1FF9-4259-4C54-B443-85CFA226EE6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DA5F1FF9-4259-4C54-B443-85CFA226EE6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DA5F1FF9-4259-4C54-B443-85CFA226EE6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DA5F1FF9-4259-4C54-B443-85CFA226EE6A}.Release|Any CPU.Build.0 = Release|Any CPU
- {3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3E2DE2B6-13BC-4C27-BCB9-A423B86CAF77}.Release|Any CPU.Build.0 = Release|Any CPU
- {9AE1B827-21AC-4063-AB22-C8804B7F931E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9AE1B827-21AC-4063-AB22-C8804B7F931E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9AE1B827-21AC-4063-AB22-C8804B7F931E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9AE1B827-21AC-4063-AB22-C8804B7F931E}.Release|Any CPU.Build.0 = Release|Any CPU
- {0097673D-DBCE-476E-82FE-E78A56E58AA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0097673D-DBCE-476E-82FE-E78A56E58AA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0097673D-DBCE-476E-82FE-E78A56E58AA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0097673D-DBCE-476E-82FE-E78A56E58AA2}.Release|Any CPU.Build.0 = Release|Any CPU
- {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Release|Any CPU.Build.0 = Release|Any CPU
- {DE3C28DD-B602-4750-831D-345102A54CA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DE3C28DD-B602-4750-831D-345102A54CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DE3C28DD-B602-4750-831D-345102A54CA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DE3C28DD-B602-4750-831D-345102A54CA0}.Release|Any CPU.Build.0 = Release|Any CPU
- {14342787-B4EF-4076-8C91-BA6C523DE8DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {14342787-B4EF-4076-8C91-BA6C523DE8DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {14342787-B4EF-4076-8C91-BA6C523DE8DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {14342787-B4EF-4076-8C91-BA6C523DE8DF}.Release|Any CPU.Build.0 = Release|Any CPU
- {E2BFA463-6402-4EF8-8945-FD9A10A914D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E2BFA463-6402-4EF8-8945-FD9A10A914D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E2BFA463-6402-4EF8-8945-FD9A10A914D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E2BFA463-6402-4EF8-8945-FD9A10A914D1}.Release|Any CPU.Build.0 = Release|Any CPU
- {A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A175EFAE-476C-4DAA-87D5-742C18CFCC27}.Release|Any CPU.Build.0 = Release|Any CPU
- {09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C}.Release|Any CPU.Build.0 = Release|Any CPU
- {342D2657-2F84-493C-B74B-9D2CAE5D9DAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {342D2657-2F84-493C-B74B-9D2CAE5D9DAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {342D2657-2F84-493C-B74B-9D2CAE5D9DAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {342D2657-2F84-493C-B74B-9D2CAE5D9DAB}.Release|Any CPU.Build.0 = Release|Any CPU
- {26918642-829D-4FA2-B60A-BE8D83F4E063}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {26918642-829D-4FA2-B60A-BE8D83F4E063}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {26918642-829D-4FA2-B60A-BE8D83F4E063}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {26918642-829D-4FA2-B60A-BE8D83F4E063}.Release|Any CPU.Build.0 = Release|Any CPU
- {11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Release|Any CPU.Build.0 = Release|Any CPU
- {FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Debug|Any CPU.ActiveCfg = Release|Any CPU
- {FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Debug|Any CPU.Build.0 = Release|Any CPU
- {FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FDFB9C25-552D-420B-9D4A-DB0BB6472239}.Release|Any CPU.Build.0 = Release|Any CPU
- {A7644C3B-B843-44F1-9940-560D56CB0936}.Debug|Any CPU.ActiveCfg = Release|Any CPU
- {A7644C3B-B843-44F1-9940-560D56CB0936}.Debug|Any CPU.Build.0 = Release|Any CPU
- {A7644C3B-B843-44F1-9940-560D56CB0936}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A7644C3B-B843-44F1-9940-560D56CB0936}.Release|Any CPU.Build.0 = Release|Any CPU
- {742C3613-514C-4D6B-804A-2A7925F278F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {742C3613-514C-4D6B-804A-2A7925F278F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {742C3613-514C-4D6B-804A-2A7925F278F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {742C3613-514C-4D6B-804A-2A7925F278F3}.Release|Any CPU.Build.0 = Release|Any CPU
- {98A16FFD-0C99-4665-AC64-DC17E86879A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {98A16FFD-0C99-4665-AC64-DC17E86879A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {98A16FFD-0C99-4665-AC64-DC17E86879A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {98A16FFD-0C99-4665-AC64-DC17E86879A2}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {811A76CF-1CF6-440F-963B-BBE31BD72A82} = {B39A8919-9F95-48FE-AD7B-76E08B509888}
- {5CCB5571-7C30-4E7D-967D-0E2158EBD91F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {2905FF23-53FB-45E6-AA49-6AF47A172056} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {99135EAB-653D-47E4-A378-C96E1278CA44} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {3E53A01A-B331-47F3-B828-4A5717E77A24} = {8B6A8209-894F-4BA1-B880-965FD453982C}
- {6417E941-21BC-467B-A771-0DE389353CE6} = {8B6A8209-894F-4BA1-B880-965FD453982C}
- {8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}
- {4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}
- {E1AA3DBF-9056-4530-9376-18119A7A3FFE} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {88060192-33D5-4932-B0F9-8BD2763E857D} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {410AC439-81A1-4EB5-B5E9-6A7FC6B77F4B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {D0A739B9-3C68-4BA6-A328-41606954B6BD} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {2B888490-D14A-4BCA-AB4B-48676FA93C9B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {F1381F98-4D24-409A-A6C5-1C5B1E08BB08} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {FBCAF3D0-2808-4934-8E96-3F607594517B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {A0CC0258-D18C-4AB3-854F-7101680FC3F9} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {F1FDC5B0-4654-416F-AE69-E3E9BBD87801} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {29132311-1848-4FD6-AE0C-4FF841151BD3} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E}
- {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
- {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} = {B39A8919-9F95-48FE-AD7B-76E08B509888}
- {E1582370-37B3-403C-917F-8209551B1634} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {050CC912-FF49-4A8B-B534-9544017446DD} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
- {E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {D49233F8-F29C-47DD-9975-C4C9E4502720} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C}
- {3C471044-3640-45E3-B1B2-16D2FF8399EE} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C}
- {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
- {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
- {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7}
- {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7}
- {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
- {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {BC594FD5-4AF2-409E-A1E6-04123F54D7C5} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {676D6BFD-029D-4E43-BFC7-3892265CE251} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {CE728F96-A593-462C-B8D4-1D5AFFDB5B4F} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {F2CE566B-E7F6-447A-AB1A-3F574A6FE43A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {26A98DA1-D89D-4A95-8152-349F404DA2E2} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
- {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
- {EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
- {3B8519C1-2F51-4F12-A348-120AB91D4532} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {4A39637C-9338-4925-A4DB-D072E292EC78} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
- {15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
- {DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
- {A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {F8928267-688E-4A51-989C-612A72446D33} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {6B60A970-D5D2-49C2-8BAB-F9C7973B74B6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {4CDAD037-34A2-4CCF-A03A-C6C7B988A572} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7}
- {ED976634-B118-43F8-8B26-0279C7A7044F} = {FF237916-7150-496B-89ED-6CA3292896E7}
- {4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {B0FD6A48-FBAB-4676-B36A-DE76B0922B12} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {9D6AEF22-221F-4F4B-B335-A4BA510F002C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {5BF0C3B8-E595-4940-AB30-2DA206C2F085} = {9D6AEF22-221F-4F4B-B335-A4BA510F002C}
- {0A948D71-99C5-43E9-BACB-B0BA59EA25B4} = {5BF0C3B8-E595-4940-AB30-2DA206C2F085}
- {9CCA131B-DE95-4D44-8788-C3CAE28574CD} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {D7FE3E0F-3FE0-4F87-A2F5-24F1454D84C0} = {9CCA131B-DE95-4D44-8788-C3CAE28574CD}
- {DA5F1FF9-4259-4C54-B443-85CFA226EE6A} = {9CCA131B-DE95-4D44-8788-C3CAE28574CD}
- {9AE1B827-21AC-4063-AB22-C8804B7F931E} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {0097673D-DBCE-476E-82FE-E78A56E58AA2} = {B39A8919-9F95-48FE-AD7B-76E08B509888}
- {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {DE3C28DD-B602-4750-831D-345102A54CA0} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {14342787-B4EF-4076-8C91-BA6C523DE8DF} = {9B9E3891-2366-4253-A952-D08BCEB71098}
- {E2BFA463-6402-4EF8-8945-FD9A10A914D1} = {7670D720-6E84-4AFC-8331-A5C399481905}
- {A175EFAE-476C-4DAA-87D5-742C18CFCC27} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {342D2657-2F84-493C-B74B-9D2CAE5D9DAB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {26918642-829D-4FA2-B60A-BE8D83F4E063} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {11522B0D-BF31-42D5-8FC5-41E58F319AF9} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
- {FDFB9C25-552D-420B-9D4A-DB0BB6472239} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
- {A7644C3B-B843-44F1-9940-560D56CB0936} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
- {742C3613-514C-4D6B-804A-2A7925F278F3} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
- {98A16FFD-0C99-4665-AC64-DC17E86879A2} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
- EndGlobalSection
-EndGlobal
diff --git a/Avalonia.slnx b/Avalonia.slnx
new file mode 100644
index 0000000000..8505649676
--- /dev/null
+++ b/Avalonia.slnx
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Avalonia.sln.DotSettings b/Avalonia.slnx.DotSettings
similarity index 100%
rename from Avalonia.sln.DotSettings
rename to Avalonia.slnx.DotSettings
diff --git a/docs/build.md b/docs/build.md
index 2285e56d01..bd8ae3fc02 100644
--- a/docs/build.md
+++ b/docs/build.md
@@ -42,7 +42,7 @@ dotnet run
If you want to open Avalonia in Visual Studio you have two options:
-- Avalonia.sln: This contains the whole of Avalonia in including desktop, mobile and web. You must have a number of dotnet workloads installed in order to build everything in this solution
+- Avalonia.slnx: This contains the whole of Avalonia in including desktop, mobile and web. You must have a number of dotnet workloads installed in order to build everything in this solution
- Avalonia.Desktop.slnf: This solution filter opens only the parts of Avalonia required to run on desktop. This requires no extra workloads to be installed.
Avalonia requires Visual Studio 2022 or newer. The free Visual Studio Community edition works fine.
diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs
index 41c5cbe0cf..b9e27bdfcc 100644
--- a/nukebuild/Build.cs
+++ b/nukebuild/Build.cs
@@ -481,7 +481,7 @@ partial class Build : NukeBuild
.SetProperty("AvaloniaVersion", Parameters.Version)
.SetProperty("NuGetPackageRoot", nugetCacheDirectory)
.SetPackageDirectory(nugetCacheDirectory)
- .SetProjectFile(buildTestsDirectory / "BuildTests.sln")
+ .SetProjectFile(buildTestsDirectory / "BuildTests.slnx")
.SetProcessAdditionalArguments("--nodeReuse:false"));
// Standard compilation - should have compiled XAML
diff --git a/tests/BuildTests/BuildTests.sln b/tests/BuildTests/BuildTests.sln
deleted file mode 100644
index f049ba4ba2..0000000000
--- a/tests/BuildTests/BuildTests.sln
+++ /dev/null
@@ -1,75 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.3.32811.315
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildTests", "BuildTests\BuildTests.csproj", "{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildTests.Desktop", "BuildTests.Desktop\BuildTests.Desktop.csproj", "{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildTests.Browser", "BuildTests.Browser\BuildTests.Browser.csproj", "{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildTests.iOS", "BuildTests.iOS\BuildTests.iOS.csproj", "{EBD9022F-BC83-4846-9A11-6F7F3772DC64}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildTests.Android", "BuildTests.Android\BuildTests.Android.csproj", "{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3DA99C4E-89E3-4049-9C22-0A7EC60D83D8}"
- ProjectSection(SolutionItems) = preProject
- Directory.Packages.props = Directory.Packages.props
- Directory.Build.props = Directory.Build.props
- Directory.Build.targets = Directory.Build.targets
- IncludeBuildTestsAvaloniaItems.props = IncludeBuildTestsAvaloniaItems.props
- EndProjectSection
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildTests.NativeAot", "BuildTests.NativeAot\BuildTests.NativeAot.csproj", "{767D97D5-4E74-4B54-ACFB-D2D845A2AB85}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildTests.WpfHybrid", "BuildTests.WpfHybrid\BuildTests.WpfHybrid.csproj", "{B84C58C1-AE11-4C10-8E18-8482085486F1}"
-EndProject
-Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BuildTests.FSharp", "BuildTests.FSharp\BuildTests.FSharp.fsproj", "{7040B498-C281-490A-98D4-39FCDADAFDBF}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.Build.0 = Release|Any CPU
- {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.Build.0 = Release|Any CPU
- {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.Build.0 = Release|Any CPU
- {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.Build.0 = Release|Any CPU
- {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.Build.0 = Release|Any CPU
- {767D97D5-4E74-4B54-ACFB-D2D845A2AB85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {767D97D5-4E74-4B54-ACFB-D2D845A2AB85}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {767D97D5-4E74-4B54-ACFB-D2D845A2AB85}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {767D97D5-4E74-4B54-ACFB-D2D845A2AB85}.Release|Any CPU.Build.0 = Release|Any CPU
- {B84C58C1-AE11-4C10-8E18-8482085486F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B84C58C1-AE11-4C10-8E18-8482085486F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B84C58C1-AE11-4C10-8E18-8482085486F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B84C58C1-AE11-4C10-8E18-8482085486F1}.Release|Any CPU.Build.0 = Release|Any CPU
- {7040B498-C281-490A-98D4-39FCDADAFDBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7040B498-C281-490A-98D4-39FCDADAFDBF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7040B498-C281-490A-98D4-39FCDADAFDBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7040B498-C281-490A-98D4-39FCDADAFDBF}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {83CB65B8-011F-4ED7-BCD3-A6CFA935EF7E}
- EndGlobalSection
-EndGlobal
diff --git a/tests/BuildTests/BuildTests.slnx b/tests/BuildTests/BuildTests.slnx
new file mode 100644
index 0000000000..d2dfb5a6d1
--- /dev/null
+++ b/tests/BuildTests/BuildTests.slnx
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 9fcccb94a9d42f76fb7a790496506638e6c60491 Mon Sep 17 00:00:00 2001
From: Jumar Macato <16554748+jmacato@users.noreply.github.com>
Date: Thu, 2 Apr 2026 16:07:11 +0800
Subject: [PATCH 47/57] AvnAccessibilityElement fixes (#21062)
---
native/Avalonia.Native/src/OSX/automation.mm | 33 ++++++++++++++++++--
1 file changed, 30 insertions(+), 3 deletions(-)
diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm
index 005eaf0b50..6eee49f619 100644
--- a/native/Avalonia.Native/src/OSX/automation.mm
+++ b/native/Avalonia.Native/src/OSX/automation.mm
@@ -317,7 +317,30 @@
- (id)accessibilityParent
{
auto parentPeer = _peer->GetParent();
- return parentPeer ? [AvnAccessibilityElement acquire:parentPeer] : [NSApplication sharedApplication];
+
+ if (parentPeer == nullptr)
+ return [NSApplication sharedApplication];
+
+ // When the parent is a root provider, return the AvnView (content view)
+ // rather than the AvnWindow. macOS accessibility requires that the parent
+ // chain is consistent with the children chain: AvnView exposes these
+ // elements as its accessibilityChildren, so the elements must report
+ // AvnView as their accessibilityParent. A mismatch causes macOS to be
+ // unable to resolve AXUIElementRefs back to the correct object, which
+ // makes setter calls like AXUIElementSetAttributeValue silently land on
+ // AvnView instead of the target AvnAccessibilityElement.
+ if (parentPeer->IsRootProvider())
+ {
+ auto window = parentPeer->RootProvider_GetWindow();
+ if (window != nullptr)
+ {
+ auto holder = dynamic_cast(window);
+ if (holder != nullptr)
+ return holder->GetNSView();
+ }
+ }
+
+ return [AvnAccessibilityElement acquire:parentPeer];
}
- (id)accessibilityTopLevelUIElement
@@ -403,7 +426,11 @@
- (BOOL)isAccessibilitySelectorAllowed:(SEL)selector
{
- if (selector == @selector(accessibilityPerformShowMenu))
+ if (selector == @selector(setAccessibilityValue:))
+ {
+ return _peer->IsValueProvider() || _peer->IsRangeValueProvider();
+ }
+ else if (selector == @selector(accessibilityPerformShowMenu))
{
return _peer->IsExpandCollapseProvider() && _peer->ExpandCollapseProvider_GetShowsMenu();
}
@@ -422,7 +449,7 @@
{
return _peer->IsRangeValueProvider();
}
-
+
return [super isAccessibilitySelectorAllowed:selector];
}
From f6feac734c6cde1a08b1bfb8e4598c177c9050ae Mon Sep 17 00:00:00 2001
From: Benedikt Stebner
Date: Fri, 3 Apr 2026 10:17:45 +0200
Subject: [PATCH 48/57] Measure/Arrange TextPresenter with
WidthIncludingTrailingWhitespace (#21067)
---
.../Presenters/TextPresenter.cs | 4 +--
.../Presenters/TextPresenter_Tests.cs | 25 +++++++++++++++++
.../TextBlockTests.cs | 27 +++++++++++++++++++
3 files changed, 54 insertions(+), 2 deletions(-)
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index e723572f95..b7d52e9894 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -647,7 +647,7 @@ namespace Avalonia.Controls.Presenters
InvalidateArrange();
// The textWidth used here is matching that TextBlock uses to measure the text.
- var textWidth = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing;
+ var textWidth = TextLayout.WidthIncludingTrailingWhitespace;
return new Size(textWidth, TextLayout.Height);
}
@@ -655,7 +655,7 @@ namespace Avalonia.Controls.Presenters
{
var finalWidth = finalSize.Width;
- var textWidth = TextLayout.OverhangLeading + TextLayout.WidthIncludingTrailingWhitespace + TextLayout.OverhangTrailing;
+ var textWidth = TextLayout.WidthIncludingTrailingWhitespace;
textWidth = Math.Ceiling(textWidth);
if (finalSize.Width < textWidth)
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs
index f911e0cf2f..db2b2abfed 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs
@@ -75,5 +75,30 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(fontStretch, presenter.TextLayout.TextLines[0].TextRuns[0].Properties!.Typeface.Stretch);
}
}
+
+ [Fact]
+ public void Measure_And_Arrange_Should_Use_WidthIncludingTrailingWhitespace_For_Bounds()
+ {
+ using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+ {
+ var presenter = new TextPresenter
+ {
+ Text = "fy",
+ FontStyle = FontStyle.Italic,
+ FontSize = 48,
+ UseLayoutRounding = false
+ };
+
+ presenter.Measure(Size.Infinity);
+
+ var expectedSize = new Size(presenter.TextLayout.WidthIncludingTrailingWhitespace, presenter.TextLayout.Height);
+
+ Assert.Equal(expectedSize, presenter.DesiredSize);
+
+ presenter.Arrange(new Rect(default, presenter.DesiredSize));
+
+ Assert.Equal(new Rect(default, expectedSize), presenter.Bounds);
+ }
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
index 2df686bbe8..9a3b0e205d 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
@@ -561,6 +561,33 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(0, 0, 32.45454545454545, 19.022727272727273), target.Bounds);
}
+ [Fact]
+ public void Measure_And_Arrange_Should_Use_WidthIncludingTrailingWhitespace_For_Bounds()
+ {
+ using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
+
+ var target = new TextBlock
+ {
+ Text = "fy",
+ FontStyle = FontStyle.Italic,
+ FontSize = 48,
+ UseLayoutRounding = false,
+ Padding = new Thickness(3, 2, 5, 4)
+ };
+
+ target.Measure(Size.Infinity);
+
+ var expectedSize =
+ new Size(target.TextLayout.WidthIncludingTrailingWhitespace, target.TextLayout.Height)
+ .Inflate(target.Padding);
+
+ Assert.Equal(expectedSize, target.DesiredSize);
+
+ target.Arrange(new Rect(default, target.DesiredSize));
+
+ Assert.Equal(new Rect(default, expectedSize), target.Bounds);
+ }
+
private class TestTextBlock : TextBlock
{
public Size Constraint => _constraint;
From d78cdd1c8c013cb07e94978e6ee12f376817988b Mon Sep 17 00:00:00 2001
From: Jumar Macato <16554748+jmacato@users.noreply.github.com>
Date: Fri, 3 Apr 2026 16:19:17 +0800
Subject: [PATCH 49/57] Fix/linux atspi backend (#21072)
* Fix ATSPI SyncContext to UIThread
* Update Avalonia.DBus submodule to fix empty array serialization and error handling
* use public api and revert to instantiation.
---
external/Avalonia.DBus | 2 +-
src/Avalonia.FreeDesktop.AtSpi/AtSpiServer.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/external/Avalonia.DBus b/external/Avalonia.DBus
index f91a822c25..864a052828 160000
--- a/external/Avalonia.DBus
+++ b/external/Avalonia.DBus
@@ -1 +1 @@
-Subproject commit f91a822c258476f185e51112388775591e6ef9d6
+Subproject commit 864a05282841bf04006890f04d11d60d1a046aa9
diff --git a/src/Avalonia.FreeDesktop.AtSpi/AtSpiServer.cs b/src/Avalonia.FreeDesktop.AtSpi/AtSpiServer.cs
index 16d36eb590..06b9b7aa3f 100644
--- a/src/Avalonia.FreeDesktop.AtSpi/AtSpiServer.cs
+++ b/src/Avalonia.FreeDesktop.AtSpi/AtSpiServer.cs
@@ -61,7 +61,7 @@ namespace Avalonia.FreeDesktop.AtSpi
_isEmbedded = false;
}
- _syncContext = new AvaloniaSynchronizationContext(DispatcherPriority.Normal);
+ _syncContext = new AvaloniaSynchronizationContext(Dispatcher.UIThread, DispatcherPriority.Default);
var address = await GetAccessibilityBusAddressAsync();
From 39f733c7a57f76cfded22df34d1fea6be9b83ff6 Mon Sep 17 00:00:00 2001
From: hatulaile <158590752+hatulaile@users.noreply.github.com>
Date: Fri, 3 Apr 2026 16:19:26 +0800
Subject: [PATCH 50/57] Fixed first-time notification issue in the Clipboard
sample (#21071)
---
samples/ControlCatalog/Pages/ClipboardPage.xaml.cs | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
index 4cdde6b824..65f58d0444 100644
--- a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
@@ -21,8 +21,6 @@ namespace ControlCatalog.Pages
DataFormat.CreateBytesApplicationFormat("controlcatalog-binary-data");
private INotificationManager? _notificationManager;
- private INotificationManager NotificationManager => _notificationManager
- ??= new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
private readonly DispatcherTimer _clipboardLastDataObjectChecker;
private DataTransfer? _storedDataTransfer;
@@ -107,7 +105,7 @@ namespace ControlCatalog.Pages
if (invalidFile.Count > 0)
{
- NotificationManager.Show(new Notification("Warning", "There is one o more invalid path.", NotificationType.Warning));
+ _notificationManager?.Show(new Notification("Warning", "There is one o more invalid path.", NotificationType.Warning));
}
if (files.Count > 0)
@@ -116,11 +114,11 @@ namespace ControlCatalog.Pages
foreach (var file in files)
dataTransfer.Add(DataTransferItem.Create(DataFormat.File, file));
await clipboard.SetDataAsync(dataTransfer);
- NotificationManager.Show(new Notification("Success", "Copy completed.", NotificationType.Success));
+ _notificationManager?.Show(new Notification("Success", "Copy completed.", NotificationType.Success));
}
else
{
- NotificationManager.Show(new Notification("Warning", "Any files to copy in Clipboard.", NotificationType.Warning));
+ _notificationManager?.Show(new Notification("Warning", "Any files to copy in Clipboard.", NotificationType.Warning));
}
}
}
@@ -179,6 +177,7 @@ namespace ControlCatalog.Pages
{
_clipboardLastDataObjectChecker.Start();
base.OnAttachedToVisualTree(e);
+ _notificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
From b69e05eb285ec70eb4f26a084deb7f9b4db9cceb Mon Sep 17 00:00:00 2001
From: Jumar Macato <16554748+jmacato@users.noreply.github.com>
Date: Sat, 4 Apr 2026 01:50:27 +0800
Subject: [PATCH 51/57] Post NSAccessibility notifications for property changes
on macOS (#21074)
---
native/Avalonia.Native/src/OSX/AvnWindow.mm | 12 +++++++++
native/Avalonia.Native/src/OSX/automation.mm | 28 ++++++++++++++++++--
src/Avalonia.Native/AvnAutomationPeer.cs | 5 ++++
src/Avalonia.Native/avn.idl | 5 ++++
4 files changed, 48 insertions(+), 2 deletions(-)
diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm
index c4e715229b..b2f20d556b 100644
--- a/native/Avalonia.Native/src/OSX/AvnWindow.mm
+++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm
@@ -656,6 +656,18 @@
- (void)raisePropertyChanged:(AvnAutomationProperty)property
{
+ switch (property)
+ {
+ case AutomationPeer_Name:
+ NSAccessibilityPostNotification(self, NSAccessibilityTitleChangedNotification);
+ break;
+ case AutomationPeer_BoundingRectangle:
+ NSAccessibilityPostNotification(self, NSAccessibilityMovedNotification);
+ NSAccessibilityPostNotification(self, NSAccessibilityResizedNotification);
+ break;
+ default:
+ break;
+ }
}
@end
diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm
index 6eee49f619..412e7ddc5f 100644
--- a/native/Avalonia.Native/src/OSX/automation.mm
+++ b/native/Avalonia.Native/src/OSX/automation.mm
@@ -488,8 +488,32 @@
- (void)raisePropertyChanged:(AvnAutomationProperty)property
{
- if (property == AutomationPeer_Name && _peer->GetLiveSetting() != LiveSettingOff)
- [self raiseLiveRegionChanged];
+ switch (property)
+ {
+ case AutomationPeer_Name:
+ NSAccessibilityPostNotification(self, NSAccessibilityTitleChangedNotification);
+ if (_peer->GetLiveSetting() != LiveSettingOff)
+ [self raiseLiveRegionChanged];
+ break;
+ case ValueProvider_Value:
+ case RangeValueProvider_Value:
+ NSAccessibilityPostNotification(self, NSAccessibilityValueChangedNotification);
+ break;
+ case AutomationPeer_BoundingRectangle:
+ NSAccessibilityPostNotification(self, NSAccessibilityMovedNotification);
+ NSAccessibilityPostNotification(self, NSAccessibilityResizedNotification);
+ break;
+ case SelectionItemProvider_IsSelected:
+ case SelectionProvider_Selection:
+ NSAccessibilityPostNotification(self, NSAccessibilitySelectedChildrenChangedNotification);
+ break;
+ case ToggleProvider_ToggleState:
+ case ExpandCollapseProvider_ExpandCollapseState:
+ NSAccessibilityPostNotification(self, NSAccessibilityValueChangedNotification);
+ break;
+ default:
+ break;
+ }
}
- (void)raiseLiveRegionChanged
diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs
index 25905da02a..3822864edc 100644
--- a/src/Avalonia.Native/AvnAutomationPeer.cs
+++ b/src/Avalonia.Native/AvnAutomationPeer.cs
@@ -19,6 +19,11 @@ namespace Avalonia.Native
{ AutomationElementIdentifiers.ClassNameProperty, AvnAutomationProperty.AutomationPeer_ClassName },
{ AutomationElementIdentifiers.NameProperty, AvnAutomationProperty.AutomationPeer_Name },
{ RangeValuePatternIdentifiers.ValueProperty, AvnAutomationProperty.RangeValueProvider_Value },
+ { ValuePatternIdentifiers.ValueProperty, AvnAutomationProperty.ValueProvider_Value },
+ { TogglePatternIdentifiers.ToggleStateProperty, AvnAutomationProperty.ToggleProvider_ToggleState },
+ { ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, AvnAutomationProperty.ExpandCollapseProvider_ExpandCollapseState },
+ { SelectionItemPatternIdentifiers.IsSelectedProperty, AvnAutomationProperty.SelectionItemProvider_IsSelected },
+ { SelectionPatternIdentifiers.SelectionProperty, AvnAutomationProperty.SelectionProvider_Selection },
};
private static readonly ConditionalWeakTable s_wrappers = new();
diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl
index ce9ce0d5b0..b9b5977632 100644
--- a/src/Avalonia.Native/avn.idl
+++ b/src/Avalonia.Native/avn.idl
@@ -403,6 +403,11 @@ enum AvnAutomationProperty
AutomationPeer_ClassName,
AutomationPeer_Name,
RangeValueProvider_Value,
+ ValueProvider_Value,
+ ToggleProvider_ToggleState,
+ ExpandCollapseProvider_ExpandCollapseState,
+ SelectionItemProvider_IsSelected,
+ SelectionProvider_Selection,
}
struct AvnSize
From 1060839683d2f8bb1b752d5bc021a53f90a0129d Mon Sep 17 00:00:00 2001
From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com>
Date: Tue, 7 Apr 2026 03:31:28 +1000
Subject: [PATCH 52/57] Fix access keys not working when KeySymbol is null
(#21077)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* test: add regression test for access key with system key events
Regression test for #20961: verifies that access keys fire correctly
when triggered via Alt+key (system key events).
* fix: provide KeySymbol for system key events via MapVirtualKey
On Windows, WM_SYSKEYDOWN (Alt+key) intentionally skips ToUnicodeEx
to avoid corrupting keyboard state. This left KeySymbol null, which
broke access keys after #20662 switched from Key to KeySymbol.
Use MapVirtualKey(VK, MAPVK_VK_TO_CHAR) as a layout-aware fallback
for system key events — it resolves the character without touching
keyboard state.
Fixes #20961
* chore: retrigger CI
---
.../Avalonia.Win32/Input/KeyInterop.cs | 18 +++++++++++++
.../Avalonia.Win32/WindowImpl.AppWndProc.cs | 7 +++--
.../Input/AccessKeyHandlerTests.cs | 27 +++++++++++++++++++
3 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs
index 834feb861e..f13694e4fb 100644
--- a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs
+++ b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs
@@ -493,6 +493,24 @@ namespace Avalonia.Win32.Input
PhysicalKey.None;
}
+ ///
+ /// Gets a key symbol from a Windows virtual-key using MapVirtualKey.
+ /// Unlike , this does not call ToUnicodeEx and is safe to use
+ /// during WM_SYSKEYDOWN/UP where ToUnicodeEx would corrupt keyboard state.
+ ///
+ /// The Windows virtual-key.
+ /// A key symbol, or null if none matched.
+ public static string? GetKeySymbolFromVirtualKey(int virtualKey)
+ {
+ var ch = MapVirtualKey((uint)virtualKey, (uint)MapVirtualKeyMapTypes.MAPVK_VK_TO_CHAR);
+ if (ch == 0)
+ return null;
+
+ // Bit 31 is set for dead keys — strip it to get the base character.
+ var c = (char)(ch & 0x7FFFFFFF);
+ return KeySymbolHelper.IsAllowedAsciiKeySymbol(c) ? c.ToString() : null;
+ }
+
///
/// Gets a key symbol from a Windows virtual-key and key data.
///
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
index 82aaac226c..2eba69960b 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
@@ -1446,8 +1446,11 @@ namespace Avalonia.Win32
var physicalKey = KeyInterop.PhysicalKeyFromVirtualKey(virtualKey, keyData);
// Avoid calling GetKeySymbol() for WM_SYSKEYDOWN/UP:
- // it ultimately calls User32!ToUnicodeEx, which messes up the keyboard state in this case.
- var keySymbol = useKeySymbol ? KeyInterop.GetKeySymbol(virtualKey, keyData) : null;
+ // it ultimately calls ToUnicodeEx, which corrupts keyboard state for system key events.
+ // Use MapVirtualKey-based fallback instead — it's layout-aware without touching keyboard state.
+ var keySymbol = useKeySymbol
+ ? KeyInterop.GetKeySymbol(virtualKey, keyData)
+ : KeyInterop.GetKeySymbolFromVirtualKey(virtualKey);
if (key == Key.None && physicalKey == PhysicalKey.None && string.IsNullOrWhiteSpace(keySymbol))
return null;
diff --git a/tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs b/tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs
index 9443e22855..166b777ba1 100644
--- a/tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs
@@ -233,6 +233,33 @@ namespace Avalonia.Base.UnitTests.Input
}
}
+ [Fact]
+ public void Should_Raise_AccessKey_For_System_Key_Event_With_KeySymbol()
+ {
+ // Regression test for #20961: on Windows, WM_SYSKEYDOWN (Alt+key) previously
+ // left KeySymbol null, breaking access keys. MapVirtualKey now provides KeySymbol.
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var button = new Button();
+ var root = new TestRoot(button);
+ var target = new AccessKeyHandler();
+ var raised = 0;
+
+ KeyboardDevice.Instance?.SetFocusedElement(button, NavigationMethod.Unspecified, KeyModifiers.None);
+
+ target.SetOwner(root);
+ target.Register("F", button);
+ button.AddHandler(AccessKeyHandler.AccessKeyEvent, (s, e) => ++raised);
+
+ KeyDown(root, Key.LeftAlt);
+ Assert.Equal(0, raised);
+
+ // MapVirtualKey provides lowercase KeySymbol for system key events
+ KeyDown(root, Key.F, "f", KeyModifiers.Alt);
+ Assert.Equal(1, raised);
+ }
+ }
+
[Fact]
public void Should_Open_MainMenu_On_Alt_KeyUp()
{
From 04558f076894382211a6eceecc5e42eb5a06cb91 Mon Sep 17 00:00:00 2001
From: Julien Lebosquain
Date: Wed, 8 Apr 2026 10:20:53 +0200
Subject: [PATCH 53/57] Master now targets 12.1
---
build/SharedVersion.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build/SharedVersion.props b/build/SharedVersion.props
index b8c0dd4d43..cff9a24f54 100644
--- a/build/SharedVersion.props
+++ b/build/SharedVersion.props
@@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
Avalonia
- 12.0.999
+ 12.1.999
Avalonia Team
Copyright 2013-$([System.DateTime]::Now.ToString(`yyyy`)) © The AvaloniaUI Project
https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link
From 387c9d96c0dfacaacea3c781256fbad958c5c9b3 Mon Sep 17 00:00:00 2001
From: Dong Bin <14807942+rabbitism@users.noreply.github.com>
Date: Wed, 8 Apr 2026 16:08:41 +0800
Subject: [PATCH 54/57] fix(datepicker): fix date picker spacer location.
(#21093)
---
.../DateTimePickers/DatePickerPresenter.cs | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
index bfe928309f..eac695b0b1 100644
--- a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
+++ b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
@@ -457,18 +457,17 @@ namespace Avalonia.Controls
}
}
- ConfigureSpacer(items._firstSpacer, columnIndex > 1);
- ConfigureSpacer(items._secondSpacer, columnIndex > 2);
+ ConfigureSpacer(items._firstSpacer, columnIndex > 1, 1);
+ ConfigureSpacer(items._secondSpacer, columnIndex > 2, 3);
+ return;
- static void ConfigureSpacer(Control? spacer, bool visible)
+ static void ConfigureSpacer(Control? spacer, bool visible, int column)
{
if (spacer == null)
return;
-
// ternary conditional operator is used to make sure grid cells will be validated
- Grid.SetColumn(spacer, visible ? 1 : 0);
+ Grid.SetColumn(spacer, visible ? column : 0);
spacer.IsVisible = visible;
-
}
}
From 04f9129788fe35148b95a25d82b80e6019f9e373 Mon Sep 17 00:00:00 2001
From: Emmanuel Hansen
Date: Wed, 8 Apr 2026 08:11:01 +0000
Subject: [PATCH 55/57] Update ControlCatalog to use the Page system (#21044)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* replace hamburger view with drawer page. use ContentPage in control catalog
* Added Icons
* fix listbox and carousel page
* make MainView drawer page
* add PageNavigationHost to control catalog
---------
Co-authored-by: Javier Suárez Ruiz
---
samples/ControlCatalog/App.xaml | 3 +-
samples/ControlCatalog/App.xaml.cs | 37 +-
samples/ControlCatalog/Assets/Icon.png | Bin 0 -> 14349 bytes
samples/ControlCatalog/ControlCatalog.csproj | 8 +-
samples/ControlCatalog/Icons.cs | 194 ++++++++
samples/ControlCatalog/MainView.xaml | 467 ++++++------------
samples/ControlCatalog/MainView.xaml.cs | 97 +++-
samples/ControlCatalog/NavHeaderItem.xaml | 101 ++++
.../ControlCatalog/Pages/AcceleratorPage.xaml | 185 +++----
.../Pages/AcceleratorPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/AcrylicPage.xaml | 246 ++++-----
.../ControlCatalog/Pages/AcrylicPage.xaml.cs | 2 +-
.../Pages/AdornerLayerPage.xaml | 113 +++--
.../Pages/AdornerLayerPage.xaml.cs | 2 +-
.../Pages/AutoCompleteBoxPage.xaml | 140 +++---
.../Pages/AutoCompleteBoxPage.xaml.cs | 2 +-
.../Pages/BitmapCachePage.axaml | 76 +--
.../Pages/BitmapCachePage.axaml.cs | 4 +-
samples/ControlCatalog/Pages/BorderPage.xaml | 112 +++--
.../ControlCatalog/Pages/BorderPage.xaml.cs | 2 +-
.../Pages/ButtonSpinnerPage.xaml | 5 +-
.../Pages/ButtonSpinnerPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/ButtonsPage.xaml | 13 +-
.../ControlCatalog/Pages/ButtonsPage.xaml.cs | 2 +-
.../Pages/CalendarDatePickerPage.xaml | 5 +-
.../Pages/CalendarDatePickerPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/CalendarPage.xaml | 5 +-
.../ControlCatalog/Pages/CalendarPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/CanvasPage.xaml | 5 +-
.../ControlCatalog/Pages/CanvasPage.xaml.cs | 2 +-
.../Pages/CarouselDemoPage.xaml | 21 +-
.../Pages/CarouselDemoPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/CarouselPage.xaml | 5 +-
.../ControlCatalog/Pages/CarouselPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/CheckBoxPage.xaml | 5 +-
.../ControlCatalog/Pages/CheckBoxPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/ClipboardPage.xaml | 5 +-
.../Pages/ClipboardPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/ColorPickerPage.xaml | 9 +-
.../Pages/ColorPickerPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/ComboBoxPage.xaml | 4 +-
.../ControlCatalog/Pages/ComboBoxPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/CommandBarPage.xaml | 13 +-
.../Pages/CommandBarPage.xaml.cs | 2 +-
.../Pages/CompositionPage.axaml | 121 +++--
.../Pages/CompositionPage.axaml.cs | 2 +-
.../Pages/ConnectedAnimationDemoPage.xaml | 7 +-
.../Pages/ConnectedAnimationDemoPage.xaml.cs | 2 +-
.../Pages/ContainerQueryPage.xaml | 5 +-
.../Pages/ContainerQueryPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/ContentDemoPage.xaml | 21 +-
.../Pages/ContentDemoPage.xaml.cs | 2 +-
.../Pages/ContextFlyoutPage.xaml | 9 +-
.../Pages/ContextFlyoutPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/ContextMenuPage.xaml | 5 +-
.../Pages/ContextMenuPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/CursorPage.xaml | 5 +-
.../ControlCatalog/Pages/CursorPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/CustomDrawing.xaml | 9 +-
.../Pages/CustomDrawing.xaml.cs | 2 +-
.../ControlCatalog/Pages/DataGridPage.xaml | 5 +-
.../ControlCatalog/Pages/DataGridPage.xaml.cs | 2 +-
.../Pages/DataValidationPage.axaml | 9 +-
.../Pages/DataValidationPage.axaml.cs | 2 +-
.../Pages/DateTimePickerPage.xaml | 5 +-
.../Pages/DateTimePickerPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/DialogsPage.xaml | 5 +-
.../ControlCatalog/Pages/DialogsPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/DragAndDropPage.xaml | 9 +-
.../Pages/DragAndDropPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/DrawerDemoPage.xaml | 21 +-
.../Pages/DrawerDemoPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/ExpanderPage.xaml | 5 +-
.../ControlCatalog/Pages/ExpanderPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/FlyoutsPage.axaml | 9 +-
.../ControlCatalog/Pages/FlyoutsPage.axaml.cs | 2 +-
samples/ControlCatalog/Pages/FocusPage.xaml | 5 +-
.../ControlCatalog/Pages/FocusPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/GesturePage.xaml | 21 +-
.../{GesturePage.cs => GesturePage.xaml.cs} | 2 +-
.../Pages/HeaderedContentPage.axaml | 5 +-
.../Pages/HeaderedContentPage.axaml.cs | 2 +-
samples/ControlCatalog/Pages/ImagePage.xaml | 5 +-
.../ControlCatalog/Pages/ImagePage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/LabelsPage.axaml | 9 +-
.../ControlCatalog/Pages/LabelsPage.axaml.cs | 2 +-
.../Pages/LayoutTransformControlPage.xaml | 5 +-
.../Pages/LayoutTransformControlPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/ListBoxPage.xaml | 5 +-
.../ControlCatalog/Pages/ListBoxPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/MenuPage.xaml | 5 +-
samples/ControlCatalog/Pages/MenuPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/NativeEmbedPage.xaml | 5 +-
.../Pages/NativeEmbedPage.xaml.cs | 2 +-
.../Pages/NavigationDemoPage.xaml | 21 +-
.../Pages/NavigationDemoPage.xaml.cs | 2 +-
.../Pages/NotificationsPage.xaml | 5 +-
.../Pages/NotificationsPage.xaml.cs | 2 +-
.../Pages/NumericUpDownPage.xaml | 5 +-
.../Pages/NumericUpDownPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/OpenGlPage.xaml | 5 +-
.../ControlCatalog/Pages/OpenGlPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/PipsPagerPage.xaml | 21 +-
.../Pages/PipsPagerPage.xaml.cs | 2 +-
.../Pages/PlatformInfoPage.xaml | 5 +-
.../Pages/PlatformInfoPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/PointersPage.xaml | 23 +-
.../ControlCatalog/Pages/PointersPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/ProgressBarPage.xaml | 12 +-
.../Pages/ProgressBarPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/RadioButtonPage.xaml | 5 +-
.../Pages/RadioButtonPage.xaml.cs | 2 +-
.../Pages/RefreshContainerPage.axaml | 5 +-
.../Pages/RefreshContainerPage.axaml.cs | 2 +-
.../Pages/RelativePanelPage.axaml | 5 +-
.../Pages/RelativePanelPage.axaml.cs | 2 +-
samples/ControlCatalog/Pages/ScreenPage.cs | 2 +-
.../Pages/ScrollViewerPage.xaml | 15 +-
.../Pages/ScrollViewerPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/SliderPage.xaml | 5 +-
.../ControlCatalog/Pages/SliderPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/SplitViewPage.xaml | 5 +-
.../Pages/SplitViewPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/TabControlPage.xaml | 5 +-
.../Pages/TabControlPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/TabStripPage.xaml | 5 +-
.../ControlCatalog/Pages/TabStripPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/TabbedDemoPage.xaml | 21 +-
.../Pages/TabbedDemoPage.xaml.cs | 2 +-
.../ControlCatalog/Pages/TextBlockPage.xaml | 5 +-
.../Pages/TextBlockPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/TextBoxPage.xaml | 5 +-
.../ControlCatalog/Pages/TextBoxPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/ThemePage.axaml | 9 +-
.../ControlCatalog/Pages/ThemePage.axaml.cs | 2 +-
.../Pages/ToggleSwitchPage.xaml | 25 +-
.../Pages/ToggleSwitchPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/ToolTipPage.xaml | 7 +-
.../ControlCatalog/Pages/ToolTipPage.xaml.cs | 2 +-
.../TransitioningContentControlPage.axaml | 17 +-
.../TransitioningContentControlPage.axaml.cs | 2 +-
.../ControlCatalog/Pages/TreeViewPage.xaml | 5 +-
.../ControlCatalog/Pages/TreeViewPage.xaml.cs | 2 +-
samples/ControlCatalog/Pages/ViewboxPage.xaml | 5 +-
.../ControlCatalog/Pages/ViewboxPage.xaml.cs | 2 +-
.../Pages/WindowCustomizationsPage.xaml | 5 +-
.../Pages/WindowCustomizationsPage.xaml.cs | 2 +-
samples/ControlCatalog/ScrollPage.xaml | 34 ++
.../ViewModels/MainWindowViewModel.cs | 11 +-
.../MainWindowViewModel_PageList.cs | 196 ++++++++
.../HamburgerMenu/HamburgerMenu.xaml | 8 +-
151 files changed, 1703 insertions(+), 1128 deletions(-)
create mode 100644 samples/ControlCatalog/Assets/Icon.png
create mode 100644 samples/ControlCatalog/Icons.cs
create mode 100644 samples/ControlCatalog/NavHeaderItem.xaml
rename samples/ControlCatalog/Pages/{GesturePage.cs => GesturePage.xaml.cs} (96%)
create mode 100644 samples/ControlCatalog/ScrollPage.xaml
create mode 100644 samples/ControlCatalog/ViewModels/MainWindowViewModel_PageList.cs
diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml
index 179f64233e..6ab30cc180 100644
--- a/samples/ControlCatalog/App.xaml
+++ b/samples/ControlCatalog/App.xaml
@@ -8,7 +8,8 @@
-
+
+
diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs
index f14fbb1fa3..71e4113a3a 100644
--- a/samples/ControlCatalog/App.xaml.cs
+++ b/samples/ControlCatalog/App.xaml.cs
@@ -18,7 +18,7 @@ namespace ControlCatalog
private FluentTheme? _fluentTheme;
private SimpleTheme? _simpleTheme;
private IStyle? _colorPickerFluent, _colorPickerSimple;
-
+
public App()
{
DataContext = new ApplicationViewModel();
@@ -34,7 +34,7 @@ namespace ControlCatalog
_simpleTheme = (SimpleTheme)Resources["SimpleTheme"]!;
_colorPickerFluent = (IStyle)Resources["ColorPickerFluent"]!;
_colorPickerSimple = (IStyle)Resources["ColorPickerSimple"]!;
-
+
SetCatalogThemes(CatalogTheme.Fluent);
}
@@ -44,16 +44,22 @@ namespace ControlCatalog
{
desktopLifetime.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() };
}
- else if(ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
+ else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
{
- singleViewFactoryApplicationLifetime.MainViewFactory = () => new MainView { DataContext = new MainWindowViewModel() };
+ singleViewFactoryApplicationLifetime.MainViewFactory = () => new PageNavigationHost()
+ {
+ Page = new MainView { DataContext = new MainWindowViewModel() }
+ };
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
{
- singleViewLifetime.MainView = new MainView { DataContext = new MainWindowViewModel() };
+ singleViewLifetime.MainView = new PageNavigationHost()
+ {
+ Page = new MainView { DataContext = new MainWindowViewModel() }
+ };
}
- if (this.TryGetFeature() is {} activatableApplicationLifetime)
+ if (this.TryGetFeature() is { } activatableApplicationLifetime)
{
activatableApplicationLifetime.Activated += (sender, args) =>
Console.WriteLine($"App activated: {args.Kind}");
@@ -99,14 +105,14 @@ namespace ControlCatalog
}
private CatalogTheme _prevTheme;
- public static CatalogTheme CurrentTheme => ((App)Current!)._prevTheme;
+ public static CatalogTheme CurrentTheme => ((App)Current!)._prevTheme;
public static void SetCatalogThemes(CatalogTheme theme)
{
var app = (App)Current!;
var prevTheme = app._prevTheme;
app._prevTheme = theme;
var shouldReopenWindow = prevTheme != theme;
-
+
if (app._themeStylesContainer.Count == 0)
{
app._themeStylesContainer.Add(new Style());
@@ -130,18 +136,27 @@ namespace ControlCatalog
if (app.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
var oldWindow = desktopLifetime.MainWindow;
- var newWindow = new MainWindow();
+ var newWindow = new MainWindow()
+ {
+ DataContext = new MainWindowViewModel()
+ };
desktopLifetime.MainWindow = newWindow;
newWindow.Show();
oldWindow?.Close();
}
else if (app.ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
{
- singleViewFactoryApplicationLifetime.MainViewFactory = () => new MainView { DataContext = new MainWindowViewModel() };
+ singleViewFactoryApplicationLifetime.MainViewFactory = () => new PageNavigationHost()
+ {
+ Page = new MainView { DataContext = new MainWindowViewModel() }
+ };
}
else if (app.ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
{
- singleViewLifetime.MainView = new MainView();
+ singleViewLifetime.MainView = new PageNavigationHost()
+ {
+ Page = new MainView { DataContext = new MainWindowViewModel() }
+ };
}
}
}
diff --git a/samples/ControlCatalog/Assets/Icon.png b/samples/ControlCatalog/Assets/Icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..41a2a618fb02e4cb7f6a15caf572b693bfe1ebb1
GIT binary patch
literal 14349
zcmdtJ^EU0vMF0h26NSCC5pomCGr^Esh3km|#NF&{iw9?Wnjl|N@
z`CNWJ_xJlef5G#^&FjT4v*$W9bLKr~&Ybg}2rW%D5<&(-5C}x_NFAXA0)gZH{o~^T
zErD&~zd#^~-;WTAdY)f*(0E^|PA(JdJoCHg9rW(}-q6s{
z#*KHDghqFB4W6%q0k{gg|^3gd>rC)VTQMFLhj#M%Yxxfs$Hp@c8So3dxQzuFO6D`+4HKNdX3
zU$5ATBk*Iq3h=^S&l?Fl7GP}W<6
z!%2fSTZ=YX;Xa}7q3U>`3qD>$uw??ic;UcQ;9H#~hXP-|5dmoD!@!eAn4`WPE8Fhp
zy2m^eUcq1`spUO?vi%Bn9Z$EuO(AZVukaXyO(D>WJHqBYfuJ(b8>&849Qm+KnMB)`
z7Tegq#og}ib#n=|-&DBx7egWPvHNLcUtf2*)w1R@*xP~qm)tG5^s?F&uss>G895?J
z)+xMi{%i;=thrkw+=bwr#awz1G8)#-%fBfEt2^N|VoCnOw900JZowTqgkDlMYoE1;
zEv{Rqo@$o^(wJYH&%NWxF}z|GBjw)U#@H!2+d=_qPn@{V%UW(TeRfd_$Bk@+G`RR)
zL-J0qFyb-;P0Gizoc!4D3Z~>PwAY=fEO8Kqo2S=yJ>MHGqo(Av-&zO8J$#UH^E)UT
zb&Dm(0x#p0AK5jO0X1_5Km7UQW&hyM$hM96gmRU=i7b#S~P_+Dt;c71rc67ek^I{8e}
z!&O^D5YEY$m!>;AL+4NcN&?wq+!faB^Wllyf_BG|w>-^n!>+@@N?anv!@5_7tS84ald_T6RH$cUk@E2iz0pV~`iDy(+ATb$#M)FF6Yr7Guh
z=v|hYwbKJ)Gkh~wJKOBC(jZ7jnpp;DF)0ZS?Y}>K5@5o*?Jn}kkB;@zCwlbb=PGkX
zhG0&DP5NcUYQ?PaI*$>B0|qlaT1ZGrH$S)IoHXwg79GwKXt{_Ik)bTaUX7lYaB)8*
zElXwFcjkddrx+h2uG9;zy+hq0*gjKcJx~Jf8wplQq9c>xd#yVG9$sh?ukhy}7aKC(
zL8lu&e#7p2SUZMdh72V_#3=Yx5`NxB`18eKR)Pnumt2}#?PDW7PwSuHv=#b1Et;hc
zWm7t&I-jPfs5|w%BA=^`2Bz}o;PbPlr`ylHztg2*cSPvBt{9Sq;fpOl{kch!V?gTL
zjZx(kIWKznMCuBa^O0Z}`yD?+1c@M`fF9ek5ysSbn`g&AyXh|Q$=tUaZ~C_@O-%&i
zNR?s8zF@(I}S`XI==cb(a8iJ`ktce_D^p+
zlg0bW)|hNy>aP#T=s}cG%+flgBeEa`7Y4&jZ)P>vC-O*IJ|n|e@7ou9lSfjmjjmrD
z=L{5Qv~won7k{@r=2s4>Y=2Mwk=s~)$-j(|?d*JDNZaG|_J++sofd4e)~t;ONOV*&
z2qndG@!Xb8MQa_M7Y!+Tq+i$v1Py`94Obc61reN#z65=ra}1B~ssIU>^T;8#1fO3{
zwEY6te1RSA8zP84JgSkVT9w)bnpCuk+VKD
zBm&xUs4&V_B9?1a-7MZ{La}I;)j_aF`pL*$m8JOvdO3(g!Da5?R7~TX++u)JvDf~$
ziO9*$eOj-7vQ9XDzxch`m@4I)bg8V1|5Ur#jQo?8q{t%Hh3CD&T$~OzcRHP}@J#3L
zR0RZB7QimQz?-BT(e$uS?6(wL(_))>3Ko~8z(kaNPcBE!tC*!H===!dK42VsS04Zn
zV3#NCFco!CIh-?>P&)J^?y}pHbRF&P*^B~_#>k{By({m$5sD0}D>Lr2hOGs$s9LoM
z*X+l;w!95l5rX@!#bo0-1$MjuIlkTyMsPxBu`YjqYK^@a!>AU}oZUNb-x;d<+iE}e
zni+v7;c!ObVW-VcnPyb!+b$V&U>4qc{(8_wci8y%OSOg`)q6Lu{|F(Fg(PJQZlLS4&wa`m3AkGySA9ZT-v&{T%CWaU
z`Ye*WoDm7g)N$zCuEtktw4OGiSXX#NiDp!YVyQoGfS7{ocZHNg`Z{D7gzWxCwkG!)
z+9ehel9gbu1#A!5A)#X1lty{On;Ve|hrp-HJR(uQ!|;lJuy?2?vMH^tWpHoGyu1h`
zYkteY>B+2~Sl@EinULwJ?GcVgk)f7FqR=v3@>kq59e=%6S*7MpPpn4laU&|zGSDVy
zZuslsDDpeDn}w(c`%fsF3oD*#&g4LZg4fEsG8qD_J=%lmdo8rg)S<1|9k(^E3Df8w
zXk&z?JaKsL8Pe|Rt{0=~La)ZkTp9I;nMvP_)G}a-9TI~;er2MM*e0sFl&xu-ie3}O
zMXK#&&s?<|4Hs@&@hudH{FBgSqaJ=Ddxj!4mxwQ>V`VOHl5Gx8IjACJG8B7=!{{D{
zk4n((Y$Cj~q2=QRMr=xT)EJc6(7K4mDJ8kZQm+fs(0%tYxEF!kmZ@&%md(3bT21=&
z`SYQf8lf|Gyf*i}+S+snR)xoNeSz%c*|=2F<-0|c54_Q8-Fm6M>B#l~OV
zqkqStq!)WEc@^GmA#hiJOj0J(Pwm@n+gIB1=i0Q(yILxBGvz)pxx`8RW8IwW#J=vF
z#Iw5uU}jjYv%;39`(7=|`Ycm}!{@4LY)X2IG}Plhzm8Vi%_UYme7}85Hgj8C9<@Jz
z3i9!()dm){o4mWn#V8+rechefyw^SK36+c9$qo%2wco5T@@)SJLCcnFQmNsDhj(9B
zxLI<=Lh3*ej_B;X6?0G;#&NqSz!E7r2~iFaRt}VBY$HJLxwH89IQyr}&$AiDG
zXtJ!A`Psl+FB2uKr1GtP?cz4p3yows{QMbr)%({jcj9f$QKr{wYU4Hw9W3Ug9h&uV
z`c3wqj}-z`Z5!_F{y)r&xZYb2FS929?^WeJq;MRP_}T)KtZJ!;2#FjJNuG`1up8?zr`iQ_PbnuJ+wLzX*a3wg#~X
zRUSiGASPtzLqB6poi#PVNgji%(JiUpIu_#dJnRnHTj$eM(&r>7#E7T{Jm?17L63su
z>elXX+qr67O=w`U;fB@Zyp97;2VBXqpgj;V8fst5hA
zf+9&5=egS-iH&dJlkW|^&SeHXnLL)8shUBog;dzyDEw_iz3C!H5{bY0IkqVaXAHrUq*zuDap`BK5I^%qBWhm+
z)|VGw-A;jAOeRhzygnpsLkrWoq0S|EVff#|;X@fNrz=6`XQ*gN^JimETlN-cA6%1x
zd?tPs&yKHN>$!L&{`3ayG_&4@<0TVI-}+|cWf^1t1EwGjjne+WJ!%P_5A4A3SJXpf
zo#9@&mS3ly+Ay|uKkl4UlP2#SSNcFr``5}{Xyd}N
zUTD^ib4NAcrl&CvDsg)M3<_q?eKf!^xb*>%z8c!Q|IWj}d)uo{#z7>|o>#4iS?n~*
zfetPc-udZjVTNX=DZ%Xh%Px#+^PhJ&4~oiIKPfOV5RvJ%J^SpfAob+c8WYpdioabjrN4(7A(T4R1zQ#HFY+lweKL+;>J#6U-XWJxl$
zAA(}Aps#m?`aCq>+*@!H3%qK6+vUI%Bx&t*+6k(}qRiX&?$2@W#zQ0#`Z`v;Ab
z0?ycDA@95{_yTvfUfO`jRAK2Ki7P!gj_!BKLX3VsX*jta6rc_WB`#iQZ#KShdi!SK
zQ}!5yV|bfLMFBnpcFTBjrowk0FGbviFEH8MW95DfoI&XOoFASF#}Qj#XW~cVxMft|
zUZG_SLY~$rs(UF`Ci3>M(Q&RP%X6~aaqjia7=aujm#!TI!t+E^ip?dJ*WOtkyMCwx
zd<}4~jIOV^a@yCiPZLNye1^n+<1wiTX4dU)K4NLP)2R(tg?)o)C};RHO6z7SQ`JM^
zLBKs>m8$;LuSjr0^WG00;yCQLdt!eBvPTbk^>bny>0p{QvPEYo?!5|_k=yvsB!A4o
zQma^a8Pw;YBF?JLKILAY4;gq*~jRg%u=8E(^tB!ybs@j&$lV=YSN_J{NhHMQ$In
zpA80NdpbK;cTgrn-kKYDHNQ+TJMJv0^-9Gp8sLP>_@HUU%BQ|Z8nA<|djMRulOsQj
z<}dmLuBj+sjWBFUt9GD}sorIUMypQ7Ui~`Zczs^FSNvNQ$j$NYh?rSm*$!A-e>5Ju1h5GZtO^)W(8t9i>#52{4>8DSZG)9
znthp1Hr;MHA5$Y%yPUa4{41uQ-y(UvYwt%4?yyH%Z-rwJDUE6aYOuwk6$x!7UxDGv
zXo;r3JI#DPyEnzte<8mW)FNaL`fK|Z)bJeT)@#bQg=V)2|9KP&bF#uq1`QIL!XM8_
zb!Xq1H*UZ3)y`LQY$ghidLo9I)#;y#;`_qVFCcj|S^0C1a5kz^>btS)Qb8)0^<(6PLz~KrKD#BmR~i{vh^-&)eY4yh
zGO;~^b)Oy;-|lK5O;g@a#{c-;Ns%rS?@Z3_6&`KougE%Sx7S%@wY)t`;N&4-+noM4
zZM;oJ!!~B7Zo-&2_BW65%-wi!vw_@e{v~yKO46)rm$KA8su*}tn;
zMJc}Z@ABYbr4^JF25zh1HtMDMZ6||VQOYGeM|1ft-m;}5*u#ozuGF?$9K
z45N@edvkYt8D&dKhIpk{`J%n*Me?*N-|Aun#S`zEse3j`OCDCyThf0e+0UQve%+aC
z`LyNzYBufU*=Ku}rZ;`zF>TnkUDfj6dW-v9;H1ZmNlb?3C_B>ct(-512(R(GKuJBr
z{jDg)Jj^$Lk3?TOLKPno*=EBd-^0b0+Wlb{+{|`RM|?
zMsq@?wWzpnc5}9dZ`|H|k)=)33`YYrlPT_3_W
zwt1Z=fTX*!LYlq5FM!;nyw8KjuWMO9<+oNap7-^ni>&1>%nXZ>^~JH$Ml>%S?CGEr
z2ZBl%?SELL5<{Xr$x!v8-4B|)>(0`k-bLkuR&K-=hwH7Ga?JMyvyJuny0^N%xg~mW
z^Y=loDkp|IL0IL=AcwG1{`6mT^S{7}&f!T29n%ZI5~Vd!g)Q+yIh7T1lCzCX>q7v58ggfvy^
zsGPkzSvds!t4a|LKb+M-d;L!A_B!+uFzgiNrrD|m(`BSJ_^3tbXVhI0_w_;|RYi%`
z&~6W*d0;!$?k$RKEZ#fA(MjL4com{MgrwPH`mqmvM7_J}j#o%i0HS~26Vd|yN>`_7
zi>xoQ!TwVuv|U;;$bMH;x$CLisQsgzwiq&m
zR6cMD>G9oL1rX3~H1-3-tiz=RcA0XxU#E7)Z4cpb6Rti=#os8}R%>!00EYwX0<9~?5p06{n~1FQ|2P^YjaQ6--w*V3^t;XC8_%D
zh*C8s38%2xG;`Hf@dcaCUFE}?Sn@mjlRVK{E6qIf3CH5Sq*3o8)dmIGOub*F*#^l|
ze&?nhp^zu2wr6$v4jt^4w+;m?=9Yv7sh9wVdx{kZ
z10sD9O0nJ;zbwgzt@eCcP30-plf~8sgWjcUSQ^gRSh21i27K(3qP>i|mIanYQ*!5K
zeU**gXyqb7IK`J+lm5k*ttMlObi0tvIn6o(BV@DG;kT>+_hvSnp3U`}q=>*p(HSgU
z@AxUXTtY}LL6td6i;IIw(-2`=VEwaq`UH=WoHlu@B%E5<*{|#QtC67zEpOTM=~xE^
z?FPlDMJ)K}9~P;AZuJAaKuT*EzTNkH&Zg=el(VOJpwyPPKk7b#Db&X;c)VuxjLbmI
z+5yJ=;YcOg!{7)#>wlq*;@X+{*|H(lOu)8O?EZ#3OCfOC7%LHDzpbiyp+QddY|_SEB`NJYp1)ZtZ-uJCd;;W{5WY23;YEGdaBu
zS-@^T>Ac^Mf@~2(lV}9^V`}FrPiV2sOahGGh9VQCY3@^a{ld`r?jXt-)p2muiKh2w
z*Hue0){lh9e}>efSk+(yM6VljqTy!m?Q+ct6*X|1tf3Pcv7$3fJ}xo>sE%`7ODVsrNv^
z`19L$lTQQvH>MJa->gaaHd*vjI);8C9_=k+7&~u;os%$Ez%HYVINWB+AS>g#+4;8Nh-VZ1PYo7v
zoEzi3^RMJYTXWoZZ&EbY!P1Uey`xcK@0n9JP8DC4@_Xq7QTvmTBabw43Xw7$}X|x65rclT8
z!l!Nhv!IJ^t{fjuEkag>@fw`^f(g9v#r@OUD)W0)gpm{eq~ts$PWSEOI5iQ(Q`EFn
z({mVR6K)>JT4jR9sctE8tbzGSW+YpZJCkcE<_aRRG`DXwb@#;g)wv--3VWo;`|kDt
z@c9r%-naKfU~Z_%B!rM@)14)7sJVK)U==-I#M{h5Ze3-ev41O+!GkjkT5^!@gi
zRaVJCj68+*rlrT3sKA7*QX1aRZRn`_rUC&2s|!_hFyZr%uBU6=zFw(9WT#!yu*
z(-S+Y{oj6ZjS4ScArVC8cb9Y6-4RT??SIOELB(S0EV+~Mq2$2XlH(W}hWnDov=3u6
zFEuO?hl-ui-tWWuB*fn&WD26#n|318K($`z@I^YFzYibh>zrcazbl#|5mn*a`n2OW
zFjrmJEL|3>6p};cf?+IUJV=FlSk=t0=U+F<1^zQV*Xv~K_?F@QfiOoLt&2yz79mJI
z|798!V^j0nb|=vRa`h;EbxxuoX{Qv1I=fy>B3hw)IQB}2M+tF(CS6O}A1)aZWgnaP
zH$NUM4jU!Mmc1+~#K_RZJ?iJFHQK{5P^-W=9@x7ZF?>bah$GoJnyQZiLp@aF9u_a}
z7u+tmvxFcyc;UYb!Z0jikE(1KcXSLyS)xN~0pI0H*Znfdqg14G;0UVS8pV->Fbznx
z(<*GATD(lA2E#6zd=Keew4EpNvv0qbm5`EK1n{TH=B7~3QD#JVqTw1g-7MG3VpX@0
zdVh}$AwoZenJeO5SsAh-M~1h1RpJOb*0soY*zux4f^Z!rKWC{;kN6hlxsg0}ZiT&4
z&D5(MB69HQlY>A+w643yN5OWz&rRhzonyjf&+R)snqp?^uN%u
z_c^$@s$_2F$3Na8PYii6;Cky`vC$5majHMtX(>i+w04MHFKr$Q4
zY}1{Rz;}|{sVa(GXUA|XeJH9@0@2l(MZg@=hZ6epuFU6vUHjw&hqHBa2Me*{$14!CjIA_L}uMgwZj{u;6o$
zd;T>AtY@==U{8JRiSySfKCM5%mY@H-EZ4+q%|95Xp&Yf;8b0~-CMxynwh?o#3zloW
z^##zkw1({gbIEOwFh7GQC%J=zi=Nq(l5dhQ*Bv&Vn5ClL?!WoXpnZ~{%WLC@ygo2g
zIlEjT>|bm7S14p`S2RCUOiua?rl_*Nsh%Nh_>sDO8+a`V>sm&I
zj1lLUP4Rd-T>MFs%rZ?~a#)(sAxyua-I;Q~4Lak5s7t=KArae7DEqYqIz4D4I|?h~
zZ!`51`f*jJaC8~ESi?t0%x(xD8SGZ}Bj{rhwL$+qc%C5tM8#?9ecf3CVN2Uflj0%E
z*sDU99DdTj9*b&h?>xs@h{y}ux+;`{y-6IzOv;22*jDNaAg70iqFU{U?qf=$7zgro
zc?%r%Xdn4zJKDg8ih!iAZmVQUY~!@50obTjkQ^=?BP}5Tx+U23A;h>NjW3L^Y6X%$
z&v3~=+?LwMv^-bw$RLdwfAi)u*j^N2**yN6K|sia%J^NPLM1D8VaNg;4GWV;Ewct+
zIwQOHXJ6g5o?wRP6d;l0HLfce2F@u01b_C^6PBKhvmz)A)U}2$oGE`73kE}8erLXj
zb`_zrS@n@SRxmI>uSzjnwu-XI&*0U*r+0MSt_I|u_$+{YrjFn84<`|A2cwNcWb!4QB5{zWFlu<8#@77OChtJg8o4`nmcHp}bC
zNJ|(qryB?erE&cC0KDVGGUJ}(4#ou?NoAbn|@`x}z840)|wHURbK}#E4L*YhKJ~5#eK4xog*W|9Q
zbf)ZV+?j)00YzP^FeG1{~DWIjdjosP4AIT
zlQ{A`*vRv@x}bPp3#xDQzM0)kUxOshAYSUgaM0uVDP|G*;0X4(2VJW7tTf(!WN^*V
zGtbVwZ91|1h|n7(0Sz#C6>+j6*2BDcUf=h})ondZfNSJO1|03x85UZ`}ao6|q=~
zXUPF`x$P6lW7#$R68yq9L+zQt($^xc^%3?ZNvuLUAX2+sz?r-ct4o4AE3Nw!olEQx
zER=3st9MaosDjRIvo~T~G8p(!+aIi&rV6~-$q7ksXNt|nw@zacZE?BQC_s3fTi1K%
zlueYfegs2XH2FT~dmm5ItOjLX4>;k{lEZIB!(JxRjZZuf2_cW8DM8qj~yZxb!!Tw5XbB^ag!&Q9cV
zD9;u_#FLSC46A(=%1Hub#8!uW>&dV;f+`eGu({!XN*eyM!xe0iq9$2VY_Jzwyo~QN
zCo@(u)xN!n~vRDMQuVD^w9qX?qF
zUBr&K;Qg>4KG`X{6>SIF!E%Z@4XdWhe|v!49gVjVG`0o1niv1nPzqraD=y+Dd;
zXB9$c*kQ{)X;E<*TzZ}5f55Y<4t720><(VSuRt2$x@J`Y=Rk9pDNLP`I)xDRSM7b8
ze}gOeQL#)UGjm>?yuPmcBy|}61bIEEF!#u1(bm;}EtSkZ}T!*go
zu-{s&kS@aHe8wSQs@ho<&{sK$yg+pLJ{`~&^&dt>_8+pq+Sm2fhtRAg0-HdlgO~PV
znHJ)vozPd=N}W)0`=na0=lL?UtZmaNZL4*G>Ay?d6WJ&{gNUJwwb~sbga4|MDKZod
z{x(TlAHEW4)i&j9b~v!sSH6_KfM#U^I=c!+zddsNO%EF+BJbGIHp_E=&kvv3J{4iv
z%qsGj$N&fjJwOHr<`b5ITb|4|R&GwDoLgg3>85w~DqM;-^NsbH=>bH{lL@XkcxgbZ
z96~|xCQw%ES3LS(_oU<#ciNv7Vs~XO;SIcb{iEF$skH>C
zf6-;sR(A1!)USo8OE&KS$6>8IKis_PUj=pZdmBiy@+Gr!NIdx_NO7}mxbTJUAcu0U
zRZC(BlCZI6wqcc^sgK_c_#k<-wE8oZ-Zeq{7QtQoqcD7&XPFS2p%f`<*kwH?T3R`G
z3ktq`J2O*zeCh8&D@mLA1c-CqwnzocC2*f>@0y0;-xnAG>pc3lJ=abL+agB&J!&mJ
zA0HX2dM*P9C(m2^%T)JY5oK;;w3Rz
ziBIpIxKr6ZHV2&dv9j?-;Xvh&^#H7ML-=KJ<=;@G*v5)s3QiLwa{JQ18$jQF^0jO*
zXL(I>!-tV~%0h@fIUpB$QYJ_SDzHR~8uQ)H7$Q_*FFUgN@vFjTP==k@K`gU(GE5Qu
zToBQB4;MesYyZi4C;V1VtQ)|H$?G9XaxTUh_sd~m@r$Ng{#cNrgmF&G0P=*sdjbhL
zVM_TFi4Do9SJ9@xXUdAC4?DLOyI-Y6-j5Gx5InvvnO>EeXCKug!em{l@I9I=zSZuw
z@?V`OQbKGCIG4m_rq|-Ek+C-pN;*9EO2q&>H!s_Y+y(Q`t~)iQgH)lUPHy)iuaxgs
z3>lO;QN3Xb(vw*$J#YodKb`>!*r+@;SjFI%?GXURhDT!$3tct#L)F};C;xmi@)UKr
z_ZTP@28V)m2E!adEsTZp|KLOo19=yrNOBsVH2rG87n>@T+aRrl`VozcI{?H6PBS`I
z_b*ZUGHhl~Y*
zb$Wx(^4^e-dWnq_qdtb?Q|T}%9kT$PFq*hmhcM*pNNwYXOMvP#D}YrC(TN7@#Ht?c
zVY!`w&BE0GC}Rhoatr$)rjm=*-X||iG|)w3obO)Moupg|BUrHA%2F;CLe(qVz!&!x
zm!;^&Ud^+Bu1ogD$CvV@hrj;L(6)Q@p&@>gIU>)*sLZ7{%6mdVddR8S#8xi?*FgO@gLzJDrU;u|b&eq#aJp`vh^m`$?L7hd
zshhfRdV(87pg-X*Pq29ZjsKn+tcE@EcMJj;5VIPql;+2UT(+%;@PhaJYOon8iimN|
zpj*&oI4kFt_^Oju2}Ol@OnbAi>j^TpDC2-wi++5yHAgvek8qbjkqpA5dq(+8KOoY^
zU*U`$g;x0H$)VKogIrz2bmGh)vji|4|AU~Q4ZiXy523?zpT{^3m
zSLiEtJbXV>UP{1Rpl$8_H
z6VbzGCU*v!0>bGP3y4tcldmiiRxJZiXKkl5-GnD{rLJu|-}4Vu(wJ)w`M)p*b%S2z
zshL+vC_B5ZBvC1cu&rFz`xo>=*(BA4%Gn-C}VX{Nr;BmTi<yuzDo8hLQ&C`TC6bzdBqL5S;dOtXx`I!o=-)|0CSL4g?`qfw}G3C#wp0ZtC+-
z{d_{t&IA>epb
z*U2Zh`S|{%_Mh0setBCWfrC>3&@3LX8?uo_moWc7UD~)K|A9@|vg0yB1axsvnIqyq
z(wuWbaABLD+APF)2crMmnEJ2raHU&&2WcKWATj~;`5iccER;fm!@=>2L<^~vDqYen
zk$1v~_yU020Asj#cl-;}I;{8hrS`_--J
zpP*gvf3!FEfcmkBCZB%g;WqpS+!$SX0&+rTT=m2+ZT_8D`ova%L74IZ9EH-c8rv*i
zMiA+{L1QESHCD?12N=dLmuDZf(RNUSV8ken?SIRV^~5^RChNlCp*%-VBV9Xbm^uZ3
zRRCWPgy5CGuSr&u7(TH4{pb!L9@s>c0P!nIuH0%xgH22h?A!C-sQ@4a3crb|P5wts
zi3BIO2lD?lu8lN;)02)>UeM`BT86rcq6EId|JraY0=z=`cd@2T%56B91yY~%zq98j
zhO$)r-02H;r!4#Mf+|z{e-M-9W*K-s5`02wSFoZ4mjEIPV*q*p*@ywsl~40MO+0)V?HS_K$>i-??%0T($e2ZXw&<^C&B>YL97{!;qD
zw%!0>lFN>e#3umE8e>%de#H+rk2WxNg+>qoP{Y{b6QNmftH8B5yve8D>{J5^#`^m)
z&)cES0AK)Im;{64t^br%)Cm|V9O7#4l39-L;rv%C098IfWy-%*eF4ia*c?EM(ErCR
zJbL;cu)0h)ljzFJxH)#rV431sYOMdio0J#$u*xdc3-e&Um_mxCJ>m2J`4m_sVLDcD
zh2`$2fME-XWf(jX>^lg3r1c-Oh|xkgEdP`-wi&@BwF6c3pA!OO%xyRU!g_!!uMQgY
zvFoJ$JNfiHH1aFB+u^_dP6$yz1L0_=DYd~JJEhpTxpcc2^XiTG(%#MgkLH0>MfH2S7)ab*gH
zx}%qxXX>I8o!$itI*;Q|iURbUzbtyz6amQy4JICrf{o)QF&!XL#Bs{=zz{e-#_i=G
z6TN!Mr=JXg3RspT4mKo&p~mtigcj5+SpBy?y=RFAr0^1nEsl6mb!U@3I+xdS>ah16
zhKnyY{8en#0{0++M+QABge~$oD+x=dfe6^lf7x_MmttX)Yh1m+zHQg92@wyF5yO>-ZLH7_B^knD!_lB@
zZ*kVrRi{Cy;wp6tBWzLmkQQ<+RB3pg{I5PgQO=3-0iJ8r;}qw;0UxOEEUv4(w^}$u
zrI(kqW=oQ_AL`e=KxQ5i4xJzr!Uo0(9k$Y+w+5itNgh80o{nq>3Hv+@q@K8Y+uWGr
zR~?dlN3`^W)4Qn8oEQ~C{X#btr)WY6m1CycdkbOV3N-6`B=2~jq`4n6<%mJ$#H0_gIS%X}Q?yUOy
z>zK$%jq{64(yU(D!KHc1Jz~Q8?6^JWbR0gI`~gf^)MU%_Gr6f`m$_Od#d&(iOC;B}
zG&Q%Otz8H^MTSYUa@<4Iv}nI#Ppq995Q4D2uAz-?10juGKD5+-drp0>iJ
+
@@ -38,7 +39,12 @@
-
+
+
+
+
+ MSBuild:Compile
+
diff --git a/samples/ControlCatalog/Icons.cs b/samples/ControlCatalog/Icons.cs
new file mode 100644
index 0000000000..58d0e0b7d4
--- /dev/null
+++ b/samples/ControlCatalog/Icons.cs
@@ -0,0 +1,194 @@
+namespace ControlCatalog
+{
+ internal static class Icons
+ {
+ // Layers/Composition
+ public const string Layers = "M12,16L19.36,10.27L21,9L12,2L3,9L4.63,10.27M12,18.54L4.62,12.81L3,14.07L12,21.07L21,14.07L19.37,12.8";
+
+ // Keyboard
+ public const string Keyboard = "M19,10H17V8H19M19,13H17V11H19M16,10H14V8H16M16,13H14V11H16M16,17H8V15H16M7,10H5V8H7M7,13H5V11H7M8,11H10V13H8M8,8H10V10H8M11,11H13V13H11M11,8H13V10H11M20,5H4C2.89,5 2,5.89 2,7V17A2,2 0 0,0 4,19H20A2,2 0 0,0 22,17V7C22,5.89 21.1,5 20,5Z";
+
+ // Blur/Acrylic
+ public const string Blur = "M14.5,10C16.43,10 18,8.43 18,6.5C18,4.57 16.43,3 14.5,3C12.57,3 11,4.57 11,6.5C11,8.43 12.57,10 14.5,10M14.5,4A2.5,2.5 0 0,1 17,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,4M8.5,14C10.43,14 12,12.43 12,10.5C12,8.57 10.43,7 8.5,7C6.57,7 5,8.57 5,10.5C5,12.43 6.57,14 8.5,14M8.5,8A2.5,2.5 0 0,1 11,10.5A2.5,2.5 0 0,1 8.5,13A2.5,2.5 0 0,1 6,10.5A2.5,2.5 0 0,1 8.5,8M14.5,16A4.5,4.5 0 0,0 10,20.5A4.5,4.5 0 0,0 14.5,25A4.5,4.5 0 0,0 19,20.5A4.5,4.5 0 0,0 14.5,16M14.5,24A3.5,3.5 0 0,1 11,20.5A3.5,3.5 0 0,1 14.5,17A3.5,3.5 0 0,1 18,20.5A3.5,3.5 0 0,1 14.5,24Z";
+
+ // Adorner/Sparkle
+ public const string Sparkle = "M12,1L9,9L1,12L9,15L12,23L15,15L23,12L15,9";
+
+ // Text/AutoComplete
+ public const string TextInput = "M20,21H4V3H20V21M6,9H14V7H6V9M6,13H18V11H6V13M6,17H14V15H6V17Z";
+
+ // Border/Square
+ public const string Border = "M4,4H20V20H4V4M6,8V6H8V8H6M6,12V10H8V12H6M6,16V14H8V16H6M6,20V18H8V20H6M10,20V18H12V20H10M14,20V18H16V20H14M18,20V18H20V20H18M18,16V14H20V16H18M18,12V10H20V12H18M18,8V6H20V8H18M14,8V6H16V8H14M10,8V6H12V8H10Z";
+
+ // Lightning/Cache
+ public const string Lightning = "M11,21H7L13,3H17L14,10H20L11,21Z";
+
+ // Cursor Click/Button
+ public const string CursorClick = "M10.76,8.69A0.76,0.76 0 0,0 10,9.45V20.9C10,21.32 10.34,21.66 10.76,21.66C10.95,21.66 11.11,21.6 11.24,21.5L13.15,19.95L14.81,23.57C14.94,23.84 15.21,24 15.5,24C15.62,24 15.74,23.97 15.85,23.92L17.28,23.26C17.69,23.07 17.86,22.59 17.66,22.17L16,18.54L18.44,18.19C18.87,18.13 19.1,17.73 18.91,17.37L12.1,8.89C11.95,8.76 11.79,8.69 10.76,8.69M15,10V8H20V10H15M13.83,4.76L16.66,1.93L18.07,3.34L15.24,6.17L13.83,4.76M10,0V5H8V0H10M3.93,14.66L6.76,11.83L8.17,13.24L5.34,16.07L3.93,14.66M3.93,3.34L5.34,1.93L8.17,4.76L6.76,6.17L3.93,3.34M7,10H2V8H7V10Z";
+
+ // Spinner/ButtonSpinner
+ public const string Spinner = "M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z";
+
+ // Calendar
+ public const string Calendar = "M19,19H5V8H19M16,1V3H8V1H6V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3H18V1M9,15H7V13H9V15M13,15H11V13H13V15M17,15H15V13H17V15M9,11H7V9H9V11M13,11H11V9H13V11M17,11H15V9H17V11Z";
+
+ // Canvas/Artboard
+ public const string Canvas = "M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3Z";
+
+ // Command/Terminal
+ public const string Terminal = "M20,19V7H4V19H20M20,3A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5C2,3.89 2.9,3 4,3H20M13,17V15H18V17H13M9.58,13L5.57,9H8.4L11.7,12.3C12.09,12.69 12.09,13.33 11.7,13.72L8.42,17H5.59L9.58,13Z";
+
+ // Slides/Carousel
+ public const string Slides = "M2,6H6V18H2V6M7,6H17V18H7V6M18,6H22V18H18V6Z";
+
+ // Checkbox
+ public const string Checkbox = "M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M10,17L5,12L6.41,10.58L10,14.17L17.59,6.58L19,8";
+
+ // Clipboard
+ public const string Clipboard = "M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3";
+
+ // Palette/Color
+ public const string Palette = "M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12Z";
+
+ // Dropdown/ComboBox
+ public const string Dropdown = "M7,10L12,15L17,10H7Z";
+
+ // Container/Box
+ public const string Container = "M2,2H8V4H16V2H22V8H20V16H22V22H16V20H8V22H2V16H4V8H2V2M16,8V6H8V8H6V16H8V18H16V16H18V8H16M4,4V6H6V4H4M18,4V6H20V4H18M4,18V20H6V18H4M18,18V20H20V18H18Z";
+
+ // Document/Page
+ public const string Document = "M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z";
+
+ // Menu/Context
+ public const string Menu = "M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z";
+
+ // Cursor/Pointer
+ public const string Cursor = "M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.39,12.92 18.83,13.03L14.83,13.78L17,18.5C17.24,19 17,19.59 16.5,19.83L13.64,21.97Z";
+
+ // Brush/Drawing
+ public const string Brush = "M20.71,4.63L19.37,3.29C19,2.9 18.35,2.9 17.96,3.29L9,12.25L11.75,15L20.71,6.04C21.1,5.65 21.1,5 20.71,4.63M7,14A3,3 0 0,0 4,17C4,18.31 2.84,19 2,19C2.92,20.22 4.5,21 6,21A4,4 0 0,0 10,17A3,3 0 0,0 7,14Z";
+
+ // Grid/Table
+ public const string Grid = "M10,4V8H14V4H10M16,4V8H20V4H16M16,10V14H20V10H16M16,16V20H20V16H16M14,20V16H10V20H14M8,20V16H4V20H8M8,14V10H4V14H8M8,8V4H4V8H8M10,14H14V10H10V14Z";
+
+ // Shield/Validation
+ public const string Shield = "M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1Z";
+
+ // Clock
+ public const string Clock = "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.9L16.2,16.2Z";
+
+ // Dialog/Message
+ public const string Dialog = "M20,2H4A2,2 0 0,0 2,4V22L6,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2Z";
+
+ // DragDrop/Move
+ public const string DragDrop = "M13,6V11H18V7.75L22.25,12L18,16.25V13H13V18H16.25L12,22.25L7.75,18H11V13H6V16.25L1.75,12L6,7.75V11H11V6H7.75L12,1.75L16.25,6H13Z";
+
+ // Sidebar/Drawer
+ public const string Drawer = "M3,2H21A1,1 0 0,1 22,3V21A1,1 0 0,1 21,22H3A1,1 0 0,1 2,21V3A1,1 0 0,1 3,2M10,4V20H20V4H10M4,4V20H8V4H4Z";
+
+ // Expand
+ public const string Expand = "M10,21V19H6.41L10.91,14.5L9.5,13.09L5,17.59V14H3V21H10M14.5,10.91L19,6.41V10H21V3H14V5H17.59L13.09,9.5L14.5,10.91Z";
+
+ // Flyout/Popup
+ public const string Flyout = "M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M7,9H10V7H7V9M7,13H17V11H7V13M7,17H14V15H7V17Z";
+
+ // Target/Focus
+ public const string Target = "M12,8A4,4 0 0,0 8,12A4,4 0 0,0 12,16A4,4 0 0,0 16,12A4,4 0 0,0 12,8M3.05,13H1V11H3.05C3.5,6.83 6.83,3.5 11,3.05V1H13V3.05C17.17,3.5 20.5,6.83 20.95,11H23V13H20.95C20.5,17.17 17.17,20.5 13,20.95V23H11V20.95C6.83,20.5 3.5,17.17 3.05,13M12,5A7,7 0 0,0 5,12A7,7 0 0,0 12,19A7,7 0 0,0 19,12A7,7 0 0,0 12,5Z";
+
+ // Gesture/Touch
+ public const string Gesture = "M5,15A2,2 0 0,0 3,17V19H7V17A2,2 0 0,0 5,15M5,11A6,6 0 0,0 -1,17H1A4,4 0 0,1 5,13V11M5,7C0.03,7 -4,11.03 -4,16V17H-2V16C-2,12.13 1.13,9 5,9V7M20.5,9.5L12,18L8.5,14.5L7.08,15.91L12,20.84L21.92,10.91L20.5,9.5Z";
+
+ // Image/Photo
+ public const string Image = "M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z";
+
+ // Tag/Label
+ public const string Tag = "M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.45 21.77,11.95 21.41,11.58Z";
+
+ // Transform/Rotate
+ public const string Transform = "M16.89,15.5L18.31,16.89C19.21,15.73 19.76,14.39 19.93,13H17.91C17.77,13.87 17.43,14.72 16.89,15.5M13,17.9V19.92C14.39,19.75 15.74,19.21 16.9,18.31L15.46,16.87C14.71,17.41 13.87,17.76 13,17.9M19.93,11C19.76,9.61 19.21,8.27 18.31,7.11L16.89,8.53C17.43,9.28 17.77,10.13 17.91,11M15.55,5.55L11,1V5.07C7.06,5.56 4,8.92 4,12.95C4,17.18 7.32,20.56 11.46,20.93C11.64,20.95 11.82,20.97 12,20.97C12.18,20.97 12.36,20.95 12.54,20.93C12.69,20.92 12.83,20.89 13,20.87V18.9C12.82,18.92 12.65,18.95 12.47,18.95C12.32,18.96 12.16,18.97 12,18.97C8.42,18.97 5.53,16.12 5.53,12.58C5.53,9.36 7.88,6.67 11,6.15V10L15.55,5.55Z";
+
+ // List
+ public const string List = "M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z";
+
+ // Bell/Notification
+ public const string Bell = "M21,19V20H3V19L5,17V11C5,7.9 7.03,5.17 10,4.29C10,4.19 10,4.1 10,4A2,2 0 0,1 12,2A2,2 0 0,1 14,4C14,4.1 14,4.19 14,4.29C16.97,5.17 19,7.9 19,11V17L21,19M14,21A2,2 0 0,1 12,23A2,2 0 0,1 10,21";
+
+ // Number/Hash
+ public const string Number = "M5.41,21L6.12,17H2.12L2.47,15H6.47L7.53,9H3.53L3.88,7H7.88L8.59,3H10.59L9.88,7H15.88L16.59,3H18.59L17.88,7H21.88L21.53,9H17.53L16.47,15H20.47L20.12,17H16.12L15.41,21H13.41L14.12,17H8.12L7.41,21H5.41M9.47,9L8.41,15H14.41L15.47,9H9.47Z";
+
+ // Cube/3D
+ public const string Cube = "M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L5,8.09V15.91L12,19.85L19,15.91V8.09L12,4.15Z";
+
+ // Dots/Pagination
+ public const string Dots = "M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z";
+
+ // Info/Information
+ public const string Info = "M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z";
+
+ // Progress/Loading
+ public const string Progress = "M13,2.03V2.05L13,4.05C17.39,4.59 20.5,8.58 19.96,12.97C19.5,16.61 16.64,19.5 13,19.93V21.93C18.5,21.38 22.5,16.5 21.95,11C21.5,6.25 17.73,2.5 13,2.03M11,2.06C9.05,2.25 7.19,3 5.67,4.26L7.1,5.74C8.22,4.84 9.57,4.26 11,4.06V2.06M4.26,5.67C3,7.19 2.25,9.04 2.05,11H4.05C4.24,9.58 4.8,8.23 5.69,7.1L4.26,5.67M2.06,13C2.26,14.96 3.03,16.81 4.27,18.33L5.69,16.9C4.81,15.77 4.24,14.42 4.06,13H2.06M7.1,18.37L5.67,19.74C7.18,21 9.04,21.79 11,22V20C9.58,19.82 8.23,19.25 7.1,18.37Z";
+
+ // Radio
+ public const string Radio = "M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7Z";
+
+ // Refresh
+ public const string Refresh = "M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z";
+
+ // Layout/Grid
+ public const string Layout = "M19,11H13V5H19M19,19H13V13H19M11,11H5V5H11M11,19H5V13H11M3,3V21H21V3";
+
+ // Scroll
+ public const string Scroll = "M15,20H9V12H4.16L12,4.16L19.84,12H15V20Z";
+
+ // Slider/Tune
+ public const string Slider = "M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z";
+
+ // Split/SplitView
+ public const string Split = "M18,4H6C4.89,4 4,4.89 4,6V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18V6C20,4.89 19.1,4 18,4M18,6V18H13V6H18Z";
+
+ // Tab
+ public const string Tab = "M21,3H3C1.89,3 1,3.89 1,5V19A2,2 0 0,0 3,21H21C22.1,21 23,20.1 23,19V5C23,3.89 22.1,3 21,3M21,19H3V5H13V9H21V19Z";
+
+ // Theme/Palette
+ public const string Theme = "M12,18V6A6,6 0 0,1 18,12A6,6 0 0,1 12,18M20,15.31L23.31,12L20,8.69V4H15.31L12,0.69L8.69,4H4V8.69L0.69,12L4,15.31V20H8.69L12,23.31L15.31,20H20V15.31Z";
+
+ // Toggle/Switch
+ public const string Toggle = "M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z";
+
+ // Tooltip/Info bubble
+ public const string Tooltip = "M4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4A2,2 0 0,1 4,2M11,15H13V13H11V15M11,11H13V5H11V11Z";
+
+ // Animation/Transition
+ public const string Transition = "M2,5.27L3.28,4L20,20.72L18.73,22L12.73,16H7V19L3,15L7,11V14H9.73L5,9.27V14H3V5.27M21,9L17,5V8H14V10H17V13L21,9M17,17V14H15V17H17Z";
+
+ // Tree/Hierarchy
+ public const string Tree = "M15,20A1,1 0 0,0 16,19V4H8V19A1,1 0 0,0 9,20H2V22H22V20H15Z";
+
+ // Viewbox/Resize
+ public const string Viewbox = "M5,15H3V19A2,2 0 0,0 5,21H9V19H5M5,5H9V3H5A2,2 0 0,0 3,5V9H5M19,3H15V5H19V9H21V5A2,2 0 0,0 19,3M19,19H15V21H19A2,2 0 0,0 21,19V15H19";
+
+ // Puzzle/Embed
+ public const string Puzzle = "M20.5,11H19V7C19,5.89 18.1,5 17,5H13V3.5A2.5,2.5 0 0,0 10.5,1A2.5,2.5 0 0,0 8,3.5V5H4A2,2 0 0,0 2,7V10.8H3.5C5.04,10.8 6.2,11.96 6.2,13.5C6.2,15.04 5.04,16.2 3.5,16.2H2V20A2,2 0 0,0 4,22H7.8V20.5C7.8,18.96 8.96,17.8 10.5,17.8C12.04,17.8 13.2,18.96 13.2,20.5V22H17A2,2 0 0,0 19,20V16H20.5A2.5,2.5 0 0,0 23,13.5A2.5,2.5 0 0,0 20.5,11Z";
+
+ // Window/Application
+ public const string Window = "M4,4H20V20H4V4M6,8V18H18V8H6Z";
+
+ // Header/Title
+ public const string Header = "M3,4H5V10H9V4H11V18H9V12H5V18H3V4M13,8H15.31L15.63,5H17.63L17.31,8H19.31L19.63,5H21.63L21.31,8H23V10H21.1L20.9,12H23V14H20.69L20.37,17H18.37L18.69,14H16.69L16.37,17H14.37L14.69,14H13V12H14.9L15.1,10H13V8M17.1,10L16.9,12H18.9L19.1,10H17.1Z";
+
+ // Monitor/Screen
+ public const string Monitor = "M21,16H3V4H21M21,2H3C1.89,2 1,2.89 1,4V16A2,2 0 0,0 3,18H10V20H8V22H16V20H14V18H21A2,2 0 0,0 23,16V4C23,2.89 22.1,2 21,2Z";
+
+ // Navigation/Compass
+ public const string Navigation = "M12,2L4.5,20.29L5.21,21L12,18L18.79,21L19.5,20.29L12,2Z";
+
+ // OpenGL/3D Box (same as Cube but outline variant)
+ public const string Cube3D = "M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z";
+
+ // Horizontal dots/Pager
+ public const string HorizontalDots = "M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z";
+
+ // Slider tune/adjust
+ public const string Tune = "M3,17V19H9V17H3M3,5V7H13V5H3M13,21V19H21V17H13V15H11V21H13M7,9V11H3V13H7V15H9V9H7M21,13V11H11V13H21M15,9H17V7H21V5H17V3H15V9Z";
+ }
+}
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 3a6d57801a..ea71773d13 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -1,312 +1,157 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ 20
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ None
+ BorderOnly
+ Full
+
+
+
+
+ Default
+ Light
+ Dark
+
+
+
+
+ Fluent
+ Simple
+
+
+
+
+ None
+ Transparent
+ Blur
+ AcrylicBlur
+ Mica
+
+
+
+
+ LeftToRight
+ RightToLeft
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- None
- BorderOnly
- Full
-
-
-
-
- Default
- Light
- Dark
-
-
-
-
- Fluent
- Simple
-
-
-
-
- None
- Transparent
- Blur
- AcrylicBlur
- Mica
-
-
-
-
- LeftToRight
- RightToLeft
-
-
-
-
-
-
-
-
-
+
+
diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs
index 82172a8ec4..5ad85da149 100644
--- a/samples/ControlCatalog/MainView.xaml.cs
+++ b/samples/ControlCatalog/MainView.xaml.cs
@@ -10,13 +10,80 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog
{
- public partial class MainView : UserControl
+ public partial class MainView : DrawerPage
{
private Action? _disposeTransparencySetters;
public MainView()
{
InitializeComponent();
+
+ Loaded += MainView_Loaded;
+ Unloaded += MainView_Unloaded;
+ }
+
+ private const double WideBreakpoint = 1008;
+ private const double NarrowBreakpoint = 640;
+
+ private void MainView_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ if (DataContext == null)
+ return;
+
+ SizeChanged += OnDrawerSizeChanged;
+ UpdateAdaptiveLayout();
+ }
+
+ private void MainView_Unloaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ SizeChanged -= OnDrawerSizeChanged;
+ _lastAppliedMode = null;
+ }
+
+ private SplitViewDisplayMode? _lastAppliedMode;
+ private bool _updatingLayout;
+
+ private void OnDrawerSizeChanged(object? sender, SizeChangedEventArgs e)
+ {
+ if (e.WidthChanged)
+ UpdateAdaptiveLayout();
+ }
+
+ private void UpdateAdaptiveLayout()
+ {
+ if (_updatingLayout || DataContext == null)
+ return;
+
+ var width = Bounds.Width;
+ if (width <= 0)
+ return;
+
+ SplitViewDisplayMode targetMode;
+ if (width >= WideBreakpoint)
+ targetMode = SplitViewDisplayMode.Inline;
+ else if (width >= NarrowBreakpoint)
+ targetMode = SplitViewDisplayMode.CompactInline;
+ else
+ targetMode = SplitViewDisplayMode.Overlay;
+
+ if (_lastAppliedMode == targetMode)
+ return;
+
+ _updatingLayout = true;
+ try
+ {
+ _lastAppliedMode = targetMode;
+ ViewModel.DisplayMode = targetMode;
+
+ if (targetMode == SplitViewDisplayMode.Inline)
+ ViewModel.IsDrawerOpened = true;
+ else if (targetMode == SplitViewDisplayMode.Overlay)
+ ViewModel.IsDrawerOpened = false;
+ }
+ finally
+ {
+ _updatingLayout = false;
+ }
}
private void Themes_SelectionChanged(object? sender, SelectionChangedEventArgs e)
@@ -66,22 +133,38 @@ namespace ControlCatalog
var semiTransparentBrush = new ImmutableSolidColorBrush(Colors.Gray, 0.2);
_disposeTransparencySetters =
(Action)topLevel.SetValue(BackgroundProperty, transparentBrush, Avalonia.Data.BindingPriority.Style)!.Dispose +
- Sidebar.SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style)!.Dispose +
- Sidebar.SetValue(SplitView.PaneBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style)!.Dispose;
+ SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style)!.Dispose +
+ SetValue(DrawerPage.DrawerBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style)!.Dispose;
}
}
}
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+
+ if (ViewModel != null)
+ {
+ ViewModel.Navigator = NavPage;
+ }
+ }
+
internal MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!;
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
- if (TopLevel.GetTopLevel(this) is Window window)
- Decorations.SelectedIndex = (int)window.WindowDecorations;
+ if (DataContext == null)
+ return;
- var insets = TopLevel.GetTopLevel(this)!.InsetsManager;
+ UpdateAdaptiveLayout();
+
+ var topLevel = TopLevel.GetTopLevel(this)!;
+ if (topLevel is Window window)
+ ViewModel.SelectedDecorationIndex = (int)window.WindowDecorations;
+
+ var insets = topLevel.InsetsManager;
if (insets != null)
{
// In real life application these events should be unsubscribed to avoid memory leaks.
@@ -111,6 +194,8 @@ namespace ControlCatalog
ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true;
};
}
+
+ ViewModel.SelectedPageIndex = 0;
}
}
}
diff --git a/samples/ControlCatalog/NavHeaderItem.xaml b/samples/ControlCatalog/NavHeaderItem.xaml
new file mode 100644
index 0000000000..6867661ebb
--- /dev/null
+++ b/samples/ControlCatalog/NavHeaderItem.xaml
@@ -0,0 +1,101 @@
+
+
+
+
+
+ #99FFFFFF
+ #FF1F1F1F
+ #FF000000
+ #FF171717
+
+
+ #99000000
+ #FFE6E6E6
+ #FFFFFFFF
+ #FFF2F2F2
+
+
+ 36
+ 1 1 1 1 #2000, 0 0 1 1 #2fff
+
+
+
+ Disabled
+
+ Test
+
+ Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/AcceleratorPage.xaml b/samples/ControlCatalog/Pages/AcceleratorPage.xaml
index 18072ef1c3..62fcf0d746 100644
--- a/samples/ControlCatalog/Pages/AcceleratorPage.xaml
+++ b/samples/ControlCatalog/Pages/AcceleratorPage.xaml
@@ -1,115 +1,116 @@
-
-
+
-
-
-
+
-
+
-
- Accelerator Support
+
+ Accelerator Support
-
-
-
- This is tab 1 content
-
- This is tab 1 content
-
- This is tab 1 content
-
+
+
+
+ This is tab 1 content
+
+ This is tab 1 content
+
+ This is tab 1 content
+
-
-
- This is tab 2 content
-
-
+
+
+ This is tab 2 content
+
+
-
-
- This is tab 4 content
-
-
- This is fab 5 content
-
-
-
+
+
+ This is tab 4 content
+
+
+ This is fab 5 content
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
- _Button 1
- _Button 2
- _Button 3
+
+ _Button 1
+ _Button 2
+ _Button 3
+
-
-
+
diff --git a/samples/ControlCatalog/Pages/AcceleratorPage.xaml.cs b/samples/ControlCatalog/Pages/AcceleratorPage.xaml.cs
index e013d9aaac..40afacc053 100644
--- a/samples/ControlCatalog/Pages/AcceleratorPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/AcceleratorPage.xaml.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class AcceleratorPage : UserControl
+ public partial class AcceleratorPage : ContentPage
{
public AcceleratorPage()
{
diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml
index 4fd0933879..4a68fbfafd 100644
--- a/samples/ControlCatalog/Pages/AcrylicPage.xaml
+++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml
@@ -1,12 +1,13 @@
-
-
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs b/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs
index 9516655bb9..adb5b05706 100644
--- a/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs
@@ -2,7 +2,7 @@
namespace ControlCatalog.Pages
{
- public partial class AcrylicPage : UserControl
+ public partial class AcrylicPage : ContentPage
{
public AcrylicPage()
{
diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml
index e9a245a8e1..35907c7ef8 100644
--- a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml
+++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml
@@ -1,52 +1,52 @@
-
+
+
+ Rotation
+
+
-
-
- Rotation
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs
index 526b8a492b..5ee8d44479 100644
--- a/samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/AdornerLayerPage.xaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
- public partial class AdornerLayerPage : UserControl
+ public partial class AdornerLayerPage : ContentPage
{
private Control? _adorner;
diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
index 35e917f996..4edb00b7c3 100644
--- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
@@ -1,79 +1,83 @@
-
-
- A control into which the user can input text
+
+
+ A control into which the user can input text
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
index 47b6eb7f4a..295aa9d139 100644
--- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
@@ -11,7 +11,7 @@ using ControlCatalog.Models;
namespace ControlCatalog.Pages
{
- public partial class AutoCompleteBoxPage : UserControl
+ public partial class AutoCompleteBoxPage : ContentPage
{
private static StateData[] BuildAllStates()
{
diff --git a/samples/ControlCatalog/Pages/BitmapCachePage.axaml b/samples/ControlCatalog/Pages/BitmapCachePage.axaml
index 5902be8e51..4298f29e8f 100644
--- a/samples/ControlCatalog/Pages/BitmapCachePage.axaml
+++ b/samples/ControlCatalog/Pages/BitmapCachePage.axaml
@@ -1,45 +1,49 @@
-
+
-
-
-
-
-
-
- Render at scale
-
- Scale
-
- Enable clear type
+
+
+
+
+
+
+ Render at scale
+
+ Scale
+
+ Enable clear type
- Snap to device pixels
- Subpixel offset X
-
-
-
-
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
-
-
-
+ Snap to device pixels
+ Subpixel offset X
+
+
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
-
+
+
+
-
+
+
diff --git a/samples/ControlCatalog/Pages/BitmapCachePage.axaml.cs b/samples/ControlCatalog/Pages/BitmapCachePage.axaml.cs
index 92b3cbec68..be7cf7a7ea 100644
--- a/samples/ControlCatalog/Pages/BitmapCachePage.axaml.cs
+++ b/samples/ControlCatalog/Pages/BitmapCachePage.axaml.cs
@@ -4,10 +4,10 @@ using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages;
-public partial class BitmapCachePage : UserControl
+public partial class BitmapCachePage : ContentPage
{
public BitmapCachePage()
{
InitializeComponent();
}
-}
\ No newline at end of file
+}
diff --git a/samples/ControlCatalog/Pages/BorderPage.xaml b/samples/ControlCatalog/Pages/BorderPage.xaml
index c811ddaa22..297e3fcfb8 100644
--- a/samples/ControlCatalog/Pages/BorderPage.xaml
+++ b/samples/ControlCatalog/Pages/BorderPage.xaml
@@ -1,61 +1,65 @@
-
-
-
-
-
- A control which decorates a child with a border and background
+
+
+
+
+
+ A control which decorates a child with a border and background
-
-
- Border
-
-
- Background And CenterBorder
-
-
- Background And InnerBorderEdge
-
-
- Background And OuterBorderEdge
-
-
- Rounded Corners
-
-
- Rounded Corners
-
-
-
-
-
+
+
+ Border
+
+
+ Background And CenterBorder
+
+
+ Background And InnerBorderEdge
+
+
+ Background And OuterBorderEdge
+
+
+ Rounded Corners
+
+
+ Rounded Corners
+
+
+
+
+
+
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/BorderPage.xaml.cs b/samples/ControlCatalog/Pages/BorderPage.xaml.cs
index 944df86f8c..71aebf24ff 100644
--- a/samples/ControlCatalog/Pages/BorderPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/BorderPage.xaml.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class BorderPage : UserControl
+ public partial class BorderPage : ContentPage
{
public BorderPage()
{
diff --git a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml
index 900e304559..c21a772c2a 100644
--- a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml
+++ b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml
@@ -1,6 +1,7 @@
-
@@ -30,4 +31,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs
index fad4b466fe..28f4b20084 100644
--- a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs
@@ -3,7 +3,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class ButtonSpinnerPage : UserControl
+ public partial class ButtonSpinnerPage : ContentPage
{
public ButtonSpinnerPage()
{
diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml b/samples/ControlCatalog/Pages/ButtonsPage.xaml
index f13581aa18..695ae28515 100644
--- a/samples/ControlCatalog/Pages/ButtonsPage.xaml
+++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml
@@ -1,8 +1,9 @@
-
-
+
@@ -16,9 +17,9 @@
-
+
-
+
-
+
@@ -288,4 +289,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs
index 6dcee1ab7d..611fc76241 100644
--- a/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs
@@ -3,7 +3,7 @@ using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
- public partial class ButtonsPage : UserControl
+ public partial class ButtonsPage : ContentPage
{
private int repeatButtonClickCount = 0;
diff --git a/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
index 8734758d26..769dbc0dac 100644
--- a/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
+++ b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
@@ -1,7 +1,8 @@
-
A control for selecting dates with a calendar drop-down
@@ -52,4 +53,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs
index 73e1fe4bba..0e4fcbd986 100644
--- a/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs
@@ -3,7 +3,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class CalendarDatePickerPage : UserControl
+ public partial class CalendarDatePickerPage : ContentPage
{
public CalendarDatePickerPage()
{
diff --git a/samples/ControlCatalog/Pages/CalendarPage.xaml b/samples/ControlCatalog/Pages/CalendarPage.xaml
index e142c4da72..99727e9124 100644
--- a/samples/ControlCatalog/Pages/CalendarPage.xaml
+++ b/samples/ControlCatalog/Pages/CalendarPage.xaml
@@ -1,5 +1,6 @@
-
A calendar control for selecting dates
@@ -49,4 +50,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/CalendarPage.xaml.cs b/samples/ControlCatalog/Pages/CalendarPage.xaml.cs
index 56f511be55..d9118326f9 100644
--- a/samples/ControlCatalog/Pages/CalendarPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CalendarPage.xaml.cs
@@ -3,7 +3,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class CalendarPage : UserControl
+ public partial class CalendarPage : ContentPage
{
public CalendarPage()
{
diff --git a/samples/ControlCatalog/Pages/CanvasPage.xaml b/samples/ControlCatalog/Pages/CanvasPage.xaml
index 416164f391..a9e342181d 100644
--- a/samples/ControlCatalog/Pages/CanvasPage.xaml
+++ b/samples/ControlCatalog/Pages/CanvasPage.xaml
@@ -1,5 +1,6 @@
-
A panel which lays out its children by explicit coordinates
@@ -34,4 +35,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/CanvasPage.xaml.cs b/samples/ControlCatalog/Pages/CanvasPage.xaml.cs
index e52df597a9..b94aabca94 100644
--- a/samples/ControlCatalog/Pages/CanvasPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CanvasPage.xaml.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class CanvasPage : UserControl
+ public partial class CanvasPage : ContentPage
{
public CanvasPage()
{
diff --git a/samples/ControlCatalog/Pages/CarouselDemoPage.xaml b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml
index df4317fcad..792441ddb4 100644
--- a/samples/ControlCatalog/Pages/CarouselDemoPage.xaml
+++ b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml
@@ -1,11 +1,14 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs
index 36c9961658..0d59d296f6 100644
--- a/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CarouselDemoPage.xaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
- public partial class CarouselDemoPage : UserControl
+ public partial class CarouselDemoPage : ContentPage
{
private static readonly (string Group, string Title, string Description, Func Factory)[] Demos =
{
diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml b/samples/ControlCatalog/Pages/CarouselPage.xaml
index c6e20fec5b..bdb40b9038 100644
--- a/samples/ControlCatalog/Pages/CarouselPage.xaml
+++ b/samples/ControlCatalog/Pages/CarouselPage.xaml
@@ -1,5 +1,6 @@
-
A swipeable items control that can reveal adjacent pages with ViewportFraction.
@@ -115,4 +116,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml.cs b/samples/ControlCatalog/Pages/CarouselPage.xaml.cs
index 0a0c973b90..c4a6ffa12f 100644
--- a/samples/ControlCatalog/Pages/CarouselPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CarouselPage.xaml.cs
@@ -7,7 +7,7 @@ using ControlCatalog.Pages.Transitions;
namespace ControlCatalog.Pages
{
- public partial class CarouselPage : UserControl
+ public partial class CarouselPage : ContentPage
{
public CarouselPage()
{
diff --git a/samples/ControlCatalog/Pages/CheckBoxPage.xaml b/samples/ControlCatalog/Pages/CheckBoxPage.xaml
index 2f60fc5dae..4f9c59448b 100644
--- a/samples/ControlCatalog/Pages/CheckBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/CheckBoxPage.xaml
@@ -1,5 +1,6 @@
-
A check box control
@@ -25,4 +26,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/CheckBoxPage.xaml.cs b/samples/ControlCatalog/Pages/CheckBoxPage.xaml.cs
index 95ff21ef6d..b63d7084d6 100644
--- a/samples/ControlCatalog/Pages/CheckBoxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CheckBoxPage.xaml.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class CheckBoxPage : UserControl
+ public partial class CheckBoxPage : ContentPage
{
public CheckBoxPage()
{
diff --git a/samples/ControlCatalog/Pages/ClipboardPage.xaml b/samples/ControlCatalog/Pages/ClipboardPage.xaml
index 864a520aca..86e99b9305 100644
--- a/samples/ControlCatalog/Pages/ClipboardPage.xaml
+++ b/samples/ControlCatalog/Pages/ClipboardPage.xaml
@@ -1,5 +1,6 @@
-
Example of ClipboardPage capabilities
@@ -27,4 +28,4 @@
/>
-
+
diff --git a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
index 65f58d0444..0bc5447313 100644
--- a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
@@ -15,7 +15,7 @@ using Avalonia.Threading;
namespace ControlCatalog.Pages
{
- public partial class ClipboardPage : UserControl
+ public partial class ClipboardPage : ContentPage
{
private readonly DataFormat _customBinaryDataFormat =
DataFormat.CreateBytesApplicationFormat("controlcatalog-binary-data");
diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml
index fb397085e7..f0b303e0d4 100644
--- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml
+++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml
@@ -1,4 +1,4 @@
-
-
+
-
+
-
+
diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs b/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
index 4d2d236ed3..cc225b576d 100644
--- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
@@ -5,7 +5,7 @@ using Avalonia.Media;
namespace ControlCatalog.Pages
{
- public partial class ColorPickerPage : UserControl
+ public partial class ColorPickerPage : ContentPage
{
public ColorPickerPage()
{
diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml
index 449c10f000..149fd73d0a 100644
--- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml
@@ -1,4 +1,4 @@
-
-
+
diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
index 301b052cb8..720aae9ef5 100644
--- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
@@ -4,7 +4,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class ComboBoxPage : UserControl
+ public partial class ComboBoxPage : ContentPage
{
public ComboBoxPage()
{
diff --git a/samples/ControlCatalog/Pages/CommandBarPage.xaml b/samples/ControlCatalog/Pages/CommandBarPage.xaml
index 6cf280ea95..3c648ea012 100644
--- a/samples/ControlCatalog/Pages/CommandBarPage.xaml
+++ b/samples/ControlCatalog/Pages/CommandBarPage.xaml
@@ -1,8 +1,11 @@
-
-
+
-
-
-
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CommandBarPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBarPage.xaml.cs
index b759c7b605..0b81c698d2 100644
--- a/samples/ControlCatalog/Pages/CommandBarPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CommandBarPage.xaml.cs
@@ -7,7 +7,7 @@ using Avalonia.Media;
namespace ControlCatalog.Pages
{
- public partial class CommandBarPage : UserControl
+ public partial class CommandBarPage : ContentPage
{
private static readonly (string Group, string Title, string Description, Func Factory)[] Demos =
{
diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml b/samples/ControlCatalog/Pages/CompositionPage.axaml
index 4d9bb41781..a3e9b047a8 100644
--- a/samples/ControlCatalog/Pages/CompositionPage.axaml
+++ b/samples/ControlCatalog/Pages/CompositionPage.axaml
@@ -1,62 +1,61 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Resize me
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Resize me
+
+
+
+
+
+
+
+ Thread.Sleep(10000);
+
+
+
+
+
+
+ Thread.Sleep(10000);
+ Start
+ Stop
+ Precise dirty rects
-
-
-
- Thread.Sleep(10000);
-
-
-
-
-
-
- Thread.Sleep(10000);
- Start
- Stop
- Precise dirty rects
-
-
-
-
-
-
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs
index 287bde0be8..8f5d75f4ce 100644
--- a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs
@@ -15,7 +15,7 @@ using Math = System.Math;
namespace ControlCatalog.Pages;
-public partial class CompositionPage : UserControl
+public partial class CompositionPage : TabbedPage
{
private ImplicitAnimationCollection? _implicitAnimations;
private CompositionCustomVisual? _customVisual;
diff --git a/samples/ControlCatalog/Pages/ConnectedAnimationDemoPage.xaml b/samples/ControlCatalog/Pages/ConnectedAnimationDemoPage.xaml
index bf37b4cc4f..ca4a85af8d 100644
--- a/samples/ControlCatalog/Pages/ConnectedAnimationDemoPage.xaml
+++ b/samples/ControlCatalog/Pages/ConnectedAnimationDemoPage.xaml
@@ -1,6 +1,8 @@
-
+
-
+
+
diff --git a/samples/ControlCatalog/Pages/ConnectedAnimationDemoPage.xaml.cs b/samples/ControlCatalog/Pages/ConnectedAnimationDemoPage.xaml.cs
index 0c13bb360e..5d666730ae 100644
--- a/samples/ControlCatalog/Pages/ConnectedAnimationDemoPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ConnectedAnimationDemoPage.xaml.cs
@@ -7,7 +7,7 @@ using Avalonia.Media;
namespace ControlCatalog.Pages
{
- public partial class ConnectedAnimationDemoPage : UserControl
+ public partial class ConnectedAnimationDemoPage : ContentPage
{
private static readonly (string Group, string Title, string Description, Func Factory)[] Demos = [];
diff --git a/samples/ControlCatalog/Pages/ContainerQueryPage.xaml b/samples/ControlCatalog/Pages/ContainerQueryPage.xaml
index e6b0558a04..96f7a87586 100644
--- a/samples/ControlCatalog/Pages/ContainerQueryPage.xaml
+++ b/samples/ControlCatalog/Pages/ContainerQueryPage.xaml
@@ -1,9 +1,10 @@
-
@@ -107,4 +108,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/ContainerQueryPage.xaml.cs b/samples/ControlCatalog/Pages/ContainerQueryPage.xaml.cs
index acd67c6629..b43723005e 100644
--- a/samples/ControlCatalog/Pages/ContainerQueryPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ContainerQueryPage.xaml.cs
@@ -2,7 +2,7 @@
namespace ControlCatalog.Pages
{
- public partial class ContainerQueryPage : UserControl
+ public partial class ContainerQueryPage : ContentPage
{
public ContainerQueryPage()
{
diff --git a/samples/ControlCatalog/Pages/ContentDemoPage.xaml b/samples/ControlCatalog/Pages/ContentDemoPage.xaml
index d8f2155a1b..bcf0ca67ef 100644
--- a/samples/ControlCatalog/Pages/ContentDemoPage.xaml
+++ b/samples/ControlCatalog/Pages/ContentDemoPage.xaml
@@ -1,11 +1,14 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ContentDemoPage.xaml.cs b/samples/ControlCatalog/Pages/ContentDemoPage.xaml.cs
index e667cfc0ff..b47dde07c1 100644
--- a/samples/ControlCatalog/Pages/ContentDemoPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ContentDemoPage.xaml.cs
@@ -7,7 +7,7 @@ using Avalonia.Media;
namespace ControlCatalog.Pages
{
- public partial class ContentDemoPage : UserControl
+ public partial class ContentDemoPage : ContentPage
{
private static readonly (string Group, string Title, string Description, Func Factory)[] Demos =
{
diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml
index c23393e63c..fa52c05bfa 100644
--- a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml
+++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml
@@ -1,18 +1,19 @@
-
-
+
-
+
@@ -156,4 +157,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs
index bef2c8212d..f25b9341c3 100644
--- a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs
@@ -7,7 +7,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class ContextFlyoutPage : UserControl
+ public partial class ContextFlyoutPage : ContentPage
{
public ContextFlyoutPage()
{
diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml
index 0e7c7b9451..4d1d02aed8 100644
--- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml
+++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml
@@ -1,7 +1,8 @@
-
A right click menu that can be applied to any control.
@@ -86,4 +87,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
index a71398181c..86ce807127 100644
--- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
@@ -6,7 +6,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class ContextMenuPage : UserControl
+ public partial class ContextMenuPage : ContentPage
{
public ContextMenuPage()
{
diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml b/samples/ControlCatalog/Pages/CursorPage.xaml
index 66f2b8b2e3..ae20193c97 100644
--- a/samples/ControlCatalog/Pages/CursorPage.xaml
+++ b/samples/ControlCatalog/Pages/CursorPage.xaml
@@ -1,6 +1,7 @@
-
@@ -27,4 +28,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml.cs b/samples/ControlCatalog/Pages/CursorPage.xaml.cs
index f9119175e2..88699ac6f2 100644
--- a/samples/ControlCatalog/Pages/CursorPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CursorPage.xaml.cs
@@ -3,7 +3,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class CursorPage : UserControl
+ public partial class CursorPage : ContentPage
{
public CursorPage()
{
diff --git a/samples/ControlCatalog/Pages/CustomDrawing.xaml b/samples/ControlCatalog/Pages/CustomDrawing.xaml
index 04b7fcfea5..f1cb2dab14 100644
--- a/samples/ControlCatalog/Pages/CustomDrawing.xaml
+++ b/samples/ControlCatalog/Pages/CustomDrawing.xaml
@@ -1,15 +1,16 @@
-
-
+
-
+
@@ -104,4 +105,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/CustomDrawing.xaml.cs b/samples/ControlCatalog/Pages/CustomDrawing.xaml.cs
index d3fedb76be..0eae942075 100644
--- a/samples/ControlCatalog/Pages/CustomDrawing.xaml.cs
+++ b/samples/ControlCatalog/Pages/CustomDrawing.xaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
- public partial class CustomDrawing : UserControl
+ public partial class CustomDrawing : ContentPage
{
public CustomDrawing()
{
diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml
index 3187e13f9f..04adf56700 100644
--- a/samples/ControlCatalog/Pages/DataGridPage.xaml
+++ b/samples/ControlCatalog/Pages/DataGridPage.xaml
@@ -1,5 +1,6 @@
-
@@ -8,4 +9,4 @@
Click="OnLinkClicked" />
-
+
diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs
index 5fa4ec8b0e..1154506e2e 100644
--- a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Interactivity;
namespace ControlCatalog.Pages;
-public partial class DataGridPage : UserControl
+public partial class DataGridPage : ContentPage
{
public DataGridPage()
{
diff --git a/samples/ControlCatalog/Pages/DataValidationPage.axaml b/samples/ControlCatalog/Pages/DataValidationPage.axaml
index d46562addd..de4e398725 100644
--- a/samples/ControlCatalog/Pages/DataValidationPage.axaml
+++ b/samples/ControlCatalog/Pages/DataValidationPage.axaml
@@ -1,14 +1,15 @@
-
-
+
-
+
-
+
diff --git a/samples/ControlCatalog/Pages/DataValidationPage.axaml.cs b/samples/ControlCatalog/Pages/DataValidationPage.axaml.cs
index e38f85fec4..26bb75ca79 100644
--- a/samples/ControlCatalog/Pages/DataValidationPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/DataValidationPage.axaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages;
-public partial class DataValidationPage : UserControl
+public partial class DataValidationPage : ContentPage
{
public DataValidationPage()
{
diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
index 7a1bfaa824..2183204c05 100644
--- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
+++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
@@ -1,7 +1,8 @@
-
@@ -207,4 +208,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs
index b5968a6b2a..99250d5fb0 100644
--- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs
@@ -2,7 +2,7 @@
namespace ControlCatalog.Pages
{
- public partial class DateTimePickerPage : UserControl
+ public partial class DateTimePickerPage : ContentPage
{
public DateTimePickerPage()
{
diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml
index e6b3568909..296b632b91 100644
--- a/samples/ControlCatalog/Pages/DialogsPage.xaml
+++ b/samples/ControlCatalog/Pages/DialogsPage.xaml
@@ -1,6 +1,7 @@
-
-
+
diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
index adcf844552..616e3d4f62 100644
--- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
@@ -12,7 +12,7 @@ using Avalonia.Platform.Storage;
namespace ControlCatalog.Pages
{
- public partial class DialogsPage : UserControl
+ public partial class DialogsPage : ContentPage
{
public DialogsPage()
{
diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml
index 7982ddc1d0..2ca9ed27a0 100644
--- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml
+++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml
@@ -1,15 +1,16 @@
-
-
+
-
+
Example of Drag+Drop capabilities
@@ -57,4 +58,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
index fe2b306477..b2a91da381 100644
--- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
@@ -13,7 +13,7 @@ using Avalonia.Platform.Storage;
namespace ControlCatalog.Pages
{
- public partial class DragAndDropPage : UserControl
+ public partial class DragAndDropPage : ContentPage
{
private readonly DataFormat _customFormat =
DataFormat.CreateStringApplicationFormat("xxx-avalonia-controlcatalog-custom");
diff --git a/samples/ControlCatalog/Pages/DrawerDemoPage.xaml b/samples/ControlCatalog/Pages/DrawerDemoPage.xaml
index 671a9a8487..f1edd0bc63 100644
--- a/samples/ControlCatalog/Pages/DrawerDemoPage.xaml
+++ b/samples/ControlCatalog/Pages/DrawerDemoPage.xaml
@@ -1,11 +1,14 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/DrawerDemoPage.xaml.cs b/samples/ControlCatalog/Pages/DrawerDemoPage.xaml.cs
index cd8aa9cd5b..27eb93ff03 100644
--- a/samples/ControlCatalog/Pages/DrawerDemoPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DrawerDemoPage.xaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
- public partial class DrawerDemoPage : UserControl
+ public partial class DrawerDemoPage : ContentPage
{
private static readonly (string Group, string Title, string Description, Func Factory)[] Demos =
{
diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml b/samples/ControlCatalog/Pages/ExpanderPage.xaml
index b5a2e6cdd0..322da30a61 100644
--- a/samples/ControlCatalog/Pages/ExpanderPage.xaml
+++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml
@@ -1,7 +1,8 @@
-
Expands to show nested content
@@ -72,4 +73,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs b/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
index 05c5cbee53..65b7b7e30b 100644
--- a/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
@@ -3,7 +3,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class ExpanderPage : UserControl
+ public partial class ExpanderPage : ContentPage
{
public ExpanderPage()
{
diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml
index 64a005c987..840557bb9f 100644
--- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml
+++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml
@@ -1,11 +1,12 @@
-
-
+
+
@@ -282,4 +283,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs b/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
index 91e92bc227..d6f54df074 100644
--- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
@@ -7,7 +7,7 @@ using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
- public partial class FlyoutsPage : UserControl
+ public partial class FlyoutsPage : ContentPage
{
public FlyoutsPage()
{
diff --git a/samples/ControlCatalog/Pages/FocusPage.xaml b/samples/ControlCatalog/Pages/FocusPage.xaml
index f4bad7d138..6cb2647907 100644
--- a/samples/ControlCatalog/Pages/FocusPage.xaml
+++ b/samples/ControlCatalog/Pages/FocusPage.xaml
@@ -1,9 +1,10 @@
-
@@ -47,4 +48,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/FocusPage.xaml.cs b/samples/ControlCatalog/Pages/FocusPage.xaml.cs
index 2cc8067885..18aae471ab 100644
--- a/samples/ControlCatalog/Pages/FocusPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/FocusPage.xaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages;
-public partial class FocusPage : UserControl
+public partial class FocusPage : ContentPage
{
public FocusPage()
{
diff --git a/samples/ControlCatalog/Pages/GesturePage.xaml b/samples/ControlCatalog/Pages/GesturePage.xaml
index bce18eab1d..da9ed5f807 100644
--- a/samples/ControlCatalog/Pages/GesturePage.xaml
+++ b/samples/ControlCatalog/Pages/GesturePage.xaml
@@ -1,11 +1,14 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/GesturePage.cs b/samples/ControlCatalog/Pages/GesturePage.xaml.cs
similarity index 96%
rename from samples/ControlCatalog/Pages/GesturePage.cs
rename to samples/ControlCatalog/Pages/GesturePage.xaml.cs
index d8b89e0ecb..56c09573d0 100644
--- a/samples/ControlCatalog/Pages/GesturePage.cs
+++ b/samples/ControlCatalog/Pages/GesturePage.xaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
- public partial class GesturePage : UserControl
+ public partial class GesturePage : ContentPage
{
private static readonly (string Group, string Title, string Description, Func Factory)[] Demos =
{
diff --git a/samples/ControlCatalog/Pages/HeaderedContentPage.axaml b/samples/ControlCatalog/Pages/HeaderedContentPage.axaml
index 86a2b00b72..cbf12695fd 100644
--- a/samples/ControlCatalog/Pages/HeaderedContentPage.axaml
+++ b/samples/ControlCatalog/Pages/HeaderedContentPage.axaml
@@ -1,8 +1,9 @@
-
-
+
diff --git a/samples/ControlCatalog/Pages/HeaderedContentPage.axaml.cs b/samples/ControlCatalog/Pages/HeaderedContentPage.axaml.cs
index ab967c4214..bfe033f90c 100644
--- a/samples/ControlCatalog/Pages/HeaderedContentPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/HeaderedContentPage.axaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages;
-public partial class HeaderedContentPage : UserControl
+public partial class HeaderedContentPage : ContentPage
{
public HeaderedContentPage()
{
diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml
index 444d370e42..abd9ff47b0 100644
--- a/samples/ControlCatalog/Pages/ImagePage.xaml
+++ b/samples/ControlCatalog/Pages/ImagePage.xaml
@@ -1,5 +1,6 @@
-
@@ -65,4 +66,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs
index f8630d7db6..eccdb9435f 100644
--- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs
@@ -5,7 +5,7 @@ using Avalonia.Media.Imaging;
namespace ControlCatalog.Pages
{
- public partial class ImagePage : UserControl
+ public partial class ImagePage : ContentPage
{
public ImagePage()
{
diff --git a/samples/ControlCatalog/Pages/LabelsPage.axaml b/samples/ControlCatalog/Pages/LabelsPage.axaml
index 5bfb2ee10e..cd367aa38a 100644
--- a/samples/ControlCatalog/Pages/LabelsPage.axaml
+++ b/samples/ControlCatalog/Pages/LabelsPage.axaml
@@ -1,13 +1,14 @@
-
-
+
-
+
@@ -41,4 +42,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/LabelsPage.axaml.cs b/samples/ControlCatalog/Pages/LabelsPage.axaml.cs
index f3a7647f8c..b26112b351 100644
--- a/samples/ControlCatalog/Pages/LabelsPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/LabelsPage.axaml.cs
@@ -3,7 +3,7 @@ using ControlCatalog.Models;
namespace ControlCatalog.Pages
{
- public partial class LabelsPage : UserControl
+ public partial class LabelsPage : ContentPage
{
private Person? _person;
diff --git a/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml
index 8cf3610b47..d12bbfc302 100644
--- a/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml
+++ b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml
@@ -1,5 +1,6 @@
-
@@ -24,4 +25,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml.cs b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml.cs
index 2060790594..e3815238be 100644
--- a/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class LayoutTransformControlPage : UserControl
+ public partial class LayoutTransformControlPage : ContentPage
{
public LayoutTransformControlPage()
{
diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml
index e3a706bfed..f73a4c18ed 100644
--- a/samples/ControlCatalog/Pages/ListBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml
@@ -1,6 +1,7 @@
-
@@ -47,4 +48,4 @@
SelectionMode="{Binding SelectionMode^}"
WrapSelection="{Binding WrapSelection}"/>
-
+
diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
index 3dac437f3a..208b5cce4b 100644
--- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
@@ -3,7 +3,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class ListBoxPage : UserControl
+ public partial class ListBoxPage : ContentPage
{
public ListBoxPage()
{
diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml
index bbcc759ca7..d9fb5d81bf 100644
--- a/samples/ControlCatalog/Pages/MenuPage.xaml
+++ b/samples/ControlCatalog/Pages/MenuPage.xaml
@@ -1,7 +1,8 @@
-
Exported menu fallback
@@ -95,4 +96,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml.cs b/samples/ControlCatalog/Pages/MenuPage.xaml.cs
index a07306b81d..56a229a594 100644
--- a/samples/ControlCatalog/Pages/MenuPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/MenuPage.xaml.cs
@@ -4,7 +4,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class MenuPage : UserControl
+ public partial class MenuPage : ContentPage
{
public MenuPage()
{
diff --git a/samples/ControlCatalog/Pages/NativeEmbedPage.xaml b/samples/ControlCatalog/Pages/NativeEmbedPage.xaml
index 89e636112c..71d622bae4 100644
--- a/samples/ControlCatalog/Pages/NativeEmbedPage.xaml
+++ b/samples/ControlCatalog/Pages/NativeEmbedPage.xaml
@@ -1,10 +1,11 @@
-
@@ -65,4 +66,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs b/samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs
index b9bac10f9a..1cd1f78cc0 100644
--- a/samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs
@@ -7,7 +7,7 @@ using Avalonia.Platform;
namespace ControlCatalog.Pages
{
- public partial class NativeEmbedPage : UserControl
+ public partial class NativeEmbedPage : ContentPage
{
public NativeEmbedPage()
{
diff --git a/samples/ControlCatalog/Pages/NavigationDemoPage.xaml b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml
index 4849b2d5b8..0f7536b6a3 100644
--- a/samples/ControlCatalog/Pages/NavigationDemoPage.xaml
+++ b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml
@@ -1,11 +1,14 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs
index d65a43a6ad..7ca41ab893 100644
--- a/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/NavigationDemoPage.xaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
- public partial class NavigationDemoPage : UserControl
+ public partial class NavigationDemoPage : ContentPage
{
private static readonly (string Group, string Title, string Description, Func Factory)[] Demos =
{
diff --git a/samples/ControlCatalog/Pages/NotificationsPage.xaml b/samples/ControlCatalog/Pages/NotificationsPage.xaml
index b4094d8a2e..2ae1db7d43 100644
--- a/samples/ControlCatalog/Pages/NotificationsPage.xaml
+++ b/samples/ControlCatalog/Pages/NotificationsPage.xaml
@@ -1,8 +1,9 @@
-
-
+
diff --git a/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs b/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs
index f3db564571..5ff67e10dc 100644
--- a/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs
@@ -5,7 +5,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class NotificationsPage : UserControl
+ public partial class NotificationsPage : ContentPage
{
private NotificationViewModel _viewModel;
diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml
index a112f79c02..dcc84c3ae8 100644
--- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml
+++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml
@@ -1,8 +1,9 @@
-
-
+
diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
index 379090830d..db64fba0ef 100644
--- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
@@ -9,7 +9,7 @@ using MiniMvvm;
namespace ControlCatalog.Pages
{
- public partial class NumericUpDownPage : UserControl
+ public partial class NumericUpDownPage : ContentPage
{
public NumericUpDownPage()
{
diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml b/samples/ControlCatalog/Pages/OpenGlPage.xaml
index 0e557cc1ad..9119be05f5 100644
--- a/samples/ControlCatalog/Pages/OpenGlPage.xaml
+++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml
@@ -1,7 +1,8 @@
-
@@ -10,4 +11,4 @@
IsVisible="False"
VerticalAlignment="Bottom" HorizontalAlignment="Right" Click="SnapshotClick">Snapshot
-
+
diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs
index 09173e01a5..51dc4c2c72 100644
--- a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs
@@ -11,7 +11,7 @@ using ControlCatalog.Pages.OpenGl;
namespace ControlCatalog.Pages
{
- public partial class OpenGlPage : UserControl
+ public partial class OpenGlPage : ContentPage
{
public OpenGlPage()
{
diff --git a/samples/ControlCatalog/Pages/PipsPagerPage.xaml b/samples/ControlCatalog/Pages/PipsPagerPage.xaml
index 54112daae0..aee21a7d01 100644
--- a/samples/ControlCatalog/Pages/PipsPagerPage.xaml
+++ b/samples/ControlCatalog/Pages/PipsPagerPage.xaml
@@ -1,11 +1,14 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs b/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs
index 0559a265e4..5a3b142f59 100644
--- a/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/PipsPagerPage.xaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
- public partial class PipsPagerPage : UserControl
+ public partial class PipsPagerPage : ContentPage
{
private static readonly (string Group, string Title, string Description, Func Factory)[] Demos =
{
diff --git a/samples/ControlCatalog/Pages/PlatformInfoPage.xaml b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml
index 22c47f6bef..f86d53b740 100644
--- a/samples/ControlCatalog/Pages/PlatformInfoPage.xaml
+++ b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml
@@ -1,4 +1,4 @@
-
@@ -53,4 +54,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs
index 5066bc51de..db1bbef40c 100644
--- a/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs
@@ -3,7 +3,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class PlatformInfoPage : UserControl
+ public partial class PlatformInfoPage : ContentPage
{
public PlatformInfoPage()
{
diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml b/samples/ControlCatalog/Pages/PointersPage.xaml
index dda85316f6..2f958b901b 100644
--- a/samples/ControlCatalog/Pages/PointersPage.xaml
+++ b/samples/ControlCatalog/Pages/PointersPage.xaml
@@ -1,12 +1,12 @@
-
-
-
+
-
-
+
+
-
-
+
+
-
-
+
+
Capture 2
-
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/PointersPage.xaml.cs b/samples/ControlCatalog/Pages/PointersPage.xaml.cs
index adf45029ec..388c8fa382 100644
--- a/samples/ControlCatalog/Pages/PointersPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/PointersPage.xaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Input;
namespace ControlCatalog.Pages;
-public partial class PointersPage : UserControl
+public partial class PointersPage : TabbedPage
{
public PointersPage()
{
diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml
index 52e1e198b5..3b80b5a6bb 100644
--- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml
+++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml
@@ -1,4 +1,8 @@
-
+
A progress bar control
@@ -18,10 +22,10 @@
-
-
@@ -36,4 +40,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml.cs b/samples/ControlCatalog/Pages/ProgressBarPage.xaml.cs
index 42b9fe29d7..244161a61e 100644
--- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class ProgressBarPage : UserControl
+ public partial class ProgressBarPage : ContentPage
{
public ProgressBarPage()
{
diff --git a/samples/ControlCatalog/Pages/RadioButtonPage.xaml b/samples/ControlCatalog/Pages/RadioButtonPage.xaml
index 8623d81b3f..efb63ea124 100644
--- a/samples/ControlCatalog/Pages/RadioButtonPage.xaml
+++ b/samples/ControlCatalog/Pages/RadioButtonPage.xaml
@@ -1,5 +1,6 @@
-
Allows the selection of a single option of many
@@ -37,4 +38,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/RadioButtonPage.xaml.cs b/samples/ControlCatalog/Pages/RadioButtonPage.xaml.cs
index 798df9fe77..8199a7caf5 100644
--- a/samples/ControlCatalog/Pages/RadioButtonPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/RadioButtonPage.xaml.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class RadioButtonPage : UserControl
+ public partial class RadioButtonPage : ContentPage
{
public RadioButtonPage()
{
diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml
index f6ea26a84c..06f54dd28a 100644
--- a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml
+++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml
@@ -1,4 +1,4 @@
-
-
+
diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
index a710cd7e5c..2c886fa9de 100644
--- a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
@@ -3,7 +3,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class RefreshContainerPage : UserControl
+ public partial class RefreshContainerPage : ContentPage
{
private RefreshContainerViewModel _viewModel;
diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml b/samples/ControlCatalog/Pages/RelativePanelPage.axaml
index a6647b0d36..ccc39befd4 100644
--- a/samples/ControlCatalog/Pages/RelativePanelPage.axaml
+++ b/samples/ControlCatalog/Pages/RelativePanelPage.axaml
@@ -1,8 +1,9 @@
-
@@ -60,4 +61,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
index aec13a18e3..3c59deb791 100644
--- a/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
@@ -2,7 +2,7 @@
namespace ControlCatalog.Pages
{
- public partial class RelativePanelPage : UserControl
+ public partial class RelativePanelPage : ContentPage
{
public RelativePanelPage()
{
diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs
index 66e90c2b34..9791152923 100644
--- a/samples/ControlCatalog/Pages/ScreenPage.cs
+++ b/samples/ControlCatalog/Pages/ScreenPage.cs
@@ -11,7 +11,7 @@ using Avalonia.Threading;
namespace ControlCatalog.Pages
{
- public class ScreenPage : UserControl
+ public class ScreenPage : ContentPage
{
private double _leftMost;
private double _topMost;
diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml
index f931542a04..0567dbe451 100644
--- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml
+++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml
@@ -1,10 +1,10 @@
-
-
-
+
-
-
+
+
-
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
index 7c684357d3..cfdd5bab54 100644
--- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
@@ -92,7 +92,7 @@ namespace ControlCatalog.Pages
public List AvailableSnapPointsAlignment { get; }
}
- public partial class ScrollViewerPage : UserControl
+ public partial class ScrollViewerPage : TabbedPage
{
public ScrollViewerPage()
{
diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml
index 7821e885ba..445564cb54 100644
--- a/samples/ControlCatalog/Pages/SliderPage.xaml
+++ b/samples/ControlCatalog/Pages/SliderPage.xaml
@@ -1,5 +1,6 @@
-
@@ -80,4 +81,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml.cs b/samples/ControlCatalog/Pages/SliderPage.xaml.cs
index 8345df8716..f65f714300 100644
--- a/samples/ControlCatalog/Pages/SliderPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/SliderPage.xaml.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class SliderPage : UserControl
+ public partial class SliderPage : ContentPage
{
public SliderPage()
{
diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml b/samples/ControlCatalog/Pages/SplitViewPage.xaml
index 201765ad88..144688e358 100644
--- a/samples/ControlCatalog/Pages/SplitViewPage.xaml
+++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml
@@ -1,8 +1,9 @@
-
@@ -109,4 +110,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs b/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs
index b711db3c49..4917a13ccf 100644
--- a/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs
@@ -3,7 +3,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class SplitViewPage : UserControl
+ public partial class SplitViewPage : ContentPage
{
public SplitViewPage()
{
diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml b/samples/ControlCatalog/Pages/TabControlPage.xaml
index a3bacfd92a..e7aa18ea84 100644
--- a/samples/ControlCatalog/Pages/TabControlPage.xaml
+++ b/samples/ControlCatalog/Pages/TabControlPage.xaml
@@ -1,7 +1,8 @@
-
@@ -108,4 +109,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml.cs b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs
index b7f34eda86..366b5216ef 100644
--- a/samples/ControlCatalog/Pages/TabControlPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs
@@ -6,7 +6,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class TabControlPage : UserControl
+ public partial class TabControlPage : ContentPage
{
public TabControlPage()
{
diff --git a/samples/ControlCatalog/Pages/TabStripPage.xaml b/samples/ControlCatalog/Pages/TabStripPage.xaml
index b576beeb5f..f4a47af353 100644
--- a/samples/ControlCatalog/Pages/TabStripPage.xaml
+++ b/samples/ControlCatalog/Pages/TabStripPage.xaml
@@ -1,6 +1,7 @@
-
@@ -31,4 +32,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/TabStripPage.xaml.cs b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs
index d2873f6586..d830109833 100644
--- a/samples/ControlCatalog/Pages/TabStripPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs
@@ -3,7 +3,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class TabStripPage : UserControl
+ public partial class TabStripPage : ContentPage
{
public TabStripPage()
{
diff --git a/samples/ControlCatalog/Pages/TabbedDemoPage.xaml b/samples/ControlCatalog/Pages/TabbedDemoPage.xaml
index bb1467060b..58a3b4fe82 100644
--- a/samples/ControlCatalog/Pages/TabbedDemoPage.xaml
+++ b/samples/ControlCatalog/Pages/TabbedDemoPage.xaml
@@ -1,11 +1,14 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/TabbedDemoPage.xaml.cs b/samples/ControlCatalog/Pages/TabbedDemoPage.xaml.cs
index 51da6330a1..88692c97f3 100644
--- a/samples/ControlCatalog/Pages/TabbedDemoPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/TabbedDemoPage.xaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
- public partial class TabbedDemoPage : UserControl
+ public partial class TabbedDemoPage : ContentPage
{
private static readonly (string Group, string Title, string Description, Func Factory)[] Demos =
{
diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml
index 7c0300d318..3418cffe74 100644
--- a/samples/ControlCatalog/Pages/TextBlockPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml
@@ -1,5 +1,6 @@
-
A control that can display text
@@ -137,4 +138,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml.cs b/samples/ControlCatalog/Pages/TextBlockPage.xaml.cs
index d097d1d083..d10d255a4f 100644
--- a/samples/ControlCatalog/Pages/TextBlockPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class TextBlockPage : UserControl
+ public partial class TextBlockPage : ContentPage
{
public TextBlockPage()
{
diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml
index 346e561dfc..9b5a412f86 100644
--- a/samples/ControlCatalog/Pages/TextBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml
@@ -1,6 +1,7 @@
-
@@ -101,4 +102,4 @@
FontFamily="avares://ControlCatalog/Assets/Fonts#WenQuanYi Micro Hei"
Text="计算机科学(是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。它通常被形容为对那些创造、描述以及转换信息的算法处理的系统研究。计算机科学包含很多分支领域;有些强调特定结果的计算,比如计算机图形学;而有些是探討计算问题的性质,比如计算复杂性理论;还有一些领域專注于怎样实现计算,比如程式語言理論是研究描述计算的方法,而程式设计是应用特定的程式語言解决特定的计算问题,人机交互则是專注于怎样使计算机和计算变得有用、好用,以及随时随地为人所用。
有时公众会误以为计算机科学就是解决计算机问题的事业(比如信息技术),或者只是与使用计算机的经验有关,如玩游戏、上网或者文字处理。其实计算机科学所关注的,不仅仅是去理解实现类似游戏、浏览器这些软件的程序的性质,更要通过现有的知识创造新的程序或者改进已有的程序。" />
-
+
diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml.cs b/samples/ControlCatalog/Pages/TextBoxPage.xaml.cs
index 035f951a6b..3c38a05c53 100644
--- a/samples/ControlCatalog/Pages/TextBoxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class TextBoxPage : UserControl
+ public partial class TextBoxPage : ContentPage
{
public TextBoxPage()
{
diff --git a/samples/ControlCatalog/Pages/ThemePage.axaml b/samples/ControlCatalog/Pages/ThemePage.axaml
index 7eb95471a0..ccc5769fe7 100644
--- a/samples/ControlCatalog/Pages/ThemePage.axaml
+++ b/samples/ControlCatalog/Pages/ThemePage.axaml
@@ -1,13 +1,14 @@
-
-
+
@@ -52,7 +53,7 @@
-
+
-
+
diff --git a/samples/ControlCatalog/Pages/ThemePage.axaml.cs b/samples/ControlCatalog/Pages/ThemePage.axaml.cs
index d355b399a4..4f45e93ed0 100644
--- a/samples/ControlCatalog/Pages/ThemePage.axaml.cs
+++ b/samples/ControlCatalog/Pages/ThemePage.axaml.cs
@@ -4,7 +4,7 @@ using Avalonia.Styling;
namespace ControlCatalog.Pages
{
- public partial class ThemePage : UserControl
+ public partial class ThemePage : ContentPage
{
public static ThemeVariant Pink { get; } = new("Pink", ThemeVariant.Light);
diff --git a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml
index 6afe6dd135..126cc7e7d2 100644
--- a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml
+++ b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml
@@ -1,5 +1,6 @@
-
@@ -17,7 +18,7 @@
+ Text="<ToggleSwitch>headered</ToggleSwitch>"/>
@@ -26,14 +27,14 @@
+ OnContent="On"
+ OffContent="Off"
+ Margin="10"/>
-
+
@@ -55,7 +56,7 @@ ContentOff="Off" />"
-
+
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml.cs b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml.cs
index 77d77daeed..a60eb295b0 100644
--- a/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ToggleSwitchPage.xaml.cs
@@ -2,7 +2,7 @@
namespace ControlCatalog.Pages
{
- public partial class ToggleSwitchPage : UserControl
+ public partial class ToggleSwitchPage : ContentPage
{
public ToggleSwitchPage()
{
diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml
index 8b76cccbc9..cc3a50b8e9 100644
--- a/samples/ControlCatalog/Pages/ToolTipPage.xaml
+++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml
@@ -1,5 +1,6 @@
-
@@ -10,7 +11,7 @@
HorizontalAlignment="Center">
-
+
diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml.cs b/samples/ControlCatalog/Pages/ToolTipPage.xaml.cs
index cd0b5c72a8..e436407dc5 100644
--- a/samples/ControlCatalog/Pages/ToolTipPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml.cs
@@ -6,7 +6,7 @@ using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
- public partial class ToolTipPage : UserControl
+ public partial class ToolTipPage : ContentPage
{
public ToolTipPage()
{
diff --git a/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
index 03ef86fb61..5435ed278d 100644
--- a/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
+++ b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml
@@ -1,19 +1,20 @@
-
-
+
-
+
-
+
-
+
-
+
8
-
+
@@ -81,4 +82,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs
index 863ddae6e0..818ab6219d 100644
--- a/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs
+++ b/samples/ControlCatalog/Pages/TransitioningContentControlPage.axaml.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class TransitioningContentControlPage : UserControl
+ public partial class TransitioningContentControlPage : ContentPage
{
public TransitioningContentControlPage()
{
diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml
index 2e3fca58cb..88dc65b5f2 100644
--- a/samples/ControlCatalog/Pages/TreeViewPage.xaml
+++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml
@@ -1,6 +1,7 @@
-
@@ -32,4 +33,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
index c43c2eb49e..d03590febd 100644
--- a/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
@@ -3,7 +3,7 @@ using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
- public partial class TreeViewPage : UserControl
+ public partial class TreeViewPage : ContentPage
{
public TreeViewPage()
{
diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml b/samples/ControlCatalog/Pages/ViewboxPage.xaml
index e66655253c..3848e405db 100644
--- a/samples/ControlCatalog/Pages/ViewboxPage.xaml
+++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml
@@ -1,6 +1,7 @@
-
@@ -50,4 +51,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs b/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
index c140cb9857..f7b1ea3f75 100644
--- a/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
namespace ControlCatalog.Pages
{
- public partial class ViewboxPage : UserControl
+ public partial class ViewboxPage : ContentPage
{
public ViewboxPage()
{
diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
index df84c0bd6b..0017ee09b5 100644
--- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
+++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
@@ -1,9 +1,10 @@
-
@@ -50,4 +51,4 @@
-
+
diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs
index d62c7ec10f..2ee8677eba 100644
--- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs
@@ -2,7 +2,7 @@
namespace ControlCatalog.Pages
{
- public partial class WindowCustomizationsPage : UserControl
+ public partial class WindowCustomizationsPage : ContentPage
{
public WindowCustomizationsPage()
{
diff --git a/samples/ControlCatalog/ScrollPage.xaml b/samples/ControlCatalog/ScrollPage.xaml
new file mode 100644
index 0000000000..6d5ecb815e
--- /dev/null
+++ b/samples/ControlCatalog/ScrollPage.xaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
index ce9209f61a..0d877b253f 100644
--- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
@@ -8,7 +8,7 @@ using MiniMvvm;
namespace ControlCatalog.ViewModels
{
- class MainWindowViewModel : ViewModelBase
+ partial class MainWindowViewModel : ViewModelBase
{
private WindowState _windowState;
private WindowState[] _windowStates = Array.Empty();
@@ -20,6 +20,7 @@ namespace ControlCatalog.ViewModels
private bool _canResize;
private bool _canMinimize;
private bool _canMaximize;
+ private int _selectedDecorationIndex;
public MainWindowViewModel()
{
@@ -51,6 +52,8 @@ namespace ControlCatalog.ViewModels
CanResize = true;
CanMinimize = true;
CanMaximize = true;
+
+ Filter();
}
public bool ExtendClientAreaEnabled
@@ -113,6 +116,12 @@ namespace ControlCatalog.ViewModels
set { RaiseAndSetIfChanged(ref _canMaximize, value); }
}
+ public int SelectedDecorationIndex
+ {
+ get { return _selectedDecorationIndex; }
+ set { RaiseAndSetIfChanged(ref _selectedDecorationIndex, value); }
+ }
+
public MiniCommand AboutCommand { get; }
diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel_PageList.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel_PageList.cs
new file mode 100644
index 0000000000..4c85f64765
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel_PageList.cs
@@ -0,0 +1,196 @@
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Dialogs;
+using System;
+using System.ComponentModel.DataAnnotations;
+using Avalonia;
+using MiniMvvm;
+using Avalonia.Collections;
+using ControlCatalog.Pages;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace ControlCatalog.ViewModels
+{
+ partial class MainWindowViewModel
+ {
+ private int _selectedPageIndex;
+ private bool _isDrawerOpened = true;
+ private bool _ignoreListChange = false;
+ private string? _query = "";
+ private PageItem? _currentItem;
+ private SplitViewDisplayMode _displayMode;
+
+ private List _items = new()
+ {
+ new PageItem("Composition", () => new CompositionPage(), Icons.Layers),
+ new PageItem("Accelerator", () => new AcceleratorPage(), Icons.Keyboard),
+ new PageItem("Acrylic", () => new AcrylicPage(), Icons.Blur),
+ new PageItem("AdornerLayer", () => new AdornerLayerPage(), Icons.Sparkle),
+ new PageItem("AutoCompleteBox",() => new AutoCompleteBoxPage(), Icons.TextInput),
+ new PageItem("Border",() => new BorderPage(), Icons.Border),
+ new PageItem("BitmapCache",() => new BitmapCachePage(), Icons.Lightning),
+ new PageItem("Buttons",() => new ButtonsPage(), Icons.CursorClick),
+ new PageItem("ButtonSpinner",() => new ButtonSpinnerPage(), Icons.Spinner),
+ new PageItem("Calendar",() => new CalendarPage(), Icons.Calendar),
+ new PageItem("Canvas",() => new CanvasPage(), Icons.Canvas),
+ new PageItem("CommandBar",() => new CommandBarPage(), Icons.Terminal),
+ new PageItem("Carousel",() => new Pages.CarouselPage(), Icons.Slides),
+ new PageItem("CarouselPage",() => new CarouselDemoPage(), Icons.Slides),
+ new PageItem("CheckBox",() => new CheckBoxPage(), Icons.Checkbox),
+ new PageItem("Clipboard",() => new ClipboardPage(), Icons.Clipboard),
+ new PageItem("ColorPicker",() => new ColorPickerPage(), Icons.Palette),
+ new PageItem("ComboBox",() => new ComboBoxPage(), Icons.Dropdown),
+ new PageItem("Container Queries",() => new ContainerQueryPage(), Icons.Container),
+ new PageItem("ContentPage",() => new ContentDemoPage(), Icons.Document),
+ new PageItem("ContextFlyout",() => new ContextFlyoutPage(), Icons.Menu),
+ new PageItem("ContextMenu",() => new ContextMenuPage(), Icons.Menu),
+ new PageItem("Cursor",() => new CursorPage(), Icons.Cursor),
+ new PageItem("Custom Drawing",() => new CustomDrawing(), Icons.Brush),
+ new PageItem("DataGrid",() => new DataGridPage(), Icons.Grid),
+ new PageItem("Data Validation",() => new DataValidationPage(), Icons.Shield),
+ new PageItem("Date/Time Picker",() => new DateTimePickerPage(), Icons.Clock),
+ new PageItem("CalendarDatePicker",() => new CalendarDatePickerPage(), Icons.Calendar),
+ new PageItem("Dialogs",() => new DialogsPage(), Icons.Dialog),
+ new PageItem("Drag+Drop",() => new DragAndDropPage(), Icons.DragDrop),
+ new PageItem("DrawerPage",() => new DrawerDemoPage(), Icons.Drawer),
+ new PageItem("Expander",() => new ExpanderPage(), Icons.Expand),
+ new PageItem("Flyouts",() => new FlyoutsPage(), Icons.Flyout),
+ new PageItem("Focus",() => new FocusPage(), Icons.Target),
+ new PageItem("Gestures",() => new GesturePage(), Icons.Gesture),
+ new PageItem("Image",() => new ImagePage(), Icons.Image),
+ new PageItem("Label",() => new LabelsPage(), Icons.Tag),
+ new PageItem("LayoutTransformControl",() => new LayoutTransformControlPage(), Icons.Transform),
+ new PageItem("ListBox",() => new ListBoxPage(), Icons.List),
+ new PageItem("Menu",() => new MenuPage(), Icons.Menu),
+ new PageItem("NavigationPage",() => new NavigationDemoPage(), Icons.Navigation),
+ new PageItem("Notifications",() => new NotificationsPage(), Icons.Bell),
+ new PageItem("NumericUpDown",() => new NumericUpDownPage(), Icons.Number),
+ new PageItem("OpenGL",() => new OpenGlPage(), Icons.Cube3D),
+ new PageItem("OpenGL Lease",() => new OpenGlLeasePage(), Icons.Cube3D),
+ new PageItem("PipsPager",() => new PipsPagerPage(), Icons.HorizontalDots),
+ new PageItem("Platform Information",() => new PlatformInfoPage(), Icons.Info),
+ new PageItem("Pointers",() => new PointersPage(), Icons.Cursor),
+ new PageItem("ProgressBar",() => new ProgressBarPage(), Icons.Progress),
+ new PageItem("RadioButton",() => new RadioButtonPage(), Icons.Radio),
+ new PageItem("RefreshContainer",() => new RefreshContainerPage(), Icons.Refresh),
+ new PageItem("RelativePanel",() => new RelativePanelPage(), Icons.Layout),
+ new PageItem("ScrollViewer",() => new ScrollViewerPage(), Icons.Scroll),
+ new PageItem("Slider",() => new SliderPage(), Icons.Tune),
+ new PageItem("SplitView",() => new SplitViewPage(), Icons.Split),
+ new PageItem("TabbedPage",() => new TabbedDemoPage(), Icons.Tab),
+ new PageItem("TabControl",() => new TabControlPage(), Icons.Tab),
+ new PageItem("TabStrip",() => new TabStripPage(), Icons.Tab),
+ new PageItem("TextBox",() => new TextBoxPage(), Icons.TextInput),
+ new PageItem("TextBlock",() => new TextBlockPage(), Icons.TextInput),
+ new PageItem("Theme Variants",() => new ThemePage(), Icons.Theme),
+ new PageItem("ToggleSwitch",() => new ToggleSwitchPage(), Icons.Toggle),
+ new PageItem("ToolTip",() => new ToolTipPage(), Icons.Tooltip),
+ new PageItem("TransitioningContentControl",() => new TransitioningContentControlPage(), Icons.Transition),
+ new PageItem("TreeView",() => new TreeViewPage(), Icons.Tree),
+ new PageItem("Viewbox",() => new ViewboxPage(), Icons.Viewbox),
+ new PageItem("Native Embed",() => new NativeEmbedPage(), Icons.Puzzle),
+ new PageItem("Window Customizations",() => new WindowCustomizationsPage(), Icons.Window),
+ new PageItem("HeaderedContentControl",() => new HeaderedContentPage(), Icons.Header),
+ new PageItem("Screens",() => new ScreenPage(), Icons.Monitor),
+ };
+
+ public AvaloniaList Pages { get; } = new AvaloniaList();
+
+ public void Filter(string? query = "")
+ {
+ try
+ {
+ _ignoreListChange = true;
+ Pages.Clear();
+
+ if (!string.IsNullOrWhiteSpace(query))
+ Pages.AddRange(_items.Where(x => x.Header.Contains(query)));
+ else
+ Pages.AddRange(_items);
+ }
+ finally
+ {
+ _ignoreListChange = false;
+ if (_currentItem != null)
+ {
+ var newIndex = Pages.IndexOf(_currentItem);
+ if (newIndex != -1)
+ {
+ SelectedPageIndex = newIndex;
+ }
+ }
+ }
+ }
+
+ public INavigation? Navigator { get; internal set; }
+
+ public int SelectedPageIndex
+ {
+ get { return _selectedPageIndex; }
+ set
+ {
+ RaiseAndSetIfChanged(ref _selectedPageIndex, value);
+
+ if (!_ignoreListChange)
+ {
+ NavigateTo(_selectedPageIndex);
+
+ if (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay)
+ IsDrawerOpened = false;
+ }
+ }
+ }
+
+ public bool IsDrawerOpened
+ {
+ get { return _isDrawerOpened; }
+ set { RaiseAndSetIfChanged(ref _isDrawerOpened, value); }
+ }
+
+ public SplitViewDisplayMode DisplayMode
+ {
+ get { return _displayMode; }
+ set { RaiseAndSetIfChanged(ref _displayMode, value); }
+ }
+
+ public string? Query
+ {
+ get { return _query; }
+ set
+ {
+ RaiseAndSetIfChanged(ref _query, value);
+
+ Filter(value);
+ }
+ }
+
+ private async void NavigateTo(int pageIndex)
+ {
+ if (pageIndex < 0 || pageIndex >= Pages.Count || Navigator is null)
+ return;
+
+ var item = Pages[pageIndex];
+
+ if (item != null)
+ {
+ var view = item.Factory();
+ if (view is Page page && view.GetType() != Navigator.NavigationStack.LastOrDefault()?.GetType())
+ {
+ _currentItem = item;
+ await Navigator.ReplaceAsync(page);
+ }
+ }
+ }
+ }
+
+ class PageItem(string header, Func factory, string? iconData = null)
+ {
+ public string Header { get; } = header;
+ public Func Factory { get; } = factory;
+ public string? IconData { get; } = iconData;
+
+ public bool IsVisible { get; set; } = true;
+ }
+}
diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
index d9b3e3c73f..b4d9900617 100644
--- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
+++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
@@ -33,8 +33,8 @@
#FFFFFFFF
#FFF2F2F2
-
-
+
+
40
220
36
@@ -134,14 +134,14 @@
-
+