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
#CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
dotnet_diagnostic.CA1847.severity = warning
#CACA2211:Non-constant fields should not be visible
dotnet_diagnostic.CA2211.severity = error
# Wrapping preferences
csharp_wrap_before_ternary_opsigns = false

1
Directory.Build.props

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

31
NOTICE.md

@ -303,3 +303,34 @@ https://github.com/chromium/chromium
// 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.
# 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:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401'
displayName: 'Use .NET Core SDK 6.0.404'
inputs:
version: 6.0.401
version: 6.0.404
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100'
displayName: 'Use .NET Core SDK 7.0.101'
inputs:
version: 7.0.100
version: 7.0.101
- script: system_profiler SPDisplaysDataType |grep Resolution
@ -51,14 +51,14 @@ jobs:
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401'
displayName: 'Use .NET Core SDK 6.0.404'
inputs:
version: 6.0.401
version: 6.0.404
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 7.0.100'
displayName: 'Use .NET Core SDK 7.0.101'
inputs:
version: 7.0.100
version: 7.0.101
- task: Windows Application Driver@0
inputs:

24
azure-pipelines.yml

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

2
build/Base.props

@ -1,5 +1,5 @@
<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.Threading.Tasks.Extensions" Version="4.5.4" />
<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>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<LangVersion>preview</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<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>

2
build/System.Memory.props

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

6
global.json

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

2
samples/ControlCatalog.Android/MainActivity.cs

@ -5,7 +5,7 @@ using Avalonia.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
{
}

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

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

2
samples/ControlCatalog.Android/SplashActivity.cs

@ -28,6 +28,8 @@ namespace ControlCatalog.Android
base.OnResume();
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)
{
AvaloniaLocator.CurrentMutable
.GetService<IAssetLoader>()
.GetRequiredService<IAssetLoader>()
.SetDefaultAssembly(typeof(App).Assembly);
}
}

3
samples/ControlCatalog/MainView.xaml

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

23
samples/ControlCatalog/MainView.xaml.cs

@ -6,6 +6,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.VisualTree;
using ControlCatalog.Models;
using ControlCatalog.Pages;
@ -59,17 +60,25 @@ namespace ControlCatalog
};
var transparencyLevels = this.Get<ComboBox>("TransparencyLevels");
IDisposable? backgroundSetter = null, paneBackgroundSetter = null;
IDisposable? topLevelBackgroundSideSetter = null, sideBarBackgroundSetter = null, paneBackgroundSetter = null;
transparencyLevels.SelectionChanged += (sender, e) =>
{
backgroundSetter?.Dispose();
topLevelBackgroundSideSetter?.Dispose();
sideBarBackgroundSetter?.Dispose();
paneBackgroundSetter?.Dispose();
if (transparencyLevels.SelectedItem is WindowTransparencyLevel selected
&& selected != WindowTransparencyLevel.None)
if (transparencyLevels.SelectedItem is WindowTransparencyLevel selected)
{
var semiTransparentBrush = new ImmutableSolidColorBrush(Colors.Gray, 0.5);
backgroundSetter = sideBar.SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style);
paneBackgroundSetter = sideBar.SetValue(SplitView.PaneBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style);
var topLevel = (TopLevel)this.GetVisualRoot()!;
topLevel.TransparencyLevelHint = selected;
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}"
ExtendClientAreaChromeHints="{Binding ChromeHints}"
ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}"
TransparencyLevelHint="{Binding TransparencyLevel}"
x:Name="MainWindow"
Background="Transparent"
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 " +
"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. " +
"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
{
public IBrush Brush;
public IBrush? Brush;
public Point Point;
public double Radius;
public double? Pressure;

58
samples/ControlCatalog/Pages/ScreenPage.cs

@ -36,44 +36,44 @@ namespace ControlCatalog.Pages
var drawBrush = Brushes.Black;
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);
return;
}
_leftMost = screen.Bounds.X / 10f;
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,
screen.Bounds.Height / 10f);
Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f,
screen.WorkingArea.Height / 10f);
context.DrawRectangle(p, boundsRect);
context.DrawRectangle(p, workingAreaRect);
Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f, screen.Bounds.Width / 10f,
screen.Bounds.Height / 10f);
Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f,
screen.WorkingArea.Height / 10f);
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 =
CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20));
var formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height));
formattedText = CreateFormattedText($"Scaling: {screen.Scaling * 100}%");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40));
formattedText =
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 =
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.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60));
formattedText =
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));
}

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

@ -51,7 +51,7 @@ namespace ControlCatalog.Pages
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)));
}
}

2
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -38,7 +38,7 @@
UseFloatingWatermark="True"
PasswordChar="*"
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="Right aligned text" TextAlignment="Right" />
<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="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" />
<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>
</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 });
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))
.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 bitmap = new Bitmap(s);
CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16));

9
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

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

2
samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs

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

5
samples/Directory.Build.props

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

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

@ -1,6 +1,5 @@
using Android.OS;
using AndroidX.AppCompat.App;
using Avalonia.Controls;
namespace Avalonia.Android
{
@ -8,15 +7,22 @@ namespace Avalonia.Android
{
protected abstract AppBuilder CreateAppBuilder();
private static AppBuilder s_appBuilder;
protected override void OnCreate(Bundle? 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.Prepare();
this.SetBackgroundColor(global::Android.Graphics.Color.Transparent);
}
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)
{
Holder.AddCallback(this);
Holder.SetFormat(global::Android.Graphics.Format.Transparent);
_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 Math = System.Math;
using AndroidRect = Android.Graphics.Rect;
using Android.Graphics.Drawables;
namespace Avalonia.Android.Platform.SkiaPlatform
{
@ -283,7 +284,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public Action LostFocus { 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);
@ -301,7 +302,87 @@ namespace Avalonia.Android.Platform.SkiaPlatform
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;
}
var eventTime = (ulong)DateTime.Now.Millisecond;
var eventTime = (ulong)e.EventTime;
var inputRoot = _view.InputRoot;
var actionMasked = e.ActionMasked;
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);
if (result != null && !(result is T))
if (result == null)
{
throw new InvalidOperationException(
$"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'.");
return null;
}
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>
@ -74,13 +79,13 @@ namespace Avalonia.Controls
throw new KeyNotFoundException($"Could not find control '{name}'.");
}
if (!(result is T))
if (result is T typed)
{
throw new InvalidOperationException(
$"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'.");
return typed;
}
return (T)result;
throw new InvalidOperationException(
$"Expected control '{name}' to be '{typeof(T)} but it was '{result.GetType()}'.");
}
/// <summary>

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

@ -38,7 +38,7 @@ namespace Avalonia.Data.Converters
}
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()
{
return AvaloniaLocator.Current.GetService<ICursorFactory>() ??
throw new Exception("Could not create Cursor: ICursorFactory not registered.");
return AvaloniaLocator.Current.GetRequiredService<ICursorFactory>();
}
}
}

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

@ -17,7 +17,9 @@ namespace Avalonia.Input.GestureRecognizers
private bool _canVerticallyScroll;
private int _gestureId;
private int _scrollStartDistance = 30;
private Point _pointerPressedPoint;
private VelocityTracker? _velocityTracker;
// Movement per second
private Vector _inertia;
private ulong? _lastMoveTimestamp;
@ -91,7 +93,7 @@ namespace Avalonia.Input.GestureRecognizers
EndGesture();
_tracking = e.Pointer;
_gestureId = ScrollGestureEventArgs.GetNextFreeId();
_trackedRootPoint = e.GetPosition((Visual?)_target);
_trackedRootPoint = _pointerPressedPoint = e.GetPosition((Visual?)_target);
}
}
@ -111,6 +113,13 @@ namespace Avalonia.Input.GestureRecognizers
_scrolling = true;
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);
}
}
@ -118,14 +127,11 @@ namespace Avalonia.Input.GestureRecognizers
if (_scrolling)
{
var vector = _trackedRootPoint - rootPoint;
var elapsed = _lastMoveTimestamp.HasValue && _lastMoveTimestamp < e.Timestamp ?
TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) :
TimeSpan.Zero;
_velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), _pointerPressedPoint - rootPoint);
_lastMoveTimestamp = e.Timestamp;
_trackedRootPoint = rootPoint;
if (elapsed.TotalSeconds > 0)
_inertia = vector / elapsed.TotalSeconds;
_target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
e.Handled = true;
}
@ -150,12 +156,14 @@ namespace Avalonia.Input.GestureRecognizers
}
}
public void PointerReleased(PointerReleasedEventArgs e)
{
if (e.Pointer == _tracking && _scrolling)
{
_inertia = _velocityTracker?.GetFlingVelocity().PixelsPerSecond ?? Vector.Zero;
e.Handled = true;
if (_inertia == default
|| e.Timestamp == 0
@ -183,9 +191,18 @@ namespace Avalonia.Input.GestureRecognizers
var distance = speed * elapsedSinceLastTick.TotalSeconds;
_target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance));
if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
// EndGesture using InertialScrollSpeedEnd only in the direction of scrolling
if (CanVerticallyScroll && CanHorizontallyScroll && 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();
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.Threading;
using Avalonia.Interactivity;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.Reactive;
using Avalonia.VisualTree;
@ -9,6 +11,21 @@ namespace Avalonia.Input
public static class Gestures
{
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>(
"Tapped",
RoutingStrategies.Bubble,
@ -46,6 +63,7 @@ namespace Avalonia.Input
private static readonly WeakReference<object?> s_lastPress = new WeakReference<object?>(null);
private static Point s_lastPressPoint;
private static IPointer? s_lastPointer;
public static readonly RoutedEvent<PinchEventArgs> PinchEvent =
RoutedEvent.Register<PinchEventArgs>(
@ -59,14 +77,40 @@ namespace Avalonia.Input
RoutedEvent.Register<PullGestureEventArgs>(
"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 =
RoutedEvent.Register<PullGestureEndedEventArgs>(
"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()
{
InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed);
InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased);
InputElement.PointerMovedEvent.RouteFinished.Subscribe(PointerMoved);
}
public static void AddTappedHandler(Interactive element, EventHandler<RoutedEventArgs> handler)
@ -111,11 +155,42 @@ namespace Avalonia.Input
var e = (PointerPressedEventArgs)ev;
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)
{
s_isDoubleTapped = false;
s_lastPress.SetTarget(ev.Source);
s_lastPointer = e.Pointer;
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)
{
@ -149,7 +224,12 @@ namespace Avalonia.Input
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));
}
@ -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>
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>
/// Defines the <see cref="DoubleTapped"/> event.
/// </summary>
public static readonly RoutedEvent<TappedEventArgs> DoubleTappedEvent = Gestures.DoubleTappedEvent;
private bool _isEffectivelyEnabled = true;
private bool _isFocused;
private bool _isKeyboardFocusWithin;
@ -353,6 +358,15 @@ namespace Avalonia.Input
add { AddHandler(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>
/// 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 static readonly UvMeasure Zero = default(UvMeasure);
internal static readonly UvMeasure Zero = default;
internal double U { get; set; }

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

@ -279,7 +279,7 @@ namespace Avalonia.Media
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)
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.PushOpacity(Opacity))
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default(DrawingContext.PushedState))
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default(DrawingContext.PushedState))
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default)
{
foreach (var drawing in Children)
{

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

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

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

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

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

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

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

@ -91,7 +91,7 @@ namespace Avalonia.Media.TextFormatting
continue;
}
if (textRun is ShapedTextCharacters shapedText)
if (textRun is ShapedTextRun shapedText)
{
var glyphRun = shapedText.GlyphRun;
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>
/// A text run that holds shaped characters.
/// </summary>
public sealed class ShapedTextCharacters : DrawableTextRun
public sealed class ShapedTextRun : DrawableTextRun
{
private GlyphRun? _glyphRun;
public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties)
public ShapedTextRun(ShapedBuffer shapedBuffer, TextRunProperties properties)
{
ShapedBuffer = shapedBuffer;
CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference;
@ -155,7 +155,7 @@ namespace Avalonia.Media.TextFormatting
return length > 0;
}
internal SplitResult<ShapedTextCharacters> Split(int length)
internal SplitResult<ShapedTextRun> Split(int length)
{
if (IsReversed)
{
@ -171,7 +171,7 @@ namespace Avalonia.Media.TextFormatting
var splitBuffer = ShapedBuffer.Split(length);
var first = new ShapedTextCharacters(splitBuffer.First, Properties);
var first = new ShapedTextRun(splitBuffer.First, Properties);
#if DEBUG
@ -182,9 +182,9 @@ namespace Avalonia.Media.TextFormatting
#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()

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

@ -91,12 +91,12 @@ namespace Avalonia.Media.TextFormatting
public override TextRunProperties Properties { get; }
/// <summary>
/// Gets a list of <see cref="ShapeableTextCharacters"/>.
/// Gets a list of <see cref="UnshapedTextRun"/>.
/// </summary>
/// <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)
{
@ -120,7 +120,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="biDiLevel">The bidi level of the run.</param>
/// <param name="previousProperties"></param>
/// <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)
{
var defaultTypeface = defaultProperties.Typeface;
@ -133,12 +133,12 @@ namespace Avalonia.Media.TextFormatting
{
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);
}
}
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
@ -146,7 +146,7 @@ namespace Avalonia.Media.TextFormatting
{
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);
}
}
@ -176,7 +176,7 @@ namespace Avalonia.Media.TextFormatting
if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _))
{
//Fallback found
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
return new UnshapedTextRun(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
@ -196,10 +196,10 @@ namespace Avalonia.Media.TextFormatting
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>
@ -264,7 +264,7 @@ namespace Avalonia.Media.TextFormatting
}
}
length += currentGrapheme.Text.Length;
length += currentGrapheme.Length;
}
return length > 0;

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

@ -31,7 +31,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextCharacters shapedRun:
case ShapedTextRun shapedRun:
{
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);
if (currentRun is ShapedTextCharacters shapedTextCharacters)
if (currentRun is ShapedTextRun shapedTextCharacters)
{
var split = shapedTextCharacters.Split(length - currentLength);
@ -206,16 +206,16 @@ namespace Avalonia.Media.TextFormatting
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 length = currentRun.Length;
var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar;
while (index + 1 < processedRuns.Count)
{
if (processedRuns[index + 1] is not ShapeableTextCharacters nextRun)
if (processedRuns[index + 1] is not UnshapedTextRun nextRun)
{
break;
}
@ -258,10 +258,10 @@ namespace Avalonia.Media.TextFormatting
return drawableTextRuns;
}
private static IReadOnlyList<ShapedTextCharacters> ShapeTogether(
IReadOnlyList<ShapeableTextCharacters> textRuns, CharacterBufferReference text, int length, TextShaperOptions options)
private static IReadOnlyList<ShapedTextRun> ShapeTogether(
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);
@ -271,7 +271,7 @@ namespace Avalonia.Media.TextFormatting
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!;
}
@ -280,9 +280,9 @@ namespace Avalonia.Media.TextFormatting
}
/// <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>
/// <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>
/// <returns></returns>
private static IEnumerable<IReadOnlyList<TextRun>> CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ArraySlice<sbyte> levels)
@ -474,7 +474,7 @@ namespace Avalonia.Media.TextFormatting
{
switch (currentRun)
{
case ShapedTextCharacters shapedTextCharacters:
case ShapedTextRun shapedTextCharacters:
{
if(shapedTextCharacters.ShapedBuffer.Length > 0)
{
@ -538,7 +538,7 @@ namespace Avalonia.Media.TextFormatting
var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(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();
}
@ -744,7 +744,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>
/// The shaped symbol.
/// </returns>
internal static ShapedTextCharacters CreateSymbol(TextRun textRun, FlowDirection flowDirection)
internal static ShapedTextRun CreateSymbol(TextRun textRun, FlowDirection flowDirection)
{
var textShaper = TextShaper.Current;
@ -760,7 +760,7 @@ namespace Avalonia.Media.TextFormatting
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)
{
case ShapedTextCharacters shapedRun:
case ShapedTextRun shapedRun:
{
currentWidth += currentRun.Size.Width;
@ -118,7 +118,7 @@ namespace Avalonia.Media.TextFormatting
switch (run)
{
case ShapedTextCharacters endShapedRun:
case ShapedTextRun endShapedRun:
{
if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
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];
if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
if (currentRun is ShapedTextRun shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
{
var rightToLeftIndex = i;
currentPosition += currentRun.Length;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
{
var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextCharacters;
var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun;
if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight)
{
@ -255,7 +255,7 @@ namespace Avalonia.Media.TextFormatting
switch (run)
{
case ShapedTextCharacters shapedRun:
case ShapedTextRun shapedRun:
{
characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
@ -303,7 +303,7 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = _textRuns[index];
if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
if (currentRun is ShapedTextRun shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
{
var i = index;
@ -313,7 +313,7 @@ namespace Avalonia.Media.TextFormatting
{
var nextRun = _textRuns[i + 1];
if (nextRun is ShapedTextCharacters nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight)
if (nextRun is ShapedTextRun nextShapedRun && !nextShapedRun.ShapedBuffer.IsLeftToRight)
{
i++;
@ -407,7 +407,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextCharacters shapedTextCharacters:
case ShapedTextRun shapedTextCharacters:
{
currentGlyphRun = shapedTextCharacters.GlyphRun;
@ -476,7 +476,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextCharacters shapedRun:
case ShapedTextRun shapedRun:
{
nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
break;
@ -550,7 +550,7 @@ namespace Avalonia.Media.TextFormatting
double combinedWidth;
if (currentRun is ShapedTextCharacters currentShapedRun)
if (currentRun is ShapedTextRun currentShapedRun)
{
var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster;
@ -592,7 +592,7 @@ namespace Avalonia.Media.TextFormatting
var rightToLeftIndex = index;
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)
{
@ -624,12 +624,12 @@ namespace Avalonia.Media.TextFormatting
for (int i = rightToLeftIndex - 1; i >= index; i--)
{
if (TextRuns[i] is not ShapedTextCharacters)
if (TextRuns[i] is not ShapedTextRun)
{
continue;
}
currentShapedRun = (ShapedTextCharacters)TextRuns[i];
currentShapedRun = (ShapedTextRun)TextRuns[i];
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
@ -769,7 +769,7 @@ namespace Avalonia.Media.TextFormatting
var characterLength = 0;
var endX = startX;
if (currentRun is ShapedTextCharacters currentShapedRun)
if (currentRun is ShapedTextRun currentShapedRun)
{
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
@ -883,7 +883,7 @@ namespace Avalonia.Media.TextFormatting
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;
@ -945,7 +945,7 @@ namespace Avalonia.Media.TextFormatting
private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection)
{
if (run is ShapedTextCharacters shapedTextCharacters)
if (run is ShapedTextRun shapedTextCharacters)
{
return shapedTextCharacters.BidiLevel;
}
@ -1027,7 +1027,7 @@ namespace Avalonia.Media.TextFormatting
{
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();
}
@ -1145,7 +1145,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextCharacters shapedRun:
case ShapedTextRun shapedRun:
{
var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
@ -1230,7 +1230,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextCharacters shapedRun:
case ShapedTextRun shapedRun:
{
var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
@ -1294,7 +1294,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextCharacters shapedRun:
case ShapedTextRun shapedRun:
{
var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
@ -1303,7 +1303,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
if (previousRun is ShapedTextRun previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
{
if (shapedRun.ShapedBuffer.IsLeftToRight)
{
@ -1394,7 +1394,7 @@ namespace Avalonia.Media.TextFormatting
{
switch (_textRuns[index])
{
case ShapedTextCharacters textRun:
case ShapedTextRun textRun:
{
var textMetrics =
new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize);

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

@ -63,14 +63,11 @@
{
get { return 0; }
}
/// <summary>
/// Gets the default incremental tab width.
/// </summary>
public virtual double DefaultIncrementalTab
{
get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; }
}
public virtual double DefaultIncrementalTab => 0;
/// <summary>
/// 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)
{
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;
}
var textShaperImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
if (textShaperImpl == null)
throw new InvalidOperationException("No text shaper implementation was registered.");
var textShaperImpl = AvaloniaLocator.Current.GetRequiredService<ITextShaperImpl>();
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;
namespace Avalonia.Media.TextFormatting.Unicode
@ -222,6 +223,71 @@ namespace Avalonia.Media.TextFormatting.Unicode
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>
/// 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>
public readonly ref struct Grapheme
{
public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
public Grapheme(Codepoint firstCodepoint, int offset, int length)
{
FirstCodepoint = firstCodepoint;
Text = text;
Offset = offset;
Length = length;
}
/// <summary>
@ -19,8 +20,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
public Codepoint FirstCodepoint { get; }
/// <summary>
/// The text that is representing the <see cref="Grapheme"/>.
/// The Offset to the FirstCodepoint
/// </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:
var text = _text.Take(processor.CurrentCodeUnitOffset);
Current = new Grapheme(firstCodepoint, text.Span);
Current = new Grapheme(firstCodepoint, _text.OffsetToFirstChar, 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>
/// A group of characters that can be shaped.
/// </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)
{
CharacterBufferReference = characterBufferReference;
@ -24,30 +24,30 @@ namespace Avalonia.Media.TextFormatting
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;
}
if (BidiLevel != shapeableTextCharacters.BidiLevel)
if (BidiLevel != unshapedTextRun.BidiLevel)
{
return false;
}
if (!MathUtilities.AreClose(Properties.FontRenderingEmSize,
shapeableTextCharacters.Properties.FontRenderingEmSize))
unshapedTextRun.Properties.FontRenderingEmSize))
{
return false;
}
if (Properties.Typeface != shapeableTextCharacters.Properties.Typeface)
if (Properties.Typeface != unshapedTextRun.Properties.Typeface)
{
return false;
}
if (Properties.BaselineAlignment != shapeableTextCharacters.Properties.BaselineAlignment)
if (Properties.BaselineAlignment != unshapedTextRun.Properties.BaselineAlignment)
{
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 HoldWaitDuration { get; set; } = TimeSpan.FromMilliseconds(300);
}
}

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

@ -27,5 +27,7 @@ namespace Avalonia.Platform
/// tap gesture.
/// </summary>
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);
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;
}

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)
{
@ -56,7 +56,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
_renderCommands = reader.ReadObject<CompositionDrawList?>();
_contentBounds = null;
}
base.DeserializeChangesCore(reader, commitedAt);
base.DeserializeChangesCore(reader, committedAt);
}
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++;
if (Opacity != 1)
canvas.PushOpacity(Opacity);
if (AdornedVisual != null)
{
canvas.PostTransform = Matrix.Identity;
canvas.Transform = Matrix.Identity;
canvas.PushClip(AdornedVisual._combinedTransformedClipBounds);
}
var transform = GlobalTransformMatrix;
canvas.PostTransform = MatrixUtils.ToMatrix(transform);
canvas.Transform = Matrix.Identity;
if (Opacity != 1)
canvas.PushOpacity(Opacity);
var boundsRect = new Rect(new Size(Size.X, Size.Y));
if (ClipToBounds && !HandlesClipToBounds)
canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
@ -67,6 +74,8 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopGeometryClip();
if (ClipToBounds && !HandlesClipToBounds)
canvas.PopClip();
if (AdornedVisual != null)
canvas.PopClip();
if(Opacity != 1)
canvas.PopOpacity();
}
@ -155,15 +164,25 @@ namespace Avalonia.Rendering.Composition.Server
_clipSizeDirty = false;
}
_combinedTransformedClipBounds =
AdornedVisual?._combinedTransformedClipBounds
?? Parent?._combinedTransformedClipBounds
?? new Rect(Root!.Size);
_combinedTransformedClipBounds = Parent?._combinedTransformedClipBounds ?? new Rect(Root!.Size);
if (_transformedClipBounds != null)
_combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value);
EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1);
IsVisibleInFrame = _parent?.IsVisibleInFrame != false && Visible && EffectiveOpacity > 0.04 && !_isBackface &&
!_combinedTransformedClipBounds.IsDefault;
IsHitTestVisibleInFrame = _parent?.IsHitTestVisibleInFrame != false
&& Visible
&& !_isBackface
&& !_combinedTransformedClipBounds.IsDefault;
IsVisibleInFrame = IsHitTestVisibleInFrame
&& _parent?.IsVisibleInFrame != false
&& EffectiveOpacity > 0.04;
if (wasVisible != IsVisibleInFrame || positionChanged)
{
@ -187,7 +206,7 @@ namespace Avalonia.Rendering.Composition.Server
readback.Revision = root.Revision;
readback.Matrix = GlobalTransformMatrix;
readback.TargetId = Root.Id;
readback.Visible = IsVisibleInFrame;
readback.Visible = IsHitTestVisibleInFrame;
}
void AddDirtyRect(Rect rc)
@ -248,6 +267,7 @@ namespace Avalonia.Rendering.Composition.Server
}
public bool IsVisibleInFrame { get; set; }
public bool IsHitTestVisibleInFrame { get; set; }
public double EffectiveOpacity { get; set; }
public Rect TransformedOwnContentBounds { get; set; }
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);
}
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>();
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);
}
}
}
}

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>();
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt)
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{
if (reader.Read<byte>() == 1)
{
@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition.Server
for (var c = 0; c < count; c++)
List.Add(reader.ReadObject<T>());
}
base.DeserializeChangesCore(reader, commitedAt);
base.DeserializeChangesCore(reader, committedAt);
}
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,
TimeSpan commitedAt, IAnimationInstance animation) where T : struct
TimeSpan committedAt, IAnimationInstance animation) where T : struct
{
if (IsActive && _animations.TryGetValue(prop, out var oldAnimation))
oldAnimation.Deactivate();
_animations[prop] = animation;
animation.Initialize(commitedAt, ExpressionVariant.Create(field), prop);
animation.Initialize(committedAt, ExpressionVariant.Create(field), prop);
if(IsActive)
animation.Activate();
@ -165,7 +165,7 @@ namespace Avalonia.Rendering.Composition.Server
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
&& reader.Read<byte>() == 1)
@ -174,7 +174,7 @@ namespace Avalonia.Rendering.Composition.Server
public void DeserializeChanges(BatchStreamReader reader, Batch batch)
{
DeserializeChangesCore(reader, batch.CommitedAt);
DeserializeChangesCore(reader, batch.CommittedAt);
ValuesInvalidated();
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 TimeSpan CommitedAt { get; set; }
public TimeSpan CommittedAt { get; set; }
public void Complete()
{

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

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server;
@ -27,6 +28,37 @@ public record struct BatchStreamSegment<TData>
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
{
private readonly BatchStreamData _output;
@ -74,7 +106,15 @@ internal class BatchStreamWriter : IDisposable
var size = Unsafe.SizeOf<T>();
if (_currentDataSegment.Data == IntPtr.Zero || _currentDataSegment.ElementCount + size > _memoryPool.BufferSize)
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;
}
@ -125,7 +165,16 @@ internal class BatchStreamReader : IDisposable
if (_memoryOffset + size > _currentDataSegment.ElementCount)
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;
if (_memoryOffset == _currentDataSegment.ElementCount)
{

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

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

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

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

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

@ -303,7 +303,7 @@ namespace Avalonia.Rendering.SceneGraph
if (ClipToBounds)
{
context.Transform = Matrix.Identity;
if (ClipToBoundsRadius.IsEmpty)
if (ClipToBoundsRadius.IsDefault)
context.PushClip(ClipBounds);
else
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)
return true;
if (SourceRect.Position != default(Point))
if (SourceRect.Position != default)
return true;
if (SourceRect.Size.AspectRatio == _imageSize.AspectRatio)
return false;

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

@ -176,13 +176,7 @@ namespace Avalonia.Threading
{
if (!IsEnabled)
{
var threading = AvaloniaLocator.Current.GetService<IPlatformThreadingInterface>();
if (threading == null)
{
throw new Exception("Could not start timer: IPlatformThreadingInterface is not registered.");
}
var threading = AvaloniaLocator.Current.GetRequiredService<IPlatformThreadingInterface>();
_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))
{
value = default(TValue);
value = default;
return false;
}
else

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

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

1
src/Avalonia.Base/Visual.cs

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

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

@ -65,10 +65,8 @@ namespace Avalonia.Build.Tasks
{
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")
{
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
protected static Color[,]? _colorChart = null;
protected static object _colorChartMutex = new object();
private static Color[,]? _colorChart = null;
private static readonly object _colorChartMutex = new();
/// <summary>
/// Initializes all color chart colors.

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

@ -10,8 +10,8 @@ namespace Avalonia.Controls
/// <inheritdoc cref="FlatColorPalette"/>
public class FlatHalfColorPalette : IColorPalette
{
protected static Color[,]? _colorChart = null;
protected static object _colorChartMutex = new object();
private static Color[,]? _colorChart = null;
private static readonly object _colorChartMutex = new();
/// <summary>
/// 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
// This is a reduced palette for uniformity
protected static Color[,]? _colorChart = null;
protected static object _colorChartMutex = new object();
private static Color[,]? _colorChart = null;
private static readonly object _colorChartMutex = new();
/// <summary>
/// Initializes all color chart colors.

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

@ -10,8 +10,8 @@ namespace Avalonia.Controls
/// <inheritdoc cref="MaterialColorPalette"/>
public class MaterialHalfColorPalette : IColorPalette
{
protected static Color[,]? _colorChart = null;
protected static object _colorChartMutex = new object();
private static Color[,]? _colorChart = null;
private static readonly object _colorChartMutex = new();
/// <summary>
/// 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_elementTopRightCornerHeaderName = "PART_TopRightCornerHeader";
private const string DATAGRID_elementBottomRightCornerHeaderName = "PART_BottomRightCorner";
private const string DATAGRID_elementValidationSummary = "PART_ValidationSummary";
private const string DATAGRID_elementVerticalScrollbarName = "PART_VerticalScrollbar";
private const bool DATAGRID_defaultAutoGenerateColumns = true;
internal const bool DATAGRID_defaultCanUserReorderColumns = true;
internal const bool DATAGRID_defaultCanUserResizeColumns = true;
internal const bool DATAGRID_defaultCanUserSortColumns = true;
private const DataGridRowDetailsVisibilityMode DATAGRID_defaultRowDetailsVisibility = DataGridRowDetailsVisibilityMode.VisibleWhenSelected;
private const DataGridSelectionMode DATAGRID_defaultSelectionMode = DataGridSelectionMode.Extended;
/// <summary>
/// The default order to use for columns when there is no <see cref="DisplayAttribute.Order"/>
@ -1124,7 +1119,7 @@ namespace Avalonia.Controls
EnsureColumnHeadersVisibility();
if (!newValueCols)
{
_columnHeadersPresenter.Measure(default(Size));
_columnHeadersPresenter.Measure(default);
}
else
{
@ -1165,7 +1160,7 @@ namespace Avalonia.Controls
_topLeftCornerHeader.IsVisible = newValueRows && newValueCols;
if (_topLeftCornerHeader.IsVisible)
{
_topLeftCornerHeader.Measure(default(Size));
_topLeftCornerHeader.Measure(default);
}
}
@ -3300,7 +3295,7 @@ namespace Avalonia.Controls
newCell.IsVisible = column.IsVisible;
if (row.OwningGrid.CellTheme is {} cellTheme)
{
newCell.SetValue(ThemeProperty, cellTheme, BindingPriority.TemplatedParent);
newCell.SetValue(ThemeProperty, cellTheme, BindingPriority.Template);
}
}
row.Cells.Insert(column.Index, newCell);
@ -4158,6 +4153,7 @@ namespace Avalonia.Controls
if (exitEditingMode)
{
CurrentColumn.EndCellEditInternal();
_editingColumnIndex = -1;
editingCell.UpdatePseudoClasses();

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

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

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

@ -185,7 +185,7 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="IsVisible"/> property.
/// </summary>
public static StyledProperty<bool> IsVisibleProperty =
public static readonly StyledProperty<bool> IsVisibleProperty =
Control.IsVisibleProperty.AddOwner<DataGridColumn>();
/// <summary>
@ -800,11 +800,22 @@ namespace Avalonia.Controls
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)
{
CancelCellEdit(editingElement, uneditedValue);
}
internal void EndCellEditInternal()
{
EndCellEdit();
}
/// <summary>
/// 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
@ -897,7 +908,7 @@ namespace Avalonia.Controls
result[!ContentControl.ContentTemplateProperty] = this[!HeaderTemplateProperty];
if (OwningGrid.ColumnHeaderTheme is {} columnTheme)
{
result.SetValue(StyledElement.ThemeProperty, columnTheme, BindingPriority.TemplatedParent);
result.SetValue(StyledElement.ThemeProperty, columnTheme, BindingPriority.Template);
}
return result;

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

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

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

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

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

@ -1029,7 +1029,7 @@ namespace Avalonia.Controls
dataGridRow.DataContext = dataContext;
if (RowTheme is {} rowTheme)
{
dataGridRow.SetValue(ThemeProperty, rowTheme, BindingPriority.TemplatedParent);
dataGridRow.SetValue(ThemeProperty, rowTheme, BindingPriority.Template);
}
CompleteCellsCollection(dataGridRow);
@ -2743,7 +2743,7 @@ namespace Avalonia.Controls
groupHeader.Level = rowGroupInfo.Level;
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

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

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

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

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

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

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Metadata;
namespace Avalonia.Controls.ApplicationLifetimes
@ -19,7 +18,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// <summary>
/// Gets the arguments passed to the
/// <see cref="ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime{T}(T, string[], ShutdownMode)"/>
/// <see cref="ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime(AppBuilder, string[], ShutdownMode)"/>
/// method.
/// </summary>
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));
ItemFilterProperty.Changed.AddClassHandler<AutoCompleteBox>((x,e) => x.OnItemFilterPropertyChanged(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));
}

8
src/Avalonia.Controls/BorderVisual.cs

@ -50,7 +50,7 @@ class CompositionBorderVisual : CompositionDrawListVisual
if (ClipToBounds)
{
var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y)));
if (_cornerRadius.IsEmpty)
if (_cornerRadius.IsDefault)
canvas.PushClip(clipRect);
else
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>())
_cornerRadius = reader.Read<CornerRadius>();
}
@ -73,4 +73,4 @@ class CompositionBorderVisual : CompositionDrawListVisual
protected override bool HandlesClipToBounds => true;
}
}
}

11
src/Avalonia.Controls/ContentControl.cs

@ -116,14 +116,19 @@ namespace Avalonia.Controls
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);
}
if (e.NewValue is ILogical newChild)
if (toAdd is ILogical newChild)
{
LogicalChildren.Add(newChild);
}

21
src/Avalonia.Controls/Control.cs

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

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

@ -8,6 +8,6 @@
/// <summary>
/// Provides access to the internal <see cref="ToolTip.ToolTipProperty"/> for use in DevTools.
/// </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