Browse Source

Merge branch 'master' into feature/vulkan

feature/vulkan
Nikita Tsukanov 3 years ago
committed by GitHub
parent
commit
80b671cdf0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Avalonia.sln
  2. 31
      NOTICE.md
  3. 2
      build/Base.props
  4. 5
      build/JetBrains.Annotations.props
  5. 2
      build/System.Memory.props
  6. 1
      nukebuild/DotNetConfigHelper.cs
  7. 3
      samples/ControlCatalog/MainView.xaml
  8. 212
      samples/ControlCatalog/Pages/GesturePage.cs
  9. 117
      samples/ControlCatalog/Pages/GesturePage.xaml
  10. 5
      samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs
  11. 4
      samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs
  12. 2
      src/Android/Avalonia.Android/AndroidPlatform.cs
  13. 14
      src/Android/Avalonia.Android/AvaloniaSplashActivity.cs
  14. 2
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs
  15. 1
      src/Avalonia.Base/Animation/AnimationInstance`1.cs
  16. 4
      src/Avalonia.Base/Avalonia.Base.csproj
  17. 26
      src/Avalonia.Base/Compatibility/OperatingSystem.cs
  18. 34
      src/Avalonia.Base/Contract.cs
  19. 4
      src/Avalonia.Base/Data/InstancedBinding.cs
  20. 128
      src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs
  21. 33
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  22. 65
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  23. 375
      src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs
  24. 129
      src/Avalonia.Base/Input/Gestures.cs
  25. 51
      src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs
  26. 16
      src/Avalonia.Base/Input/InputElement.cs
  27. 24
      src/Avalonia.Base/Input/PinchEventArgs.cs
  28. 6
      src/Avalonia.Base/Input/Raw/RawInputEventArgs.cs
  29. 6
      src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
  30. 5
      src/Avalonia.Base/Logging/LogArea.cs
  31. 7
      src/Avalonia.Base/Media/DrawingGroup.cs
  32. 3
      src/Avalonia.Base/Media/Typeface.cs
  33. 2
      src/Avalonia.Base/PixelVector.cs
  34. 2
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  35. 2
      src/Avalonia.Base/Platform/IPlatformSettings.cs
  36. 19
      src/Avalonia.Base/Platform/IRuntimePlatform.cs
  37. 44
      src/Avalonia.Base/Platform/StandardRuntimePlatform.cs
  38. 13
      src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs
  39. 32
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  40. 53
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs
  41. 5
      src/Avalonia.Base/Visual.cs
  42. 1
      src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
  43. 1
      src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
  44. 4
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  45. 1
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  46. 2
      src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs
  47. 239
      src/Avalonia.Controls/AppBuilder.cs
  48. 244
      src/Avalonia.Controls/AppBuilderBase.cs
  49. 5
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  50. 1
      src/Avalonia.Controls/Avalonia.Controls.csproj
  51. 90
      src/Avalonia.Controls/ComboBox.cs
  52. 11
      src/Avalonia.Controls/ContentControl.cs
  53. 13
      src/Avalonia.Controls/Control.cs
  54. 6
      src/Avalonia.Controls/LoggingExtensions.cs
  55. 7
      src/Avalonia.Controls/TransitioningContentControl.cs
  56. 1
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  57. 1
      src/Avalonia.Controls/WindowBase.cs
  58. 6
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  59. 32
      src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs
  60. 7
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  61. 3
      src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs
  62. 5
      src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs
  63. 3
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  64. 23
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  65. 3
      src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
  66. 1
      src/Avalonia.Native/Helpers.cs
  67. 1
      src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs
  68. 6
      src/Avalonia.OpenGL/Egl/EglInterface.cs
  69. 3
      src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
  70. 4
      src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
  71. 14
      src/Avalonia.X11/Avalonia.X11.csproj
  72. 1
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  73. 1
      src/Avalonia.X11/X11Info.cs
  74. 2
      src/Avalonia.X11/X11Platform.cs
  75. 1
      src/Avalonia.X11/X11Screens.cs
  76. 3
      src/Avalonia.X11/X11Window.Ime.cs
  77. 7
      src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs
  78. 7
      src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs
  79. 12
      src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs
  80. 1
      src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs
  81. 50
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  82. 3
      src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
  83. 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs
  84. 4
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CommandAccessorPlugin.cs
  85. 14
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs
  86. 4
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs
  87. 43
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs
  88. 6
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  89. 4
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  90. 8
      src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs
  91. 3
      src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs
  92. 1
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  93. 2
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  94. 8
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  95. 11
      src/Windows/Avalonia.Win32/Win32Platform.cs
  96. 12
      src/Windows/Avalonia.Win32/WindowImpl.cs
  97. 1
      src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs
  98. 1
      src/iOS/Avalonia.iOS/AvaloniaView.Text.cs
  99. 2
      src/iOS/Avalonia.iOS/Platform.cs
  100. 338
      tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs

1
Avalonia.sln

@ -100,7 +100,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\EmbedXaml.props = build\EmbedXaml.props build\EmbedXaml.props = build\EmbedXaml.props
build\HarfBuzzSharp.props = build\HarfBuzzSharp.props build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
build\ImageSharp.props = build\ImageSharp.props build\ImageSharp.props = build\ImageSharp.props
build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props
build\Microsoft.CSharp.props = build\Microsoft.CSharp.props build\Microsoft.CSharp.props = build\Microsoft.CSharp.props
build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props build\Microsoft.Reactive.Testing.props = build\Microsoft.Reactive.Testing.props

31
NOTICE.md

@ -303,3 +303,34 @@ https://github.com/chromium/chromium
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Flutter
https://github.com/flutter/flutter
//Copyright 2014 The Flutter Authors. All rights reserved.
//Redistribution and use in source and binary forms, with or without modification,
//are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
//ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
//WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
//DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
//ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
//(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
//LOSS OF USE, DATA, OR PROFITS;
//OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
//ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

2
build/Base.props

@ -1,5 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup Condition="'$(TargetFramework)' != 'net6'">
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.6.0" /> <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.6.0" />
</ItemGroup> </ItemGroup>

5
build/JetBrains.Annotations.props

@ -1,5 +0,0 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="10.3.0" />
</ItemGroup>
</Project>

2
build/System.Memory.props

@ -1,5 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup Condition="'$(TargetFramework)' != 'net6'">
<PackageReference Include="System.Memory" Version="4.5.3" /> <PackageReference Include="System.Memory" Version="4.5.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>

1
nukebuild/DotNetConfigHelper.cs

@ -1,5 +1,4 @@
using System.Globalization; using System.Globalization;
using JetBrains.Annotations;
using Nuke.Common.Tools.DotNet; using Nuke.Common.Tools.DotNet;
// ReSharper disable ReturnValueOfPureMethodIsNotUsed // ReSharper disable ReturnValueOfPureMethodIsNotUsed

3
samples/ControlCatalog/MainView.xaml

@ -92,6 +92,9 @@
<TabItem Header="Flyouts"> <TabItem Header="Flyouts">
<pages:FlyoutsPage /> <pages:FlyoutsPage />
</TabItem> </TabItem>
<TabItem Header="Gestures">
<pages:GesturePage />
</TabItem>
<TabItem Header="Image" <TabItem Header="Image"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled"> ScrollViewer.VerticalScrollBarVisibility="Disabled">

212
samples/ControlCatalog/Pages/GesturePage.cs

@ -0,0 +1,212 @@
using System;
using System.Numerics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Rendering.Composition;
namespace ControlCatalog.Pages
{
public class GesturePage : UserControl
{
private bool _isInit;
private float _currentScale;
public GesturePage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if(_isInit)
{
return;
}
_isInit = true;
SetPullHandlers(this.Find<Border>("TopPullZone"), false);
SetPullHandlers(this.Find<Border>("BottomPullZone"), true);
SetPullHandlers(this.Find<Border>("RightPullZone"), true);
SetPullHandlers(this.Find<Border>("LeftPullZone"), false);
var image = this.Find<Image>("PinchImage");
SetPinchHandlers(image);
var reset = this.Find<Button>("ResetButton");
reset!.Click += (s, e) =>
{
var compositionVisual = ElementComposition.GetElementVisual(image);
if(compositionVisual!= null)
{
_currentScale = 1;
compositionVisual.Scale = new Vector3(1,1,1);
image.InvalidateMeasure();
}
};
}
private void SetPinchHandlers(Control? control)
{
if (control == null)
{
return;
}
_currentScale = 1;
Vector3 currentOffset = default;
bool isZooming = false;
CompositionVisual? compositionVisual = null;
void InitComposition(Control visual)
{
if (compositionVisual != null)
{
return;
}
compositionVisual = ElementComposition.GetElementVisual(visual);
}
control.LayoutUpdated += (s, e) =>
{
InitComposition(control!);
if (compositionVisual != null)
{
compositionVisual.Scale = new(_currentScale, _currentScale, 1);
if(currentOffset == default)
{
currentOffset = compositionVisual.Offset;
}
}
};
control.AddHandler(Gestures.PinchEvent, (s, e) =>
{
InitComposition(control!);
isZooming = true;
if(compositionVisual != null)
{
var scale = _currentScale * (float)e.Scale;
compositionVisual.Scale = new(scale, scale, 1);
}
});
control.AddHandler(Gestures.PinchEndedEvent, (s, e) =>
{
InitComposition(control!);
isZooming = false;
if (compositionVisual != null)
{
_currentScale = compositionVisual.Scale.X;
}
});
control.AddHandler(Gestures.ScrollGestureEvent, (s, e) =>
{
InitComposition(control!);
if (compositionVisual != null && !isZooming)
{
currentOffset -= new Vector3((float)e.Delta.X, (float)e.Delta.Y, 0);
compositionVisual.Offset = currentOffset;
}
});
}
private void SetPullHandlers(Control? control, bool inverse)
{
if (control == null)
{
return;
}
var ball = control.FindLogicalDescendantOfType<Border>();
Vector3 defaultOffset = default;
CompositionVisual? ballCompositionVisual = null;
if (ball != null)
{
InitComposition(ball);
}
else
{
return;
}
control.LayoutUpdated += (s, e) =>
{
InitComposition(ball!);
if (ballCompositionVisual != null)
{
defaultOffset = ballCompositionVisual.Offset;
}
};
control.AddHandler(Gestures.PullGestureEvent, (s, e) =>
{
Vector3 center = new((float)control.Bounds.Center.X, (float)control.Bounds.Center.Y, 0);
InitComposition(ball!);
if (ballCompositionVisual != null)
{
ballCompositionVisual.Offset = defaultOffset + new System.Numerics.Vector3((float)e.Delta.X * 0.4f, (float)e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1);
}
});
control.AddHandler(Gestures.PullGestureEndedEvent, (s, e) =>
{
InitComposition(ball!);
if (ballCompositionVisual != null)
{
ballCompositionVisual.Offset = defaultOffset;
}
});
void InitComposition(Control control)
{
if (ballCompositionVisual != null)
{
return;
}
ballCompositionVisual = ElementComposition.GetElementVisual(ball);
if (ballCompositionVisual != null)
{
var offsetAnimation = ballCompositionVisual.Compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(100);
var implicitAnimations = ballCompositionVisual.Compositor.CreateImplicitAnimationCollection();
implicitAnimations["Offset"] = offsetAnimation;
ballCompositionVisual.ImplicitAnimations = implicitAnimations;
}
}
}
}
}

117
samples/ControlCatalog/Pages/GesturePage.xaml

@ -0,0 +1,117 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DesignHeight="800"
d:DesignWidth="400"
x:Class="ControlCatalog.Pages.GesturePage">
<StackPanel Orientation="Vertical"
Spacing="4">
<TextBlock FontWeight="Bold"
FontSize="18"
Margin="5">Pull Gexture (Touch / Pen)</TextBlock>
<TextBlock Margin="5">Pull from colored rectangles</TextBlock>
<Border>
<DockPanel HorizontalAlignment="Stretch"
ClipToBounds="True"
Margin="5"
Height="200">
<Border DockPanel.Dock="Top"
Margin="2"
Name="TopPullZone"
Background="Transparent"
BorderBrush="Red"
HorizontalAlignment="Stretch"
Height="50"
BorderThickness="1">
<Border.GestureRecognizers>
<PullGestureRecognizer PullDirection="TopToBottom"/>
</Border.GestureRecognizers>
<Border Width="10"
Height="10"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CornerRadius="5"
Name="TopBall"
Background="Green"/>
</Border>
<Border DockPanel.Dock="Bottom"
BorderBrush="Green"
Margin="2"
Background="Transparent"
Name="BottomPullZone"
HorizontalAlignment="Stretch"
Height="50"
BorderThickness="1">
<Border.GestureRecognizers>
<PullGestureRecognizer PullDirection="BottomToTop"/>
</Border.GestureRecognizers>
<Border Width="10"
Name="BottomBall"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Height="10"
CornerRadius="5"
Background="Green"/>
</Border>
<Border DockPanel.Dock="Right"
Margin="2"
Background="Transparent"
Name="RightPullZone"
BorderBrush="Blue"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
Width="50"
BorderThickness="1">
<Border.GestureRecognizers>
<PullGestureRecognizer PullDirection="RightToLeft"/>
</Border.GestureRecognizers>
<Border Width="10"
Height="10"
Name="RightBall"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CornerRadius="5"
Background="Green"/>
</Border>
<Border DockPanel.Dock="Left"
Margin="2"
Background="Transparent"
Name="LeftPullZone"
BorderBrush="Orange"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Width="50"
BorderThickness="1">
<Border.GestureRecognizers>
<PullGestureRecognizer PullDirection="LeftToRight"/>
</Border.GestureRecognizers>
<Border Width="10"
Height="10"
Name="LeftBall"
HorizontalAlignment="Center"
VerticalAlignment="Center"
CornerRadius="5"
Background="Green"/>
</Border>
</DockPanel>
</Border>
<TextBlock FontWeight="Bold"
FontSize="18"
Margin="5">Pinch/Zoom Gexture (Multi Touch)</TextBlock>
<Border ClipToBounds="True">
<Image Stretch="UniformToFill"
Margin="5"
Name="PinchImage"
Source="/Assets/delicate-arch-896885_640.jpg">
<Image.GestureRecognizers>
<PinchGestureRecognizer/>
<ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True"/>
</Image.GestureRecognizers>
</Image>
</Border>
<Button HorizontalAlignment="Center" Name="ResetButton">Reset</Button>
</StackPanel>
</UserControl>

5
samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs

@ -1,5 +1,8 @@
using Avalonia.Controls; using System;
using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media.Immutable;
using ControlCatalog.ViewModels; using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages

4
samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs

@ -1,3 +1,5 @@
using System;
using System.Runtime.InteropServices;
using Avalonia; using Avalonia;
using Avalonia.Platform; using Avalonia.Platform;
using MiniMvvm; using MiniMvvm;
@ -13,7 +15,7 @@ public class PlatformInformationViewModel : ViewModelBase
if (runtimeInfo is { } info) if (runtimeInfo is { } info)
{ {
if (info.IsBrowser) if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER")))
{ {
if (info.IsDesktop) if (info.IsDesktop)
{ {

2
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -15,7 +15,7 @@ namespace Avalonia
{ {
public static class AndroidApplicationExtensions public static class AndroidApplicationExtensions
{ {
public static T UseAndroid<T>(this T builder) where T : AppBuilderBase<T>, new() public static AppBuilder UseAndroid(this AppBuilder builder)
{ {
return builder return builder
.UseWindowingSubsystem(() => AndroidPlatform.Initialize(), "Android") .UseWindowingSubsystem(() => AndroidPlatform.Initialize(), "Android")

14
src/Android/Avalonia.Android/AvaloniaSplashActivity.cs

@ -1,6 +1,5 @@
using Android.OS; using Android.OS;
using AndroidX.AppCompat.App; using AndroidX.AppCompat.App;
using AndroidX.Lifecycle;
namespace Avalonia.Android namespace Avalonia.Android
{ {
@ -8,15 +7,22 @@ namespace Avalonia.Android
{ {
protected abstract AppBuilder CreateAppBuilder(); protected abstract AppBuilder CreateAppBuilder();
private static AppBuilder s_appBuilder;
protected override void OnCreate(Bundle? savedInstanceState) protected override void OnCreate(Bundle? savedInstanceState)
{ {
base.OnCreate(savedInstanceState); base.OnCreate(savedInstanceState);
var builder = CreateAppBuilder(); if (s_appBuilder == null)
{
var builder = CreateAppBuilder();
var lifetime = new SingleViewLifetime();
var lifetime = new SingleViewLifetime(); builder.SetupWithLifetime(lifetime);
builder.SetupWithLifetime(lifetime); s_appBuilder = builder;
}
} }
} }

2
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs

@ -38,7 +38,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
return null; return null;
} }
var eventTime = (ulong)DateTime.Now.Millisecond; var eventTime = (ulong)e.EventTime;
var inputRoot = _view.InputRoot; var inputRoot = _view.InputRoot;
var actionMasked = e.ActionMasked; var actionMasked = e.ActionMasked;
var modifiers = GetModifiers(e.MetaState, e.ButtonState); var modifiers = GetModifiers(e.MetaState, e.ButtonState);

1
src/Avalonia.Base/Animation/AnimationInstance`1.cs

@ -5,7 +5,6 @@ using Avalonia.Animation.Animators;
using Avalonia.Animation.Utils; using Avalonia.Animation.Utils;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Reactive; using Avalonia.Reactive;
using JetBrains.Annotations;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {

4
src/Avalonia.Base/Avalonia.Base.csproj

@ -15,7 +15,6 @@
<Import Project="..\..\build\Base.props" /> <Import Project="..\..\build\Base.props" />
<Import Project="..\..\build\Binding.props" /> <Import Project="..\..\build\Binding.props" />
<Import Project="..\..\build\Rx.props" /> <Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\JetBrains.Annotations.props" />
<Import Project="..\..\build\System.Memory.props" /> <Import Project="..\..\build\System.Memory.props" />
<Import Project="..\..\build\ApiDiff.props" /> <Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
@ -29,10 +28,13 @@
<ItemGroup Label="InternalsVisibleTo"> <ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Desktop, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Markup, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.Xaml, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Markup.Xaml, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.OpenGL, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.ColorPicker, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls.ColorPicker, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.DataGrid, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls.DataGrid, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />

26
src/Avalonia.Base/Compatibility/OperatingSystem.cs

@ -0,0 +1,26 @@
using System;
using System.Runtime.InteropServices;
namespace Avalonia.Compatibility
{
internal sealed class OperatingSystemEx
{
#if NET6_0_OR_GREATER
public static bool IsWindows() => OperatingSystem.IsWindows();
public static bool IsMacOS() => OperatingSystem.IsMacOS();
public static bool IsLinux() => OperatingSystem.IsLinux();
public static bool IsAndroid() => OperatingSystem.IsAndroid();
public static bool IsIOS() => OperatingSystem.IsIOS();
public static bool IsBrowser() => OperatingSystem.IsBrowser();
public static bool IsOSPlatform(string platform) => OperatingSystem.IsOSPlatform(platform);
#else
public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
public static bool IsAndroid() => IsOSPlatform("ANDROID");
public static bool IsIOS() => IsOSPlatform("IOS");
public static bool IsBrowser() => IsOSPlatform("BROWSER");
public static bool IsOSPlatform(string platform) => RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform));
#endif
}
}

34
src/Avalonia.Base/Contract.cs

@ -1,34 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
namespace Avalonia
{
/// <summary>
/// A stub of Code Contract's Contract class.
/// </summary>
/// <remarks>
/// It would be nice to use Code Contracts on Avalonia but last time I tried it slowed things
/// to a crawl and often crashed. Instead use the same signature for checking preconditions
/// in the hope that it might become usable at some point.
/// </remarks>
public static class Contract
{
/// <summary>
/// Specifies a precondition.
/// </summary>
/// <typeparam name="TException">
/// The exception to throw if <paramref name="condition"/> is false.
/// </typeparam>
/// <param name="condition">The precondition.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("condition:false=>stop")]
public static void Requires<TException>(bool condition) where TException : Exception, new()
{
if (!condition)
{
throw new TException();
}
}
}
}

4
src/Avalonia.Base/Data/InstancedBinding.cs

@ -28,11 +28,9 @@ namespace Avalonia.Data
/// </remarks> /// </remarks>
public InstancedBinding(ISubject<object?> subject, BindingMode mode, BindingPriority priority) public InstancedBinding(ISubject<object?> subject, BindingMode mode, BindingPriority priority)
{ {
Contract.Requires<ArgumentNullException>(subject != null);
Mode = mode; Mode = mode;
Priority = priority; Priority = priority;
Value = subject; Value = subject ?? throw new ArgumentNullException(nameof(subject));
} }
private InstancedBinding(object? value, BindingMode mode, BindingPriority priority) private InstancedBinding(object? value, BindingMode mode, BindingPriority priority)

128
src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs

@ -0,0 +1,128 @@
using Avalonia.Input.GestureRecognizers;
namespace Avalonia.Input
{
public class PinchGestureRecognizer : StyledElement, IGestureRecognizer
{
private IInputElement? _target;
private IGestureRecognizerActionsDispatcher? _actions;
private float _initialDistance;
private IPointer? _firstContact;
private Point _firstPoint;
private IPointer? _secondContact;
private Point _secondPoint;
private Point _origin;
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
{
_target = target;
_actions = actions;
}
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
PointerPressed(e);
}
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
PointerReleased(e);
}
public void PointerCaptureLost(IPointer pointer)
{
RemoveContact(pointer);
}
public void PointerMoved(PointerEventArgs e)
{
if (_target != null && _target is Visual visual)
{
if(_firstContact == e.Pointer)
{
_firstPoint = e.GetPosition(visual);
}
else if (_secondContact == e.Pointer)
{
_secondPoint = e.GetPosition(visual);
}
else
{
return;
}
if (_firstContact != null && _secondContact != null)
{
var distance = GetDistance(_firstPoint, _secondPoint);
var scale = distance / _initialDistance;
_target?.RaiseEvent(new PinchEventArgs(scale, _origin));
}
}
}
public void PointerPressed(PointerPressedEventArgs e)
{
if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
{
if (_firstContact == null)
{
_firstContact = e.Pointer;
_firstPoint = e.GetPosition(visual);
return;
}
else if (_secondContact == null && _firstContact != e.Pointer)
{
_secondContact = e.Pointer;
_secondPoint = e.GetPosition(visual);
}
else
{
return;
}
if (_firstContact != null && _secondContact != null)
{
_initialDistance = GetDistance(_firstPoint, _secondPoint);
_origin = new Point((_firstPoint.X + _secondPoint.X) / 2.0f, (_firstPoint.Y + _secondPoint.Y) / 2.0f);
_actions!.Capture(_firstContact, this);
_actions!.Capture(_secondContact, this);
}
}
}
public void PointerReleased(PointerReleasedEventArgs e)
{
RemoveContact(e.Pointer);
}
private void RemoveContact(IPointer pointer)
{
if (_firstContact == pointer || _secondContact == pointer)
{
if (_secondContact == pointer)
{
_secondContact = null;
}
if (_firstContact == pointer)
{
_firstContact = _secondContact;
_secondContact = null;
}
_target?.RaiseEvent(new PinchEndedEventArgs());
}
}
private float GetDistance(Point a, Point b)
{
var length = _secondPoint - _firstPoint;
return (float)new Vector(length.X, length.Y).Length;
}
}
}

33
src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs

@ -1,15 +1,19 @@
using Avalonia.Input.GestureRecognizers; using System;
using Avalonia.Input.GestureRecognizers;
namespace Avalonia.Input namespace Avalonia.Input
{ {
public class PullGestureRecognizer : StyledElement, IGestureRecognizer public class PullGestureRecognizer : StyledElement, IGestureRecognizer
{ {
internal static int MinPullDetectionSize = 50;
private IInputElement? _target; private IInputElement? _target;
private IGestureRecognizerActionsDispatcher? _actions; private IGestureRecognizerActionsDispatcher? _actions;
private Point _initialPosition; private Point _initialPosition;
private int _gestureId; private int _gestureId;
private IPointer? _tracking; private IPointer? _tracking;
private PullDirection _pullDirection; private PullDirection _pullDirection;
private bool _pullInProgress;
/// <summary> /// <summary>
/// Defines the <see cref="PullDirection"/> property. /// Defines the <see cref="PullDirection"/> property.
@ -31,23 +35,12 @@ namespace Avalonia.Input
PullDirection = pullDirection; PullDirection = pullDirection;
} }
public PullGestureRecognizer() { }
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
{ {
_target = target; _target = target;
_actions = actions; _actions = actions;
_target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
_target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
}
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
PointerPressed(e);
}
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
PointerReleased(e);
} }
public void PointerCaptureLost(IPointer pointer) public void PointerCaptureLost(IPointer pointer)
@ -94,6 +87,7 @@ namespace Avalonia.Input
break; break;
} }
_pullInProgress = true;
_target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection)); _target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection));
} }
} }
@ -111,16 +105,16 @@ namespace Avalonia.Input
switch (PullDirection) switch (PullDirection)
{ {
case PullDirection.TopToBottom: case PullDirection.TopToBottom:
canPull = position.Y < bounds.Height * 0.1; canPull = position.Y < Math.Max(MinPullDetectionSize, bounds.Height * 0.1);
break; break;
case PullDirection.BottomToTop: case PullDirection.BottomToTop:
canPull = position.Y > bounds.Height - (bounds.Height * 0.1); canPull = position.Y > Math.Min(bounds.Height - MinPullDetectionSize, bounds.Height - (bounds.Height * 0.1));
break; break;
case PullDirection.LeftToRight: case PullDirection.LeftToRight:
canPull = position.X < bounds.Width * 0.1; canPull = position.X < Math.Max(MinPullDetectionSize, bounds.Width * 0.1);
break; break;
case PullDirection.RightToLeft: case PullDirection.RightToLeft:
canPull = position.X > bounds.Width - (bounds.Width * 0.1); canPull = position.X > Math.Min(bounds.Width - MinPullDetectionSize, bounds.Width - (bounds.Width * 0.1));
break; break;
} }
@ -135,7 +129,7 @@ namespace Avalonia.Input
public void PointerReleased(PointerReleasedEventArgs e) public void PointerReleased(PointerReleasedEventArgs e)
{ {
if (_tracking == e.Pointer) if (_tracking == e.Pointer && _pullInProgress)
{ {
EndPull(); EndPull();
} }
@ -145,6 +139,7 @@ namespace Avalonia.Input
{ {
_tracking = null; _tracking = null;
_initialPosition = default; _initialPosition = default;
_pullInProgress = false;
_target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection)); _target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection));
} }

65
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -16,7 +16,10 @@ namespace Avalonia.Input.GestureRecognizers
private bool _canHorizontallyScroll; private bool _canHorizontallyScroll;
private bool _canVerticallyScroll; private bool _canVerticallyScroll;
private int _gestureId; private int _gestureId;
private int _scrollStartDistance = 30;
private Point _pointerPressedPoint;
private VelocityTracker? _velocityTracker;
// Movement per second // Movement per second
private Vector _inertia; private Vector _inertia;
private ulong? _lastMoveTimestamp; private ulong? _lastMoveTimestamp;
@ -38,6 +41,15 @@ namespace Avalonia.Input.GestureRecognizers
nameof(CanVerticallyScroll), nameof(CanVerticallyScroll),
o => o.CanVerticallyScroll, o => o.CanVerticallyScroll,
(o, v) => o.CanVerticallyScroll = v); (o, v) => o.CanVerticallyScroll = v);
/// <summary>
/// Defines the <see cref="ScrollStartDistance"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, int> ScrollStartDistanceProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, int>(
nameof(ScrollStartDistance),
o => o.ScrollStartDistance,
(o, v) => o.ScrollStartDistance = v);
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally. /// Gets or sets a value indicating whether the content can be scrolled horizontally.
@ -56,6 +68,15 @@ namespace Avalonia.Input.GestureRecognizers
get => _canVerticallyScroll; get => _canVerticallyScroll;
set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
} }
/// <summary>
/// Gets or sets a value indicating the distance the pointer moves before scrolling is started
/// </summary>
public int ScrollStartDistance
{
get => _scrollStartDistance;
set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value);
}
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions) public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
@ -72,12 +93,9 @@ namespace Avalonia.Input.GestureRecognizers
EndGesture(); EndGesture();
_tracking = e.Pointer; _tracking = e.Pointer;
_gestureId = ScrollGestureEventArgs.GetNextFreeId(); _gestureId = ScrollGestureEventArgs.GetNextFreeId();
_trackedRootPoint = e.GetPosition((Visual?)_target); _trackedRootPoint = _pointerPressedPoint = e.GetPosition((Visual?)_target);
} }
} }
// Arbitrary chosen value, probably need to move that to platform settings or something
private const double ScrollStartDistance = 30;
// Pixels per second speed that is considered to be the stop of inertial scroll // Pixels per second speed that is considered to be the stop of inertial scroll
private const double InertialScrollSpeedEnd = 5; private const double InertialScrollSpeedEnd = 5;
@ -95,6 +113,13 @@ namespace Avalonia.Input.GestureRecognizers
_scrolling = true; _scrolling = true;
if (_scrolling) if (_scrolling)
{ {
_velocityTracker = new VelocityTracker();
// Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance
_trackedRootPoint = new Point(
_trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? _scrollStartDistance : -_scrollStartDistance),
_trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? _scrollStartDistance : -_scrollStartDistance));
_actions!.Capture(e.Pointer, this); _actions!.Capture(e.Pointer, this);
} }
} }
@ -102,14 +127,11 @@ namespace Avalonia.Input.GestureRecognizers
if (_scrolling) if (_scrolling)
{ {
var vector = _trackedRootPoint - rootPoint; var vector = _trackedRootPoint - rootPoint;
var elapsed = _lastMoveTimestamp.HasValue && _lastMoveTimestamp < e.Timestamp ?
TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) : _velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), _pointerPressedPoint - rootPoint);
TimeSpan.Zero;
_lastMoveTimestamp = e.Timestamp; _lastMoveTimestamp = e.Timestamp;
_trackedRootPoint = rootPoint; _trackedRootPoint = rootPoint;
if (elapsed.TotalSeconds > 0)
_inertia = vector / elapsed.TotalSeconds;
_target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
e.Handled = true; e.Handled = true;
} }
@ -134,12 +156,14 @@ namespace Avalonia.Input.GestureRecognizers
} }
} }
public void PointerReleased(PointerReleasedEventArgs e) public void PointerReleased(PointerReleasedEventArgs e)
{ {
if (e.Pointer == _tracking && _scrolling) if (e.Pointer == _tracking && _scrolling)
{ {
_inertia = _velocityTracker?.GetFlingVelocity().PixelsPerSecond ?? Vector.Zero;
e.Handled = true; e.Handled = true;
if (_inertia == default if (_inertia == default
|| e.Timestamp == 0 || e.Timestamp == 0
@ -167,9 +191,18 @@ namespace Avalonia.Input.GestureRecognizers
var distance = speed * elapsedSinceLastTick.TotalSeconds; var distance = speed * elapsedSinceLastTick.TotalSeconds;
_target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance));
// EndGesture using InertialScrollSpeedEnd only in the direction of scrolling
if (CanVerticallyScroll && CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd) {
EndGesture();
return false;
}
else if (CanVerticallyScroll && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
{
EndGesture();
return false;
}
else if (CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd)
{ {
EndGesture(); EndGesture();
return false; return false;

375
src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs

@ -0,0 +1,375 @@
// Code in this file is derived from
// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/velocity_tracker.dart
using System;
using System.Diagnostics;
using Avalonia.Utilities;
namespace Avalonia.Input.GestureRecognizers
{
// Possible enhancement: add Flutter's 'IOSScrollViewFlingVelocityTracker' and 'MacOSScrollViewFlingVelocityTracker'?
internal readonly record struct Velocity(Vector PixelsPerSecond)
{
public Velocity ClampMagnitude(double minValue, double maxValue)
{
Debug.Assert(minValue >= 0.0);
Debug.Assert(maxValue >= 0.0 && maxValue >= minValue);
double valueSquared = PixelsPerSecond.SquaredLength;
if (valueSquared > maxValue * maxValue)
{
double length = PixelsPerSecond.Length;
return new Velocity(length != 0.0 ? (PixelsPerSecond / length) * maxValue : Vector.Zero);
// preventing double.NaN in Vector PixelsPerSecond is important -- if a NaN eventually gets into a
// ScrollGestureEventArgs it results in runtime errors.
}
if (valueSquared < minValue * minValue)
{
double length = PixelsPerSecond.Length;
return new Velocity(length != 0.0 ? (PixelsPerSecond / length) * minValue : Vector.Zero);
}
return this;
}
}
/// A two dimensional velocity estimate.
///
/// VelocityEstimates are computed by [VelocityTracker.getVelocityEstimate]. An
/// estimate's [confidence] measures how well the velocity tracker's position
/// data fit a straight line, [duration] is the time that elapsed between the
/// first and last position sample used to compute the velocity, and [offset]
/// is similarly the difference between the first and last positions.
///
/// See also:
///
/// * [VelocityTracker], which computes [VelocityEstimate]s.
/// * [Velocity], which encapsulates (just) a velocity vector and provides some
/// useful velocity operations.
internal record VelocityEstimate(Vector PixelsPerSecond, double Confidence, TimeSpan Duration, Vector Offset);
internal record struct PointAtTime(bool Valid, Vector Point, TimeSpan Time);
/// Computes a pointer's velocity based on data from [PointerMoveEvent]s.
///
/// The input data is provided by calling [addPosition]. Adding data is cheap.
///
/// To obtain a velocity, call [getVelocity] or [getVelocityEstimate]. This will
/// compute the velocity based on the data added so far. Only call these when
/// you need to use the velocity, as they are comparatively expensive.
///
/// The quality of the velocity estimation will be better if more data points
/// have been received.
internal class VelocityTracker
{
private const int AssumePointerMoveStoppedMilliseconds = 40;
private const int HistorySize = 20;
private const int HorizonMilliseconds = 100;
private const int MinSampleSize = 3;
private const double MinFlingVelocity = 50.0; // Logical pixels / second
private const double MaxFlingVelocity = 8000.0;
private readonly PointAtTime[] _samples = new PointAtTime[HistorySize];
private int _index = 0;
/// <summary>
/// Adds a position as the given time to the tracker.
/// </summary>
/// <param name="time"></param>
/// <param name="position"></param>
public void AddPosition(TimeSpan time, Vector position)
{
_index++;
if (_index == HistorySize)
{
_index = 0;
}
_samples[_index] = new PointAtTime(true, position, time);
}
/// Returns an estimate of the velocity of the object being tracked by the
/// tracker given the current information available to the tracker.
///
/// Information is added using [addPosition].
///
/// Returns null if there is no data on which to base an estimate.
protected virtual VelocityEstimate? GetVelocityEstimate()
{
Span<double> x = stackalloc double[HistorySize];
Span<double> y = stackalloc double[HistorySize];
Span<double> w = stackalloc double[HistorySize];
Span<double> time = stackalloc double[HistorySize];
int sampleCount = 0;
int index = _index;
var newestSample = _samples[index];
if (!newestSample.Valid)
{
return null;
}
var previousSample = newestSample;
var oldestSample = newestSample;
// Starting with the most recent PointAtTime sample, iterate backwards while
// the samples represent continuous motion.
do
{
var sample = _samples[index];
if (!sample.Valid)
{
break;
}
double age = (newestSample.Time - sample.Time).TotalMilliseconds;
double delta = Math.Abs((sample.Time - previousSample.Time).TotalMilliseconds);
previousSample = sample;
if (age > HorizonMilliseconds || delta > AssumePointerMoveStoppedMilliseconds)
{
break;
}
oldestSample = sample;
var position = sample.Point;
x[sampleCount] = position.X;
y[sampleCount] = position.Y;
w[sampleCount] = 1.0;
time[sampleCount] = -age;
index = (index == 0 ? HistorySize : index) - 1;
sampleCount++;
} while (sampleCount < HistorySize);
if (sampleCount >= MinSampleSize)
{
var xFit = LeastSquaresSolver.Solve(2, time.Slice(0, sampleCount), x.Slice(0, sampleCount), w.Slice(0, sampleCount));
if (xFit != null)
{
var yFit = LeastSquaresSolver.Solve(2, time.Slice(0, sampleCount), y.Slice(0, sampleCount), w.Slice(0, sampleCount));
if (yFit != null)
{
return new VelocityEstimate( // convert from pixels/ms to pixels/s
PixelsPerSecond: new Vector(xFit.Coefficients[1] * 1000, yFit.Coefficients[1] * 1000),
Confidence: xFit.Confidence * yFit.Confidence,
Duration: newestSample.Time - oldestSample.Time,
Offset: newestSample.Point - oldestSample.Point
);
}
}
}
// We're unable to make a velocity estimate but we did have at least one
// valid pointer position.
return new VelocityEstimate(
PixelsPerSecond: Vector.Zero,
Confidence: 1.0,
Duration: newestSample.Time - oldestSample.Time,
Offset: newestSample.Point - oldestSample.Point
);
}
/// <summary>
/// Computes the velocity of the pointer at the time of the last
/// provided data point.
///
/// This can be expensive. Only call this when you need the velocity.
///
/// Returns [Velocity.zero] if there is no data from which to compute an
/// estimate or if the estimated velocity is zero.///
/// </summary>
/// <returns></returns>
internal Velocity GetVelocity()
{
var estimate = GetVelocityEstimate();
if (estimate == null || estimate.PixelsPerSecond.IsDefault)
{
return new Velocity(Vector.Zero);
}
return new Velocity(estimate.PixelsPerSecond);
}
internal virtual Velocity GetFlingVelocity()
{
return GetVelocity().ClampMagnitude(MinFlingVelocity, MaxFlingVelocity);
}
}
/// An nth degree polynomial fit to a dataset.
internal class PolynomialFit
{
/// Creates a polynomial fit of the given degree.
///
/// There are n + 1 coefficients in a fit of degree n.
internal PolynomialFit(int degree)
{
Coefficients = new double[degree + 1];
}
/// The polynomial coefficients of the fit.
public double[] Coefficients { get; }
/// An indicator of the quality of the fit.
///
/// Larger values indicate greater quality.
public double Confidence { get; set; }
}
internal class LeastSquaresSolver
{
private const double PrecisionErrorTolerance = 1e-10;
/// <summary>
/// Fits a polynomial of the given degree to the data points.
/// When there is not enough data to fit a curve null is returned.
/// </summary>
public static PolynomialFit? Solve(int degree, ReadOnlySpan<double> x, ReadOnlySpan<double> y, ReadOnlySpan<double> w)
{
if (degree > x.Length)
{
// Not enough data to fit a curve.
return null;
}
PolynomialFit result = new PolynomialFit(degree);
// Shorthands for the purpose of notation equivalence to original C++ code.
int m = x.Length;
int n = degree + 1;
// Expand the X vector to a matrix A, pre-multiplied by the weights.
_Matrix a = new _Matrix(m, stackalloc double[n * m]);
for (int h = 0; h < m; h += 1)
{
a[0, h] = w[h];
for (int i = 1; i < n; i += 1)
{
a[i, h] = a[i - 1, h] * x[h];
}
}
// Apply the Gram-Schmidt process to A to obtain its QR decomposition.
// Orthonormal basis, column-major order Vector.
_Matrix q = new _Matrix(m, stackalloc double[n * m]);
// Upper triangular matrix, row-major order.
_Matrix r = new _Matrix(n, stackalloc double[n * n]);
for (int j = 0; j < n; j += 1)
{
for (int h = 0; h < m; h += 1)
{
q[j, h] = a[j, h];
}
for (int i = 0; i < j; i += 1)
{
double dot = Multiply(q.GetRow(j), q.GetRow(i));
for (int h = 0; h < m; h += 1)
{
q[j, h] = q[j, h] - dot * q[i, h];
}
}
double norm = Norm(q.GetRow(j));
if (norm < PrecisionErrorTolerance)
{
// Vectors are linearly dependent or zero so no solution.
return null;
}
double inverseNorm = 1.0 / norm;
for (int h = 0; h < m; h += 1)
{
q[j, h] = q[j, h] * inverseNorm;
}
for (int i = 0; i < n; i += 1)
{
r[j, i] = i < j ? 0.0 : Multiply(q.GetRow(j), a.GetRow(i));
}
}
// Solve R B = Qt W Y to find B. This is easy because R is upper triangular.
// We just work from bottom-right to top-left calculating B's coefficients.
// "m" isn't expected to be bigger than HistorySize=20, so allocation on stack is safe.
Span<double> wy = stackalloc double[m];
for (int h = 0; h < m; h += 1)
{
wy[h] = y[h] * w[h];
}
for (int i = n - 1; i >= 0; i -= 1)
{
result.Coefficients[i] = Multiply(q.GetRow(i), wy);
for (int j = n - 1; j > i; j -= 1)
{
result.Coefficients[i] -= r[i, j] * result.Coefficients[j];
}
result.Coefficients[i] /= r[i, i];
}
// Calculate the coefficient of determination (confidence) as:
// 1 - (sumSquaredError / sumSquaredTotal)
// ...where sumSquaredError is the residual sum of squares (variance of the
// error), and sumSquaredTotal is the total sum of squares (variance of the
// data) where each has been weighted.
double yMean = 0.0;
for (int h = 0; h < m; h += 1)
{
yMean += y[h];
}
yMean /= m;
double sumSquaredError = 0.0;
double sumSquaredTotal = 0.0;
for (int h = 0; h < m; h += 1)
{
double term = 1.0;
double err = y[h] - result.Coefficients[0];
for (int i = 1; i < n; i += 1)
{
term *= x[h];
err -= term * result.Coefficients[i];
}
sumSquaredError += w[h] * w[h] * err * err;
double v = y[h] - yMean;
sumSquaredTotal += w[h] * w[h] * v * v;
}
result.Confidence = sumSquaredTotal <= PrecisionErrorTolerance ? 1.0 :
1.0 - (sumSquaredError / sumSquaredTotal);
return result;
}
private static double Multiply(Span<double> v1, Span<double> v2)
{
double result = 0.0;
for (int i = 0; i < v1.Length; i += 1)
{
result += v1[i] * v2[i];
}
return result;
}
private static double Norm(Span<double> v)
{
return Math.Sqrt(Multiply(v, v));
}
private readonly ref struct _Matrix
{
private readonly int _columns;
private readonly Span<double> _elements;
internal _Matrix(int cols, Span<double> elements)
{
_columns = cols;
_elements = elements;
}
public double this[int row, int col]
{
get => _elements[row * _columns + col];
set => _elements[row * _columns + col] = value;
}
public Span<double> GetRow(int row) => _elements.Slice(row * _columns, _columns);
}
}
}

129
src/Avalonia.Base/Input/Gestures.cs

@ -1,6 +1,8 @@
using System; using System;
using System.Threading;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
@ -8,6 +10,21 @@ namespace Avalonia.Input
public static class Gestures public static class Gestures
{ {
private static bool s_isDoubleTapped = false; private static bool s_isDoubleTapped = false;
private static bool s_isHolding;
private static CancellationTokenSource? s_holdCancellationToken;
/// <summary>
/// Defines the IsHoldingEnabled attached property.
/// </summary>
public static readonly AttachedProperty<bool> IsHoldingEnabledProperty =
AvaloniaProperty.RegisterAttached<StyledElement, bool>("IsHoldingEnabled", typeof(Gestures), true);
/// <summary>
/// Defines the IsHoldWithMouseEnabled attached property.
/// </summary>
public static readonly AttachedProperty<bool> IsHoldWithMouseEnabledProperty =
AvaloniaProperty.RegisterAttached<StyledElement, bool>("IsHoldWithMouseEnabled", typeof(Gestures), false);
public static readonly RoutedEvent<TappedEventArgs> TappedEvent = RoutedEvent.Register<TappedEventArgs>( public static readonly RoutedEvent<TappedEventArgs> TappedEvent = RoutedEvent.Register<TappedEventArgs>(
"Tapped", "Tapped",
RoutingStrategies.Bubble, RoutingStrategies.Bubble,
@ -45,19 +62,54 @@ namespace Avalonia.Input
private static readonly WeakReference<object?> s_lastPress = new WeakReference<object?>(null); private static readonly WeakReference<object?> s_lastPress = new WeakReference<object?>(null);
private static Point s_lastPressPoint; private static Point s_lastPressPoint;
private static IPointer? s_lastPointer;
public static readonly RoutedEvent<PinchEventArgs> PinchEvent =
RoutedEvent.Register<PinchEventArgs>(
"PinchEvent", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<PinchEndedEventArgs> PinchEndedEvent =
RoutedEvent.Register<PinchEndedEventArgs>(
"PinchEndedEvent", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<PullGestureEventArgs> PullGestureEvent = public static readonly RoutedEvent<PullGestureEventArgs> PullGestureEvent =
RoutedEvent.Register<PullGestureEventArgs>( RoutedEvent.Register<PullGestureEventArgs>(
"PullGesture", RoutingStrategies.Bubble, typeof(Gestures)); "PullGesture", RoutingStrategies.Bubble, typeof(Gestures));
/// <summary>
/// Occurs when a user performs a press and hold gesture (with a single touch, mouse, or pen/stylus contact).
/// </summary>
public static readonly RoutedEvent<HoldingRoutedEventArgs> HoldingEvent =
RoutedEvent.Register<HoldingRoutedEventArgs>(
"Holding", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<PullGestureEndedEventArgs> PullGestureEndedEvent = public static readonly RoutedEvent<PullGestureEndedEventArgs> PullGestureEndedEvent =
RoutedEvent.Register<PullGestureEndedEventArgs>( RoutedEvent.Register<PullGestureEndedEventArgs>(
"PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); "PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
public static bool GetIsHoldingEnabled(StyledElement element)
{
return element.GetValue(IsHoldingEnabledProperty);
}
public static void SetIsHoldingEnabled(StyledElement element, bool value)
{
element.SetValue(IsHoldingEnabledProperty, value);
}
public static bool GetIsHoldWithMouseEnabled(StyledElement element)
{
return element.GetValue(IsHoldWithMouseEnabledProperty);
}
public static void SetIsHoldWithMouseEnabled(StyledElement element, bool value)
{
element.SetValue(IsHoldWithMouseEnabledProperty, value);
}
static Gestures() static Gestures()
{ {
InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed);
InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased);
InputElement.PointerMovedEvent.RouteFinished.Subscribe(PointerMoved);
} }
public static void AddTappedHandler(Interactive element, EventHandler<RoutedEventArgs> handler) public static void AddTappedHandler(Interactive element, EventHandler<RoutedEventArgs> handler)
@ -102,11 +154,42 @@ namespace Avalonia.Input
var e = (PointerPressedEventArgs)ev; var e = (PointerPressedEventArgs)ev;
var visual = (Visual)ev.Source; var visual = (Visual)ev.Source;
if(s_lastPointer != null)
{
if(s_isHolding && ev.Source is Interactive i)
{
i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_lastPointer.Type));
}
s_holdCancellationToken?.Cancel();
s_holdCancellationToken?.Dispose();
s_holdCancellationToken = null;
s_lastPointer = null;
}
s_isHolding = false;
if (e.ClickCount % 2 == 1) if (e.ClickCount % 2 == 1)
{ {
s_isDoubleTapped = false; s_isDoubleTapped = false;
s_lastPress.SetTarget(ev.Source); s_lastPress.SetTarget(ev.Source);
s_lastPointer = e.Pointer;
s_lastPressPoint = e.GetPosition((Visual)ev.Source); s_lastPressPoint = e.GetPosition((Visual)ev.Source);
s_holdCancellationToken = new CancellationTokenSource();
var token = s_holdCancellationToken.Token;
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
if (settings != null)
{
DispatcherTimer.RunOnce(() =>
{
if (!token.IsCancellationRequested && e.Source is InputElement i && GetIsHoldingEnabled(i) && (e.Pointer.Type != PointerType.Mouse || GetIsHoldWithMouseEnabled(i)))
{
s_isHolding = true;
i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Started, s_lastPressPoint, s_lastPointer.Type));
}
}, settings.HoldWaitDuration);
}
} }
else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
{ {
@ -140,7 +223,12 @@ namespace Avalonia.Input
if (tapRect.ContainsExclusive(point.Position)) if (tapRect.ContainsExclusive(point.Position))
{ {
if (e.InitialPressMouseButton == MouseButton.Right) if(s_isHolding)
{
s_isHolding = false;
i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Completed, s_lastPressPoint, s_lastPointer!.Type));
}
else if (e.InitialPressMouseButton == MouseButton.Right)
{ {
i.RaiseEvent(new TappedEventArgs(RightTappedEvent, e)); i.RaiseEvent(new TappedEventArgs(RightTappedEvent, e));
} }
@ -152,6 +240,45 @@ namespace Avalonia.Input
} }
} }
} }
s_holdCancellationToken?.Cancel();
s_holdCancellationToken?.Dispose();
s_holdCancellationToken = null;
s_lastPointer = null;
}
}
private static void PointerMoved(RoutedEventArgs ev)
{
if (ev.Route == RoutingStrategies.Bubble)
{
var e = (PointerEventArgs)ev;
if (s_lastPress.TryGetTarget(out var target))
{
if (e.Pointer == s_lastPointer)
{
var point = e.GetCurrentPoint((Visual)target);
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
var tapSize = settings?.GetTapSize(point.Pointer.Type) ?? new Size(4, 4);
var tapRect = new Rect(s_lastPressPoint, new Size())
.Inflate(new Thickness(tapSize.Width, tapSize.Height));
if (tapRect.ContainsExclusive(point.Position))
{
return;
}
if (s_isHolding && ev.Source is Interactive i)
{
i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_lastPointer!.Type));
}
}
}
s_holdCancellationToken?.Cancel();
s_holdCancellationToken?.Dispose();
s_holdCancellationToken = null;
s_isHolding = false;
} }
} }
} }

51
src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs

@ -0,0 +1,51 @@
using System;
using Avalonia.Interactivity;
namespace Avalonia.Input
{
public class HoldingRoutedEventArgs : RoutedEventArgs
{
/// <summary>
/// Gets the state of the <see cref="Gestures.HoldingEvent"/> event.
/// </summary>
public HoldingState HoldingState { get; }
/// <summary>
/// Gets the location of the touch, mouse, or pen/stylus contact.
/// </summary>
public Point Position { get; }
/// <summary>
/// Gets the pointer type of the input source.
/// </summary>
public PointerType PointerType { get; }
/// <summary>
/// Initializes a new instance of the <see cref="HoldingRoutedEventArgs"/> class.
/// </summary>
public HoldingRoutedEventArgs(HoldingState holdingState, Point position, PointerType pointerType) : base(Gestures.HoldingEvent)
{
HoldingState = holdingState;
Position = position;
PointerType = pointerType;
}
}
public enum HoldingState
{
/// <summary>
/// A single contact has been detected and a time threshold is crossed without the contact being lifted, another contact detected, or another gesture started.
/// </summary>
Started,
/// <summary>
/// The single contact is lifted.
/// </summary>
Completed,
/// <summary>
/// An additional contact is detected or a subsequent gesture (such as a slide) is detected.
/// </summary>
Cancelled,
}
}

16
src/Avalonia.Base/Input/InputElement.cs

@ -188,11 +188,16 @@ namespace Avalonia.Input
/// </summary> /// </summary>
public static readonly RoutedEvent<TappedEventArgs> TappedEvent = Gestures.TappedEvent; public static readonly RoutedEvent<TappedEventArgs> TappedEvent = Gestures.TappedEvent;
/// <summary>
/// Defines the <see cref="Holding"/> event.
/// </summary>
public static readonly RoutedEvent<HoldingRoutedEventArgs> HoldingEvent = Gestures.HoldingEvent;
/// <summary> /// <summary>
/// Defines the <see cref="DoubleTapped"/> event. /// Defines the <see cref="DoubleTapped"/> event.
/// </summary> /// </summary>
public static readonly RoutedEvent<TappedEventArgs> DoubleTappedEvent = Gestures.DoubleTappedEvent; public static readonly RoutedEvent<TappedEventArgs> DoubleTappedEvent = Gestures.DoubleTappedEvent;
private bool _isEffectivelyEnabled = true; private bool _isEffectivelyEnabled = true;
private bool _isFocused; private bool _isFocused;
private bool _isKeyboardFocusWithin; private bool _isKeyboardFocusWithin;
@ -352,6 +357,15 @@ namespace Avalonia.Input
add { AddHandler(TappedEvent, value); } add { AddHandler(TappedEvent, value); }
remove { RemoveHandler(TappedEvent, value); } remove { RemoveHandler(TappedEvent, value); }
} }
/// <summary>
/// Occurs when a hold gesture occurs on the control.
/// </summary>
public event EventHandler<HoldingRoutedEventArgs>? Holding
{
add { AddHandler(HoldingEvent, value); }
remove { RemoveHandler(HoldingEvent, value); }
}
/// <summary> /// <summary>
/// Occurs when a double-tap gesture occurs on the control. /// Occurs when a double-tap gesture occurs on the control.

24
src/Avalonia.Base/Input/PinchEventArgs.cs

@ -0,0 +1,24 @@
using Avalonia.Interactivity;
namespace Avalonia.Input
{
public class PinchEventArgs : RoutedEventArgs
{
public PinchEventArgs(double scale, Point scaleOrigin) : base(Gestures.PinchEvent)
{
Scale = scale;
ScaleOrigin = scaleOrigin;
}
public double Scale { get; } = 1;
public Point ScaleOrigin { get; }
}
public class PinchEndedEventArgs : RoutedEventArgs
{
public PinchEndedEventArgs() : base(Gestures.PinchEndedEvent)
{
}
}
}

6
src/Avalonia.Base/Input/Raw/RawInputEventArgs.cs

@ -21,11 +21,9 @@ namespace Avalonia.Input.Raw
/// <param name="root">The root from which the event originates.</param> /// <param name="root">The root from which the event originates.</param>
public RawInputEventArgs(IInputDevice device, ulong timestamp, IInputRoot root) public RawInputEventArgs(IInputDevice device, ulong timestamp, IInputRoot root)
{ {
device = device ?? throw new ArgumentNullException(nameof(device)); Device = device ?? throw new ArgumentNullException(nameof(device));
Device = device;
Timestamp = timestamp; Timestamp = timestamp;
Root = root; Root = root ?? throw new ArgumentNullException(nameof(root));
} }
/// <summary> /// <summary>

6
src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs

@ -53,9 +53,6 @@ namespace Avalonia.Input.Raw
RawInputModifiers inputModifiers) RawInputModifiers inputModifiers)
: base(device, timestamp, root) : base(device, timestamp, root)
{ {
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
Point = new RawPointerPoint(); Point = new RawPointerPoint();
Position = position; Position = position;
Type = type; Type = type;
@ -80,9 +77,6 @@ namespace Avalonia.Input.Raw
RawInputModifiers inputModifiers) RawInputModifiers inputModifiers)
: base(device, timestamp, root) : base(device, timestamp, root)
{ {
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
Point = point; Point = point;
Type = type; Type = type;
InputModifiers = inputModifiers; InputModifiers = inputModifiers;

5
src/Avalonia.Base/Logging/LogArea.cs

@ -35,6 +35,11 @@ namespace Avalonia.Logging
/// </summary> /// </summary>
public const string Control = nameof(Control); public const string Control = nameof(Control);
/// <summary>
/// The log event comes from Win32 Platform.
/// </summary>
public const string Platform = nameof(Platform);
/// <summary> /// <summary>
/// The log event comes from Win32 Platform. /// The log event comes from Win32 Platform.
/// </summary> /// </summary>

7
src/Avalonia.Base/Media/DrawingGroup.cs

@ -461,9 +461,10 @@ namespace Avalonia.Media
if (_rootDrawing == null) if (_rootDrawing == null)
{ {
// When a DrawingGroup is set, it should be made the root if if (_currentDrawingGroup != null)
// a root drawing didnt exist. {
Contract.Requires<NotSupportedException>(_currentDrawingGroup == null); throw new NotSupportedException("When a DrawingGroup is set, it should be made the root if a root drawing didnt exist.");
}
// If this is the first Drawing being added, avoid creating a DrawingGroup // If this is the first Drawing being added, avoid creating a DrawingGroup
// and set this drawing as the root drawing. This optimizes the common // and set this drawing as the root drawing. This optimizes the common

3
src/Avalonia.Base/Media/Typeface.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using JetBrains.Annotations;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -17,7 +16,7 @@ namespace Avalonia.Media
/// <param name="style">The font style.</param> /// <param name="style">The font style.</param>
/// <param name="weight">The font weight.</param> /// <param name="weight">The font weight.</param>
/// <param name="stretch">The font stretch.</param> /// <param name="stretch">The font stretch.</param>
public Typeface([NotNull] FontFamily fontFamily, public Typeface(FontFamily fontFamily,
FontStyle style = FontStyle.Normal, FontStyle style = FontStyle.Normal,
FontWeight weight = FontWeight.Normal, FontWeight weight = FontWeight.Normal,
FontStretch stretch = FontStretch.Normal) FontStretch stretch = FontStretch.Normal)

2
src/Avalonia.Base/PixelVector.cs

@ -1,7 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
using JetBrains.Annotations;
namespace Avalonia namespace Avalonia
{ {
@ -135,7 +134,6 @@ namespace Avalonia
/// </summary> /// </summary>
/// <param name="other">The other vector.</param> /// <param name="other">The other vector.</param>
/// <returns>True if vectors are nearly equal.</returns> /// <returns>True if vectors are nearly equal.</returns>
[Pure]
public bool NearlyEquals(PixelVector other) public bool NearlyEquals(PixelVector other)
{ {
const float tolerance = float.Epsilon; const float tolerance = float.Epsilon;

2
src/Avalonia.Base/Platform/DefaultPlatformSettings.cs

@ -26,5 +26,7 @@ namespace Avalonia.Platform
}; };
} }
public TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500); public TimeSpan GetDoubleTapTime(PointerType type) => TimeSpan.FromMilliseconds(500);
public TimeSpan HoldWaitDuration { get; set; } = TimeSpan.FromMilliseconds(300);
} }
} }

2
src/Avalonia.Base/Platform/IPlatformSettings.cs

@ -27,5 +27,7 @@ namespace Avalonia.Platform
/// tap gesture. /// tap gesture.
/// </summary> /// </summary>
TimeSpan GetDoubleTapTime(PointerType type); TimeSpan GetDoubleTapTime(PointerType type);
TimeSpan HoldWaitDuration { get; set; }
} }
} }

19
src/Avalonia.Base/Platform/IRuntimePlatform.cs

@ -23,29 +23,10 @@ namespace Avalonia.Platform
[Unstable] [Unstable]
public record struct RuntimePlatformInfo public record struct RuntimePlatformInfo
{ {
public OperatingSystemType OperatingSystem { get; set; }
public FormFactorType FormFactor => IsDesktop ? FormFactorType.Desktop : public FormFactorType FormFactor => IsDesktop ? FormFactorType.Desktop :
IsMobile ? FormFactorType.Mobile : FormFactorType.Unknown; IsMobile ? FormFactorType.Mobile : FormFactorType.Unknown;
public bool IsDesktop { get; set; } public bool IsDesktop { get; set; }
public bool IsMobile { get; set; } public bool IsMobile { get; set; }
public bool IsBrowser { get; set; }
public bool IsCoreClr { get; set; }
public bool IsMono { get; set; }
public bool IsDotNetFramework { get; set; }
public bool IsUnix { get; set; }
}
[Unstable]
public enum OperatingSystemType
{
Unknown,
WinNT,
Linux,
OSX,
Android,
iOS,
Browser
} }
[Unstable] [Unstable]

44
src/Avalonia.Base/Platform/StandardRuntimePlatform.cs

@ -1,6 +1,6 @@
using System; using System;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using Avalonia.Compatibility;
using Avalonia.Platform.Internal; using Avalonia.Platform.Internal;
namespace Avalonia.Platform namespace Avalonia.Platform
@ -14,45 +14,13 @@ namespace Avalonia.Platform
public IUnmanagedBlob AllocBlob(int size) => new UnmanagedBlob(size); public IUnmanagedBlob AllocBlob(int size) => new UnmanagedBlob(size);
private static readonly Lazy<RuntimePlatformInfo> Info = new(() => private static readonly RuntimePlatformInfo s_info = new()
{ {
OperatingSystemType os; IsDesktop = OperatingSystemEx.IsWindows() || OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsLinux(),
IsMobile = OperatingSystemEx.IsAndroid() || OperatingSystemEx.IsIOS()
};
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
os = OperatingSystemType.OSX;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
os = OperatingSystemType.Linux;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
os = OperatingSystemType.WinNT;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("Android")))
os = OperatingSystemType.Android;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("iOS")))
os = OperatingSystemType.iOS;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("Browser")))
os = OperatingSystemType.Browser;
else
throw new Exception("Unknown OS platform " + RuntimeInformation.OSDescription);
// Source: https://github.com/dotnet/runtime/blob/main/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs public virtual RuntimePlatformInfo GetRuntimeInfo() => s_info;
var isCoreClr = Environment.Version.Major >= 5 || RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase);
var isMonoRuntime = Type.GetType("Mono.Runtime") != null;
var isFramework = !isCoreClr && RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase);
return new RuntimePlatformInfo
{
IsCoreClr = isCoreClr,
IsDotNetFramework = isFramework,
IsMono = isMonoRuntime,
IsDesktop = os is OperatingSystemType.Linux or OperatingSystemType.OSX or OperatingSystemType.WinNT,
IsMobile = os is OperatingSystemType.Android or OperatingSystemType.iOS,
IsUnix = os is OperatingSystemType.Linux or OperatingSystemType.OSX or OperatingSystemType.Android,
IsBrowser = os == OperatingSystemType.Browser,
OperatingSystem = os,
};
});
public virtual RuntimePlatformInfo GetRuntimeInfo() => Info.Value;
} }
} }

13
src/Avalonia.Base/Platform/StandardRuntimePlatformServices.cs

@ -1,4 +1,5 @@
using System.Reflection; using System.Reflection;
using Avalonia.Compatibility;
using Avalonia.Platform.Internal; using Avalonia.Platform.Internal;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
@ -18,15 +19,9 @@ namespace Avalonia.Platform
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
new Net6Loader() new Net6Loader()
#else #else
standardPlatform.GetRuntimeInfo().OperatingSystem switch OperatingSystemEx.IsWindows() ? (IDynamicLibraryLoader)new Win32Loader()
{ : OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsLinux() || OperatingSystemEx.IsAndroid() ? new UnixLoader()
OperatingSystemType.WinNT => (IDynamicLibraryLoader)new Win32Loader(), : new NotSupportedLoader()
OperatingSystemType.OSX => new UnixLoader(),
OperatingSystemType.Linux => new UnixLoader(),
OperatingSystemType.Android => new UnixLoader(),
// iOS, WASM, ...
_ => new NotSupportedLoader()
}
#endif #endif
); );
} }

32
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -42,11 +42,18 @@ namespace Avalonia.Rendering.Composition.Server
Root!.RenderedVisuals++; Root!.RenderedVisuals++;
if (Opacity != 1)
canvas.PushOpacity(Opacity);
if (AdornedVisual != null)
{
canvas.PostTransform = Matrix.Identity;
canvas.Transform = Matrix.Identity;
canvas.PushClip(AdornedVisual._combinedTransformedClipBounds);
}
var transform = GlobalTransformMatrix; var transform = GlobalTransformMatrix;
canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.PostTransform = MatrixUtils.ToMatrix(transform);
canvas.Transform = Matrix.Identity; canvas.Transform = Matrix.Identity;
if (Opacity != 1)
canvas.PushOpacity(Opacity);
var boundsRect = new Rect(new Size(Size.X, Size.Y)); var boundsRect = new Rect(new Size(Size.X, Size.Y));
if (ClipToBounds && !HandlesClipToBounds) if (ClipToBounds && !HandlesClipToBounds)
canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
@ -67,6 +74,8 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopGeometryClip(); canvas.PopGeometryClip();
if (ClipToBounds && !HandlesClipToBounds) if (ClipToBounds && !HandlesClipToBounds)
canvas.PopClip(); canvas.PopClip();
if (AdornedVisual != null)
canvas.PopClip();
if(Opacity != 1) if(Opacity != 1)
canvas.PopOpacity(); canvas.PopOpacity();
} }
@ -155,15 +164,25 @@ namespace Avalonia.Rendering.Composition.Server
_clipSizeDirty = false; _clipSizeDirty = false;
} }
_combinedTransformedClipBounds =
AdornedVisual?._combinedTransformedClipBounds
?? Parent?._combinedTransformedClipBounds
?? new Rect(Root!.Size);
_combinedTransformedClipBounds = Parent?._combinedTransformedClipBounds ?? new Rect(Root!.Size);
if (_transformedClipBounds != null) if (_transformedClipBounds != null)
_combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value); _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value);
EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1); EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1);
IsVisibleInFrame = _parent?.IsVisibleInFrame != false && Visible && EffectiveOpacity > 0.04 && !_isBackface && IsHitTestVisibleInFrame = _parent?.IsHitTestVisibleInFrame != false
!_combinedTransformedClipBounds.IsDefault; && Visible
&& !_isBackface
&& !_combinedTransformedClipBounds.IsDefault;
IsVisibleInFrame = IsHitTestVisibleInFrame
&& _parent?.IsVisibleInFrame != false
&& EffectiveOpacity > 0.04;
if (wasVisible != IsVisibleInFrame || positionChanged) if (wasVisible != IsVisibleInFrame || positionChanged)
{ {
@ -187,7 +206,7 @@ namespace Avalonia.Rendering.Composition.Server
readback.Revision = root.Revision; readback.Revision = root.Revision;
readback.Matrix = GlobalTransformMatrix; readback.Matrix = GlobalTransformMatrix;
readback.TargetId = Root.Id; readback.TargetId = Root.Id;
readback.Visible = IsVisibleInFrame; readback.Visible = IsHitTestVisibleInFrame;
} }
void AddDirtyRect(Rect rc) void AddDirtyRect(Rect rc)
@ -248,6 +267,7 @@ namespace Avalonia.Rendering.Composition.Server
} }
public bool IsVisibleInFrame { get; set; } public bool IsVisibleInFrame { get; set; }
public bool IsHitTestVisibleInFrame { get; set; }
public double EffectiveOpacity { get; set; } public double EffectiveOpacity { get; set; }
public Rect TransformedOwnContentBounds { get; set; } public Rect TransformedOwnContentBounds { get; set; }
public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y); public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y);

53
src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
@ -27,6 +28,37 @@ public record struct BatchStreamSegment<TData>
public int ElementCount { get; set; } public int ElementCount { get; set; }
} }
// Unsafe.ReadUnaligned/Unsafe.WriteUnaligned are broken on arm,
// see https://github.com/dotnet/runtime/issues/80068
static unsafe class UnalignedMemoryHelper
{
public static T ReadUnaligned<T>(byte* src) where T : unmanaged
{
#if NET6_0_OR_GREATER
Unsafe.SkipInit<T>(out var rv);
#else
T rv;
#endif
UnalignedMemcpy((byte*)&rv, src, Unsafe.SizeOf<T>());
return rv;
}
public static void WriteUnaligned<T>(byte* dst, T value) where T : unmanaged
{
UnalignedMemcpy(dst, (byte*)&value, Unsafe.SizeOf<T>());
}
[MethodImpl(MethodImplOptions.NoInlining)]
static unsafe void UnalignedMemcpy(byte* dst, byte* src, int count)
{
for (var c = 0; c < count; c++)
{
dst[c] = src[c];
}
}
}
internal class BatchStreamWriter : IDisposable internal class BatchStreamWriter : IDisposable
{ {
private readonly BatchStreamData _output; private readonly BatchStreamData _output;
@ -74,7 +106,15 @@ internal class BatchStreamWriter : IDisposable
var size = Unsafe.SizeOf<T>(); var size = Unsafe.SizeOf<T>();
if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize) if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize)
NextDataSegment(); NextDataSegment();
Unsafe.WriteUnaligned<T>((byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount, item); var ptr = (byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount;
// Unsafe.ReadUnaligned/Unsafe.WriteUnaligned are broken on arm32,
// see https://github.com/dotnet/runtime/issues/80068
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm)
UnalignedMemoryHelper.WriteUnaligned(ptr, item);
else
Unsafe.WriteUnaligned(ptr, item);
_currentDataSegment.ElementCount += size; _currentDataSegment.ElementCount += size;
} }
@ -125,7 +165,16 @@ internal class BatchStreamReader : IDisposable
if (_memoryOffset + size > _currentDataSegment.ElementCount) if (_memoryOffset + size > _currentDataSegment.ElementCount)
throw new InvalidOperationException("Attempted to read more memory then left in the current segment"); throw new InvalidOperationException("Attempted to read more memory then left in the current segment");
var rv = Unsafe.ReadUnaligned<T>((byte*)_currentDataSegment.Data + _memoryOffset); var ptr = (byte*)_currentDataSegment.Data + _memoryOffset;
T rv;
// Unsafe.ReadUnaligned/Unsafe.WriteUnaligned are broken on arm32,
// see https://github.com/dotnet/runtime/issues/80068
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm)
rv = UnalignedMemoryHelper.ReadUnaligned<T>(ptr);
else
rv = Unsafe.ReadUnaligned<T>(ptr);
_memoryOffset += size; _memoryOffset += size;
if (_memoryOffset == _currentDataSegment.ElementCount) if (_memoryOffset == _currentDataSegment.ElementCount)
{ {

5
src/Avalonia.Base/Visual.cs

@ -367,7 +367,10 @@ namespace Avalonia
/// <param name="context">The drawing context.</param> /// <param name="context">The drawing context.</param>
public virtual void Render(DrawingContext context) public virtual void Render(DrawingContext context)
{ {
Contract.Requires<ArgumentNullException>(context != null); if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
} }
/// <summary> /// <summary>

1
src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj

@ -17,7 +17,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\Rx.props" /> <Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\EmbedXaml.props" /> <Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\JetBrains.Annotations.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<!--<Import Project="..\..\build\ApiDiff.props" />--> <!--<Import Project="..\..\build\ApiDiff.props" />-->
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />

1
src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj

@ -14,7 +14,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\Rx.props" /> <Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\EmbedXaml.props" /> <Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\JetBrains.Annotations.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\ApiDiff.props" /> <Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />

4
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -641,7 +641,7 @@ namespace Avalonia.Controls
public Control GetCellContent(DataGridRow dataGridRow) public Control GetCellContent(DataGridRow dataGridRow)
{ {
Contract.Requires<ArgumentNullException>(dataGridRow != null); dataGridRow = dataGridRow ?? throw new ArgumentNullException(nameof(dataGridRow));
if (OwningGrid == null) if (OwningGrid == null)
{ {
throw DataGridError.DataGrid.NoOwningGrid(GetType()); throw DataGridError.DataGrid.NoOwningGrid(GetType());
@ -659,7 +659,7 @@ namespace Avalonia.Controls
public Control GetCellContent(object dataItem) public Control GetCellContent(object dataItem)
{ {
Contract.Requires<ArgumentNullException>(dataItem != null); dataItem = dataItem ?? throw new ArgumentNullException(nameof(dataItem));
if (OwningGrid == null) if (OwningGrid == null)
{ {
throw DataGridError.DataGrid.NoOwningGrid(GetType()); throw DataGridError.DataGrid.NoOwningGrid(GetType());

1
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@ -16,7 +16,6 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Styling; using Avalonia.Styling;
using JetBrains.Annotations;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {

2
src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs

@ -26,7 +26,6 @@ namespace Avalonia.Controls.Primitives
/// <returns>true if the grid is frozen; otherwise, false. The default is true.</returns> /// <returns>true if the grid is frozen; otherwise, false. The default is true.</returns>
public static bool GetIsFrozen(Control element) public static bool GetIsFrozen(Control element)
{ {
Contract.Requires<ArgumentNullException>(element != null);
return element.GetValue(IsFrozenProperty); return element.GetValue(IsFrozenProperty);
} }
@ -38,7 +37,6 @@ namespace Avalonia.Controls.Primitives
/// <exception cref="T:System.ArgumentNullException"><paramref name="element" /> is null.</exception> /// <exception cref="T:System.ArgumentNullException"><paramref name="element" /> is null.</exception>
public static void SetIsFrozen(Control element, bool value) public static void SetIsFrozen(Control element, bool value)
{ {
Contract.Requires<ArgumentNullException>(element != null);
element.SetValue(IsFrozenProperty, value); element.SetValue(IsFrozenProperty, value);
} }
} }

239
src/Avalonia.Controls/AppBuilder.cs

@ -1,4 +1,8 @@
using Avalonia.Controls; using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Linq;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform; using Avalonia.Platform;
namespace Avalonia namespace Avalonia
@ -6,15 +10,244 @@ namespace Avalonia
/// <summary> /// <summary>
/// Initializes platform-specific services for an <see cref="Application"/>. /// Initializes platform-specific services for an <see cref="Application"/>.
/// </summary> /// </summary>
public sealed class AppBuilder : AppBuilderBase<AppBuilder> public class AppBuilder
{ {
private static bool s_setupWasAlreadyCalled;
private Action? _optionsInitializers;
private Func<Application>? _appFactory;
private IApplicationLifetime? _lifetime;
/// <summary>
/// Gets or sets the <see cref="IRuntimePlatform"/> instance.
/// </summary>
public IRuntimePlatform RuntimePlatform { get; set; }
/// <summary>
/// Gets or sets a method to call the initialize the runtime platform services (e. g. AssetLoader)
/// </summary>
public Action RuntimePlatformServicesInitializer { get; private set; }
/// <summary>
/// Gets the <see cref="Application"/> instance being initialized.
/// </summary>
public Application? Instance { get; private set; }
/// <summary>
/// Gets the type of the Instance (even if it's not created yet)
/// </summary>
public Type? ApplicationType { get; private set; }
/// <summary>
/// Gets or sets a method to call the initialize the windowing subsystem.
/// </summary>
public Action? WindowingSubsystemInitializer { get; private set; }
/// <summary>
/// Gets the name of the currently selected windowing subsystem.
/// </summary>
public string? WindowingSubsystemName { get; private set; }
/// <summary>
/// Gets or sets a method to call the initialize the windowing subsystem.
/// </summary>
public Action? RenderingSubsystemInitializer { get; private set; }
/// <summary>
/// Gets the name of the currently selected rendering subsystem.
/// </summary>
public string? RenderingSubsystemName { get; private set; }
/// <summary>
/// Gets or sets a method to call after the <see cref="Application"/> is setup.
/// </summary>
public Action<AppBuilder> AfterSetupCallback { get; private set; } = builder => { };
public Action<AppBuilder> AfterPlatformServicesSetupCallback { get; private set; } = builder => { };
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AppBuilder"/> class. /// Initializes a new instance of the <see cref="AppBuilder"/> class.
/// </summary> /// </summary>
public AppBuilder() public AppBuilder()
: base(new StandardRuntimePlatform(), : this(new StandardRuntimePlatform(),
builder => StandardRuntimePlatformServices.Register(builder.ApplicationType?.Assembly)) builder => StandardRuntimePlatformServices.Register(builder.ApplicationType?.Assembly))
{ {
} }
/// <summary>
/// Initializes a new instance of the <see cref="AppBuilder"/> class.
/// </summary>
protected AppBuilder(IRuntimePlatform platform, Action<AppBuilder> platformServices)
{
RuntimePlatform = platform;
RuntimePlatformServicesInitializer = () => platformServices(this);
}
/// <summary>
/// Begin configuring an <see cref="Application"/>.
/// </summary>
/// <typeparam name="TApp">The subclass of <see cref="Application"/> to configure.</typeparam>
/// <returns>An <see cref="AppBuilder"/> instance.</returns>
public static AppBuilder Configure<TApp>()
where TApp : Application, new()
{
return new AppBuilder()
{
ApplicationType = typeof(TApp),
// Needed for CoreRT compatibility
_appFactory = () => new TApp()
};
}
/// <summary>
/// Begin configuring an <see cref="Application"/>.
/// </summary>
/// <param name="appFactory">Factory function for <typeparamref name="TApp"/>.</param>
/// <typeparam name="TApp">The subclass of <see cref="Application"/> to configure.</typeparam>
/// <remarks><paramref name="appFactory"/> is useful for passing of dependencies to <typeparamref name="TApp"/>.</remarks>
/// <returns>An <see cref="AppBuilder"/> instance.</returns>
public static AppBuilder Configure<TApp>(Func<TApp> appFactory)
where TApp : Application
{
return new AppBuilder()
{
ApplicationType = typeof(TApp),
_appFactory = appFactory
};
}
protected AppBuilder Self => this;
public AppBuilder AfterSetup(Action<AppBuilder> callback)
{
AfterSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterSetupCallback, callback);
return Self;
}
public AppBuilder AfterPlatformServicesSetup(Action<AppBuilder> callback)
{
AfterPlatformServicesSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterPlatformServicesSetupCallback, callback);
return Self;
}
public delegate void AppMainDelegate(Application app, string[] args);
public void Start(AppMainDelegate main, string[] args)
{
Setup();
main(Instance!, args);
}
/// <summary>
/// Sets up the platform-specific services for the application, but does not run it.
/// </summary>
/// <returns></returns>
public AppBuilder SetupWithoutStarting()
{
Setup();
return Self;
}
/// <summary>
/// Sets up the platform-specific services for the application and initialized it with a particular lifetime, but does not run it.
/// </summary>
/// <param name="lifetime"></param>
/// <returns></returns>
public AppBuilder SetupWithLifetime(IApplicationLifetime lifetime)
{
_lifetime = lifetime;
Setup();
return Self;
}
/// <summary>
/// Specifies a windowing subsystem to use.
/// </summary>
/// <param name="initializer">The method to call to initialize the windowing subsystem.</param>
/// <param name="name">The name of the windowing subsystem.</param>
/// <returns>An <see cref="AppBuilder"/> instance.</returns>
public AppBuilder UseWindowingSubsystem(Action initializer, string name = "")
{
WindowingSubsystemInitializer = initializer;
WindowingSubsystemName = name;
return Self;
}
/// <summary>
/// Specifies a rendering subsystem to use.
/// </summary>
/// <param name="initializer">The method to call to initialize the rendering subsystem.</param>
/// <param name="name">The name of the rendering subsystem.</param>
/// <returns>An <see cref="AppBuilder"/> instance.</returns>
public AppBuilder UseRenderingSubsystem(Action initializer, string name = "")
{
RenderingSubsystemInitializer = initializer;
RenderingSubsystemName = name;
return Self;
}
/// <summary>
/// Configures platform-specific options
/// </summary>
public AppBuilder With<T>(T options)
{
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToConstant(options); };
return Self;
}
/// <summary>
/// Configures platform-specific options
/// </summary>
public AppBuilder With<T>(Func<T> options)
{
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToFunc(options); };
return Self;
}
/// <summary>
/// Sets up the platform-specific services for the <see cref="Application"/>.
/// </summary>
private void Setup()
{
if (RuntimePlatformServicesInitializer == null)
{
throw new InvalidOperationException("No runtime platform services configured.");
}
if (WindowingSubsystemInitializer == null)
{
throw new InvalidOperationException("No windowing system configured.");
}
if (RenderingSubsystemInitializer == null)
{
throw new InvalidOperationException("No rendering system configured.");
}
if (_appFactory == null)
{
throw new InvalidOperationException("No Application factory configured.");
}
if (s_setupWasAlreadyCalled)
{
throw new InvalidOperationException("Setup was already called on one of AppBuilder instances");
}
s_setupWasAlreadyCalled = true;
_optionsInitializers?.Invoke();
RuntimePlatformServicesInitializer();
RenderingSubsystemInitializer();
WindowingSubsystemInitializer();
AfterPlatformServicesSetupCallback(Self);
Instance = _appFactory();
Instance.ApplicationLifetime = _lifetime;
AvaloniaLocator.CurrentMutable.BindToSelf(Instance);
Instance.RegisterServices();
Instance.Initialize();
AfterSetupCallback(Self);
Instance.OnFrameworkInitializationCompleted();
}
} }
} }

244
src/Avalonia.Controls/AppBuilderBase.cs

@ -1,244 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Linq;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
namespace Avalonia.Controls
{
/// <summary>
/// Base class for initializing platform-specific services for an <see cref="Application"/>.
/// </summary>
/// <typeparam name="TAppBuilder">The type of the AppBuilder class itself.</typeparam>
public abstract class AppBuilderBase<TAppBuilder> where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
{
private static bool s_setupWasAlreadyCalled;
private Action? _optionsInitializers;
private Func<Application>? _appFactory;
private IApplicationLifetime? _lifetime;
/// <summary>
/// Gets or sets the <see cref="IRuntimePlatform"/> instance.
/// </summary>
public IRuntimePlatform RuntimePlatform { get; set; }
/// <summary>
/// Gets or sets a method to call the initialize the runtime platform services (e. g. AssetLoader)
/// </summary>
public Action RuntimePlatformServicesInitializer { get; private set; }
/// <summary>
/// Gets the <see cref="Application"/> instance being initialized.
/// </summary>
public Application? Instance { get; private set; }
/// <summary>
/// Gets the type of the Instance (even if it's not created yet)
/// </summary>
public Type? ApplicationType { get; private set; }
/// <summary>
/// Gets or sets a method to call the initialize the windowing subsystem.
/// </summary>
public Action? WindowingSubsystemInitializer { get; private set; }
/// <summary>
/// Gets the name of the currently selected windowing subsystem.
/// </summary>
public string? WindowingSubsystemName { get; private set; }
/// <summary>
/// Gets or sets a method to call the initialize the windowing subsystem.
/// </summary>
public Action? RenderingSubsystemInitializer { get; private set; }
/// <summary>
/// Gets the name of the currently selected rendering subsystem.
/// </summary>
public string? RenderingSubsystemName { get; private set; }
/// <summary>
/// Gets or sets a method to call after the <see cref="Application"/> is setup.
/// </summary>
public Action<TAppBuilder> AfterSetupCallback { get; private set; } = builder => { };
public Action<TAppBuilder> AfterPlatformServicesSetupCallback { get; private set; } = builder => { };
protected AppBuilderBase(IRuntimePlatform platform, Action<TAppBuilder> platformServices)
{
RuntimePlatform = platform;
RuntimePlatformServicesInitializer = () => platformServices((TAppBuilder)this);
}
/// <summary>
/// Begin configuring an <see cref="Application"/>.
/// </summary>
/// <typeparam name="TApp">The subclass of <see cref="Application"/> to configure.</typeparam>
/// <returns>An <typeparamref name="TAppBuilder"/> instance.</returns>
public static TAppBuilder Configure<TApp>()
where TApp : Application, new()
{
return new TAppBuilder()
{
ApplicationType = typeof(TApp),
// Needed for CoreRT compatibility
_appFactory = () => new TApp()
};
}
/// <summary>
/// Begin configuring an <see cref="Application"/>.
/// </summary>
/// <param name="appFactory">Factory function for <typeparamref name="TApp"/>.</param>
/// <typeparam name="TApp">The subclass of <see cref="Application"/> to configure.</typeparam>
/// <remarks><paramref name="appFactory"/> is useful for passing of dependencies to <typeparamref name="TApp"/>.</remarks>
/// <returns>An <typeparamref name="TAppBuilder"/> instance.</returns>
public static TAppBuilder Configure<TApp>(Func<TApp> appFactory)
where TApp : Application
{
return new TAppBuilder()
{
ApplicationType = typeof(TApp),
_appFactory = appFactory
};
}
protected TAppBuilder Self => (TAppBuilder)this;
public TAppBuilder AfterSetup(Action<TAppBuilder> callback)
{
AfterSetupCallback = (Action<TAppBuilder>)Delegate.Combine(AfterSetupCallback, callback);
return Self;
}
public TAppBuilder AfterPlatformServicesSetup(Action<TAppBuilder> callback)
{
AfterPlatformServicesSetupCallback = (Action<TAppBuilder>)Delegate.Combine(AfterPlatformServicesSetupCallback, callback);
return Self;
}
public delegate void AppMainDelegate(Application app, string[] args);
public void Start(AppMainDelegate main, string[] args)
{
Setup();
main(Instance!, args);
}
/// <summary>
/// Sets up the platform-specific services for the application, but does not run it.
/// </summary>
/// <returns></returns>
public TAppBuilder SetupWithoutStarting()
{
Setup();
return Self;
}
/// <summary>
/// Sets up the platform-specific services for the application and initialized it with a particular lifetime, but does not run it.
/// </summary>
/// <param name="lifetime"></param>
/// <returns></returns>
public TAppBuilder SetupWithLifetime(IApplicationLifetime lifetime)
{
_lifetime = lifetime;
Setup();
return Self;
}
/// <summary>
/// Specifies a windowing subsystem to use.
/// </summary>
/// <param name="initializer">The method to call to initialize the windowing subsystem.</param>
/// <param name="name">The name of the windowing subsystem.</param>
/// <returns>An <typeparamref name="TAppBuilder"/> instance.</returns>
public TAppBuilder UseWindowingSubsystem(Action initializer, string name = "")
{
WindowingSubsystemInitializer = initializer;
WindowingSubsystemName = name;
return Self;
}
/// <summary>
/// Specifies a rendering subsystem to use.
/// </summary>
/// <param name="initializer">The method to call to initialize the rendering subsystem.</param>
/// <param name="name">The name of the rendering subsystem.</param>
/// <returns>An <typeparamref name="TAppBuilder"/> instance.</returns>
public TAppBuilder UseRenderingSubsystem(Action initializer, string name = "")
{
RenderingSubsystemInitializer = initializer;
RenderingSubsystemName = name;
return Self;
}
protected virtual bool CheckSetup => true;
/// <summary>
/// Configures platform-specific options
/// </summary>
public TAppBuilder With<T>(T options)
{
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToConstant(options); };
return Self;
}
/// <summary>
/// Configures platform-specific options
/// </summary>
public TAppBuilder With<T>(Func<T> options)
{
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToFunc(options); };
return Self;
}
/// <summary>
/// Sets up the platform-specific services for the <see cref="Application"/>.
/// </summary>
private void Setup()
{
if (RuntimePlatformServicesInitializer == null)
{
throw new InvalidOperationException("No runtime platform services configured.");
}
if (WindowingSubsystemInitializer == null)
{
throw new InvalidOperationException("No windowing system configured.");
}
if (RenderingSubsystemInitializer == null)
{
throw new InvalidOperationException("No rendering system configured.");
}
if (_appFactory == null)
{
throw new InvalidOperationException("No Application factory configured.");
}
if (s_setupWasAlreadyCalled && CheckSetup)
{
throw new InvalidOperationException("Setup was already called on one of AppBuilder instances");
}
s_setupWasAlreadyCalled = true;
_optionsInitializers?.Invoke();
RuntimePlatformServicesInitializer();
RenderingSubsystemInitializer();
WindowingSubsystemInitializer();
AfterPlatformServicesSetupCallback(Self);
Instance = _appFactory();
Instance.ApplicationLifetime = _lifetime;
AvaloniaLocator.CurrentMutable.BindToSelf(Instance);
Instance.RegisterServices();
Instance.Initialize();
AfterSetupCallback(Self);
Instance.OnFrameworkInitializationCompleted();
}
}
}

5
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@ -200,9 +200,8 @@ namespace Avalonia
{ {
public static class ClassicDesktopStyleApplicationLifetimeExtensions public static class ClassicDesktopStyleApplicationLifetimeExtensions
{ {
public static int StartWithClassicDesktopLifetime<T>( public static int StartWithClassicDesktopLifetime(
this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) this AppBuilder builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)
where T : AppBuilderBase<T>, new()
{ {
var lifetime = new ClassicDesktopStyleApplicationLifetime() var lifetime = new ClassicDesktopStyleApplicationLifetime()
{ {

1
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -7,7 +7,6 @@
<ProjectReference Include="..\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj" /> <ProjectReference Include="..\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\Rx.props" /> <Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\JetBrains.Annotations.props" />
<Import Project="..\..\build\ApiDiff.props" /> <Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />

90
src/Avalonia.Controls/ComboBox.cs

@ -95,19 +95,26 @@ namespace Avalonia.Controls
{ {
ItemsPanelProperty.OverrideDefaultValue<ComboBox>(DefaultPanel); ItemsPanelProperty.OverrideDefaultValue<ComboBox>(DefaultPanel);
FocusableProperty.OverrideDefaultValue<ComboBox>(true); FocusableProperty.OverrideDefaultValue<ComboBox>(true);
SelectedItemProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.SelectedItemChanged(e));
KeyDownEvent.AddClassHandler<ComboBox>((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel);
IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(true); IsTextSearchEnabledProperty.OverrideDefaultValue<ComboBox>(true);
IsDropDownOpenProperty.Changed.AddClassHandler<ComboBox>((x, e) => x.DropdownChanged(e));
} }
/// <summary>
/// Occurs after the drop-down (popup) list of the <see cref="ComboBox"/> closes.
/// </summary>
public event EventHandler? DropDownClosed;
/// <summary>
/// Occurs after the drop-down (popup) list of the <see cref="ComboBox"/> opens.
/// </summary>
public event EventHandler? DropDownOpened;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the dropdown is currently open. /// Gets or sets a value indicating whether the dropdown is currently open.
/// </summary> /// </summary>
public bool IsDropDownOpen public bool IsDropDownOpen
{ {
get { return _isDropDownOpen; } get => _isDropDownOpen;
set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); } set => SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value);
} }
/// <summary> /// <summary>
@ -115,8 +122,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public double MaxDropDownHeight public double MaxDropDownHeight
{ {
get { return GetValue(MaxDropDownHeightProperty); } get => GetValue(MaxDropDownHeightProperty);
set { SetValue(MaxDropDownHeightProperty, value); } set => SetValue(MaxDropDownHeightProperty, value);
} }
/// <summary> /// <summary>
@ -124,8 +131,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
protected object? SelectionBoxItem protected object? SelectionBoxItem
{ {
get { return _selectionBoxItem; } get => _selectionBoxItem;
set { SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value); } set => SetAndRaise(SelectionBoxItemProperty, ref _selectionBoxItem, value);
} }
/// <summary> /// <summary>
@ -133,8 +140,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public string? PlaceholderText public string? PlaceholderText
{ {
get { return GetValue(PlaceholderTextProperty); } get => GetValue(PlaceholderTextProperty);
set { SetValue(PlaceholderTextProperty, value); } set => SetValue(PlaceholderTextProperty, value);
} }
/// <summary> /// <summary>
@ -142,8 +149,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public IBrush? PlaceholderForeground public IBrush? PlaceholderForeground
{ {
get { return GetValue(PlaceholderForegroundProperty); } get => GetValue(PlaceholderForegroundProperty);
set { SetValue(PlaceholderForegroundProperty, value); } set => SetValue(PlaceholderForegroundProperty, value);
} }
/// <summary> /// <summary>
@ -151,8 +158,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public ItemVirtualizationMode VirtualizationMode public ItemVirtualizationMode VirtualizationMode
{ {
get { return GetValue(VirtualizationModeProperty); } get => GetValue(VirtualizationModeProperty);
set { SetValue(VirtualizationModeProperty, value); } set => SetValue(VirtualizationModeProperty, value);
} }
/// <summary> /// <summary>
@ -160,8 +167,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public HorizontalAlignment HorizontalContentAlignment public HorizontalAlignment HorizontalContentAlignment
{ {
get { return GetValue(HorizontalContentAlignmentProperty); } get => GetValue(HorizontalContentAlignmentProperty);
set { SetValue(HorizontalContentAlignmentProperty, value); } set => SetValue(HorizontalContentAlignmentProperty, value);
} }
/// <summary> /// <summary>
@ -169,8 +176,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public VerticalAlignment VerticalContentAlignment public VerticalAlignment VerticalContentAlignment
{ {
get { return GetValue(VerticalContentAlignmentProperty); } get => GetValue(VerticalContentAlignmentProperty);
set { SetValue(VerticalContentAlignmentProperty, value); } set => SetValue(VerticalContentAlignmentProperty, value);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -182,6 +189,7 @@ namespace Avalonia.Controls
ComboBoxItem.ContentTemplateProperty); ComboBoxItem.ContentTemplateProperty);
} }
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
@ -213,7 +221,12 @@ namespace Avalonia.Controls
IsDropDownOpen = false; IsDropDownOpen = false;
e.Handled = true; e.Handled = true;
} }
else if (IsDropDownOpen && e.Key == Key.Enter) else if (!IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space))
{
IsDropDownOpen = true;
e.Handled = true;
}
else if (IsDropDownOpen && (e.Key == Key.Enter || e.Key == Key.Space))
{ {
SelectFocusedItem(); SelectFocusedItem();
IsDropDownOpen = false; IsDropDownOpen = false;
@ -234,7 +247,7 @@ namespace Avalonia.Controls
} }
// This part of code is needed just to acquire initial focus, subsequent focus navigation will be done by ItemsControl. // This part of code is needed just to acquire initial focus, subsequent focus navigation will be done by ItemsControl.
else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 && else if (IsDropDownOpen && SelectedIndex < 0 && ItemCount > 0 &&
(e.Key == Key.Up || e.Key == Key.Down) && IsFocused == true) (e.Key == Key.Up || e.Key == Key.Down) && IsFocused == true)
{ {
var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c)); var firstChild = Presenter?.Panel?.Children.FirstOrDefault(c => CanFocus(c));
if (firstChild != null) if (firstChild != null)
@ -304,12 +317,11 @@ namespace Avalonia.Controls
e.Handled = true; e.Handled = true;
} }
} }
PseudoClasses.Set(pcPressed, false); PseudoClasses.Set(pcPressed, false);
base.OnPointerReleased(e); base.OnPointerReleased(e);
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
@ -324,6 +336,22 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed; _popup.Closed += PopupClosed;
} }
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == SelectedItemProperty)
{
UpdateSelectionBoxItem(change.NewValue);
TryFocusSelectedItem();
}
else if (change.Property == IsDropDownOpenProperty)
{
PseudoClasses.Set(pcDropdownOpen, change.GetNewValue<bool>());
}
base.OnPropertyChanged(change);
}
protected override AutomationPeer OnCreateAutomationPeer() protected override AutomationPeer OnCreateAutomationPeer()
{ {
return new ComboBoxAutomationPeer(this); return new ComboBoxAutomationPeer(this);
@ -345,6 +373,8 @@ namespace Avalonia.Controls
{ {
Focus(); Focus();
} }
DropDownClosed?.Invoke(this, EventArgs.Empty);
} }
private void PopupOpened(object? sender, EventArgs e) private void PopupOpened(object? sender, EventArgs e)
@ -374,6 +404,8 @@ namespace Avalonia.Controls
} }
UpdateFlowDirection(); UpdateFlowDirection();
DropDownOpened?.Invoke(this, EventArgs.Empty);
} }
private void IsVisibleChanged(bool isVisible) private void IsVisibleChanged(bool isVisible)
@ -384,12 +416,6 @@ namespace Avalonia.Controls
} }
} }
private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdateSelectionBoxItem(e.NewValue);
TryFocusSelectedItem();
}
private void TryFocusSelectedItem() private void TryFocusSelectedItem()
{ {
var selectedIndex = SelectedIndex; var selectedIndex = SelectedIndex;
@ -489,11 +515,5 @@ namespace Avalonia.Controls
MoveSelection(NavigationDirection.Previous, WrapSelection); MoveSelection(NavigationDirection.Previous, WrapSelection);
} }
} }
private void DropdownChanged(AvaloniaPropertyChangedEventArgs e)
{
bool newValue = e.GetNewValue<bool>();
PseudoClasses.Set(pcDropdownOpen, newValue);
}
} }
} }

11
src/Avalonia.Controls/ContentControl.cs

@ -116,14 +116,19 @@ namespace Avalonia.Controls
return false; return false;
} }
private void ContentChanged(AvaloniaPropertyChangedEventArgs e) protected virtual void ContentChanged(AvaloniaPropertyChangedEventArgs e)
{ {
if (e.OldValue is ILogical oldChild) UpdateLogicalTree(e.OldValue, e.NewValue);
}
protected void UpdateLogicalTree(object? toRemove, object? toAdd)
{
if (toRemove is ILogical oldChild)
{ {
LogicalChildren.Remove(oldChild); LogicalChildren.Remove(oldChild);
} }
if (e.NewValue is ILogical newChild) if (toAdd is ILogical newChild)
{ {
LogicalChildren.Add(newChild); LogicalChildren.Add(newChild);
} }

13
src/Avalonia.Controls/Control.cs

@ -366,16 +366,29 @@ namespace Avalonia.Controls
{ {
base.OnAttachedToVisualTreeCore(e); base.OnAttachedToVisualTreeCore(e);
AddHandler(Gestures.HoldingEvent, OnHoldEvent);
InitializeIfNeeded(); InitializeIfNeeded();
ScheduleOnLoadedCore(); ScheduleOnLoadedCore();
} }
private void OnHoldEvent(object? sender, HoldingRoutedEventArgs e)
{
if(e.HoldingState == HoldingState.Started)
{
// Trigger ContentRequest when hold has started
RaiseEvent(new ContextRequestedEventArgs());
}
}
/// <inheritdoc/> /// <inheritdoc/>
protected sealed override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) protected sealed override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e)
{ {
base.OnDetachedFromVisualTreeCore(e); base.OnDetachedFromVisualTreeCore(e);
RemoveHandler(Gestures.HoldingEvent, OnHoldEvent);
OnUnloadedCore(); OnUnloadedCore();
} }

6
src/Avalonia.Controls/LoggingExtensions.cs

@ -8,16 +8,14 @@ namespace Avalonia
/// <summary> /// <summary>
/// Logs Avalonia events to the <see cref="System.Diagnostics.Trace"/> sink. /// Logs Avalonia events to the <see cref="System.Diagnostics.Trace"/> sink.
/// </summary> /// </summary>
/// <typeparam name="T">The application class type.</typeparam>
/// <param name="builder">The app builder instance.</param> /// <param name="builder">The app builder instance.</param>
/// <param name="level">The minimum level to log.</param> /// <param name="level">The minimum level to log.</param>
/// <param name="areas">The areas to log. Valid values are listed in <see cref="LogArea"/>.</param> /// <param name="areas">The areas to log. Valid values are listed in <see cref="LogArea"/>.</param>
/// <returns>The app builder instance.</returns> /// <returns>The app builder instance.</returns>
public static T LogToTrace<T>( public static AppBuilder LogToTrace(
this T builder, this AppBuilder builder,
LogEventLevel level = LogEventLevel.Warning, LogEventLevel level = LogEventLevel.Warning,
params string[] areas) params string[] areas)
where T : AppBuilderBase<T>, new()
{ {
Logger.Sink = new TraceLogSink(level, areas); Logger.Sink = new TraceLogSink(level, areas);
return builder; return builder;

7
src/Avalonia.Controls/TransitioningContentControl.cs

@ -71,6 +71,11 @@ public class TransitioningContentControl : ContentControl
} }
} }
protected override void ContentChanged(AvaloniaPropertyChangedEventArgs e)
{
// We do nothing becuse we should not remove old Content until the animation is over
}
/// <summary> /// <summary>
/// Updates the content with transitions. /// Updates the content with transitions.
/// </summary> /// </summary>
@ -89,6 +94,8 @@ public class TransitioningContentControl : ContentControl
if (PageTransition != null) if (PageTransition != null)
await PageTransition.Start(this, null, true, localToken); await PageTransition.Start(this, null, true, localToken);
UpdateLogicalTree(CurrentContent, content);
if (localToken.IsCancellationRequested) if (localToken.IsCancellationRequested)
{ {
return; return;

1
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -4,7 +4,6 @@ using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities; using Avalonia.Utilities;
using JetBrains.Annotations;
namespace Avalonia.Controls.Utils namespace Avalonia.Controls.Utils
{ {

1
src/Avalonia.Controls/WindowBase.cs

@ -8,7 +8,6 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Platform; using Avalonia.Platform;
using JetBrains.Annotations;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {

6
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@ -134,12 +134,12 @@ namespace Avalonia.DesignerSupport.Remote
IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport, CommandLineArgs args, object obj); IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport, CommandLineArgs args, object obj);
} }
class AppInitializer<T> : IAppInitializer where T : AppBuilderBase<T>, new() class AppInitializer : IAppInitializer
{ {
public IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport, public IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport,
CommandLineArgs args, object obj) CommandLineArgs args, object obj)
{ {
var builder = (AppBuilderBase<T>)obj; var builder = (AppBuilder)obj;
if (args.Method == Methods.AvaloniaRemote) if (args.Method == Methods.AvaloniaRemote)
builder.UseWindowingSubsystem(() => PreviewerWindowingPlatform.Initialize(transport)); builder.UseWindowingSubsystem(() => PreviewerWindowingPlatform.Initialize(transport));
if (args.Method == Methods.Html) if (args.Method == Methods.Html)
@ -191,7 +191,7 @@ namespace Avalonia.DesignerSupport.Remote
Log($"Obtaining AppBuilder instance from {builderMethod.DeclaringType.FullName}.{builderMethod.Name}"); Log($"Obtaining AppBuilder instance from {builderMethod.DeclaringType.FullName}.{builderMethod.Name}");
var appBuilder = builderMethod.Invoke(null, null); var appBuilder = builderMethod.Invoke(null, null);
Log($"Initializing application in design mode"); Log($"Initializing application in design mode");
var initializer =(IAppInitializer)Activator.CreateInstance(typeof(AppInitializer<>).MakeGenericType(appBuilder.GetType())); var initializer =(IAppInitializer)Activator.CreateInstance(typeof(AppInitializer));
transport = initializer.ConfigureApp(transport, args, appBuilder); transport = initializer.ConfigureApp(transport, args, appBuilder);
s_transport = transport; s_transport = transport;
transport.OnMessage += OnTransportMessage; transport.OnMessage += OnTransportMessage;

32
src/Avalonia.Desktop/AppBuilderDesktopExtensions.cs

@ -1,15 +1,13 @@
using Avalonia.Compatibility;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Platform; using Avalonia.Logging;
namespace Avalonia namespace Avalonia
{ {
public static class AppBuilderDesktopExtensions public static class AppBuilderDesktopExtensions
{ {
public static TAppBuilder UsePlatformDetect<TAppBuilder>(this TAppBuilder builder) public static AppBuilder UsePlatformDetect(this AppBuilder builder)
where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
{ {
var os = builder.RuntimePlatform.GetRuntimeInfo().OperatingSystem;
// We don't have the ability to load every assembly right now, so we are // We don't have the ability to load every assembly right now, so we are
// stuck with manual configuration here // stuck with manual configuration here
// Helpers are extracted to separate methods to take the advantage of the fact // Helpers are extracted to separate methods to take the advantage of the fact
@ -17,37 +15,39 @@ namespace Avalonia
// Additionally, by having a hard reference to each assembly, // Additionally, by having a hard reference to each assembly,
// we verify that the assemblies are in the final .deps.json file // we verify that the assemblies are in the final .deps.json file
// so .NET Core knows where to load the assemblies from,. // so .NET Core knows where to load the assemblies from,.
if (os == OperatingSystemType.WinNT) if (OperatingSystemEx.IsWindows())
{ {
LoadWin32(builder); LoadWin32(builder);
LoadSkia(builder); LoadSkia(builder);
} }
else if(os==OperatingSystemType.OSX) else if(OperatingSystemEx.IsMacOS())
{ {
LoadAvaloniaNative(builder); LoadAvaloniaNative(builder);
LoadSkia(builder); LoadSkia(builder);
} }
else else if (OperatingSystemEx.IsLinux())
{ {
LoadX11(builder); LoadX11(builder);
LoadSkia(builder); LoadSkia(builder);
} }
else
{
Logger.TryGet(LogEventLevel.Warning, LogArea.Platform)?.Log(builder,
"Avalonia.Desktop package was referenced on non-desktop platform or it isn't supported");
}
return builder; return builder;
} }
static void LoadAvaloniaNative<TAppBuilder>(TAppBuilder builder) static void LoadAvaloniaNative(AppBuilder builder)
where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
=> builder.UseAvaloniaNative(); => builder.UseAvaloniaNative();
static void LoadWin32<TAppBuilder>(TAppBuilder builder) static void LoadWin32(AppBuilder builder)
where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
=> builder.UseWin32(); => builder.UseWin32();
static void LoadX11<TAppBuilder>(TAppBuilder builder) static void LoadX11(AppBuilder builder)
where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
=> builder.UseX11(); => builder.UseX11();
static void LoadSkia<TAppBuilder>(TAppBuilder builder) static void LoadSkia(AppBuilder builder)
where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
=> builder.UseSkia(); => builder.UseSkia();
} }
} }

7
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -24,16 +24,15 @@ namespace Avalonia.Dialogs
} }
} }
public static TAppBuilder UseManagedSystemDialogs<TAppBuilder>(this TAppBuilder builder) public static AppBuilder UseManagedSystemDialogs(this AppBuilder builder)
where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
{ {
builder.AfterSetup(_ => builder.AfterSetup(_ =>
AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>().ToSingleton<ManagedStorageProviderFactory<Window>>()); AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>().ToSingleton<ManagedStorageProviderFactory<Window>>());
return builder; return builder;
} }
public static TAppBuilder UseManagedSystemDialogs<TAppBuilder, TWindow>(this TAppBuilder builder) public static AppBuilder UseManagedSystemDialogs<TWindow>(this AppBuilder builder)
where TAppBuilder : AppBuilderBase<TAppBuilder>, new() where TWindow : Window, new() where TWindow : Window, new()
{ {
builder.AfterSetup(_ => builder.AfterSetup(_ =>
AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>().ToSingleton<ManagedStorageProviderFactory<TWindow>>()); AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>().ToSingleton<ManagedStorageProviderFactory<TWindow>>());

3
src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs

@ -9,8 +9,7 @@ namespace Avalonia.FreeDesktop
{ {
public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives) public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
{ {
Contract.Requires<ArgumentNullException>(mountedDrives != null); return new LinuxMountedVolumeInfoListener(ref mountedDrives);
return new LinuxMountedVolumeInfoListener(ref mountedDrives!);
} }
} }
} }

5
src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs

@ -11,11 +11,10 @@ namespace Avalonia
{ {
public static class HeadlessVncPlatformExtensions public static class HeadlessVncPlatformExtensions
{ {
public static int StartWithHeadlessVncPlatform<T>( public static int StartWithHeadlessVncPlatform(
this T builder, this AppBuilder builder,
string host, int port, string host, int port,
string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)
where T : AppBuilderBase<T>, new()
{ {
var tcpServer = new TcpListener(host == null ? IPAddress.Loopback : IPAddress.Parse(host), port); var tcpServer = new TcpListener(host == null ? IPAddress.Loopback : IPAddress.Parse(host), port);
tcpServer.Start(); tcpServer.Start();

3
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -94,8 +94,7 @@ namespace Avalonia.Headless
public static class AvaloniaHeadlessPlatformExtensions public static class AvaloniaHeadlessPlatformExtensions
{ {
public static T UseHeadless<T>(this T builder, AvaloniaHeadlessPlatformOptions opts) public static AppBuilder UseHeadless(this AppBuilder builder, AvaloniaHeadlessPlatformOptions opts)
where T : AppBuilderBase<T>, new()
{ {
if(opts.UseHeadlessDrawing) if(opts.UseHeadlessDrawing)
builder.UseRenderingSubsystem(HeadlessPlatformRenderInterface.Initialize, "Headless"); builder.UseRenderingSubsystem(HeadlessPlatformRenderInterface.Initialize, "Headless");

23
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -9,23 +9,23 @@ using Avalonia.OpenGL;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using JetBrains.Annotations;
using MicroCom.Runtime; using MicroCom.Runtime;
#nullable enable
namespace Avalonia.Native namespace Avalonia.Native
{ {
class AvaloniaNativePlatform : IWindowingPlatform class AvaloniaNativePlatform : IWindowingPlatform
{ {
private readonly IAvaloniaNativeFactory _factory; private readonly IAvaloniaNativeFactory _factory;
private AvaloniaNativePlatformOptions _options; private AvaloniaNativePlatformOptions? _options;
private AvaloniaNativeGlPlatformGraphics _platformGl; private AvaloniaNativeGlPlatformGraphics? _platformGl;
[DllImport("libAvaloniaNative")] [DllImport("libAvaloniaNative")]
static extern IntPtr CreateAvaloniaNative(); static extern IntPtr CreateAvaloniaNative();
internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice(); internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice();
[CanBeNull] internal static Compositor Compositor { get; private set; } internal static Compositor? Compositor { get; private set; }
[CanBeNull] internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; } internal static PlatformRenderInterfaceContextManager? RenderInterface { get; private set; }
public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options)
{ {
@ -63,7 +63,7 @@ namespace Avalonia.Native
public void SetupApplicationName() public void SetupApplicationName()
{ {
if (!string.IsNullOrWhiteSpace(Application.Current.Name)) if (!string.IsNullOrWhiteSpace(Application.Current!.Name))
{ {
_factory.MacOptions.SetApplicationTitle(Application.Current.Name); _factory.MacOptions.SetApplicationTitle(Application.Current.Name);
} }
@ -118,10 +118,13 @@ namespace Avalonia.Native
AvaloniaLocator.CurrentMutable.Bind<IRenderLoop>().ToConstant(renderLoop); AvaloniaLocator.CurrentMutable.Bind<IRenderLoop>().ToConstant(renderLoop);
var hotkeys = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>(); var hotkeys = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers)); if (hotkeys is not null)
hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); {
hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers)); hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers));
hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers));
hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers));
hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers));
}
if (_options.UseGpu) if (_options.UseGpu)
{ {

3
src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs

@ -6,8 +6,7 @@ namespace Avalonia
{ {
public static class AvaloniaNativePlatformExtensions public static class AvaloniaNativePlatformExtensions
{ {
public static T UseAvaloniaNative<T>(this T builder) public static AppBuilder UseAvaloniaNative(this AppBuilder builder)
where T : AppBuilderBase<T>, new()
{ {
builder.UseWindowingSubsystem(() => builder.UseWindowingSubsystem(() =>
{ {

1
src/Avalonia.Native/Helpers.cs

@ -1,5 +1,4 @@
using Avalonia.Native.Interop; using Avalonia.Native.Interop;
using JetBrains.Annotations;
namespace Avalonia.Native namespace Avalonia.Native
{ {

1
src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs

@ -72,7 +72,6 @@ namespace Avalonia.Native
{ {
public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives) public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
{ {
Contract.Requires<ArgumentNullException>(mountedDrives != null);
return new MacOSMountedVolumeInfoListener(mountedDrives); return new MacOSMountedVolumeInfoListener(mountedDrives);
} }
} }

6
src/Avalonia.OpenGL/Egl/EglInterface.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Compatibility;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
using Avalonia.SourceGenerator; using Avalonia.SourceGenerator;
@ -24,10 +25,9 @@ namespace Avalonia.OpenGL.Egl
static Func<string, IntPtr> Load() static Func<string, IntPtr> Load()
{ {
var os = AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem; if(OperatingSystemEx.IsLinux())
if(os == OperatingSystemType.Linux)
return Load("libEGL.so.1"); return Load("libEGL.so.1");
if (os == OperatingSystemType.Android) if (OperatingSystemEx.IsAndroid())
return Load("libEGL.so"); return Load("libEGL.so");
throw new PlatformNotSupportedException(); throw new PlatformNotSupportedException();

3
src/Avalonia.ReactiveUI/AppBuilderExtensions.cs

@ -12,8 +12,7 @@ namespace Avalonia.ReactiveUI
/// scheduler, an activation for view fetcher, a template binding hook. Remember /// scheduler, an activation for view fetcher, a template binding hook. Remember
/// to call this method if you are using ReactiveUI in your application. /// to call this method if you are using ReactiveUI in your application.
/// </summary> /// </summary>
public static TAppBuilder UseReactiveUI<TAppBuilder>(this TAppBuilder builder) public static AppBuilder UseReactiveUI(this AppBuilder builder) =>
where TAppBuilder : AppBuilderBase<TAppBuilder>, new() =>
builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() => builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() =>
{ {
if (Locator.CurrentMutable is null) if (Locator.CurrentMutable is null)

4
src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml

@ -50,7 +50,8 @@
Opacity="0" Opacity="0"
Fill="{DynamicResource CheckBoxCheckGlyphForegroundUnchecked}" Fill="{DynamicResource CheckBoxCheckGlyphForegroundUnchecked}"
Stretch="Uniform" Stretch="Uniform"
VerticalAlignment="Center" /> VerticalAlignment="Center"
FlowDirection="LeftToRight" />
</Panel> </Panel>
</Viewbox> </Viewbox>
</Grid> </Grid>
@ -146,7 +147,6 @@
<Setter Property="Data" Value="M1507 31L438 1101L-119 543L-29 453L438 919L1417 -59L1507 31Z" /> <Setter Property="Data" Value="M1507 31L438 1101L-119 543L-29 453L438 919L1417 -59L1507 31Z" />
<Setter Property="Width" Value="9" /> <Setter Property="Width" Value="9" />
<Setter Property="Opacity" Value="1" /> <Setter Property="Opacity" Value="1" />
<Setter Property="FlowDirection" Value="LeftToRight" />
</Style> </Style>
<!-- Checked PointerOver State --> <!-- Checked PointerOver State -->

14
src/Avalonia.X11/Avalonia.X11.csproj

@ -10,10 +10,12 @@
<ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> <ProjectReference Include="..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
<ProjectReference Include="..\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj" /> <ProjectReference Include="..\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj" />
<Compile Include="..\Shared\RawEventGrouping.cs" /> <Compile Include="..\Shared\RawEventGrouping.cs" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\SourceGenerators.props" /> <Import Project="..\..\build\SourceGenerators.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<ItemGroup>
<Compile Remove="..\Shared\SourceGeneratorAttributes.cs" /> <ItemGroup>
</ItemGroup> <Compile Remove="..\Shared\SourceGeneratorAttributes.cs"/>
<None Include="..\Shared\SourceGeneratorAttributes.cs" Visible="false"/>
</ItemGroup>
</Project> </Project>

1
src/Avalonia.X11/NativeDialogs/Gtk.cs

@ -3,7 +3,6 @@ using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
using JetBrains.Annotations;
// ReSharper disable IdentifierTypo // ReSharper disable IdentifierTypo
namespace Avalonia.X11.NativeDialogs namespace Avalonia.X11.NativeDialogs

1
src/Avalonia.X11/X11Info.cs

@ -2,7 +2,6 @@ using System;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using JetBrains.Annotations;
using static Avalonia.X11.XLib; using static Avalonia.X11.XLib;
// ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable UnusedAutoPropertyAccessor.Local
namespace Avalonia.X11 namespace Avalonia.X11

2
src/Avalonia.X11/X11Platform.cs

@ -317,7 +317,7 @@ namespace Avalonia
} }
public static class AvaloniaX11PlatformExtensions public static class AvaloniaX11PlatformExtensions
{ {
public static T UseX11<T>(this T builder) where T : AppBuilderBase<T>, new() public static AppBuilder UseX11(this AppBuilder builder)
{ {
builder.UseWindowingSubsystem(() => builder.UseWindowingSubsystem(() =>
new AvaloniaX11Platform().Initialize(AvaloniaLocator.Current.GetService<X11PlatformOptions>() ?? new AvaloniaX11Platform().Initialize(AvaloniaLocator.Current.GetService<X11PlatformOptions>() ??

1
src/Avalonia.X11/X11Screens.cs

@ -5,7 +5,6 @@ using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Platform; using Avalonia.Platform;
using static Avalonia.X11.XLib; using static Avalonia.X11.XLib;
using JetBrains.Annotations;
namespace Avalonia.X11 namespace Avalonia.X11
{ {

3
src/Avalonia.X11/X11Window.Ime.cs

@ -7,7 +7,6 @@ using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
using JetBrains.Annotations;
using static Avalonia.X11.XLib; using static Avalonia.X11.XLib;
namespace Avalonia.X11 namespace Avalonia.X11
@ -206,7 +205,7 @@ namespace Avalonia.X11
// This class is used to attach the text value of the key to an asynchronously dispatched KeyDown event // This class is used to attach the text value of the key to an asynchronously dispatched KeyDown event
class RawKeyEventArgsWithText : RawKeyEventArgs class RawKeyEventArgsWithText : RawKeyEventArgs
{ {
public RawKeyEventArgsWithText([NotNull] IKeyboardDevice device, ulong timestamp, [NotNull] IInputRoot root, public RawKeyEventArgsWithText(IKeyboardDevice device, ulong timestamp, IInputRoot root,
RawKeyEventType type, Key key, RawInputModifiers modifiers, string text) : RawKeyEventType type, Key key, RawInputModifiers modifiers, string text) :
base(device, timestamp, root, type, key, modifiers) base(device, timestamp, root, type, key, modifiers)
{ {

7
src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs

@ -8,14 +8,13 @@ namespace Avalonia.Browser.Blazor;
[SupportedOSPlatform("browser")] [SupportedOSPlatform("browser")]
public static class WebAppBuilder public static class WebAppBuilder
{ {
public static T SetupWithSingleViewLifetime<T>( public static AppBuilder SetupWithSingleViewLifetime(
this T builder) this AppBuilder builder)
where T : AppBuilderBase<T>, new()
{ {
return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); return builder.SetupWithLifetime(new BlazorSingleViewLifetime());
} }
public static T UseBlazor<T>(this T builder) where T : AppBuilderBase<T>, new() public static AppBuilder UseBlazor(this AppBuilder builder)
{ {
return builder return builder
.UseBrowser() .UseBrowser()

7
src/Browser/Avalonia.Browser/BrowserRuntimePlatform.cs

@ -11,12 +11,11 @@ internal class BrowserRuntimePlatform : StandardRuntimePlatform
{ {
private static readonly Lazy<RuntimePlatformInfo> Info = new(() => private static readonly Lazy<RuntimePlatformInfo> Info = new(() =>
{ {
var isMobile = AvaloniaModule.IsMobile();
var result = new RuntimePlatformInfo var result = new RuntimePlatformInfo
{ {
IsCoreClr = true, // WASM browser is always CoreCLR IsMobile = isMobile,
IsBrowser = true, // BrowserRuntimePlatform only runs on Browser. IsDesktop = !isMobile
OperatingSystem = OperatingSystemType.Browser,
IsMobile = AvaloniaModule.IsMobile()
}; };
return result; return result;

12
src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs

@ -2,8 +2,6 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using Avalonia.Browser.Skia;
using Avalonia.Platform;
namespace Avalonia.Browser; namespace Avalonia.Browser;
@ -27,9 +25,8 @@ public class BrowserPlatformOptions
[SupportedOSPlatform("browser")] [SupportedOSPlatform("browser")]
public static class WebAppBuilder public static class WebAppBuilder
{ {
public static T SetupBrowserApp<T>( public static AppBuilder SetupBrowserApp(
this T builder, string mainDivId) this AppBuilder builder, string mainDivId)
where T : AppBuilderBase<T>, new()
{ {
var lifetime = new BrowserSingleViewLifetime(); var lifetime = new BrowserSingleViewLifetime();
@ -42,9 +39,8 @@ public static class WebAppBuilder
.SetupWithLifetime(lifetime); .SetupWithLifetime(lifetime);
} }
public static T UseBrowser<T>( public static AppBuilder UseBrowser(
this T builder) this AppBuilder builder)
where T : AppBuilderBase<T>, new()
{ {
return builder return builder
.UseWindowingSubsystem(BrowserWindowingPlatform.Register) .UseWindowingSubsystem(BrowserWindowingPlatform.Register)

1
src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs

@ -1,6 +1,5 @@
using Avalonia.LinuxFramebuffer.Output; using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Media; using Avalonia.Media;
using JetBrains.Annotations;
namespace Avalonia.LinuxFramebuffer namespace Avalonia.LinuxFramebuffer
{ {

50
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Threading; using System.Threading;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Embedding; using Avalonia.Controls.Embedding;
@ -12,12 +13,10 @@ using Avalonia.LinuxFramebuffer.Input;
using Avalonia.LinuxFramebuffer.Input.EvDev; using Avalonia.LinuxFramebuffer.Input.EvDev;
using Avalonia.LinuxFramebuffer.Input.LibInput; using Avalonia.LinuxFramebuffer.Input.LibInput;
using Avalonia.LinuxFramebuffer.Output; using Avalonia.LinuxFramebuffer.Output;
using Avalonia.OpenGL;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Threading; #nullable enable
using JetBrains.Annotations;
namespace Avalonia.LinuxFramebuffer namespace Avalonia.LinuxFramebuffer
{ {
@ -26,9 +25,9 @@ namespace Avalonia.LinuxFramebuffer
IOutputBackend _fb; IOutputBackend _fb;
private static readonly Stopwatch St = Stopwatch.StartNew(); private static readonly Stopwatch St = Stopwatch.StartNew();
internal static uint Timestamp => (uint)St.ElapsedTicks; internal static uint Timestamp => (uint)St.ElapsedTicks;
public static InternalPlatformThreadingInterface Threading; public static InternalPlatformThreadingInterface? Threading;
internal static Compositor Compositor { get; private set; } internal static Compositor? Compositor { get; private set; }
LinuxFramebufferPlatform(IOutputBackend backend) LinuxFramebufferPlatform(IOutputBackend backend)
@ -60,7 +59,7 @@ namespace Avalonia.LinuxFramebuffer
} }
internal static LinuxFramebufferLifetime Initialize<T>(T builder, IOutputBackend outputBackend, IInputBackend inputBackend) where T : AppBuilderBase<T>, new() internal static LinuxFramebufferLifetime Initialize(AppBuilder builder, IOutputBackend outputBackend, IInputBackend? inputBackend)
{ {
var platform = new LinuxFramebufferPlatform(outputBackend); var platform = new LinuxFramebufferPlatform(outputBackend);
builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev"); builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev");
@ -71,8 +70,8 @@ namespace Avalonia.LinuxFramebuffer
class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime
{ {
private readonly IOutputBackend _fb; private readonly IOutputBackend _fb;
[CanBeNull] private readonly IInputBackend _inputBackend; private readonly IInputBackend? _inputBackend;
private TopLevel _topLevel; private TopLevel? _topLevel;
private readonly CancellationTokenSource _cts = new CancellationTokenSource(); private readonly CancellationTokenSource _cts = new CancellationTokenSource();
public CancellationToken Token => _cts.Token; public CancellationToken Token => _cts.Token;
@ -81,15 +80,15 @@ namespace Avalonia.LinuxFramebuffer
_fb = fb; _fb = fb;
} }
public LinuxFramebufferLifetime(IOutputBackend fb, IInputBackend input) public LinuxFramebufferLifetime(IOutputBackend fb, IInputBackend? input)
{ {
_fb = fb; _fb = fb;
_inputBackend = input; _inputBackend = input;
} }
public Control MainView public Control? MainView
{ {
get => (Control)_topLevel?.Content; get => (Control?)_topLevel?.Content;
set set
{ {
if (_topLevel == null) if (_topLevel == null)
@ -119,8 +118,8 @@ namespace Avalonia.LinuxFramebuffer
} }
public int ExitCode { get; private set; } public int ExitCode { get; private set; }
public event EventHandler<ControlledApplicationLifetimeStartupEventArgs> Startup; public event EventHandler<ControlledApplicationLifetimeStartupEventArgs>? Startup;
public event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit; public event EventHandler<ControlledApplicationLifetimeExitEventArgs>? Exit;
public void Start(string[] args) public void Start(string[] args)
{ {
@ -140,20 +139,17 @@ namespace Avalonia.LinuxFramebuffer
public static class LinuxFramebufferPlatformExtensions public static class LinuxFramebufferPlatformExtensions
{ {
public static int StartLinuxFbDev<T>(this T builder, string[] args, string fbdev = null, double scaling = 1, IInputBackend inputBackend = default) public static int StartLinuxFbDev(this AppBuilder builder, string[] args, string? fbdev = null, double scaling = 1, IInputBackend? inputBackend = default)
where T : AppBuilderBase<T>, new() => => StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }, inputBackend);
StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }, inputBackend); public static int StartLinuxFbDev(this AppBuilder builder, string[] args, string fbdev, PixelFormat? format, double scaling, IInputBackend? inputBackend = default)
public static int StartLinuxFbDev<T>(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling, IInputBackend inputBackend = default) => StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }, inputBackend);
where T : AppBuilderBase<T>, new() =>
StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }, inputBackend); public static int StartLinuxDrm(this AppBuilder builder, string[] args, string? card = null, double scaling = 1, IInputBackend? inputBackend = default)
=> StartLinuxDirect(builder, args, new DrmOutput(card) { Scaling = scaling }, inputBackend);
public static int StartLinuxDrm<T>(this T builder, string[] args, string card = null, double scaling = 1, IInputBackend inputBackend = default) public static int StartLinuxDrm(this AppBuilder builder, string[] args, string? card = null, bool connectorsForceProbe = false, DrmOutputOptions? options = null, IInputBackend? inputBackend = default)
where T : AppBuilderBase<T>, new() => StartLinuxDirect(builder, args, new DrmOutput(card) { Scaling = scaling }, inputBackend); => StartLinuxDirect(builder, args, new DrmOutput(card, connectorsForceProbe, options), inputBackend);
public static int StartLinuxDrm<T>(this T builder, string[] args, string card = null, bool connectorsForceProbe = false, [CanBeNull] DrmOutputOptions options = null, IInputBackend inputBackend = default)
where T : AppBuilderBase<T>, new() => StartLinuxDirect(builder, args, new DrmOutput(card, connectorsForceProbe, options), inputBackend); public static int StartLinuxDirect(this AppBuilder builder, string[] args, IOutputBackend outputBackend, IInputBackend? inputBackend = default)
public static int StartLinuxDirect<T>(this T builder, string[] args, IOutputBackend outputBackend, IInputBackend inputBackend = default)
where T : AppBuilderBase<T>, new()
{ {
var lifetime = LinuxFramebufferPlatform.Initialize(builder, outputBackend, inputBackend); var lifetime = LinuxFramebufferPlatform.Initialize(builder, outputBackend, inputBackend);
builder.SetupWithLifetime(lifetime); builder.SetupWithLifetime(lifetime);

3
src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs

@ -8,7 +8,6 @@ using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces; using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
using JetBrains.Annotations;
using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods;
using static Avalonia.LinuxFramebuffer.Output.LibDrm; using static Avalonia.LinuxFramebuffer.Output.LibDrm;
using static Avalonia.LinuxFramebuffer.Output.LibDrm.GbmColorFormats; using static Avalonia.LinuxFramebuffer.Output.LibDrm.GbmColorFormats;
@ -50,7 +49,7 @@ namespace Avalonia.LinuxFramebuffer.Output
_outputOptions = options; _outputOptions = options;
Init(card, resources, connector, modeInfo); Init(card, resources, connector, modeInfo);
} }
public DrmOutput(string path = null, bool connectorsForceProbe = false, [CanBeNull] DrmOutputOptions options = null) public DrmOutput(string path = null, bool connectorsForceProbe = false, DrmOutputOptions? options = null)
{ {
if(options != null) if(options != null)
_outputOptions = options; _outputOptions = options;

1
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs

@ -366,6 +366,7 @@ internal class AvaloniaXamlIlOptionMarkupExtensionTransformer : IXamlAstTransfor
foreach (var branch in ExtensionNodeContainer.Branches) foreach (var branch in ExtensionNodeContainer.Branches)
{ {
var next = codeGen.DefineLabel(); var next = codeGen.DefineLabel();
codeGen.Emit(OpCodes.Nop);
if (branch.HasContext) if (branch.HasContext)
{ {
codeGen.Ldloc(context.ContextLocal); codeGen.Ldloc(context.ContextLocal);

4
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CommandAccessorPlugin.cs

@ -44,9 +44,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
public CommandAccessor(WeakReference<object> reference, Action<object, object> execute, Func<object, object, bool> canExecute, ISet<string> dependsOnProperties) public CommandAccessor(WeakReference<object> reference, Action<object, object> execute, Func<object, object, bool> canExecute, ISet<string> dependsOnProperties)
{ {
Contract.Requires<ArgumentNullException>(reference != null); _reference = reference ?? throw new ArgumentNullException(nameof(reference));
_reference = reference;
_dependsOnProperties = dependsOnProperties; _dependsOnProperties = dependsOnProperties;
_command = new Command(reference, execute, canExecute); _command = new Command(reference, execute, canExecute);

14
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs

@ -28,11 +28,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
public AvaloniaPropertyAccessor(WeakReference<AvaloniaObject> reference, AvaloniaProperty property) public AvaloniaPropertyAccessor(WeakReference<AvaloniaObject> reference, AvaloniaProperty property)
{ {
Contract.Requires<ArgumentNullException>(reference != null); _reference = reference ?? throw new ArgumentNullException(nameof(reference));;
Contract.Requires<ArgumentNullException>(property != null); _property = property ?? throw new ArgumentNullException(nameof(property));;
_reference = reference;
_property = property;
} }
public AvaloniaObject Instance public AvaloniaObject Instance
@ -77,11 +74,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
public InpcPropertyAccessor(WeakReference<object> reference, IPropertyInfo property) public InpcPropertyAccessor(WeakReference<object> reference, IPropertyInfo property)
{ {
Contract.Requires<ArgumentNullException>(reference != null); _reference = reference ?? throw new ArgumentNullException(nameof(reference));
Contract.Requires<ArgumentNullException>(property != null); _property = property ?? throw new ArgumentNullException(nameof(property));
_reference = reference;
_property = property;
} }
public override Type PropertyType => _property.PropertyType; public override Type PropertyType => _property.PropertyType;

4
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs

@ -6,7 +6,7 @@ using Avalonia.Platform;
namespace Avalonia.Markup.Xaml.MarkupExtensions; namespace Avalonia.Markup.Xaml.MarkupExtensions;
public class OnFormFactorExtension : OnFormFactorExtensionBase<object, On> public sealed class OnFormFactorExtension : OnFormFactorExtensionBase<object, On>
{ {
public OnFormFactorExtension() public OnFormFactorExtension()
{ {
@ -24,7 +24,7 @@ public class OnFormFactorExtension : OnFormFactorExtensionBase<object, On>
} }
} }
public class OnFormFactorExtension<TReturn> : OnFormFactorExtensionBase<TReturn, On<TReturn>> public sealed class OnFormFactorExtension<TReturn> : OnFormFactorExtensionBase<TReturn, On<TReturn>>
{ {
public OnFormFactorExtension() public OnFormFactorExtension()
{ {

43
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs

@ -1,11 +1,11 @@
#nullable enable #nullable enable
using System; using System;
using Avalonia.Compatibility;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform;
namespace Avalonia.Markup.Xaml.MarkupExtensions; namespace Avalonia.Markup.Xaml.MarkupExtensions;
public class OnPlatformExtension : OnPlatformExtensionBase<object, On> public sealed class OnPlatformExtension : OnPlatformExtensionBase<object, On>
{ {
public OnPlatformExtension() public OnPlatformExtension()
{ {
@ -17,13 +17,13 @@ public class OnPlatformExtension : OnPlatformExtensionBase<object, On>
Default = defaultValue; Default = defaultValue;
} }
public static bool ShouldProvideOption(IServiceProvider serviceProvider, OperatingSystemType option) public static bool ShouldProvideOption(string option)
{ {
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem == option; return ShouldProvideOptionInternal(option);
} }
} }
public class OnPlatformExtension<TReturn> : OnPlatformExtensionBase<TReturn, On<TReturn>> public sealed class OnPlatformExtension<TReturn> : OnPlatformExtensionBase<TReturn, On<TReturn>>
{ {
public OnPlatformExtension() public OnPlatformExtension()
{ {
@ -35,9 +35,9 @@ public class OnPlatformExtension<TReturn> : OnPlatformExtensionBase<TReturn, On<
Default = defaultValue; Default = defaultValue;
} }
public static bool ShouldProvideOption(IServiceProvider serviceProvider, OperatingSystemType option) public static bool ShouldProvideOption(string option)
{ {
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem == option; return ShouldProvideOptionInternal(option);
} }
} }
@ -47,27 +47,44 @@ public abstract class OnPlatformExtensionBase<TReturn, TOn> : IAddChild<TOn>
[MarkupExtensionDefaultOption] [MarkupExtensionDefaultOption]
public TReturn? Default { get; set; } public TReturn? Default { get; set; }
[MarkupExtensionOption(OperatingSystemType.WinNT)] [MarkupExtensionOption("WINDOWS")]
public TReturn? Windows { get; set; } public TReturn? Windows { get; set; }
[MarkupExtensionOption(OperatingSystemType.OSX)] [MarkupExtensionOption("OSX")]
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public TReturn? macOS { get; set; } public TReturn? macOS { get; set; }
[MarkupExtensionOption(OperatingSystemType.Linux)] [MarkupExtensionOption("LINUX")]
public TReturn? Linux { get; set; } public TReturn? Linux { get; set; }
[MarkupExtensionOption(OperatingSystemType.Android)] [MarkupExtensionOption("ANDROID")]
public TReturn? Android { get; set; } public TReturn? Android { get; set; }
[MarkupExtensionOption(OperatingSystemType.iOS)] [MarkupExtensionOption("IOS")]
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public TReturn? iOS { get; set; } public TReturn? iOS { get; set; }
[MarkupExtensionOption(OperatingSystemType.Browser)] [MarkupExtensionOption("BROWSER")]
public TReturn? Browser { get; set; } public TReturn? Browser { get; set; }
// Required for the compiler, will be replaced with actual method compile time. // Required for the compiler, will be replaced with actual method compile time.
public object ProvideValue() { return this; } public object ProvideValue() { return this; }
void IAddChild<TOn>.AddChild(TOn child) {} void IAddChild<TOn>.AddChild(TOn child) {}
private protected static bool ShouldProvideOptionInternal(string option)
{
// Instead of using OperatingSystem.IsOSPlatform(string) we use specific "Is***" methods so whole method can be trimmed by the mono linked.
// Keep in mind it works only with const "option" parameter.
// IsOSPlatform might work better with trimming in the future, so it should be re-visited after .NET 8/9.
return option switch
{
"WINDOWS" => OperatingSystemEx.IsWindows(),
"OSX" => OperatingSystemEx.IsMacOS(),
"LINUX" => OperatingSystemEx.IsLinux(),
"ANDROID" => OperatingSystemEx.IsAndroid(),
"IOS" => OperatingSystemEx.IsIOS(),
"BROWSER" => OperatingSystemEx.IsBrowser(),
_ => OperatingSystemEx.IsOSPlatform(option)
};
}
} }

6
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -10,7 +10,6 @@ using Avalonia.Rendering.SceneGraph;
using Avalonia.Rendering.Utilities; using Avalonia.Rendering.Utilities;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using JetBrains.Annotations;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
@ -675,13 +674,14 @@ namespace Avalonia.Skia
} }
} }
[CanBeNull] #nullable enable
public object GetFeature(Type t) public object? GetFeature(Type t)
{ {
if (t == typeof(ISkiaSharpApiLeaseFeature)) if (t == typeof(ISkiaSharpApiLeaseFeature))
return new SkiaLeaseFeature(this); return new SkiaLeaseFeature(this);
return null; return null;
} }
#nullable restore
/// <summary> /// <summary>
/// Configure paint wrapper for using gradient brush. /// Configure paint wrapper for using gradient brush.

4
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@ -1,8 +1,8 @@
using System; using System;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
using JetBrains.Annotations;
using SkiaSharp; using SkiaSharp;
#nullable enable
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
@ -10,7 +10,7 @@ namespace Avalonia.Skia
[Unstable] [Unstable]
public class GlyphRunImpl : IGlyphRunImpl public class GlyphRunImpl : IGlyphRunImpl
{ {
public GlyphRunImpl([NotNull] SKTextBlob textBlob) public GlyphRunImpl(SKTextBlob textBlob)
{ {
TextBlob = textBlob ?? throw new ArgumentNullException (nameof (textBlob)); TextBlob = textBlob ?? throw new ArgumentNullException (nameof (textBlob));
} }

8
src/Skia/Avalonia.Skia/Helpers/PixelFormatHelper.cs

@ -1,4 +1,5 @@
using Avalonia.Platform; using Avalonia.Compatibility;
using Avalonia.Platform;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Skia.Helpers namespace Avalonia.Skia.Helpers
@ -18,10 +19,7 @@ namespace Avalonia.Skia.Helpers
var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType; var colorType = format?.ToSkColorType() ?? SKImageInfo.PlatformColorType;
// TODO: This looks like some leftover hack // TODO: This looks like some leftover hack
var runtimePlatform = AvaloniaLocator.Current?.GetService<IRuntimePlatform>(); if (OperatingSystemEx.IsLinux())
var runtime = runtimePlatform?.GetRuntimeInfo();
if (runtime?.IsDesktop == true && runtime.Value.OperatingSystem == OperatingSystemType.Linux)
{ {
colorType = SKColorType.Bgra8888; colorType = SKColorType.Bgra8888;
} }

3
src/Skia/Avalonia.Skia/SkiaApplicationExtensions.cs

@ -12,10 +12,9 @@ namespace Avalonia
/// <summary> /// <summary>
/// Enable Skia renderer. /// Enable Skia renderer.
/// </summary> /// </summary>
/// <typeparam name="T">Builder type.</typeparam>
/// <param name="builder">Builder.</param> /// <param name="builder">Builder.</param>
/// <returns>Configure builder.</returns> /// <returns>Configure builder.</returns>
public static T UseSkia<T>(this T builder) where T : AppBuilderBase<T>, new() public static AppBuilder UseSkia(this AppBuilder builder)
{ {
return builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize( return builder.UseRenderingSubsystem(() => SkiaPlatform.Initialize(
AvaloniaLocator.Current.GetService<SkiaOptions>() ?? new SkiaOptions()), AvaloniaLocator.Current.GetService<SkiaOptions>() ?? new SkiaOptions()),

1
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@ -17,7 +17,6 @@
<Import Project="..\..\..\build\Rx.props" /> <Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\SharpDX.props" /> <Import Project="..\..\..\build\SharpDX.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" /> <Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\..\build\JetBrains.Annotations.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" /> <Import Project="..\..\..\build\TrimmingEnable.props" />

2
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -16,7 +16,7 @@ namespace Avalonia
{ {
public static class Direct2DApplicationExtensions public static class Direct2DApplicationExtensions
{ {
public static T UseDirect2D1<T>(this T builder) where T : AppBuilderBase<T>, new() public static AppBuilder UseDirect2D1(this AppBuilder builder)
{ {
builder.UseRenderingSubsystem(Direct2D1.Direct2D1Platform.Initialize, "Direct2D1"); builder.UseRenderingSubsystem(Direct2D1.Direct2D1Platform.Initialize, "Direct2D1");
return builder; return builder;

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

@ -820,6 +820,14 @@ namespace Avalonia.Win32.Interop
DWMWA_LAST DWMWA_LAST
}; };
public enum DwmWindowCornerPreference : uint
{
DWMWCP_DEFAULT = 0,
DWMWCP_DONOTROUND,
DWMWCP_ROUND,
DWMWCP_ROUNDSMALL
}
public enum MapVirtualKeyMapTypes : uint public enum MapVirtualKeyMapTypes : uint
{ {
MAPVK_VK_TO_VSC = 0x00, MAPVK_VK_TO_VSC = 0x00,

11
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -19,16 +19,14 @@ using Avalonia.Threading;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Win32.Input; using Avalonia.Win32.Input;
using Avalonia.Win32.Interop; using Avalonia.Win32.Interop;
using JetBrains.Annotations;
using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.Interop.UnmanagedMethods;
namespace Avalonia namespace Avalonia
{ {
#nullable enable
public static class Win32ApplicationExtensions public static class Win32ApplicationExtensions
{ {
public static T UseWin32<T>( public static AppBuilder UseWin32(this AppBuilder builder)
this T builder)
where T : AppBuilderBase<T>, new()
{ {
return builder.UseWindowingSubsystem( return builder.UseWindowingSubsystem(
() => Win32.Win32Platform.Initialize( () => Win32.Win32Platform.Initialize(
@ -111,9 +109,10 @@ namespace Avalonia
/// <summary> /// <summary>
/// Provides a way to use a custom-implemented graphics context such as a custom ISkiaGpu /// Provides a way to use a custom-implemented graphics context such as a custom ISkiaGpu
/// </summary> /// </summary>
[CanBeNull] public IPlatformGraphics CustomPlatformGraphics { get; set; } public IPlatformGraphics? CustomPlatformGraphics { get; set; }
} }
} }
#nullable restore
namespace Avalonia.Win32 namespace Avalonia.Win32
{ {
@ -264,6 +263,8 @@ namespace Avalonia.Win32
public bool CurrentThreadIsLoopThread => _uiThread == Thread.CurrentThread; public bool CurrentThreadIsLoopThread => _uiThread == Thread.CurrentThread;
public TimeSpan HoldWaitDuration { get; set; } = TimeSpan.FromMilliseconds(300);
public event Action<DispatcherPriority?> Signaled; public event Action<DispatcherPriority?> Signaled;
public event EventHandler<ShutdownRequestedEventArgs> ShutdownRequested; public event EventHandler<ShutdownRequestedEventArgs> ShutdownRequested;

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

@ -1030,6 +1030,12 @@ namespace Avalonia.Win32
{ {
var margins = UpdateExtendMargins(); var margins = UpdateExtendMargins();
DwmExtendFrameIntoClientArea(_hwnd, ref margins); DwmExtendFrameIntoClientArea(_hwnd, ref margins);
unsafe
{
int cornerPreference = (int)DwmWindowCornerPreference.DWMWCP_ROUND;
DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(int));
}
} }
else else
{ {
@ -1040,6 +1046,12 @@ namespace Avalonia.Win32
_extendedMargins = new Thickness(); _extendedMargins = new Thickness();
Resize(new Size(rcWindow.Width / RenderScaling, rcWindow.Height / RenderScaling), PlatformResizeReason.Layout); Resize(new Size(rcWindow.Width / RenderScaling, rcWindow.Height / RenderScaling), PlatformResizeReason.Layout);
unsafe
{
int cornerPreference = (int)DwmWindowCornerPreference.DWMWCP_DEFAULT;
DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(int));
}
} }
if (!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && if (!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) &&

1
src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs

@ -8,7 +8,6 @@ namespace Avalonia.Win32
{ {
public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives) public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
{ {
Contract.Requires<ArgumentNullException>(mountedDrives != null);
return new WindowsMountedVolumeInfoListener(mountedDrives); return new WindowsMountedVolumeInfoListener(mountedDrives);
} }
} }

1
src/iOS/Avalonia.iOS/AvaloniaView.Text.cs

@ -1,6 +1,5 @@
#nullable enable #nullable enable
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
using JetBrains.Annotations;
using UIKit; using UIKit;
namespace Avalonia.iOS; namespace Avalonia.iOS;

2
src/iOS/Avalonia.iOS/Platform.cs

@ -12,7 +12,7 @@ namespace Avalonia
{ {
public static class IOSApplicationExtensions public static class IOSApplicationExtensions
{ {
public static T UseiOS<T>(this T builder) where T : AppBuilderBase<T>, new() public static AppBuilder UseiOS(this AppBuilder builder)
{ {
return builder return builder
.UseWindowingSubsystem(iOS.Platform.Register, "iOS") .UseWindowingSubsystem(iOS.Platform.Register, "iOS")

338
tests/Avalonia.Base.UnitTests/Input/GesturesTests.cs

@ -1,7 +1,13 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests.Input namespace Avalonia.Base.UnitTests.Input
@ -167,6 +173,242 @@ namespace Avalonia.Base.UnitTests.Input
Assert.False(raised); Assert.False(raised);
} }
[Fact]
public void Hold_Should_Be_Raised_After_Hold_Duration()
{
using var scope = AvaloniaLocator.EnterScope();
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
iSettingsMock.Setup(x => x.GetTapSize(It.IsAny<PointerType>())).Returns(new Size(16, 16));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
var scheduledTimers = new List<(TimeSpan time, Action action)>();
using var app = UnitTestApplication.Start(new TestServices(
threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
Border border = new Border();
Gestures.SetIsHoldWithMouseEnabled(border, true);
var decorator = new Decorator
{
Child = border
};
HoldingState holding = HoldingState.Cancelled;
decorator.AddHandler(Gestures.HoldingEvent, (s, e) => holding = e.HoldingState);
_mouse.Down(border);
Assert.False(holding != HoldingState.Cancelled);
// Verify timer duration, but execute it immediately.
var timer = Assert.Single(scheduledTimers);
Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
timer.action();
Assert.True(holding == HoldingState.Started);
_mouse.Up(border);
Assert.True(holding == HoldingState.Completed);
}
[Fact]
public void Hold_Should_Not_Raised_When_Pointer_Released_Before_Timer()
{
using var scope = AvaloniaLocator.EnterScope();
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
var scheduledTimers = new List<(TimeSpan time, Action action)>();
using var app = UnitTestApplication.Start(new TestServices(
threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
Border border = new Border();
Gestures.SetIsHoldWithMouseEnabled(border, true);
var decorator = new Decorator
{
Child = border
};
var raised = false;
decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Started);
_mouse.Down(border);
Assert.False(raised);
_mouse.Up(border);
Assert.False(raised);
// Verify timer duration, but execute it immediately.
var timer = Assert.Single(scheduledTimers);
Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
timer.action();
Assert.False(raised);
}
[Fact]
public void Hold_Should_Not_Raised_When_Pointer_Is_Moved_Before_Timer()
{
using var scope = AvaloniaLocator.EnterScope();
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
var scheduledTimers = new List<(TimeSpan time, Action action)>();
using var app = UnitTestApplication.Start(new TestServices(
threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
Border border = new Border();
Gestures.SetIsHoldWithMouseEnabled(border, true);
var decorator = new Decorator
{
Child = border
};
var raised = false;
decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Completed);
_mouse.Down(border);
Assert.False(raised);
_mouse.Move(border, position: new Point(20, 20));
Assert.False(raised);
// Verify timer duration, but execute it immediately.
var timer = Assert.Single(scheduledTimers);
Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
timer.action();
Assert.False(raised);
}
[Fact]
public void Hold_Should_Be_Cancelled_When_Second_Contact_Is_Detected()
{
using var scope = AvaloniaLocator.EnterScope();
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
var scheduledTimers = new List<(TimeSpan time, Action action)>();
using var app = UnitTestApplication.Start(new TestServices(
threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
Border border = new Border();
Gestures.SetIsHoldWithMouseEnabled(border, true);
var decorator = new Decorator
{
Child = border
};
var cancelled = false;
decorator.AddHandler(Gestures.HoldingEvent, (s, e) => cancelled = e.HoldingState == HoldingState.Cancelled);
_mouse.Down(border);
Assert.False(cancelled);
var timer = Assert.Single(scheduledTimers);
Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
timer.action();
var secondMouse = new MouseTestHelper();
secondMouse.Down(border);
Assert.True(cancelled);
}
[Fact]
public void Hold_Should_Be_Cancelled_When_Pointer_Moves_Too_Far()
{
using var scope = AvaloniaLocator.EnterScope();
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
iSettingsMock.Setup(x => x.GetTapSize(It.IsAny<PointerType>())).Returns(new Size(16, 16));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
var scheduledTimers = new List<(TimeSpan time, Action action)>();
using var app = UnitTestApplication.Start(new TestServices(
threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
Border border = new Border();
Gestures.SetIsHoldWithMouseEnabled(border, true);
var decorator = new Decorator
{
Child = border
};
var cancelled = false;
decorator.AddHandler(Gestures.HoldingEvent, (s, e) => cancelled = e.HoldingState == HoldingState.Cancelled);
_mouse.Down(border);
var timer = Assert.Single(scheduledTimers);
Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
timer.action();
_mouse.Move(border, position: new Point(3, 3));
Assert.False(cancelled);
_mouse.Move(border, position: new Point(20, 20));
Assert.True(cancelled);
}
[Fact]
public void Hold_Should_Not_Be_Raised_For_Multiple_Contacts()
{
using var scope = AvaloniaLocator.EnterScope();
var iSettingsMock = new Mock<IPlatformSettings>();
iSettingsMock.Setup(x => x.HoldWaitDuration).Returns(TimeSpan.FromMilliseconds(300));
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(iSettingsMock.Object);
var scheduledTimers = new List<(TimeSpan time, Action action)>();
using var app = UnitTestApplication.Start(new TestServices(
threadingInterface: CreatePlatformThreadingInterface(t => scheduledTimers.Add(t))));
Border border = new Border();
Gestures.SetIsHoldWithMouseEnabled(border, true);
var decorator = new Decorator
{
Child = border
};
var raised = false;
decorator.AddHandler(Gestures.HoldingEvent, (s, e) => raised = e.HoldingState == HoldingState.Completed);
var secondMouse = new MouseTestHelper();
_mouse.Down(border, MouseButton.Left);
// Verify timer duration, but execute it immediately.
var timer = Assert.Single(scheduledTimers);
Assert.Equal(iSettingsMock.Object.HoldWaitDuration, timer.time);
timer.action();
secondMouse.Down(border, MouseButton.Left);
Assert.False(raised);
}
private static IPlatformThreadingInterface CreatePlatformThreadingInterface(Action<(TimeSpan, Action)> callback)
{
var threadingInterface = new Mock<IPlatformThreadingInterface>();
threadingInterface.SetupGet(p => p.CurrentThreadIsLoopThread).Returns(true);
threadingInterface.Setup(p => p
.StartTimer(It.IsAny<DispatcherPriority>(), It.IsAny<TimeSpan>(), It.IsAny<Action>()))
.Callback<DispatcherPriority, TimeSpan, Action>((_, t, a) => callback((t, a)));
return threadingInterface.Object;
}
private static void AddHandlers( private static void AddHandlers(
Decorator decorator, Decorator decorator,
Border border, Border border,
@ -201,5 +443,101 @@ namespace Avalonia.Base.UnitTests.Input
border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt"));
border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt"));
} }
[Fact]
public void Pinched_Should_Not_Be_Raised_For_Same_Pointer()
{
var touch = new TouchTestHelper();
Border border = new Border()
{
Width = 100,
Height = 100,
Background = new SolidColorBrush(Colors.Red)
};
border.GestureRecognizers.Add(new PinchGestureRecognizer());
var decorator = new Decorator
{
Child = border
};
var raised = false;
decorator.AddHandler(Gestures.PinchEvent, (s, e) => raised = true);
var firstPoint = new Point(5, 5);
var secondPoint = new Point(10, 10);
touch.Down(border, position: firstPoint);
touch.Down(border, position: secondPoint);
touch.Down(border, position: new Point(20, 20));
Assert.False(raised);
}
[Fact]
public void Pinched_Should_Be_Raised_For_Two_Pointers_Moving()
{
Border border = new Border()
{
Width = 100,
Height = 100,
Background = new SolidColorBrush(Colors.Red)
};
border.GestureRecognizers.Add(new PinchGestureRecognizer());
var decorator = new Decorator
{
Child = border
};
var raised = false;
decorator.AddHandler(Gestures.PinchEvent, (s, e) => raised = true);
var firstPoint = new Point(5, 5);
var secondPoint = new Point(10, 10);
var firstTouch = new TouchTestHelper();
var secondTouch = new TouchTestHelper();
firstTouch.Down(border, position: firstPoint);
secondTouch.Down(border, position: secondPoint);
secondTouch.Move(border, position: new Point(20, 20));
Assert.True(raised);
}
[Fact]
public void Scrolling_Should_Start_After_Start_Distance_Is_Exceded()
{
Border border = new Border()
{
Width = 100,
Height = 100,
Background = new SolidColorBrush(Colors.Red)
};
border.GestureRecognizers.Add(new ScrollGestureRecognizer()
{
CanHorizontallyScroll = true,
CanVerticallyScroll = true,
ScrollStartDistance = 50
});
var decorator = new Decorator
{
Child = border
};
var raised = false;
decorator.AddHandler(Gestures.ScrollGestureEvent, (s, e) => raised = true);
var firstTouch = new TouchTestHelper();
firstTouch.Down(border, position: new Point(5, 5));
firstTouch.Move(border, position: new Point(20, 20));
Assert.False(raised);
firstTouch.Move(border, position: new Point(70, 20));
Assert.True(raised);
}
} }
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save