Browse Source

Merge branch 'master' into refactor/bindings

pull/13970/head
Steven Kirk 3 years ago
parent
commit
0cb33dbd78
  1. 4
      packages/Avalonia/AvaloniaBuildTasks.props
  2. 53
      samples/SafeAreaDemo/ViewModels/MainViewModel.cs
  3. 20
      samples/SafeAreaDemo/Views/MainView.xaml
  4. 3
      samples/SafeAreaDemo/Views/MainView.xaml.cs
  5. 183
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  6. 13
      src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs
  7. 5
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  8. 6
      src/Avalonia.Base/Animation/Animatable.cs
  9. 15
      src/Avalonia.Base/Animation/Animation.cs
  10. 16
      src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
  11. 149
      src/Avalonia.Base/Animation/Animators/Animator`1.cs
  12. 16
      src/Avalonia.Base/Animation/Animators/BaseBrushAnimator.cs
  13. 16
      src/Avalonia.Base/Input/PointerOverPreProcessor.cs
  14. 2
      src/Avalonia.Base/Media/DrawingContext.cs
  15. 4
      src/Avalonia.Base/Media/Effects/EffectAnimator.cs
  16. 2
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  17. 3
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  18. 15
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  19. 7
      src/Avalonia.Base/PropertyStore/IValueEntry.cs
  20. 2
      src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
  21. 2
      src/Avalonia.Base/PropertyStore/ValueFrame.cs
  22. 6
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  23. 34
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs
  24. 59
      src/Avalonia.Base/RoundedRect.cs
  25. 24
      src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
  26. 2
      src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
  27. 2
      src/Avalonia.Base/Styling/Setter.cs
  28. 47
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  29. 6
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  30. 10
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  31. 20
      src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
  32. 8
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  33. 1
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  34. 18
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs
  35. 11
      src/Avalonia.Controls/ItemsControl.cs
  36. 4
      src/Avalonia.Controls/ListBoxItem.cs
  37. 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  38. 89
      src/Avalonia.Controls/Platform/IInputPane.cs
  39. 78
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  40. 21
      src/Avalonia.Controls/Primitives/RangeBase.cs
  41. 3
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  42. 74
      src/Avalonia.Controls/SelectableTextBlock.cs
  43. 8
      src/Avalonia.Controls/TabControl.cs
  44. 23
      src/Avalonia.Controls/TextBlock.cs
  45. 13
      src/Avalonia.Controls/TextBox.cs
  46. 1
      src/Avalonia.Controls/TopLevel.cs
  47. 42
      src/Avalonia.Controls/Utils/RealizedStackElements.cs
  48. 36
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  49. 6
      src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs
  50. 1
      src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml
  51. 9
      src/Avalonia.Themes.Fluent/Controls/Expander.xaml
  52. 1
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  53. 2
      src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
  54. 1
      src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml
  55. 1
      src/Avalonia.Themes.Simple/Controls/TextBox.xaml
  56. 2
      src/Avalonia.Themes.Simple/Controls/TimePicker.xaml
  57. 3
      src/Browser/Avalonia.Browser/Avalonia.Browser.props
  58. 2
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  59. 37
      src/Browser/Avalonia.Browser/BrowserInputPane.cs
  60. 10
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  61. 4
      src/Browser/Avalonia.Browser/Interop/InputHelper.cs
  62. 18
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts
  63. 22
      src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
  64. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  65. 77
      src/Windows/Avalonia.Win32/Input/WindowsInputPane.cs
  66. 5
      src/Windows/Avalonia.Win32/Interop/TaskBarList.cs
  67. 14
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  68. 16
      src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs
  69. 16
      src/Windows/Avalonia.Win32/Win32Com/win32.idl
  70. 2
      src/Windows/Avalonia.Win32/Win32StorageProvider.cs
  71. 1
      src/Windows/Avalonia.Win32/WinRT/winrt.idl
  72. 1
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  73. 10
      src/Windows/Avalonia.Win32/WindowImpl.cs
  74. 5
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  75. 54
      src/iOS/Avalonia.iOS/UIKitInputPane.cs
  76. 8
      tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs
  77. 140
      tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
  78. 4
      tests/Avalonia.Base.UnitTests/Animation/SpringTests.cs
  79. 35
      tests/Avalonia.Base.UnitTests/RoundedRectTests.cs
  80. 17
      tests/Avalonia.Base.UnitTests/Styling/TestSelectors.cs
  81. 42
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  82. 52
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  83. 34
      tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
  84. 19
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  85. 2
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  86. 77
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  87. 213
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

4
packages/Avalonia/AvaloniaBuildTasks.props

@ -5,7 +5,7 @@
<PropertyPageSchema Include="$(MSBuildThisFileDirectory)AvaloniaItemSchema.xaml" />
</ItemGroup>
<ItemGroup Condition="'$(EnableDefaultItems)'=='True'">
<AvaloniaXaml Include="**\*.axaml" SubType="Designer" />
<AvaloniaXaml Include="**\*.paml" SubType="Designer" />
<AvaloniaXaml Include="**\*.axaml" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" SubType="Designer" />
<AvaloniaXaml Include="**\*.paml" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" SubType="Designer" />
</ItemGroup>
</Project>

53
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));
}
}
}

20
samples/SafeAreaDemo/Views/MainView.xaml

@ -11,10 +11,11 @@
Background="#ccc"
TopLevel.AutoSafeAreaPadding="{Binding AutoSafeAreaPadding, Mode=TwoWay}">
<Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border BorderBrush="Red"
Margin="{Binding ViewPadding}"
BorderThickness="1">
VerticalAlignment="Stretch"
Bounds="{Binding CanvasSize, Mode=OneWayToSource}">
<Border BorderBrush="Red"
Margin="{Binding ViewPadding}"
BorderThickness="1">
<Grid>
<Label Margin="5"
Foreground="Red"
@ -51,5 +52,16 @@
</Grid>
</DockPanel>
</Border>
<Button Margin="{Binding InputPaneMarkerMargin}"
VerticalAlignment="Bottom"
Content="X">
<Button.Transitions>
<Transitions>
<ThicknessTransition Property="Margin"
Duration="{Binding InputPaneDuration}"
Easing="{Binding InputPaneEasing}"/>
</Transitions>
</Button.Transitions>
</Button>
</Grid>
</UserControl>

3
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;
}
}

183
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<SafeAreaChangedArgs> SafeAreaChanged;
public event EventHandler<InputPaneStateEventArgs> 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<WindowInsetsAnimationCompat> 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<WindowInsetsAnimationCompat> 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);
}
}
}

13
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<RoutedEventArgs>? 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;
}
}
}
}

5
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -45,7 +45,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly AndroidInputMethod<ViewImpl> _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;
}

6
src/Avalonia.Base/Animation/Animatable.cs

@ -17,8 +17,8 @@ namespace Avalonia.Animation
/// <summary>
/// Defines the <see cref="Clock"/> property.
/// </summary>
internal static readonly StyledProperty<IClock> ClockProperty =
AvaloniaProperty.Register<Animatable, IClock>(nameof(Clock), inherits: true);
internal static readonly StyledProperty<IClock?> ClockProperty =
AvaloniaProperty.Register<Animatable, IClock?>(nameof(Clock), inherits: true);
/// <summary>
/// Defines the <see cref="Transitions"/> property.
@ -36,7 +36,7 @@ namespace Avalonia.Animation
/// <summary>
/// Gets or sets the clock which controls the animations on the control.
/// </summary>
internal IClock Clock
internal IClock? Clock
{
get => GetValue(ClockProperty);
set => SetValue(ClockProperty, value);

15
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<IAnimator>();
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);
}

16
src/Avalonia.Base/Animation/AnimatorKeyFrame.cs

@ -16,19 +16,6 @@ namespace Avalonia.Animation
public static readonly DirectProperty<AnimatorKeyFrame, object?> ValueProperty =
AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object?>(nameof(Value), k => k.Value, (k, v) => k.Value = v);
public AnimatorKeyFrame()
{
}
public AnimatorKeyFrame(Type? animatorType, Func<IAnimator>? animatorFactory, Cue cue)
{
AnimatorType = animatorType;
AnimatorFactory = animatorFactory;
Cue = cue;
KeySpline = null;
}
public AnimatorKeyFrame(Type? animatorType, Func<IAnimator>? 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<IAnimator>? 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;

149
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
/// </summary>
internal abstract class Animator<T> : AvaloniaList<AnimatorKeyFrame>, IAnimator
{
/// <summary>
/// List of type-converted keyframes.
/// </summary>
private readonly List<AnimatorKeyFrame> _convertedKeyframes = new List<AnimatorKeyFrame>();
private bool _isVerifiedAndConverted;
/// <summary>
/// Gets or sets the target property for the keyframe.
/// </summary>
public AvaloniaProperty? Property { get; set; }
public Animator()
{
// Invalidate keyframes when changed.
this.CollectionChanged += delegate { _isVerifiedAndConverted = false; };
}
/// <inheritdoc/>
public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete)
{
if (!_isVerifiedAndConverted)
VerifyConvertKeyFrames();
var subject = new DisposeAnimationInstanceSubject<T>(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<T> 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.
/// </summary>
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 });
}
}
}
}

16
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
});
}

16
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)
{

2
src/Avalonia.Base/Media/DrawingContext.cs

@ -123,7 +123,7 @@ namespace Avalonia.Media
/// <param name="boxShadows">Box shadow effect parameters</param>
/// <remarks>
/// 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.
/// </remarks>
public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect,
double radiusX = 0, double radiusY = 0,

4
src/Avalonia.Base/Media/Effects/EffectAnimator.cs

@ -38,7 +38,9 @@ internal class EffectAnimator : Animator<IEffect?>
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

2
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@ -48,7 +48,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>
/// The created text style run.
/// </returns>
private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySpan<char> text, int firstTextSourceIndex,
internal static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySpan<char> text, int firstTextSourceIndex,
TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
if (textModifier == null || textModifier.Count == 0)

3
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)
{

15
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);

7
src/Avalonia.Base/PropertyStore/IValueEntry.cs

@ -8,13 +8,16 @@ namespace Avalonia.PropertyStore
/// </summary>
internal interface IValueEntry
{
bool HasValue { get; }
/// <summary>
/// Gets the property that this value applies to.
/// </summary>
AvaloniaProperty Property { get; }
/// <summary>
/// Checks whether the entry has a value, starting the entry if necessary.
/// </summary>
bool HasValue();
/// <summary>
/// Gets the value associated with the entry.
/// </summary>

2
src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs

@ -19,13 +19,13 @@ namespace Avalonia.PropertyStore
}
public StyledProperty<T> 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<T>.GetValue() => _value;

2
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; }

6
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)

34
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);
}
}

59
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
/// </summary>
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);
}
/// <summary>
/// Determines whether a point is in the bounds of the rounded rectangle, exclusive of the
/// rounded rectangle's bottom/right edge.
/// </summary>
/// <param name="p">The point.</param>
/// <returns>true if the point is in the bounds of the rounded rectangle; otherwise false.</returns>
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;
}
}
}

24
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;
}

2
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)

2
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)

47
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));
}
}
}
/// <summary>
@ -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);
/// <summary>

6
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -1828,12 +1828,6 @@ namespace Avalonia.Controls
private set;
}
internal bool UpdatedStateOnMouseLeftButtonDown
{
get;
set;
}
/// <summary>
/// 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

10
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();
}

20
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;

8
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);
}
}
}

1
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)

18
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.Properties.cs

@ -15,6 +15,15 @@ namespace Avalonia.Controls
{
public partial class AutoCompleteBox
{
/// <summary>
/// Defines see <see cref="TextBox.CaretIndex"/> property.
/// </summary>
public static readonly StyledProperty<int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<AutoCompleteBox>(new(
defaultValue: 0,
defaultBindingMode:BindingMode.TwoWay,
coerce: TextBox.CoerceCaretIndex));
public static readonly StyledProperty<string?> WatermarkProperty =
TextBox.WatermarkProperty.AddOwner<AutoCompleteBox>();
@ -158,6 +167,15 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<AutoCompleteBox, Func<string?, CancellationToken, Task<IEnumerable<object>>>?>(
nameof(AsyncPopulator));
/// <summary>
/// Gets or sets the caret index
/// </summary>
public int CaretIndex
{
get => GetValue(CaretIndexProperty);
set => SetValue(CaretIndexProperty, value);
}
/// <summary>
/// Gets or sets the minimum number of characters required to be entered
/// in the text box before the <see cref="AutoCompleteBox" /> displays possible matches.

11
src/Avalonia.Controls/ItemsControl.cs

@ -528,6 +528,17 @@ namespace Avalonia.Controls
_itemsPresenter = e.NameScope.Find<ItemsPresenter>("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);
}
/// <summary>
/// Handles directional navigation within the <see cref="ItemsControl"/>.
/// </summary>

4
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;
}

2
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)
{

89
src/Avalonia.Controls/Platform/IInputPane.cs

@ -0,0 +1,89 @@
using System;
using Avalonia.Animation.Easings;
using Avalonia.Metadata;
namespace Avalonia.Controls.Platform
{
/// <summary>
/// Listener for the platform's input pane(eg, software keyboard). Provides access to the input pane height and state.
/// </summary>
[NotClientImplementable]
public interface IInputPane
{
/// <summary>
/// The current input pane state
/// </summary>
InputPaneState State { get; }
/// <summary>
/// The current input pane bounds.
/// </summary>
Rect OccludedRect { get; }
/// <summary>
/// Occurs when the input pane's state has changed.
/// </summary>
event EventHandler<InputPaneStateEventArgs>? StateChanged;
}
/// <summary>
/// The input pane opened state.
/// </summary>
public enum InputPaneState
{
/// <summary>
/// The input pane is either closed, or doesn't form part of the platform insets, i.e. it's floating or is an overlay.
/// </summary>
Closed,
/// <summary>
/// The input pane is open.
/// </summary>
Open
}
/// <summary>
/// Provides state change information about the input pane.
/// </summary>
public sealed class InputPaneStateEventArgs : EventArgs
{
/// <summary>
/// The new state of the input pane
/// </summary>
public InputPaneState NewState { get; }
/// <summary>
/// The initial bounds of the input pane.
/// </summary>
public Rect? StartRect { get; }
/// <summary>
/// The final bounds of the input pane.
/// </summary>
public Rect EndRect { get; }
/// <summary>
/// The duration of the input pane's state change animation.
/// </summary>
public TimeSpan AnimationDuration { get; }
/// <summary>
/// The easing of the input pane's state changed animation.
/// </summary>
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)
{
}
}
}

78
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<IBrush?> CaretBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush?>(nameof(CaretBrush));
public static readonly StyledProperty<TimeSpan> CaretBlinkIntervalProperty =
TextBox.CaretBlinkIntervalProperty.AddOwner<TextPresenter>();
public static readonly StyledProperty<int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>(new(coerce: TextBox.CoerceCaretIndex));
@ -88,7 +92,7 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<IBrush?> BackgroundProperty =
Border.BackgroundProperty.AddOwner<TextPresenter>();
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<TextPresenter>(CaretBrushProperty, SelectionBrushProperty, TextElement.ForegroundProperty);
AffectsRender<TextPresenter>(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);
}
/// <summary>
/// Gets or sets the caret blink rate
/// </summary>
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):

21
src/Avalonia.Controls/Primitives/RangeBase.cs

@ -10,6 +10,8 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public abstract class RangeBase : TemplatedControl
{
private bool _isDataContextChanging;
/// <summary>
/// Defines the <see cref="Minimum"/> property.
/// </summary>
@ -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);
}
}
/// <inheritdoc />
protected override void OnDataContextBeginUpdate()
{
_isDataContextChanging = true;
base.OnDataContextBeginUpdate();
}
/// <inheritdoc />
protected override void OnDataContextEndUpdate()
{
base.OnDataContextEndUpdate();
_isDataContextChanging = false;
}
/// <summary>
/// Checks if the double value is not infinity nor NaN.

3
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);
}
/// <inheritdoc />

74
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<IBrush?> SelectionBrushProperty =
TextBox.SelectionBrushProperty.AddOwner<SelectableTextBlock>();
public static readonly StyledProperty<IBrush?> SelectionForegroundBrushProperty =
TextBox.SelectionForegroundBrushProperty.AddOwner<SelectableTextBlock>();
public static readonly DirectProperty<SelectableTextBlock, bool> CanCopyProperty =
TextBox.CanCopyProperty.AddOwner<SelectableTextBlock>(o => o.CanCopy);
@ -63,6 +67,15 @@ namespace Avalonia.Controls
set => SetValue(SelectionBrushProperty, value);
}
/// <summary>
/// Gets or sets a brush that is used for the foreground of selected text
/// </summary>
public IBrush? SelectionForegroundBrush
{
get => GetValue(SelectionForegroundBrushProperty);
set => SetValue(SelectionForegroundBrushProperty, value);
}
/// <summary>
/// Gets or sets a character index for the beginning of the current selection.
/// </summary>
@ -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<ValueSpan<TextRunProperties>>? 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<TextRunProperties>(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();
}
}

8
src/Avalonia.Controls/TabControl.cs

@ -73,7 +73,6 @@ namespace Avalonia.Controls
{
SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel);
TabStripPlacementProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateTabStripPlacement());
AffectsMeasure<TabControl>(TabStripPlacementProperty);
SelectedItemProperty.Changed.AddClassHandler<TabControl>((x, e) => x.UpdateSelectedContent());
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<TabControl>(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);

23
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<TextRun>? _textRuns;
protected Size _constraint;
protected IReadOnlyList<TextRun>? _textRuns;
private InlineCollection? _inlines;
/// <summary>
@ -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<TextRun> _textRuns;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier;
public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
public InlinesTextSource(IReadOnlyList<TextRun> textRuns, IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier = null)
{
_textRuns = textRuns;
_textModifier = textModifier;
}
public IReadOnlyList<TextRun> 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();
}
}
}
}
}

13
src/Avalonia.Controls/TextBox.cs

@ -92,6 +92,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush?> CaretBrushProperty =
AvaloniaProperty.Register<TextBox, IBrush?>(nameof(CaretBrush));
/// <summary>
/// Defines the <see cref="CaretBlinkInterval"/> property
/// </summary>
public static readonly StyledProperty<TimeSpan> CaretBlinkIntervalProperty =
AvaloniaProperty.Register<TextBox, TimeSpan>(nameof(CaretBlinkInterval), defaultValue: TimeSpan.FromMilliseconds(500));
/// <summary>
/// Defines the <see cref="SelectionStart"/> property
/// </summary>
@ -443,6 +449,13 @@ namespace Avalonia.Controls
set => SetValue(CaretBrushProperty, value);
}
/// <inheritdoc cref="TextPresenter.CaretBlinkInterval"/>
public TimeSpan CaretBlinkInterval
{
get => GetValue(CaretBlinkIntervalProperty);
set => SetValue(CaretBlinkIntervalProperty, value);
}
/// <summary>
/// Gets or sets the starting position of the text selected in the TextBox
/// </summary>

1
src/Avalonia.Controls/TopLevel.cs

@ -547,6 +547,7 @@ namespace Avalonia.Controls
?? new NoopStorageProvider();
public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature<IInsetsManager>();
public IInputPane? InputPane => PlatformImpl?.TryGetFeature<IInputPane>();
/// <summary>
/// Gets the platform's clipboard implementation

42
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
}
}
/// <summary>
/// Updates the elements in response to items being replaced in the source collection.
/// </summary>
/// <param name="index">The index in the source collection of the remove.</param>
/// <param name="count">The number of items removed.</param>
/// <param name="recycleElement">A method used to recycle elements.</param>
public void ItemsReplaced(int index, int count, Action<Control> 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;
}
}
}
}
/// <summary>
/// Recycles all elements in response to the source collection being reset.
/// </summary>

36
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<IInputElement?>() == _focusedElement)
{
// TabOnceActiveElement has moved away from _focusedElement so we can recycle it.
RecycleElement(_focusedElement, _focusedIndex);
_focusedElement = null;
_focusedIndex = -1;
}
}
/// <inheritdoc/>

6
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!;
}

1
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}"

9
src/Avalonia.Themes.Fluent/Controls/Expander.xaml

@ -115,7 +115,7 @@
</ControlTemplate>
</Setter>
<Style Selector="^:checked /template/ Path#ExpandCollapseChevron">
<Style Selector="^[Tag=expanded] /template/ Path#ExpandCollapseChevron">
<Style.Animations>
<Animation FillMode="Both" Duration="0:0:0.0625">
<KeyFrame Cue="100%">
@ -125,7 +125,7 @@
</Style.Animations>
</Style>
<Style Selector="^:not(:checked) /template/ Path#ExpandCollapseChevron">
<Style Selector="^[Tag=collapsed] /template/ Path#ExpandCollapseChevron">
<Style.Animations>
<Animation FillMode="Both" Duration="0:0:0.0625">
<KeyFrame Cue="0%">
@ -262,8 +262,13 @@
<Setter Property="VerticalAlignment" Value="Stretch" />
</Style>
<Style Selector="^:expanded /template/ ToggleButton#ExpanderHeader">
<Setter Property="Tag" Value="expanded" />
</Style>
<Style Selector="^:not(:expanded) /template/ ToggleButton#ExpanderHeader">
<Setter Property="CornerRadius" Value="{Binding $parent[Expander].CornerRadius}" />
<Setter Property="Tag" Value="collapsed" />
</Style>
<Style Selector="^:expanded:up /template/ ToggleButton#ExpanderHeader">
<Setter Property="CornerRadius" Value="{TemplateBinding CornerRadius, Converter={StaticResource BottomCornerRadiusFilterConverter}}" />

1
src/Avalonia.Themes.Fluent/Controls/TextBox.xaml

@ -155,6 +155,7 @@
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
<TextPresenter Name="PART_TextPresenter"
Text="{TemplateBinding Text, Mode=TwoWay}"
CaretBlinkInterval="{TemplateBinding CaretBlinkInterval}"
CaretIndex="{TemplateBinding CaretIndex}"
SelectionStart="{TemplateBinding SelectionStart}"
SelectionEnd="{TemplateBinding SelectionEnd}"

2
src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml

@ -94,7 +94,7 @@
MinWidth="{DynamicResource TimePickerThemeMinWidth}"
MaxWidth="{DynamicResource TimePickerThemeMaxWidth}"
HorizontalAlignment="Stretch"
VerticalAlignment="Top">
VerticalAlignment="Stretch">
<Grid Name="PART_FlyoutButtonContentGrid">
<!--Ignore col defs here, set in code-->

1
src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml

@ -16,6 +16,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
CaretIndex="{TemplateBinding CaretIndex, Mode=TwoWay}"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
Watermark="{TemplateBinding Watermark}" />

1
src/Avalonia.Themes.Simple/Controls/TextBox.xaml

@ -143,6 +143,7 @@
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}" />
<TextPresenter Name="PART_TextPresenter"
CaretBlinkInterval="{TemplateBinding CaretBlinkInterval}"
CaretBrush="{TemplateBinding CaretBrush}"
CaretIndex="{TemplateBinding CaretIndex}"
LineHeight="{TemplateBinding LineHeight}"

2
src/Avalonia.Themes.Simple/Controls/TimePicker.xaml

@ -94,7 +94,7 @@
MinWidth="{DynamicResource TimePickerThemeMinWidth}"
MaxWidth="{DynamicResource TimePickerThemeMaxWidth}"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
VerticalAlignment="Stretch"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"

3
src/Browser/Avalonia.Browser/Avalonia.Browser.props

@ -3,5 +3,8 @@
<ShouldIncludeAvaloniaJavaScript Condition=" '$(ShouldIncludeAvaloniaJavaScript)' == '' ">True</ShouldIncludeAvaloniaJavaScript>
<ShouldIncludeNativeSkiaSharp Condition=" '$(ShouldIncludeNativeSkiaSharp)' == '' ">True</ShouldIncludeNativeSkiaSharp>
<ShouldIncludeNativeHarfBuzzSharp Condition=" '$(ShouldIncludeNativeHarfBuzzSharp)' == '' ">True</ShouldIncludeNativeHarfBuzzSharp>
<!-- .NET 8 changes default location for dotnet script to the "_framework" subfolder, -->
<!-- But this change would introduce a breaking change to Avalonia users, so we revert it back to the .NET 7 state. -->
<WasmRuntimeAssetsLocation Condition="'$(WasmRuntimeAssetsLocation)' == ''">./</WasmRuntimeAssetsLocation>
</PropertyGroup>
</Project>

2
src/Browser/Avalonia.Browser/AvaloniaView.cs

@ -70,7 +70,7 @@ namespace Avalonia.Browser
_splash = DomHelper.GetElementById("avalonia-splash");
_topLevelImpl = new BrowserTopLevelImpl(this);
_topLevelImpl = new BrowserTopLevelImpl(this, _containerElement);
_topLevel = new WebEmbeddableControlRoot(_topLevelImpl, () =>
{

37
src/Browser/Avalonia.Browser/BrowserInputPane.cs

@ -0,0 +1,37 @@
using System;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Browser.Interop;
using Avalonia.Controls.Platform;
namespace Avalonia.Browser;
internal class BrowserInputPane : IInputPane
{
public BrowserInputPane(JSObject container)
{
InputHelper.SubscribeKeyboardGeometryChange(container, OnGeometryChange);
}
public InputPaneState State { get; private set; }
public Rect OccludedRect { get; private set; }
public event EventHandler<InputPaneStateEventArgs>? StateChanged;
private bool OnGeometryChange(JSObject args)
{
var oldState = (OccludedRect, State);
OccludedRect = new Rect(
args.GetPropertyAsDouble("x"),
args.GetPropertyAsDouble("y"),
args.GetPropertyAsDouble("width"),
args.GetPropertyAsDouble("height"));
State = OccludedRect.Width != 0 ? InputPaneState.Open : InputPaneState.Closed;
if (oldState != (OccludedRect, State))
{
StateChanged?.Invoke(this, new InputPaneStateEventArgs(State, null, OccludedRect));
}
return true;
}
}

10
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
using Avalonia.Browser.Skia;
using Avalonia.Browser.Storage;
@ -33,8 +34,9 @@ namespace Avalonia.Browser
private readonly ISystemNavigationManagerImpl _systemNavigationManager;
private readonly ClipboardImpl _clipboard;
private readonly IInsetsManager? _insetsManager;
private readonly IInputPane _inputPane;
public BrowserTopLevelImpl(AvaloniaView avaloniaView)
public BrowserTopLevelImpl(AvaloniaView avaloniaView, JSObject container)
{
Surfaces = Enumerable.Empty<object>();
_avaloniaView = avaloniaView;
@ -47,6 +49,7 @@ namespace Avalonia.Browser
_storageProvider = new BrowserStorageProvider();
_systemNavigationManager = new BrowserSystemNavigationManagerImpl();
_clipboard = new ClipboardImpl();
_inputPane = new BrowserInputPane(container);
}
public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds;
@ -273,6 +276,11 @@ namespace Avalonia.Browser
{
return _clipboard;
}
if (featureType == typeof(IInputPane))
{
return _inputPane;
}
return null;
}

4
src/Browser/Avalonia.Browser/Interop/InputHelper.cs

@ -51,6 +51,10 @@ internal static partial class InputHelper
public static partial void SubscribeDropEvents(JSObject containerElement,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] Func<JSObject, bool> dragEvent);
[JSImport("InputHelper.subscribeKeyboardGeometryChange", AvaloniaModule.MainModuleName)]
public static partial void SubscribeKeyboardGeometryChange(JSObject containerElement,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] Func<JSObject, bool> handler);
[JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)]
[return: JSMarshalAs<JSType.Array<JSType.Object>>]
public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent);

18
src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts

@ -245,6 +245,24 @@ export class InputHelper {
return pointerEvent.getCoalescedEvents();
}
public static subscribeKeyboardGeometryChange(
element: HTMLInputElement,
handler: (args: any) => boolean) {
if ("virtualKeyboard" in navigator) {
// (navigator as any).virtualKeyboard.overlaysContent = true;
(navigator as any).virtualKeyboard.addEventListener("geometrychange", (event: any) => {
const elementRect = element.getBoundingClientRect();
const keyboardRect = event.target.boundingRect as DOMRect;
handler({
x: keyboardRect.x - elementRect.x,
y: keyboardRect.y - elementRect.y,
width: keyboardRect.width,
height: keyboardRect.height
});
});
}
}
public static clearInput(inputElement: HTMLInputElement) {
inputElement.value = "";
}

22
src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs

@ -41,7 +41,7 @@ public static class HeadlessWindowExtensions
}
/// <summary>
/// Simulates keyboard press on the headless window/toplevel.
/// Simulates a keyboard press on the headless window/toplevel.
/// </summary>
[Obsolete("Use the overload that takes a physical key and key symbol instead, or KeyPressQwerty alternatively.")]
public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
@ -61,7 +61,7 @@ public static class HeadlessWindowExtensions
RunJobsOnImpl(topLevel, w => w.KeyPress(physicalKey.ToQwertyKey(), modifiers, physicalKey, physicalKey.ToQwertyKeySymbol()));
/// <summary>
/// Simulates keyboard release on the headless window/toplevel.
/// Simulates a keyboard release on the headless window/toplevel.
/// </summary>
[Obsolete("Use the overload that takes a physical key and key symbol instead, or KeyReleaseQwerty alternatively.")]
public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
@ -81,41 +81,47 @@ public static class HeadlessWindowExtensions
RunJobsOnImpl(topLevel, w => w.KeyRelease(physicalKey.ToQwertyKey(), modifiers, physicalKey, physicalKey.ToQwertyKeySymbol()));
/// <summary>
/// Simulates a text input event on the headless window/toplevel
/// Simulates a text input event on the headless window/toplevel.
/// </summary>
/// <remarks>
/// This event is independent of KeyPress and KeyRelease. If you need to simulate text input to a TextBox or a similar control, please use KeyTextInput.
/// </remarks>
public static void KeyTextInput(this TopLevel topLevel, string text) =>
RunJobsOnImpl(topLevel, w => w.TextInput(text));
/// <summary>
/// Simulates mouse down on the headless window/toplevel.
/// Simulates a mouse down on the headless window/toplevel.
/// </summary>
/// <remarks>
/// In the headless platform, there is a single mouse pointer. There are no helper methods for touch or pen input.
/// </remarks>
public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button,
RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsOnImpl(topLevel, w => w.MouseDown(point, button, modifiers));
/// <summary>
/// Simulates mouse move on the headless window/toplevel.
/// Simulates a mouse move on the headless window/toplevel.
/// </summary>
public static void MouseMove(this TopLevel topLevel, Point point,
RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsOnImpl(topLevel, w => w.MouseMove(point, modifiers));
/// <summary>
/// Simulates mouse up on the headless window/toplevel.
/// Simulates a mouse up on the headless window/toplevel.
/// </summary>
public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button,
RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsOnImpl(topLevel, w => w.MouseUp(point, button, modifiers));
/// <summary>
/// Simulates mouse wheel on the headless window/toplevel.
/// Simulates a mouse wheel on the headless window/toplevel.
/// </summary>
public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta,
RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsOnImpl(topLevel, w => w.MouseWheel(point, delta, modifiers));
/// <summary>
/// Simulates drag'n'drop target on the headless window/toplevel.
/// Simulates a drag and drop target event on the headless window/toplevel. This event simulates a user moving files from another app to the current app.
/// </summary>
public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data,
DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) =>

2
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@ -1 +1 @@
Subproject commit b7ed273273949a5dd9f01e682ab97f61b43697ad
Subproject commit 320e57fa1c95d10de33d9a96a323d1bc27563fa1

77
src/Windows/Avalonia.Win32/Input/WindowsInputPane.cs

@ -0,0 +1,77 @@
using System;
using Avalonia.Controls.Platform;
using Avalonia.MicroCom;
using Avalonia.Win32.Interop;
using Avalonia.Win32.Win32Com;
namespace Avalonia.Win32.Input;
internal unsafe class WindowsInputPane : IInputPane, IDisposable
{
// GUID: D5120AA3-46BA-44C5-822D-CA8092C1FC72
private static readonly Guid CLSID_FrameworkInputPane = new(0xD5120AA3, 0x46BA, 0x44C5, 0x82, 0x2D, 0xCA, 0x80, 0x92, 0xC1, 0xFC, 0x72);
// GUID: 5752238B-24F0-495A-82F1-2FD593056796
private static readonly Guid SID_IFrameworkInputPane = new(0x5752238B, 0x24F0, 0x495A, 0x82, 0xF1, 0x2F, 0xD5, 0x93, 0x05, 0x67, 0x96);
private readonly WindowImpl _windowImpl;
private readonly IFrameworkInputPane _inputPane;
private readonly uint _cookie;
public WindowsInputPane(WindowImpl windowImpl)
{
_windowImpl = windowImpl;
_inputPane = UnmanagedMethods.CreateInstance<IFrameworkInputPane>(in CLSID_FrameworkInputPane, in SID_IFrameworkInputPane);
using (var handler = new Handler(this))
{
uint cookie = 0;
_inputPane.AdviseWithHWND(windowImpl.Handle.Handle, handler, &cookie);
_cookie = cookie;
}
}
public InputPaneState State { get; private set; }
public Rect OccludedRect { get; private set; }
public event EventHandler<InputPaneStateEventArgs>? StateChanged;
private void OnStateChanged(bool showing, UnmanagedMethods.RECT? prcInputPaneScreenLocation)
{
var oldState = (OccludedRect, State);
OccludedRect = prcInputPaneScreenLocation.HasValue
? ScreenRectToClient(prcInputPaneScreenLocation.Value)
: default;
State = showing ? InputPaneState.Open : InputPaneState.Closed;
if (oldState != (OccludedRect, State))
{
StateChanged?.Invoke(this, new InputPaneStateEventArgs(State, null, OccludedRect));
}
}
private Rect ScreenRectToClient(UnmanagedMethods.RECT screenRect)
{
var position = new PixelPoint(screenRect.left, screenRect.top);
var size = new PixelSize(screenRect.Width, screenRect.Height);
return new Rect(_windowImpl.PointToClient(position), size.ToSize(_windowImpl.DesktopScaling));
}
public void Dispose()
{
if (_cookie != 0)
{
_inputPane.Unadvise(_cookie);
}
_inputPane.Dispose();
}
private class Handler : CallbackBase, IFrameworkInputPaneHandler
{
private readonly WindowsInputPane _pane;
public Handler(WindowsInputPane pane) => _pane = pane;
public void Showing(UnmanagedMethods.RECT* rect, int _) => _pane.OnStateChanged(true, *rect);
public void Hiding(int fEnsureFocusedElementInView) => _pane.OnStateChanged(false, null);
}
}

5
src/Windows/Avalonia.Win32/Interop/TaskBarList.cs

@ -19,10 +19,7 @@ namespace Avalonia.Win32.Interop
{
if (s_taskBarList == IntPtr.Zero)
{
Guid clsid = ShellIds.TaskBarList;
Guid iid = ShellIds.ITaskBarList2;
int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out s_taskBarList);
int result = CoCreateInstance(in ShellIds.TaskBarList, IntPtr.Zero, 1, in ShellIds.ITaskBarList2, out s_taskBarList);
if (s_taskBarList != IntPtr.Zero)
{

14
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -1481,18 +1481,14 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll", EntryPoint = "SetCursor")]
internal static extern IntPtr SetCursor(IntPtr hCursor);
[DllImport("ole32.dll", PreserveSig = true)]
internal static extern int CoCreateInstance(ref Guid clsid,
IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter);
[DllImport("ole32.dll", PreserveSig = true)]
internal static extern int CoCreateInstance(ref Guid clsid,
IntPtr ignore1, int ignore2, ref Guid iid, [Out] out IntPtr pUnkOuter);
internal static extern int CoCreateInstance(in Guid clsid,
IntPtr ignore1, int ignore2, in Guid iid, [Out] out IntPtr pUnkOuter);
internal static T CreateInstance<T>(ref Guid clsid, ref Guid iid) where T : IUnknown
internal static T CreateInstance<T>(in Guid clsid, in Guid iid) where T : IUnknown
{
var hresult = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out IntPtr pUnk);
var hresult = CoCreateInstance(in clsid, IntPtr.Zero, 1, in iid, out IntPtr pUnk);
if (hresult != 0)
{
throw new COMException("CreateInstance", hresult);

16
src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs

@ -46,8 +46,7 @@ namespace Avalonia.Win32.OpenGl.Angle
}, AngleOptions.PlatformApi.DirectX11);
}
public static unsafe AngleWin32EglDisplay CreateD3D11Display(Win32AngleEglInterface egl,
bool preferDiscreteAdapter = false)
public static unsafe AngleWin32EglDisplay CreateD3D11Display(Win32AngleEglInterface egl)
{
var featureLevels = new[]
{
@ -65,7 +64,9 @@ namespace Avalonia.Win32.OpenGl.Angle
using var factory = MicroComRuntime.CreateProxyFor<IDXGIFactory1>(pDxgiFactory, true);
void* pAdapter = null;
if (preferDiscreteAdapter)
// As for now, we only need to redefine default adapter only on ARM64 just in case of Adreno GPU.
var redefineDefaultAdapter = RuntimeInformation.ProcessArchitecture == Architecture.Arm64;
if (redefineDefaultAdapter)
{
ushort adapterIndex = 0;
var adapters = new List<(IDXGIAdapter1 adapter, string name)>();
@ -80,9 +81,14 @@ namespace Avalonia.Win32.OpenGl.Angle
if (adapters.Count == 0)
throw new OpenGlException("No adapters found");
chosenAdapter = adapters
.OrderByDescending(x => x.name.Contains("nvidia") ? 2 : x.name.Contains("amd") ? 1 : 0)
.First().adapter.CloneReference();
.OrderByDescending(x =>
// Put adreno in lower priority - it's broken in Avalonia.
x.name.Contains("adreno") ? -1 : 0)
.First().adapter
.CloneReference();
foreach (var a in adapters)
a.adapter.Dispose();
}

16
src/Windows/Avalonia.Win32/Win32Com/win32.idl

@ -312,3 +312,19 @@ interface IDropTarget : IUnknown
[in] DropEffect* pdwEffect
);
}
[uuid(226C537B-1E76-4D9E-A760-33DB29922F18)]
interface IFrameworkInputPaneHandler : IUnknown
{
HRESULT Showing(RECT* prcInputPaneScreenLocation, boolean fEnsureFocusedElementInView);
HRESULT Hiding(boolean fEnsureFocusedElementInView);
}
[uuid(5752238B-24F0-495A-82F1-2FD593056796)]
interface IFrameworkInputPane : IUnknown
{
int Advise(IUnknown* pWindow, IFrameworkInputPaneHandler* pHandler, uint* pdwCookie);
int AdviseWithHWND(HWND hwnd, IFrameworkInputPaneHandler* pHandler, uint* pdwCookie);
int Unadvise(uint dwCookie);
int Location(RECT* prcInputPaneScreenLocation);
}

2
src/Windows/Avalonia.Win32/Win32StorageProvider.cs

@ -87,7 +87,7 @@ namespace Avalonia.Win32
{
var clsid = isOpenFile ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
var iid = UnmanagedMethods.ShellIds.IFileDialog;
var frm = UnmanagedMethods.CreateInstance<IFileDialog>(ref clsid, ref iid);
var frm = UnmanagedMethods.CreateInstance<IFileDialog>(in clsid, in iid);
var options = frm.Options;
options |= DefaultDialogOptions;

1
src/Windows/Avalonia.Win32/WinRT/winrt.idl

@ -871,3 +871,4 @@ interface IAccessibilitySettings : IInspectable
[propget] HRESULT HighContrast([out] [retval] boolean* value);
[propget] HRESULT HighContrastScheme([out] [retval] HSTRING* value);
}

1
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -110,6 +110,7 @@ namespace Avalonia.Win32
}
_framebuffer.Dispose();
_inputPane?.Dispose();
//Window doesn't exist anymore
_hwnd = IntPtr.Zero;

10
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -77,6 +77,7 @@ namespace Avalonia.Win32
private readonly Win32NativeControlHost _nativeControlHost;
private readonly IStorageProvider _storageProvider;
private readonly WindowsInputPane? _inputPane;
private WndProc _wndProcDelegate;
private string? _className;
private IntPtr _hwnd;
@ -164,7 +165,7 @@ namespace Avalonia.Win32
Screen = new ScreenImpl();
_storageProvider = new Win32StorageProvider(this);
_inputPane = Win32Platform.WindowsVersion >= PlatformConstants.Windows10 ? new WindowsInputPane(this) : null;
_nativeControlHost = new Win32NativeControlHost(this, !UseRedirectionBitmap);
_defaultTransparencyLevel = UseRedirectionBitmap ? WindowTransparencyLevel.None : WindowTransparencyLevel.Transparent;
_transparencyLevel = _defaultTransparencyLevel;
@ -342,6 +343,11 @@ namespace Avalonia.Win32
{
return AvaloniaLocator.Current.GetRequiredService<IClipboard>();
}
if (featureType == typeof(IInputPane))
{
return _inputPane;
}
return null;
}
@ -1342,7 +1348,7 @@ namespace Avalonia.Win32
else
SetFullScreen(newProperties.IsFullScreen);
if (!_isFullScreenActive)
if (!_isFullScreenActive && ((oldProperties.Decorations != newProperties.Decorations) || forceChanges))
{
var style = GetStyle();

5
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -227,6 +227,11 @@ namespace Avalonia.iOS
return _clipboard;
}
if (featureType == typeof(IInputPane))
{
return UIKitInputPane.Instance;
}
return null;
}
}

54
src/iOS/Avalonia.iOS/UIKitInputPane.cs

@ -0,0 +1,54 @@
using System;
using System.Diagnostics;
using Avalonia.Animation.Easings;
using Avalonia.Controls.Platform;
using Foundation;
using UIKit;
#nullable enable
namespace Avalonia.iOS;
internal sealed class UIKitInputPane : IInputPane
{
public static UIKitInputPane Instance { get; } = new();
public UIKitInputPane()
{
NSNotificationCenter
.DefaultCenter
.AddObserver(UIKeyboard.WillShowNotification, KeyboardUpNotification);
NSNotificationCenter
.DefaultCenter
.AddObserver(UIKeyboard.WillHideNotification, KeyboardDownNotification);
}
public InputPaneState State { get; private set; }
public Rect OccludedRect { get; private set; }
public event EventHandler<InputPaneStateEventArgs>? StateChanged;
private void KeyboardDownNotification(NSNotification obj) => RaiseEventFromNotification(false, obj);
private void KeyboardUpNotification(NSNotification obj) => RaiseEventFromNotification(true, obj);
private void RaiseEventFromNotification(bool isUp, NSNotification notification)
{
State = isUp ? InputPaneState.Open : InputPaneState.Closed;
var startFrame = UIKeyboard.FrameBeginFromNotification(notification);
var endFrame = UIKeyboard.FrameEndFromNotification(notification);
var duration = UIKeyboard.AnimationDurationFromNotification(notification);
var curve = (UIViewAnimationOptions)UIKeyboard.AnimationCurveFromNotification(notification);
IEasing? easing =
curve.HasFlag(UIViewAnimationOptions.CurveLinear) ? new LinearEasing()
: curve.HasFlag(UIViewAnimationOptions.CurveEaseIn) ? new SineEaseIn()
: curve.HasFlag(UIViewAnimationOptions.CurveEaseOut) ? new SineEaseOut()
: curve.HasFlag(UIViewAnimationOptions.CurveEaseInOut) ? new SineEaseInOut()
: null;
var startRect = new Rect(startFrame.X, startFrame.Y, startFrame.Width, startFrame.Height);
OccludedRect = new Rect(endFrame.X, endFrame.Y, endFrame.Width, endFrame.Height);
StateChanged?.Invoke(this, new InputPaneStateEventArgs(
State, startRect, OccludedRect, TimeSpan.FromSeconds(duration), easing));
}
}

8
tests/Avalonia.Base.UnitTests/Animation/AnimatableTests.cs

@ -126,18 +126,16 @@ namespace Avalonia.Base.UnitTests.Animation
var rect = new Rectangle() { Width = 11, };
var originalValue = rect.Width;
var clock = new TestClock();
animation.RunAsync(rect, clock);
clock.Step(TimeSpan.Zero);
Assert.Equal(rect.Width, 1);
Assert.Equal(1, rect.Width);
clock.Step(TimeSpan.FromSeconds(2));
Assert.Equal(rect.Width, 2);
Assert.Equal(2, rect.Width);
clock.Step(TimeSpan.FromSeconds(3));
//here we have invalid value so value should be expected and set to initial original value
Assert.Equal(rect.Width, originalValue);
Assert.Equal(11, rect.Width);
}
[Fact]

140
tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs

@ -131,17 +131,23 @@ namespace Avalonia.Base.UnitTests.Animation
}
[Theory]
[InlineData(FillMode.Backward, 0, 0d, 0.7d)]
[InlineData(FillMode.Both, 0, 0d, 0.7d)]
[InlineData(FillMode.Forward, 100, 0d, 0.7d)]
[InlineData(FillMode.Backward, 0, 0.3d, 0.7d)]
[InlineData(FillMode.Both, 0, 0.3d, 0.7d)]
[InlineData(FillMode.Forward, 100, 0.3d, 0.7d)]
public void Check_FillMode_Start_Value(FillMode fillMode, double target, double startCue, double endCue)
[InlineData(FillMode.Backward, 50.0, 0.0, 0.7, false)]
[InlineData(FillMode.Backward, 50.0, 0.0, 0.7, true )]
[InlineData(FillMode.Both, 50.0, 0.0, 0.7, false)]
[InlineData(FillMode.Both, 50.0, 0.0, 0.7, true )]
[InlineData(FillMode.Forward, 50.0, 0.0, 0.7, false)] // no delay but cue 0.0: the animation has started normally, explaining the 50.0 target without fill
[InlineData(FillMode.Forward, 100.0, 0.0, 0.7, true )]
[InlineData(FillMode.Backward, 50.0, 0.3, 0.7, false)]
[InlineData(FillMode.Backward, 50.0, 0.3, 0.7, true )]
[InlineData(FillMode.Both, 50.0, 0.3, 0.7, false)]
[InlineData(FillMode.Both, 50.0, 0.3, 0.7, true )]
[InlineData(FillMode.Forward, 100.0, 0.3, 0.7, false)]
[InlineData(FillMode.Forward, 100.0, 0.3, 0.7, true )]
public void Check_FillMode_Start_Value(FillMode fillMode, double target, double startCue, double endCue, bool delay)
{
var keyframe1 = new KeyFrame()
{
Setters = { new Setter(Layoutable.WidthProperty, 0d), }, Cue = new Cue(startCue)
Setters = { new Setter(Layoutable.WidthProperty, 50d), }, Cue = new Cue(startCue)
};
var keyframe2 = new KeyFrame()
@ -152,7 +158,7 @@ namespace Avalonia.Base.UnitTests.Animation
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(10d),
Delay = TimeSpan.FromSeconds(5d),
Delay = delay ? TimeSpan.FromSeconds(5d) : TimeSpan.Zero,
FillMode = fillMode,
Children = { keyframe1, keyframe2 }
};
@ -169,28 +175,34 @@ namespace Avalonia.Base.UnitTests.Animation
}
[Theory]
[InlineData(FillMode.Backward, 100, 0.3d, 1d)]
[InlineData(FillMode.Both, 300, 0.3d, 1d)]
[InlineData(FillMode.Forward, 300, 0.3d, 1d)]
[InlineData(FillMode.Backward, 100, 0.3d, 0.7d)]
[InlineData(FillMode.Both, 300, 0.3d, 0.7d)]
[InlineData(FillMode.Forward, 300, 0.3d, 0.7d)]
public void Check_FillMode_End_Value(FillMode fillMode, double target, double startCue, double endCue)
[InlineData(FillMode.Backward, 100.0, 0.3, 1.0, false)]
[InlineData(FillMode.Backward, 100.0, 0.3, 1.0, true )]
[InlineData(FillMode.Both, 300.0, 0.3, 1.0, false)]
[InlineData(FillMode.Both, 300.0, 0.3, 1.0, true )]
[InlineData(FillMode.Forward, 300.0, 0.3, 1.0, false)]
[InlineData(FillMode.Forward, 300.0, 0.3, 1.0, true )]
[InlineData(FillMode.Backward, 100.0, 0.3, 0.7, false)]
[InlineData(FillMode.Backward, 100.0, 0.3, 0.7, true )]
[InlineData(FillMode.Both, 300.0, 0.3, 0.7, false)]
[InlineData(FillMode.Both, 300.0, 0.3, 0.7, true )]
[InlineData(FillMode.Forward, 300.0, 0.3, 0.7, false)]
[InlineData(FillMode.Forward, 300.0, 0.3, 0.7, true )]
public void Check_FillMode_End_Value(FillMode fillMode, double target, double startCue, double endCue, bool delay)
{
var keyframe1 = new KeyFrame()
{
Setters = { new Setter(Layoutable.WidthProperty, 0d), }, Cue = new Cue(0.7d)
Setters = { new Setter(Layoutable.WidthProperty, 0d), }, Cue = new Cue(startCue)
};
var keyframe2 = new KeyFrame()
{
Setters = { new Setter(Layoutable.WidthProperty, 300d), }, Cue = new Cue(1d)
Setters = { new Setter(Layoutable.WidthProperty, 300d), }, Cue = new Cue(endCue)
};
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(10d),
Delay = TimeSpan.FromSeconds(5d),
Delay = delay ? TimeSpan.FromSeconds(5d) : TimeSpan.Zero,
FillMode = fillMode,
Children = { keyframe1, keyframe2 }
};
@ -502,6 +514,96 @@ namespace Avalonia.Base.UnitTests.Animation
animationRun.Wait();
}
[Theory]
[InlineData(0, 1, 2)]
[InlineData(0, 2, 1)]
[InlineData(1, 0, 2)]
[InlineData(1, 2, 0)]
[InlineData(2, 0, 1)]
[InlineData(2, 1, 0)]
public void KeyFrames_Order_Does_Not_Matter(int index0, int index1, int index2)
{
static KeyFrame CreateKeyFrame(double width, double cue)
=> new()
{
Setters = { new Setter(Layoutable.WidthProperty, width) },
Cue = new Cue(cue)
};
var keyFrames = new[]
{
CreateKeyFrame(100.0, 0.0),
CreateKeyFrame(200.0, 0.5),
CreateKeyFrame(300.0, 1.0)
};
var animation = new Animation
{
Duration = TimeSpan.FromSeconds(1.0),
IterationCount = new IterationCount(1),
Easing = new LinearEasing(),
FillMode = FillMode.Forward
};
animation.Children.Add(keyFrames[index0]);
animation.Children.Add(keyFrames[index1]);
animation.Children.Add(keyFrames[index2]);
var border = new Border
{
Height = 100.0,
Width = 50.0
};
var clock = new TestClock();
animation.RunAsync(border, clock);
clock.Step(TimeSpan.Zero);
Assert.Equal(100.0, border.Width);
clock.Step(TimeSpan.FromSeconds(0.5));
Assert.Equal(200.0, border.Width);
clock.Step(TimeSpan.FromSeconds(1.0));
Assert.Equal(300.0, border.Width);
}
[Theory]
[InlineData(0.0)]
[InlineData(0.5)]
[InlineData(1.0)]
public void Single_KeyFrame_Works(double cue)
{
var animation = new Animation
{
Duration = TimeSpan.FromSeconds(1.0),
IterationCount = new IterationCount(1),
Easing = new LinearEasing(),
FillMode = FillMode.Forward,
Children =
{
new KeyFrame
{
Setters = { new Setter(Layoutable.WidthProperty, 100.0) },
Cue = new Cue(cue)
}
}
};
var border = new Border
{
Height = 100.0,
Width = 50.0
};
var clock = new TestClock();
animation.RunAsync(border, clock);
clock.Step(TimeSpan.Zero);
clock.Step(TimeSpan.FromSeconds(cue));
Assert.Equal(100.0, border.Width);
}
private sealed class FakeAnimator : InterpolatingAnimator<double>
{
public double LastProgress { get; set; } = double.NaN;

4
tests/Avalonia.Base.UnitTests/Animation/SpringTests.cs

@ -80,9 +80,9 @@ public class SpringTests
animation.RunAsync(rect, clock);
clock.Step(TimeSpan.Zero);
Assert.Equal(rotateTransform.Angle, -2.5);
Assert.Equal(-2.5, rotateTransform.Angle);
clock.Step(TimeSpan.FromSeconds(5));
Assert.Equal(rotateTransform.Angle, 5.522828945000075);
Assert.Equal(5.522828945000075, rotateTransform.Angle);
var tolerance = 0.01;
clock.Step(TimeSpan.Parse("00:00:10.0153932"));

35
tests/Avalonia.Base.UnitTests/RoundedRectTests.cs

@ -0,0 +1,35 @@
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class RoundedRectTests
{
[Theory,
// Corners
InlineData(0, 0, false),
InlineData(100, 0, false),
InlineData(100, 100, false),
InlineData(0, 100, false),
// Indent 10px
InlineData(10, 10, false),
InlineData(90, 10, true),
InlineData(90, 90, false),
InlineData(10, 90, true),
// Indent 17px
InlineData(17, 17, false),
InlineData(83, 17, true),
InlineData(83, 83, true),
InlineData(17, 83, true),
// Center
InlineData(50, 50, true),
]
public void ContainsExclusive_Should_Return_Expected_Result_For_Point(double x, double y, bool expectedResult)
{
var rrect = new RoundedRect(new Rect(0, 0, 100, 100), new CornerRadius(60, 10, 50, 30));
Assert.Equal(expectedResult, rrect.ContainsExclusive(new Point(x, y)));
}
}
}

17
tests/Avalonia.Base.UnitTests/Styling/TestSelectors.cs

@ -1,17 +0,0 @@
using System;
using Avalonia.Styling;
namespace Avalonia.Base.UnitTests.Styling
{
public static class TestSelectors
{
public static Selector SubscribeCheck(this Selector selector)
{
throw new NotImplementedException();
//return new Selector(
// selector,
// control => new SelectorMatch(((TestControlBase)control).SubscribeCheckObservable),
// "");
}
}
}

42
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@ -456,6 +456,45 @@ namespace Avalonia.Controls.UnitTests
});
}
[Fact]
public void CaretIndex_Changes()
{
string text = "Sample text";
string expectedText = "Saple text";
RunTest((control, textbox) =>
{
control.Text = text;
control.Measure(Size.Infinity);
Dispatcher.UIThread.RunJobs();
textbox.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = Key.Right
});
Dispatcher.UIThread.RunJobs();
Assert.Equal(1, control.CaretIndex);
Assert.Equal(textbox.CaretIndex, control.CaretIndex);
control.CaretIndex = 3;
Assert.Equal(3, control.CaretIndex);
Assert.Equal(textbox.CaretIndex, control.CaretIndex);
textbox.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
Key = Key.Back
});
Dispatcher.UIThread.RunJobs();
Assert.Equal(2, control.CaretIndex);
Assert.Equal(textbox.CaretIndex, control.CaretIndex);
Assert.True(control.Text == expectedText && textbox.Text == expectedText);
});
}
/// <summary>
/// Retrieves a defined predicate filter through a new AutoCompleteBox
/// control instance.
@ -1111,7 +1150,8 @@ namespace Avalonia.Controls.UnitTests
var textBox =
new TextBox
{
Name = "PART_TextBox"
Name = "PART_TextBox",
[!!TextBox.CaretIndexProperty] = control[!!AutoCompleteBox.CaretIndexProperty]
}.RegisterInNameScope(scope);
var listbox =
new ListBox

52
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -217,6 +217,9 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(10, target.Presenter.Panel.Children.Count);
Assert.True(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
// The selected item must not be the anchor, otherwise it won't get recycled.
target.Selection.AnchorIndex = -1;
// Scroll down a page.
target.Scroll.Offset = new Vector(0, 10);
Layout(target);
@ -1289,6 +1292,55 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void ListBoxItem_Should_Not_Block_Tapped_Events()
{
// #13474
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
Pointer _pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Touch, true);
ulong nextStamp = 1;
var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray();
var target = new ListBox
{
Template = ListBoxTemplate(),
ItemsSource = items,
SelectionMode = SelectionMode.Toggle,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 })
};
Prepare(target);
var lbItems = target.GetLogicalChildren().OfType<ListBoxItem>().ToArray();
var item = lbItems[0];
int tappedCount = 0;
target.Tapped += (s, e) =>
{
tappedCount++;
};
_mouse.Click(item);
Assert.Equal(1, tappedCount);
// Raise PointerPressed and PointerReleased events with the Left Button pressed. TouchTestHelper
// assumes no button pressed, which prevents it from generating Tapped events, or I would use that.
item.RaiseEvent(new PointerPressedEventArgs(item, _pointer, (Visual)item, default, nextStamp++,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), KeyModifiers.None));
item.RaiseEvent(new PointerReleasedEventArgs(item, _pointer, (Visual)item, default, nextStamp++,
PointerPointProperties.None, KeyModifiers.None, MouseButton.Left));
Assert.Equal(2, tappedCount);
}
}
private static void RaiseKeyEvent(Control target, Key key, KeyModifiers inputModifiers = 0)
{
target.RaiseEvent(new KeyEventArgs

34
tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Security.Cryptography.X509Certificates;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
@ -27,6 +28,32 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(100, target.Maximum);
}
[Fact]
public void ChangingDataContextShouldNotChangeOldDataContext()
{
var viewModel = new RangeTestViewModel()
{
Minimum = -5000,
Maximum = 5000,
Value = 4000
};
var target = new TestRange
{
[!RangeBase.MinimumProperty] = new Binding(nameof(viewModel.Minimum)),
[!RangeBase.MaximumProperty] = new Binding(nameof(viewModel.Maximum)),
[!RangeBase.ValueProperty] = new Binding(nameof(viewModel.Value)),
};
var root = new TestRoot(target);
target.DataContext = viewModel;
target.DataContext = null;
Assert.Equal(4000, viewModel.Value);
Assert.Equal(-5000, viewModel.Minimum);
Assert.Equal(5000, viewModel.Maximum);
}
[Fact]
public void Value_Should_Be_Coerced_To_Range()
{
@ -217,5 +244,12 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
}
private class RangeTestViewModel
{
public double Minimum { get; set; }
public double Maximum { get; set; }
public double Value { get; set; }
}
}
}

19
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -1226,6 +1226,25 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(0, root.SelectedIndex);
}
[Fact]
public void TabOnceActiveElement_Should_Be_Initialized_With_SelectedItem()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new ListBox
{
Template = Template(),
ItemsSource = new[] { "Foo", "Bar", "Baz " },
SelectedIndex = 1,
};
Prepare(target);
var container = target.ContainerFromIndex(1)!;
Assert.Same(container, KeyboardNavigation.GetTabOnceActiveElement(target));
}
}
[Fact]
public void Setting_SelectedItem_With_Pointer_Should_Set_TabOnceActiveElement()
{

2
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@ -1110,7 +1110,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(19, panel.LastRealizedIndex);
// The selection should be preserved.
Assert.Empty(SelectedContainers(target));
Assert.Equal(new[] { 1 }, SelectedContainers(target));
Assert.Equal(1, target.SelectedIndex);
Assert.Same(items[1], target.SelectedItem);
Assert.Equal(new[] { 1 }, target.Selection.SelectedIndexes);

77
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@ -572,6 +572,83 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal("Header from style", tabItem.Header);
}
[Fact]
public void TabItem_TabStripPlacement_Should_Be_Correctly_Set()
{
var items = new object[]
{
"Foo",
new TabItem { Content = new TextBlock { Text = "Baz" } }
};
var target = new TabControl
{
Template = TabControlTemplate(),
DataContext = "Base",
ItemsSource = items
};
ApplyTemplate(target);
var result = target.GetLogicalChildren()
.OfType<TabItem>()
.ToList();
Assert.Collection(
result,
x => Assert.Equal(Dock.Top, x.TabStripPlacement),
x => Assert.Equal(Dock.Top, x.TabStripPlacement)
);
target.TabStripPlacement = Dock.Right;
result = target.GetLogicalChildren()
.OfType<TabItem>()
.ToList();
Assert.Collection(
result,
x => Assert.Equal(Dock.Right, x.TabStripPlacement),
x => Assert.Equal(Dock.Right, x.TabStripPlacement)
);
}
[Fact]
public void TabItem_TabStripPlacement_Should_Be_Correctly_Set_For_New_Items()
{
var items = new object[]
{
"Foo",
new TabItem { Content = new TextBlock { Text = "Baz" } }
};
var target = new TabControl
{
Template = TabControlTemplate(),
DataContext = "Base"
};
ApplyTemplate(target);
target.ItemsSource = items;
var result = target.GetLogicalChildren()
.OfType<TabItem>()
.ToList();
Assert.Collection(
result,
x => Assert.Equal(Dock.Top, x.TabStripPlacement),
x => Assert.Equal(Dock.Top, x.TabStripPlacement)
);
target.TabStripPlacement = Dock.Right;
result = target.GetLogicalChildren()
.OfType<TabItem>()
.ToList();
Assert.Collection(
result,
x => Assert.Equal(Dock.Right, x.TabStripPlacement),
x => Assert.Equal(Dock.Right, x.TabStripPlacement)
);
}
private static IControlTemplate TabControlTemplate()
{
return new FuncControlTemplate<TabControl>((parent, scope) =>

213
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@ -8,6 +8,7 @@ using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
@ -314,15 +315,19 @@ namespace Avalonia.Controls.UnitTests
{
using var app = App();
var (target, scroll, itemsControl) = CreateTarget();
var items = (IList)itemsControl.ItemsSource!;
var focused = target.GetRealizedElements().First()!;
focused.Focusable = true;
focused.Focus();
Assert.True(focused.IsKeyboardFocusWithin);
Assert.Equal(focused, KeyboardNavigation.GetTabOnceActiveElement(itemsControl));
scroll.Offset = new Vector(0, 200);
Layout(target);
items.RemoveAt(0);
Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
Assert.All(target.GetRealizedElements(), x => Assert.NotSame(focused, x));
}
@ -611,6 +616,125 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(10, raised);
}
[Fact]
public void ContainerIndexChanged_Is_Raised_On_Insert()
{
using var app = App();
var (target, scroll, itemsControl) = CreateTarget();
var items = (IList)itemsControl.ItemsSource!;
var raised = 0;
var index = 1;
itemsControl.ContainerIndexChanged += (s, e) =>
{
++raised;
Assert.Equal(index, e.OldIndex);
Assert.Equal(++index, e.NewIndex);
};
items.Insert(index, "new");
Assert.Equal(9, raised);
}
[Fact]
public void ContainerIndexChanged_Is_Raised_When_Item_Inserted_Before_Realized_Elements()
{
using var app = App();
var (target, scroll, itemsControl) = CreateTarget();
var items = (IList)itemsControl.ItemsSource!;
var raised = 0;
var index = 20;
itemsControl.ContainerIndexChanged += (s, e) =>
{
++raised;
Assert.Equal(index, e.OldIndex);
Assert.Equal(++index, e.NewIndex);
};
scroll.Offset = new Vector(0, 200);
Layout(target);
items.Insert(10, "new");
Assert.Equal(10, raised);
}
[Fact]
public void ContainerIndexChanged_Is_Raised_On_Remove()
{
using var app = App();
var (target, scroll, itemsControl) = CreateTarget();
var items = (IList)itemsControl.ItemsSource!;
var raised = 0;
var index = 1;
itemsControl.ContainerIndexChanged += (s, e) =>
{
++raised;
Assert.Equal(index + 1, e.OldIndex);
Assert.Equal(index++, e.NewIndex);
};
items.RemoveAt(index);
Assert.Equal(8, raised);
}
[Fact]
public void ContainerIndexChanged_Is_Raised_When_Item_Removed_Before_Realized_Elements()
{
using var app = App();
var (target, scroll, itemsControl) = CreateTarget();
var items = (IList)itemsControl.ItemsSource!;
var raised = 0;
var index = 20;
itemsControl.ContainerIndexChanged += (s, e) =>
{
Assert.Equal(index, e.OldIndex);
Assert.Equal(index - 1, e.NewIndex);
++index;
++raised;
};
scroll.Offset = new Vector(0, 200);
Layout(target);
items.RemoveAt(10);
Assert.Equal(10, raised);
}
[Fact]
public void Fires_Correct_Container_Lifecycle_Events_On_Replace()
{
using var app = App();
var (target, scroll, itemsControl) = CreateTarget();
var items = (IList)itemsControl.ItemsSource!;
var events = new List<string>();
itemsControl.ContainerPrepared += (s, e) => events.Add($"Prepared #{e.Container.GetHashCode()} = {e.Index}");
itemsControl.ContainerClearing += (s, e) => events.Add($"Clearing #{e.Container.GetHashCode()}");
itemsControl.ContainerIndexChanged += (s, e) => events.Add($"IndexChanged #{e.Container.GetHashCode()} {e.OldIndex} -> {e.NewIndex}");
var toReplace = target.GetRealizedElements().ElementAt(2)!;
items[2] = "New Item";
Assert.Equal(
new[] { $"Clearing #{toReplace.GetHashCode()}" },
events);
events.Clear();
itemsControl.UpdateLayout();
Assert.Equal(
new[] { $"Prepared #{toReplace.GetHashCode()} = 2" },
events);
events.Clear();
}
[Fact]
public void Scrolling_Down_With_Larger_Element_Does_Not_Cause_Jump_And_Arrives_At_End()
{
@ -843,6 +967,86 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, panel.VisualChildren.Count);
}
[Fact]
public void Alternating_Backgrounds_Should_Be_Correct_After_Scrolling()
{
// Issue #12381.
static void AssertColors(VirtualizingStackPanel target)
{
var containers = target.GetRealizedContainers()!
.Cast<ListBoxItem>()
.ToList();
for (var i = target.FirstRealizedIndex; i <= target.LastRealizedIndex; i++)
{
var container = Assert.IsType<ListBoxItem>(target.ContainerFromIndex(i));
var expectedBackground = i % 2 == 0 ? Colors.Green : Colors.Red;
var brush = Assert.IsAssignableFrom<ISolidColorBrush>(container.Background);
Assert.Equal(expectedBackground, brush.Color);
}
}
using var app = App();
var styles = new[]
{
new Style(x => x.OfType<ListBoxItem>())
{
Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.White) },
},
new Style(x => x.OfType<ListBoxItem>().NthChild(2, 1))
{
Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Green) },
},
new Style(x => x.OfType<ListBoxItem>().NthChild(2, 0))
{
Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
},
};
var (target, scroll, itemsControl) = CreateUnrootedTarget<ListBox>();
// We need to display an odd number of items to reproduce the issue.
var root = CreateRoot(itemsControl, clientSize: new(100, 90), styles: styles);
root.LayoutManager.ExecuteInitialLayoutPass();
var containers = target.GetRealizedContainers()!
.Cast<ListBoxItem>()
.ToList();
Assert.Equal(0, target.FirstRealizedIndex);
Assert.Equal(8, target.LastRealizedIndex);
AssertColors(target);
scroll.Offset = new Vector(0, 10);
target.UpdateLayout();
Assert.Equal(1, target.FirstRealizedIndex);
Assert.Equal(9, target.LastRealizedIndex);
AssertColors(target);
}
[Fact]
public void Inserting_Item_Before_Viewport_Preserves_FirstRealizedIndex()
{
// Issue #12744
using var app = App();
var (target, scroll, itemsControl) = CreateTarget();
var items = (IList)itemsControl.ItemsSource!;
// Scroll down 20 items.
scroll.Offset = new Vector(0, 200);
target.UpdateLayout();
Assert.Equal(20, target.FirstRealizedIndex);
// Insert an item at the beginning.
items.Insert(0, "New Item");
target.UpdateLayout();
// The first realized index should still be 20 as the scroll should be unchanged.
Assert.Equal(20, target.FirstRealizedIndex);
Assert.Equal(new(0, 200), scroll.Offset);
}
private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
{
return target.GetRealizedElements()
@ -903,7 +1107,7 @@ namespace Avalonia.Controls.UnitTests
where T : ItemsControl, new()
{
var (target, scroll, itemsControl) = CreateUnrootedTarget<T>(items, itemTemplate);
var root = CreateRoot(itemsControl, styles);
var root = CreateRoot(itemsControl, styles: styles);
root.LayoutManager.ExecuteInitialLayoutPass();
@ -942,10 +1146,13 @@ namespace Avalonia.Controls.UnitTests
return (target, scroll, itemsControl);
}
private static TestRoot CreateRoot(Control? child, IEnumerable<Style>? styles = null)
private static TestRoot CreateRoot(
Control? child,
Size? clientSize = null,
IEnumerable<Style>? styles = null)
{
var root = new TestRoot(true, child);
root.ClientSize = new(100, 100);
root.ClientSize = clientSize ?? new(100, 100);
if (styles is not null)
root.Styles.AddRange(styles);

Loading…
Cancel
Save