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
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
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
GlobalSection(SharedMSBuildProjectFiles) = preSolution
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|iPhoneSimulator.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -2070,6 +2122,7 @@ Global
{351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
{909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

25
azure-pipelines.yml

@ -35,16 +35,9 @@ jobs:
vmImage: 'macOS-10.14'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.101'
displayName: 'Use .NET Core SDK 3.1.401'
inputs:
packageType: sdk
version: 3.1.101
- task: UseDotNet@2
displayName: 'Use .NET Core Runtime 3.1.1'
inputs:
packageType: runtime
version: 3.1.1
version: 3.1.401
- task: CmdLine@2
displayName: 'Install Mono 5.18'
@ -63,13 +56,6 @@ jobs:
xcodeVersion: '10' # Options: 8, 9, default, specifyPath
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
displayName: 'Install Nuke'
inputs:
@ -88,7 +74,7 @@ jobs:
export PATH="$PATH:$HOME/.dotnet/tools"
dotnet --info
printenv
nuke --target CiAzureOSX --configuration Release
nuke --target CiAzureOSX --configuration Release --skip-previewer
- task: PublishTestResults@2
inputs:
@ -112,6 +98,11 @@ jobs:
pool:
vmImage: 'windows-2019'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.401'
inputs:
version: 3.1.401
- task: CmdLine@2
displayName: 'Install Nuke'
inputs:

4
build/HarfBuzzSharp.props

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

1
dirs.proj

@ -7,6 +7,7 @@
<ProjectReference Remove="**/*.shproj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*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 Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\Android')">-->
<ItemGroup>

2
global.json

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

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

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

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

@ -1209,6 +1209,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
bool _queuedDisplayFromThread;
NSTrackingArea* _area;
bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver;
AvnInputModifiers _modifierState;
NSEvent* _lastMouseDownEvent;
bool _lastKeyHandled;
AvnPixelSize _lastPixelSize;
@ -1251,6 +1252,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_lastPixelSize.Height = 100;
_lastPixelSize.Width = 100;
[self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]];
_modifierState = AvnInputModifiersNone;
return self;
}
@ -1594,6 +1597,63 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
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
{
[self keyboardEvent:event withType:KeyDown];

42
nukebuild/Build.cs

@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Nuke.Common;
using Nuke.Common.Git;
@ -15,6 +16,7 @@ using Nuke.Common.Tools.MSBuild;
using Nuke.Common.Tools.Npm;
using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections;
using Pharmacist.Core;
using static Nuke.Common.EnvironmentInfo;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction;
@ -124,6 +126,7 @@ partial class Build : NukeBuild
Target CompileHtmlPreviewer => _ => _
.DependsOn(Clean)
.OnlyWhenStatic(() => !Parameters.SkipPreviewer)
.Executes(() =>
{
var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp";
@ -139,7 +142,7 @@ partial class Build : NukeBuild
Target Compile => _ => _
.DependsOn(Clean)
.DependsOn(CompileHtmlPreviewer)
.Executes(() =>
.Executes(async () =>
{
if (Parameters.IsRunningOnWindows)
MsBuildCommon(Parameters.MSBuildSolution, c => c
@ -153,8 +156,44 @@ partial class Build : NukeBuild
.AddProperty("PackageVersion", Parameters.Version)
.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)
{
Information($"Running tests from {projectName}");
@ -202,6 +241,7 @@ partial class Build : NukeBuild
RunCoreTest("Avalonia.Visuals.UnitTests");
RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.Events.UnitTests");
});
Target RunRenderTests => _ => _

5
nukebuild/BuildParameters.cs

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

1
nukebuild/_build.csproj

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

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

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

10
samples/ControlCatalog/Pages/SplitViewPage.xaml

@ -58,12 +58,18 @@
<SplitView.Pane>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<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">
<!--Path glyph from materialdesignicons.com-->
<Border Width="48">
@ -76,7 +82,7 @@
<TextBlock Text="People" VerticalAlignment="Center" />
</StackPanel>
</ListBoxItem>
<TextBlock Grid.Row="2" Text="Item at bottom" Margin="60,12" />
<TextBlock Grid.Row="3" Text="Item at bottom" Margin="60,12" />
</Grid>
</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.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.
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 '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.
Total Issues: 16
Total Issues: 18

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

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

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

@ -8,6 +8,13 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// </summary>
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>
/// 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.

10
src/Avalonia.Controls/ColumnDefinitions.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using Avalonia.Collections;
namespace Avalonia.Controls
@ -13,7 +14,7 @@ namespace Avalonia.Controls
/// <summary>
/// Initializes a new instance of the <see cref="ColumnDefinitions"/> class.
/// </summary>
public ColumnDefinitions() : base ()
public ColumnDefinitions()
{
}
@ -27,6 +28,11 @@ namespace Avalonia.Controls
AddRange(GridLength.ParseLengths(s).Select(x => new ColumnDefinition(x)));
}
public override string ToString()
{
return string.Join(",", this.Select(x => x.Width));
}
/// <summary>
/// Parses a string representation of column definitions collection.
/// </summary>
@ -34,4 +40,4 @@ namespace Avalonia.Controls
/// <returns>The <see cref="ColumnDefinitions"/>.</returns>
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
{
Constraint = constraint,
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight),
Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
FontSize = FontSize,
Text = text ?? string.Empty,
TextAlignment = TextAlignment,
@ -499,7 +499,7 @@ namespace Avalonia.Controls.Presenters
return new FormattedText
{
Text = "X",
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily, FontStyle, FontWeight),
Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
FontSize = FontSize,
TextAlignment = TextAlignment,
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.")
};
SetValue(IsVisibleProperty, isVisible, BindingPriority.Style);
SetValue(IsVisibleProperty, isVisible);
}
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.Specialized;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Controls
@ -681,8 +681,15 @@ namespace Avalonia.Controls
if (oldValue != null)
{
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
foreach (var element in Children)
@ -699,8 +706,15 @@ namespace Avalonia.Controls
if (newValue != null)
{
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;

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()
{
if (_operation is object)

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

@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Utils;
#nullable enable
@ -234,6 +235,11 @@ namespace Avalonia.Controls.Selection
var shiftIndex = -1;
List<T>? removed = null;
if (!IsValidCollectionChange(e))
{
return;
}
switch (e.Action)
{
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
{
public int ShiftIndex;

62
src/Avalonia.Controls/SplitView.cs

@ -145,7 +145,7 @@ namespace Avalonia.Controls
private bool _isPaneOpen;
private Panel _pane;
private CompositeDisposable _pointerDisposables;
private IDisposable _pointerDisposable;
public SplitView()
{
@ -320,37 +320,14 @@ namespace Avalonia.Controls
var topLevel = this.VisualRoot;
if (topLevel is Window window)
{
//Logic adapted from Popup
//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));
_pointerDisposable = window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_pointerDisposables?.Dispose();
}
private void OnWindowLostFocus()
{
if (IsPaneOpen && ShouldClosePane())
{
IsPaneOpen = false;
}
_pointerDisposable?.Dispose();
}
private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
@ -371,7 +348,12 @@ namespace Avalonia.Controls
var src = e.Source as IVisual;
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;
break;
@ -385,31 +367,7 @@ namespace Avalonia.Controls
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()
{
return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay);

2
src/Avalonia.Controls/TextBlock.cs

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

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

@ -12,12 +12,13 @@ namespace Avalonia.Diagnostics.ViewModels
private readonly IVisual _control;
private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
private AvaloniaPropertyViewModel _selectedProperty;
private string _propertyFilter;
public ControlDetailsViewModel(IVisual control, string propertyFilter)
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
{
_control = control;
TreePage = treePage;
var properties = GetAvaloniaProperties(control)
.Concat(GetClrProperties(control))
.OrderBy(x => x, PropertyComparer.Instance)
@ -25,7 +26,6 @@ namespace Avalonia.Diagnostics.ViewModels
.ToList();
_propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList());
_propertyFilter = propertyFilter;
var view = new DataGridCollectionView(properties);
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
{
get => _propertyFilter;
set
{
if (RaiseAndSetIfChanged(ref _propertyFilter, value))
{
PropertiesView.Refresh();
}
}
}
public DataGridCollectionView PropertiesView { get; }
public AvaloniaPropertyViewModel SelectedProperty
{
@ -137,9 +127,14 @@ namespace Avalonia.Diagnostics.ViewModels
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;

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

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

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

@ -8,8 +8,8 @@ namespace Avalonia.Diagnostics.ViewModels
internal abstract class PropertyViewModel : ViewModelBase
{
private const BindingFlags PublicStatic = BindingFlags.Public | BindingFlags.Static;
private static readonly Type[] StringParameter = new[] { typeof(string) };
private static readonly Type[] StringIFormatProviderParameters = new[] { typeof(string), typeof(IFormatProvider) };
private static readonly Type[] StringParameter = { typeof(string) };
private static readonly Type[] StringIFormatProviderParameters = { typeof(string), typeof(IFormatProvider) };
public abstract object Key { get; }
public abstract string Name { get; }
@ -26,35 +26,46 @@ namespace Avalonia.Diagnostics.ViewModels
}
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);
if (converter != null && converter.CanConvertFrom(typeof(string)))
var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
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)
{
return method.Invoke(null, new object[] { s, CultureInfo.InvariantCulture });
}
throw new InvalidCastException("Unable to convert value.");
}
method = targetType.GetMethod("Parse", PublicStatic, null, StringParameter, null);
protected static object ConvertFromString(string s, Type targetType)
{
var converter = TypeDescriptor.GetConverter(targetType);
if (method != null)
{
return method.Invoke(null, new object[] { s });
}
if (converter.CanConvertFrom(typeof(string)))
{
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.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text.RegularExpressions;
using Avalonia.Controls;
using Avalonia.Controls.Selection;
using Avalonia.VisualTree;
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 ControlDetailsViewModel _details;
private string _propertyFilter;
private string _propertyFilter = string.Empty;
private bool _useRegexFilter;
public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes)
{
@ -26,15 +31,10 @@ namespace Avalonia.Diagnostics.ViewModels
get => _selectedNode;
private set
{
if (Details != null)
{
_propertyFilter = Details.PropertyFilter;
}
if (RaiseAndSetIfChanged(ref _selectedNode, value))
{
Details = value != null ?
new ControlDetailsViewModel(value.Visual, _propertyFilter) :
new ControlDetailsViewModel(this, value.Visual) :
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()
{
foreach (var node in Nodes)
@ -130,5 +187,17 @@ namespace Avalonia.Diagnostics.ViewModels
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:conv="clr-namespace:Avalonia.Diagnostics.Converters"
x:Class="Avalonia.Diagnostics.Views.ControlDetailsView">
<Grid ColumnDefinitions="*">
<DockPanel Grid.Column="0">
<TextBox DockPanel.Dock="Top"
BorderThickness="0"
Text="{Binding PropertyFilter}"
Watermark="Filter properties"/>
<DataGrid Items="{Binding PropertiesView}"
BorderThickness="0"
RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
CanUserResizeColumns="true">
<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>
</DockPanel>
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,*">
<TextBox Grid.Row="0" Grid.Column="0"
BorderThickness="0"
Text="{Binding TreePage.PropertyFilter}"
Watermark="Filter properties" />
<CheckBox Grid.Row="0"
Grid.Column="1"
Content="Regex"
IsChecked="{Binding TreePage.UseRegexFilter}"/>
<DataGrid Items="{Binding PropertiesView}"
Grid.Row="1" Grid.ColumnSpan="2"
BorderThickness="0"
RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
CanUserResizeColumns="true">
<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>
</UserControl>

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

@ -24,6 +24,20 @@
IsEnabled="False"/>
</MenuItem.Icon>
</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>
</Menu>

4
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -155,9 +155,9 @@ namespace Avalonia.Headless
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;
}
}

22
src/Avalonia.Input/AccessKeyHandler.cs

@ -28,7 +28,7 @@ namespace Avalonia.Input
/// <summary>
/// The window to which the handler belongs.
/// </summary>
private IInputRoot _owner;
private IInputRoot? _owner;
/// <summary>
/// Whether access keys are currently being shown;
@ -48,17 +48,17 @@ namespace Avalonia.Input
/// <summary>
/// Element to restore following AltKey taking focus.
/// </summary>
private IInputElement _restoreFocusElement;
private IInputElement? _restoreFocusElement;
/// <summary>
/// The window's main menu.
/// </summary>
private IMainMenu _mainMenu;
private IMainMenu? _mainMenu;
/// <summary>
/// Gets or sets the window's main menu.
/// </summary>
public IMainMenu MainMenu
public IMainMenu? MainMenu
{
get => _mainMenu;
set
@ -86,14 +86,12 @@ namespace Avalonia.Input
/// </remarks>
public void SetOwner(IInputRoot owner)
{
Contract.Requires<ArgumentNullException>(owner != null);
if (_owner != null)
{
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, 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
// access key markers in the window (i.e. "_File").
_owner.ShowAccessKeys = _showingAccessKeys = true;
_owner!.ShowAccessKeys = _showingAccessKeys = true;
}
else
{
@ -241,7 +239,7 @@ namespace Avalonia.Input
{
if (_showingAccessKeys)
{
_owner.ShowAccessKeys = false;
_owner!.ShowAccessKeys = false;
}
}
@ -250,13 +248,13 @@ namespace Avalonia.Input
/// </summary>
private void CloseMenu()
{
MainMenu.Close();
_owner.ShowAccessKeys = _showingAccessKeys = false;
MainMenu!.Close();
_owner!.ShowAccessKeys = _showingAccessKeys = false;
}
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">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>Enable</Nullable>
<WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

6
src/Avalonia.Input/DataObject.cs

@ -11,7 +11,7 @@ namespace Avalonia.Input
return _items.ContainsKey(dataFormat);
}
public object Get(string dataFormat)
public object? Get(string dataFormat)
{
if (_items.ContainsKey(dataFormat))
return _items[dataFormat];
@ -23,12 +23,12 @@ namespace Avalonia.Input
return _items.Keys;
}
public IEnumerable<string> GetFileNames()
public IEnumerable<string>? GetFileNames()
{
return Get(DataFormats.FileNames) as IEnumerable<string>;
}
public string GetText()
public string? GetText()
{
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();
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();
if (target != null && DragDrop.GetAllowDrop(target))
@ -19,7 +19,7 @@ namespace Avalonia.Input
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)
return DragDropEffects.None;

47
src/Avalonia.Input/FocusManager.cs

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

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

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

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

@ -1,6 +1,5 @@
using System;
using System.Diagnostics;
using Avalonia.Interactivity;
using Avalonia.Threading;
namespace Avalonia.Input.GestureRecognizers
@ -11,9 +10,9 @@ namespace Avalonia.Input.GestureRecognizers
{
private bool _scrolling;
private Point _trackedRootPoint;
private IPointer _tracking;
private IInputElement _target;
private IGestureRecognizerActionsDispatcher _actions;
private IPointer? _tracking;
private IInputElement? _target;
private IGestureRecognizerActionsDispatcher? _actions;
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private int _gestureId;
@ -95,7 +94,7 @@ namespace Avalonia.Input.GestureRecognizers
_scrolling = true;
if (_scrolling)
{
_actions.Capture(e.Pointer, this);
_actions!.Capture(e.Pointer, this);
}
}
@ -110,7 +109,7 @@ namespace Avalonia.Input.GestureRecognizers
_trackedRootPoint = rootPoint;
if (elapsed.TotalSeconds > 0)
_inertia = vector / elapsed.TotalSeconds;
_target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
_target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
e.Handled = true;
}
}
@ -128,7 +127,7 @@ namespace Avalonia.Input.GestureRecognizers
{
_inertia = default;
_scrolling = false;
_target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
_target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
_gestureId = 0;
_lastMoveTimestamp = null;
}
@ -165,7 +164,7 @@ namespace Avalonia.Input.GestureRecognizers
var speed = _inertia * Math.Pow(0.15, st.Elapsed.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>(
"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);
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
static Gestures()
{
@ -69,6 +71,11 @@ namespace Avalonia.Input
private static void PointerPressed(RoutedEventArgs ev)
{
if (ev.Source is null)
{
return;
}
if (ev.Route == RoutingStrategies.Bubble)
{
var e = (PointerPressedEventArgs)ev;
@ -76,7 +83,7 @@ namespace Avalonia.Input
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)
{

2
src/Avalonia.Input/IAccessKeyHandler.cs

@ -8,7 +8,7 @@ namespace Avalonia.Input
/// <summary>
/// Gets or sets the window's main menu.
/// </summary>
IMainMenu MainMenu { get; set; }
IMainMenu? MainMenu { get; set; }
/// <summary>
/// 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.
/// <seealso cref="DataFormats.Text"/>
/// </summary>
string GetText();
string? GetText();
/// <summary>
/// Returns a list of filenames if the DataObject contains filenames.
/// <seealso cref="DataFormats.FileNames"/>
/// </summary>
IEnumerable<string> GetFileNames();
IEnumerable<string>? GetFileNames();
/// <summary>
/// Tries to get the data of the given DataFormat.
/// </summary>
object Get(string dataFormat);
object? Get(string dataFormat);
}
}

6
src/Avalonia.Input/IFocusManager.cs

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

2
src/Avalonia.Input/IInputElement.cs

@ -78,7 +78,7 @@ namespace Avalonia.Input
/// <summary>
/// Gets or sets the associated mouse cursor.
/// </summary>
Cursor Cursor { get; }
Cursor? Cursor { get; }
/// <summary>
/// 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>
/// Gets or sets the input element that the pointer is currently over.
/// </summary>
IInputElement PointerOverElement { get; set; }
IInputElement? PointerOverElement { get; set; }
/// <summary>
/// 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
/// </summary>
[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
{
IInputElement FocusedElement { get; }
IInputElement? FocusedElement { get; }
void SetFocusedElement(
IInputElement element,
IInputElement? element,
NavigationMethod method,
KeyModifiers modifiers);
}

4
src/Avalonia.Input/IPointer.cs

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

4
src/Avalonia.Input/IPointerDevice.cs

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

8
src/Avalonia.Input/InputElement.cs

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

2
src/Avalonia.Input/KeyEventArgs.cs

@ -5,7 +5,7 @@ namespace Avalonia.Input
{
public class KeyEventArgs : RoutedEventArgs
{
public IKeyboardDevice Device { get; set; }
public IKeyboardDevice? Device { 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
{
private IInputElement _focusedElement;
private IInputElement? _focusedElement;
public event PropertyChangedEventHandler PropertyChanged;
public event PropertyChangedEventHandler? PropertyChanged;
public static IKeyboardDevice Instance => AvaloniaLocator.Current.GetService<IKeyboardDevice>();
@ -18,7 +18,7 @@ namespace Avalonia.Input
public IFocusManager FocusManager => AvaloniaLocator.Current.GetService<IFocusManager>();
public IInputElement FocusedElement
public IInputElement? FocusedElement
{
get
{
@ -33,7 +33,7 @@ namespace Avalonia.Input
}
public void SetFocusedElement(
IInputElement element,
IInputElement? element,
NavigationMethod method,
KeyModifiers keyModifiers)
{

13
src/Avalonia.Input/KeyboardNavigationHandler.cs

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

73
src/Avalonia.Input/MouseDevice.cs

@ -20,7 +20,7 @@ namespace Avalonia.Input
private readonly Pointer _pointer;
private bool _disposed;
public MouseDevice(Pointer pointer = null)
public MouseDevice(Pointer? pointer = null)
{
_pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
}
@ -34,7 +34,7 @@ namespace Avalonia.Input
/// <see cref="Capture"/> method.
/// </remarks>
[Obsolete("Use IPointer instead")]
public IInputElement Captured => _pointer.Captured;
public IInputElement? Captured => _pointer.Captured;
/// <summary>
/// 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
/// by the <see cref="Captured"/> property.
/// </remarks>
public void Capture(IInputElement control)
public void Capture(IInputElement? control)
{
_pointer.Capture(control);
}
@ -66,7 +66,7 @@ namespace Avalonia.Input
/// <returns>The mouse position in the control's coordinates.</returns>
public Point GetPosition(IVisual relativeTo)
{
Contract.Requires<ArgumentNullException>(relativeTo != null);
relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo));
if (relativeTo.VisualRoot == null)
{
@ -75,7 +75,7 @@ namespace Avalonia.Input
var rootPoint = relativeTo.VisualRoot.PointToClient(Position);
var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo);
return rootPoint * transform.Value;
return rootPoint * transform!.Value;
}
public void ProcessRawEvent(RawInputEventArgs e)
@ -126,7 +126,7 @@ namespace Avalonia.Input
private void ProcessRawEvent(RawPointerEventArgs e)
{
Contract.Requires<ArgumentNullException>(e != null);
e = e ?? throw new ArgumentNullException(nameof(e));
var mouse = (MouseDevice)e.Device;
if(mouse._disposed)
@ -173,8 +173,8 @@ namespace Avalonia.Input
private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties,
KeyModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
ClearPointerOver(this, timestamp, root, properties, inputModifiers);
}
@ -214,8 +214,8 @@ namespace Avalonia.Input
PointerPointProperties properties,
KeyModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
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,
KeyModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
IInputElement source;
IInputElement? source;
if (_pointer.Captured == null)
{
@ -265,18 +265,23 @@ namespace Avalonia.Input
source = _pointer.Captured;
}
var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root,
p, timestamp, properties, inputModifiers);
if (source is object)
{
var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root,
p, timestamp, properties, inputModifiers);
source?.RaiseEvent(e);
return e.Handled;
source.RaiseEvent(e);
return e.Handled;
}
return false;
}
private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props,
KeyModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p);
@ -298,8 +303,8 @@ namespace Avalonia.Input
PointerPointProperties props,
Vector delta, KeyModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var hit = HitTest(root, p);
@ -317,21 +322,21 @@ namespace Avalonia.Input
private IInteractive GetSource(IVisual hit)
{
Contract.Requires<ArgumentNullException>(hit != null);
hit = hit ?? throw new ArgumentNullException(nameof(hit));
return _pointer.Captured ??
(hit as IInteractive) ??
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);
}
PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source,
PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source,
PointerPointProperties properties,
KeyModifiers inputModifiers)
{
@ -343,8 +348,8 @@ namespace Avalonia.Input
PointerPointProperties properties,
KeyModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var element = root.PointerOverElement;
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,
KeyModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
var element = root.InputHitTest(p);
@ -412,11 +417,11 @@ namespace Avalonia.Input
PointerPointProperties properties,
KeyModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
Contract.Requires<ArgumentNullException>(element != null);
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
element = element ?? throw new ArgumentNullException(nameof(element));
IInputElement branch = null;
IInputElement? branch = null;
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"/>
/// was the last in the requested direction.
/// </returns>
public static IInputElement GetNextInTabOrder(
public static IInputElement? GetNextInTabOrder(
IInputElement element,
NavigationDirection direction,
bool outsideElement = false)
{
Contract.Requires<ArgumentNullException>(element != null);
Contract.Requires<ArgumentException>(
direction == NavigationDirection.Next ||
direction == NavigationDirection.Previous);
element = element ?? throw new ArgumentNullException(nameof(element));
if (direction != NavigationDirection.Next && direction != NavigationDirection.Previous)
{
throw new ArgumentException("Invalid direction: must be Next or Previous.");
}
var container = element.GetVisualParent<IInputElement>();
@ -110,7 +112,7 @@ namespace Avalonia.Input.Navigation
if (customNext.handled)
{
yield return customNext.next;
yield return customNext.next!;
}
else
{
@ -143,12 +145,14 @@ namespace Avalonia.Input.Navigation
/// If true will not descend into <paramref name="element"/> to find next control.
/// </param>
/// <returns>The next element, or null if the element is the last.</returns>
private static IInputElement GetNextInContainer(
private static IInputElement? GetNextInContainer(
IInputElement element,
IInputElement container,
NavigationDirection direction,
bool outsideElement)
{
IInputElement? e = element;
if (direction == NavigationDirection.Next && !outsideElement)
{
var descendant = GetFocusableDescendants(element, direction).FirstOrDefault();
@ -167,13 +171,13 @@ namespace Avalonia.Input.Navigation
// INavigableContainer.
if (navigable != null)
{
while (element != null)
while (e != null)
{
element = navigable.GetControl(direction, element, false);
e = navigable.GetControl(direction, e, false);
if (element != null &&
element.CanFocus() &&
KeyboardNavigation.GetIsTabStop((InputElement) element))
if (e != null &&
e.CanFocus() &&
KeyboardNavigation.GetIsTabStop((InputElement)e))
{
break;
}
@ -183,12 +187,12 @@ namespace Avalonia.Input.Navigation
{
// TODO: Do a spatial search here if the container doesn't implement
// 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)
{
@ -196,7 +200,7 @@ namespace Avalonia.Input.Navigation
}
}
return element;
return e;
}
return null;
@ -209,13 +213,13 @@ namespace Avalonia.Input.Navigation
/// <param name="container">The container.</param>
/// <param name="direction">The direction of the search.</param>
/// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement GetFirstInNextContainer(
private static IInputElement? GetFirstInNextContainer(
IInputElement element,
IInputElement container,
NavigationDirection direction)
{
var parent = container.GetVisualParent<IInputElement>();
IInputElement next = null;
IInputElement? next = null;
if (parent != null)
{
@ -268,7 +272,7 @@ namespace Avalonia.Input.Navigation
return next;
}
private static (bool handled, IInputElement next) GetCustomNext(IInputElement element,
private static (bool handled, IInputElement? next) GetCustomNext(IInputElement element,
NavigationDirection direction)
{
if (element is ICustomKeyboardNavigation custom)

9
src/Avalonia.Input/Pointer.cs

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace Avalonia.Input
@ -20,7 +19,7 @@ namespace Avalonia.Input
public int Id { get; }
IInputElement FindCommonParent(IInputElement control1, IInputElement control2)
IInputElement? FindCommonParent(IInputElement? control1, IInputElement? control2)
{
if (control1 == null || control2 == null)
return null;
@ -28,12 +27,12 @@ namespace Avalonia.Input
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)
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 bool IsPrimary { get; }

14
src/Avalonia.Input/PointerEventArgs.cs

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

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

@ -7,4 +7,4 @@
DragLeave,
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>
public RawInputEventArgs(IInputDevice device, ulong timestamp, IInputRoot root)
{
Contract.Requires<ArgumentNullException>(device != null);
device = device ?? throw new ArgumentNullException(nameof(device));
Device = device;
Timestamp = timestamp;

4
src/Avalonia.Input/TextInputEventArgs.cs

@ -4,8 +4,8 @@ namespace Avalonia.Input
{
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)
{
base.OnDetachedFromVisualTreeCore(e);
if (e.Root is ILayoutRoot r)
{
if (_layoutUpdated is object)
@ -772,6 +770,8 @@ namespace Avalonia.Layout
r.LayoutManager.UnregisterEffectiveViewportListener(this);
}
}
base.OnDetachedFromVisualTreeCore(e);
}
/// <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
}
public List<PlatformApi> AllowedPlatformApis = new List<PlatformApi>
{
PlatformApi.DirectX9
};
public IList<PlatformApi> AllowedPlatformApis { get; set; } = null;
}
}

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_DEVICE_TYPE_HARDWARE_ANGLE = 0x320A;
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
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_DEVICE_TYPE_D3D_WARP_ANGLE = 0x320B;
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;
public IntPtr Handle => _display;
private AngleOptions.PlatformApi? _angleApi;
private int _sampleCount;
private int _stencilSize;
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 (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
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);
if (display == IntPtr.Zero)
display = egl.GetDisplay(IntPtr.Zero);
}
else
{
if (_egl.GetPlatformDisplayEXT == null)
if (egl.GetPlatformDisplayEXT == null)
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)
throw OpenGlException.GetFormattedException("eglGetDisplay", _egl);
public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs)
: 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))
throw OpenGlException.GetFormattedException("eglInitialize", _egl);
@ -172,5 +156,15 @@ namespace Avalonia.OpenGL
throw OpenGlException.GetFormattedException("eglCreateWindowSurface", _egl);
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)
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
{
var disp = new EglDisplay();
var disp = displayFactory();
return new EglGlPlatformFeature
{
_display = disp,

84
src/Avalonia.OpenGL/EglGlPlatformSurface.cs

@ -3,33 +3,26 @@ using System.Threading;
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 EglContext _context;
private readonly IEglWindowGlPlatformSurfaceInfo _info;
public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info)
public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info) : base()
{
_display = context.Display;
_context = context;
_info = info;
}
public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget()
public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget()
{
var glSurface = _display.CreateWindowSurface(_info.Handle);
return new RenderTarget(_display, _context, glSurface, _info);
}
class RenderTarget : IGlPlatformSurfaceRenderTargetWithCorruptionInfo
class RenderTarget : EglPlatformSurfaceRenderTargetBase
{
private readonly EglDisplay _display;
private readonly EglContext _context;
@ -38,7 +31,7 @@ namespace Avalonia.OpenGL
private PixelSize _initialSize;
public RenderTarget(EglDisplay display, EglContext context,
EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info)
EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) : base(display, context)
{
_display = display;
_context = context;
@ -47,70 +40,11 @@ namespace Avalonia.OpenGL
_initialSize = info.Size;
}
public void Dispose() => _glSurface.Dispose();
public override void Dispose() => _glSurface.Dispose();
public 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;
}
}
public override bool IsCorrupted => _initialSize != _info.Size;
class Session : IGlPlatformSurfaceRenderingSession
{
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; }
}
public override IGlPlatformSurfaceRenderingSession BeginDraw() => base.BeginDraw(_glSurface, _info);
}
}
}

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))
{
}
[DllImport("libegl.dll", CharSet = CharSet.Ansi)]
static extern IntPtr eglGetProcAddress(string proc);
static Func<string, IntPtr> Load()
{
var os = AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem;
if(os == OperatingSystemType.Linux || os == OperatingSystemType.Android)
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();
}
@ -147,6 +143,21 @@ namespace Avalonia.OpenGL
return null;
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
}

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="Foreground" Value="{DynamicResource CheckBoxForegroundUnchecked}" />
<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="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Top" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="MinWidth" Value="120" />
<Setter Property="MinHeight" Value="32" />

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

@ -13,11 +13,11 @@
<Setter Property="Background" Value="{DynamicResource RadioButtonBackground}" />
<Setter Property="Foreground" Value="{DynamicResource RadioButtonForeground}" />
<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="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Top" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="MinWidth" Value="120" />
<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>
public sealed class FontManager
{
private readonly ConcurrentDictionary<FontKey, Typeface> _typefaceCache =
new ConcurrentDictionary<FontKey, Typeface>();
private readonly ConcurrentDictionary<Typeface, GlyphTypeface> _glyphTypefaceCache =
new ConcurrentDictionary<Typeface, GlyphTypeface>();
private readonly FontFamily _defaultFontFamily;
public FontManager(IFontManagerImpl platformImpl)
@ -76,79 +76,52 @@ namespace Avalonia.Media
PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
/// <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>
/// <param name="fontFamily">The font family.</param>
/// <param name="fontStyle">The font style.</param>
/// <param name="fontWeight">The font weight.</param>
/// <param name="typeface">The typeface.</param>
/// <returns>
/// The typeface.
/// The <see cref="GlyphTypeface"/>.
/// </returns>
public Typeface GetOrAddTypeface(FontFamily fontFamily, FontStyle fontStyle = FontStyle.Normal,
FontWeight fontWeight = FontWeight.Normal)
public GlyphTypeface GetOrAddGlyphTypeface(Typeface typeface)
{
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 (_typefaceCache.TryAdd(key, typeface))
{
return typeface;
}
if (fontFamily == _defaultFontFamily)
if (typeface.FontFamily == _defaultFontFamily)
{
return null;
}
fontFamily = _defaultFontFamily;
typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight);
}
}
/// <summary>
/// Tries to match a specified character to a typeface that supports specified font properties.
/// Returns <c>null</c> if no fallback was found.
/// Tries to match a specified character to a <see cref="Typeface"/> that supports specified font properties.
/// </summary>
/// <param name="codepoint">The codepoint to match against.</param>
/// <param name="fontStyle">The font style.</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="culture">The culture.</param>
/// <param name="typeface">The matching <see cref="Typeface"/>.</param>
/// <returns>
/// The matched typeface.
/// <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
/// </returns>
public Typeface MatchCharacter(int codepoint,
FontStyle fontStyle = FontStyle.Normal,
FontWeight fontWeight = FontWeight.Normal,
FontFamily fontFamily = null, CultureInfo culture = null)
{
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;
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
FontWeight fontWeight,
FontFamily fontFamily, CultureInfo culture, out Typeface typeface) =>
PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out typeface);
}
}

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 _);
//ToDo: Fix FontFamily fallback
currentTypeface =
FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.FontFamily);
var matchFound =
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
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)
{
var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
_paragraphProperties, previousLine?.LineBreak);
_paragraphProperties, previousLine?.TextLineBreak);
currentPosition += textLine.TextRange.Length;
@ -230,7 +230,7 @@ namespace Avalonia.Media.TextFormatting
if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) &&
height + textLine.LineMetrics.Size.Height > MaxHeight)
{
if (previousLine?.LineBreak != null && _textTrimming != TextTrimming.None)
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{
var collapsedLine =
previousLine.Collapse(GetCollapsingProperties(MaxWidth));
@ -255,7 +255,7 @@ namespace Avalonia.Media.TextFormatting
previousLine = textLine;
if (currentPosition != _text.Length || textLine.LineBreak == null)
if (currentPosition != _text.Length || textLine.TextLineBreak == null)
{
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.
/// </summary>
/// <returns>
/// A <see cref="LineBreak"/> value that represents the line break.
/// A <see cref="TextLineBreak"/> value that represents the line break.
/// </returns>
public abstract TextLineBreak LineBreak { get; }
public abstract TextLineBreak TextLineBreak { get; }
/// <summary>
/// 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;
LineMetrics = lineMetrics;
LineBreak = lineBreak;
TextLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
}
@ -27,7 +27,7 @@ namespace Avalonia.Media.TextFormatting
public override TextLineMetrics LineMetrics { get; }
/// <inheritdoc/>
public override TextLineBreak LineBreak { get; }
public override TextLineBreak TextLineBreak { get; }
/// <inheritdoc/>
public override bool HasCollapsed { get; }
@ -122,7 +122,7 @@ namespace Avalonia.Media.TextFormatting
textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height),
LineMetrics.TextBaseline, textRange, false);
return new TextLineImpl(shapedTextCharacters, textLineMetrics, LineBreak, true);
return new TextLineImpl(shapedTextCharacters, textLineMetrics, TextLineBreak, true);
}
availableWidth -= currentRun.GlyphRun.Bounds.Width;
@ -268,6 +268,17 @@ namespace Avalonia.Media.TextFormatting
var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
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 ?
foundCharacterHit :
new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);

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

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

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

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

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

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

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

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

4
src/Avalonia.Visuals/Visual.cs

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

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

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

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

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

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

@ -2,31 +2,28 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using SkiaSharp;
namespace Avalonia.Skia
{
internal class SKTypefaceCollection
{
private readonly ConcurrentDictionary<FontKey, SKTypeface> _typefaces =
new ConcurrentDictionary<FontKey, SKTypeface>();
private readonly ConcurrentDictionary<Typeface, SKTypeface> _typefaces =
new ConcurrentDictionary<Typeface, SKTypeface>();
public void AddTypeface(FontKey key, SKTypeface typeface)
public void AddTypeface(Typeface key, SKTypeface typeface)
{
_typefaces.TryAdd(key, typeface);
}
public SKTypeface Get(Typeface typeface)
{
var key = new FontKey(typeface.FontFamily.Name, typeface.Style, typeface.Weight);
return GetNearestMatch(_typefaces, key);
return GetNearestMatch(_typefaces, typeface);
}
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;
}
@ -42,7 +39,7 @@ namespace Avalonia.Skia
{
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;
}
@ -53,7 +50,7 @@ namespace Avalonia.Skia
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;
}

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

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

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

@ -16,6 +16,10 @@ namespace Avalonia
/// <summary>
/// The maximum number of bytes for video memory to store textures and resources.
/// </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.Globalization;
using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Platform;
using SharpDX.DirectWrite;
using FontFamily = Avalonia.Media.FontFamily;
@ -34,7 +33,7 @@ namespace Avalonia.Direct2D1.Media
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle,
FontWeight fontWeight,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
FontFamily fontFamily, CultureInfo culture, out Typeface typeface)
{
var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount;
@ -51,12 +50,12 @@ namespace Avalonia.Direct2D1.Media
var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
fontKey = new FontKey(fontFamilyName, fontStyle, fontWeight);
typeface = new Typeface(fontFamilyName, fontStyle, fontWeight);
return true;
}
fontKey = default;
typeface = default;
return false;
}

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

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

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

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

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

@ -1335,6 +1335,47 @@ namespace Avalonia.Controls.UnitTests.Primitives
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)
{
var root = new TestRoot
@ -1397,6 +1438,28 @@ namespace Avalonia.Controls.UnitTests.Primitives
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
{
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, 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

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, 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

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()
{
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>();
return r.CreateFormattedText(text,
FontManager.Current.GetOrAddTypeface(fontFamily, fontStyle, fontWeight),
new Typeface(fontFamily, fontStyle, fontWeight),
fontSize,
textAlignment,
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 };
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)
{
@ -48,7 +48,7 @@ namespace Avalonia.Skia.UnitTests.Media
continue;
}
fontKey = new FontKey(customTypeface.FontFamily.Name, fontStyle, fontWeight);
typeface = new Typeface(customTypeface.FontFamily.Name, fontStyle, fontWeight);
return true;
}
@ -56,7 +56,7 @@ namespace Avalonia.Skia.UnitTests.Media
var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint);
fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight);
typeface = new Typeface(fallback?.FamilyName ?? _defaultFamilyName, fontStyle, fontWeight);
return true;
}
@ -73,13 +73,13 @@ namespace Avalonia.Skia.UnitTests.Media
skTypeface = typefaceCollection.Get(typeface);
break;
}
case "Noto Sans":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
break;
}
case FontFamily.DefaultFontFamilyName:
case "Noto Mono":
{
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())
{
const string text = "1234الدولي";
const string text = "ABCDالدولي";
var defaultProperties = new GenericTextRunProperties(Typeface.Default);

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

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Media;
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()
{
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface

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

Loading…
Cancel
Save