Browse Source

Merge branch 'master' into feature/add_handling_input_message_previewer

pull/4418/head
Rustam Sayfutdinov 5 years ago
parent
commit
57ef1e7c81
  1. 53
      Avalonia.sln
  2. 25
      azure-pipelines.yml
  3. 4
      build/HarfBuzzSharp.props
  4. 1
      dirs.proj
  5. 2
      global.json
  6. 2
      native/Avalonia.Native/src/OSX/rendertarget.mm
  7. 60
      native/Avalonia.Native/src/OSX/window.mm
  8. 42
      nukebuild/Build.cs
  9. 5
      nukebuild/BuildParameters.cs
  10. 1
      nukebuild/_build.csproj
  11. 12
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  12. 10
      samples/ControlCatalog/Pages/SplitViewPage.xaml
  13. 4
      src/Avalonia.Controls/ApiCompatBaseline.txt
  14. 14
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  15. 7
      src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
  16. 10
      src/Avalonia.Controls/ColumnDefinitions.cs
  17. 4
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  18. 2
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  19. 24
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  20. 23
      src/Avalonia.Controls/Selection/SelectionModel.cs
  21. 34
      src/Avalonia.Controls/Selection/SelectionNodeBase.cs
  22. 62
      src/Avalonia.Controls/SplitView.cs
  23. 2
      src/Avalonia.Controls/TextBlock.cs
  24. 29
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  25. 43
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  26. 51
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  27. 87
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  28. 45
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  29. 14
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  30. 4
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  31. 22
      src/Avalonia.Input/AccessKeyHandler.cs
  32. 2
      src/Avalonia.Input/Avalonia.Input.csproj
  33. 6
      src/Avalonia.Input/DataObject.cs
  34. 6
      src/Avalonia.Input/DragDropDevice.cs
  35. 47
      src/Avalonia.Input/FocusManager.cs
  36. 16
      src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs
  37. 15
      src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
  38. 9
      src/Avalonia.Input/Gestures.cs
  39. 2
      src/Avalonia.Input/IAccessKeyHandler.cs
  40. 6
      src/Avalonia.Input/IDataObject.cs
  41. 6
      src/Avalonia.Input/IFocusManager.cs
  42. 2
      src/Avalonia.Input/IInputElement.cs
  43. 4
      src/Avalonia.Input/IInputRoot.cs
  44. 4
      src/Avalonia.Input/IKeyboardDevice.cs
  45. 4
      src/Avalonia.Input/IPointer.cs
  46. 4
      src/Avalonia.Input/IPointerDevice.cs
  47. 8
      src/Avalonia.Input/InputElement.cs
  48. 2
      src/Avalonia.Input/KeyEventArgs.cs
  49. 8
      src/Avalonia.Input/KeyboardDevice.cs
  50. 13
      src/Avalonia.Input/KeyboardNavigationHandler.cs
  51. 73
      src/Avalonia.Input/MouseDevice.cs
  52. 42
      src/Avalonia.Input/Navigation/TabNavigation.cs
  53. 9
      src/Avalonia.Input/Pointer.cs
  54. 14
      src/Avalonia.Input/PointerEventArgs.cs
  55. 2
      src/Avalonia.Input/Raw/RawDragEventType.cs
  56. 2
      src/Avalonia.Input/Raw/RawInputEventArgs.cs
  57. 4
      src/Avalonia.Input/TextInputEventArgs.cs
  58. 4
      src/Avalonia.Layout/Layoutable.cs
  59. 31
      src/Avalonia.OpenGL/Angle/AngleEglInterface.cs
  60. 88
      src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs
  61. 5
      src/Avalonia.OpenGL/AngleOptions.cs
  62. 13
      src/Avalonia.OpenGL/EglConsts.cs
  63. 76
      src/Avalonia.OpenGL/EglDisplay.cs
  64. 7
      src/Avalonia.OpenGL/EglGlPlatformFeature.cs
  65. 84
      src/Avalonia.OpenGL/EglGlPlatformSurface.cs
  66. 103
      src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs
  67. 29
      src/Avalonia.OpenGL/EglInterface.cs
  68. 12
      src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj
  69. 4
      src/Avalonia.Themes.Fluent/CheckBox.xaml
  70. 4
      src/Avalonia.Themes.Fluent/RadioButton.xaml
  71. 15
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  72. 67
      src/Avalonia.Visuals/Media/FontManager.cs
  73. 40
      src/Avalonia.Visuals/Media/Fonts/FontKey.cs
  74. 7
      src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
  75. 6
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  76. 4
      src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
  77. 17
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  78. 31
      src/Avalonia.Visuals/Media/Typeface.cs
  79. 5
      src/Avalonia.Visuals/Platform/IFontManagerImpl.cs
  80. 3
      src/Avalonia.Visuals/Rendering/RenderLayer.cs
  81. 2
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  82. 4
      src/Avalonia.Visuals/Visual.cs
  83. 7
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  84. 2
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs
  85. 19
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  86. 2
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  87. 6
      src/Skia/Avalonia.Skia/SkiaOptions.cs
  88. 7
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  89. 3
      src/Windows/Avalonia.Win32/Win32GlManager.cs
  90. 13
      src/Windows/Avalonia.Win32/WindowImpl.cs
  91. 63
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  92. 32
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs
  93. 31
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs
  94. 31
      tests/Avalonia.LeakTests/ControlTests.cs
  95. 15
      tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj
  96. 44
      tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs
  97. 2
      tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs
  98. 8
      tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs
  99. 2
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs
  100. 26
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

53
Avalonia.sln

@ -222,6 +222,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "sr
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}"
EndProject
Global Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@ -2012,6 +2016,54 @@ Global
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhone.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhone.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|Any CPU.Build.0 = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.ActiveCfg = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.Build.0 = Release|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.ActiveCfg = Release|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.Build.0 = Release|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -2070,6 +2122,7 @@ Global
{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
{909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

25
azure-pipelines.yml

@ -35,16 +35,9 @@ jobs:
vmImage: 'macOS-10.14' vmImage: 'macOS-10.14'
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.101' displayName: 'Use .NET Core SDK 3.1.401'
inputs: inputs:
packageType: sdk version: 3.1.401
version: 3.1.101
- task: UseDotNet@2
displayName: 'Use .NET Core Runtime 3.1.1'
inputs:
packageType: runtime
version: 3.1.1
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Mono 5.18' displayName: 'Install Mono 5.18'
@ -63,13 +56,6 @@ jobs:
xcodeVersion: '10' # Options: 8, 9, default, specifyPath xcodeVersion: '10' # Options: 8, 9, default, specifyPath
args: '-derivedDataPath ./' args: '-derivedDataPath ./'
- task: CmdLine@2
displayName: 'Install CastXML'
inputs:
script: |
brew update
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Nuke' displayName: 'Install Nuke'
inputs: inputs:
@ -88,7 +74,7 @@ jobs:
export PATH="$PATH:$HOME/.dotnet/tools" export PATH="$PATH:$HOME/.dotnet/tools"
dotnet --info dotnet --info
printenv printenv
nuke --target CiAzureOSX --configuration Release nuke --target CiAzureOSX --configuration Release --skip-previewer
- task: PublishTestResults@2 - task: PublishTestResults@2
inputs: inputs:
@ -112,6 +98,11 @@ jobs:
pool: pool:
vmImage: 'windows-2019' vmImage: 'windows-2019'
steps: steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.401'
inputs:
version: 3.1.401
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Install Nuke' displayName: 'Install Nuke'
inputs: inputs:

4
build/HarfBuzzSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup>
<PackageReference Include="HarfBuzzSharp" Version="2.6.1.5" /> <PackageReference Include="HarfBuzzSharp" Version="2.6.1.6" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1.5" /> <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1.6" />
</ItemGroup> </ItemGroup>
</Project> </Project>

1
dirs.proj

@ -7,6 +7,7 @@
<ProjectReference Remove="**/*.shproj" /> <ProjectReference Remove="**/*.shproj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" /> <ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" /> <ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" />
<ProjectReference Remove="tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj" />
</ItemGroup> </ItemGroup>
<!--<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\Android')">--> <!--<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\Android')">-->
<ItemGroup> <ItemGroup>

2
global.json

@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "3.1.101" "version": "3.1.401"
}, },
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43", "Microsoft.Build.Traversal": "1.0.43",

2
native/Avalonia.Native/src/OSX/rendertarget.mm

@ -110,7 +110,7 @@
if(_renderbuffer != 0) if(_renderbuffer != 0)
glDeleteRenderbuffers(1, &_renderbuffer); glDeleteRenderbuffers(1, &_renderbuffer);
} }
IOSurfaceDecrementUseCount(surface); CFRelease(surface);
} }
@end @end

60
native/Avalonia.Native/src/OSX/window.mm

@ -1209,6 +1209,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
bool _queuedDisplayFromThread; bool _queuedDisplayFromThread;
NSTrackingArea* _area; NSTrackingArea* _area;
bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver; bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver;
AvnInputModifiers _modifierState;
NSEvent* _lastMouseDownEvent; NSEvent* _lastMouseDownEvent;
bool _lastKeyHandled; bool _lastKeyHandled;
AvnPixelSize _lastPixelSize; AvnPixelSize _lastPixelSize;
@ -1251,6 +1252,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_lastPixelSize.Height = 100; _lastPixelSize.Height = 100;
_lastPixelSize.Width = 100; _lastPixelSize.Width = 100;
[self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]];
_modifierState = AvnInputModifiersNone;
return self; return self;
} }
@ -1594,6 +1597,63 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return result; return result;
} }
- (void)flagsChanged:(NSEvent *)event
{
auto newModifierState = [self getModifiers:[event modifierFlags]];
bool isAltCurrentlyPressed = (_modifierState & Alt) == Alt;
bool isControlCurrentlyPressed = (_modifierState & Control) == Control;
bool isShiftCurrentlyPressed = (_modifierState & Shift) == Shift;
bool isCommandCurrentlyPressed = (_modifierState & Windows) == Windows;
bool isAltPressed = (newModifierState & Alt) == Alt;
bool isControlPressed = (newModifierState & Control) == Control;
bool isShiftPressed = (newModifierState & Shift) == Shift;
bool isCommandPressed = (newModifierState & Windows) == Windows;
if (isAltPressed && !isAltCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if (isAltCurrentlyPressed && !isAltPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
if (isControlPressed && !isControlCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if (isControlCurrentlyPressed && !isControlPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
if (isShiftPressed && !isShiftCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if(isShiftCurrentlyPressed && !isShiftPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
if(isCommandPressed && !isCommandCurrentlyPressed)
{
[self keyboardEvent:event withType:KeyDown];
}
else if(isCommandCurrentlyPressed && ! isCommandPressed)
{
[self keyboardEvent:event withType:KeyUp];
}
_modifierState = newModifierState;
[[self inputContext] handleEvent:event];
[super flagsChanged:event];
}
- (void)keyDown:(NSEvent *)event - (void)keyDown:(NSEvent *)event
{ {
[self keyboardEvent:event withType:KeyDown]; [self keyboardEvent:event withType:KeyDown];

42
nukebuild/Build.cs

@ -5,6 +5,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
using Nuke.Common; using Nuke.Common;
using Nuke.Common.Git; using Nuke.Common.Git;
@ -15,6 +16,7 @@ using Nuke.Common.Tools.MSBuild;
using Nuke.Common.Tools.Npm; using Nuke.Common.Tools.Npm;
using Nuke.Common.Utilities; using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections; using Nuke.Common.Utilities.Collections;
using Pharmacist.Core;
using static Nuke.Common.EnvironmentInfo; using static Nuke.Common.EnvironmentInfo;
using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction; using static Nuke.Common.IO.PathConstruction;
@ -124,6 +126,7 @@ partial class Build : NukeBuild
Target CompileHtmlPreviewer => _ => _ Target CompileHtmlPreviewer => _ => _
.DependsOn(Clean) .DependsOn(Clean)
.OnlyWhenStatic(() => !Parameters.SkipPreviewer)
.Executes(() => .Executes(() =>
{ {
var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp"; var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp";
@ -139,7 +142,7 @@ partial class Build : NukeBuild
Target Compile => _ => _ Target Compile => _ => _
.DependsOn(Clean) .DependsOn(Clean)
.DependsOn(CompileHtmlPreviewer) .DependsOn(CompileHtmlPreviewer)
.Executes(() => .Executes(async () =>
{ {
if (Parameters.IsRunningOnWindows) if (Parameters.IsRunningOnWindows)
MsBuildCommon(Parameters.MSBuildSolution, c => c MsBuildCommon(Parameters.MSBuildSolution, c => c
@ -153,8 +156,44 @@ partial class Build : NukeBuild
.AddProperty("PackageVersion", Parameters.Version) .AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration) .SetConfiguration(Parameters.Configuration)
); );
await CompileReactiveEvents();
}); });
async Task CompileReactiveEvents()
{
var avaloniaBuildOutput = Path.Combine(RootDirectory, "packages", "Avalonia", "bin", Parameters.Configuration);
var avaloniaAssemblies = GlobFiles(avaloniaBuildOutput, "**/Avalonia*.dll")
.Where(file => !file.Contains("Avalonia.Build.Tasks") &&
!file.Contains("Avalonia.Remote.Protocol"));
var eventsDirectory = GlobDirectories($"{RootDirectory}/src/**/Avalonia.ReactiveUI.Events").First();
var eventsBuildFile = Path.Combine(eventsDirectory, "Events_Avalonia.cs");
if (File.Exists(eventsBuildFile))
File.Delete(eventsBuildFile);
using (var stream = File.Create(eventsBuildFile))
using (var writer = new StreamWriter(stream))
{
await ObservablesForEventGenerator.ExtractEventsFromAssemblies(
writer, avaloniaAssemblies, new string[0], "netstandard2.0"
);
}
var eventsProject = Path.Combine(eventsDirectory, "Avalonia.ReactiveUI.Events.csproj");
if (Parameters.IsRunningOnWindows)
MsBuildCommon(eventsProject, c => c
.SetArgumentConfigurator(a => a.Add("/r"))
.AddTargets("Build")
);
else
DotNetBuild(c => c
.SetProjectFile(eventsProject)
.AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration)
);
}
void RunCoreTest(string projectName) void RunCoreTest(string projectName)
{ {
Information($"Running tests from {projectName}"); Information($"Running tests from {projectName}");
@ -202,6 +241,7 @@ partial class Build : NukeBuild
RunCoreTest("Avalonia.Visuals.UnitTests"); RunCoreTest("Avalonia.Visuals.UnitTests");
RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.Events.UnitTests");
}); });
Target RunRenderTests => _ => _ Target RunRenderTests => _ => _

5
nukebuild/BuildParameters.cs

@ -19,10 +19,14 @@ public partial class Build
[Parameter("force-nuget-version")] [Parameter("force-nuget-version")]
public string ForceNugetVersion { get; set; } public string ForceNugetVersion { get; set; }
[Parameter("skip-previewer")]
public bool SkipPreviewer { get; set; }
public class BuildParameters public class BuildParameters
{ {
public string Configuration { get; } public string Configuration { get; }
public bool SkipTests { get; } public bool SkipTests { get; }
public bool SkipPreviewer {get;}
public string MainRepo { get; } public string MainRepo { get; }
public string MasterBranch { get; } public string MasterBranch { get; }
public string RepositoryName { get; } public string RepositoryName { get; }
@ -63,6 +67,7 @@ public partial class Build
// ARGUMENTS // ARGUMENTS
Configuration = b.Configuration ?? "Release"; Configuration = b.Configuration ?? "Release";
SkipTests = b.SkipTests; SkipTests = b.SkipTests;
SkipPreviewer = b.SkipPreviewer;
// CONFIGURATION // CONFIGURATION
MainRepo = "https://github.com/AvaloniaUI/Avalonia"; MainRepo = "https://github.com/AvaloniaUI/Avalonia";

1
nukebuild/_build.csproj

@ -17,6 +17,7 @@
<PackageReference Include="ILRepack.NETStandard" Version="2.0.4" /> <PackageReference Include="ILRepack.NETStandard" Version="2.0.4" />
<!-- Keep in sync with Avalonia.Build.Tasks --> <!-- Keep in sync with Avalonia.Build.Tasks -->
<PackageReference Include="Avalonia.Unofficial.Cecil" Version="20190417.2.0" /> <PackageReference Include="Avalonia.Unofficial.Cecil" Version="20190417.2.0" />
<PackageReference Include="Pharmacist.Core" Version="1.8.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

12
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

@ -9,6 +9,10 @@
<RootNamespace>ControlCatalog.iOS</RootNamespace> <RootNamespace>ControlCatalog.iOS</RootNamespace>
<IPhoneResourcePrefix>Resources</IPhoneResourcePrefix> <IPhoneResourcePrefix>Resources</IPhoneResourcePrefix>
<AssemblyName>ControlCatalogiOS</AssemblyName> <AssemblyName>ControlCatalogiOS</AssemblyName>
<MtouchEnableSGenConc>true</MtouchEnableSGenConc>
<MtouchHttpClientHandler>NSUrlSessionHandler</MtouchHttpClientHandler>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<ProvisioningType>automatic</ProvisioningType>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhoneSimulator' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhoneSimulator' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -19,8 +23,8 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<ConsolePause>false</ConsolePause> <ConsolePause>false</ConsolePause>
<MtouchArch>i386</MtouchArch> <MtouchArch>x86_64</MtouchArch>
<MtouchLink>SdkOnly</MtouchLink> <MtouchLink>None</MtouchLink>
<MtouchDebug>True</MtouchDebug> <MtouchDebug>True</MtouchDebug>
<MtouchSdkVersion>9.1</MtouchSdkVersion> <MtouchSdkVersion>9.1</MtouchSdkVersion>
<MtouchProfiling>False</MtouchProfiling> <MtouchProfiling>False</MtouchProfiling>
@ -43,7 +47,7 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<MtouchLink>None</MtouchLink> <MtouchLink>None</MtouchLink>
<MtouchArch>i386</MtouchArch> <MtouchArch>x86_64</MtouchArch>
<ConsolePause>false</ConsolePause> <ConsolePause>false</ConsolePause>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhone' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|iPhone' ">
@ -173,8 +177,10 @@
<Project>{d0a739b9-3c68-4ba6-a328-41606954b6bd}</Project> <Project>{d0a739b9-3c68-4ba6-a328-41606954b6bd}</Project>
<Name>ControlCatalog</Name> <Name>ControlCatalog</Name>
</ProjectReference> </ProjectReference>
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.2-preview.33" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
<Import Project="..\..\build\LegacyProject.targets" /> <Import Project="..\..\build\LegacyProject.targets" />
<Import Project="..\..\build\SkiaSharp.props" /> <Import Project="..\..\build\SkiaSharp.props" />
<Import Project="..\..\build\HarfBuzzSharp.props" />
</Project> </Project>

10
samples/ControlCatalog/Pages/SplitViewPage.xaml

@ -58,12 +58,18 @@
<SplitView.Pane> <SplitView.Pane>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Text="PANE CONTENT" FontWeight="Bold" Name="PaneHeader" Margin="5,12,0,0" /> <TextBlock Text="PANE CONTENT" FontWeight="Bold" Name="PaneHeader" Margin="5,12,0,0" />
<ListBoxItem Grid.Row="1" VerticalAlignment="Top" Margin="0 10"> <ComboBox Width="150" Grid.Row="1">
<ComboBoxItem Content="Item1"/>
<ComboBoxItem Content="Item2"/>
<ComboBoxItem Content="Item3"/>
</ComboBox>
<ListBoxItem Grid.Row="2" VerticalAlignment="Top" Margin="0 10">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<!--Path glyph from materialdesignicons.com--> <!--Path glyph from materialdesignicons.com-->
<Border Width="48"> <Border Width="48">
@ -76,7 +82,7 @@
<TextBlock Text="People" VerticalAlignment="Center" /> <TextBlock Text="People" VerticalAlignment="Center" />
</StackPanel> </StackPanel>
</ListBoxItem> </ListBoxItem>
<TextBlock Grid.Row="2" Text="Item at bottom" Margin="60,12" /> <TextBlock Grid.Row="3" Text="Item at bottom" Margin="60,12" />
</Grid> </Grid>
</SplitView.Pane> </SplitView.Pane>

4
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -12,7 +12,9 @@ MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Tree
MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Controls.TreeView.SelectionChangedEvent' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Interactivity.RoutedEvent<Avalonia.Controls.SelectionChangedEventArgs> Avalonia.Controls.TreeView.SelectionChangedEvent' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.TreeView.Selection.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Controls.ISelectionModel Avalonia.Controls.TreeView.Selection.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.TreeView.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.TreeView.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.String[] Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.Args' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.String[] Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.Args.get()' is present in the implementation but not in the contract.
MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.Controls.Primitives.SelectingItemsControl.SelectionProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.DirectProperty<Avalonia.Controls.Primitives.SelectingItemsControl, Avalonia.Controls.ISelectionModel> Avalonia.Controls.Primitives.SelectingItemsControl.SelectionProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected Avalonia.Controls.ISelectionModel Avalonia.Controls.Primitives.SelectingItemsControl.Selection.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected Avalonia.Controls.ISelectionModel Avalonia.Controls.Primitives.SelectingItemsControl.Selection.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Controls.Primitives.SelectingItemsControl.Selection.set(Avalonia.Controls.ISelectionModel)' does not exist in the implementation but it does exist in the contract.
Total Issues: 16 Total Issues: 18

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

@ -47,6 +47,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit; public event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit;
/// <summary>
/// Gets the arguments passed to the AppBuilder Start method.
/// </summary>
public string[] Args { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public ShutdownMode ShutdownMode { get; set; } public ShutdownMode ShutdownMode { get; set; }
@ -68,9 +73,6 @@ namespace Avalonia.Controls.ApplicationLifetimes
else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow) else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow)
Shutdown(); Shutdown();
} }
public void Shutdown(int exitCode = 0) public void Shutdown(int exitCode = 0)
{ {
@ -123,7 +125,11 @@ namespace Avalonia
this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) this T builder, string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose)
where T : AppBuilderBase<T>, new() where T : AppBuilderBase<T>, new()
{ {
var lifetime = new ClassicDesktopStyleApplicationLifetime() {ShutdownMode = shutdownMode}; var lifetime = new ClassicDesktopStyleApplicationLifetime()
{
Args = args,
ShutdownMode = shutdownMode
};
builder.SetupWithLifetime(lifetime); builder.SetupWithLifetime(lifetime);
return lifetime.Start(args); return lifetime.Start(args);
} }

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

@ -8,6 +8,13 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// </summary> /// </summary>
public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime
{ {
/// <summary>
/// Gets the arguments passed to the
/// <see cref="ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime{T}(T, string[], ShutdownMode)"/>
/// method.
/// </summary>
string[] Args { get; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="ShutdownMode"/>. This property indicates whether the application is shutdown explicitly or implicitly. /// Gets or sets the <see cref="ShutdownMode"/>. This property indicates whether the application is shutdown explicitly or implicitly.
/// If <see cref="ShutdownMode"/> is set to OnExplicitShutdown the application is only closes if Shutdown is called. /// If <see cref="ShutdownMode"/> is set to OnExplicitShutdown the application is only closes if Shutdown is called.

10
src/Avalonia.Controls/ColumnDefinitions.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Text;
using Avalonia.Collections; using Avalonia.Collections;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -13,7 +14,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ColumnDefinitions"/> class. /// Initializes a new instance of the <see cref="ColumnDefinitions"/> class.
/// </summary> /// </summary>
public ColumnDefinitions() : base () public ColumnDefinitions()
{ {
} }
@ -27,6 +28,11 @@ namespace Avalonia.Controls
AddRange(GridLength.ParseLengths(s).Select(x => new ColumnDefinition(x))); AddRange(GridLength.ParseLengths(s).Select(x => new ColumnDefinition(x)));
} }
public override string ToString()
{
return string.Join(",", this.Select(x => x.Width));
}
/// <summary> /// <summary>
/// Parses a string representation of column definitions collection. /// Parses a string representation of column definitions collection.
/// </summary> /// </summary>
@ -34,4 +40,4 @@ namespace Avalonia.Controls
/// <returns>The <see cref="ColumnDefinitions"/>.</returns> /// <returns>The <see cref="ColumnDefinitions"/>.</returns>
public static ColumnDefinitions Parse(string s) => new ColumnDefinitions(s); public static ColumnDefinitions Parse(string s) => new ColumnDefinitions(s);
} }
} }

4
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -282,7 +282,7 @@ namespace Avalonia.Controls.Presenters
return new FormattedText return new FormattedText
{ {
Constraint = constraint, Constraint = constraint,
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
FontSize = FontSize, FontSize = FontSize,
Text = text ?? string.Empty, Text = text ?? string.Empty,
TextAlignment = TextAlignment, TextAlignment = TextAlignment,
@ -499,7 +499,7 @@ namespace Avalonia.Controls.Presenters
return new FormattedText return new FormattedText
{ {
Text = "X", Text = "X",
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
FontSize = FontSize, FontSize = FontSize,
TextAlignment = TextAlignment, TextAlignment = TextAlignment,
Constraint = availableSize, Constraint = availableSize,

2
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -141,7 +141,7 @@ namespace Avalonia.Controls.Primitives
_ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.") _ => throw new InvalidOperationException("Invalid value for ScrollBar.Visibility.")
}; };
SetValue(IsVisibleProperty, isVisible, BindingPriority.Style); SetValue(IsVisibleProperty, isVisible);
} }
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)

24
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -7,10 +7,10 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -681,8 +681,15 @@ namespace Avalonia.Controls
if (oldValue != null) if (oldValue != null)
{ {
oldValue.UninitializeForContext(LayoutContext); oldValue.UninitializeForContext(LayoutContext);
oldValue.MeasureInvalidated -= InvalidateMeasureForLayout;
oldValue.ArrangeInvalidated -= InvalidateArrangeForLayout; WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
oldValue,
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
oldValue,
nameof(AttachedLayout.ArrangeInvalidated),
InvalidateArrangeForLayout);
// Walk through all the elements and make sure they are cleared // Walk through all the elements and make sure they are cleared
foreach (var element in Children) foreach (var element in Children)
@ -699,8 +706,15 @@ namespace Avalonia.Controls
if (newValue != null) if (newValue != null)
{ {
newValue.InitializeForContext(LayoutContext); newValue.InitializeForContext(LayoutContext);
newValue.MeasureInvalidated += InvalidateMeasureForLayout;
newValue.ArrangeInvalidated += InvalidateArrangeForLayout; WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
newValue,
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
newValue,
nameof(AttachedLayout.ArrangeInvalidated),
InvalidateArrangeForLayout);
} }
bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout;

23
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -436,6 +436,29 @@ namespace Avalonia.Controls.Selection
} }
} }
private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{
if (!base.IsValidCollectionChange(e))
{
return false;
}
if (ItemsView is object && e.Action == NotifyCollectionChangedAction.Add)
{
if (e.NewStartingIndex <= _selectedIndex)
{
return _selectedIndex + e.NewItems.Count < ItemsView.Count;
}
if (e.NewStartingIndex <= _anchorIndex)
{
return _anchorIndex + e.NewItems.Count < ItemsView.Count;
}
}
return true;
}
protected override void OnSourceCollectionChangeFinished() protected override void OnSourceCollectionChangeFinished()
{ {
if (_operation is object) if (_operation is object)

34
src/Avalonia.Controls/Selection/SelectionNodeBase.cs

@ -2,6 +2,7 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
#nullable enable #nullable enable
@ -234,6 +235,11 @@ namespace Avalonia.Controls.Selection
var shiftIndex = -1; var shiftIndex = -1;
List<T>? removed = null; List<T>? removed = null;
if (!IsValidCollectionChange(e))
{
return;
}
switch (e.Action) switch (e.Action)
{ {
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
@ -276,6 +282,34 @@ namespace Avalonia.Controls.Selection
} }
} }
private protected virtual bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e)
{
// If the selection is modified in a CollectionChanged handler before the selection
// model's CollectionChanged handler has had chance to run then we can end up with
// a selected index that refers to the *new* state of the Source intermixed with
// indexes that reference an old state of the source.
//
// There's not much we can do in this situation, so detect whether shifting the
// current selected indexes would result in an invalid index in the source, and if
// so bail.
//
// See unit test Handles_Selection_Made_In_CollectionChanged for more details.
if (ItemsView is object &&
RangesEnabled &&
Ranges.Count > 0 &&
e.Action == NotifyCollectionChangedAction.Add)
{
var lastIndex = Ranges.Last().End;
if (e.NewStartingIndex <= lastIndex)
{
return lastIndex + e.NewItems.Count < ItemsView.Count;
}
}
return true;
}
private protected struct CollectionChangeState private protected struct CollectionChangeState
{ {
public int ShiftIndex; public int ShiftIndex;

62
src/Avalonia.Controls/SplitView.cs

@ -145,7 +145,7 @@ namespace Avalonia.Controls
private bool _isPaneOpen; private bool _isPaneOpen;
private Panel _pane; private Panel _pane;
private CompositeDisposable _pointerDisposables; private IDisposable _pointerDisposable;
public SplitView() public SplitView()
{ {
@ -320,37 +320,14 @@ namespace Avalonia.Controls
var topLevel = this.VisualRoot; var topLevel = this.VisualRoot;
if (topLevel is Window window) if (topLevel is Window window)
{ {
//Logic adapted from Popup _pointerDisposable = window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
//Basically if we're using an overlay DisplayMode, close the pane if we don't click on the pane
IDisposable subscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler,
Action<T, TEventHandler> subscribe, Action<T, TEventHandler> unsubscribe)
{
subscribe(target, handler);
return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler));
}
_pointerDisposables = new CompositeDisposable(
window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel),
InputManager.Instance?.Process.Subscribe(OnNonClientClick),
subscribeToEventHandler<Window, EventHandler>(window, Window_Deactivated,
(x, handler) => x.Deactivated += handler, (x, handler) => x.Deactivated -= handler),
subscribeToEventHandler<IWindowImpl, Action>(window.PlatformImpl, OnWindowLostFocus,
(x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler));
} }
} }
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnDetachedFromVisualTree(e); base.OnDetachedFromVisualTree(e);
_pointerDisposables?.Dispose(); _pointerDisposable?.Dispose();
}
private void OnWindowLostFocus()
{
if (IsPaneOpen && ShouldClosePane())
{
IsPaneOpen = false;
}
} }
private void PointerPressedOutside(object sender, PointerPressedEventArgs e) private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
@ -371,7 +348,12 @@ namespace Avalonia.Controls
var src = e.Source as IVisual; var src = e.Source as IVisual;
while (src != null) while (src != null)
{ {
if (src == _pane) // Make assumption that if Popup is in visual tree,
// owning control is within pane
// This works because if pane is triggered to close
// when clicked anywhere else in Window, the pane
// would close before the popup is opened
if (src == _pane || src is PopupRoot)
{ {
closePane = false; closePane = false;
break; break;
@ -385,31 +367,7 @@ namespace Avalonia.Controls
e.Handled = true; e.Handled = true;
} }
} }
private void OnNonClientClick(RawInputEventArgs obj)
{
if (!IsPaneOpen)
{
return;
}
var mouse = obj as RawPointerEventArgs;
if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
if (ShouldClosePane())
IsPaneOpen = false;
}
}
private void Window_Deactivated(object sender, EventArgs e)
{
if (IsPaneOpen && ShouldClosePane())
{
IsPaneOpen = false;
}
}
private bool ShouldClosePane() private bool ShouldClosePane()
{ {
return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay); return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay);

2
src/Avalonia.Controls/TextBlock.cs

@ -452,7 +452,7 @@ namespace Avalonia.Controls
return new TextLayout( return new TextLayout(
text ?? string.Empty, text ?? string.Empty,
FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight), new Typeface(FontFamily, FontStyle, FontWeight),
FontSize, FontSize,
Foreground, Foreground,
TextAlignment, TextAlignment,

29
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -12,12 +12,13 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly IVisual _control; private readonly IVisual _control;
private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex; private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
private AvaloniaPropertyViewModel _selectedProperty; private AvaloniaPropertyViewModel _selectedProperty;
private string _propertyFilter;
public ControlDetailsViewModel(IVisual control, string propertyFilter) public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
{ {
_control = control; _control = control;
TreePage = treePage;
var properties = GetAvaloniaProperties(control) var properties = GetAvaloniaProperties(control)
.Concat(GetClrProperties(control)) .Concat(GetClrProperties(control))
.OrderBy(x => x, PropertyComparer.Instance) .OrderBy(x => x, PropertyComparer.Instance)
@ -25,7 +26,6 @@ namespace Avalonia.Diagnostics.ViewModels
.ToList(); .ToList();
_propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList()); _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList());
_propertyFilter = propertyFilter;
var view = new DataGridCollectionView(properties); var view = new DataGridCollectionView(properties);
view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)));
@ -43,19 +43,9 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
public DataGridCollectionView PropertiesView { get; } public TreePageViewModel TreePage { get; }
public string PropertyFilter public DataGridCollectionView PropertiesView { get; }
{
get => _propertyFilter;
set
{
if (RaiseAndSetIfChanged(ref _propertyFilter, value))
{
PropertiesView.Refresh();
}
}
}
public AvaloniaPropertyViewModel SelectedProperty public AvaloniaPropertyViewModel SelectedProperty
{ {
@ -137,9 +127,14 @@ namespace Avalonia.Diagnostics.ViewModels
private bool FilterProperty(object arg) private bool FilterProperty(object arg)
{ {
if (!string.IsNullOrWhiteSpace(PropertyFilter) && arg is PropertyViewModel property) if (!string.IsNullOrWhiteSpace(TreePage.PropertyFilter) && arg is PropertyViewModel property)
{ {
return property.Name.IndexOf(PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1; if (TreePage.UseRegexFilter)
{
return TreePage.FilterRegex?.IsMatch(property.Name) ?? true;
}
return property.Name.IndexOf(TreePage.PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1;
} }
return true; return true;

43
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -9,7 +9,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
internal class MainViewModel : ViewModelBase, IDisposable internal class MainViewModel : ViewModelBase, IDisposable
{ {
private readonly IControl _root; private readonly TopLevel _root;
private readonly TreePageViewModel _logicalTree; private readonly TreePageViewModel _logicalTree;
private readonly TreePageViewModel _visualTree; private readonly TreePageViewModel _visualTree;
private readonly EventsPageViewModel _events; private readonly EventsPageViewModel _events;
@ -19,8 +19,10 @@ namespace Avalonia.Diagnostics.ViewModels
private string _focusedControl; private string _focusedControl;
private string _pointerOverElement; private string _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay;
public MainViewModel(IControl root) public MainViewModel(TopLevel root)
{ {
_root = root; _root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root)); _logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
@ -40,12 +42,42 @@ namespace Avalonia.Diagnostics.ViewModels
get => _shouldVisualizeMarginPadding; get => _shouldVisualizeMarginPadding;
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value); set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
} }
public bool ShouldVisualizeDirtyRects
{
get => _shouldVisualizeDirtyRects;
set
{
_root.Renderer.DrawDirtyRects = value;
RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value);
}
}
public void ToggleVisualizeDirtyRects()
{
ShouldVisualizeDirtyRects = !ShouldVisualizeDirtyRects;
}
public void ToggleVisualizeMarginPadding() public void ToggleVisualizeMarginPadding()
{ {
ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding; ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding;
} }
public bool ShowFpsOverlay
{
get => _showFpsOverlay;
set
{
_root.Renderer.DrawFps = value;
RaiseAndSetIfChanged(ref _showFpsOverlay, value);
}
}
public void ToggleFpsOverlay()
{
ShowFpsOverlay = !ShowFpsOverlay;
}
public ConsoleViewModel Console { get; } public ConsoleViewModel Console { get; }
public ViewModelBase Content public ViewModelBase Content
@ -128,10 +160,7 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
var tree = Content as TreePageViewModel; var tree = Content as TreePageViewModel;
if (tree != null) tree?.SelectControl(control);
{
tree.SelectControl(control);
}
} }
public void Dispose() public void Dispose()
@ -140,6 +169,8 @@ namespace Avalonia.Diagnostics.ViewModels
_pointerOverSubscription.Dispose(); _pointerOverSubscription.Dispose();
_logicalTree.Dispose(); _logicalTree.Dispose();
_visualTree.Dispose(); _visualTree.Dispose();
_root.Renderer.DrawDirtyRects = false;
_root.Renderer.DrawFps = false;
} }
private void UpdateFocusedControl() private void UpdateFocusedControl()

51
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

@ -8,8 +8,8 @@ namespace Avalonia.Diagnostics.ViewModels
internal abstract class PropertyViewModel : ViewModelBase internal abstract class PropertyViewModel : ViewModelBase
{ {
private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static; private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
private static readonly Type[] StringParameter = new[] { typeof(string) }; private static readonly Type[] StringParameter = { typeof(string) };
private static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) }; private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) };
public abstract object Key { get; } public abstract object Key { get; }
public abstract string Name { get; } public abstract string Name { get; }
@ -26,35 +26,46 @@ namespace Avalonia.Diagnostics.ViewModels
} }
var converter = TypeDescriptor.GetConverter(value); var converter = TypeDescriptor.GetConverter(value);
return converter?.ConvertToString(value) ?? value.ToString();
//CollectionConverter does not deliver any important information. It just displays "(Collection)".
if (!converter.CanConvertTo(typeof(string)) ||
converter.GetType() == typeof(CollectionConverter))
{
return value.ToString();
}
return converter.ConvertToString(value);
} }
protected static object ConvertFromString(string s, Type targetType) private static object InvokeParse(string s, Type targetType)
{ {
var converter = TypeDescriptor.GetConverter(targetType); var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
if (converter != null && converter.CanConvertFrom(typeof(string))) if (method != null)
{ {
return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s); return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture });
} }
else
method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null);
if (method != null)
{ {
var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null); return method.Invoke(null, new object[] { s });
}
if (method != null) throw new InvalidCastException("Unable to convert value.");
{ }
return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture });
}
method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null); protected static object ConvertFromString(string s, Type targetType)
{
var converter = TypeDescriptor.GetConverter(targetType);
if (method != null) if (converter.CanConvertFrom(typeof(string)))
{ {
return method.Invoke(null, new object[] { s }); return converter.ConvertFrom(null, CultureInfo.InvariantCulture, s);
}
} }
throw new InvalidCastException("Unable to convert value."); return InvokeParse(s, targetType);
} }
} }
} }

87
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@ -1,15 +1,20 @@
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text.RegularExpressions;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Selection;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal class TreePageViewModel : ViewModelBase, IDisposable internal class TreePageViewModel : ViewModelBase, IDisposable, INotifyDataErrorInfo
{ {
private readonly Dictionary<string, string> _errors = new Dictionary<string, string>();
private TreeNode _selectedNode; private TreeNode _selectedNode;
private ControlDetailsViewModel _details; private ControlDetailsViewModel _details;
private string _propertyFilter; private string _propertyFilter = string.Empty;
private bool _useRegexFilter;
public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes) public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes)
{ {
@ -26,15 +31,10 @@ namespace Avalonia.Diagnostics.ViewModels
get => _selectedNode; get => _selectedNode;
private set private set
{ {
if (Details != null)
{
_propertyFilter = Details.PropertyFilter;
}
if (RaiseAndSetIfChanged(ref _selectedNode, value)) if (RaiseAndSetIfChanged(ref _selectedNode, value))
{ {
Details = value != null ? Details = value != null ?
new ControlDetailsViewModel(value.Visual, _propertyFilter) : new ControlDetailsViewModel(this, value.Visual) :
null; null;
} }
} }
@ -54,6 +54,63 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
public Regex FilterRegex { get; set; }
private void UpdateFilterRegex()
{
void ClearError()
{
if (_errors.Remove(nameof(PropertyFilter)))
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
}
}
if (UseRegexFilter)
{
try
{
FilterRegex = new Regex(PropertyFilter, RegexOptions.Compiled);
ClearError();
}
catch (Exception exception)
{
_errors[nameof(PropertyFilter)] = exception.Message;
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
}
}
else
{
ClearError();
}
}
public string PropertyFilter
{
get => _propertyFilter;
set
{
if (RaiseAndSetIfChanged(ref _propertyFilter, value))
{
UpdateFilterRegex();
Details.PropertiesView.Refresh();
}
}
}
public bool UseRegexFilter
{
get => _useRegexFilter;
set
{
if (RaiseAndSetIfChanged(ref _useRegexFilter, value))
{
UpdateFilterRegex();
Details.PropertiesView.Refresh();
}
}
}
public void Dispose() public void Dispose()
{ {
foreach (var node in Nodes) foreach (var node in Nodes)
@ -130,5 +187,17 @@ namespace Avalonia.Diagnostics.ViewModels
return null; return null;
} }
public IEnumerable GetErrors(string propertyName)
{
if (_errors.TryGetValue(propertyName, out var error))
{
yield return error;
}
}
public bool HasErrors => _errors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
} }
} }

45
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -2,24 +2,31 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters" xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"> x:Class="Avalonia.Diagnostics.Views.ControlDetailsView">
<Grid ColumnDefinitions="*"> <Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*">
<DockPanel Grid.Column="0">
<TextBox DockPanel.Dock="Top" <TextBox Grid.Row="0" Grid.Column="0"
BorderThickness="0" BorderThickness="0"
Text="{Binding PropertyFilter}" Text="{Binding TreePage.PropertyFilter}"
Watermark="Filter properties"/> Watermark="Filter properties" />
<DataGrid Items="{Binding PropertiesView}"
BorderThickness="0" <CheckBox Grid.Row="0"
RowBackground="Transparent" Grid.Column="1"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}" Content="Regex"
CanUserResizeColumns="true"> IsChecked="{Binding TreePage.UseRegexFilter}"/>
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True"/> <DataGrid Items="{Binding PropertiesView}"
<DataGridTextColumn Header="Value" Binding="{Binding Value}"/> Grid.Row="1" Grid.ColumnSpan="2"
<DataGridTextColumn Header="Type" Binding="{Binding Type}"/> BorderThickness="0"
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True"/> RowBackground="Transparent"
</DataGrid.Columns> SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
</DataGrid> CanUserResizeColumns="true">
</DockPanel> <DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" />
<DataGridTextColumn Header="Type" Binding="{Binding Type}" />
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
</Grid> </Grid>
</UserControl> </UserControl>

14
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -24,6 +24,20 @@
IsEnabled="False"/> IsEnabled="False"/>
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Header="Visualize dirty rects" Command="{Binding ToggleVisualizeDirtyRects}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShouldVisualizeDirtyRects}"
IsEnabled="False"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Show fps overlay" Command="{Binding ToggleFpsOverlay}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShowFpsOverlay}"
IsEnabled="False"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem> </MenuItem>
</Menu> </Menu>

4
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -155,9 +155,9 @@ namespace Avalonia.Headless
return new List<string> { "Arial" }; return new List<string> { "Arial" };
} }
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out Typeface typeface)
{ {
fontKey = new FontKey("Arial", fontStyle, fontWeight); typeface = new Typeface("Arial", fontStyle, fontWeight);
return true; return true;
} }
} }

22
src/Avalonia.Input/AccessKeyHandler.cs

@ -28,7 +28,7 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// The window to which the handler belongs. /// The window to which the handler belongs.
/// </summary> /// </summary>
private IInputRoot _owner; private IInputRoot? _owner;
/// <summary> /// <summary>
/// Whether access keys are currently being shown; /// Whether access keys are currently being shown;
@ -48,17 +48,17 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Element to restore following AltKey taking focus. /// Element to restore following AltKey taking focus.
/// </summary> /// </summary>
private IInputElement _restoreFocusElement; private IInputElement? _restoreFocusElement;
/// <summary> /// <summary>
/// The window's main menu. /// The window's main menu.
/// </summary> /// </summary>
private IMainMenu _mainMenu; private IMainMenu? _mainMenu;
/// <summary> /// <summary>
/// Gets or sets the window's main menu. /// Gets or sets the window's main menu.
/// </summary> /// </summary>
public IMainMenu MainMenu public IMainMenu? MainMenu
{ {
get => _mainMenu; get => _mainMenu;
set set
@ -86,14 +86,12 @@ namespace Avalonia.Input
/// </remarks> /// </remarks>
public void SetOwner(IInputRoot owner) public void SetOwner(IInputRoot owner)
{ {
Contract.Requires<ArgumentNullException>(owner != null);
if (_owner != null) if (_owner != null)
{ {
throw new InvalidOperationException("AccessKeyHandler owner has already been set."); throw new InvalidOperationException("AccessKeyHandler owner has already been set.");
} }
_owner = owner; _owner = owner ?? throw new ArgumentNullException(nameof(owner));
_owner.AddHandler(InputElement.KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel); _owner.AddHandler(InputElement.KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel);
_owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown, RoutingStrategies.Bubble); _owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown, RoutingStrategies.Bubble);
@ -149,7 +147,7 @@ namespace Avalonia.Input
// When Alt is pressed without a main menu, or with a closed main menu, show // When Alt is pressed without a main menu, or with a closed main menu, show
// access key markers in the window (i.e. "_File"). // access key markers in the window (i.e. "_File").
_owner.ShowAccessKeys = _showingAccessKeys = true; _owner!.ShowAccessKeys = _showingAccessKeys = true;
} }
else else
{ {
@ -241,7 +239,7 @@ namespace Avalonia.Input
{ {
if (_showingAccessKeys) if (_showingAccessKeys)
{ {
_owner.ShowAccessKeys = false; _owner!.ShowAccessKeys = false;
} }
} }
@ -250,13 +248,13 @@ namespace Avalonia.Input
/// </summary> /// </summary>
private void CloseMenu() private void CloseMenu()
{ {
MainMenu.Close(); MainMenu!.Close();
_owner.ShowAccessKeys = _showingAccessKeys = false; _owner!.ShowAccessKeys = _showingAccessKeys = false;
} }
private void MainMenuClosed(object sender, EventArgs e) private void MainMenuClosed(object sender, EventArgs e)
{ {
_owner.ShowAccessKeys = false; _owner!.ShowAccessKeys = false;
} }
} }
} }

2
src/Avalonia.Input/Avalonia.Input.csproj

@ -1,6 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<Nullable>Enable</Nullable>
<WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" /> <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

6
src/Avalonia.Input/DataObject.cs

@ -11,7 +11,7 @@ namespace Avalonia.Input
return _items.ContainsKey(dataFormat); return _items.ContainsKey(dataFormat);
} }
public object Get(string dataFormat) public object? Get(string dataFormat)
{ {
if (_items.ContainsKey(dataFormat)) if (_items.ContainsKey(dataFormat))
return _items[dataFormat]; return _items[dataFormat];
@ -23,12 +23,12 @@ namespace Avalonia.Input
return _items.Keys; return _items.Keys;
} }
public IEnumerable<string> GetFileNames() public IEnumerable<string>? GetFileNames()
{ {
return Get(DataFormats.FileNames) as IEnumerable<string>; return Get(DataFormats.FileNames) as IEnumerable<string>;
} }
public string GetText() public string? GetText()
{ {
return Get(DataFormats.Text) as string; return Get(DataFormats.Text) as string;
} }

6
src/Avalonia.Input/DragDropDevice.cs

@ -9,9 +9,9 @@ namespace Avalonia.Input
{ {
public static readonly DragDropDevice Instance = new DragDropDevice(); public static readonly DragDropDevice Instance = new DragDropDevice();
private Interactive _lastTarget = null; private Interactive? _lastTarget = null;
private Interactive GetTarget(IInputRoot root, Point local) private Interactive? GetTarget(IInputRoot root, Point local)
{ {
var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType<Interactive>()?.FirstOrDefault(); var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType<Interactive>()?.FirstOrDefault();
if (target != null && DragDrop.GetAllowDrop(target)) if (target != null && DragDrop.GetAllowDrop(target))
@ -19,7 +19,7 @@ namespace Avalonia.Input
return null; return null;
} }
private DragDropEffects RaiseDragEvent(Interactive target, IInputRoot inputRoot, Point point, RoutedEvent<DragEventArgs> routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers) private DragDropEffects RaiseDragEvent(Interactive? target, IInputRoot inputRoot, Point point, RoutedEvent<DragEventArgs> routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers)
{ {
if (target == null) if (target == null)
return DragDropEffects.None; return DragDropEffects.None;

47
src/Avalonia.Input/FocusManager.cs

@ -15,8 +15,8 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// The focus scopes in which the focus is currently defined. /// The focus scopes in which the focus is currently defined.
/// </summary> /// </summary>
private readonly ConditionalWeakTable<IFocusScope, IInputElement> _focusScopes = private readonly ConditionalWeakTable<IFocusScope, IInputElement?> _focusScopes =
new ConditionalWeakTable<IFocusScope, IInputElement>(); new ConditionalWeakTable<IFocusScope, IInputElement?>();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FocusManager"/> class. /// Initializes a new instance of the <see cref="FocusManager"/> class.
@ -37,12 +37,12 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Gets the currently focused <see cref="IInputElement"/>. /// Gets the currently focused <see cref="IInputElement"/>.
/// </summary> /// </summary>
public IInputElement Current => KeyboardDevice.Instance?.FocusedElement; public IInputElement? Current => KeyboardDevice.Instance?.FocusedElement;
/// <summary> /// <summary>
/// Gets the current focus scope. /// Gets the current focus scope.
/// </summary> /// </summary>
public IFocusScope Scope public IFocusScope? Scope
{ {
get; get;
private set; private set;
@ -55,7 +55,7 @@ namespace Avalonia.Input
/// <param name="method">The method by which focus was changed.</param> /// <param name="method">The method by which focus was changed.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param> /// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
public void Focus( public void Focus(
IInputElement control, IInputElement? control,
NavigationMethod method = NavigationMethod.Unspecified, NavigationMethod method = NavigationMethod.Unspecified,
KeyModifiers keyModifiers = KeyModifiers.None) KeyModifiers keyModifiers = KeyModifiers.None)
{ {
@ -75,17 +75,18 @@ namespace Avalonia.Input
// If control is null, set focus to the topmost focus scope. // If control is null, set focus to the topmost focus scope.
foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList()) foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList())
{ {
IInputElement element; if (_focusScopes.TryGetValue(scope, out var element) && element != null)
if (_focusScopes.TryGetValue(scope, out element) && element != null)
{ {
Focus(element, method); Focus(element, method);
return; return;
} }
} }
// Couldn't find a focus scope, clear focus. if (Scope is object)
SetFocusedElement(Scope, null); {
// Couldn't find a focus scope, clear focus.
SetFocusedElement(Scope, null);
}
} }
} }
@ -102,13 +103,13 @@ namespace Avalonia.Input
/// </remarks> /// </remarks>
public void SetFocusedElement( public void SetFocusedElement(
IFocusScope scope, IFocusScope scope,
IInputElement element, IInputElement? element,
NavigationMethod method = NavigationMethod.Unspecified, NavigationMethod method = NavigationMethod.Unspecified,
KeyModifiers keyModifiers = KeyModifiers.None) KeyModifiers keyModifiers = KeyModifiers.None)
{ {
Contract.Requires<ArgumentNullException>(scope != null); scope = scope ?? throw new ArgumentNullException(nameof(scope));
if (_focusScopes.TryGetValue(scope, out IInputElement existingElement)) if (_focusScopes.TryGetValue(scope, out var existingElement))
{ {
if (element != existingElement) if (element != existingElement)
{ {
@ -133,11 +134,9 @@ namespace Avalonia.Input
/// <param name="scope">The new focus scope.</param> /// <param name="scope">The new focus scope.</param>
public void SetFocusScope(IFocusScope scope) public void SetFocusScope(IFocusScope scope)
{ {
Contract.Requires<ArgumentNullException>(scope != null); scope = scope ?? throw new ArgumentNullException(nameof(scope));
IInputElement e; if (!_focusScopes.TryGetValue(scope, out var e))
if (!_focusScopes.TryGetValue(scope, out e))
{ {
// TODO: Make this do something useful, i.e. select the first focusable // TODO: Make this do something useful, i.e. select the first focusable
// control, select a control that the user has specified to have default // control, select a control that the user has specified to have default
@ -164,17 +163,19 @@ namespace Avalonia.Input
/// <returns>The focus scopes.</returns> /// <returns>The focus scopes.</returns>
private static IEnumerable<IFocusScope> GetFocusScopeAncestors(IInputElement control) private static IEnumerable<IFocusScope> GetFocusScopeAncestors(IInputElement control)
{ {
while (control != null) IInputElement? c = control;
while (c != null)
{ {
var scope = control as IFocusScope; var scope = c as IFocusScope;
if (scope != null && control.VisualRoot?.IsVisible == true) if (scope != null && c.VisualRoot?.IsVisible == true)
{ {
yield return scope; yield return scope;
} }
control = control.GetVisualParent<IInputElement>() ?? c = c.GetVisualParent<IInputElement>() ??
((control as IHostedVisualTreeRoot)?.Host as IInputElement); ((c as IHostedVisualTreeRoot)?.Host as IInputElement);
} }
} }
@ -190,7 +191,7 @@ namespace Avalonia.Input
if (sender == e.Source && ev.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) if (sender == e.Source && ev.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
{ {
IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement; IVisual? element = ev.Pointer?.Captured ?? e.Source as IInputElement;
while (element != null) while (element != null)
{ {

16
src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs

@ -1,8 +1,6 @@
using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Styling; using Avalonia.Styling;
@ -11,8 +9,8 @@ namespace Avalonia.Input.GestureRecognizers
public class GestureRecognizerCollection : IReadOnlyCollection<IGestureRecognizer>, IGestureRecognizerActionsDispatcher public class GestureRecognizerCollection : IReadOnlyCollection<IGestureRecognizer>, IGestureRecognizerActionsDispatcher
{ {
private readonly IInputElement _inputElement; private readonly IInputElement _inputElement;
private List<IGestureRecognizer> _recognizers; private List<IGestureRecognizer>? _recognizers;
private Dictionary<IPointer, IGestureRecognizer> _pointerGrabs; private Dictionary<IPointer, IGestureRecognizer>? _pointerGrabs;
public GestureRecognizerCollection(IInputElement inputElement) public GestureRecognizerCollection(IInputElement inputElement)
@ -72,7 +70,7 @@ namespace Avalonia.Input.GestureRecognizers
{ {
if (_recognizers == null) if (_recognizers == null)
return false; return false;
if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) if (_pointerGrabs!.TryGetValue(e.Pointer, out var capture))
{ {
capture.PointerReleased(e); capture.PointerReleased(e);
} }
@ -90,7 +88,7 @@ namespace Avalonia.Input.GestureRecognizers
{ {
if (_recognizers == null) if (_recognizers == null)
return false; return false;
if (_pointerGrabs.TryGetValue(e.Pointer, out var capture)) if (_pointerGrabs!.TryGetValue(e.Pointer, out var capture))
{ {
capture.PointerMoved(e); capture.PointerMoved(e);
} }
@ -108,7 +106,7 @@ namespace Avalonia.Input.GestureRecognizers
{ {
if (_recognizers == null) if (_recognizers == null)
return; return;
_pointerGrabs.Remove(e.Pointer); _pointerGrabs!.Remove(e.Pointer);
foreach (var r in _recognizers) foreach (var r in _recognizers)
{ {
r.PointerCaptureLost(e.Pointer); r.PointerCaptureLost(e.Pointer);
@ -118,8 +116,8 @@ namespace Avalonia.Input.GestureRecognizers
void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer) void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer)
{ {
pointer.Capture(_inputElement); pointer.Capture(_inputElement);
_pointerGrabs[pointer] = recognizer; _pointerGrabs![pointer] = recognizer;
foreach (var r in _recognizers) foreach (var r in _recognizers!)
{ {
if (r != recognizer) if (r != recognizer)
r.PointerCaptureLost(pointer); r.PointerCaptureLost(pointer);

15
src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Interactivity;
using Avalonia.Threading; using Avalonia.Threading;
namespace Avalonia.Input.GestureRecognizers namespace Avalonia.Input.GestureRecognizers
@ -11,9 +10,9 @@ namespace Avalonia.Input.GestureRecognizers
{ {
private bool _scrolling; private bool _scrolling;
private Point _trackedRootPoint; private Point _trackedRootPoint;
private IPointer _tracking; private IPointer? _tracking;
private IInputElement _target; private IInputElement? _target;
private IGestureRecognizerActionsDispatcher _actions; private IGestureRecognizerActionsDispatcher? _actions;
private bool _canHorizontallyScroll; private bool _canHorizontallyScroll;
private bool _canVerticallyScroll; private bool _canVerticallyScroll;
private int _gestureId; private int _gestureId;
@ -95,7 +94,7 @@ namespace Avalonia.Input.GestureRecognizers
_scrolling = true; _scrolling = true;
if (_scrolling) if (_scrolling)
{ {
_actions.Capture(e.Pointer, this); _actions!.Capture(e.Pointer, this);
} }
} }
@ -110,7 +109,7 @@ namespace Avalonia.Input.GestureRecognizers
_trackedRootPoint = rootPoint; _trackedRootPoint = rootPoint;
if (elapsed.TotalSeconds > 0) if (elapsed.TotalSeconds > 0)
_inertia = vector / elapsed.TotalSeconds; _inertia = vector / elapsed.TotalSeconds;
_target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector)); _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
e.Handled = true; e.Handled = true;
} }
} }
@ -128,7 +127,7 @@ namespace Avalonia.Input.GestureRecognizers
{ {
_inertia = default; _inertia = default;
_scrolling = false; _scrolling = false;
_target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId)); _target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
_gestureId = 0; _gestureId = 0;
_lastMoveTimestamp = null; _lastMoveTimestamp = null;
} }
@ -165,7 +164,7 @@ namespace Avalonia.Input.GestureRecognizers
var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds);
var distance = speed * elapsedSinceLastTick.TotalSeconds; var distance = speed * elapsedSinceLastTick.TotalSeconds;
_target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance)); _target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance));

9
src/Avalonia.Input/Gestures.cs

@ -29,7 +29,9 @@ namespace Avalonia.Input
RoutedEvent.Register<ScrollGestureEventArgs>( RoutedEvent.Register<ScrollGestureEventArgs>(
"ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
private static WeakReference<IInteractive> s_lastPress = new WeakReference<IInteractive>(null); private static WeakReference<IInteractive> s_lastPress = new WeakReference<IInteractive>(null);
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
static Gestures() static Gestures()
{ {
@ -69,6 +71,11 @@ namespace Avalonia.Input
private static void PointerPressed(RoutedEventArgs ev) private static void PointerPressed(RoutedEventArgs ev)
{ {
if (ev.Source is null)
{
return;
}
if (ev.Route == RoutingStrategies.Bubble) if (ev.Route == RoutingStrategies.Bubble)
{ {
var e = (PointerPressedEventArgs)ev; var e = (PointerPressedEventArgs)ev;
@ -76,7 +83,7 @@ namespace Avalonia.Input
if (e.ClickCount <= 1) if (e.ClickCount <= 1)
{ {
s_lastPress = new WeakReference<IInteractive>(e.Source); s_lastPress = new WeakReference<IInteractive>(ev.Source);
} }
else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
{ {

2
src/Avalonia.Input/IAccessKeyHandler.cs

@ -8,7 +8,7 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Gets or sets the window's main menu. /// Gets or sets the window's main menu.
/// </summary> /// </summary>
IMainMenu MainMenu { get; set; } IMainMenu? MainMenu { get; set; }
/// <summary> /// <summary>
/// Sets the owner of the access key handler. /// Sets the owner of the access key handler.

6
src/Avalonia.Input/IDataObject.cs

@ -23,17 +23,17 @@ namespace Avalonia.Input
/// Returns the dragged text if the DataObject contains any text. /// Returns the dragged text if the DataObject contains any text.
/// <seealso cref="DataFormats.Text"/> /// <seealso cref="DataFormats.Text"/>
/// </summary> /// </summary>
string GetText(); string? GetText();
/// <summary> /// <summary>
/// Returns a list of filenames if the DataObject contains filenames. /// Returns a list of filenames if the DataObject contains filenames.
/// <seealso cref="DataFormats.FileNames"/> /// <seealso cref="DataFormats.FileNames"/>
/// </summary> /// </summary>
IEnumerable<string> GetFileNames(); IEnumerable<string>? GetFileNames();
/// <summary> /// <summary>
/// Tries to get the data of the given DataFormat. /// Tries to get the data of the given DataFormat.
/// </summary> /// </summary>
object Get(string dataFormat); object? Get(string dataFormat);
} }
} }

6
src/Avalonia.Input/IFocusManager.cs

@ -8,12 +8,12 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Gets the currently focused <see cref="IInputElement"/>. /// Gets the currently focused <see cref="IInputElement"/>.
/// </summary> /// </summary>
IInputElement Current { get; } IInputElement? Current { get; }
/// <summary> /// <summary>
/// Gets the current focus scope. /// Gets the current focus scope.
/// </summary> /// </summary>
IFocusScope Scope { get; } IFocusScope? Scope { get; }
/// <summary> /// <summary>
/// Focuses a control. /// Focuses a control.
@ -22,7 +22,7 @@ namespace Avalonia.Input
/// <param name="method">The method by which focus was changed.</param> /// <param name="method">The method by which focus was changed.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param> /// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
void Focus( void Focus(
IInputElement control, IInputElement? control,
NavigationMethod method = NavigationMethod.Unspecified, NavigationMethod method = NavigationMethod.Unspecified,
KeyModifiers keyModifiers = KeyModifiers.None); KeyModifiers keyModifiers = KeyModifiers.None);

2
src/Avalonia.Input/IInputElement.cs

@ -78,7 +78,7 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Gets or sets the associated mouse cursor. /// Gets or sets the associated mouse cursor.
/// </summary> /// </summary>
Cursor Cursor { get; } Cursor? Cursor { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether this control and all its parents are enabled. /// Gets a value indicating whether this control and all its parents are enabled.

4
src/Avalonia.Input/IInputRoot.cs

@ -20,7 +20,7 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Gets or sets the input element that the pointer is currently over. /// Gets or sets the input element that the pointer is currently over.
/// </summary> /// </summary>
IInputElement PointerOverElement { get; set; } IInputElement? PointerOverElement { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether access keys are shown in the window. /// Gets or sets a value indicating whether access keys are shown in the window.
@ -31,6 +31,6 @@ namespace Avalonia.Input
/// Gets associated mouse device /// Gets associated mouse device
/// </summary> /// </summary>
[CanBeNull] [CanBeNull]
IMouseDevice MouseDevice { get; } IMouseDevice? MouseDevice { get; }
} }
} }

4
src/Avalonia.Input/IKeyboardDevice.cs

@ -58,10 +58,10 @@ namespace Avalonia.Input
public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged
{ {
IInputElement FocusedElement { get; } IInputElement? FocusedElement { get; }
void SetFocusedElement( void SetFocusedElement(
IInputElement element, IInputElement? element,
NavigationMethod method, NavigationMethod method,
KeyModifiers modifiers); KeyModifiers modifiers);
} }

4
src/Avalonia.Input/IPointer.cs

@ -3,8 +3,8 @@ namespace Avalonia.Input
public interface IPointer public interface IPointer
{ {
int Id { get; } int Id { get; }
void Capture(IInputElement control); void Capture(IInputElement? control);
IInputElement Captured { get; } IInputElement? Captured { get; }
PointerType Type { get; } PointerType Type { get; }
bool IsPrimary { get; } bool IsPrimary { get; }

4
src/Avalonia.Input/IPointerDevice.cs

@ -6,10 +6,10 @@ namespace Avalonia.Input
public interface IPointerDevice : IInputDevice public interface IPointerDevice : IInputDevice
{ {
[Obsolete("Use IPointer")] [Obsolete("Use IPointer")]
IInputElement Captured { get; } IInputElement? Captured { get; }
[Obsolete("Use IPointer")] [Obsolete("Use IPointer")]
void Capture(IInputElement control); void Capture(IInputElement? control);
[Obsolete("Use PointerEventArgs.GetPosition")] [Obsolete("Use PointerEventArgs.GetPosition")]
Point GetPosition(IVisual relativeTo); Point GetPosition(IVisual relativeTo);

8
src/Avalonia.Input/InputElement.cs

@ -37,8 +37,8 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Gets or sets associated mouse cursor. /// Gets or sets associated mouse cursor.
/// </summary> /// </summary>
public static readonly StyledProperty<Cursor> CursorProperty = public static readonly StyledProperty<Cursor?> CursorProperty =
AvaloniaProperty.Register<InputElement, Cursor>(nameof(Cursor), null, true); AvaloniaProperty.Register<InputElement, Cursor?>(nameof(Cursor), null, true);
/// <summary> /// <summary>
/// Defines the <see cref="IsFocused"/> property. /// Defines the <see cref="IsFocused"/> property.
@ -160,7 +160,7 @@ namespace Avalonia.Input
private bool _isFocused; private bool _isFocused;
private bool _isFocusVisible; private bool _isFocusVisible;
private bool _isPointerOver; private bool _isPointerOver;
private GestureRecognizerCollection _gestureRecognizers; private GestureRecognizerCollection? _gestureRecognizers;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="InputElement"/> class. /// Initializes static members of the <see cref="InputElement"/> class.
@ -336,7 +336,7 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Gets or sets associated mouse cursor. /// Gets or sets associated mouse cursor.
/// </summary> /// </summary>
public Cursor Cursor public Cursor? Cursor
{ {
get { return GetValue(CursorProperty); } get { return GetValue(CursorProperty); }
set { SetValue(CursorProperty, value); } set { SetValue(CursorProperty, value); }

2
src/Avalonia.Input/KeyEventArgs.cs

@ -5,7 +5,7 @@ namespace Avalonia.Input
{ {
public class KeyEventArgs : RoutedEventArgs public class KeyEventArgs : RoutedEventArgs
{ {
public IKeyboardDevice Device { get; set; } public IKeyboardDevice? Device { get; set; }
public Key Key { get; set; } public Key Key { get; set; }

8
src/Avalonia.Input/KeyboardDevice.cs

@ -8,9 +8,9 @@ namespace Avalonia.Input
{ {
public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged public class KeyboardDevice : IKeyboardDevice, INotifyPropertyChanged
{ {
private IInputElement _focusedElement; private IInputElement? _focusedElement;
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
public static IKeyboardDevice Instance => AvaloniaLocator.Current.GetService<IKeyboardDevice>(); public static IKeyboardDevice Instance => AvaloniaLocator.Current.GetService<IKeyboardDevice>();
@ -18,7 +18,7 @@ namespace Avalonia.Input
public IFocusManager FocusManager => AvaloniaLocator.Current.GetService<IFocusManager>(); public IFocusManager FocusManager => AvaloniaLocator.Current.GetService<IFocusManager>();
public IInputElement FocusedElement public IInputElement? FocusedElement
{ {
get get
{ {
@ -33,7 +33,7 @@ namespace Avalonia.Input
} }
public void SetFocusedElement( public void SetFocusedElement(
IInputElement element, IInputElement? element,
NavigationMethod method, NavigationMethod method,
KeyModifiers keyModifiers) KeyModifiers keyModifiers)
{ {

13
src/Avalonia.Input/KeyboardNavigationHandler.cs

@ -13,7 +13,7 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// The window to which the handler belongs. /// The window to which the handler belongs.
/// </summary> /// </summary>
private IInputRoot _owner; private IInputRoot? _owner;
/// <summary> /// <summary>
/// Sets the owner of the keyboard navigation handler. /// Sets the owner of the keyboard navigation handler.
@ -24,15 +24,12 @@ namespace Avalonia.Input
/// </remarks> /// </remarks>
public void SetOwner(IInputRoot owner) public void SetOwner(IInputRoot owner)
{ {
Contract.Requires<ArgumentNullException>(owner != null);
if (_owner != null) if (_owner != null)
{ {
throw new InvalidOperationException("AccessKeyHandler owner has already been set."); throw new InvalidOperationException("AccessKeyHandler owner has already been set.");
} }
_owner = owner; _owner = owner ?? throw new ArgumentNullException(nameof(owner));
_owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown); _owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown);
} }
@ -45,11 +42,11 @@ namespace Avalonia.Input
/// The next element in the specified direction, or null if <paramref name="element"/> /// The next element in the specified direction, or null if <paramref name="element"/>
/// was the last in the requested direction. /// was the last in the requested direction.
/// </returns> /// </returns>
public static IInputElement GetNext( public static IInputElement? GetNext(
IInputElement element, IInputElement element,
NavigationDirection direction) NavigationDirection direction)
{ {
Contract.Requires<ArgumentNullException>(element != null); element = element ?? throw new ArgumentNullException(nameof(element));
var customHandler = element.GetSelfAndVisualAncestors() var customHandler = element.GetSelfAndVisualAncestors()
.OfType<ICustomKeyboardNavigation>() .OfType<ICustomKeyboardNavigation>()
@ -97,7 +94,7 @@ namespace Avalonia.Input
NavigationDirection direction, NavigationDirection direction,
KeyModifiers keyModifiers = KeyModifiers.None) KeyModifiers keyModifiers = KeyModifiers.None)
{ {
Contract.Requires<ArgumentNullException>(element != null); element = element ?? throw new ArgumentNullException(nameof(element));
var next = GetNext(element, direction); var next = GetNext(element, direction);

73
src/Avalonia.Input/MouseDevice.cs

@ -20,7 +20,7 @@ namespace Avalonia.Input
private readonly Pointer _pointer; private readonly Pointer _pointer;
private bool _disposed; private bool _disposed;
public MouseDevice(Pointer pointer = null) public MouseDevice(Pointer? pointer = null)
{ {
_pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
} }
@ -34,7 +34,7 @@ namespace Avalonia.Input
/// <see cref="Capture"/> method. /// <see cref="Capture"/> method.
/// </remarks> /// </remarks>
[Obsolete("Use IPointer instead")] [Obsolete("Use IPointer instead")]
public IInputElement Captured => _pointer.Captured; public IInputElement? Captured => _pointer.Captured;
/// <summary> /// <summary>
/// Gets the mouse position, in screen coordinates. /// Gets the mouse position, in screen coordinates.
@ -54,7 +54,7 @@ namespace Avalonia.Input
/// within the control's bounds or not. The current mouse capture control is exposed /// within the control's bounds or not. The current mouse capture control is exposed
/// by the <see cref="Captured"/> property. /// by the <see cref="Captured"/> property.
/// </remarks> /// </remarks>
public void Capture(IInputElement control) public void Capture(IInputElement? control)
{ {
_pointer.Capture(control); _pointer.Capture(control);
} }
@ -66,7 +66,7 @@ namespace Avalonia.Input
/// <returns>The mouse position in the control's coordinates.</returns> /// <returns>The mouse position in the control's coordinates.</returns>
public Point GetPosition(IVisual relativeTo) public Point GetPosition(IVisual relativeTo)
{ {
Contract.Requires<ArgumentNullException>(relativeTo != null); relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo));
if (relativeTo.VisualRoot == null) if (relativeTo.VisualRoot == null)
{ {
@ -75,7 +75,7 @@ namespace Avalonia.Input
var rootPoint = relativeTo.VisualRoot.PointToClient(Position); var rootPoint = relativeTo.VisualRoot.PointToClient(Position);
var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo);
return rootPoint * transform.Value; return rootPoint * transform!.Value;
} }
public void ProcessRawEvent(RawInputEventArgs e) public void ProcessRawEvent(RawInputEventArgs e)
@ -126,7 +126,7 @@ namespace Avalonia.Input
private void ProcessRawEvent(RawPointerEventArgs e) private void ProcessRawEvent(RawPointerEventArgs e)
{ {
Contract.Requires<ArgumentNullException>(e != null); e = e ?? throw new ArgumentNullException(nameof(e));
var mouse = (MouseDevice)e.Device; var mouse = (MouseDevice)e.Device;
if(mouse._disposed) if(mouse._disposed)
@ -173,8 +173,8 @@ namespace Avalonia.Input
private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties,
KeyModifiers inputModifiers) KeyModifiers inputModifiers)
{ {
Contract.Requires<ArgumentNullException>(device != null); device = device ?? throw new ArgumentNullException(nameof(device));
Contract.Requires<ArgumentNullException>(root != null); root = root ?? throw new ArgumentNullException(nameof(root));
ClearPointerOver(this, timestamp, root, properties, inputModifiers); ClearPointerOver(this, timestamp, root, properties, inputModifiers);
} }
@ -214,8 +214,8 @@ namespace Avalonia.Input
PointerPointProperties properties, PointerPointProperties properties,
KeyModifiers inputModifiers) KeyModifiers inputModifiers)
{ {
Contract.Requires<ArgumentNullException>(device != null); device = device ?? throw new ArgumentNullException(nameof(device));
Contract.Requires<ArgumentNullException>(root != null); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var hit = HitTest(root, p);
@ -250,10 +250,10 @@ namespace Avalonia.Input
private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties,
KeyModifiers inputModifiers) KeyModifiers inputModifiers)
{ {
Contract.Requires<ArgumentNullException>(device != null); device = device ?? throw new ArgumentNullException(nameof(device));
Contract.Requires<ArgumentNullException>(root != null); root = root ?? throw new ArgumentNullException(nameof(root));
IInputElement source; IInputElement? source;
if (_pointer.Captured == null) if (_pointer.Captured == null)
{ {
@ -265,18 +265,23 @@ namespace Avalonia.Input
source = _pointer.Captured; source = _pointer.Captured;
} }
var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root, if (source is object)
p, timestamp, properties, inputModifiers); {
var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root,
p, timestamp, properties, inputModifiers);
source?.RaiseEvent(e); source.RaiseEvent(e);
return e.Handled; return e.Handled;
}
return false;
} }
private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props,
KeyModifiers inputModifiers) KeyModifiers inputModifiers)
{ {
Contract.Requires<ArgumentNullException>(device != null); device = device ?? throw new ArgumentNullException(nameof(device));
Contract.Requires<ArgumentNullException>(root != null); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var hit = HitTest(root, p);
@ -298,8 +303,8 @@ namespace Avalonia.Input
PointerPointProperties props, PointerPointProperties props,
Vector delta, KeyModifiers inputModifiers) Vector delta, KeyModifiers inputModifiers)
{ {
Contract.Requires<ArgumentNullException>(device != null); device = device ?? throw new ArgumentNullException(nameof(device));
Contract.Requires<ArgumentNullException>(root != null); root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p); var hit = HitTest(root, p);
@ -317,21 +322,21 @@ namespace Avalonia.Input
private IInteractive GetSource(IVisual hit) private IInteractive GetSource(IVisual hit)
{ {
Contract.Requires<ArgumentNullException>(hit != null); hit = hit ?? throw new ArgumentNullException(nameof(hit));
return _pointer.Captured ?? return _pointer.Captured ??
(hit as IInteractive) ?? (hit as IInteractive) ??
hit.GetSelfAndVisualAncestors().OfType<IInteractive>().FirstOrDefault(); hit.GetSelfAndVisualAncestors().OfType<IInteractive>().FirstOrDefault();
} }
private IInputElement HitTest(IInputElement root, Point p) private IInputElement? HitTest(IInputElement root, Point p)
{ {
Contract.Requires<ArgumentNullException>(root != null); root = root ?? throw new ArgumentNullException(nameof(root));
return _pointer.Captured ?? root.InputHitTest(p); return _pointer.Captured ?? root.InputHitTest(p);
} }
PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source,
PointerPointProperties properties, PointerPointProperties properties,
KeyModifiers inputModifiers) KeyModifiers inputModifiers)
{ {
@ -343,8 +348,8 @@ namespace Avalonia.Input
PointerPointProperties properties, PointerPointProperties properties,
KeyModifiers inputModifiers) KeyModifiers inputModifiers)
{ {
Contract.Requires<ArgumentNullException>(device != null); device = device ?? throw new ArgumentNullException(nameof(device));
Contract.Requires<ArgumentNullException>(root != null); root = root ?? throw new ArgumentNullException(nameof(root));
var element = root.PointerOverElement; var element = root.PointerOverElement;
var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers);
@ -384,12 +389,12 @@ namespace Avalonia.Input
} }
} }
private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties properties, PointerPointProperties properties,
KeyModifiers inputModifiers) KeyModifiers inputModifiers)
{ {
Contract.Requires<ArgumentNullException>(device != null); device = device ?? throw new ArgumentNullException(nameof(device));
Contract.Requires<ArgumentNullException>(root != null); root = root ?? throw new ArgumentNullException(nameof(root));
var element = root.InputHitTest(p); var element = root.InputHitTest(p);
@ -412,11 +417,11 @@ namespace Avalonia.Input
PointerPointProperties properties, PointerPointProperties properties,
KeyModifiers inputModifiers) KeyModifiers inputModifiers)
{ {
Contract.Requires<ArgumentNullException>(device != null); device = device ?? throw new ArgumentNullException(nameof(device));
Contract.Requires<ArgumentNullException>(root != null); root = root ?? throw new ArgumentNullException(nameof(root));
Contract.Requires<ArgumentNullException>(element != null); element = element ?? throw new ArgumentNullException(nameof(element));
IInputElement branch = null; IInputElement? branch = null;
var el = element; var el = element;

42
src/Avalonia.Input/Navigation/TabNavigation.cs

@ -22,15 +22,17 @@ namespace Avalonia.Input.Navigation
/// The next element in the specified direction, or null if <paramref name="element"/> /// The next element in the specified direction, or null if <paramref name="element"/>
/// was the last in the requested direction. /// was the last in the requested direction.
/// </returns> /// </returns>
public static IInputElement GetNextInTabOrder( public static IInputElement? GetNextInTabOrder(
IInputElement element, IInputElement element,
NavigationDirection direction, NavigationDirection direction,
bool outsideElement = false) bool outsideElement = false)
{ {
Contract.Requires<ArgumentNullException>(element != null); element = element ?? throw new ArgumentNullException(nameof(element));
Contract.Requires<ArgumentException>(
direction == NavigationDirection.Next || if (direction != NavigationDirection.Next && direction != NavigationDirection.Previous)
direction == NavigationDirection.Previous); {
throw new ArgumentException("Invalid direction: must be Next or Previous.");
}
var container = element.GetVisualParent<IInputElement>(); var container = element.GetVisualParent<IInputElement>();
@ -110,7 +112,7 @@ namespace Avalonia.Input.Navigation
if (customNext.handled) if (customNext.handled)
{ {
yield return customNext.next; yield return customNext.next!;
} }
else else
{ {
@ -143,12 +145,14 @@ namespace Avalonia.Input.Navigation
/// If true will not descend into <paramref name="element"/> to find next control. /// If true will not descend into <paramref name="element"/> to find next control.
/// </param> /// </param>
/// <returns>The next element, or null if the element is the last.</returns> /// <returns>The next element, or null if the element is the last.</returns>
private static IInputElement GetNextInContainer( private static IInputElement? GetNextInContainer(
IInputElement element, IInputElement element,
IInputElement container, IInputElement container,
NavigationDirection direction, NavigationDirection direction,
bool outsideElement) bool outsideElement)
{ {
IInputElement? e = element;
if (direction == NavigationDirection.Next && !outsideElement) if (direction == NavigationDirection.Next && !outsideElement)
{ {
var descendant = GetFocusableDescendants(element, direction).FirstOrDefault(); var descendant = GetFocusableDescendants(element, direction).FirstOrDefault();
@ -167,13 +171,13 @@ namespace Avalonia.Input.Navigation
// INavigableContainer. // INavigableContainer.
if (navigable != null) if (navigable != null)
{ {
while (element != null) while (e != null)
{ {
element = navigable.GetControl(direction, element, false); e = navigable.GetControl(direction, e, false);
if (element != null && if (e != null &&
element.CanFocus() && e.CanFocus() &&
KeyboardNavigation.GetIsTabStop((InputElement) element)) KeyboardNavigation.GetIsTabStop((InputElement)e))
{ {
break; break;
} }
@ -183,12 +187,12 @@ namespace Avalonia.Input.Navigation
{ {
// TODO: Do a spatial search here if the container doesn't implement // TODO: Do a spatial search here if the container doesn't implement
// INavigableContainer. // INavigableContainer.
element = null; e = null;
} }
if (element != null && direction == NavigationDirection.Previous) if (e != null && direction == NavigationDirection.Previous)
{ {
var descendant = GetFocusableDescendants(element, direction).LastOrDefault(); var descendant = GetFocusableDescendants(e, direction).LastOrDefault();
if (descendant != null) if (descendant != null)
{ {
@ -196,7 +200,7 @@ namespace Avalonia.Input.Navigation
} }
} }
return element; return e;
} }
return null; return null;
@ -209,13 +213,13 @@ namespace Avalonia.Input.Navigation
/// <param name="container">The container.</param> /// <param name="container">The container.</param>
/// <param name="direction">The direction of the search.</param> /// <param name="direction">The direction of the search.</param>
/// <returns>The first element, or null if there are no more elements.</returns> /// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement GetFirstInNextContainer( private static IInputElement? GetFirstInNextContainer(
IInputElement element, IInputElement element,
IInputElement container, IInputElement container,
NavigationDirection direction) NavigationDirection direction)
{ {
var parent = container.GetVisualParent<IInputElement>(); var parent = container.GetVisualParent<IInputElement>();
IInputElement next = null; IInputElement? next = null;
if (parent != null) if (parent != null)
{ {
@ -268,7 +272,7 @@ namespace Avalonia.Input.Navigation
return next; return next;
} }
private static (bool handled, IInputElement next) GetCustomNext(IInputElement element, private static (bool handled, IInputElement? next) GetCustomNext(IInputElement element,
NavigationDirection direction) NavigationDirection direction)
{ {
if (element is ICustomKeyboardNavigation custom) if (element is ICustomKeyboardNavigation custom)

9
src/Avalonia.Input/Pointer.cs

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Interactivity;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
@ -20,7 +19,7 @@ namespace Avalonia.Input
public int Id { get; } public int Id { get; }
IInputElement FindCommonParent(IInputElement control1, IInputElement control2) IInputElement? FindCommonParent(IInputElement? control1, IInputElement? control2)
{ {
if (control1 == null || control2 == null) if (control1 == null || control2 == null)
return null; return null;
@ -28,12 +27,12 @@ namespace Avalonia.Input
return control2.GetSelfAndVisualAncestors().OfType<IInputElement>().FirstOrDefault(seen.Contains); return control2.GetSelfAndVisualAncestors().OfType<IInputElement>().FirstOrDefault(seen.Contains);
} }
protected virtual void PlatformCapture(IInputElement element) protected virtual void PlatformCapture(IInputElement? element)
{ {
} }
public void Capture(IInputElement control) public void Capture(IInputElement? control)
{ {
if (Captured != null) if (Captured != null)
Captured.DetachedFromVisualTree -= OnCaptureDetached; Captured.DetachedFromVisualTree -= OnCaptureDetached;
@ -66,7 +65,7 @@ namespace Avalonia.Input
} }
public IInputElement Captured { get; private set; } public IInputElement? Captured { get; private set; }
public PointerType Type { get; } public PointerType Type { get; }
public bool IsPrimary { get; } public bool IsPrimary { get; }

14
src/Avalonia.Input/PointerEventArgs.cs

@ -7,14 +7,14 @@ namespace Avalonia.Input
{ {
public class PointerEventArgs : RoutedEventArgs public class PointerEventArgs : RoutedEventArgs
{ {
private readonly IVisual _rootVisual; private readonly IVisual? _rootVisual;
private readonly Point _rootVisualPosition; private readonly Point _rootVisualPosition;
private readonly PointerPointProperties _properties; private readonly PointerPointProperties _properties;
public PointerEventArgs(RoutedEvent routedEvent, public PointerEventArgs(RoutedEvent routedEvent,
IInteractive source, IInteractive? source,
IPointer pointer, IPointer pointer,
IVisual rootVisual, Point rootVisualPosition, IVisual? rootVisual, Point rootVisualPosition,
ulong timestamp, ulong timestamp,
PointerPointProperties properties, PointerPointProperties properties,
KeyModifiers modifiers) KeyModifiers modifiers)
@ -40,8 +40,8 @@ namespace Avalonia.Input
public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException(); public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException();
public IInputElement Captured => _ev.Pointer.Captured; public IInputElement? Captured => _ev.Pointer.Captured;
public void Capture(IInputElement control) public void Capture(IInputElement? control)
{ {
_ev.Pointer.Capture(control); _ev.Pointer.Capture(control);
} }
@ -52,7 +52,7 @@ namespace Avalonia.Input
public IPointer Pointer { get; } public IPointer Pointer { get; }
public ulong Timestamp { get; } public ulong Timestamp { get; }
private IPointerDevice _device; private IPointerDevice? _device;
[Obsolete("Use Pointer to get pointer-specific information")] [Obsolete("Use Pointer to get pointer-specific information")]
public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this)); public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this));
@ -76,7 +76,7 @@ namespace Avalonia.Input
public KeyModifiers KeyModifiers { get; } public KeyModifiers KeyModifiers { get; }
public Point GetPosition(IVisual relativeTo) public Point GetPosition(IVisual? relativeTo)
{ {
if (_rootVisual == null) if (_rootVisual == null)
return default; return default;

2
src/Avalonia.Input/Raw/RawDragEventType.cs

@ -7,4 +7,4 @@
DragLeave, DragLeave,
Drop Drop
} }
} }

2
src/Avalonia.Input/Raw/RawInputEventArgs.cs

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

4
src/Avalonia.Input/TextInputEventArgs.cs

@ -4,8 +4,8 @@ namespace Avalonia.Input
{ {
public class TextInputEventArgs : RoutedEventArgs public class TextInputEventArgs : RoutedEventArgs
{ {
public IKeyboardDevice Device { get; set; } public IKeyboardDevice? Device { get; set; }
public string Text { get; set; } public string? Text { get; set; }
} }
} }

4
src/Avalonia.Layout/Layoutable.cs

@ -758,8 +758,6 @@ namespace Avalonia.Layout
protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e) protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e)
{ {
base.OnDetachedFromVisualTreeCore(e);
if (e.Root is ILayoutRoot r) if (e.Root is ILayoutRoot r)
{ {
if (_layoutUpdated is object) if (_layoutUpdated is object)
@ -772,6 +770,8 @@ namespace Avalonia.Layout
r.LayoutManager.UnregisterEffectiveViewportListener(this); r.LayoutManager.UnregisterEffectiveViewportListener(this);
} }
} }
base.OnDetachedFromVisualTreeCore(e);
} }
/// <summary> /// <summary>

31
src/Avalonia.OpenGL/Angle/AngleEglInterface.cs

@ -0,0 +1,31 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
namespace Avalonia.OpenGL.Angle
{
public class AngleEglInterface : EglInterface
{
[DllImport("libegl.dll", CharSet = CharSet.Ansi)]
static extern IntPtr eglGetProcAddress(string proc);
public AngleEglInterface() : base(LoadAngle())
{
}
static Func<string, IntPtr> LoadAngle()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new PlatformNotSupportedException();
{
var disp = eglGetProcAddress("eglGetPlatformDisplayEXT");
if (disp == IntPtr.Zero)
throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point");
return eglGetProcAddress;
}
}
}
}

88
src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs

@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using static Avalonia.OpenGL.EglConsts;
namespace Avalonia.OpenGL.Angle
{
public class AngleWin32EglDisplay : EglDisplay
{
struct AngleInfo
{
public IntPtr Display { get; set; }
public AngleOptions.PlatformApi PlatformApi { get; set; }
}
static AngleInfo CreateAngleDisplay(EglInterface _egl)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new PlatformNotSupportedException();
var display = IntPtr.Zero;
AngleOptions.PlatformApi angleApi = default;
{
if (_egl.GetPlatformDisplayEXT == null)
throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll");
var allowedApis = AvaloniaLocator.Current.GetService<AngleOptions>()?.AllowedPlatformApis
?? new [] { AngleOptions.PlatformApi.DirectX11, AngleOptions.PlatformApi.DirectX9 };
foreach (var platformApi in allowedApis)
{
int dapi;
if (platformApi == AngleOptions.PlatformApi.DirectX9)
dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE;
else if (platformApi == AngleOptions.PlatformApi.DirectX11)
dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE;
else
continue;
display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero,
new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE });
if (display != IntPtr.Zero)
{
angleApi = platformApi;
break;
}
}
if (display == IntPtr.Zero)
throw new OpenGlException("Unable to create ANGLE display");
return new AngleInfo { Display = display, PlatformApi = angleApi };
}
}
private AngleWin32EglDisplay(EglInterface egl, AngleInfo info) : base(egl, info.Display)
{
PlatformApi = info.PlatformApi;
}
public AngleWin32EglDisplay(EglInterface egl) : this(egl, CreateAngleDisplay(egl))
{
}
public AngleWin32EglDisplay() : this(new AngleEglInterface())
{
}
public AngleOptions.PlatformApi PlatformApi { get; }
public IntPtr GetDirect3DDevice()
{
if (!EglInterface.QueryDisplayAttribExt(Handle, EglConsts.EGL_DEVICE_EXT, out var eglDevice))
throw new OpenGlException("Unable to get EGL_DEVICE_EXT");
if (!EglInterface.QueryDeviceAttribExt(eglDevice, PlatformApi == AngleOptions.PlatformApi.DirectX9 ? EGL_D3D9_DEVICE_ANGLE : EGL_D3D11_DEVICE_ANGLE, out var d3dDeviceHandle))
throw new OpenGlException("Unable to get EGL_D3D9_DEVICE_ANGLE");
return d3dDeviceHandle;
}
public EglSurface WrapDirect3D11Texture(IntPtr handle)
{
if (PlatformApi != AngleOptions.PlatformApi.DirectX11)
throw new InvalidOperationException("Current platform API is " + PlatformApi);
return CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE });
}
}
}

5
src/Avalonia.OpenGL/AngleOptions.cs

@ -10,9 +10,6 @@ namespace Avalonia.OpenGL
DirectX11 DirectX11
} }
public List<PlatformApi> AllowedPlatformApis = new List<PlatformApi> public IList<PlatformApi> AllowedPlatformApis { get; set; } = null;
{
PlatformApi.DirectX9
};
} }
} }

13
src/Avalonia.OpenGL/EglConsts.cs

@ -186,11 +186,24 @@ namespace Avalonia.OpenGL
public const int EGL_PLATFORM_ANGLE_TYPE_DEFAULT_ANGLE = 0x3206; public const int EGL_PLATFORM_ANGLE_TYPE_DEFAULT_ANGLE = 0x3206;
public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_HARDWARE_ANGLE = 0x320A; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_HARDWARE_ANGLE = 0x320A;
public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_NULL_ANGLE = 0x345E; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_NULL_ANGLE = 0x345E;
public const int EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE = 0x320D;
public const int EGL_PLATFORM_ANGLE_TYPE_OPENGLES_ANGLE = 0x320E;
//EGL_ANGLE_platform_angle_d3d //EGL_ANGLE_platform_angle_d3d
public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE = 0x3209; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE = 0x3209;
public const int EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE = 0x320F; public const int EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE = 0x320F;
public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_WARP_ANGLE = 0x320B; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_WARP_ANGLE = 0x320B;
public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_REFERENCE_ANGLE = 0x320C; public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_REFERENCE_ANGLE = 0x320C;
//EXT_device_query
public const int EGL_DEVICE_EXT = 0x322C;
//ANGLE_device_d3d
public const int EGL_D3D9_DEVICE_ANGLE = 0x33A0;
public const int EGL_D3D11_DEVICE_ANGLE = 0x33A1;
public const int EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE = 0x3200;
public const int EGL_D3D_TEXTURE_ANGLE = 0x33A3;
} }
} }

76
src/Avalonia.OpenGL/EglDisplay.cs

@ -15,7 +15,6 @@ namespace Avalonia.OpenGL
private readonly int _surfaceType; private readonly int _surfaceType;
public IntPtr Handle => _display; public IntPtr Handle => _display;
private AngleOptions.PlatformApi? _angleApi;
private int _sampleCount; private int _sampleCount;
private int _stencilSize; private int _stencilSize;
private GlVersion _version; private GlVersion _version;
@ -24,56 +23,41 @@ namespace Avalonia.OpenGL
{ {
} }
public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs)
{
_egl = egl;
static IntPtr CreateDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs)
{
var display = IntPtr.Zero;
if (platformType == -1 && platformDisplay == IntPtr.Zero) if (platformType == -1 && platformDisplay == IntPtr.Zero)
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (display == IntPtr.Zero)
{ display = egl.GetDisplay(IntPtr.Zero);
if (_egl.GetPlatformDisplayEXT == null)
throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll");
var allowedApis = AvaloniaLocator.Current.GetService<AngleOptions>()?.AllowedPlatformApis
?? new List<AngleOptions.PlatformApi> {AngleOptions.PlatformApi.DirectX9};
foreach (var platformApi in allowedApis)
{
int dapi;
if (platformApi == AngleOptions.PlatformApi.DirectX9)
dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE;
else if (platformApi == AngleOptions.PlatformApi.DirectX11)
dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE;
else
continue;
_display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero,
new[] {EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE});
if (_display != IntPtr.Zero)
{
_angleApi = platformApi;
break;
}
}
if (_display == IntPtr.Zero)
throw new OpenGlException("Unable to create ANGLE display");
}
if (_display == IntPtr.Zero)
_display = _egl.GetDisplay(IntPtr.Zero);
} }
else else
{ {
if (_egl.GetPlatformDisplayEXT == null) if (egl.GetPlatformDisplayEXT == null)
throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl");
_display = _egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs); display = egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs);
} }
if (display == IntPtr.Zero)
throw OpenGlException.GetFormattedException("eglGetDisplay", egl);
return display;
}
if (_display == IntPtr.Zero) public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs)
throw OpenGlException.GetFormattedException("eglGetDisplay", _egl); : this(egl, CreateDisplay(egl, platformType, platformDisplay, attrs))
{
}
public EglDisplay(EglInterface egl, IntPtr display)
{
_egl = egl;
_display = display;
if(_display == IntPtr.Zero)
throw new ArgumentException();
if (!_egl.Initialize(_display, out var major, out var minor)) if (!_egl.Initialize(_display, out var major, out var minor))
throw OpenGlException.GetFormattedException("eglInitialize", _egl); throw OpenGlException.GetFormattedException("eglInitialize", _egl);
@ -172,5 +156,15 @@ namespace Avalonia.OpenGL
throw OpenGlException.GetFormattedException("eglCreateWindowSurface", _egl); throw OpenGlException.GetFormattedException("eglCreateWindowSurface", _egl);
return new EglSurface(this, _egl, s); return new EglSurface(this, _egl, s);
} }
public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs)
{
var s = _egl.CreatePbufferFromClientBuffer(_display, bufferType, handle,
_config, attribs);
if (s == IntPtr.Zero)
throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", _egl);
return new EglSurface(this, _egl, s);
}
} }
} }

7
src/Avalonia.OpenGL/EglGlPlatformFeature.cs

@ -20,12 +20,13 @@ namespace Avalonia.OpenGL
if (feature != null) if (feature != null)
AvaloniaLocator.CurrentMutable.Bind<IWindowingPlatformGlFeature>().ToConstant(feature); AvaloniaLocator.CurrentMutable.Bind<IWindowingPlatformGlFeature>().ToConstant(feature);
} }
public static EglGlPlatformFeature TryCreate() public static EglGlPlatformFeature TryCreate() => TryCreate(() => new EglDisplay());
public static EglGlPlatformFeature TryCreate(Func<EglDisplay> displayFactory)
{ {
try try
{ {
var disp = new EglDisplay(); var disp = displayFactory();
return new EglGlPlatformFeature return new EglGlPlatformFeature
{ {
_display = disp, _display = disp,

84
src/Avalonia.OpenGL/EglGlPlatformSurface.cs

@ -3,33 +3,26 @@ using System.Threading;
namespace Avalonia.OpenGL namespace Avalonia.OpenGL
{ {
public class EglGlPlatformSurface : IGlPlatformSurface public class EglGlPlatformSurface : EglGlPlatformSurfaceBase
{ {
public interface IEglWindowGlPlatformSurfaceInfo
{
IntPtr Handle { get; }
PixelSize Size { get; }
double Scaling { get; }
}
private readonly EglDisplay _display; private readonly EglDisplay _display;
private readonly EglContext _context; private readonly EglContext _context;
private readonly IEglWindowGlPlatformSurfaceInfo _info; private readonly IEglWindowGlPlatformSurfaceInfo _info;
public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info) public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info) : base()
{ {
_display = context.Display; _display = context.Display;
_context = context; _context = context;
_info = info; _info = info;
} }
public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget()
{ {
var glSurface = _display.CreateWindowSurface(_info.Handle); var glSurface = _display.CreateWindowSurface(_info.Handle);
return new RenderTarget(_display, _context, glSurface, _info); return new RenderTarget(_display, _context, glSurface, _info);
} }
class RenderTarget : IGlPlatformSurfaceRenderTargetWithCorruptionInfo class RenderTarget : EglPlatformSurfaceRenderTargetBase
{ {
private readonly EglDisplay _display; private readonly EglDisplay _display;
private readonly EglContext _context; private readonly EglContext _context;
@ -38,7 +31,7 @@ namespace Avalonia.OpenGL
private PixelSize _initialSize; private PixelSize _initialSize;
public RenderTarget(EglDisplay display, EglContext context, public RenderTarget(EglDisplay display, EglContext context,
EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) : base(display, context)
{ {
_display = display; _display = display;
_context = context; _context = context;
@ -47,70 +40,11 @@ namespace Avalonia.OpenGL
_initialSize = info.Size; _initialSize = info.Size;
} }
public void Dispose() => _glSurface.Dispose(); public override void Dispose() => _glSurface.Dispose();
public bool IsCorrupted => _initialSize != _info.Size; public override bool IsCorrupted => _initialSize != _info.Size;
public IGlPlatformSurfaceRenderingSession BeginDraw()
{
var l = _context.Lock();
try
{
if (IsCorrupted)
throw new RenderTargetCorruptedException();
var restoreContext = _context.MakeCurrent(_glSurface);
_display.EglInterface.WaitClient();
_display.EglInterface.WaitGL();
_display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE);
return new Session(_display, _context, _glSurface, _info, l, restoreContext);
}
catch
{
l.Dispose();
throw;
}
}
class Session : IGlPlatformSurfaceRenderingSession public override IGlPlatformSurfaceRenderingSession BeginDraw() => base.BeginDraw(_glSurface, _info);
{
private readonly EglContext _context;
private readonly EglSurface _glSurface;
private readonly IEglWindowGlPlatformSurfaceInfo _info;
private readonly EglDisplay _display;
private IDisposable _lock;
private readonly IDisposable _restoreContext;
public Session(EglDisplay display, EglContext context,
EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info,
IDisposable @lock, IDisposable restoreContext)
{
_context = context;
_display = display;
_glSurface = glSurface;
_info = info;
_lock = @lock;
_restoreContext = restoreContext;
}
public void Dispose()
{
_context.GlInterface.Flush();
_display.EglInterface.WaitGL();
_glSurface.SwapBuffers();
_display.EglInterface.WaitClient();
_display.EglInterface.WaitGL();
_display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE);
_restoreContext.Dispose();
_lock.Dispose();
}
public IGlContext Context => _context;
public PixelSize Size => _info.Size;
public double Scaling => _info.Scaling;
public bool IsYFlipped { get; }
}
} }
} }
} }

103
src/Avalonia.OpenGL/EglGlPlatformSurfaceBase.cs

@ -0,0 +1,103 @@
using System;
namespace Avalonia.OpenGL
{
public abstract class EglGlPlatformSurfaceBase : IGlPlatformSurface
{
public interface IEglWindowGlPlatformSurfaceInfo
{
IntPtr Handle { get; }
PixelSize Size { get; }
double Scaling { get; }
}
public abstract IGlPlatformSurfaceRenderTarget CreateGlRenderTarget();
}
public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTargetWithCorruptionInfo
{
private readonly EglDisplay _display;
private readonly EglContext _context;
protected EglPlatformSurfaceRenderTargetBase(EglDisplay display, EglContext context)
{
_display = display;
_context = context;
}
public abstract bool IsCorrupted { get; }
public virtual void Dispose()
{
}
public abstract IGlPlatformSurfaceRenderingSession BeginDraw();
protected IGlPlatformSurfaceRenderingSession BeginDraw(EglSurface surface,
EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, Action onFinish = null, bool isYFlipped = false)
{
var l = _context.Lock();
try
{
if (IsCorrupted)
throw new RenderTargetCorruptedException();
var restoreContext = _context.MakeCurrent(surface);
_display.EglInterface.WaitClient();
_display.EglInterface.WaitGL();
_display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE);
return new Session(_display, _context, surface, info, l, restoreContext, onFinish, isYFlipped);
}
catch
{
l.Dispose();
throw;
}
}
class Session : IGlPlatformSurfaceRenderingSession
{
private readonly EglContext _context;
private readonly EglSurface _glSurface;
private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info;
private readonly EglDisplay _display;
private readonly IDisposable _lock;
private readonly IDisposable _restoreContext;
private readonly Action _onFinish;
public Session(EglDisplay display, EglContext context,
EglSurface glSurface, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info,
IDisposable @lock, IDisposable restoreContext, Action onFinish, bool isYFlipped)
{
IsYFlipped = isYFlipped;
_context = context;
_display = display;
_glSurface = glSurface;
_info = info;
_lock = @lock;
_restoreContext = restoreContext;
_onFinish = onFinish;
}
public void Dispose()
{
_context.GlInterface.Flush();
_display.EglInterface.WaitGL();
_glSurface.SwapBuffers();
_display.EglInterface.WaitClient();
_display.EglInterface.WaitGL();
_display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE);
_restoreContext.Dispose();
_lock.Dispose();
_onFinish?.Invoke();
}
public IGlContext Context => _context;
public PixelSize Size => _info.Size;
public double Scaling => _info.Scaling;
public bool IsYFlipped { get; }
}
}
}

29
src/Avalonia.OpenGL/EglInterface.cs

@ -17,25 +17,21 @@ namespace Avalonia.OpenGL
} }
public EglInterface(Func<string, IntPtr> getProcAddress) : base(getProcAddress)
{
}
public EglInterface(string library) : base(Load(library)) public EglInterface(string library) : base(Load(library))
{ {
} }
[DllImport("libegl.dll", CharSet = CharSet.Ansi)]
static extern IntPtr eglGetProcAddress(string proc);
static Func<string, IntPtr> Load() static Func<string, IntPtr> Load()
{ {
var os = AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem; var os = AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem;
if(os == OperatingSystemType.Linux || os == OperatingSystemType.Android) if(os == OperatingSystemType.Linux || os == OperatingSystemType.Android)
return Load("libEGL.so.1"); return Load("libEGL.so.1");
if (os == OperatingSystemType.WinNT)
{
var disp = eglGetProcAddress("eglGetPlatformDisplayEXT");
if (disp == IntPtr.Zero)
throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point");
return eglGetProcAddress;
}
throw new PlatformNotSupportedException(); throw new PlatformNotSupportedException();
} }
@ -147,6 +143,21 @@ namespace Avalonia.OpenGL
return null; return null;
return Marshal.PtrToStringAnsi(rv); return Marshal.PtrToStringAnsi(rv);
} }
public delegate IntPtr EglCreatePbufferFromClientBuffer(IntPtr display, int buftype, IntPtr buffer, IntPtr config, int[] attrib_list);
[GlEntryPoint("eglCreatePbufferFromClientBuffer")]
public EglCreatePbufferFromClientBuffer CreatePbufferFromClientBuffer { get; }
public delegate bool EglQueryDisplayAttribEXT(IntPtr display, int attr, out IntPtr res);
[GlEntryPoint("eglQueryDisplayAttribEXT"), GlOptionalEntryPoint]
public EglQueryDisplayAttribEXT QueryDisplayAttribExt { get; }
public delegate bool EglQueryDeviceAttribEXT(IntPtr display, int attr, out IntPtr res);
[GlEntryPoint("eglQueryDeviceAttribEXT"), GlOptionalEntryPoint]
public EglQueryDisplayAttribEXT QueryDeviceAttribExt { get; }
// ReSharper restore UnassignedGetOnlyAutoProperty // ReSharper restore UnassignedGetOnlyAutoProperty
} }

12
src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Avalonia.ReactiveUI</PackageId>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Pharmacist.Common" Version="1.8.1" />
</ItemGroup>
</Project>

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

@ -8,11 +8,11 @@
<Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUnchecked}" /> <Setter Property="Background" Value="{DynamicResource CheckBoxBackgroundUnchecked}" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUnchecked}" /> <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUnchecked}" />
<Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUnchecked}" /> <Setter Property="BorderBrush" Value="{DynamicResource CheckBoxBorderBrushUnchecked}" />
<Setter Property="Padding" Value="8,5,0,0" /> <Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" /> <Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Top" /> <Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" /> <Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="MinWidth" Value="120" /> <Setter Property="MinWidth" Value="120" />
<Setter Property="MinHeight" Value="32" /> <Setter Property="MinHeight" Value="32" />

4
src/Avalonia.Themes.Fluent/RadioButton.xaml

@ -13,11 +13,11 @@
<Setter Property="Background" Value="{DynamicResource RadioButtonBackground}" /> <Setter Property="Background" Value="{DynamicResource RadioButtonBackground}" />
<Setter Property="Foreground" Value="{DynamicResource RadioButtonForeground}" /> <Setter Property="Foreground" Value="{DynamicResource RadioButtonForeground}" />
<Setter Property="BorderBrush" Value="{DynamicResource RadioButtonBorderBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource RadioButtonBorderBrush}" />
<Setter Property="Padding" Value="8,6,0,0" /> <Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" /> <Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Top" /> <Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" /> <Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="MinWidth" Value="120" /> <Setter Property="MinWidth" Value="120" />
<Setter Property="Template"> <Setter Property="Template">

15
src/Avalonia.Visuals/ApiCompatBaseline.txt

@ -0,0 +1,15 @@
Compat issues with assembly Avalonia.Visuals:
MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.GetOrAddTypeface(Avalonia.Media.FontFamily, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.MatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
CannotSealType : Type 'Avalonia.Media.Typeface' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
TypeCannotChangeClassification : Type 'Avalonia.Media.Typeface' is a 'struct' in the implementation but is a 'class' in the contract.
CannotMakeMemberNonVirtual : Member 'public System.Boolean Avalonia.Media.Typeface.Equals(System.Object)' is non-virtual in the implementation but is virtual in the contract.
CannotMakeMemberNonVirtual : Member 'public System.Int32 Avalonia.Media.Typeface.GetHashCode()' is non-virtual in the implementation but is virtual in the contract.
TypesMustExist : Type 'Avalonia.Media.Fonts.FontKey' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.LineBreak.get()' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak.get()' is abstract in the implementation but is missing in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' is present in the implementation but not in the contract.
Total Issues: 13

67
src/Avalonia.Visuals/Media/FontManager.cs

@ -13,8 +13,8 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public sealed class FontManager public sealed class FontManager
{ {
private readonly ConcurrentDictionary<FontKey, Typeface> _typefaceCache = private readonly ConcurrentDictionary<Typeface, GlyphTypeface> _glyphTypefaceCache =
new ConcurrentDictionary<FontKey, Typeface>(); new ConcurrentDictionary<Typeface, GlyphTypeface>();
private readonly FontFamily _defaultFontFamily; private readonly FontFamily _defaultFontFamily;
public FontManager(IFontManagerImpl platformImpl) public FontManager(IFontManagerImpl platformImpl)
@ -76,79 +76,52 @@ namespace Avalonia.Media
PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates); PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
/// <summary> /// <summary>
/// Returns a new typeface, or an existing one if a matching typeface exists. /// Returns a new <see cref="GlyphTypeface"/>, or an existing one if a matching <see cref="GlyphTypeface"/> exists.
/// </summary> /// </summary>
/// <param name="fontFamily">The font family.</param> /// <param name="typeface">The typeface.</param>
/// <param name="fontStyle">The font style.</param>
/// <param name="fontWeight">The font weight.</param>
/// <returns> /// <returns>
/// The typeface. /// The <see cref="GlyphTypeface"/>.
/// </returns> /// </returns>
public Typeface GetOrAddTypeface(FontFamily fontFamily, FontStyle fontStyle = FontStyle.Normal, public GlyphTypeface GetOrAddGlyphTypeface(Typeface typeface)
FontWeight fontWeight = FontWeight.Normal)
{ {
while (true) while (true)
{ {
if (fontFamily.IsDefault) if (_glyphTypefaceCache.TryGetValue(typeface, out var glyphTypeface))
{ {
fontFamily = _defaultFontFamily; return glyphTypeface;
} }
var key = new FontKey(fontFamily.Name, fontStyle, fontWeight); glyphTypeface = new GlyphTypeface(typeface);
if (_typefaceCache.TryGetValue(key, out var typeface)) if (_glyphTypefaceCache.TryAdd(typeface, glyphTypeface))
{ {
return typeface; return glyphTypeface;
} }
typeface = new Typeface(fontFamily, fontStyle, fontWeight); if (typeface.FontFamily == _defaultFontFamily)
if (_typefaceCache.TryAdd(key, typeface))
{
return typeface;
}
if (fontFamily == _defaultFontFamily)
{ {
return null; return null;
} }
fontFamily = _defaultFontFamily; typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight);
} }
} }
/// <summary> /// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties. /// Tries to match a specified character to a <see cref="Typeface"/> that supports specified font properties.
/// Returns <c>null</c> if no fallback was found.
/// </summary> /// </summary>
/// <param name="codepoint">The codepoint to match against.</param> /// <param name="codepoint">The codepoint to match against.</param>
/// <param name="fontStyle">The font style.</param> /// <param name="fontStyle">The font style.</param>
/// <param name="fontWeight">The font weight.</param> /// <param name="fontWeight">The font weight.</param>
/// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param> /// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
/// <param name="culture">The culture.</param> /// <param name="culture">The culture.</param>
/// <param name="typeface">The matching <see cref="Typeface"/>.</param>
/// <returns> /// <returns>
/// The matched typeface. /// <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns> /// </returns>
public Typeface MatchCharacter(int codepoint, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
FontStyle fontStyle = FontStyle.Normal, FontWeight fontWeight,
FontWeight fontWeight = FontWeight.Normal, FontFamily fontFamily, CultureInfo culture, out Typeface typeface) =>
FontFamily fontFamily = null, CultureInfo culture = null) PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out typeface);
{
foreach (var cachedTypeface in _typefaceCache.Values)
{
// First try to find a cached typeface by style and weight to avoid redundant glyph index lookup.
if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight
&& cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0)
{
return cachedTypeface;
}
}
var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out var key) ?
_typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Style, key.Weight)) :
null;
return matchedTypeface;
}
} }
} }

40
src/Avalonia.Visuals/Media/Fonts/FontKey.cs

@ -1,40 +0,0 @@
using System;
namespace Avalonia.Media.Fonts
{
public readonly struct FontKey : IEquatable<FontKey>
{
public FontKey(string familyName, FontStyle style, FontWeight weight)
{
FamilyName = familyName;
Style = style;
Weight = weight;
}
public string FamilyName { get; }
public FontStyle Style { get; }
public FontWeight Weight { get; }
public override int GetHashCode()
{
var hash = FamilyName.GetHashCode();
hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight;
return hash;
}
public override bool Equals(object other)
{
return other is FontKey key && Equals(key);
}
public bool Equals(FontKey other)
{
return FamilyName == other.FamilyName &&
Style == other.Style &&
Weight == other.Weight;
}
}
}

7
src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs

@ -70,10 +70,11 @@ namespace Avalonia.Media.TextFormatting
var codepoint = Codepoint.ReadAt(text, count, out _); var codepoint = Codepoint.ReadAt(text, count, out _);
//ToDo: Fix FontFamily fallback //ToDo: Fix FontFamily fallback
currentTypeface = var matchFound =
FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.FontFamily); FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface);
if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) if (matchFound && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
{ {
//Fallback found //Fallback found
return new ShapeableTextCharacters(text.Take(count), return new ShapeableTextCharacters(text.Take(count),

6
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@ -221,7 +221,7 @@ namespace Avalonia.Media.TextFormatting
while (currentPosition < _text.Length) while (currentPosition < _text.Length)
{ {
var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth, var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
_paragraphProperties, previousLine?.LineBreak); _paragraphProperties, previousLine?.TextLineBreak);
currentPosition += textLine.TextRange.Length; currentPosition += textLine.TextRange.Length;
@ -230,7 +230,7 @@ namespace Avalonia.Media.TextFormatting
if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) && if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) &&
height + textLine.LineMetrics.Size.Height > MaxHeight) height + textLine.LineMetrics.Size.Height > MaxHeight)
{ {
if (previousLine?.LineBreak != null && _textTrimming != TextTrimming.None) if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{ {
var collapsedLine = var collapsedLine =
previousLine.Collapse(GetCollapsingProperties(MaxWidth)); previousLine.Collapse(GetCollapsingProperties(MaxWidth));
@ -255,7 +255,7 @@ namespace Avalonia.Media.TextFormatting
previousLine = textLine; previousLine = textLine;
if (currentPosition != _text.Length || textLine.LineBreak == null) if (currentPosition != _text.Length || textLine.TextLineBreak == null)
{ {
continue; continue;
} }

4
src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs

@ -35,9 +35,9 @@ namespace Avalonia.Media.TextFormatting
/// Gets the state of the line when broken by line breaking process. /// Gets the state of the line when broken by line breaking process.
/// </summary> /// </summary>
/// <returns> /// <returns>
/// A <see cref="LineBreak"/> value that represents the line break. /// A <see cref="TextLineBreak"/> value that represents the line break.
/// </returns> /// </returns>
public abstract TextLineBreak LineBreak { get; } public abstract TextLineBreak TextLineBreak { get; }
/// <summary> /// <summary>
/// Gets a value that indicates whether the line is collapsed. /// Gets a value that indicates whether the line is collapsed.

17
src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs

@ -13,7 +13,7 @@ namespace Avalonia.Media.TextFormatting
{ {
_textRuns = textRuns; _textRuns = textRuns;
LineMetrics = lineMetrics; LineMetrics = lineMetrics;
LineBreak = lineBreak; TextLineBreak = lineBreak;
HasCollapsed = hasCollapsed; HasCollapsed = hasCollapsed;
} }
@ -27,7 +27,7 @@ namespace Avalonia.Media.TextFormatting
public override TextLineMetrics LineMetrics { get; } public override TextLineMetrics LineMetrics { get; }
/// <inheritdoc/> /// <inheritdoc/>
public override TextLineBreak LineBreak { get; } public override TextLineBreak TextLineBreak { get; }
/// <inheritdoc/> /// <inheritdoc/>
public override bool HasCollapsed { get; } public override bool HasCollapsed { get; }
@ -122,7 +122,7 @@ namespace Avalonia.Media.TextFormatting
textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height), textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height),
LineMetrics.TextBaseline, textRange, false); LineMetrics.TextBaseline, textRange, false);
return new TextLineImpl(shapedTextCharacters, textLineMetrics, LineBreak, true); return new TextLineImpl(shapedTextCharacters, textLineMetrics, TextLineBreak, true);
} }
availableWidth -= currentRun.GlyphRun.Bounds.Width; availableWidth -= currentRun.GlyphRun.Bounds.Width;
@ -268,6 +268,17 @@ namespace Avalonia.Media.TextFormatting
var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
TextRange.Length; TextRange.Length;
var characterIndex = codepointIndex - run.Text.Start;
var codepoint = Codepoint.ReadAt(run.GlyphRun.Characters, characterIndex, out _);
if (codepoint.IsBreakChar)
{
foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(codepointIndex - 1, out _);
isAtEnd = true;
}
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ? nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
foundCharacterHit : foundCharacterHit :
new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength); new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);

31
src/Avalonia.Visuals/Media/Typeface.cs

@ -8,17 +8,15 @@ namespace Avalonia.Media
/// Represents a typeface. /// Represents a typeface.
/// </summary> /// </summary>
[DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")] [DebuggerDisplay("Name = {FontFamily.Name}, Weight = {Weight}, Style = {Style}")]
public class Typeface : IEquatable<Typeface> public readonly struct Typeface : IEquatable<Typeface>
{ {
private GlyphTypeface _glyphTypeface;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Typeface"/> class. /// Initializes a new instance of the <see cref="Typeface"/> class.
/// </summary> /// </summary>
/// <param name="fontFamily">The font family.</param> /// <param name="fontFamily">The font family.</param>
/// <param name="style">The font style.</param> /// <param name="style">The font style.</param>
/// <param name="weight">The font weight.</param> /// <param name="weight">The font weight.</param>
public Typeface([NotNull]FontFamily fontFamily, public Typeface([NotNull] FontFamily fontFamily,
FontStyle style = FontStyle.Normal, FontStyle style = FontStyle.Normal,
FontWeight weight = FontWeight.Normal) FontWeight weight = FontWeight.Normal)
{ {
@ -45,7 +43,7 @@ namespace Avalonia.Media
{ {
} }
public static Typeface Default => FontManager.Current?.GetOrAddTypeface(FontFamily.Default); public static Typeface Default { get; } = new Typeface(FontFamily.Default);
/// <summary> /// <summary>
/// Gets the font family. /// Gets the font family.
@ -68,7 +66,7 @@ namespace Avalonia.Media
/// <value> /// <value>
/// The glyph typeface. /// The glyph typeface.
/// </value> /// </value>
public GlyphTypeface GlyphTypeface => _glyphTypeface ?? (_glyphTypeface = new GlyphTypeface(this)); public GlyphTypeface GlyphTypeface => FontManager.Current.GetOrAddGlyphTypeface(this);
public static bool operator !=(Typeface a, Typeface b) public static bool operator !=(Typeface a, Typeface b)
{ {
@ -77,32 +75,17 @@ namespace Avalonia.Media
public static bool operator ==(Typeface a, Typeface b) public static bool operator ==(Typeface a, Typeface b)
{ {
if (ReferenceEquals(a, b)) return a.Equals(b);
{
return true;
}
return !(a is null) && a.Equals(b);
} }
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
if (obj is Typeface typeface) return obj is Typeface typeface && Equals(typeface);
{
return Equals(typeface);
}
return false;
} }
public bool Equals(Typeface other) public bool Equals(Typeface other)
{ {
if (other is null) return FontFamily == other.FontFamily && Style == other.Style && Weight == other.Weight;
{
return false;
}
return FontFamily.Equals(other.FontFamily) && Style == other.Style && Weight == other.Weight;
} }
public override int GetHashCode() public override int GetHashCode()

5
src/Avalonia.Visuals/Platform/IFontManagerImpl.cs

@ -1,7 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
@ -26,13 +25,13 @@ namespace Avalonia.Platform
/// <param name="fontWeight">The font weight.</param> /// <param name="fontWeight">The font weight.</param>
/// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param> /// <param name="fontFamily">The font family. This is optional and used for fallback lookup.</param>
/// <param name="culture">The culture.</param> /// <param name="culture">The culture.</param>
/// <param name="fontKey">The matching font key.</param> /// <param name="typeface">The matching typeface.</param>
/// <returns> /// <returns>
/// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise. /// <c>True</c>, if the <see cref="IFontManagerImpl"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns> /// </returns>
bool TryMatchCharacter(int codepoint, FontStyle fontStyle, bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
FontWeight fontWeight, FontWeight fontWeight,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey); FontFamily fontFamily, CultureInfo culture, out Typeface typeface);
/// <summary> /// <summary>
/// Creates a glyph typeface. /// Creates a glyph typeface.

3
src/Avalonia.Visuals/Rendering/RenderLayer.cs

@ -30,12 +30,13 @@ namespace Avalonia.Rendering
{ {
if (Size != size || Scaling != scaling) if (Size != size || Scaling != scaling)
{ {
Bitmap.Dispose();
var resized = RefCountable.Create(drawingContext.CreateLayer(size)); var resized = RefCountable.Create(drawingContext.CreateLayer(size));
using (var context = resized.Item.CreateDrawingContext(null)) using (var context = resized.Item.CreateDrawingContext(null))
{ {
context.Clear(Colors.Transparent); context.Clear(Colors.Transparent);
Bitmap.Dispose();
Bitmap = resized; Bitmap = resized;
Scaling = scaling; Scaling = scaling;
Size = size; Size = size;

2
src/Avalonia.Visuals/Rendering/RendererBase.cs

@ -20,7 +20,7 @@ namespace Avalonia.Rendering
_useManualFpsCounting = useManualFpsCounting; _useManualFpsCounting = useManualFpsCounting;
_fpsText = new FormattedText _fpsText = new FormattedText
{ {
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default), Typeface = new Typeface(FontFamily.Default),
FontSize = s_fontSize FontSize = s_fontSize
}; };
} }

4
src/Avalonia.Visuals/Visual.cs

@ -455,7 +455,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Called when the control is added to a visual tree. /// Called when the control is added to a rooted visual tree.
/// </summary> /// </summary>
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
protected virtual void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected virtual void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
@ -463,7 +463,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Called when the control is removed from a visual tree. /// Called when the control is removed from a rooted visual tree.
/// </summary> /// </summary>
/// <param name="e">The event args.</param> /// <param name="e">The event args.</param>
protected virtual void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) protected virtual void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)

7
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform; using Avalonia.Platform;
using SkiaSharp; using SkiaSharp;
@ -31,7 +30,7 @@ namespace Avalonia.Skia
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
FontWeight fontWeight, FontWeight fontWeight,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) FontFamily fontFamily, CultureInfo culture, out Typeface fontKey)
{ {
SKFontStyle skFontStyle; SKFontStyle skFontStyle;
@ -81,7 +80,7 @@ namespace Avalonia.Skia
continue; continue;
} }
fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight);
return true; return true;
} }
@ -92,7 +91,7 @@ namespace Avalonia.Skia
if (skTypeface != null) if (skTypeface != null)
{ {
fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); fontKey = new Typeface(skTypeface.FamilyName, fontStyle, fontWeight);
return true; return true;
} }

2
src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs

@ -35,7 +35,6 @@ namespace Avalonia.Skia
IGlPlatformSurfaceRenderingSession glSession) IGlPlatformSurfaceRenderingSession glSession)
{ {
GrContext = grContext; GrContext = grContext;
GrContext.PurgeResources();
_backendRenderTarget = backendRenderTarget; _backendRenderTarget = backendRenderTarget;
_surface = surface; _surface = surface;
_glSession = glSession; _glSession = glSession;
@ -46,7 +45,6 @@ namespace Avalonia.Skia
_surface.Dispose(); _surface.Dispose();
_backendRenderTarget.Dispose(); _backendRenderTarget.Dispose();
GrContext.Flush(); GrContext.Flush();
GrContext.PurgeResources();
_glSession.Dispose(); _glSession.Dispose();
} }

19
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -2,31 +2,28 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
internal class SKTypefaceCollection internal class SKTypefaceCollection
{ {
private readonly ConcurrentDictionary<FontKey, SKTypeface> _typefaces = private readonly ConcurrentDictionary<Typeface, SKTypeface> _typefaces =
new ConcurrentDictionary<FontKey, SKTypeface>(); new ConcurrentDictionary<Typeface, SKTypeface>();
public void AddTypeface(FontKey key, SKTypeface typeface) public void AddTypeface(Typeface key, SKTypeface typeface)
{ {
_typefaces.TryAdd(key, typeface); _typefaces.TryAdd(key, typeface);
} }
public SKTypeface Get(Typeface typeface) public SKTypeface Get(Typeface typeface)
{ {
var key = new FontKey(typeface.FontFamily.Name, typeface.Style, typeface.Weight); return GetNearestMatch(_typefaces, typeface);
return GetNearestMatch(_typefaces, key);
} }
private static SKTypeface GetNearestMatch(IDictionary<FontKey, SKTypeface> typefaces, FontKey key) private static SKTypeface GetNearestMatch(IDictionary<Typeface, SKTypeface> typefaces, Typeface key)
{ {
if (typefaces.TryGetValue(new FontKey(key.FamilyName, key.Style, key.Weight), out var typeface)) if (typefaces.TryGetValue(key, out var typeface))
{ {
return typeface; return typeface;
} }
@ -42,7 +39,7 @@ namespace Avalonia.Skia
{ {
if (weight - j >= 100) if (weight - j >= 100)
{ {
if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight - j)), out typeface))
{ {
return typeface; return typeface;
} }
@ -53,7 +50,7 @@ namespace Avalonia.Skia
continue; continue;
} }
if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) if (typefaces.TryGetValue(new Typeface(key.FontFamily, (FontStyle)i, (FontWeight)(weight + j)), out typeface))
{ {
return typeface; return typeface;
} }

2
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@ -56,7 +56,7 @@ namespace Avalonia.Skia
continue; continue;
} }
var key = new FontKey(fontFamily.Name, typeface.FontSlant.ToAvalonia(), var key = new Typeface(fontFamily, typeface.FontSlant.ToAvalonia(),
(FontWeight)typeface.FontWeight); (FontWeight)typeface.FontWeight);
typeFaceCollection.AddTypeface(key, typeface); typeFaceCollection.AddTypeface(key, typeface);

6
src/Skia/Avalonia.Skia/SkiaOptions.cs

@ -16,6 +16,10 @@ namespace Avalonia
/// <summary> /// <summary>
/// The maximum number of bytes for video memory to store textures and resources. /// The maximum number of bytes for video memory to store textures and resources.
/// </summary> /// </summary>
public long? MaxGpuResourceSizeBytes { get; set; } /// <remarks>
/// This is set by default to the recommended value for Avalonia.
/// Setting this to null will give you the default Skia value.
/// </remarks>
public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures.
} }
} }

7
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@ -1,7 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform; using Avalonia.Platform;
using SharpDX.DirectWrite; using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily; using FontFamily = Avalonia.Media.FontFamily;
@ -34,7 +33,7 @@ namespace Avalonia.Direct2D1.Media
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
FontWeight fontWeight, FontWeight fontWeight,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) FontFamily fontFamily, CultureInfo culture, out Typeface typeface)
{ {
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
@ -51,12 +50,12 @@ namespace Avalonia.Direct2D1.Media
var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
fontKey = new FontKey(fontFamilyName, fontStyle, fontWeight); typeface = new Typeface(fontFamilyName, fontStyle, fontWeight);
return true; return true;
} }
fontKey = default; typeface = default;
return false; return false;
} }

3
src/Windows/Avalonia.Win32/Win32GlManager.cs

@ -1,4 +1,5 @@
using Avalonia.OpenGL; using Avalonia.OpenGL;
using Avalonia.OpenGL.Angle;
namespace Avalonia.Win32 namespace Avalonia.Win32
{ {
@ -15,7 +16,7 @@ namespace Avalonia.Win32
{ {
if (!s_attemptedToInitialize) if (!s_attemptedToInitialize)
{ {
EglFeature = EglGlPlatformFeature.TryCreate(); EglFeature = EglGlPlatformFeature.TryCreate(() => new AngleWin32EglDisplay());
s_attemptedToInitialize = true; s_attemptedToInitialize = true;
} }

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

@ -853,7 +853,7 @@ namespace Avalonia.Win32
private void ShowWindow(WindowState state) private void ShowWindow(WindowState state)
{ {
ShowWindowCommand command; ShowWindowCommand? command;
var newWindowProperties = _windowProperties; var newWindowProperties = _windowProperties;
@ -875,8 +875,8 @@ namespace Avalonia.Win32
case WindowState.FullScreen: case WindowState.FullScreen:
newWindowProperties.IsFullScreen = true; newWindowProperties.IsFullScreen = true;
UpdateWindowProperties(newWindowProperties); command = IsWindowVisible(_hwnd) ? (ShowWindowCommand?)null : ShowWindowCommand.Restore;
return; break;
default: default:
throw new ArgumentException("Invalid WindowState."); throw new ArgumentException("Invalid WindowState.");
@ -884,7 +884,10 @@ namespace Avalonia.Win32
UpdateWindowProperties(newWindowProperties); UpdateWindowProperties(newWindowProperties);
UnmanagedMethods.ShowWindow(_hwnd, command); if (command.HasValue)
{
UnmanagedMethods.ShowWindow(_hwnd, command.Value);
}
if (state == WindowState.Maximized) if (state == WindowState.Maximized)
{ {
@ -1007,10 +1010,12 @@ namespace Avalonia.Win32
if (newProperties.IsResizable) if (newProperties.IsResizable)
{ {
style |= WindowStyles.WS_SIZEFRAME; style |= WindowStyles.WS_SIZEFRAME;
style |= WindowStyles.WS_MAXIMIZEBOX;
} }
else else
{ {
style &= ~WindowStyles.WS_SIZEFRAME; style &= ~WindowStyles.WS_SIZEFRAME;
style &= ~WindowStyles.WS_MAXIMIZEBOX;
} }
SetStyle(style); SetStyle(style);

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

@ -1335,6 +1335,47 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.False(model.SingleSelect); Assert.False(model.SingleSelect);
} }
[Fact]
public void Does_The_Best_It_Can_With_AutoSelecting_ViewModel()
{
// Tests the following scenario:
//
// - Items changes from empty to having 1 item
// - ViewModel auto-selects item 0 in CollectionChanged
// - SelectionModel receives CollectionChanged
// - And so adjusts the selected item from 0 to 1, which is past the end of the items.
//
// There's not much we can do about this situation because the order in which
// CollectionChanged handlers are called can't be known (the problem also exists with
// WPF). The best we can do is not select an invalid index.
var vm = new SelectionViewModel();
vm.Items.CollectionChanged += (s, e) =>
{
if (vm.SelectedIndex == -1 && vm.Items.Count > 0)
{
vm.SelectedIndex = 0;
}
};
var target = new ListBox
{
[!ListBox.ItemsProperty] = new Binding("Items"),
[!ListBox.SelectedIndexProperty] = new Binding("SelectedIndex"),
DataContext = vm,
};
Prepare(target);
vm.Items.Add("foo");
vm.Items.Add("bar");
Assert.Equal(0, target.SelectedIndex);
Assert.Equal(new[] { 0 }, target.Selection.SelectedIndexes);
Assert.Equal("foo", target.SelectedItem);
Assert.Equal(new[] { "foo" }, target.SelectedItems);
}
private static void Prepare(SelectingItemsControl target) private static void Prepare(SelectingItemsControl target)
{ {
var root = new TestRoot var root = new TestRoot
@ -1397,6 +1438,28 @@ namespace Avalonia.Controls.UnitTests.Primitives
public int SelectedIndex { get; set; } public int SelectedIndex { get; set; }
} }
private class SelectionViewModel : NotifyingBase
{
private int _selectedIndex = -1;
public SelectionViewModel()
{
Items = new ObservableCollection<string>();
}
public int SelectedIndex
{
get => _selectedIndex;
set
{
_selectedIndex = value;
RaisePropertyChanged();
}
}
public ObservableCollection<string> Items { get; }
}
private class RootWithItems : TestRoot private class RootWithItems : TestRoot
{ {
public List<string> Items { get; set; } = new List<string>() { "a", "b", "c", "d", "e" }; public List<string> Items { get; set; } = new List<string>() { "a", "b", "c", "d", "e" };

32
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs

@ -1230,6 +1230,38 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal(1, resetRaised); Assert.Equal(1, resetRaised);
Assert.Equal(1, selectedIndexRaised); Assert.Equal(1, selectedIndexRaised);
} }
[Fact]
public void Handles_Selection_Made_In_CollectionChanged()
{
// Tests the following scenario:
//
// - Items changes from empty to having 2 items
// - ViewModel auto-selects range 0..1 in CollectionChanged
// - SelectionModel receives CollectionChanged
// - And so adjusts the selected item from 0..1 to 2..4, which is past the end of
// the items.
//
// There's not much we can do about this situation because the order in which
// CollectionChanged handlers are called can't be known (the problem also exists with
// WPF). The best we can do is not select an invalid index.
var target = CreateTarget(createData: false);
var data = new AvaloniaList<string>();
data.CollectionChanged += (s, e) =>
{
target.SelectRange(0, 1);
};
target.Source = data;
data.AddRange(new[] { "foo", "bar" });
Assert.Equal(0, target.SelectedIndex);
Assert.Equal(new[] { 0, 1 }, target.SelectedIndexes);
Assert.Equal("foo", target.SelectedItem);
Assert.Equal(new[] { "foo", "bar" }, target.SelectedItems);
Assert.Equal(0, target.AnchorIndex);
}
} }
public class BatchUpdate public class BatchUpdate

31
tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs

@ -1052,6 +1052,37 @@ namespace Avalonia.Controls.UnitTests.Selection
Assert.Equal(1, resetRaised); Assert.Equal(1, resetRaised);
Assert.Equal(1, selectedIndexRaised); Assert.Equal(1, selectedIndexRaised);
} }
[Fact]
public void Handles_Selection_Made_In_CollectionChanged()
{
// Tests the following scenario:
//
// - Items changes from empty to having 1 item
// - ViewModel auto-selects item 0 in CollectionChanged
// - SelectionModel receives CollectionChanged
// - And so adjusts the selected item from 0 to 1, which is past the end of the items.
//
// There's not much we can do about this situation because the order in which
// CollectionChanged handlers are called can't be known (the problem also exists with
// WPF). The best we can do is not select an invalid index.
var target = CreateTarget(createData: false);
var data = new AvaloniaList<string>();
data.CollectionChanged += (s, e) =>
{
target.Select(0);
};
target.Source = data;
data.Add("foo");
Assert.Equal(0, target.SelectedIndex);
Assert.Equal(new[] { 0 }, target.SelectedIndexes);
Assert.Equal("foo", target.SelectedItem);
Assert.Equal(new[] { "foo" }, target.SelectedItems);
Assert.Equal(0, target.AnchorIndex);
}
} }
public class BatchUpdate public class BatchUpdate

31
tests/Avalonia.LeakTests/ControlTests.cs

@ -552,6 +552,37 @@ namespace Avalonia.LeakTests
} }
} }
[Fact]
public void ItemsRepeater_Is_Freed()
{
using (Start())
{
Func<Window> run = () =>
{
var window = new Window
{
Content = new ItemsRepeater(),
};
window.Show();
window.LayoutManager.ExecuteInitialLayoutPass();
Assert.IsType<ItemsRepeater>(window.Presenter.Child);
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
};
var result = run();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<ItemsRepeater>()).ObjectsCount));
}
}
private IDisposable Start() private IDisposable Start()
{ {
return UnitTestApplication.Start(TestServices.StyledWindow.With( return UnitTestApplication.Start(TestServices.StyledWindow.With(

15
tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj" />
</ItemGroup>
</Project>

44
tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs

@ -0,0 +1,44 @@
using System;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.ReactiveUI.Events.UnitTests
{
public class BasicControlEventsTest
{
public class EventsControl : UserControl
{
public bool IsAttached { get; private set; }
public EventsControl()
{
var attached = this
.Events()
.AttachedToVisualTree
.Select(args => true);
this.Events()
.DetachedFromVisualTree
.Select(args => false)
.Merge(attached)
.Subscribe(marker => IsAttached = marker);
}
}
[Fact]
public void Should_Generate_Events_Wrappers()
{
var root = new TestRoot();
var control = new EventsControl();
Assert.False(control.IsAttached);
root.Child = control;
Assert.True(control.IsAttached);
root.Child = null;
Assert.False(control.IsAttached);
}
}
}

2
tests/Avalonia.RenderTests/Media/FormattedTextImplTests.cs

@ -51,7 +51,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
{ {
var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>(); var r = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return r.CreateFormattedText(text, return r.CreateFormattedText(text,
FontManager.Current.GetOrAddTypeface(fontFamily, fontStyle, fontWeight), new Typeface(fontFamily, fontStyle, fontWeight),
fontSize, fontSize,
textAlignment, textAlignment,
wrapping, wrapping,

8
tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs

@ -39,7 +39,7 @@ namespace Avalonia.Skia.UnitTests.Media
private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily, public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily,
CultureInfo culture, out FontKey fontKey) CultureInfo culture, out Typeface typeface)
{ {
foreach (var customTypeface in _customTypefaces) foreach (var customTypeface in _customTypefaces)
{ {
@ -48,7 +48,7 @@ namespace Avalonia.Skia.UnitTests.Media
continue; continue;
} }
fontKey = new FontKey(customTypeface.FontFamily.Name, fontStyle, fontWeight); typeface = new Typeface(customTypeface.FontFamily.Name, fontStyle, fontWeight);
return true; return true;
} }
@ -56,7 +56,7 @@ namespace Avalonia.Skia.UnitTests.Media
var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight, var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint); SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint);
fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight); typeface = new Typeface(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight);
return true; return true;
} }
@ -73,13 +73,13 @@ namespace Avalonia.Skia.UnitTests.Media
skTypeface = typefaceCollection.Get(typeface); skTypeface = typefaceCollection.Get(typeface);
break; break;
} }
case "Noto Sans": case "Noto Sans":
{ {
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily); var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface); skTypeface = typefaceCollection.Get(typeface);
break; break;
} }
case FontFamily.DefaultFontFamilyName:
case "Noto Mono": case "Noto Mono":
{ {
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily);

2
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs

@ -130,7 +130,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{ {
using (Start()) using (Start())
{ {
const string text = "1234الدولي"; const string text = "ABCDالدولي";
var defaultProperties = new GenericTextRunProperties(Typeface.Default); var defaultProperties = new GenericTextRunProperties(Typeface.Default);

26
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
@ -331,6 +333,30 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
[Fact]
public void Should_Ignore_Invisible_Characters()
{
using (Start())
{
var defaultTextRunProperties =
new GenericTextRunProperties(Typeface.Default);
const string text = "01234567🎉\n";
var source = new SingleBufferTextSource(text, defaultTextRunProperties);
var textParagraphProperties = new GenericTextParagraphProperties(defaultTextRunProperties);
var formatter = TextFormatter.Current;
var textLine = formatter.FormatLine(source, 0, double.PositiveInfinity, textParagraphProperties);
var nextCharacterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(8, 2));
Assert.Equal(new CharacterHit(8, 2), nextCharacterHit);
}
}
private static IDisposable Start() private static IDisposable Start()
{ {
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface

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

Loading…
Cancel
Save