diff --git a/packages/Avalonia/AvaloniaBuildTasks.props b/packages/Avalonia/AvaloniaBuildTasks.props
index deea3aa391..50dc7b615f 100644
--- a/packages/Avalonia/AvaloniaBuildTasks.props
+++ b/packages/Avalonia/AvaloniaBuildTasks.props
@@ -5,7 +5,7 @@
-
-
+
+
diff --git a/samples/SafeAreaDemo/ViewModels/MainViewModel.cs b/samples/SafeAreaDemo/ViewModels/MainViewModel.cs
index 3d826d8a9c..c52536d157 100644
--- a/samples/SafeAreaDemo/ViewModels/MainViewModel.cs
+++ b/samples/SafeAreaDemo/ViewModels/MainViewModel.cs
@@ -1,4 +1,6 @@
-using Avalonia;
+using System;
+using Avalonia;
+using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using MiniMvvm;
@@ -12,7 +14,26 @@ namespace SafeAreaDemo.ViewModels
private IInsetsManager? _insetsManager;
private bool _hideSystemBars;
private bool _autoSafeAreaPadding;
+ private IInputPane? _inputPane;
+ public InputPaneState InputPaneState
+ {
+ get
+ {
+ return _inputPane?.State ?? InputPaneState.Closed;
+ }
+ }
+
+ public IEasing? InputPaneEasing { get; private set; }
+ public TimeSpan? InputPaneDuration { get; private set; }
+
+ public Thickness InputPaneMarkerMargin => InputPaneState == InputPaneState.Open
+ ? new Thickness(0, 0, 0, Math.Max(0, CanvasSize.Height - InputPaneRect.Top))
+ : default;
+ public Rect InputPaneRect => _inputPane?.OccludedRect ?? default;
+
+ public Rect CanvasSize { get; set; }
+
public Thickness SafeAreaPadding
{
get
@@ -90,12 +111,16 @@ namespace SafeAreaDemo.ViewModels
}
}
- internal void Initialize(Control mainView, IInsetsManager? InsetsManager)
+ internal void Initialize(Control mainView, IInsetsManager? InsetsManager, IInputPane? inputPane)
{
if (_insetsManager != null)
{
_insetsManager.SafeAreaChanged -= InsetsManager_SafeAreaChanged;
}
+ if (_inputPane != null)
+ {
+ _inputPane.StateChanged -= InputPaneOnStateChanged;
+ }
_autoSafeAreaPadding = mainView.GetValue(TopLevel.AutoSafeAreaPaddingProperty);
_insetsManager = InsetsManager;
@@ -107,6 +132,20 @@ namespace SafeAreaDemo.ViewModels
_displayEdgeToEdge = _insetsManager.DisplayEdgeToEdge;
_hideSystemBars = !(_insetsManager.IsSystemBarVisible ?? false);
}
+
+ _inputPane = inputPane;
+ if (_inputPane != null)
+ {
+ _inputPane.StateChanged += InputPaneOnStateChanged;
+ }
+ RaiseKeyboardChanged();
+ }
+
+ private void InputPaneOnStateChanged(object? sender, InputPaneStateEventArgs e)
+ {
+ InputPaneDuration = e.AnimationDuration;
+ InputPaneEasing = e.Easing ?? new LinearEasing();
+ RaiseKeyboardChanged();
}
private void InsetsManager_SafeAreaChanged(object? sender, SafeAreaChangedArgs e)
@@ -118,6 +157,16 @@ namespace SafeAreaDemo.ViewModels
{
this.RaisePropertyChanged(nameof(SafeAreaPadding));
this.RaisePropertyChanged(nameof(ViewPadding));
+ this.RaisePropertyChanged(nameof(InputPaneMarkerMargin));
+ }
+
+ private void RaiseKeyboardChanged()
+ {
+ this.RaisePropertyChanged(nameof(InputPaneState));
+ this.RaisePropertyChanged(nameof(InputPaneRect));
+ this.RaisePropertyChanged(nameof(InputPaneEasing));
+ this.RaisePropertyChanged(nameof(InputPaneDuration));
+ this.RaisePropertyChanged(nameof(InputPaneMarkerMargin));
}
}
}
diff --git a/samples/SafeAreaDemo/Views/MainView.xaml b/samples/SafeAreaDemo/Views/MainView.xaml
index 966b0a02ea..85163e7dad 100644
--- a/samples/SafeAreaDemo/Views/MainView.xaml
+++ b/samples/SafeAreaDemo/Views/MainView.xaml
@@ -11,10 +11,11 @@
Background="#ccc"
TopLevel.AutoSafeAreaPadding="{Binding AutoSafeAreaPadding, Mode=TwoWay}">
-
+ VerticalAlignment="Stretch"
+ Bounds="{Binding CanvasSize, Mode=OneWayToSource}">
+
+
diff --git a/samples/SafeAreaDemo/Views/MainView.xaml.cs b/samples/SafeAreaDemo/Views/MainView.xaml.cs
index bacb721d27..e4cd53c1a4 100644
--- a/samples/SafeAreaDemo/Views/MainView.xaml.cs
+++ b/samples/SafeAreaDemo/Views/MainView.xaml.cs
@@ -18,8 +18,9 @@ namespace SafeAreaDemo.Views
base.OnLoaded(e);
var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager;
+ var inputPane = TopLevel.GetTopLevel(this)?.InputPane;
var viewModel = new MainViewModel();
- viewModel.Initialize(this, insetsManager);
+ viewModel.Initialize(this, insetsManager, inputPane);
DataContext = viewModel;
}
}
diff --git a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
index 6d8ae873a2..c38124e6da 100644
--- a/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
+++ b/src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
@@ -2,53 +2,68 @@
using System.Collections.Generic;
using Android.OS;
using Android.Views;
+using Android.Views.Animations;
using AndroidX.Core.View;
using Avalonia.Android.Platform.SkiaPlatform;
+using Avalonia.Animation.Easings;
using Avalonia.Controls.Platform;
using Avalonia.Media;
+using AndroidWindow = Android.Views.Window;
namespace Avalonia.Android.Platform
{
- internal class AndroidInsetsManager : Java.Lang.Object, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener
+ internal sealed class AndroidInsetsManager : WindowInsetsAnimationCompat.Callback, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener, IInputPane
{
private readonly AvaloniaMainActivity _activity;
private readonly TopLevelImpl _topLevel;
- private readonly InsetsAnimationCallback _callback;
private bool _displayEdgeToEdge;
- private bool _usesLegacyLayouts;
private bool? _systemUiVisibility;
private SystemBarTheme? _statusBarTheme;
private bool? _isDefaultSystemBarLightTheme;
private Color? _systemBarColor;
+ private InputPaneState _state;
+ private Rect _previousRect;
+ private readonly bool _usesLegacyLayouts;
+ private AndroidWindow Window => _activity.Window ?? throw new InvalidOperationException("Activity.Window must be set.");
+
public event EventHandler SafeAreaChanged;
+ public event EventHandler StateChanged;
+
+ public InputPaneState State
+ {
+ get => _state; set
+ {
+ var oldState = _state;
+ _state = value;
+
+ if (oldState != value && Build.VERSION.SdkInt <= BuildVersionCodes.Q)
+ {
+ var currentRect = OccludedRect;
+ StateChanged?.Invoke(this, new InputPaneStateEventArgs(value, _previousRect, currentRect, TimeSpan.Zero, null));
+ _previousRect = currentRect;
+ }
+ }
+ }
public bool DisplayEdgeToEdge
{
- get => _displayEdgeToEdge;
+ get => _displayEdgeToEdge;
set
{
_displayEdgeToEdge = value;
- var window = _activity.Window;
-
- if (OperatingSystem.IsAndroidVersionAtLeast(28) && window?.Attributes is { } attributes)
+ if (OperatingSystem.IsAndroidVersionAtLeast(28) && Window.Attributes is { } attributes)
{
attributes.LayoutInDisplayCutoutMode = value ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default;
}
- if (window is not null)
- {
- WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value);
- }
+ WindowCompat.SetDecorFitsSystemWindows(Window, !value);
- if(value)
+ if (value)
{
- if (window is not null)
- {
- window.AddFlags(WindowManagerFlags.TranslucentStatus);
- window.AddFlags(WindowManagerFlags.TranslucentNavigation);
- }
+ Window.AddFlags(WindowManagerFlags.TranslucentStatus);
+ Window.AddFlags(WindowManagerFlags.TranslucentNavigation);
}
else
{
@@ -57,20 +72,12 @@ namespace Avalonia.Android.Platform
}
}
- public AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel)
+ internal AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel) : base(DispatchModeStop)
{
_activity = activity;
_topLevel = topLevel;
- _callback = new InsetsAnimationCallback(WindowInsetsAnimationCompat.Callback.DispatchModeStop);
-
- _callback.InsetsManager = this;
-
- if (_activity.Window is { } window)
- {
- ViewCompat.SetOnApplyWindowInsetsListener(window.DecorView, this);
- ViewCompat.SetWindowInsetsAnimationCallback(window.DecorView, _callback);
- }
+ ViewCompat.SetOnApplyWindowInsetsListener(Window.DecorView, this);
if (Build.VERSION.SdkInt < BuildVersionCodes.R)
{
@@ -79,32 +86,48 @@ namespace Avalonia.Android.Platform
}
DisplayEdgeToEdge = false;
+
+ ViewCompat.SetWindowInsetsAnimationCallback(Window.DecorView, this);
}
public Thickness SafeAreaPadding
{
get
{
- var insets = _activity.Window is { } window ? ViewCompat.GetRootWindowInsets(window.DecorView) : null;
+ var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
if (insets != null)
{
var renderScaling = _topLevel.RenderScaling;
var inset = insets.GetInsets(
- (_displayEdgeToEdge ?
+ _displayEdgeToEdge ?
WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() |
- WindowInsetsCompat.Type.DisplayCutout() :
- 0) | WindowInsetsCompat.Type.Ime());
- var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars());
- var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime());
+ WindowInsetsCompat.Type.DisplayCutout() : 0);
return new Thickness(inset.Left / renderScaling,
inset.Top / renderScaling,
inset.Right / renderScaling,
- (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !_displayEdgeToEdge) || !_usesLegacyLayouts) ?
- imeInset.Bottom - (_displayEdgeToEdge ? 0 : navBarInset.Bottom) :
- inset.Bottom) / renderScaling);
+ inset.Bottom / renderScaling);
+ }
+
+ return default;
+ }
+ }
+
+ public Rect OccludedRect
+ {
+ get
+ {
+ var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
+
+ if (insets != null)
+ {
+ var navbarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()).Bottom;
+
+ var height = Math.Max((float)((insets.GetInsets(WindowInsetsCompat.Type.Ime()).Bottom - navbarInset) / _topLevel.RenderScaling), 0);
+
+ return new Rect(0, _topLevel.ClientSize.Height - SafeAreaPadding.Bottom - height, _topLevel.ClientSize.Width, height);
}
return default;
@@ -113,8 +136,16 @@ namespace Avalonia.Android.Platform
public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets)
{
- NotifySafeAreaChanged(SafeAreaPadding);
insets = ViewCompat.OnApplyWindowInsets(v, insets);
+ NotifySafeAreaChanged(SafeAreaPadding);
+
+ if (_previousRect == default)
+ {
+ _previousRect = OccludedRect;
+ }
+
+ State = insets.IsVisible(WindowInsetsCompat.Type.Ime()) ? InputPaneState.Open : InputPaneState.Closed;
+
return insets;
}
@@ -126,6 +157,12 @@ namespace Avalonia.Android.Platform
public void OnGlobalLayout()
{
NotifySafeAreaChanged(SafeAreaPadding);
+
+ if (_usesLegacyLayouts)
+ {
+ var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
+ State = insets?.IsVisible(WindowInsetsCompat.Type.Ime()) == true ? InputPaneState.Open : InputPaneState.Closed;
+ }
}
public SystemBarTheme? SystemBarTheme
@@ -134,7 +171,7 @@ namespace Avalonia.Android.Platform
{
try
{
- var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View);
+ var compat = new WindowInsetsControllerCompat(Window, _topLevel.View);
return compat.AppearanceLightStatusBars ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
}
@@ -152,7 +189,7 @@ namespace Avalonia.Android.Platform
return;
}
- var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View);
+ var compat = new WindowInsetsControllerCompat(Window, _topLevel.View);
if (_isDefaultSystemBarLightTheme == null)
{
@@ -161,7 +198,7 @@ namespace Avalonia.Android.Platform
if (value == null)
{
- value = (bool)_isDefaultSystemBarLightTheme ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
+ value = _isDefaultSystemBarLightTheme.Value ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
}
compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light;
@@ -173,7 +210,7 @@ namespace Avalonia.Android.Platform
{
get
{
- if(_activity.Window == null)
+ if (_activity.Window == null)
{
return true;
}
@@ -190,7 +227,7 @@ namespace Avalonia.Android.Platform
return;
}
- var compat = WindowCompat.GetInsetsController(_activity.Window, _topLevel.View);
+ var compat = WindowCompat.GetInsetsController(Window, _topLevel.View);
if (value == null || value.Value)
{
@@ -210,7 +247,7 @@ namespace Avalonia.Android.Platform
public Color? SystemBarColor
{
- get => _systemBarColor;
+ get => _systemBarColor;
set
{
_systemBarColor = value;
@@ -240,40 +277,48 @@ namespace Avalonia.Android.Platform
SystemBarColor = _systemBarColor;
}
- private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback
+ public override WindowInsetsAnimationCompat.BoundsCompat OnStart(WindowInsetsAnimationCompat animation, WindowInsetsAnimationCompat.BoundsCompat bounds)
{
- public InsetsAnimationCallback(int dispatchMode) : base(dispatchMode)
+ if ((animation.TypeMask & WindowInsetsCompat.Type.Ime()) != 0)
{
- }
-
- public AndroidInsetsManager InsetsManager { get; set; }
+ var insets = ViewCompat.GetRootWindowInsets(Window.DecorView);
- public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList runningAnimations)
- {
- foreach (var anim in runningAnimations)
+ if (insets != null)
{
- if ((anim.TypeMask & WindowInsetsCompat.Type.Ime()) != 0)
- {
- var renderScaling = InsetsManager._topLevel.RenderScaling;
-
- var inset = insets.GetInsets((InsetsManager.DisplayEdgeToEdge ? WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime());
- var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars());
- var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime());
+ var navbarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars()).Bottom;
+ var height = Math.Max(0, (float)((bounds.LowerBound.Bottom - navbarInset) / _topLevel.RenderScaling));
+ var upperRect = new Rect(0, _topLevel.ClientSize.Height - SafeAreaPadding.Bottom - height, _topLevel.ClientSize.Width, height);
+ height = Math.Max(0, (float)((bounds.UpperBound.Bottom - navbarInset) / _topLevel.RenderScaling));
+ var lowerRect = new Rect(0, _topLevel.ClientSize.Height - SafeAreaPadding.Bottom - height, _topLevel.ClientSize.Width, height);
+ var duration = TimeSpan.FromMilliseconds(animation.DurationMillis);
- var bottomPadding = (imeInset.Bottom > 0 && !InsetsManager.DisplayEdgeToEdge ? imeInset.Bottom - navBarInset.Bottom : inset.Bottom);
- bottomPadding = (int)(bottomPadding * anim.InterpolatedFraction);
-
- var padding = new Thickness(inset.Left / renderScaling,
- inset.Top / renderScaling,
- inset.Right / renderScaling,
- bottomPadding / renderScaling);
- InsetsManager?.NotifySafeAreaChanged(padding);
- break;
- }
+ bool isOpening = State == InputPaneState.Open;
+ StateChanged?.Invoke(this, new InputPaneStateEventArgs(State, isOpening ? upperRect : lowerRect, isOpening ? lowerRect : upperRect, duration, new AnimationEasing(animation.Interpolator)));
}
- return insets;
}
+
+ return base.OnStart(animation, bounds);
+ }
+
+ public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList runningAnimations)
+ {
+ return insets;
+ }
+ }
+
+ internal sealed class AnimationEasing : Easing
+ {
+ private readonly IInterpolator _interpolator;
+
+ public AnimationEasing(IInterpolator interpolator)
+ {
+ _interpolator = interpolator;
+ }
+
+ public override double Ease(double progress)
+ {
+ return _interpolator.GetInterpolation((float)progress);
}
}
}
diff --git a/src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs b/src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs
index 880918bebe..ec619fd0f3 100644
--- a/src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs
+++ b/src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs
@@ -6,8 +6,10 @@ using Avalonia.Platform;
namespace Avalonia.Android.Platform
{
- internal class AndroidSystemNavigationManagerImpl : ISystemNavigationManagerImpl
+ internal class AndroidSystemNavigationManagerImpl : ISystemNavigationManagerImpl, IDisposable
{
+ private readonly IActivityNavigationService? _navigationService;
+
public event EventHandler? BackRequested;
public AndroidSystemNavigationManagerImpl(IActivityNavigationService? navigationService)
@@ -16,6 +18,7 @@ namespace Avalonia.Android.Platform
{
navigationService.BackRequested += OnBackRequested;
}
+ _navigationService = navigationService;
}
private void OnBackRequested(object? sender, AndroidBackRequestedEventArgs e)
@@ -26,5 +29,13 @@ namespace Avalonia.Android.Platform
e.Handled = routedEventArgs.Handled;
}
+
+ public void Dispose()
+ {
+ if (_navigationService != null)
+ {
+ _navigationService.BackRequested -= OnBackRequested;
+ }
+ }
}
}
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
index d1ef928367..e9f9050af1 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
@@ -45,7 +45,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly AndroidInputMethod _textInputMethod;
private readonly INativeControlHostImpl _nativeControlHost;
private readonly IStorageProvider _storageProvider;
- private readonly ISystemNavigationManagerImpl _systemNavigationManager;
+ private readonly AndroidSystemNavigationManagerImpl _systemNavigationManager;
private readonly AndroidInsetsManager _insetsManager;
private readonly ClipboardImpl _clipboard;
private ViewImpl _view;
@@ -155,6 +155,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public virtual void Dispose()
{
+ _systemNavigationManager.Dispose();
_view.Dispose();
_view = null;
}
@@ -395,7 +396,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return _nativeControlHost;
}
- if (featureType == typeof(IInsetsManager))
+ if (featureType == typeof(IInsetsManager) || featureType == typeof(IInputPane))
{
return _insetsManager;
}
diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs
index d67a9501da..30c1f78c36 100644
--- a/src/Avalonia.Base/Animation/Animatable.cs
+++ b/src/Avalonia.Base/Animation/Animatable.cs
@@ -17,8 +17,8 @@ namespace Avalonia.Animation
///
/// Defines the property.
///
- internal static readonly StyledProperty ClockProperty =
- AvaloniaProperty.Register(nameof(Clock), inherits: true);
+ internal static readonly StyledProperty ClockProperty =
+ AvaloniaProperty.Register(nameof(Clock), inherits: true);
///
/// Defines the property.
@@ -36,7 +36,7 @@ namespace Avalonia.Animation
///
/// Gets or sets the clock which controls the animations on the control.
///
- internal IClock Clock
+ internal IClock? Clock
{
get => GetValue(ClockProperty);
set => SetValue(ClockProperty, value);
diff --git a/src/Avalonia.Base/Animation/Animation.cs b/src/Avalonia.Base/Animation/Animation.cs
index f584cad951..7e139c5ae6 100644
--- a/src/Avalonia.Base/Animation/Animation.cs
+++ b/src/Avalonia.Base/Animation/Animation.cs
@@ -234,6 +234,8 @@ namespace Avalonia.Animation
}
}
+ animatorKeyFrames.Sort(static (x, y) => x.Cue.CueValue.CompareTo(y.Cue.CueValue));
+
var newAnimatorInstances = new List();
foreach (var handler in handlerList)
@@ -247,9 +249,22 @@ namespace Avalonia.Animation
{
var animator = newAnimatorInstances.First(a => a.GetType() == keyframe.AnimatorType &&
a.Property == keyframe.Property);
+
+ if (animator.Count == 0 && FillMode is FillMode.Backward or FillMode.Both)
+ keyframe.FillBefore = true;
+
animator.Add(keyframe);
}
+ if (FillMode is FillMode.Forward or FillMode.Both)
+ {
+ foreach (var newAnimatorInstance in newAnimatorInstances)
+ {
+ if (newAnimatorInstance.Count > 0)
+ newAnimatorInstance[newAnimatorInstance.Count - 1].FillAfter = true;
+ }
+ }
+
return (newAnimatorInstances, subscriptions);
}
diff --git a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
index 4db58fac5c..190524402a 100644
--- a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
+++ b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
@@ -16,19 +16,6 @@ namespace Avalonia.Animation
public static readonly DirectProperty ValueProperty =
AvaloniaProperty.RegisterDirect(nameof(Value), k => k.Value, (k, v) => k.Value = v);
- public AnimatorKeyFrame()
- {
-
- }
-
- public AnimatorKeyFrame(Type? animatorType, Func? animatorFactory, Cue cue)
- {
- AnimatorType = animatorType;
- AnimatorFactory = animatorFactory;
- Cue = cue;
- KeySpline = null;
- }
-
public AnimatorKeyFrame(Type? animatorType, Func? animatorFactory, Cue cue, KeySpline? keySpline)
{
AnimatorType = animatorType;
@@ -37,11 +24,12 @@ namespace Avalonia.Animation
KeySpline = keySpline;
}
- internal bool isNeutral;
public Type? AnimatorType { get; }
public Func? AnimatorFactory { get; }
public Cue Cue { get; }
public KeySpline? KeySpline { get; }
+ public bool FillBefore { get; set; }
+ public bool FillAfter { get; set; }
public AvaloniaProperty? Property { get; private set; }
private object? _value;
diff --git a/src/Avalonia.Base/Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs
index f93067d642..8a4469d020 100644
--- a/src/Avalonia.Base/Animation/Animators/Animator`1.cs
+++ b/src/Avalonia.Base/Animation/Animators/Animator`1.cs
@@ -1,7 +1,5 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using Avalonia.Animation.Utils;
+using System.Diagnostics;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
@@ -13,94 +11,72 @@ namespace Avalonia.Animation.Animators
///
internal abstract class Animator : AvaloniaList, IAnimator
{
- ///
- /// List of type-converted keyframes.
- ///
- private readonly List _convertedKeyframes = new List();
-
- private bool _isVerifiedAndConverted;
-
///
/// Gets or sets the target property for the keyframe.
///
public AvaloniaProperty? Property { get; set; }
- public Animator()
- {
- // Invalidate keyframes when changed.
- this.CollectionChanged += delegate { _isVerifiedAndConverted = false; };
- }
-
///
public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable match, Action? onComplete)
{
- if (!_isVerifiedAndConverted)
- VerifyConvertKeyFrames();
-
var subject = new DisposeAnimationInstanceSubject(this, animation, control, clock, onComplete);
return new CompositeDisposable(match.Subscribe(subject), subject);
}
protected T InterpolationHandler(double animationTime, T neutralValue)
{
- AnimatorKeyFrame firstKeyframe, lastKeyframe;
+ if (Count == 0)
+ return neutralValue;
+
+ var (beforeKeyFrame, afterKeyFrame) = FindKeyFrames(animationTime);
- int kvCount = _convertedKeyframes.Count;
- if (kvCount > 2)
+ double beforeTime, afterTime;
+ T beforeValue, afterValue;
+
+ if (beforeKeyFrame is null)
{
- if (animationTime <= 0.0)
- {
- firstKeyframe = _convertedKeyframes[0];
- lastKeyframe = _convertedKeyframes[1];
- }
- else if (animationTime >= 1.0)
- {
- firstKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 2];
- lastKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 1];
- }
- else
- {
- int index = FindClosestBeforeKeyFrame(animationTime);
- firstKeyframe = _convertedKeyframes[index];
- lastKeyframe = _convertedKeyframes[index + 1];
- }
+ beforeTime = 0.0;
+ beforeValue = afterKeyFrame is { FillBefore: true, Value: T fillValue } ? fillValue : neutralValue;
}
else
{
- firstKeyframe = _convertedKeyframes[0];
- lastKeyframe = _convertedKeyframes[1];
+ beforeTime = beforeKeyFrame.Cue.CueValue;
+ beforeValue = beforeKeyFrame.Value is T value ? value : neutralValue;
}
- double t0 = firstKeyframe.Cue.CueValue;
- double t1 = lastKeyframe.Cue.CueValue;
-
- double progress = (animationTime - t0) / (t1 - t0);
-
- T oldValue, newValue;
-
- if (!firstKeyframe.isNeutral && firstKeyframe.Value is T firstKeyframeValue)
- oldValue = firstKeyframeValue;
+ if (afterKeyFrame is null)
+ {
+ afterTime = 1.0;
+ afterValue = beforeKeyFrame is { FillAfter: true, Value: T fillValue } ? fillValue : neutralValue;
+ }
else
- oldValue = neutralValue;
+ {
+ afterTime = afterKeyFrame.Cue.CueValue;
+ afterValue = afterKeyFrame.Value is T value ? value : neutralValue;
+ }
- if (!lastKeyframe.isNeutral && lastKeyframe.Value is T lastKeyframeValue)
- newValue = lastKeyframeValue;
- else
- newValue = neutralValue;
+ var progress = (animationTime - beforeTime) / (afterTime - beforeTime);
- if (lastKeyframe.KeySpline != null)
- progress = lastKeyframe.KeySpline.GetSplineProgress(progress);
+ if (afterKeyFrame?.KeySpline is { } keySpline)
+ progress = keySpline.GetSplineProgress(progress);
- return Interpolate(progress, oldValue, newValue);
+ return Interpolate(progress, beforeValue, afterValue);
}
- private int FindClosestBeforeKeyFrame(double time)
+ private (AnimatorKeyFrame? Before, AnimatorKeyFrame? After) FindKeyFrames(double time)
{
- for (int i = 0; i < _convertedKeyframes.Count; i++)
- if (_convertedKeyframes[i].Cue.CueValue > time)
- return i - 1;
+ Debug.Assert(Count >= 1);
- throw new Exception("Index time is out of keyframe time range.");
+ for (var i = 0; i < Count; i++)
+ {
+ var keyFrame = this[i];
+ var keyFrameTime = keyFrame.Cue.CueValue;
+
+ if (time < keyFrameTime || keyFrameTime == 1.0)
+ return (i > 0 ? this[i - 1] : null, keyFrame);
+ }
+
+ return (this[Count - 1], null);
}
public virtual IDisposable BindAnimation(Animatable control, IObservable instance)
@@ -123,7 +99,7 @@ namespace Avalonia.Animation.Animators
clock ?? control.Clock ?? Clock.GlobalClock,
onComplete,
InterpolationHandler);
-
+
return BindAnimation(control, instance);
}
@@ -131,52 +107,5 @@ namespace Avalonia.Animation.Animators
/// Interpolates in-between two key values given the desired progress time.
///
public abstract T Interpolate(double progress, T oldValue, T newValue);
-
- private void VerifyConvertKeyFrames()
- {
- foreach (AnimatorKeyFrame keyframe in this)
- {
- _convertedKeyframes.Add(keyframe);
- }
-
- AddNeutralKeyFramesIfNeeded();
-
- _isVerifiedAndConverted = true;
- }
-
- private void AddNeutralKeyFramesIfNeeded()
- {
- bool hasStartKey, hasEndKey;
- hasStartKey = hasEndKey = false;
-
- // Check if there's start and end keyframes.
- foreach (var frame in _convertedKeyframes)
- {
- if (frame.Cue.CueValue == 0.0d)
- {
- hasStartKey = true;
- }
- else if (frame.Cue.CueValue == 1.0d)
- {
- hasEndKey = true;
- }
- }
-
- if (!hasStartKey || !hasEndKey)
- AddNeutralKeyFrames(hasStartKey, hasEndKey);
- }
-
- private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey)
- {
- if (!hasStartKey)
- {
- _convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
- }
-
- if (!hasEndKey)
- {
- _convertedKeyframes.Add(new AnimatorKeyFrame(null, null, new Cue(1.0d)) { Value = default(T), isNeutral = true });
- }
- }
}
}
diff --git a/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs b/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs
index 96617b1732..c81be67060 100644
--- a/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs
+++ b/src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs
@@ -86,14 +86,18 @@ namespace Avalonia.Animation.Animators
{
gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), () => new GradientBrushAnimator(), keyframe.Cue, keyframe.KeySpline)
{
- Value = GradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush)
+ Value = GradientBrushAnimator.ConvertSolidColorBrushToGradient(firstGradient, solidColorBrush),
+ FillBefore = keyframe.FillBefore,
+ FillAfter = keyframe.FillAfter
});
}
else if (keyframe.Value is IGradientBrush)
{
gradientAnimator.Add(new AnimatorKeyFrame(typeof(GradientBrushAnimator), () => new GradientBrushAnimator(), keyframe.Cue, keyframe.KeySpline)
{
- Value = keyframe.Value
+ Value = keyframe.Value,
+ FillBefore = keyframe.FillBefore,
+ FillAfter = keyframe.FillAfter
});
}
else
@@ -118,7 +122,9 @@ namespace Avalonia.Animation.Animators
{
solidColorBrushAnimator.Add(new AnimatorKeyFrame(typeof(ISolidColorBrushAnimator), () => new ISolidColorBrushAnimator(), keyframe.Cue, keyframe.KeySpline)
{
- Value = keyframe.Value
+ Value = keyframe.Value,
+ FillBefore = keyframe.FillBefore,
+ FillAfter = keyframe.FillAfter
});
}
else
@@ -149,7 +155,9 @@ namespace Avalonia.Animation.Animators
{
animator.Add(new AnimatorKeyFrame(animatorType, animatorFactory, keyframe.Cue, keyframe.KeySpline)
{
- Value = keyframe.Value
+ Value = keyframe.Value,
+ FillBefore = keyframe.FillBefore,
+ FillAfter = keyframe.FillAfter
});
}
diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs
index 7134a42666..347ba35a41 100644
--- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs
+++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs
@@ -41,14 +41,16 @@ namespace Avalonia.Input
_lastActivePointerDevice = pointerDevice;
}
- if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown
- or RawPointerEventType.TouchCancel or RawPointerEventType.TouchEnd
- && _currentPointer is var (lastPointer, lastPosition))
+ if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown
+ or RawPointerEventType.TouchCancel or RawPointerEventType.TouchEnd)
{
- _currentPointer = null;
- ClearPointerOver(lastPointer, args.Root, 0, PointToClient(args.Root, lastPosition),
- new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
- args.InputModifiers.ToKeyModifiers());
+ if (_currentPointer is var (lastPointer, lastPosition))
+ {
+ _currentPointer = null;
+ ClearPointerOver(lastPointer, args.Root, 0, PointToClient(args.Root, lastPosition),
+ new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
+ args.InputModifiers.ToKeyModifiers());
+ }
}
else if (args.Type is RawPointerEventType.TouchBegin or RawPointerEventType.TouchUpdate && args.Root is Visual visual)
{
diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs
index 5d258cb040..bd2c43878d 100644
--- a/src/Avalonia.Base/Media/DrawingContext.cs
+++ b/src/Avalonia.Base/Media/DrawingContext.cs
@@ -123,7 +123,7 @@ namespace Avalonia.Media
/// Box shadow effect parameters
///
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
- /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
+ /// If the pen is null, then no stroke is performed. If both the pen and the brush are null, then the drawing is not visible.
///
public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect,
double radiusX = 0, double radiusY = 0,
diff --git a/src/Avalonia.Base/Media/Effects/EffectAnimator.cs b/src/Avalonia.Base/Media/Effects/EffectAnimator.cs
index cdf6aa6b63..e2c4cc096c 100644
--- a/src/Avalonia.Base/Media/Effects/EffectAnimator.cs
+++ b/src/Avalonia.Base/Media/Effects/EffectAnimator.cs
@@ -38,7 +38,9 @@ internal class EffectAnimator : Animator
createdAnimator.Add(new AnimatorKeyFrame(typeof(TAnimator), () => new TAnimator(), keyFrame.Cue,
keyFrame.KeySpline)
{
- Value = keyFrame.Value
+ Value = keyFrame.Value,
+ FillBefore = keyFrame.FillBefore,
+ FillAfter = keyFrame.FillAfter
});
}
else
diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
index 2f8c4ad263..a4fc1edd14 100644
--- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
@@ -48,7 +48,7 @@ namespace Avalonia.Media.TextFormatting
///
/// The created text style run.
///
- private static ValueSpan CreateTextStyleRun(ReadOnlySpan text, int firstTextSourceIndex,
+ internal static ValueSpan CreateTextStyleRun(ReadOnlySpan text, int firstTextSourceIndex,
TextRunProperties defaultProperties, IReadOnlyList>? textModifier)
{
if (textModifier == null || textModifier.Count == 0)
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index efaab511ce..d9e6d6486d 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -1245,8 +1245,9 @@ namespace Avalonia.Media.TextFormatting
{
var textMetrics = textRun.TextMetrics;
var glyphRun = textRun.GlyphRun;
+ var runBounds = glyphRun.InkBounds.WithX(widthIncludingWhitespace + glyphRun.InkBounds.X);
- bounds = bounds.Union(glyphRun.InkBounds);
+ bounds = bounds.Union(runBounds);
if (fontRenderingEmSize < textMetrics.FontRenderingEmSize)
{
diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
index ee0a7a3740..dc2f19d389 100644
--- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
+++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
@@ -49,15 +49,6 @@ namespace Avalonia.PropertyStore
_uncommon = new() { _hasDataValidation = true };
}
- public bool HasValue
- {
- get
- {
- Start(produceValue: false);
- return _hasValue;
- }
- }
-
public bool IsSubscribed => _subscription is not null;
public AvaloniaProperty Property { get; }
AvaloniaProperty IValueEntry.Property => Property;
@@ -70,6 +61,12 @@ namespace Avalonia.PropertyStore
BindingCompleted();
}
+ public bool HasValue()
+ {
+ Start(produceValue: false);
+ return _hasValue;
+ }
+
public TValue GetValue()
{
Start(produceValue: false);
diff --git a/src/Avalonia.Base/PropertyStore/IValueEntry.cs b/src/Avalonia.Base/PropertyStore/IValueEntry.cs
index 5898bef491..7ac7e83276 100644
--- a/src/Avalonia.Base/PropertyStore/IValueEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/IValueEntry.cs
@@ -8,13 +8,16 @@ namespace Avalonia.PropertyStore
///
internal interface IValueEntry
{
- bool HasValue { get; }
-
///
/// Gets the property that this value applies to.
///
AvaloniaProperty Property { get; }
+ ///
+ /// Checks whether the entry has a value, starting the entry if necessary.
+ ///
+ bool HasValue();
+
///
/// Gets the value associated with the entry.
///
diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
index 16b96eff5d..2d136fb1e5 100644
--- a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
@@ -19,13 +19,13 @@ namespace Avalonia.PropertyStore
}
public StyledProperty Property { get; }
- public bool HasValue => true;
AvaloniaProperty IValueEntry.Property => Property;
public void Unsubscribe() { }
public void Dispose() => _owner.OnEntryDisposed(this);
+ bool IValueEntry.HasValue() => true;
object? IValueEntry.GetValue() => _value;
T IValueEntry.GetValue() => _value;
diff --git a/src/Avalonia.Base/PropertyStore/ValueFrame.cs b/src/Avalonia.Base/PropertyStore/ValueFrame.cs
index 4d74ddcbb1..0b51189a15 100644
--- a/src/Avalonia.Base/PropertyStore/ValueFrame.cs
+++ b/src/Avalonia.Base/PropertyStore/ValueFrame.cs
@@ -28,7 +28,7 @@ namespace Avalonia.PropertyStore
}
public int EntryCount => _index.Count;
- public bool IsActive => GetIsActive(out _);
+ public bool IsActive() => GetIsActive(out _);
public ValueStore? Owner => !_isShared ? _owner :
throw new AvaloniaInternalException("Cannot get owner for shared ValueFrame");
public BindingPriority Priority { get; }
diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs
index e8f358cb7f..7008f841f4 100644
--- a/src/Avalonia.Base/PropertyStore/ValueStore.cs
+++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs
@@ -983,7 +983,7 @@ namespace Avalonia.PropertyStore
// evaluated last as it can cause bindings to be subscribed.
if (foundEntry &&
HasHigherPriority(entry!, priority, current, changedValueEntry) &&
- entry!.HasValue)
+ entry!.HasValue())
{
if (current is not null)
{
@@ -1051,7 +1051,7 @@ namespace Avalonia.PropertyStore
{
var frame = _frames[i];
- if (!frame.IsActive)
+ if (!frame.IsActive())
continue;
var priority = frame.Priority;
@@ -1067,7 +1067,7 @@ namespace Avalonia.PropertyStore
if (!HasHigherPriority(entry, priority, effectiveValue, changedValueEntry))
continue;
- if (!entry.HasValue)
+ if (!entry.HasValue())
continue;
if (effectiveValue is not null)
diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs
index df6b478e83..c6a3859e3d 100644
--- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs
@@ -1,5 +1,4 @@
using Avalonia.Media;
-using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Drawing.Nodes;
@@ -7,24 +6,41 @@ class RenderDataRectangleNode : RenderDataBrushAndPenNode
{
public RoundedRect Rect { get; set; }
public BoxShadows BoxShadows { get; set; }
-
+
public override bool HitTest(Point p)
{
- if (ServerBrush != null) // it's safe to check for null
+ var strokeThicknessAdjustment = (ClientPen?.Thickness / 2) ?? 0;
+
+ if (Rect.IsRounded)
{
- var rect = Rect.Rect.Inflate((ClientPen?.Thickness / 2) ?? 0);
- return rect.ContainsExclusive(p);
+ var outerRoundedRect = Rect.Inflate(strokeThicknessAdjustment, strokeThicknessAdjustment);
+ if (outerRoundedRect.ContainsExclusive(p))
+ {
+ if (ServerBrush != null) // it's safe to check for null
+ return true;
+
+ var innerRoundedRect = Rect.Deflate(strokeThicknessAdjustment, strokeThicknessAdjustment);
+ return !innerRoundedRect.ContainsExclusive(p);
+ }
}
else
{
- var borderRect = Rect.Rect.Inflate((ClientPen?.Thickness / 2) ?? 0);
- var emptyRect = Rect.Rect.Deflate((ClientPen?.Thickness / 2) ?? 0);
- return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p);
+ var outerRect = Rect.Rect.Inflate(strokeThicknessAdjustment);
+ if (outerRect.ContainsExclusive(p))
+ {
+ if (ServerBrush != null) // it's safe to check for null
+ return true;
+
+ var innerRect = Rect.Rect.Deflate(strokeThicknessAdjustment);
+ return !innerRect.ContainsExclusive(p);
+ }
}
+
+ return false;
}
public override void Invoke(ref RenderDataNodeRenderContext context) =>
context.Context.DrawRectangle(ServerBrush, ServerPen, Rect, BoxShadows);
public override Rect? Bounds => BoxShadows.TransformBounds(Rect.Rect).Inflate((ServerPen?.Thickness ?? 0) / 2);
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Base/RoundedRect.cs b/src/Avalonia.Base/RoundedRect.cs
index 4c6f46ffe0..0b23a8c0ca 100644
--- a/src/Avalonia.Base/RoundedRect.cs
+++ b/src/Avalonia.Base/RoundedRect.cs
@@ -150,5 +150,64 @@ namespace Avalonia
/// For now it's internal to keep some loud community members happy about the API being pretty
///
internal bool IsEmpty() => this == default;
+
+ private static bool IsOutsideCorner(double dx, double dy, double radius)
+ {
+ return (dx < 0) && (dy < 0) && (dx * dx + dy * dy > radius * radius);
+ }
+
+ ///
+ /// Determines whether a point is in the bounds of the rounded rectangle, exclusive of the
+ /// rounded rectangle's bottom/right edge.
+ ///
+ /// The point.
+ /// true if the point is in the bounds of the rounded rectangle; otherwise false.
+ public bool ContainsExclusive(Point p)
+ {
+ // Do a simple rectangular bounds check first
+ if (!Rect.ContainsExclusive(p))
+ return false;
+
+ // If any radii totals exceed available bounds, determine a scale factor that needs to be applied
+ var scaleFactor = 1.0;
+ if (Rect.Width > 0)
+ {
+ var radiiWidth = Math.Max(RadiiTopLeft.X + RadiiTopRight.X, RadiiBottomLeft.X + RadiiBottomRight.X);
+ if (radiiWidth > Rect.Width)
+ scaleFactor = Math.Min(scaleFactor, Rect.Width / radiiWidth);
+ }
+ if (Rect.Height > 0)
+ {
+ var radiiHeight = Math.Max(RadiiTopLeft.Y + RadiiBottomLeft.Y, RadiiTopRight.Y + RadiiBottomRight.Y);
+ if (radiiHeight > Rect.Height)
+ scaleFactor = Math.Min(scaleFactor, Rect.Height / radiiHeight);
+ }
+
+ // Before corner hit-testing, make the point relative to the bounds' upper-left
+ p = new Point(p.X - Rect.X, p.Y - Rect.Y);
+
+ // Top-left corner
+ var radius = Math.Min(RadiiTopLeft.X, RadiiTopLeft.Y) * scaleFactor;
+ if (IsOutsideCorner(p.X - radius, p.Y - radius, radius))
+ return false;
+
+ // Top-right corner
+ radius = Math.Min(RadiiTopRight.X, RadiiTopRight.Y) * scaleFactor;
+ if (IsOutsideCorner(Rect.Width - radius - p.X, p.Y - radius, radius))
+ return false;
+
+ // Bottom-right corner
+ radius = Math.Min(RadiiBottomRight.X, RadiiBottomRight.Y) * scaleFactor;
+ if (IsOutsideCorner(Rect.Width - radius - p.X, Rect.Height - radius - p.Y, radius))
+ return false;
+
+ // Bottom-left corner
+ radius = Math.Min(RadiiBottomLeft.X, RadiiBottomLeft.Y) * scaleFactor;
+ if (IsOutsideCorner(p.X - radius, Rect.Height - radius - p.Y, radius))
+ return false;
+
+ return true;
+ }
+
}
}
diff --git a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
index 8fe0bb2537..888a92fcdf 100644
--- a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
+++ b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
@@ -1,4 +1,5 @@
-using Avalonia.LogicalTree;
+using System.Collections.Generic;
+using Avalonia.LogicalTree;
namespace Avalonia.Styling.Activators
{
@@ -12,7 +13,7 @@ namespace Avalonia.Styling.Activators
private readonly int _step;
private readonly int _offset;
private readonly bool _reversed;
- private int? _index;
+ private int _index = -1;
public NthChildActivator(
ILogical control,
@@ -28,7 +29,7 @@ namespace Avalonia.Styling.Activators
protected override bool EvaluateIsActive()
{
- var index = _index ?? _provider.GetChildIndex(_control);
+ var index = _index >= 0 ? _index : _provider.GetChildIndex(_control);
return NthChildSelector.Evaluate(index, _provider, _step, _offset, _reversed).IsMatch;
}
@@ -50,26 +51,25 @@ namespace Avalonia.Styling.Activators
// 3. We're a reversed (nth-last-child) selector and total count has changed
switch (e.Action)
{
- // We're using the _index field to pass the index of the child to EvaluateIsActive
- // *only* when the active state is re-evaluated via this event handler. The docs
- // for EvaluateIsActive say:
+ // The docs for EvaluateIsActive say:
//
// > This method should read directly from its inputs and not rely on any
// > subscriptions to fire in order to be up-to-date.
//
// Which is good advice in general, however in this case we need to break the rule
- // and use the value from the event subscription instead of calling
+ // and use the value from the event subscription where possible instead of calling
// IChildIndexProvider.GetChildIndex. This is because this event can be fired during
- // the process of realizing an element of a virtualized list; in this case calling
- // GetChildIndex may not return the correct index as the element isn't yet realized.
+ // the process of realizing an element of a virtualized list; in this case there may
+ // be more than one `nth-child` style on a the list item and when the other is
+ // re-evaluated calling GetChildIndex may not return the correct index as the element
+ // isn't yet realized.
case ChildIndexChangedAction.ChildIndexChanged when e.Child == _control:
- _index = e.Index;
+ _index = e.Index >= 0 ? e.Index : _provider.GetChildIndex(_control);
ReevaluateIsActive();
- _index = null;
break;
case ChildIndexChangedAction.ChildIndexesReset:
case ChildIndexChangedAction.TotalCountChanged when _reversed:
- _index = null;
+ _index = _provider.GetChildIndex(_control);
ReevaluateIsActive();
break;
}
diff --git a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
index 7604c26244..d9a2f55da9 100644
--- a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
+++ b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
@@ -15,9 +15,9 @@ namespace Avalonia.Styling
Property = property;
}
- public bool HasValue => true;
public AvaloniaProperty Property { get; }
+ public bool HasValue() => true;
public object? GetValue() => _value ??= _template.Build();
bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs
index a59dd0dba8..8bfda18d6d 100644
--- a/src/Avalonia.Base/Styling/Setter.cs
+++ b/src/Avalonia.Base/Styling/Setter.cs
@@ -59,7 +59,6 @@ namespace Avalonia.Styling
}
}
- bool IValueEntry.HasValue => true;
AvaloniaProperty IValueEntry.Property => EnsureProperty();
public override string ToString() => $"Setter: {Property} = {Value}";
@@ -91,6 +90,7 @@ namespace Avalonia.Styling
return this;
}
+ bool IValueEntry.HasValue() => true;
object? IValueEntry.GetValue() => Value;
bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
index d21a7bdb3f..8934b08b98 100644
--- a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
+++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
@@ -1153,15 +1153,23 @@ namespace Avalonia.Collections
get { return GetItemAt(index); }
}
- bool IList.IsFixedSize => false;
- bool IList.IsReadOnly => true;
+ bool IList.IsFixedSize => SourceList?.IsFixedSize ?? true;
+ bool IList.IsReadOnly => SourceList?.IsReadOnly ?? true;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
object IList.this[int index]
{
get => this[index];
- set => throw new NotSupportedException();
+ set
+ {
+ SourceList[index] = value;
+ if (SourceList is not INotifyCollectionChanged)
+ {
+ // TODO: implement Replace
+ ProcessCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, value));
+ }
+ }
}
///
@@ -3992,9 +4000,36 @@ namespace Avalonia.Collections
}
}
- int IList.Add(object value) => throw new NotSupportedException();
- void IList.Clear() => throw new NotSupportedException();
- void IList.Insert(int index, object value) => throw new NotSupportedException();
+ int IList.Add(object value)
+ {
+ var index = SourceList.Add(value);
+ if (SourceList is not INotifyCollectionChanged)
+ {
+ ProcessCollectionChanged(
+ new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, value));
+ }
+ return index;
+ }
+
+ void IList.Clear()
+ {
+ SourceList.Clear();
+ if (SourceList is not INotifyCollectionChanged)
+ {
+ ProcessCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+ }
+
+ void IList.Insert(int index, object value)
+ {
+ SourceList.Insert(index, value);
+ if (SourceList is not INotifyCollectionChanged)
+ {
+ // TODO: implement Insert
+ ProcessCollectionChanged(
+ new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, value));
+ }
+ }
void ICollection.CopyTo(Array array, int index) => InternalList.CopyTo(array, index);
///
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index 88270ee5cc..e4573c3759 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -1828,12 +1828,6 @@ namespace Avalonia.Controls
private set;
}
- internal bool UpdatedStateOnMouseLeftButtonDown
- {
- get;
- set;
- }
-
///
/// Indicates whether or not to use star-sizing logic. If the DataGrid has infinite available space,
/// then star sizing doesn't make sense. In this case, all star columns grow to a predefined size of
diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs
index 599bea056b..2f5f2c25e0 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs
@@ -169,9 +169,13 @@ namespace Avalonia.Controls
return;
}
OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e));
+ if (e.Handled)
+ {
+ return;
+ }
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
- if (!e.Handled && OwningGrid.IsTabStop)
+ if (OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}
@@ -185,13 +189,11 @@ namespace Avalonia.Controls
{
e.Handled = handled;
}
-
- OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
}
}
else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
{
- if (!e.Handled && OwningGrid.IsTabStop)
+ if (OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
index fc9aac0ab8..835bb566fe 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
@@ -349,15 +349,15 @@ namespace Avalonia.Controls
{
Debug.Assert(index >= 0);
- IList list = List;
- if (list != null)
+ if (DataSource is DataGridCollectionView collectionView)
{
- return (index < list.Count) ? list[index] : null;
+ return (index < collectionView.Count) ? collectionView.GetItemAt(index) : null;
}
- if (DataSource is DataGridCollectionView collectionView)
+ IList list = List;
+ if (list != null)
{
- return (index < collectionView.Count) ? collectionView.GetItemAt(index) : null;
+ return (index < list.Count) ? list[index] : null;
}
IEnumerable enumerable = DataSource;
@@ -419,15 +419,15 @@ namespace Avalonia.Controls
public int IndexOf(object dataItem)
{
- IList list = List;
- if (list != null)
+ if (DataSource is DataGridCollectionView cv)
{
- return list.IndexOf(dataItem);
+ return cv.IndexOf(dataItem);
}
- if (DataSource is DataGridCollectionView cv)
+ IList list = List;
+ if (list != null)
{
- return cv.IndexOf(dataItem);
+ return list.IndexOf(dataItem);
}
IEnumerable enumerable = DataSource;
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
index dfda7d6e4f..48d6ca7f44 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
@@ -771,14 +771,6 @@ namespace Avalonia.Controls
if (OwningGrid != null)
{
OwningGrid.IsDoubleClickRecordsClickOnCall(this);
- if (OwningGrid.UpdatedStateOnMouseLeftButtonDown)
- {
- OwningGrid.UpdatedStateOnMouseLeftButtonDown = false;
- }
- else
- {
- e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, -1, Slot, false);
- }
}
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
index c42d21d126..a480a7af15 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
@@ -195,7 +195,6 @@ namespace Avalonia.Controls.Primitives
Debug.Assert(sender is DataGridRowHeader);
Debug.Assert(sender == this);
e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, -1, Slot, false);
- OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
}
}
else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs
index 47a0c531ba..8a1d38f88a 100644
--- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs
+++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs
@@ -15,6 +15,15 @@ namespace Avalonia.Controls
{
public partial class AutoCompleteBox
{
+ ///
+ /// Defines see property.
+ ///
+ public static readonly StyledProperty CaretIndexProperty =
+ TextBox.CaretIndexProperty.AddOwner(new(
+ defaultValue: 0,
+ defaultBindingMode:BindingMode.TwoWay,
+ coerce: TextBox.CoerceCaretIndex));
+
public static readonly StyledProperty WatermarkProperty =
TextBox.WatermarkProperty.AddOwner();
@@ -158,6 +167,15 @@ namespace Avalonia.Controls
AvaloniaProperty.Register>>?>(
nameof(AsyncPopulator));
+ ///
+ /// Gets or sets the caret index
+ ///
+ public int CaretIndex
+ {
+ get => GetValue(CaretIndexProperty);
+ set => SetValue(CaretIndexProperty, value);
+ }
+
///
/// Gets or sets the minimum number of characters required to be entered
/// in the text box before the displays possible matches.
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index a42ac48864..366c145356 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -528,6 +528,17 @@ namespace Avalonia.Controls
_itemsPresenter = e.NameScope.Find("PART_ItemsPresenter");
}
+ protected override void OnGotFocus(GotFocusEventArgs e)
+ {
+ base.OnGotFocus(e);
+
+ // If the focus is coming from a child control, set the tab once active element to
+ // the focused control. This ensures that tabbing back into the control will focus
+ // the last focused control when TabNavigationMode == Once.
+ if (e.Source != this && e.Source is IInputElement ie)
+ KeyboardNavigation.SetTabOnceActiveElement(this, ie);
+ }
+
///
/// Handles directional navigation within the .
///
diff --git a/src/Avalonia.Controls/ListBoxItem.cs b/src/Avalonia.Controls/ListBoxItem.cs
index aa95511524..5ee4854554 100644
--- a/src/Avalonia.Controls/ListBoxItem.cs
+++ b/src/Avalonia.Controls/ListBoxItem.cs
@@ -104,7 +104,11 @@ namespace Avalonia.Controls
// As we only update selection from touch/pen on pointer release, we need to raise
// the pointer event on the owner to trigger a commit.
if (e.Pointer.Type != PointerType.Mouse)
+ {
+ var sourceBackup = e.Source;
owner.RaiseEvent(e);
+ e.Source = sourceBackup;
+ }
e.Handled = true;
}
diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
index 8f1b9dfd22..2369c1f983 100644
--- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
+++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
@@ -49,7 +49,7 @@ namespace Avalonia.Controls.Platform
internal IMenu? Menu { get; private set; }
- protected static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400);
+ public static TimeSpan MenuShowDelay { get; set;} = TimeSpan.FromMilliseconds(400);
protected internal virtual void GotFocus(object? sender, GotFocusEventArgs e)
{
diff --git a/src/Avalonia.Controls/Platform/IInputPane.cs b/src/Avalonia.Controls/Platform/IInputPane.cs
new file mode 100644
index 0000000000..c36788570c
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/IInputPane.cs
@@ -0,0 +1,89 @@
+using System;
+using Avalonia.Animation.Easings;
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls.Platform
+{
+ ///
+ /// Listener for the platform's input pane(eg, software keyboard). Provides access to the input pane height and state.
+ ///
+ [NotClientImplementable]
+ public interface IInputPane
+ {
+ ///
+ /// The current input pane state
+ ///
+ InputPaneState State { get; }
+
+ ///
+ /// The current input pane bounds.
+ ///
+ Rect OccludedRect { get; }
+
+ ///
+ /// Occurs when the input pane's state has changed.
+ ///
+ event EventHandler? StateChanged;
+ }
+
+ ///
+ /// The input pane opened state.
+ ///
+ public enum InputPaneState
+ {
+ ///
+ /// The input pane is either closed, or doesn't form part of the platform insets, i.e. it's floating or is an overlay.
+ ///
+ Closed,
+
+ ///
+ /// The input pane is open.
+ ///
+ Open
+ }
+
+ ///
+ /// Provides state change information about the input pane.
+ ///
+ public sealed class InputPaneStateEventArgs : EventArgs
+ {
+ ///
+ /// The new state of the input pane
+ ///
+ public InputPaneState NewState { get; }
+
+ ///
+ /// The initial bounds of the input pane.
+ ///
+ public Rect? StartRect { get; }
+
+ ///
+ /// The final bounds of the input pane.
+ ///
+ public Rect EndRect { get; }
+
+ ///
+ /// The duration of the input pane's state change animation.
+ ///
+ public TimeSpan AnimationDuration { get; }
+
+ ///
+ /// The easing of the input pane's state changed animation.
+ ///
+ public IEasing? Easing { get; }
+
+ public InputPaneStateEventArgs(InputPaneState newState, Rect? startRect, Rect endRect, TimeSpan animationDuration, IEasing? easing)
+ {
+ NewState = newState;
+ StartRect = startRect;
+ EndRect = endRect;
+ AnimationDuration = animationDuration;
+ Easing = easing;
+ }
+
+ public InputPaneStateEventArgs(InputPaneState newState, Rect? startRect, Rect endRect)
+ : this(newState, startRect, endRect, default, null)
+ {
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index 88577c19db..9a89ccaf28 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Data;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
@@ -34,6 +35,9 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty CaretBrushProperty =
AvaloniaProperty.Register(nameof(CaretBrush));
+ public static readonly StyledProperty CaretBlinkIntervalProperty =
+ TextBox.CaretBlinkIntervalProperty.AddOwner();
+
public static readonly StyledProperty SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner(new(coerce: TextBox.CoerceCaretIndex));
@@ -88,7 +92,7 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty BackgroundProperty =
Border.BackgroundProperty.AddOwner();
- private readonly DispatcherTimer _caretTimer;
+ private DispatcherTimer? _caretTimer;
private bool _caretBlink;
private TextLayout? _textLayout;
private Size _constraint;
@@ -101,13 +105,10 @@ namespace Avalonia.Controls.Presenters
static TextPresenter()
{
- AffectsRender(CaretBrushProperty, SelectionBrushProperty, TextElement.ForegroundProperty);
+ AffectsRender(CaretBrushProperty, SelectionBrushProperty, SelectionForegroundBrushProperty, TextElement.ForegroundProperty);
}
- public TextPresenter()
- {
- _caretTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
- }
+ public TextPresenter() { }
public event EventHandler? CaretBoundsChanged;
@@ -288,6 +289,15 @@ namespace Avalonia.Controls.Presenters
set => SetValue(CaretBrushProperty, value);
}
+ ///
+ /// Gets or sets the caret blink rate
+ ///
+ public TimeSpan CaretBlinkInterval
+ {
+ get => GetValue(CaretBlinkIntervalProperty);
+ set => SetValue(CaretBlinkIntervalProperty, value);
+ }
+
public int SelectionStart
{
get => GetValue(SelectionStartProperty);
@@ -443,7 +453,7 @@ namespace Avalonia.Controls.Presenters
public void ShowCaret()
{
_caretBlink = true;
- _caretTimer.Start();
+ _caretTimer?.Start();
InvalidateVisual();
}
@@ -454,7 +464,7 @@ namespace Avalonia.Controls.Presenters
{
TextSelectionHandleCanvas.ShowHandles = false;
}
- _caretTimer.Stop();
+ _caretTimer?.Stop();
InvalidateVisual();
}
@@ -465,18 +475,18 @@ namespace Avalonia.Controls.Presenters
return;
}
- if (_caretTimer.IsEnabled)
+ if (_caretTimer?.IsEnabled ?? false)
{
_caretBlink = true;
- _caretTimer.Stop();
- _caretTimer.Start();
+ _caretTimer?.Stop();
+ _caretTimer?.Start();
InvalidateVisual();
}
else
{
- _caretTimer.Start();
+ _caretTimer?.Start();
InvalidateVisual();
- _caretTimer.Stop();
+ _caretTimer?.Stop();
}
if (IsMeasureValid)
@@ -716,6 +726,33 @@ namespace Avalonia.Controls.Presenters
CaretChanged();
}
+ private void ResetCaretTimer()
+ {
+ bool isEnabled = false;
+
+ if (_caretTimer != null)
+ {
+ _caretTimer.Tick -= CaretTimerTick;
+
+ if (_caretTimer.IsEnabled)
+ {
+ _caretTimer.Stop();
+ isEnabled = true;
+ }
+
+ _caretTimer = null;
+ }
+
+ if (CaretBlinkInterval.TotalMilliseconds > 0)
+ {
+ _caretTimer = new DispatcherTimer { Interval = CaretBlinkInterval };
+ _caretTimer.Tick += CaretTimerTick;
+
+ if (isEnabled)
+ _caretTimer.Start();
+ }
+ }
+
public CharacterHit GetNextCharacterHit(LogicalDirection direction = LogicalDirection.Forward)
{
if (Text is null)
@@ -855,7 +892,7 @@ namespace Avalonia.Controls.Presenters
{
base.OnAttachedToVisualTree(e);
- _caretTimer.Tick += CaretTimerTick;
+ ResetCaretTimer();
if (TextSelectionHandleCanvas is { } canvas && _layer != null && !_layer.Children.Contains(canvas))
_layer?.Add(TextSelectionHandleCanvas);
@@ -882,9 +919,11 @@ namespace Avalonia.Controls.Presenters
c.SetPresenter(null);
}
- _caretTimer.Stop();
-
- _caretTimer.Tick -= CaretTimerTick;
+ if (_caretTimer != null)
+ {
+ _caretTimer.Stop();
+ _caretTimer.Tick -= CaretTimerTick;
+ }
}
private void OnPreeditChanged(string? preeditText, int? cursorPosition)
@@ -939,6 +978,11 @@ namespace Avalonia.Controls.Presenters
}
}
+ if (change.Property == CaretBlinkIntervalProperty)
+ {
+ ResetCaretTimer();
+ }
+
switch (change.Property.Name)
{
case nameof(PreeditText):
diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs
index 33ab78b66f..b54dd314f9 100644
--- a/src/Avalonia.Controls/Primitives/RangeBase.cs
+++ b/src/Avalonia.Controls/Primitives/RangeBase.cs
@@ -10,6 +10,8 @@ namespace Avalonia.Controls.Primitives
///
public abstract class RangeBase : TemplatedControl
{
+ private bool _isDataContextChanging;
+
///
/// Defines the property.
///
@@ -74,7 +76,7 @@ namespace Avalonia.Controls.Primitives
private void OnMinimumChanged()
{
- if (IsInitialized)
+ if (IsInitialized && !_isDataContextChanging)
{
CoerceValue(MaximumProperty);
CoerceValue(ValueProperty);
@@ -99,8 +101,9 @@ namespace Avalonia.Controls.Primitives
private void OnMaximumChanged()
{
- if (IsInitialized)
+ if (IsInitialized && !_isDataContextChanging)
{
+ CoerceValue(MinimumProperty);
CoerceValue(ValueProperty);
}
}
@@ -170,6 +173,20 @@ namespace Avalonia.Controls.Primitives
RaiseEvent(valueChangedEventArgs);
}
}
+
+ ///
+ protected override void OnDataContextBeginUpdate()
+ {
+ _isDataContextChanging = true;
+ base.OnDataContextBeginUpdate();
+ }
+
+ ///
+ protected override void OnDataContextEndUpdate()
+ {
+ base.OnDataContextEndUpdate();
+ _isDataContextChanging = false;
+ }
///
/// Checks if the double value is not infinity nor NaN.
diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
index b354f06be4..beec0bdcb1 100644
--- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
@@ -513,6 +513,9 @@ namespace Avalonia.Controls.Primitives
var containerIsSelected = GetIsSelected(container);
UpdateSelection(index, containerIsSelected, toggleModifier: true);
}
+
+ if (Selection.AnchorIndex == index)
+ KeyboardNavigation.SetTabOnceActiveElement(this, container);
}
///
diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs
index 4fa8ea989f..143ac29c02 100644
--- a/src/Avalonia.Controls/SelectableTextBlock.cs
+++ b/src/Avalonia.Controls/SelectableTextBlock.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Data;
using System.Linq;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Utils;
@@ -32,6 +33,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty SelectionBrushProperty =
TextBox.SelectionBrushProperty.AddOwner();
+ public static readonly StyledProperty SelectionForegroundBrushProperty =
+ TextBox.SelectionForegroundBrushProperty.AddOwner();
+
public static readonly DirectProperty CanCopyProperty =
TextBox.CanCopyProperty.AddOwner(o => o.CanCopy);
@@ -63,6 +67,15 @@ namespace Avalonia.Controls
set => SetValue(SelectionBrushProperty, value);
}
+ ///
+ /// Gets or sets a brush that is used for the foreground of selected text
+ ///
+ public IBrush? SelectionForegroundBrush
+ {
+ get => GetValue(SelectionForegroundBrushProperty);
+ set => SetValue(SelectionForegroundBrushProperty, value);
+ }
+
///
/// Gets or sets a character index for the beginning of the current selection.
///
@@ -167,6 +180,58 @@ namespace Avalonia.Controls
UpdateCommandStates();
}
+ protected override TextLayout CreateTextLayout(string? text)
+ {
+ var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
+
+ var defaultProperties = new GenericTextRunProperties(
+ typeface,
+ FontSize,
+ TextDecorations,
+ Foreground);
+
+ var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
+ defaultProperties, TextWrapping, LineHeight, 0, LetterSpacing)
+ {
+ LineSpacing = LineSpacing
+ };
+
+ IReadOnlyList>? textStyleOverrides = null;
+ var selectionStart = SelectionStart;
+ var selectionEnd = SelectionEnd;
+ var start = Math.Min(selectionStart, selectionEnd);
+ var length = Math.Max(selectionStart, selectionEnd) - start;
+
+ if (length > 0 && SelectionForegroundBrush != null)
+ {
+ textStyleOverrides = new[]
+ {
+ new ValueSpan(start, length,
+ new GenericTextRunProperties(typeface, FontSize,
+ foregroundBrush: SelectionForegroundBrush))
+ };
+ }
+
+ ITextSource textSource;
+
+ if (_textRuns != null)
+ {
+ textSource = new InlinesTextSource(_textRuns, textStyleOverrides);
+ }
+ else
+ {
+ textSource = new FormattedTextSource(text ?? "", defaultProperties, textStyleOverrides);
+ }
+
+ return new TextLayout(
+ textSource,
+ paragraphProperties,
+ TextTrimming,
+ _constraint.Width,
+ _constraint.Height,
+ MaxLines);
+ }
+
protected override void RenderTextLayout(DrawingContext context, Point origin)
{
var selectionStart = SelectionStart;
@@ -220,10 +285,17 @@ namespace Avalonia.Controls
{
base.OnPropertyChanged(change);
- if (change.Property == SelectionStartProperty || change.Property == SelectionEndProperty)
+ if (change.Property == SelectionStartProperty ||
+ change.Property == SelectionEndProperty)
{
RaisePropertyChanged(SelectedTextProperty, "", "");
UpdateCommandStates();
+ InvalidateTextLayout();
+ }
+
+ if(change.Property == SelectionForegroundBrushProperty)
+ {
+ InvalidateTextLayout();
}
}
diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs
index 5b7ea7b9a5..e71a5a656b 100644
--- a/src/Avalonia.Controls/TabControl.cs
+++ b/src/Avalonia.Controls/TabControl.cs
@@ -73,7 +73,6 @@ namespace Avalonia.Controls
{
SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
- TabStripPlacementProperty.Changed.AddClassHandler((x, e) => x.UpdateTabStripPlacement());
AffectsMeasure(TabStripPlacementProperty);
SelectedItemProperty.Changed.AddClassHandler((x, e) => x.UpdateSelectedContent());
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Tab);
@@ -154,7 +153,7 @@ namespace Avalonia.Controls
protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
- return new TabItem { TabStripPlacement = TabStripPlacement };
+ return new TabItem();
}
protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
@@ -166,6 +165,11 @@ namespace Avalonia.Controls
{
base.PrepareContainerForItemOverride(element, item, index);
+ if (element is TabItem tabItem)
+ {
+ tabItem.TabStripPlacement = TabStripPlacement;
+ }
+
if (index == SelectedIndex)
{
UpdateSelectedContent(element);
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 1065bedc4b..cbda1dfbe6 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -7,6 +7,7 @@ using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
+using Avalonia.Utilities;
namespace Avalonia.Controls
{
@@ -155,8 +156,8 @@ namespace Avalonia.Controls
nameof(Inlines), t => t.Inlines, (t, v) => t.Inlines = v);
private TextLayout? _textLayout;
- private Size _constraint;
- private IReadOnlyList? _textRuns;
+ protected Size _constraint;
+ protected IReadOnlyList? _textRuns;
private InlineCollection? _inlines;
///
@@ -846,7 +847,7 @@ namespace Avalonia.Controls
InvalidateTextLayout();
}
- private readonly record struct SimpleTextSource : ITextSource
+ protected readonly record struct SimpleTextSource : ITextSource
{
private readonly string _text;
private readonly TextRunProperties _defaultProperties;
@@ -875,13 +876,17 @@ namespace Avalonia.Controls
}
}
- private readonly struct InlinesTextSource : ITextSource
+#pragma warning disable CA1815 // Equals und Gleichheitsoperator für Werttypen außer Kraft setzen
+ protected readonly struct InlinesTextSource : ITextSource
+#pragma warning restore CA1815 // Equals und Gleichheitsoperator für Werttypen außer Kraft setzen
{
private readonly IReadOnlyList _textRuns;
+ private readonly IReadOnlyList>? _textModifier;
- public InlinesTextSource(IReadOnlyList textRuns)
+ public InlinesTextSource(IReadOnlyList textRuns, IReadOnlyList>? textModifier = null)
{
_textRuns = textRuns;
+ _textModifier = textModifier;
}
public IReadOnlyList TextRuns => _textRuns;
@@ -904,11 +909,13 @@ namespace Avalonia.Controls
continue;
}
- if (textRun is TextCharacters)
+ if (textRun is TextCharacters textCharacters)
{
var skip = Math.Max(0, textSourceIndex - currentPosition);
- return new TextCharacters(textRun.Text.Slice(skip), textRun.Properties!);
+ var textStyleRun = FormattedTextSource.CreateTextStyleRun(textRun.Text.Slice(skip).Span, textSourceIndex, textCharacters.Properties, _textModifier);
+
+ return new TextCharacters(textRun.Text.Slice(skip, textStyleRun.Length), textStyleRun.Value);
}
return textRun;
@@ -916,6 +923,6 @@ namespace Avalonia.Controls
return new TextEndOfParagraph();
}
- }
+ }
}
}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index b6da36d1cb..8bd0286a0a 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -92,6 +92,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty CaretBrushProperty =
AvaloniaProperty.Register(nameof(CaretBrush));
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty CaretBlinkIntervalProperty =
+ AvaloniaProperty.Register(nameof(CaretBlinkInterval), defaultValue: TimeSpan.FromMilliseconds(500));
+
///
/// Defines the property
///
@@ -443,6 +449,13 @@ namespace Avalonia.Controls
set => SetValue(CaretBrushProperty, value);
}
+ ///
+ public TimeSpan CaretBlinkInterval
+ {
+ get => GetValue(CaretBlinkIntervalProperty);
+ set => SetValue(CaretBlinkIntervalProperty, value);
+ }
+
///
/// Gets or sets the starting position of the text selected in the TextBox
///
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 6898879b32..5026896d41 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -547,6 +547,7 @@ namespace Avalonia.Controls
?? new NoopStorageProvider();
public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature();
+ public IInputPane? InputPane => PlatformImpl?.TryGetFeature();
///
/// Gets the platform's clipboard implementation
diff --git a/src/Avalonia.Controls/Utils/RealizedStackElements.cs b/src/Avalonia.Controls/Utils/RealizedStackElements.cs
index 11bbaa11c4..b8f088f090 100644
--- a/src/Avalonia.Controls/Utils/RealizedStackElements.cs
+++ b/src/Avalonia.Controls/Utils/RealizedStackElements.cs
@@ -281,19 +281,20 @@ namespace Avalonia.Controls.Utils
// elements after the insertion point.
var elementCount = _elements.Count;
var start = Math.Max(realizedIndex, 0);
- var newIndex = realizedIndex + count;
for (var i = start; i < elementCount; ++i)
{
- if (_elements[i] is Control element)
- updateElementIndex(element, newIndex - count, newIndex);
- ++newIndex;
+ if (_elements[i] is not Control element)
+ continue;
+ var oldIndex = i + first;
+ updateElementIndex(element, oldIndex, oldIndex + count);
}
if (realizedIndex < 0)
{
// The insertion point was before the first element, update the first index.
_firstIndex += count;
+ _startUUnstable = true;
}
else
{
@@ -340,7 +341,7 @@ namespace Avalonia.Controls.Utils
for (var i = 0; i < _elements.Count; ++i)
{
if (_elements[i] is Control element)
- updateElementIndex(element, newIndex - count, newIndex);
+ updateElementIndex(element, newIndex + count, newIndex);
++newIndex;
}
}
@@ -383,6 +384,37 @@ namespace Avalonia.Controls.Utils
}
}
+ ///
+ /// Updates the elements in response to items being replaced in the source collection.
+ ///
+ /// The index in the source collection of the remove.
+ /// The number of items removed.
+ /// A method used to recycle elements.
+ public void ItemsReplaced(int index, int count, Action recycleElement)
+ {
+ if (index < 0)
+ throw new ArgumentOutOfRangeException(nameof(index));
+ if (_elements is null || _elements.Count == 0)
+ return;
+
+ // Get the index within the realized _elements collection.
+ var startIndex = index - FirstIndex;
+ var endIndex = Math.Min(startIndex + count, Count);
+
+ if (startIndex >= 0 && endIndex > startIndex)
+ {
+ for (var i = startIndex; i < endIndex; ++i)
+ {
+ if (_elements[i] is { } element)
+ {
+ recycleElement(element);
+ _elements[i] = null;
+ _sizes![i] = double.NaN;
+ }
+ }
+ }
+ }
+
///
/// Recycles all elements in response to the source collection being reset.
///
diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs
index d1fb4b8269..d234bcbd0d 100644
--- a/src/Avalonia.Controls/VirtualizingStackPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs
@@ -253,6 +253,8 @@ namespace Avalonia.Controls
_realizedElements.ItemsRemoved(e.OldStartingIndex, e.OldItems!.Count, _updateElementIndex, _recycleElementOnItemRemoved);
break;
case NotifyCollectionChangedAction.Replace:
+ _realizedElements.ItemsReplaced(e.OldStartingIndex, e.OldItems!.Count, _recycleElementOnItemRemoved);
+ break;
case NotifyCollectionChangedAction.Move:
_realizedElements.ItemsRemoved(e.OldStartingIndex, e.OldItems!.Count, _updateElementIndex, _recycleElementOnItemRemoved);
_realizedElements.ItemsInserted(e.NewStartingIndex, e.NewItems!.Count, _updateElementIndex);
@@ -263,6 +265,16 @@ namespace Avalonia.Controls
}
}
+ protected override void OnItemsControlChanged(ItemsControl? oldValue)
+ {
+ base.OnItemsControlChanged(oldValue);
+
+ if (oldValue is not null)
+ oldValue.PropertyChanged -= OnItemsControlPropertyChanged;
+ if (ItemsControl is not null)
+ ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
+ }
+
protected override IInputElement? GetControl(NavigationDirection direction, IInputElement? from, bool wrap)
{
var count = Items.Count;
@@ -376,7 +388,7 @@ namespace Avalonia.Controls
var scrollToElement = GetOrCreateElement(items, index);
scrollToElement.Measure(Size.Infinity);
- // Get the expected position of the elment and put it in place.
+ // Get the expected position of the element and put it in place.
var anchorU = _realizedElements.GetOrEstimateElementU(index, ref _lastEstimatedElementSizeU);
var rect = Orientation == Orientation.Horizontal ?
new Rect(anchorU, 0, scrollToElement.DesiredSize.Width, scrollToElement.DesiredSize.Height) :
@@ -659,6 +671,7 @@ namespace Avalonia.Controls
private void RecycleElement(Control element, int index)
{
+ Debug.Assert(ItemsControl is not null);
Debug.Assert(ItemContainerGenerator is not null);
_scrollAnchorProvider?.UnregisterAnchorCandidate(element);
@@ -673,11 +686,10 @@ namespace Avalonia.Controls
{
element.IsVisible = false;
}
- else if (element.IsKeyboardFocusWithin)
+ else if (KeyboardNavigation.GetTabOnceActiveElement(ItemsControl) == element)
{
_focusedElement = element;
_focusedIndex = index;
- _focusedElement.LostFocus += OnUnrealizedFocusedElementLostFocus;
}
else
{
@@ -744,15 +756,17 @@ namespace Avalonia.Controls
}
}
- private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs e)
+ private void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
- if (_focusedElement is null || sender != _focusedElement)
- return;
-
- _focusedElement.LostFocus -= OnUnrealizedFocusedElementLostFocus;
- RecycleElement(_focusedElement, _focusedIndex);
- _focusedElement = null;
- _focusedIndex = -1;
+ if (_focusedElement is not null &&
+ e.Property == KeyboardNavigation.TabOnceActiveElementProperty &&
+ e.GetOldValue() == _focusedElement)
+ {
+ // TabOnceActiveElement has moved away from _focusedElement so we can recycle it.
+ RecycleElement(_focusedElement, _focusedIndex);
+ _focusedElement = null;
+ _focusedIndex = -1;
+ }
}
///
diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs
index 3b0b3bd028..be862a3c52 100644
--- a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs
+++ b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs
@@ -16,7 +16,7 @@ internal class OpenGlControlBaseResources : IAsyncDisposable
public CompositionDrawingSurface Surface { get; }
private readonly CompositionOpenGlSwapchain _swapchain;
public IGlContext Context { get; private set; }
-
+
public static OpenGlControlBaseResources? TryCreate(CompositionDrawingSurface surface,
ICompositionGpuInterop interop,
IOpenGlTextureSharingRenderInterfaceContextFeature feature)
@@ -54,7 +54,7 @@ internal class OpenGlControlBaseResources : IAsyncDisposable
return new OpenGlControlBaseResources(context, surface, interop, null, externalObjects);
}
- public OpenGlControlBaseResources(IGlContext context,
+ private OpenGlControlBaseResources(IGlContext context,
CompositionDrawingSurface surface,
ICompositionGpuInterop interop,
IOpenGlTextureSharingRenderInterfaceContextFeature? feature,
@@ -161,7 +161,9 @@ internal class OpenGlControlBaseResources : IAsyncDisposable
}
Surface.Dispose();
+
await _swapchain.DisposeAsync();
+ Context.Dispose();
Context = null!;
}
diff --git a/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml b/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml
index 228aeaad17..083d90b010 100644
--- a/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml
@@ -42,6 +42,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
+ CaretIndex="{TemplateBinding CaretIndex, Mode=TwoWay}"
FontSize="{TemplateBinding FontSize}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
index adaecc71ab..b725fd67f8 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
@@ -115,7 +115,7 @@
-
-
+
+