Browse Source

Merge remote-tracking branch 'origin/master' into remove-reactive-package

# Conflicts:
#	src/Avalonia.Base/Input/Gestures.cs
pull/9749/head
Max Katz 3 years ago
parent
commit
4ca0dbac6e
  1. 2
      .editorconfig
  2. 1
      Directory.Build.props
  3. 31
      NOTICE.md
  4. 16
      azure-pipelines-integrationtests.yml
  5. 24
      azure-pipelines.yml
  6. 2
      build/Base.props
  7. 1
      build/SharedVersion.props
  8. 2
      build/System.Memory.props
  9. 6
      global.json
  10. 2
      samples/ControlCatalog.Android/MainActivity.cs
  11. 4
      samples/ControlCatalog.Android/Resources/values/styles.xml
  12. 2
      samples/ControlCatalog.Android/SplashActivity.cs
  13. 13
      samples/ControlCatalog.Browser/Properties/launchSettings.json
  14. 2
      samples/ControlCatalog.Desktop/Program.cs
  15. 3
      samples/ControlCatalog/MainView.xaml
  16. 23
      samples/ControlCatalog/MainView.xaml.cs
  17. 1
      samples/ControlCatalog/MainWindow.xaml
  18. 2
      samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs
  19. 2
      samples/ControlCatalog/Pages/PointerCanvas.cs
  20. 58
      samples/ControlCatalog/Pages/ScreenPage.cs
  21. 2
      samples/ControlCatalog/Pages/TabControlPage.xaml.cs
  22. 2
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  23. 7
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  24. 7
      samples/ControlCatalog/ViewModels/ContextPageViewModel.cs
  25. 2
      samples/ControlCatalog/ViewModels/CursorPageViewModel.cs
  26. 9
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  27. 2
      samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
  28. 5
      samples/Directory.Build.props
  29. 14
      src/Android/Avalonia.Android/AvaloniaSplashActivity.cs
  30. 2
      src/Android/Avalonia.Android/AvaloniaView.cs
  31. 1
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  32. 85
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  33. 2
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidMotionEventsHelper.cs
  34. 21
      src/Avalonia.Base/Controls/NameScopeExtensions.cs
  35. 2
      src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs
  36. 3
      src/Avalonia.Base/Input/Cursor.cs
  37. 43
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  38. 375
      src/Avalonia.Base/Input/GestureRecognizers/VelocityTracker.cs
  39. 121
      src/Avalonia.Base/Input/Gestures.cs
  40. 51
      src/Avalonia.Base/Input/HoldingRoutedEventArgs.cs
  41. 16
      src/Avalonia.Base/Input/InputElement.cs
  42. 2
      src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs
  43. 2
      src/Avalonia.Base/Media/DrawingContext.cs
  44. 4
      src/Avalonia.Base/Media/DrawingGroup.cs
  45. 5
      src/Avalonia.Base/Media/FontManager.cs
  46. 2
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  47. 27
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  48. 2
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  49. 2
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  50. 12
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  51. 22
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  52. 2
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  53. 28
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  54. 4
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  55. 40
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  56. 7
      src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs
  57. 2
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  58. 5
      src/Avalonia.Base/Media/TextFormatting/TextShaper.cs
  59. 68
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  60. 14
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  61. 4
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  62. 16
      src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs
  63. 2
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  64. 2
      src/Avalonia.Base/Platform/IPlatformSettings.cs
  65. 2
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  66. 2
      src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs
  67. 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  68. 32
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  69. 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs
  70. 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs
  71. 8
      src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
  72. 2
      src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs
  73. 53
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs
  74. 6
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  75. 3
      src/Avalonia.Base/Rendering/RenderLoop.cs
  76. 2
      src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs
  77. 2
      src/Avalonia.Base/Rendering/Utilities/TileBrushCalculator.cs
  78. 8
      src/Avalonia.Base/Threading/DispatcherTimer.cs
  79. 2
      src/Avalonia.Base/Utilities/SingleOrDictionary.cs
  80. 4
      src/Avalonia.Base/Utilities/StringTokenizer.cs
  81. 1
      src/Avalonia.Base/Visual.cs
  82. 4
      src/Avalonia.Build.Tasks/ComInteropHelper.cs
  83. 4
      src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs
  84. 4
      src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatHalfColorPalette.cs
  85. 4
      src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs
  86. 4
      src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialHalfColorPalette.cs
  87. 12
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  88. 2
      src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
  89. 15
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  90. 2
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  91. 2
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  92. 4
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  93. 16
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  94. 2
      src/Avalonia.Controls.DataGrid/IndexToValueTable.cs
  95. 3
      src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
  96. 1
      src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs
  97. 8
      src/Avalonia.Controls/BorderVisual.cs
  98. 11
      src/Avalonia.Controls/ContentControl.cs
  99. 21
      src/Avalonia.Controls/Control.cs
  100. 2
      src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs

2
.editorconfig

@ -169,6 +169,8 @@ dotnet_diagnostic.CA1828.severity = warning
dotnet_diagnostic.CA1829.severity = warning dotnet_diagnostic.CA1829.severity = warning
#CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters #CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
dotnet_diagnostic.CA1847.severity = warning dotnet_diagnostic.CA1847.severity = warning
#CACA2211:Non-constant fields should not be visible
dotnet_diagnostic.CA2211.severity = error
# Wrapping preferences # Wrapping preferences
csharp_wrap_before_ternary_opsigns = false csharp_wrap_before_ternary_opsigns = false

1
Directory.Build.props

@ -7,5 +7,6 @@
<AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies> <AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
<MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver> <MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver>
<RunApiCompat>False</RunApiCompat> <RunApiCompat>False</RunApiCompat>
<LangVersion>11</LangVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

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.

16
azure-pipelines-integrationtests.yml

@ -13,14 +13,14 @@ jobs:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401' displayName: 'Use .NET Core SDK 6.0.404'
inputs: inputs:
version: 6.0.401 version: 6.0.404
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100' displayName: 'Use .NET Core SDK 7.0.101'
inputs: inputs:
version: 7.0.100 version: 7.0.101
- script: system_profiler SPDisplaysDataType |grep Resolution - script: system_profiler SPDisplaysDataType |grep Resolution
@ -51,14 +51,14 @@ jobs:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401' displayName: 'Use .NET Core SDK 6.0.404'
inputs: inputs:
version: 6.0.401 version: 6.0.404
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100' displayName: 'Use .NET Core SDK 7.0.101'
inputs: inputs:
version: 7.0.100 version: 7.0.101
- task: Windows Application Driver@0 - task: Windows Application Driver@0
inputs: inputs:

24
azure-pipelines.yml

@ -30,14 +30,14 @@ jobs:
vmImage: 'ubuntu-20.04' vmImage: 'ubuntu-20.04'
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401' displayName: 'Use .NET Core SDK 6.0.404'
inputs: inputs:
version: 6.0.401 version: 6.0.404
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0' displayName: 'Use .NET Core SDK 7.0.101'
inputs: inputs:
version: 7.0.100 version: 7.0.101
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Workloads' displayName: 'Install Workloads'
@ -67,14 +67,14 @@ jobs:
vmImage: 'macos-12' vmImage: 'macos-12'
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401' displayName: 'Use .NET Core SDK 6.0.404'
inputs: inputs:
version: 6.0.401 version: 6.0.404
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100' displayName: 'Use .NET Core SDK 7.0.101'
inputs: inputs:
version: 7.0.100 version: 7.0.101
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Workloads' displayName: 'Install Workloads'
@ -138,14 +138,14 @@ jobs:
SolutionDir: '$(Build.SourcesDirectory)' SolutionDir: '$(Build.SourcesDirectory)'
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401' displayName: 'Use .NET Core SDK 6.0.404'
inputs: inputs:
version: 6.0.401 version: 6.0.404
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100' displayName: 'Use .NET Core SDK 7.0.101'
inputs: inputs:
version: 7.0.100 version: 7.0.101
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Workloads' displayName: 'Install Workloads'

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.Threading.Tasks.Extensions" Version="4.5.4" /> <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.6.0" /> <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.6.0" />

1
build/SharedVersion.props

@ -8,7 +8,6 @@
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl> <RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn> <NoWarn>$(NoWarn);CS1591</NoWarn>
<LangVersion>preview</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Icon.png</PackageIcon> <PackageIcon>Icon.png</PackageIcon>
<PackageDescription>Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.</PackageDescription> <PackageDescription>Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly.</PackageDescription>

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>

6
global.json

@ -1,11 +1,9 @@
{ {
"sdk": { "sdk": {
"version": "7.0.100", "version": "7.0.101",
"rollForward": "latestFeature" "rollForward": "latestFeature"
}, },
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43", "Microsoft.Build.Traversal": "1.0.43"
"MSBuild.Sdk.Extras": "3.0.22",
"AggregatePackage.NuGet.Sdk" : "0.1.12"
} }
} }

2
samples/ControlCatalog.Android/MainActivity.cs

@ -5,7 +5,7 @@ using Avalonia.Android;
namespace ControlCatalog.Android namespace ControlCatalog.Android
{ {
[Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)] [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.Main", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize)]
public class MainActivity : AvaloniaMainActivity public class MainActivity : AvaloniaMainActivity
{ {
} }

4
samples/ControlCatalog.Android/Resources/values/styles.xml

@ -14,4 +14,8 @@
<item name="android:windowContentOverlay">@null</item> <item name="android:windowContentOverlay">@null</item>
</style> </style>
<style name="MyTheme.Main" parent ="MyTheme.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
</style>
</resources> </resources>

2
samples/ControlCatalog.Android/SplashActivity.cs

@ -28,6 +28,8 @@ namespace ControlCatalog.Android
base.OnResume(); base.OnResume();
StartActivity(new Intent(Application.Context, typeof(MainActivity))); StartActivity(new Intent(Application.Context, typeof(MainActivity)));
Finish();
} }
} }
} }

13
samples/ControlCatalog.Browser/Properties/launchSettings.json

@ -0,0 +1,13 @@
{
"profiles": {
"ControlCatalog.Browser": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/debug?browser={browserInspectUri}"
}
}
}

2
samples/ControlCatalog.Desktop/Program.cs

@ -23,7 +23,7 @@ namespace ControlCatalog
private static void ConfigureAssetAssembly(AppBuilder builder) private static void ConfigureAssetAssembly(AppBuilder builder)
{ {
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.GetService<IAssetLoader>() .GetRequiredService<IAssetLoader>()
.SetDefaultAssembly(typeof(App).Assembly); .SetDefaultAssembly(typeof(App).Assembly);
} }
} }

3
samples/ControlCatalog/MainView.xaml

@ -209,8 +209,7 @@
</ComboBox.Items> </ComboBox.Items>
</ComboBox> </ComboBox>
<ComboBox x:Name="TransparencyLevels" <ComboBox x:Name="TransparencyLevels"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch">
SelectedIndex="{Binding TransparencyLevel}">
<ComboBox.Items> <ComboBox.Items>
<WindowTransparencyLevel>None</WindowTransparencyLevel> <WindowTransparencyLevel>None</WindowTransparencyLevel>
<WindowTransparencyLevel>Transparent</WindowTransparencyLevel> <WindowTransparencyLevel>Transparent</WindowTransparencyLevel>

23
samples/ControlCatalog/MainView.xaml.cs

@ -6,6 +6,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.VisualTree;
using ControlCatalog.Models; using ControlCatalog.Models;
using ControlCatalog.Pages; using ControlCatalog.Pages;
@ -59,17 +60,25 @@ namespace ControlCatalog
}; };
var transparencyLevels = this.Get<ComboBox>("TransparencyLevels"); var transparencyLevels = this.Get<ComboBox>("TransparencyLevels");
IDisposable? backgroundSetter = null, paneBackgroundSetter = null; IDisposable? topLevelBackgroundSideSetter = null, sideBarBackgroundSetter = null, paneBackgroundSetter = null;
transparencyLevels.SelectionChanged += (sender, e) => transparencyLevels.SelectionChanged += (sender, e) =>
{ {
backgroundSetter?.Dispose(); topLevelBackgroundSideSetter?.Dispose();
sideBarBackgroundSetter?.Dispose();
paneBackgroundSetter?.Dispose(); paneBackgroundSetter?.Dispose();
if (transparencyLevels.SelectedItem is WindowTransparencyLevel selected if (transparencyLevels.SelectedItem is WindowTransparencyLevel selected)
&& selected != WindowTransparencyLevel.None)
{ {
var semiTransparentBrush = new ImmutableSolidColorBrush(Colors.Gray, 0.5); var topLevel = (TopLevel)this.GetVisualRoot()!;
backgroundSetter = sideBar.SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); topLevel.TransparencyLevelHint = selected;
paneBackgroundSetter = sideBar.SetValue(SplitView.PaneBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style);
if (selected != WindowTransparencyLevel.None)
{
var transparentBrush = new ImmutableSolidColorBrush(Colors.White, 0);
var semiTransparentBrush = new ImmutableSolidColorBrush(Colors.Gray, 0.2);
topLevelBackgroundSideSetter = topLevel.SetValue(BackgroundProperty, transparentBrush, Avalonia.Data.BindingPriority.Style);
sideBarBackgroundSetter = sideBar.SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style);
paneBackgroundSetter = sideBar.SetValue(SplitView.PaneBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style);
}
} }
}; };
} }

1
samples/ControlCatalog/MainWindow.xaml

@ -10,7 +10,6 @@
ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}" ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}"
ExtendClientAreaChromeHints="{Binding ChromeHints}" ExtendClientAreaChromeHints="{Binding ChromeHints}"
ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}"
TransparencyLevelHint="{Binding TransparencyLevel}"
x:Name="MainWindow" x:Name="MainWindow"
Background="Transparent" Background="Transparent"
x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"

2
samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs

@ -17,7 +17,7 @@ namespace ControlCatalog.Pages
this.Get<TextBlock>("TimePickerDesc").Text = "Use a TimePicker to let users set a time in your app, for example " + this.Get<TextBlock>("TimePickerDesc").Text = "Use a TimePicker to let users set a time in your app, for example " +
"to set a reminder. The TimePicker displays three controls for hour, minute, and AM / PM(if necessary).These controls " + "to set a reminder. The TimePicker displays three controls for hour, minute, and AM / PM(if necessary).These controls " +
"are easy to use with touch or mouse, and they can be styled and configured in several different ways. " + "are easy to use with touch or mouse, and they can be styled and configured in several different ways. " +
"12 - hour or 24 - hour clock and visiblility of AM / PM is dynamically set based on user time settings, or can be overridden."; "12 - hour or 24 - hour clock and visibility of AM / PM is dynamically set based on user time settings, or can be overridden.";
} }

2
samples/ControlCatalog/Pages/PointerCanvas.cs

@ -24,7 +24,7 @@ public class PointerCanvas : Control
{ {
struct CanvasPoint struct CanvasPoint
{ {
public IBrush Brush; public IBrush? Brush;
public Point Point; public Point Point;
public double Radius; public double Radius;
public double? Pressure; public double? Pressure;

58
samples/ControlCatalog/Pages/ScreenPage.cs

@ -36,44 +36,44 @@ namespace ControlCatalog.Pages
var drawBrush = Brushes.Black; var drawBrush = Brushes.Black;
Pen p = new Pen(drawBrush); Pen p = new Pen(drawBrush);
if (screens != null)
foreach (Screen screen in screens) foreach (Screen screen in screens)
{
if (screen.Bounds.X / 10f < _leftMost)
{ {
if (screen.Bounds.X / 10f < _leftMost) _leftMost = screen.Bounds.X / 10f;
{ Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
_leftMost = screen.Bounds.X / 10f; return;
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); }
return;
}
Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f, screen.Bounds.Width / 10f, Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f, screen.Bounds.Width / 10f,
screen.Bounds.Height / 10f); screen.Bounds.Height / 10f);
Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f, Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f,
screen.WorkingArea.Height / 10f); screen.WorkingArea.Height / 10f);
context.DrawRectangle(p, boundsRect);
context.DrawRectangle(p, workingAreaRect);
context.DrawRectangle(p, boundsRect);
context.DrawRectangle(p, workingAreaRect);
var formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height));
formattedText = var formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}"); context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height));
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20));
formattedText = CreateFormattedText($"Scaling: {screen.Scaling * 100}%"); formattedText =
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40)); CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20));
formattedText = CreateFormattedText($"IsPrimary: {screen.IsPrimary}"); formattedText = CreateFormattedText($"Scaling: {screen.Scaling * 100}%");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40));
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60)); formattedText = CreateFormattedText($"IsPrimary: {screen.IsPrimary}");
formattedText = context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60));
CreateFormattedText(
$"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}"); formattedText =
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 80)); CreateFormattedText(
} $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 80));
}
context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10)); context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10));
} }

2
samples/ControlCatalog/Pages/TabControlPage.xaml.cs

@ -51,7 +51,7 @@ namespace ControlCatalog.Pages
private static IBitmap LoadBitmap(string uri) private static IBitmap LoadBitmap(string uri)
{ {
var assets = AvaloniaLocator.Current!.GetService<IAssetLoader>()!; var assets = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
return new Bitmap(assets.Open(new Uri(uri))); return new Bitmap(assets.Open(new Uri(uri)));
} }
} }

2
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -38,7 +38,7 @@
UseFloatingWatermark="True" UseFloatingWatermark="True"
PasswordChar="*" PasswordChar="*"
Text="Password" /> Text="Password" />
<TextBox Width="200" Text="Left aligned text" TextAlignment="Left" /> <TextBox Width="200" Text="Left aligned text" TextAlignment="Left" AcceptsTab="True" />
<TextBox Width="200" Text="Center aligned text" TextAlignment="Center" /> <TextBox Width="200" Text="Center aligned text" TextAlignment="Center" />
<TextBox Width="200" Text="Right aligned text" TextAlignment="Right" /> <TextBox Width="200" Text="Right aligned text" TextAlignment="Right" />
<TextBox Width="200" Text="Custom selection brush" <TextBox Width="200" Text="Custom selection brush"

7
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@ -11,12 +11,5 @@
<CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" /> <CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" />
<CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" /> <CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" />
<Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" /> <Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" />
<ComboBox x:Name="TransparencyLevels" SelectedIndex="{Binding TransparencyLevel}">
<ComboBoxItem>None</ComboBoxItem>
<ComboBoxItem>Transparent</ComboBoxItem>
<ComboBoxItem>Blur</ComboBoxItem>
<ComboBoxItem>AcrylicBlur</ComboBoxItem>
<ComboBoxItem>Mica</ComboBoxItem>
</ComboBox>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

7
samples/ControlCatalog/ViewModels/ContextPageViewModel.cs

@ -56,12 +56,9 @@ namespace ControlCatalog.ViewModels
var result = await window.StorageProvider.OpenFilePickerAsync(new Avalonia.Platform.Storage.FilePickerOpenOptions() { AllowMultiple = true }); var result = await window.StorageProvider.OpenFilePickerAsync(new Avalonia.Platform.Storage.FilePickerOpenOptions() { AllowMultiple = true });
if (result != null) foreach (var file in result)
{ {
foreach (var file in result) System.Diagnostics.Debug.WriteLine($"Opened: {file.Name}");
{
System.Diagnostics.Debug.WriteLine($"Opened: {file.Name}");
}
} }
} }

2
samples/ControlCatalog/ViewModels/CursorPageViewModel.cs

@ -18,7 +18,7 @@ namespace ControlCatalog.ViewModels
.Select(x => new StandardCursorModel(x)) .Select(x => new StandardCursorModel(x))
.ToList(); .ToList();
var loader = AvaloniaLocator.Current!.GetService<IAssetLoader>()!; var loader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var s = loader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png")); var s = loader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png"));
var bitmap = new Bitmap(s); var bitmap = new Bitmap(s);
CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16)); CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16));

9
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -12,12 +12,9 @@ namespace ControlCatalog.ViewModels
{ {
class MainWindowViewModel : ViewModelBase class MainWindowViewModel : ViewModelBase
{ {
private IManagedNotificationManager _notificationManager;
private bool _isMenuItemChecked = true; private bool _isMenuItemChecked = true;
private WindowState _windowState; private WindowState _windowState;
private WindowState[] _windowStates = Array.Empty<WindowState>(); private WindowState[] _windowStates = Array.Empty<WindowState>();
private int _transparencyLevel;
private ExtendClientAreaChromeHints _chromeHints = ExtendClientAreaChromeHints.PreferSystemChrome; private ExtendClientAreaChromeHints _chromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
private bool _extendClientAreaEnabled; private bool _extendClientAreaEnabled;
private bool _systemTitleBarEnabled; private bool _systemTitleBarEnabled;
@ -77,12 +74,6 @@ namespace ControlCatalog.ViewModels
TitleBarHeight = -1; TitleBarHeight = -1;
} }
public int TransparencyLevel
{
get { return _transparencyLevel; }
set { this.RaiseAndSetIfChanged(ref _transparencyLevel, value); }
}
public ExtendClientAreaChromeHints ChromeHints public ExtendClientAreaChromeHints ChromeHints
{ {
get { return _chromeHints; } get { return _chromeHints; }

2
samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs

@ -19,7 +19,7 @@ namespace ControlCatalog.ViewModels
{ {
public TransitioningContentControlPageViewModel() public TransitioningContentControlPageViewModel()
{ {
var assetLoader = AvaloniaLocator.Current?.GetService<IAssetLoader>()!; var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
var images = new string[] var images = new string[]
{ {

5
samples/Directory.Build.props

@ -2,9 +2,8 @@
<PropertyGroup> <PropertyGroup>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netcoreapp2.0\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath> <AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)..\src\tools\Avalonia.Designer.HostApp\bin\Debug\netcoreapp2.0\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<LangVersion>11</LangVersion>
</PropertyGroup> </PropertyGroup>
<Import Project="..\build\SharedVersion.props" /> <Import Project="..\build\SharedVersion.props" />
<PropertyGroup>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
</PropertyGroup>
</Project> </Project>

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 Avalonia.Controls;
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/AvaloniaView.cs

@ -24,6 +24,8 @@ namespace Avalonia.Android
_root = new EmbeddableControlRoot(_view); _root = new EmbeddableControlRoot(_view);
_root.Prepare(); _root.Prepare();
this.SetBackgroundColor(global::Android.Graphics.Color.Transparent);
} }
internal TopLevelImpl TopLevelImpl => _view; internal TopLevelImpl TopLevelImpl => _view;

1
src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs

@ -22,6 +22,7 @@ namespace Avalonia.Android
public InvalidationAwareSurfaceView(Context context) : base(context) public InvalidationAwareSurfaceView(Context context) : base(context)
{ {
Holder.AddCallback(this); Holder.AddCallback(this);
Holder.SetFormat(global::Android.Graphics.Format.Transparent);
_handler = new Handler(context.MainLooper); _handler = new Handler(context.MainLooper);
} }

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

@ -26,6 +26,7 @@ using Avalonia.Rendering.Composition;
using Java.Lang; using Java.Lang;
using Math = System.Math; using Math = System.Math;
using AndroidRect = Android.Graphics.Rect; using AndroidRect = Android.Graphics.Rect;
using Android.Graphics.Drawables;
namespace Avalonia.Android.Platform.SkiaPlatform namespace Avalonia.Android.Platform.SkiaPlatform
{ {
@ -283,7 +284,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public Action LostFocus { get; set; } public Action LostFocus { get; set; }
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; } public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; public WindowTransparencyLevel TransparencyLevel { get; private set; }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
@ -301,7 +302,87 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{ {
throw new NotImplementedException(); if (TransparencyLevel != transparencyLevel)
{
bool isBelowR = Build.VERSION.SdkInt < BuildVersionCodes.R;
bool isAboveR = Build.VERSION.SdkInt > BuildVersionCodes.R;
if (_view.Context is AvaloniaMainActivity activity)
{
switch (transparencyLevel)
{
case WindowTransparencyLevel.AcrylicBlur:
case WindowTransparencyLevel.ForceAcrylicBlur:
case WindowTransparencyLevel.Mica:
case WindowTransparencyLevel.None:
if (!isBelowR)
{
activity.SetTranslucent(false);
}
if (isAboveR)
{
activity.Window?.ClearFlags(WindowManagerFlags.BlurBehind);
var attr = activity.Window?.Attributes;
if (attr != null)
{
attr.BlurBehindRadius = 0;
activity.Window.Attributes = attr;
}
}
activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.White));
if(transparencyLevel != WindowTransparencyLevel.None)
{
return;
}
break;
case WindowTransparencyLevel.Transparent:
if (!isBelowR)
{
activity.SetTranslucent(true);
}
if (isAboveR)
{
activity.Window?.ClearFlags(WindowManagerFlags.BlurBehind);
var attr = activity.Window?.Attributes;
if (attr != null)
{
attr.BlurBehindRadius = 0;
activity.Window.Attributes = attr;
}
}
activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.Transparent));
break;
case WindowTransparencyLevel.Blur:
if (isAboveR)
{
activity.SetTranslucent(true);
activity.Window?.AddFlags(WindowManagerFlags.BlurBehind);
var attr = activity.Window?.Attributes;
if (attr != null)
{
attr.BlurBehindRadius = 120;
activity.Window.Attributes = attr;
}
activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.Transparent));
}
else
{
activity.Window?.ClearFlags(WindowManagerFlags.BlurBehind);
activity.Window.SetBackgroundDrawable(new ColorDrawable(Color.White));
return;
}
break;
}
TransparencyLevel = transparencyLevel;
}
}
} }
} }

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

21
src/Avalonia.Base/Controls/NameScopeExtensions.cs

@ -25,13 +25,18 @@ namespace Avalonia.Controls
var result = nameScope.Find(name); var result = nameScope.Find(name);
if (result != null && !(result is T)) if (result == null)
{ {
throw new InvalidOperationException( return null;
$"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'.");
} }
return (T?)result; if (result is T typed)
{
return typed;
}
throw new InvalidOperationException(
$"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'.");
} }
/// <summary> /// <summary>
@ -74,13 +79,13 @@ namespace Avalonia.Controls
throw new KeyNotFoundException($"Could not find control '{name}'."); throw new KeyNotFoundException($"Could not find control '{name}'.");
} }
if (!(result is T)) if (result is T typed)
{ {
throw new InvalidOperationException( return typed;
$"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'.");
} }
return (T)result; throw new InvalidOperationException(
$"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'.");
} }
/// <summary> /// <summary>

2
src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs

@ -38,7 +38,7 @@ namespace Avalonia.Data.Converters
} }
else if (Equals(obj, default(TIn))) else if (Equals(obj, default(TIn)))
{ {
yield return default(TIn); yield return default;
} }
} }
} }

3
src/Avalonia.Base/Input/Cursor.cs

@ -71,8 +71,7 @@ namespace Avalonia.Input
private static ICursorFactory GetCursorFactory() private static ICursorFactory GetCursorFactory()
{ {
return AvaloniaLocator.Current.GetService<ICursorFactory>() ?? return AvaloniaLocator.Current.GetRequiredService<ICursorFactory>();
throw new Exception("Could not create Cursor: ICursorFactory not registered.");
} }
} }
} }

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

@ -17,7 +17,9 @@ namespace Avalonia.Input.GestureRecognizers
private bool _canVerticallyScroll; private bool _canVerticallyScroll;
private int _gestureId; private int _gestureId;
private int _scrollStartDistance = 30; 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;
@ -91,7 +93,7 @@ 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);
} }
} }
@ -111,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);
} }
} }
@ -118,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;
} }
@ -150,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
@ -183,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);
}
}
}

121
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.Reactive; using Avalonia.Reactive;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -9,6 +11,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,
@ -46,6 +63,7 @@ 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 = public static readonly RoutedEvent<PinchEventArgs> PinchEvent =
RoutedEvent.Register<PinchEventArgs>( RoutedEvent.Register<PinchEventArgs>(
@ -59,14 +77,40 @@ namespace Avalonia.Input
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)
@ -111,11 +155,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)
{ {
@ -149,7 +224,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));
} }
@ -161,6 +241,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

@ -189,11 +189,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;
@ -353,6 +358,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.

2
src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs

@ -7,7 +7,7 @@ namespace Avalonia.Layout
{ {
internal struct UvMeasure internal struct UvMeasure
{ {
internal static readonly UvMeasure Zero = default(UvMeasure); internal static readonly UvMeasure Zero = default;
internal double U { get; set; } internal double U { get; set; }

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

@ -279,7 +279,7 @@ namespace Avalonia.Media
OpacityMask, OpacityMask,
} }
public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default(Matrix)) public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default)
{ {
if (context._states is null) if (context._states is null)
throw new ObjectDisposedException(nameof(DrawingContext)); throw new ObjectDisposedException(nameof(DrawingContext));

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

@ -76,8 +76,8 @@ namespace Avalonia.Media
{ {
using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity)) using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity))
using (context.PushOpacity(Opacity)) using (context.PushOpacity(Opacity))
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default(DrawingContext.PushedState)) using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default(DrawingContext.PushedState)) using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default)
{ {
foreach (var drawing in Children) foreach (var drawing in Children)
{ {

5
src/Avalonia.Base/Media/FontManager.cs

@ -47,10 +47,7 @@ namespace Avalonia.Media
return current; return current;
} }
var fontManagerImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>(); var fontManagerImpl = AvaloniaLocator.Current.GetRequiredService<IFontManagerImpl>();
if (fontManagerImpl == null)
throw new InvalidOperationException("No font manager implementation was registered.");
current = new FontManager(fontManagerImpl); current = new FontManager(fontManagerImpl);

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

@ -218,7 +218,7 @@ namespace Avalonia.Media
OpacityMask, OpacityMask,
} }
internal PushedState(ImmediateDrawingContext context, PushedStateType type, Matrix matrix = default(Matrix)) internal PushedState(ImmediateDrawingContext context, PushedStateType type, Matrix matrix = default)
{ {
if (context._states is null) if (context._states is null)
throw new ObjectDisposedException(nameof(ImmediateDrawingContext)); throw new ObjectDisposedException(nameof(ImmediateDrawingContext));

27
src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs

@ -140,7 +140,7 @@ namespace Avalonia.Media.TextFormatting
throw new ArgumentOutOfRangeException(nameof(index)); throw new ArgumentOutOfRangeException(nameof(index));
} }
#endif #endif
return Span[index]; return CharacterBufferReference.CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index];
} }
} }
@ -163,27 +163,18 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Gets the character memory buffer /// Gets the character memory buffer
/// </summary> /// </summary>
internal ReadOnlyMemory<char> CharacterBuffer internal ReadOnlyMemory<char> CharacterBuffer => CharacterBufferReference.CharacterBuffer;
{
get { return CharacterBufferReference.CharacterBuffer; }
}
/// <summary> /// <summary>
/// Gets the character offset relative to the beginning of buffer to /// Gets the character offset relative to the beginning of buffer to
/// the first character of the run /// the first character of the run
/// </summary> /// </summary>
internal int OffsetToFirstChar internal int OffsetToFirstChar => CharacterBufferReference.OffsetToFirstChar;
{
get { return CharacterBufferReference.OffsetToFirstChar; }
}
/// <summary> /// <summary>
/// Indicate whether the character buffer range is empty /// Indicate whether the character buffer range is empty
/// </summary> /// </summary>
internal bool IsEmpty internal bool IsEmpty => CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0;
{
get { return CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0; }
}
internal CharacterBufferRange Take(int length) internal CharacterBufferRange Take(int length)
{ {
@ -280,14 +271,8 @@ namespace Avalonia.Media.TextFormatting
int IReadOnlyCollection<char>.Count => Length; int IReadOnlyCollection<char>.Count => Length;
public IEnumerator<char> GetEnumerator() public IEnumerator<char> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<char>(this);
{
return new ImmutableReadOnlyListStructEnumerator<char>(this);
}
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
{
return GetEnumerator();
}
} }
} }

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

@ -132,7 +132,7 @@ namespace Avalonia.Media.TextFormatting
{ {
var grapheme = graphemeEnumerator.Current; var grapheme = graphemeEnumerator.Current;
finalLength += grapheme.Text.Length; finalLength += grapheme.Length;
if (finalLength >= length) if (finalLength >= length)
{ {

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

@ -91,7 +91,7 @@ namespace Avalonia.Media.TextFormatting
continue; continue;
} }
if (textRun is ShapedTextCharacters shapedText) if (textRun is ShapedTextRun shapedText)
{ {
var glyphRun = shapedText.GlyphRun; var glyphRun = shapedText.GlyphRun;
var shapedBuffer = shapedText.ShapedBuffer; var shapedBuffer = shapedText.ShapedBuffer;

12
src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs → src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -6,11 +6,11 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// A text run that holds shaped characters. /// A text run that holds shaped characters.
/// </summary> /// </summary>
public sealed class ShapedTextCharacters : DrawableTextRun public sealed class ShapedTextRun : DrawableTextRun
{ {
private GlyphRun? _glyphRun; private GlyphRun? _glyphRun;
public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties) public ShapedTextRun(ShapedBuffer shapedBuffer, TextRunProperties properties)
{ {
ShapedBuffer = shapedBuffer; ShapedBuffer = shapedBuffer;
CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference; CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference;
@ -155,7 +155,7 @@ namespace Avalonia.Media.TextFormatting
return length > 0; return length > 0;
} }
internal SplitResult<ShapedTextCharacters> Split(int length) internal SplitResult<ShapedTextRun> Split(int length)
{ {
if (IsReversed) if (IsReversed)
{ {
@ -171,7 +171,7 @@ namespace Avalonia.Media.TextFormatting
var splitBuffer = ShapedBuffer.Split(length); var splitBuffer = ShapedBuffer.Split(length);
var first = new ShapedTextCharacters(splitBuffer.First, Properties); var first = new ShapedTextRun(splitBuffer.First, Properties);
#if DEBUG #if DEBUG
@ -182,9 +182,9 @@ namespace Avalonia.Media.TextFormatting
#endif #endif
var second = new ShapedTextCharacters(splitBuffer.Second!, Properties); var second = new ShapedTextRun(splitBuffer.Second!, Properties);
return new SplitResult<ShapedTextCharacters>(first, second); return new SplitResult<ShapedTextRun>(first, second);
} }
internal GlyphRun CreateGlyphRun() internal GlyphRun CreateGlyphRun()

22
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -91,12 +91,12 @@ namespace Avalonia.Media.TextFormatting
public override TextRunProperties Properties { get; } public override TextRunProperties Properties { get; }
/// <summary> /// <summary>
/// Gets a list of <see cref="ShapeableTextCharacters"/>. /// Gets a list of <see cref="UnshapedTextRun"/>.
/// </summary> /// </summary>
/// <returns>The shapeable text characters.</returns> /// <returns>The shapeable text characters.</returns>
internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties) internal IReadOnlyList<UnshapedTextRun> GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{ {
var shapeableCharacters = new List<ShapeableTextCharacters>(2); var shapeableCharacters = new List<UnshapedTextRun>(2);
while (characterBufferRange.Length > 0) while (characterBufferRange.Length > 0)
{ {
@ -120,7 +120,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="biDiLevel">The bidi level of the run.</param> /// <param name="biDiLevel">The bidi level of the run.</param>
/// <param name="previousProperties"></param> /// <param name="previousProperties"></param>
/// <returns>A list of shapeable text runs.</returns> /// <returns>A list of shapeable text runs.</returns>
private static ShapeableTextCharacters CreateShapeableRun(CharacterBufferRange characterBufferRange, private static UnshapedTextRun CreateShapeableRun(CharacterBufferRange characterBufferRange,
TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{ {
var defaultTypeface = defaultProperties.Typeface; var defaultTypeface = defaultProperties.Typeface;
@ -133,12 +133,12 @@ namespace Avalonia.Media.TextFormatting
{ {
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _)) if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _))
{ {
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, fallbackCount, return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, fallbackCount,
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
} }
} }
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
biDiLevel); biDiLevel);
} }
@ -146,7 +146,7 @@ namespace Avalonia.Media.TextFormatting
{ {
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _)) if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _))
{ {
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count,
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
} }
} }
@ -176,7 +176,7 @@ namespace Avalonia.Media.TextFormatting
if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _)) if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _))
{ {
//Fallback found //Fallback found
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface), return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
biDiLevel); biDiLevel);
} }
@ -196,10 +196,10 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
count += grapheme.Text.Length; count += grapheme.Length;
} }
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel); return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel);
} }
/// <summary> /// <summary>
@ -264,7 +264,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
length += currentGrapheme.Text.Length; length += currentGrapheme.Length;
} }
return length > 0; return length > 0;

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

@ -31,7 +31,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun) switch (currentRun)
{ {
case ShapedTextCharacters shapedRun: case ShapedTextRun shapedRun:
{ {
currentWidth += shapedRun.Size.Width; currentWidth += shapedRun.Size.Width;

28
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -124,7 +124,7 @@ namespace Avalonia.Media.TextFormatting
var second = new List<DrawableTextRun>(secondCount); var second = new List<DrawableTextRun>(secondCount);
if (currentRun is ShapedTextCharacters shapedTextCharacters) if (currentRun is ShapedTextRun shapedTextCharacters)
{ {
var split = shapedTextCharacters.Split(length - currentLength); var split = shapedTextCharacters.Split(length - currentLength);
@ -206,16 +206,16 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
case ShapeableTextCharacters shapeableRun: case UnshapedTextRun shapeableRun:
{ {
var groupedRuns = new List<ShapeableTextCharacters>(2) { shapeableRun }; var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun };
var characterBufferReference = currentRun.CharacterBufferReference; var characterBufferReference = currentRun.CharacterBufferReference;
var length = currentRun.Length; var length = currentRun.Length;
var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar; var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar;
while (index + 1 < processedRuns.Count) while (index + 1 < processedRuns.Count)
{ {
if (processedRuns[index + 1] is not ShapeableTextCharacters nextRun) if (processedRuns[index + 1] is not UnshapedTextRun nextRun)
{ {
break; break;
} }
@ -258,10 +258,10 @@ namespace Avalonia.Media.TextFormatting
return drawableTextRuns; return drawableTextRuns;
} }
private static IReadOnlyList<ShapedTextCharacters> ShapeTogether( private static IReadOnlyList<ShapedTextRun> ShapeTogether(
IReadOnlyList<ShapeableTextCharacters> textRuns, CharacterBufferReference text, int length, TextShaperOptions options) IReadOnlyList<UnshapedTextRun> textRuns, CharacterBufferReference text, int length, TextShaperOptions options)
{ {
var shapedRuns = new List<ShapedTextCharacters>(textRuns.Count); var shapedRuns = new List<ShapedTextRun>(textRuns.Count);
var shapedBuffer = TextShaper.Current.ShapeText(text, length, options); var shapedBuffer = TextShaper.Current.ShapeText(text, length, options);
@ -271,7 +271,7 @@ namespace Avalonia.Media.TextFormatting
var splitResult = shapedBuffer.Split(currentRun.Length); var splitResult = shapedBuffer.Split(currentRun.Length);
shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties)); shapedRuns.Add(new ShapedTextRun(splitResult.First, currentRun.Properties));
shapedBuffer = splitResult.Second!; shapedBuffer = splitResult.Second!;
} }
@ -280,9 +280,9 @@ namespace Avalonia.Media.TextFormatting
} }
/// <summary> /// <summary>
/// Coalesces ranges of the same bidi level to form <see cref="ShapeableTextCharacters"/> /// Coalesces ranges of the same bidi level to form <see cref="UnshapedTextRun"/>
/// </summary> /// </summary>
/// <param name="textCharacters">The text characters to form <see cref="ShapeableTextCharacters"/> from.</param> /// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param>
/// <param name="levels">The bidi levels.</param> /// <param name="levels">The bidi levels.</param>
/// <returns></returns> /// <returns></returns>
private static IEnumerable<IReadOnlyList<TextRun>> CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ArraySlice<sbyte> levels) private static IEnumerable<IReadOnlyList<TextRun>> CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ArraySlice<sbyte> levels)
@ -474,7 +474,7 @@ namespace Avalonia.Media.TextFormatting
{ {
switch (currentRun) switch (currentRun)
{ {
case ShapedTextCharacters shapedTextCharacters: case ShapedTextRun shapedTextCharacters:
{ {
if(shapedTextCharacters.ShapedBuffer.Length > 0) if(shapedTextCharacters.ShapedBuffer.Length > 0)
{ {
@ -538,7 +538,7 @@ namespace Avalonia.Media.TextFormatting
var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection); (sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) }; var textRuns = new List<DrawableTextRun> { new ShapedTextRun(shapedBuffer, properties) };
return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine();
} }
@ -744,7 +744,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns> /// <returns>
/// The shaped symbol. /// The shaped symbol.
/// </returns> /// </returns>
internal static ShapedTextCharacters CreateSymbol(TextRun textRun, FlowDirection flowDirection) internal static ShapedTextRun CreateSymbol(TextRun textRun, FlowDirection flowDirection)
{ {
var textShaper = TextShaper.Current; var textShaper = TextShaper.Current;
@ -760,7 +760,7 @@ namespace Avalonia.Media.TextFormatting
var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions); var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions);
return new ShapedTextCharacters(shapedBuffer, textRun.Properties); return new ShapedTextRun(shapedBuffer, textRun.Properties);
} }
} }
} }

4
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -65,7 +65,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun) switch (currentRun)
{ {
case ShapedTextCharacters shapedRun: case ShapedTextRun shapedRun:
{ {
currentWidth += currentRun.Size.Width; currentWidth += currentRun.Size.Width;
@ -118,7 +118,7 @@ namespace Avalonia.Media.TextFormatting
switch (run) switch (run)
{ {
case ShapedTextCharacters endShapedRun: case ShapedTextRun endShapedRun:
{ {
if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth, if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
out var suffixCount, out var suffixWidth)) out var suffixCount, out var suffixWidth))

40
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -192,14 +192,14 @@ namespace Avalonia.Media.TextFormatting
{ {
var currentRun = _textRuns[i]; var currentRun = _textRuns[i];
if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) if (currentRun is ShapedTextRun shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
{ {
var rightToLeftIndex = i; var rightToLeftIndex = i;
currentPosition += currentRun.Length; currentPosition += currentRun.Length;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1) while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
{ {
var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextCharacters; var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun;
if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight) if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight)
{ {
@ -255,7 +255,7 @@ namespace Avalonia.Media.TextFormatting
switch (run) switch (run)
{ {
case ShapedTextCharacters shapedRun: case ShapedTextRun shapedRun:
{ {
characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
@ -303,7 +303,7 @@ namespace Avalonia.Media.TextFormatting
{ {
var currentRun = _textRuns[index]; var currentRun = _textRuns[index];
if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) if (currentRun is ShapedTextRun shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
{ {
var i = index; var i = index;
@ -313,7 +313,7 @@ namespace Avalonia.Media.TextFormatting
{ {
var nextRun = _textRuns[i + 1]; var nextRun = _textRuns[i + 1];
if (nextRun is ShapedTextCharacters nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight) if (nextRun is ShapedTextRun nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight)
{ {
i++; i++;
@ -407,7 +407,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun) switch (currentRun)
{ {
case ShapedTextCharacters shapedTextCharacters: case ShapedTextRun shapedTextCharacters:
{ {
currentGlyphRun = shapedTextCharacters.GlyphRun; currentGlyphRun = shapedTextCharacters.GlyphRun;
@ -476,7 +476,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun) switch (currentRun)
{ {
case ShapedTextCharacters shapedRun: case ShapedTextRun shapedRun:
{ {
nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit); nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
break; break;
@ -550,7 +550,7 @@ namespace Avalonia.Media.TextFormatting
double combinedWidth; double combinedWidth;
if (currentRun is ShapedTextCharacters currentShapedRun) if (currentRun is ShapedTextRun currentShapedRun)
{ {
var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster; var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster;
@ -592,7 +592,7 @@ namespace Avalonia.Media.TextFormatting
var rightToLeftIndex = index; var rightToLeftIndex = index;
var rightToLeftWidth = currentShapedRun.Size.Width; var rightToLeftWidth = currentShapedRun.Size.Width;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun) while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
{ {
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
{ {
@ -624,12 +624,12 @@ namespace Avalonia.Media.TextFormatting
for (int i = rightToLeftIndex - 1; i >= index; i--) for (int i = rightToLeftIndex - 1; i >= index; i--)
{ {
if (TextRuns[i] is not ShapedTextCharacters) if (TextRuns[i] is not ShapedTextRun)
{ {
continue; continue;
} }
currentShapedRun = (ShapedTextCharacters)TextRuns[i]; currentShapedRun = (ShapedTextRun)TextRuns[i];
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
@ -769,7 +769,7 @@ namespace Avalonia.Media.TextFormatting
var characterLength = 0; var characterLength = 0;
var endX = startX; var endX = startX;
if (currentRun is ShapedTextCharacters currentShapedRun) if (currentRun is ShapedTextRun currentShapedRun)
{ {
var offset = Math.Max(0, firstTextSourceIndex - currentPosition); var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
@ -883,7 +883,7 @@ namespace Avalonia.Media.TextFormatting
return result; return result;
} }
private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextCharacters currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength) private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength)
{ {
var startX = endX; var startX = endX;
@ -945,7 +945,7 @@ namespace Avalonia.Media.TextFormatting
private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection) private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection)
{ {
if (run is ShapedTextCharacters shapedTextCharacters) if (run is ShapedTextRun shapedTextCharacters)
{ {
return shapedTextCharacters.BidiLevel; return shapedTextCharacters.BidiLevel;
} }
@ -1027,7 +1027,7 @@ namespace Avalonia.Media.TextFormatting
{ {
if (current.Level >= minLevelToReverse && current.Level % 2 != 0) if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
{ {
if (current.Run is ShapedTextCharacters { IsReversed: false } shapedTextCharacters) if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters)
{ {
shapedTextCharacters.Reverse(); shapedTextCharacters.Reverse();
} }
@ -1145,7 +1145,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun) switch (currentRun)
{ {
case ShapedTextCharacters shapedRun: case ShapedTextRun shapedRun:
{ {
var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
@ -1230,7 +1230,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun) switch (currentRun)
{ {
case ShapedTextCharacters shapedRun: case ShapedTextRun shapedRun:
{ {
var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
@ -1294,7 +1294,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun) switch (currentRun)
{ {
case ShapedTextCharacters shapedRun: case ShapedTextRun shapedRun:
{ {
var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster; var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
@ -1303,7 +1303,7 @@ namespace Avalonia.Media.TextFormatting
break; break;
} }
if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight) if (previousRun is ShapedTextRun previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
{ {
if (shapedRun.ShapedBuffer.IsLeftToRight) if (shapedRun.ShapedBuffer.IsLeftToRight)
{ {
@ -1394,7 +1394,7 @@ namespace Avalonia.Media.TextFormatting
{ {
switch (_textRuns[index]) switch (_textRuns[index])
{ {
case ShapedTextCharacters textRun: case ShapedTextRun textRun:
{ {
var textMetrics = var textMetrics =
new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize); new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize);

7
src/Avalonia.Base/Media/TextFormatting/TextParagraphProperties.cs

@ -63,14 +63,11 @@
{ {
get { return 0; } get { return 0; }
} }
/// <summary> /// <summary>
/// Gets the default incremental tab width. /// Gets the default incremental tab width.
/// </summary> /// </summary>
public virtual double DefaultIncrementalTab public virtual double DefaultIncrementalTab => 0;
{
get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; }
}
/// <summary> /// <summary>
/// Gets the letter spacing. /// Gets the letter spacing.

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

@ -44,7 +44,7 @@ namespace Avalonia.Media.TextFormatting
fixed (char* charsPtr = characterBuffer.Span) fixed (char* charsPtr = characterBuffer.Span)
{ {
return new string(charsPtr, 0, characterBuffer.Span.Length); return new string(charsPtr, _textRun.CharacterBufferReference.OffsetToFirstChar, _textRun.Length);
} }
} }
} }

5
src/Avalonia.Base/Media/TextFormatting/TextShaper.cs

@ -29,10 +29,7 @@ namespace Avalonia.Media.TextFormatting
return current; return current;
} }
var textShaperImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>(); var textShaperImpl = AvaloniaLocator.Current.GetRequiredService<ITextShaperImpl>();
if (textShaperImpl == null)
throw new InvalidOperationException("No text shaper implementation was registered.");
current = new TextShaper(textShaperImpl); current = new TextShaper(textShaperImpl);

68
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
@ -222,6 +223,71 @@ namespace Avalonia.Media.TextFormatting.Unicode
return new Codepoint(code); return new Codepoint(code);
} }
/// <summary>
/// Reads the <see cref="Codepoint"/> at specified position.
/// </summary>
/// <param name="text">The buffer to read from.</param>
/// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param>
/// <returns></returns>
public static Codepoint ReadAt(CharacterBufferRange text, int index, out int count)
{
count = 1;
if (index >= text.Length)
{
return ReplacementCodepoint;
}
var code = text[index];
ushort hi, low;
//# High surrogate
if (0xD800 <= code && code <= 0xDBFF)
{
hi = code;
if (index + 1 == text.Length)
{
return ReplacementCodepoint;
}
low = text[index + 1];
if (0xDC00 <= low && low <= 0xDFFF)
{
count = 2;
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
}
return ReplacementCodepoint;
}
//# Low surrogate
if (0xDC00 <= code && code <= 0xDFFF)
{
if (index == 0)
{
return ReplacementCodepoint;
}
hi = text[index - 1];
low = code;
if (0xD800 <= hi && hi <= 0xDBFF)
{
count = 2;
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
}
return ReplacementCodepoint;
}
return new Codepoint(code);
}
/// <summary> /// <summary>
/// Returns <see langword="true"/> if <paramref name="cp"/> is between /// Returns <see langword="true"/> if <paramref name="cp"/> is between

14
src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs

@ -7,10 +7,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary> /// </summary>
public readonly ref struct Grapheme public readonly ref struct Grapheme
{ {
public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text) public Grapheme(Codepoint firstCodepoint, int offset, int length)
{ {
FirstCodepoint = firstCodepoint; FirstCodepoint = firstCodepoint;
Text = text; Offset = offset;
Length = length;
} }
/// <summary> /// <summary>
@ -19,8 +20,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
public Codepoint FirstCodepoint { get; } public Codepoint FirstCodepoint { get; }
/// <summary> /// <summary>
/// The text that is representing the <see cref="Grapheme"/>. /// The Offset to the FirstCodepoint
/// </summary> /// </summary>
public ReadOnlySpan<char> Text { get; } public int Offset { get; }
/// <summary>
/// The length of the grapheme cluster
/// </summary>
public int Length { get; }
} }
} }

4
src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs

@ -185,9 +185,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
Return: Return:
var text = _text.Take(processor.CurrentCodeUnitOffset); Current = new Grapheme(firstCodepoint, _text.OffsetToFirstChar, processor.CurrentCodeUnitOffset);
Current = new Grapheme(firstCodepoint, text.Span);
_text = _text.Skip(processor.CurrentCodeUnitOffset); _text = _text.Skip(processor.CurrentCodeUnitOffset);

16
src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs → src/Avalonia.Base/Media/TextFormatting/UnshapedTextRun.cs

@ -5,9 +5,9 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// A group of characters that can be shaped. /// A group of characters that can be shaped.
/// </summary> /// </summary>
public sealed class ShapeableTextCharacters : TextRun public sealed class UnshapedTextRun : TextRun
{ {
public ShapeableTextCharacters(CharacterBufferReference characterBufferReference, int length, public UnshapedTextRun(CharacterBufferReference characterBufferReference, int length,
TextRunProperties properties, sbyte biDiLevel) TextRunProperties properties, sbyte biDiLevel)
{ {
CharacterBufferReference = characterBufferReference; CharacterBufferReference = characterBufferReference;
@ -24,30 +24,30 @@ namespace Avalonia.Media.TextFormatting
public sbyte BidiLevel { get; } public sbyte BidiLevel { get; }
public bool CanShapeTogether(ShapeableTextCharacters shapeableTextCharacters) public bool CanShapeTogether(UnshapedTextRun unshapedTextRun)
{ {
if (!CharacterBufferReference.Equals(shapeableTextCharacters.CharacterBufferReference)) if (!CharacterBufferReference.Equals(unshapedTextRun.CharacterBufferReference))
{ {
return false; return false;
} }
if (BidiLevel != shapeableTextCharacters.BidiLevel) if (BidiLevel != unshapedTextRun.BidiLevel)
{ {
return false; return false;
} }
if (!MathUtilities.AreClose(Properties.FontRenderingEmSize, if (!MathUtilities.AreClose(Properties.FontRenderingEmSize,
shapeableTextCharacters.Properties.FontRenderingEmSize)) unshapedTextRun.Properties.FontRenderingEmSize))
{ {
return false; return false;
} }
if (Properties.Typeface != shapeableTextCharacters.Properties.Typeface) if (Properties.Typeface != unshapedTextRun.Properties.Typeface)
{ {
return false; return false;
} }
if (Properties.BaselineAlignment != shapeableTextCharacters.Properties.BaselineAlignment) if (Properties.BaselineAlignment != unshapedTextRun.Properties.BaselineAlignment)
{ {
return false; return false;
} }

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

2
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@ -111,7 +111,7 @@ namespace Avalonia.Rendering.Composition
} }
} }
batch.CommitedAt = Server.Clock.Elapsed; batch.CommittedAt = Server.Clock.Elapsed;
_server.EnqueueBatch(batch); _server.EnqueueBatch(batch);
lock (_pendingBatchLock) lock (_pendingBatchLock)

2
src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionVariant.cs

@ -654,7 +654,7 @@ namespace Avalonia.Rendering.Composition.Expressions
} }
} }
res = default(T); res = default;
return false; return false;
} }

4
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs

@ -48,7 +48,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
} }
} }
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{ {
if (reader.Read<byte>() == 1) if (reader.Read<byte>() == 1)
{ {
@ -56,7 +56,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
_renderCommands = reader.ReadObject<CompositionDrawList?>(); _renderCommands = reader.ReadObject<CompositionDrawList?>();
_contentBounds = null; _contentBounds = null;
} }
base.DeserializeChangesCore(reader, commitedAt); base.DeserializeChangesCore(reader, committedAt);
} }
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)

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

6
src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs

@ -16,9 +16,9 @@ internal class ServerCompositionCustomVisual : ServerCompositionContainerVisual,
_handler.Attach(this); _handler.Attach(this);
} }
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{ {
base.DeserializeChangesCore(reader, commitedAt); base.DeserializeChangesCore(reader, committedAt);
var count = reader.Read<int>(); var count = reader.Read<int>();
for (var c = 0; c < count; c++) for (var c = 0; c < count; c++)
{ {
@ -79,4 +79,4 @@ internal class ServerCompositionCustomVisual : ServerCompositionContainerVisual,
?.Log(_handler, $"Exception in {_handler.GetType().Name}.{nameof(CompositionCustomVisualHandler.OnRender)} {{0}}", e); ?.Log(_handler, $"Exception in {_handler.GetType().Name}.{nameof(CompositionCustomVisualHandler.OnRender)} {{0}}", e);
} }
} }
} }

4
src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs

@ -14,7 +14,7 @@ namespace Avalonia.Rendering.Composition.Server
{ {
public List<T> List { get; } = new List<T>(); public List<T> List { get; } = new List<T>();
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{ {
if (reader.Read<byte>() == 1) if (reader.Read<byte>() == 1)
{ {
@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition.Server
for (var c = 0; c < count; c++) for (var c = 0; c < count; c++)
List.Add(reader.ReadObject<T>()); List.Add(reader.ReadObject<T>());
} }
base.DeserializeChangesCore(reader, commitedAt); base.DeserializeChangesCore(reader, committedAt);
} }
public override long LastChangedBy public override long LastChangedBy

8
src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs

@ -104,13 +104,13 @@ namespace Avalonia.Rendering.Composition.Server
} }
protected void SetAnimatedValue<T>(CompositionProperty prop, ref T field, protected void SetAnimatedValue<T>(CompositionProperty prop, ref T field,
TimeSpan commitedAt, IAnimationInstance animation) where T : struct TimeSpan committedAt, IAnimationInstance animation) where T : struct
{ {
if (IsActive && _animations.TryGetValue(prop, out var oldAnimation)) if (IsActive && _animations.TryGetValue(prop, out var oldAnimation))
oldAnimation.Deactivate(); oldAnimation.Deactivate();
_animations[prop] = animation; _animations[prop] = animation;
animation.Initialize(commitedAt, ExpressionVariant.Create(field), prop); animation.Initialize(committedAt, ExpressionVariant.Create(field), prop);
if(IsActive) if(IsActive)
animation.Activate(); animation.Activate();
@ -165,7 +165,7 @@ namespace Avalonia.Rendering.Composition.Server
public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null; public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null;
protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{ {
if (this is IDisposable disp if (this is IDisposable disp
&& reader.Read<byte>() == 1) && reader.Read<byte>() == 1)
@ -174,7 +174,7 @@ namespace Avalonia.Rendering.Composition.Server
public void DeserializeChanges(BatchStreamReader reader, Batch batch) public void DeserializeChanges(BatchStreamReader reader, Batch batch)
{ {
DeserializeChangesCore(reader, batch.CommitedAt); DeserializeChangesCore(reader, batch.CommittedAt);
ValuesInvalidated(); ValuesInvalidated();
ItselfLastChangedBy = batch.SequenceId; ItselfLastChangedBy = batch.SequenceId;
} }

2
src/Avalonia.Base/Rendering/Composition/Transport/Batch.cs

@ -29,7 +29,7 @@ namespace Avalonia.Rendering.Composition.Transport
public BatchStreamData Changes { get; private set; } public BatchStreamData Changes { get; private set; }
public TimeSpan CommitedAt { get; set; } public TimeSpan CommittedAt { get; set; }
public void Complete() public void Complete()
{ {

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

6
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@ -330,11 +330,11 @@ namespace Avalonia.Rendering
? visual is IVisualWithRoundRectClip roundClipVisual ? visual is IVisualWithRoundRectClip roundClipVisual
? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius)) ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius))
: context.PushClip(bounds) : context.PushClip(bounds)
: default(DrawingContext.PushedState)) : default)
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete
using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState)) using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default)
using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState)) using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default)
using (context.PushTransformContainer()) using (context.PushTransformContainer())
{ {
visual.Render(context); visual.Render(context);

3
src/Avalonia.Base/Rendering/RenderLoop.cs

@ -50,8 +50,7 @@ namespace Avalonia.Rendering
{ {
get get
{ {
return _timer ??= AvaloniaLocator.Current.GetService<IRenderTimer>() ?? return _timer ??= AvaloniaLocator.Current.GetRequiredService<IRenderTimer>();
throw new InvalidOperationException("Cannot locate IRenderTimer.");
} }
} }

2
src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs

@ -303,7 +303,7 @@ namespace Avalonia.Rendering.SceneGraph
if (ClipToBounds) if (ClipToBounds)
{ {
context.Transform = Matrix.Identity; context.Transform = Matrix.Identity;
if (ClipToBoundsRadius.IsEmpty) if (ClipToBoundsRadius.IsDefault)
context.PushClip(ClipBounds); context.PushClip(ClipBounds);
else else
context.PushClip(new RoundedRect(ClipBounds, ClipToBoundsRadius)); context.PushClip(new RoundedRect(ClipBounds, ClipToBoundsRadius));

2
src/Avalonia.Base/Rendering/Utilities/TileBrushCalculator.cs

@ -109,7 +109,7 @@ namespace Avalonia.Rendering.Utilities
{ {
if (IntermediateTransform != Matrix.Identity) if (IntermediateTransform != Matrix.Identity)
return true; return true;
if (SourceRect.Position != default(Point)) if (SourceRect.Position != default)
return true; return true;
if (SourceRect.Size.AspectRatio == _imageSize.AspectRatio) if (SourceRect.Size.AspectRatio == _imageSize.AspectRatio)
return false; return false;

8
src/Avalonia.Base/Threading/DispatcherTimer.cs

@ -176,13 +176,7 @@ namespace Avalonia.Threading
{ {
if (!IsEnabled) if (!IsEnabled)
{ {
var threading = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>(); var threading = AvaloniaLocator.Current.GetRequiredService<IPlatformThreadingInterface>();
if (threading == null)
{
throw new Exception("Could not start timer: IPlatformThreadingInterface is not registered.");
}
_timer = threading.StartTimer(_priority, Interval, InternalTick); _timer = threading.StartTimer(_priority, Interval, InternalTick);
} }
} }

2
src/Avalonia.Base/Utilities/SingleOrDictionary.cs

@ -42,7 +42,7 @@ namespace Avalonia.Utilities
{ {
if (!_singleValue.HasValue || !EqualityComparer<TKey>.Default.Equals(_singleValue.Value.Key, key)) if (!_singleValue.HasValue || !EqualityComparer<TKey>.Default.Equals(_singleValue.Value.Key, key))
{ {
value = default(TValue); value = default;
return false; return false;
} }
else else

4
src/Avalonia.Base/Utilities/StringTokenizer.cs

@ -63,7 +63,7 @@ namespace Avalonia.Utilities
} }
else else
{ {
result = default(Int32); result = default;
return false; return false;
} }
} }
@ -87,7 +87,7 @@ namespace Avalonia.Utilities
} }
else else
{ {
result = default(double); result = default;
return false; return false;
} }
} }

1
src/Avalonia.Base/Visual.cs

@ -484,6 +484,7 @@ namespace Avalonia
{ {
AttachToCompositor(compositingRenderer.Compositor); AttachToCompositor(compositingRenderer.Compositor);
} }
InvalidateMirrorTransform();
OnAttachedToVisualTree(e); OnAttachedToVisualTree(e);
AttachedToVisualTree?.Invoke(this, e); AttachedToVisualTree?.Invoke(this, e);
InvalidateVisual(); InvalidateVisual();

4
src/Avalonia.Build.Tasks/ComInteropHelper.cs

@ -65,10 +65,8 @@ namespace Avalonia.Build.Tasks
{ {
Instruction instruction = instructions[i]; Instruction instruction = instructions[i];
if (instruction.OpCode == OpCodes.Call && instruction.Operand is MethodReference) if (instruction.OpCode == OpCodes.Call && instruction.Operand is MethodReference methodDescription)
{ {
var methodDescription = (MethodReference)instruction.Operand;
if (methodDescription.Name.StartsWith("Calli") && methodDescription.DeclaringType.Name == "LocalInterop") if (methodDescription.Name.StartsWith("Calli") && methodDescription.DeclaringType.Name == "LocalInterop")
{ {
var callSite = new CallSite(methodDescription.ReturnType) { CallingConvention = MethodCallingConvention.StdCall }; var callSite = new CallSite(methodDescription.ReturnType) { CallingConvention = MethodCallingConvention.StdCall };

4
src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatColorPalette.cs

@ -289,8 +289,8 @@ namespace Avalonia.Controls
}; };
// See: https://htmlcolorcodes.com/assets/downloads/flat-design-colors/flat-design-color-chart.png // See: https://htmlcolorcodes.com/assets/downloads/flat-design-colors/flat-design-color-chart.png
protected static Color[,]? _colorChart = null; private static Color[,]? _colorChart = null;
protected static object _colorChartMutex = new object(); private static readonly object _colorChartMutex = new();
/// <summary> /// <summary>
/// Initializes all color chart colors. /// Initializes all color chart colors.

4
src/Avalonia.Controls.ColorPicker/ColorPalettes/FlatHalfColorPalette.cs

@ -10,8 +10,8 @@ namespace Avalonia.Controls
/// <inheritdoc cref="FlatColorPalette"/> /// <inheritdoc cref="FlatColorPalette"/>
public class FlatHalfColorPalette : IColorPalette public class FlatHalfColorPalette : IColorPalette
{ {
protected static Color[,]? _colorChart = null; private static Color[,]? _colorChart = null;
protected static object _colorChartMutex = new object(); private static readonly object _colorChartMutex = new();
/// <summary> /// <summary>
/// Initializes all color chart colors. /// Initializes all color chart colors.

4
src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs

@ -344,8 +344,8 @@ namespace Avalonia.Controls
// See: https://material.io/design/color/the-color-system.html#tools-for-picking-colors // See: https://material.io/design/color/the-color-system.html#tools-for-picking-colors
// This is a reduced palette for uniformity // This is a reduced palette for uniformity
protected static Color[,]? _colorChart = null; private static Color[,]? _colorChart = null;
protected static object _colorChartMutex = new object(); private static readonly object _colorChartMutex = new();
/// <summary> /// <summary>
/// Initializes all color chart colors. /// Initializes all color chart colors.

4
src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialHalfColorPalette.cs

@ -10,8 +10,8 @@ namespace Avalonia.Controls
/// <inheritdoc cref="MaterialColorPalette"/> /// <inheritdoc cref="MaterialColorPalette"/>
public class MaterialHalfColorPalette : IColorPalette public class MaterialHalfColorPalette : IColorPalette
{ {
protected static Color[,]? _colorChart = null; private static Color[,]? _colorChart = null;
protected static object _colorChartMutex = new object(); private static readonly object _colorChartMutex = new();
/// <summary> /// <summary>
/// Initializes all color chart colors. /// Initializes all color chart colors.

12
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -52,15 +52,10 @@ namespace Avalonia.Controls
private const string DATAGRID_elementTopLeftCornerHeaderName = "PART_TopLeftCornerHeader"; private const string DATAGRID_elementTopLeftCornerHeaderName = "PART_TopLeftCornerHeader";
private const string DATAGRID_elementTopRightCornerHeaderName = "PART_TopRightCornerHeader"; private const string DATAGRID_elementTopRightCornerHeaderName = "PART_TopRightCornerHeader";
private const string DATAGRID_elementBottomRightCornerHeaderName = "PART_BottomRightCorner"; private const string DATAGRID_elementBottomRightCornerHeaderName = "PART_BottomRightCorner";
private const string DATAGRID_elementValidationSummary = "PART_ValidationSummary";
private const string DATAGRID_elementVerticalScrollbarName = "PART_VerticalScrollbar"; private const string DATAGRID_elementVerticalScrollbarName = "PART_VerticalScrollbar";
private const bool DATAGRID_defaultAutoGenerateColumns = true;
internal const bool DATAGRID_defaultCanUserReorderColumns = true; internal const bool DATAGRID_defaultCanUserReorderColumns = true;
internal const bool DATAGRID_defaultCanUserResizeColumns = true; internal const bool DATAGRID_defaultCanUserResizeColumns = true;
internal const bool DATAGRID_defaultCanUserSortColumns = true; internal const bool DATAGRID_defaultCanUserSortColumns = true;
private const DataGridRowDetailsVisibilityMode DATAGRID_defaultRowDetailsVisibility = DataGridRowDetailsVisibilityMode.VisibleWhenSelected;
private const DataGridSelectionMode DATAGRID_defaultSelectionMode = DataGridSelectionMode.Extended;
/// <summary> /// <summary>
/// The default order to use for columns when there is no <see cref="DisplayAttribute.Order"/> /// The default order to use for columns when there is no <see cref="DisplayAttribute.Order"/>
@ -1124,7 +1119,7 @@ namespace Avalonia.Controls
EnsureColumnHeadersVisibility(); EnsureColumnHeadersVisibility();
if (!newValueCols) if (!newValueCols)
{ {
_columnHeadersPresenter.Measure(default(Size)); _columnHeadersPresenter.Measure(default);
} }
else else
{ {
@ -1165,7 +1160,7 @@ namespace Avalonia.Controls
_topLeftCornerHeader.IsVisible = newValueRows && newValueCols; _topLeftCornerHeader.IsVisible = newValueRows && newValueCols;
if (_topLeftCornerHeader.IsVisible) if (_topLeftCornerHeader.IsVisible)
{ {
_topLeftCornerHeader.Measure(default(Size)); _topLeftCornerHeader.Measure(default);
} }
} }
@ -3300,7 +3295,7 @@ namespace Avalonia.Controls
newCell.IsVisible = column.IsVisible; newCell.IsVisible = column.IsVisible;
if (row.OwningGrid.CellTheme is {} cellTheme) if (row.OwningGrid.CellTheme is {} cellTheme)
{ {
newCell.SetValue(ThemeProperty, cellTheme, BindingPriority.TemplatedParent); newCell.SetValue(ThemeProperty, cellTheme, BindingPriority.Template);
} }
} }
row.Cells.Insert(column.Index, newCell); row.Cells.Insert(column.Index, newCell);
@ -4158,6 +4153,7 @@ namespace Avalonia.Controls
if (exitEditingMode) if (exitEditingMode)
{ {
CurrentColumn.EndCellEditInternal();
_editingColumnIndex = -1; _editingColumnIndex = -1;
editingCell.UpdatePseudoClasses(); editingCell.UpdatePseudoClasses();

2
src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs

@ -31,7 +31,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="IsThreeState"/> property. /// Defines the <see cref="IsThreeState"/> property.
/// </summary> /// </summary>
public static StyledProperty<bool> IsThreeStateProperty = public static readonly StyledProperty<bool> IsThreeStateProperty =
CheckBox.IsThreeStateProperty.AddOwner<DataGridCheckBoxColumn>(); CheckBox.IsThreeStateProperty.AddOwner<DataGridCheckBoxColumn>();
/// <summary> /// <summary>

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

@ -185,7 +185,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="IsVisible"/> property. /// Defines the <see cref="IsVisible"/> property.
/// </summary> /// </summary>
public static StyledProperty<bool> IsVisibleProperty = public static readonly StyledProperty<bool> IsVisibleProperty =
Control.IsVisibleProperty.AddOwner<DataGridColumn>(); Control.IsVisibleProperty.AddOwner<DataGridColumn>();
/// <summary> /// <summary>
@ -800,11 +800,22 @@ namespace Avalonia.Controls
protected internal virtual void RefreshCellContent(Control element, string propertyName) protected internal virtual void RefreshCellContent(Control element, string propertyName)
{ } { }
/// <summary>
/// When overridden in a derived class, called when a cell in the column exits editing mode.
/// </summary>
protected virtual void EndCellEdit()
{ }
internal void CancelCellEditInternal(Control editingElement, object uneditedValue) internal void CancelCellEditInternal(Control editingElement, object uneditedValue)
{ {
CancelCellEdit(editingElement, uneditedValue); CancelCellEdit(editingElement, uneditedValue);
} }
internal void EndCellEditInternal()
{
EndCellEdit();
}
/// <summary> /// <summary>
/// Coerces a DataGridLength to a valid value. If any value components are double.NaN, this method /// Coerces a DataGridLength to a valid value. If any value components are double.NaN, this method
/// coerces them to a proper initial value. For star columns, the desired width is calculated based /// coerces them to a proper initial value. For star columns, the desired width is calculated based
@ -897,7 +908,7 @@ namespace Avalonia.Controls
result[!ContentControl.ContentTemplateProperty] = this[!HeaderTemplateProperty]; result[!ContentControl.ContentTemplateProperty] = this[!HeaderTemplateProperty];
if (OwningGrid.ColumnHeaderTheme is {} columnTheme) if (OwningGrid.ColumnHeaderTheme is {} columnTheme)
{ {
result.SetValue(StyledElement.ThemeProperty, columnTheme, BindingPriority.TemplatedParent); result.SetValue(StyledElement.ThemeProperty, columnTheme, BindingPriority.Template);
} }
return result; return result;

2
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -673,7 +673,7 @@ namespace Avalonia.Controls
}; };
if (OwningGrid.ColumnHeaderTheme is {} columnHeaderTheme) if (OwningGrid.ColumnHeaderTheme is {} columnHeaderTheme)
{ {
dragIndicator.SetValue(ThemeProperty, columnHeaderTheme, BindingPriority.TemplatedParent); dragIndicator.SetValue(ThemeProperty, columnHeaderTheme, BindingPriority.Template);
} }
dragIndicator.PseudoClasses.Add(":dragIndicator"); dragIndicator.PseudoClasses.Add(":dragIndicator");

2
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@ -254,7 +254,7 @@ namespace Avalonia.Controls
}; };
if (OwningGrid.CellTheme is {} cellTheme) if (OwningGrid.CellTheme is {} cellTheme)
{ {
_fillerCell.SetValue(ThemeProperty, cellTheme, BindingPriority.TemplatedParent); _fillerCell.SetValue(ThemeProperty, cellTheme, BindingPriority.Template);
} }
if (_cellsElement != null) if (_cellsElement != null)
{ {

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

@ -1029,7 +1029,7 @@ namespace Avalonia.Controls
dataGridRow.DataContext = dataContext; dataGridRow.DataContext = dataContext;
if (RowTheme is {} rowTheme) if (RowTheme is {} rowTheme)
{ {
dataGridRow.SetValue(ThemeProperty, rowTheme, BindingPriority.TemplatedParent); dataGridRow.SetValue(ThemeProperty, rowTheme, BindingPriority.Template);
} }
CompleteCellsCollection(dataGridRow); CompleteCellsCollection(dataGridRow);
@ -2743,7 +2743,7 @@ namespace Avalonia.Controls
groupHeader.Level = rowGroupInfo.Level; groupHeader.Level = rowGroupInfo.Level;
if (RowGroupTheme is {} rowGroupTheme) if (RowGroupTheme is {} rowGroupTheme)
{ {
groupHeader.SetValue(ThemeProperty, rowGroupTheme, BindingPriority.TemplatedParent); groupHeader.SetValue(ThemeProperty, rowGroupTheme, BindingPriority.Template);
} }
// Set the RowGroupHeader's PropertyName. Unfortunately, CollectionViewGroup doesn't have this // Set the RowGroupHeader's PropertyName. Unfortunately, CollectionViewGroup doesn't have this

16
src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs

@ -55,17 +55,25 @@ namespace Avalonia.Controls
get => _cellEditingCellTemplate; get => _cellEditingCellTemplate;
set => SetAndRaise(CellEditingTemplateProperty, ref _cellEditingCellTemplate, value); set => SetAndRaise(CellEditingTemplateProperty, ref _cellEditingCellTemplate, value);
} }
private static void OnCellTemplateChanged(AvaloniaPropertyChangedEventArgs e) private bool _forceGenerateCellFromTemplate;
protected override void EndCellEdit()
{ {
var oldValue = (IDataTemplate)e.OldValue; //the next call to generate element should not resuse the current content as we need to exit edit mode
var value = (IDataTemplate)e.NewValue; _forceGenerateCellFromTemplate = true;
base.EndCellEdit();
} }
protected override Control GenerateElement(DataGridCell cell, object dataItem) protected override Control GenerateElement(DataGridCell cell, object dataItem)
{ {
if (CellTemplate != null) if (CellTemplate != null)
{ {
if (_forceGenerateCellFromTemplate)
{
_forceGenerateCellFromTemplate = false;
return CellTemplate.Build(dataItem);
}
return (CellTemplate is IRecyclingDataTemplate recyclingDataTemplate) return (CellTemplate is IRecyclingDataTemplate recyclingDataTemplate)
? recyclingDataTemplate.Build(dataItem, cell.Content as Control) ? recyclingDataTemplate.Build(dataItem, cell.Content as Control)
: CellTemplate.Build(dataItem); : CellTemplate.Build(dataItem);

2
src/Avalonia.Controls.DataGrid/IndexToValueTable.cs

@ -422,7 +422,7 @@ namespace Avalonia.Controls
else else
{ {
found = false; found = false;
return default(T); return default;
} }
} }

3
src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Controls.ApplicationLifetimes namespace Avalonia.Controls.ApplicationLifetimes
@ -19,7 +18,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// <summary> /// <summary>
/// Gets the arguments passed to the /// Gets the arguments passed to the
/// <see cref="ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime{T}(T, string[], ShutdownMode)"/> /// <see cref="ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime(AppBuilder, string[], ShutdownMode)"/>
/// method. /// method.
/// </summary> /// </summary>
string[]? Args { get; } string[]? Args { get; }

1
src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs

@ -474,6 +474,7 @@ namespace Avalonia.Controls
FilterModeProperty.Changed.AddClassHandler<AutoCompleteBox>((x,e) => x.OnFilterModePropertyChanged(e)); FilterModeProperty.Changed.AddClassHandler<AutoCompleteBox>((x,e) => x.OnFilterModePropertyChanged(e));
ItemFilterProperty.Changed.AddClassHandler<AutoCompleteBox>((x,e) => x.OnItemFilterPropertyChanged(e)); ItemFilterProperty.Changed.AddClassHandler<AutoCompleteBox>((x,e) => x.OnItemFilterPropertyChanged(e));
ItemsProperty.Changed.AddClassHandler<AutoCompleteBox>((x,e) => x.OnItemsPropertyChanged(e)); ItemsProperty.Changed.AddClassHandler<AutoCompleteBox>((x,e) => x.OnItemsPropertyChanged(e));
ItemTemplateProperty.Changed.AddClassHandler<AutoCompleteBox>((x,e) => x.OnItemTemplatePropertyChanged(e));
IsEnabledProperty.Changed.AddClassHandler<AutoCompleteBox>((x,e) => x.OnControlIsEnabledChanged(e)); IsEnabledProperty.Changed.AddClassHandler<AutoCompleteBox>((x,e) => x.OnControlIsEnabledChanged(e));
} }

8
src/Avalonia.Controls/BorderVisual.cs

@ -50,7 +50,7 @@ class CompositionBorderVisual : CompositionDrawListVisual
if (ClipToBounds) if (ClipToBounds)
{ {
var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y))); var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y)));
if (_cornerRadius.IsEmpty) if (_cornerRadius.IsDefault)
canvas.PushClip(clipRect); canvas.PushClip(clipRect);
else else
canvas.PushClip(new RoundedRect(clipRect, _cornerRadius)); canvas.PushClip(new RoundedRect(clipRect, _cornerRadius));
@ -63,9 +63,9 @@ class CompositionBorderVisual : CompositionDrawListVisual
} }
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt) protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{ {
base.DeserializeChangesCore(reader, commitedAt); base.DeserializeChangesCore(reader, committedAt);
if (reader.Read<bool>()) if (reader.Read<bool>())
_cornerRadius = reader.Read<CornerRadius>(); _cornerRadius = reader.Read<CornerRadius>();
} }
@ -73,4 +73,4 @@ class CompositionBorderVisual : CompositionDrawListVisual
protected override bool HandlesClipToBounds => true; protected override bool HandlesClipToBounds => true;
} }
} }

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

21
src/Avalonia.Controls/Control.cs

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

2
src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs

@ -8,6 +8,6 @@
/// <summary> /// <summary>
/// Provides access to the internal <see cref="ToolTip.ToolTipProperty"/> for use in DevTools. /// Provides access to the internal <see cref="ToolTip.ToolTipProperty"/> for use in DevTools.
/// </summary> /// </summary>
public static AvaloniaProperty<ToolTip?> ToolTipProperty = ToolTip.ToolTipProperty; public static readonly AvaloniaProperty<ToolTip?> ToolTipProperty = ToolTip.ToolTipProperty;
} }
} }

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

Loading…
Cancel
Save