Browse Source

Merge branch 'master' into value-storage-perf-refactor

pull/8287/head
Steven Kirk 4 years ago
committed by GitHub
parent
commit
f6d15d3e71
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .editorconfig
  2. 0
      .ncrunch/ControlCatalog.net6.0.v3.ncrunchproject
  3. 5
      .ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject
  4. 1502
      Avalonia.sln
  5. 67
      azure-pipelines-integrationtests.yml
  6. 1
      build/CoreLibraries.props
  7. 2
      build/SourceGenerators.props
  8. 4
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  9. 13
      native/Avalonia.Native/src/OSX/AvnView.mm
  10. 52
      native/Avalonia.Native/src/OSX/SystemDialogs.mm
  11. 20
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  12. 20
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  13. 6
      native/Avalonia.Native/src/OSX/rendertarget.mm
  14. 1
      nukebuild/Build.cs
  15. 32
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  16. 3
      samples/ControlCatalog.Android/Properties/AndroidManifest.xml
  17. 1
      samples/ControlCatalog.Android/environment.device.txt
  18. 1
      samples/ControlCatalog.Android/environment.emulator.txt
  19. 3
      samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs
  20. 9
      samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs
  21. 18
      samples/ControlCatalog.NetCore/Program.cs
  22. 1
      samples/ControlCatalog.Web/App.razor.cs
  23. 1
      samples/ControlCatalog/App.xaml
  24. 2
      samples/ControlCatalog/ControlCatalog.csproj
  25. 10
      samples/ControlCatalog/Converter/MathSubtractConverter.cs
  26. 10
      samples/ControlCatalog/DecoratedWindow.xaml.cs
  27. 8
      samples/ControlCatalog/MainView.xaml
  28. 21
      samples/ControlCatalog/MainView.xaml.cs
  29. 6
      samples/ControlCatalog/MainWindow.xaml.cs
  30. 4
      samples/ControlCatalog/Models/Countries.cs
  31. 4
      samples/ControlCatalog/Models/GDPValueConverter.cs
  32. 16
      samples/ControlCatalog/Models/Person.cs
  33. 33
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
  34. 2
      samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs
  35. 4
      samples/ControlCatalog/Pages/ButtonsPage.xaml.cs
  36. 12
      samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs
  37. 4
      samples/ControlCatalog/Pages/CalendarPage.xaml.cs
  38. 11
      samples/ControlCatalog/Pages/CarouselPage.xaml.cs
  39. 44
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  40. 45
      samples/ControlCatalog/Pages/CompositionPage.axaml
  41. 153
      samples/ControlCatalog/Pages/CompositionPage.axaml.cs
  42. 22
      samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs
  43. 6
      samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
  44. 8
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  45. 4
      samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs
  46. 76
      samples/ControlCatalog/Pages/DialogsPage.xaml
  47. 262
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  48. 6
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  49. 8
      samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
  50. 13
      samples/ControlCatalog/Pages/ImagePage.xaml.cs
  51. 26
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  52. 2
      samples/ControlCatalog/Pages/LabelsPage.axaml.cs
  53. 2
      samples/ControlCatalog/Pages/MenuPage.xaml.cs
  54. 2
      samples/ControlCatalog/Pages/NativeEmbedPage.xaml.cs
  55. 8
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  56. 23
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
  57. 235
      samples/ControlCatalog/Pages/PointerCanvas.cs
  58. 109
      samples/ControlCatalog/Pages/PointerContactsTab.cs
  59. 322
      samples/ControlCatalog/Pages/PointersPage.cs
  60. 66
      samples/ControlCatalog/Pages/PointersPage.xaml
  61. 78
      samples/ControlCatalog/Pages/PointersPage.xaml.cs
  62. 27
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  63. 2
      samples/ControlCatalog/Pages/ScreenPage.cs
  64. 12
      samples/ControlCatalog/Pages/TabControlPage.xaml.cs
  65. 2
      samples/ControlCatalog/Pages/TabStripPage.xaml.cs
  66. 4
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  67. 2
      samples/ControlCatalog/ViewModels/ApplicationViewModel.cs
  68. 2
      samples/ControlCatalog/ViewModels/ContextPageViewModel.cs
  69. 2
      samples/ControlCatalog/ViewModels/CursorPageViewModel.cs
  70. 19
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  71. 13
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  72. 8
      samples/ControlCatalog/ViewModels/MenuItemViewModel.cs
  73. 2
      samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
  74. 4
      samples/ControlCatalog/ViewModels/NotificationViewModel.cs
  75. 14
      samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs
  76. 4
      samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
  77. 21
      samples/IntegrationTestApp/MainWindow.axaml
  78. 66
      samples/IntegrationTestApp/MainWindow.axaml.cs
  79. 35
      samples/IntegrationTestApp/ShowWindowTest.axaml
  80. 40
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  81. 4
      samples/RenderDemo/App.xaml.cs
  82. 4
      samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
  83. 12
      samples/RenderDemo/Pages/GlyphRunPage.xaml
  84. 113
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  85. 5
      src/Android/Avalonia.Android/AndroidPlatform.cs
  86. 39
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  87. 3
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  88. 8
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  89. 244
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  90. 177
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
  91. 20
      src/Android/Avalonia.Android/SystemDialogImpl.cs
  92. 306
      src/Avalonia.Base/Animation/Easings/CubicBezier.cs
  93. 27
      src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs
  94. BIN
      src/Avalonia.Base/Assets/BiDi.trie
  95. BIN
      src/Avalonia.Base/Assets/GraphemeBreak.trie
  96. BIN
      src/Avalonia.Base/Assets/UnicodeData.trie
  97. 13
      src/Avalonia.Base/Avalonia.Base.csproj
  98. 3
      src/Avalonia.Base/AvaloniaObject.cs
  99. 1
      src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs
  100. 2
      src/Avalonia.Base/Collections/Pooled/PooledList.cs

1
.editorconfig

@ -21,6 +21,7 @@ csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# trim_trailing_whitespace = true
# Indentation preferences
csharp_indent_block_contents = true

0
.ncrunch/ControlCatalog.v3.ncrunchproject → .ncrunch/ControlCatalog.net6.0.v3.ncrunchproject

5
.ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

1502
Avalonia.sln

File diff suppressed because it is too large

67
azure-pipelines-integrationtests.yml

@ -0,0 +1,67 @@
# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml
trigger:
- master
jobs:
- job: Mac
pool:
name: 'AvaloniaMacPool'
steps:
- script: system_profiler SPDisplaysDataType |grep Resolution
- script: |
pkill node
appium &
pkill IntegrationTestApp
./build.sh CompileNative
rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")")
pkill IntegrationTestApp
./samples/IntegrationTestApp/bundle.sh
open -n ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app
pkill IntegrationTestApp
- task: DotNetCoreCLI@2
inputs:
command: 'test'
projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj'
- script: |
pkill IntegrationTestApp
pkill node
- job: Windows
pool:
vmImage: 'windows-2022'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.202'
inputs:
version: 6.0.202
- task: Windows Application Driver@0
inputs:
OperationType: 'Start'
AgentResolution: '4K'
displayName: 'Start WinAppDriver'
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj'
- task: DotNetCoreCLI@2
inputs:
command: 'test'
projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj'
- task: Windows Application Driver@0
inputs:
OperationType: 'Stop'
displayName: 'Stop WinAppDriver'

1
build/CoreLibraries.props

@ -8,6 +8,5 @@
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Markup/Avalonia.Markup/Avalonia.Markup.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.MicroCom/Avalonia.MicroCom.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj" />
</ItemGroup>
</Project>

2
build/SourceGenerators.props

@ -1,7 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ProjectReference
Include="$(MSBuildThisFileDirectory)/../src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj"
Include="$(MSBuildThisFileDirectory)/../src/tools/DevGenerators/DevGenerators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
PrivateAssets="all" />

4
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -49,6 +49,7 @@
AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; };
BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; };
BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; };
ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -101,6 +102,7 @@
AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platformthreading.mm; sourceTree = "<group>"; };
BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = "<group>"; };
BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = "<group>"; };
ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -108,6 +110,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */,
1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */,
1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */,
AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */,
@ -122,6 +125,7 @@
AB661C1C2148230E00291242 /* Frameworks */ = {
isa = PBXGroup;
children = (
ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */,
522D5958258159C1006F7F7A /* Carbon.framework */,
1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */,
1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */,

13
native/Avalonia.Native/src/OSX/AvnView.mm

@ -127,7 +127,11 @@
[self updateRenderTarget];
auto reason = [self inLiveResize] ? ResizeUser : _resizeReason;
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
if(_parent->IsShown())
{
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
}
}
}
@ -439,7 +443,12 @@
if(_parent != nullptr)
{
_lastKeyHandled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key);
auto handled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key);
if (key != LeftCtrl && key != RightCtrl) {
_lastKeyHandled = handled;
} else {
_lastKeyHandled = false;
}
}
}

52
native/Avalonia.Native/src/OSX/SystemDialogs.mm

@ -1,5 +1,6 @@
#include "common.h"
#include "INSWindowHolder.h"
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
class SystemDialogs : public ComSingleObject<IAvnSystemDialogs, &IID_IAvnSystemDialogs>
{
@ -7,6 +8,7 @@ public:
FORWARD_IUNKNOWN()
virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle,
IAvnSystemDialogEvents* events,
bool allowMultiple,
const char* title,
const char* initialDirectory) override
{
@ -14,6 +16,7 @@ public:
{
auto panel = [NSOpenPanel openPanel];
panel.allowsMultipleSelection = allowMultiple;
panel.canChooseDirectories = true;
panel.canCreateDirectories = true;
panel.canChooseFiles = false;
@ -118,7 +121,15 @@ public:
{
auto allowedTypes = [filtersString componentsSeparatedByString:@";"];
panel.allowedFileTypes = allowedTypes;
// Prefer allowedContentTypes if available
if (@available(macOS 11.0, *))
{
panel.allowedContentTypes = ConvertToUTType(allowedTypes);
}
else
{
panel.allowedFileTypes = allowedTypes;
}
}
}
@ -207,7 +218,18 @@ public:
{
auto allowedTypes = [filtersString componentsSeparatedByString:@";"];
panel.allowedFileTypes = allowedTypes;
// Prefer allowedContentTypes if available
if (@available(macOS 11.0, *))
{
panel.allowedContentTypes = ConvertToUTType(allowedTypes);
}
else
{
panel.allowedFileTypes = allowedTypes;
}
panel.allowsOtherFileTypes = false;
panel.extensionHidden = false;
}
}
@ -250,6 +272,32 @@ public:
}
}
}
private:
NSMutableArray* ConvertToUTType(NSArray<NSString*>* allowedTypes)
{
auto originalCount = [allowedTypes count];
auto mapped = [[NSMutableArray alloc] init];
if (@available(macOS 11.0, *))
{
for (int i = 0; i < originalCount; i++)
{
auto utTypeStr = allowedTypes[i];
auto utType = [UTType typeWithIdentifier:utTypeStr];
if (utType == nil)
{
utType = [UTType typeWithMIMEType:utTypeStr];
}
if (utType != nil)
{
[mapped addObject:utType];
}
}
}
return mapped;
}
};

20
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -48,7 +48,6 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl,
[Window setContentMaxSize:lastMaxSize];
[Window setOpaque:false];
[Window setHasShadow:true];
}
HRESULT WindowBaseImpl::ObtainNSViewHandle(void **ret) {
@ -224,7 +223,7 @@ HRESULT WindowBaseImpl::GetFrameSize(AvnSize *ret) {
if (ret == nullptr)
return E_POINTER;
if(Window != nullptr){
if(Window != nullptr && _shown){
auto frame = [Window frame];
ret->Width = frame.size.width;
ret->Height = frame.size.height;
@ -298,14 +297,15 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso
}
@try {
lastSize = NSSize {x, y};
if (!_shown) {
BaseEvents->Resized(AvnSize{x, y}, reason);
}
else if(Window != nullptr) {
[Window setContentSize:lastSize];
[Window invalidateShadow];
if(x != lastSize.width || y != lastSize.height) {
lastSize = NSSize{x, y};
if (!_shown) {
BaseEvents->Resized(AvnSize{x, y}, reason);
} else if (Window != nullptr) {
[Window setContentSize:lastSize];
[Window invalidateShadow];
}
}
}
@finally {

20
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -24,6 +24,8 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase
_lastTitle = @"";
_parent = nullptr;
WindowEvents = events;
[Window setHasShadow:true];
OnInitialiseNSWindow();
}
@ -119,13 +121,16 @@ void WindowImpl::BringToFront()
{
if(Window != nullptr)
{
if(IsDialog())
{
Activate();
}
else
if (![Window isMiniaturized])
{
[Window orderFront:nullptr];
if(IsDialog())
{
Activate();
}
else
{
[Window orderFront:nullptr];
}
}
[Window invalidateShadow];
@ -488,6 +493,8 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) {
}
if (_shown) {
_actualWindowState = _lastWindowState;
switch (state) {
case Maximized:
if (currentState == FullScreen) {
@ -545,7 +552,6 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) {
break;
}
_actualWindowState = _lastWindowState;
WindowEvents->WindowStateChanged(_actualWindowState);
}

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

@ -13,6 +13,7 @@
{
@public IOSurfaceRef surface;
@public AvnPixelSize size;
@public bool hasContent;
@public float scale;
ComPtr<IAvnGlContext> _context;
GLuint _framebuffer, _texture, _renderbuffer;
@ -41,6 +42,7 @@
self->scale = scale;
self->size = size;
self->_context = context;
self->hasContent = false;
return self;
}
@ -92,6 +94,7 @@
_context->MakeCurrent(release.getPPV());
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
glFlush();
self->hasContent = true;
}
-(void) dealloc
@ -170,6 +173,8 @@ static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* ta
@synchronized (lock) {
if(_layer == nil)
return;
if(!surface->hasContent)
return;
[CATransaction begin];
[_layer setContents: nil];
if(surface != nil)
@ -213,6 +218,7 @@ static IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(IOSurfaceRenderTarget* ta
memcpy(pSurface + y*sstride, pFb + y*fstride, wbytes);
}
IOSurfaceUnlock(surf, 0, nil);
surface->hasContent = true;
[self updateLayer];
return S_OK;
}

1
nukebuild/Build.cs

@ -221,7 +221,6 @@ partial class Build : NukeBuild
RunCoreTest("Avalonia.Markup.Xaml.UnitTests");
RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.UnitTests");
RunCoreTest("Avalonia.PlatformSupport.UnitTests");
});
Target RunRenderTests => _ => _

32
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -9,39 +9,37 @@
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
<RuntimeIdentifiers>android-arm64;android-x64</RuntimeIdentifiers>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\AboutAssets.txt" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="..\..\build\Assets\Icon.png">
<Link>Resources\drawable\Icon.png</Link>
</AndroidResource>
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release' and '$(TF_BUILD)' == ''">
<DebugSymbols>True</DebugSymbols>
<PropertyGroup Condition="'$(RunAOTCompilation)'=='' and '$(Configuration)'=='Release' and '$(TF_BUILD)'==''">
<RunAOTCompilation>True</RunAOTCompilation>
<EnableLLVM>True</EnableLLVM>
<AndroidEnableProfiledAot>True</AndroidEnableProfiledAot>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<EmbedAssembliesIntoApk>False</EmbedAssembliesIntoApk>
<RunAOTCompilation>False</RunAOTCompilation>
</PropertyGroup>
<!-- PropertyGroup Condition="'$(RunAOTCompilation)'=='True'">
<EnableLLVM>True</EnableLLVM>
<AndroidAotAdditionalArguments>no-write-symbols,nodebug</AndroidAotAdditionalArguments>
<AndroidAotMode>Hybrid</AndroidAotMode>
<AndroidGenerateJniMarshalMethods>True</AndroidGenerateJniMarshalMethods>
</PropertyGroup -->
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk>
<PropertyGroup Condition="'$(AndroidEnableProfiler)'=='True'">
<IsEmulator Condition="'$(IsEmulator)' == ''">True</IsEmulator>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.ViewModel" Version="2.3.1.3" />
<AndroidEnvironment Condition="'$(IsEmulator)'=='True'" Include="environment.emulator.txt" />
<AndroidEnvironment Condition="'$(IsEmulator)'!='True'" Include="environment.device.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup>
</Project>
</Project>

3
samples/ControlCatalog.Android/Properties/AndroidManifest.xml

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<application android:label="ControlCatalog.Android" android:icon="@drawable/Icon"></application>
<application android:label="ControlCatalog.Android" android:icon="@drawable/Icon"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

1
samples/ControlCatalog.Android/environment.device.txt

@ -0,0 +1 @@
DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend

1
samples/ControlCatalog.Android/environment.emulator.txt

@ -0,0 +1 @@
DOTNET_DiagnosticPorts=10.0.2.2:9001,suspend

3
samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs

@ -22,8 +22,7 @@ public class EmbedSampleGtk : INativeDemoControl
var control = createDefault();
var nodes = Path.GetFullPath(Path.Combine(typeof(EmbedSample).Assembly.GetModules()[0].FullyQualifiedName,
"..",
"nodes.mp4"));
"..", "NativeControls", "Gtk", "nodes.mp4"));
_mplayer = Process.Start(new ProcessStartInfo("mplayer",
$"-vo x11 -zoom -loop 0 -wid {control.Handle.ToInt64()} \"{nodes}\"")
{

9
samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs

@ -2,6 +2,7 @@ using System;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Platform.Interop;
using Avalonia.X11.Interop;
using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.NativeDialogs.Gtk;
using static Avalonia.X11.NativeDialogs.Glib;
@ -10,8 +11,6 @@ namespace ControlCatalog.NetCore;
internal class GtkHelper
{
private static Task<bool> s_gtkTask;
class FileChooser : INativeControlHostDestroyableControlHandle
{
private readonly IntPtr _widget;
@ -38,11 +37,7 @@ internal class GtkHelper
public static INativeControlHostDestroyableControlHandle CreateGtkFileChooser(IntPtr parentXid)
{
if (s_gtkTask == null)
s_gtkTask = StartGtk();
if (!s_gtkTask.Result)
return null;
return RunOnGlibThread(() =>
return GtkInteropHelper.RunOnGlibThread(() =>
{
using (var title = new Utf8Buffer("Embedded"))
{

18
samples/ControlCatalog.NetCore/Program.cs

@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
@ -53,7 +53,11 @@ namespace ControlCatalog.NetCore
else if (args.Contains("--full-headless"))
{
return builder
.UseHeadless(true)
.UseHeadless(new AvaloniaHeadlessPlatformOptions
{
UseHeadlessDrawing = true,
UseCompositor = true
})
.AfterSetup(_ =>
{
DispatcherTimer.RunOnce(async () =>
@ -63,12 +67,11 @@ namespace ControlCatalog.NetCore
var tc = window.GetLogicalDescendants().OfType<TabControl>().First();
foreach (var page in tc.Items.Cast<TabItem>().ToList())
{
// Skip DatePicker because of some layout bug in grid
if (page.Header.ToString() == "DatePicker")
if (page.Header.ToString() == "DatePicker" || page.Header.ToString() == "TreeView")
continue;
Console.WriteLine("Selecting " + page.Header);
tc.SelectedItem = page;
await Task.Delay(500);
await Task.Delay(50);
}
Console.WriteLine("Selecting the first page");
tc.SelectedItem = tc.Items.OfType<object>().First();
@ -77,7 +80,7 @@ namespace ControlCatalog.NetCore
for (var c = 0; c < 3; c++)
{
GC.Collect(2, GCCollectionMode.Forced);
await Task.Delay(500);
await Task.Delay(50);
}
void FormatMem(string metric, long bytes)
@ -87,7 +90,6 @@ namespace ControlCatalog.NetCore
FormatMem("GC allocated bytes", GC.GetTotalMemory(true));
FormatMem("WorkingSet64", Process.GetCurrentProcess().WorkingSet64);
}, TimeSpan.FromSeconds(1));
})
.StartWithClassicDesktopLifetime(args);
@ -111,7 +113,7 @@ namespace ControlCatalog.NetCore
{
EnableMultiTouch = true,
UseDBusMenu = true,
EnableIme = true,
EnableIme = true
})
.With(new Win32PlatformOptions
{

1
samples/ControlCatalog.Web/App.razor.cs

@ -11,6 +11,7 @@ public partial class App
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
})
//.With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering
.SetupWithSingleViewLifetime();
base.OnParametersSet();

1
samples/ControlCatalog/App.xaml

@ -43,6 +43,7 @@
<NativeMenuItemSeparator />
<NativeMenuItem Header="Option 3" ToggleType="CheckBox" IsChecked="True" Command="{Binding ToggleCommand}" />
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Restore Defaults" Command="{Binding ToggleCommand}" />
<NativeMenuItem Header="Disabled option" IsEnabled="False" />
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="Exit" Command="{Binding ExitCommand}" />

2
samples/ControlCatalog/ControlCatalog.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
</PropertyGroup>

10
samples/ControlCatalog/Converter/MathSubtractConverter.cs

@ -6,12 +6,16 @@ namespace ControlCatalog.Converter;
public class MathSubtractConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return (double)value - (double)parameter;
if (value is double dv && parameter is double dp)
{
return dv - dp;
}
return double.NaN;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotSupportedException();
}

10
samples/ControlCatalog/DecoratedWindow.xaml.cs

@ -15,7 +15,7 @@ namespace ControlCatalog
void SetupSide(string name, StandardCursorType cursor, WindowEdge edge)
{
var ctl = this.FindControl<Control>(name);
var ctl = this.Get<Control>(name);
ctl.Cursor = new Cursor(cursor);
ctl.PointerPressed += (i, e) =>
{
@ -26,7 +26,7 @@ namespace ControlCatalog
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
this.FindControl<Control>("TitleBar").PointerPressed += (i, e) =>
this.Get<Control>("TitleBar").PointerPressed += (i, e) =>
{
PlatformImpl?.BeginMoveDrag(e);
};
@ -38,12 +38,12 @@ namespace ControlCatalog
SetupSide("TopRight", StandardCursorType.TopRightCorner, WindowEdge.NorthEast);
SetupSide("BottomLeft", StandardCursorType.BottomLeftCorner, WindowEdge.SouthWest);
SetupSide("BottomRight", StandardCursorType.BottomRightCorner, WindowEdge.SouthEast);
this.FindControl<Button>("MinimizeButton").Click += delegate { this.WindowState = WindowState.Minimized; };
this.FindControl<Button>("MaximizeButton").Click += delegate
this.Get<Button>("MinimizeButton").Click += delegate { this.WindowState = WindowState.Minimized; };
this.Get<Button>("MaximizeButton").Click += delegate
{
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
};
this.FindControl<Button>("CloseButton").Click += delegate
this.Get<Button>("CloseButton").Click += delegate
{
Close();
};

8
samples/ControlCatalog/MainView.xaml

@ -13,6 +13,9 @@
</Style>
</Grid.Styles>
<controls:HamburgerMenu Name="Sidebar">
<TabItem Header="Composition">
<pages:CompositionPage/>
</TabItem>
<TabItem Header="Acrylic">
<pages:AcrylicPage />
</TabItem>
@ -69,6 +72,9 @@
<TabItem Header="CalendarDatePicker">
<pages:CalendarDatePickerPage />
</TabItem>
<TabItem Header="Dialogs">
<pages:DialogsPage />
</TabItem>
<TabItem Header="Drag+Drop">
<pages:DragAndDropPage />
</TabItem>
@ -109,7 +115,7 @@
<TabItem Header="OpenGL">
<pages:OpenGlPage />
</TabItem>
<TabItem Header="Pointers (Touch)">
<TabItem Header="Pointers">
<pages:PointersPage />
</TabItem>
<TabItem Header="ProgressBar">

21
samples/ControlCatalog/MainView.xaml.cs

@ -18,17 +18,12 @@ namespace ControlCatalog
{
AvaloniaXamlLoader.Load(this);
var sideBar = this.FindControl<TabControl>("Sidebar");
var sideBar = this.Get<TabControl>("Sidebar");
if (AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetRuntimeInfo().IsDesktop)
if (AvaloniaLocator.Current?.GetService<IRuntimePlatform>()?.GetRuntimeInfo().IsDesktop == true)
{
IList tabItems = ((IList)sideBar.Items);
tabItems.Add(new TabItem()
{
Header = "Dialogs",
Content = new DialogsPage()
});
tabItems.Add(new TabItem()
{
Header = "Screens",
Content = new ScreenPage()
@ -36,7 +31,7 @@ namespace ControlCatalog
}
var themes = this.Find<ComboBox>("Themes");
var themes = this.Get<ComboBox>("Themes");
themes.SelectionChanged += (sender, e) =>
{
if (themes.SelectedItem is CatalogTheme theme)
@ -80,7 +75,7 @@ namespace ControlCatalog
}
};
var flowDirections = this.Find<ComboBox>("FlowDirection");
var flowDirections = this.Get<ComboBox>("FlowDirection");
flowDirections.SelectionChanged += (sender, e) =>
{
if (flowDirections.SelectedItem is FlowDirection flowDirection)
@ -89,7 +84,7 @@ namespace ControlCatalog
}
};
var decorations = this.Find<ComboBox>("Decorations");
var decorations = this.Get<ComboBox>("Decorations");
decorations.SelectionChanged += (sender, e) =>
{
if (VisualRoot is Window window
@ -99,8 +94,8 @@ namespace ControlCatalog
}
};
var transparencyLevels = this.Find<ComboBox>("TransparencyLevels");
IDisposable backgroundSetter = null, paneBackgroundSetter = null;
var transparencyLevels = this.Get<ComboBox>("TransparencyLevels");
IDisposable? backgroundSetter = null, paneBackgroundSetter = null;
transparencyLevels.SelectionChanged += (sender, e) =>
{
backgroundSetter?.Dispose();
@ -118,7 +113,7 @@ namespace ControlCatalog
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var decorations = this.Find<ComboBox>("Decorations");
var decorations = this.Get<ComboBox>("Decorations");
if (VisualRoot is Window window)
decorations.SelectedIndex = (int)window.SystemDecorations;
}

6
samples/ControlCatalog/MainWindow.xaml.cs

@ -12,7 +12,7 @@ namespace ControlCatalog
public class MainWindow : Window
{
private WindowNotificationManager _notificationArea;
private NativeMenu _recentMenu;
private NativeMenu? _recentMenu;
public MainWindow()
{
@ -28,7 +28,7 @@ namespace ControlCatalog
};
DataContext = new MainWindowViewModel(_notificationArea);
_recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu;
_recentMenu = ((NativeMenu.GetMenu(this)?.Items[0] as NativeMenuItem)?.Menu?.Items[2] as NativeMenuItem)?.Menu;
}
public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit";
@ -39,7 +39,7 @@ namespace ControlCatalog
public void OnOpenClicked(object sender, EventArgs args)
{
_recentMenu.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1)));
_recentMenu?.Items.Insert(0, new NativeMenuItem("Item " + (_recentMenu.Items.Count + 1)));
}
public void OnCloseClicked(object sender, EventArgs args)

4
samples/ControlCatalog/Models/Countries.cs

@ -237,7 +237,7 @@ namespace ControlCatalog.Models
yield return new Country("Zimbabwe", "SUB-SAHARAN AFRICA", 12236805, 390580, 31.3, 0, 0, 67.69, 1900, 90.7, 26.8, 28.01, 21.84);
}
static IReadOnlyList<Country> _all;
static IReadOnlyList<Country>? _all;
public static IReadOnlyList<Country> All
{
@ -253,4 +253,4 @@ namespace ControlCatalog.Models
}
}
}
}

4
samples/ControlCatalog/Models/GDPValueConverter.cs

@ -14,7 +14,7 @@ namespace ControlCatalog.Models
{
public class GDPValueConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is int gdp)
{
@ -29,7 +29,7 @@ namespace ControlCatalog.Models
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}

16
samples/ControlCatalog/Models/Person.cs

@ -13,8 +13,8 @@ namespace ControlCatalog.Models
{
public class Person : INotifyDataErrorInfo, INotifyPropertyChanged
{
string _firstName;
string _lastName;
string _firstName = string.Empty;
string _lastName = string.Empty;
bool _isBanned;
private int _age;
@ -76,7 +76,7 @@ namespace ControlCatalog.Models
Dictionary<string, List<string>> _errorLookup = new Dictionary<string, List<string>>();
void SetError(string propertyName, string error)
void SetError(string propertyName, string? error)
{
if (string.IsNullOrEmpty(error))
{
@ -88,11 +88,11 @@ namespace ControlCatalog.Models
if (_errorLookup.TryGetValue(propertyName, out List<string> errorList))
{
errorList.Clear();
errorList.Add(error);
errorList.Add(error!);
}
else
{
var errors = new List<string> { error };
var errors = new List<string> { error! };
_errorLookup.Add(propertyName, errors);
}
@ -102,8 +102,8 @@ namespace ControlCatalog.Models
public bool HasErrors => _errorLookup.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public event PropertyChangedEventHandler PropertyChanged;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
public event PropertyChangedEventHandler? PropertyChanged;
void OnErrorsChanged(string propertyName)
{
@ -114,7 +114,7 @@ namespace ControlCatalog.Models
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public IEnumerable GetErrors(string propertyName)
public IEnumerable? GetErrors(string propertyName)
{
if (_errorLookup.TryGetValue(propertyName, out List<string> errorList))
return errorList;

33
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs

@ -126,13 +126,13 @@ namespace ControlCatalog.Pages
binding.Bindings.Add(new Binding("Name"));
binding.Bindings.Add(new Binding("Abbreviation"));
var multibindingBox = this.FindControl<AutoCompleteBox>("MultiBindingBox");
var multibindingBox = this.Get<AutoCompleteBox>("MultiBindingBox");
multibindingBox.ValueMemberBinding = binding;
var asyncBox = this.FindControl<AutoCompleteBox>("AsyncBox");
var asyncBox = this.Get<AutoCompleteBox>("AsyncBox");
asyncBox.AsyncPopulator = PopulateAsync;
var customAutocompleteBox = this.FindControl<AutoCompleteBox>("CustomAutocompleteBox");
var customAutocompleteBox = this.Get<AutoCompleteBox>("CustomAutocompleteBox");
customAutocompleteBox.Items = Sentences.SelectMany(x => x);
customAutocompleteBox.TextFilter = LastWordContains;
customAutocompleteBox.TextSelector = AppendWord;
@ -144,11 +144,12 @@ namespace ControlCatalog.Pages
.OfType<AutoCompleteBox>();
}
private bool StringContains(string str, string query)
private bool StringContains(string str, string? query)
{
if (query == null) return false;
return str.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
}
private async Task<IEnumerable<object>> PopulateAsync(string searchText, CancellationToken cancellationToken)
private async Task<IEnumerable<object>> PopulateAsync(string? searchText, CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(1.5), cancellationToken);
@ -157,14 +158,14 @@ namespace ControlCatalog.Pages
.ToList();
}
private bool LastWordContains(string searchText, string item)
private bool LastWordContains(string? searchText, string? item)
{
var words = searchText.Split(' ');
var words = searchText?.Split(' ') ?? Array.Empty<string>();
var options = Sentences.Select(x => x.First).ToArray();
for (var i = 0; i < words.Length; ++i)
{
var word = words[i];
for (var j = 0; j < options.Length; ++j)
for (var j = 0; word is { } && j < options.Length; ++j)
{
var option = options[j];
if (option == null)
@ -183,14 +184,18 @@ namespace ControlCatalog.Pages
return options.Any(x => x != null && x.Value == item);
}
private string AppendWord(string text, string item)
private string AppendWord(string? text, string? item)
{
string[] parts = text.Split(' ');
if (parts.Length == 0)
return item;
if (item is { })
{
string[] parts = text?.Split(' ') ?? Array.Empty<string>();
if (parts.Length == 0)
return item;
parts[parts.Length - 1] = item;
return string.Join(" ", parts);
parts[parts.Length - 1] = item;
return string.Join(" ", parts);
}
return string.Empty;
}
private void InitializeComponent()

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

@ -23,7 +23,7 @@ namespace ControlCatalog.Pages
var spinner = (ButtonSpinner)sender;
var txtBox = (TextBlock)spinner.Content;
int value = Array.IndexOf(_mountains, txtBox.Text);
int value = Array.IndexOf(_mountains, txtBox?.Text);
if (e.Direction == SpinDirection.Increase)
value++;
else

4
samples/ControlCatalog/Pages/ButtonsPage.xaml.cs

@ -11,7 +11,7 @@ namespace ControlCatalog.Pages
{
InitializeComponent();
this.FindControl<RepeatButton>("RepeatButton").Click += OnRepeatButtonClick;
this.Get<RepeatButton>("RepeatButton").Click += OnRepeatButtonClick;
}
private void InitializeComponent()
@ -22,7 +22,7 @@ namespace ControlCatalog.Pages
public void OnRepeatButtonClick(object sender, object args)
{
repeatButtonClickCount++;
var textBlock = this.FindControl<TextBlock>("RepeatButtonTextBlock");
var textBlock = this.Get<TextBlock>("RepeatButtonTextBlock");
textBlock.Text = $"Repeat Button: {repeatButtonClickCount}";
}
}

12
samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml.cs

@ -10,11 +10,11 @@ namespace ControlCatalog.Pages
{
InitializeComponent();
var dp1 = this.FindControl<CalendarDatePicker>("DatePicker1");
var dp2 = this.FindControl<CalendarDatePicker>("DatePicker2");
var dp3 = this.FindControl<CalendarDatePicker>("DatePicker3");
var dp4 = this.FindControl<CalendarDatePicker>("DatePicker4");
var dp5 = this.FindControl<CalendarDatePicker>("DatePicker5");
var dp1 = this.Get<CalendarDatePicker>("DatePicker1");
var dp2 = this.Get<CalendarDatePicker>("DatePicker2");
var dp3 = this.Get<CalendarDatePicker>("DatePicker3");
var dp4 = this.Get<CalendarDatePicker>("DatePicker4");
var dp5 = this.Get<CalendarDatePicker>("DatePicker5");
dp1.SelectedDate = DateTime.Today;
dp2.SelectedDate = DateTime.Today.AddDays(10);
@ -23,7 +23,7 @@ namespace ControlCatalog.Pages
dp4.TemplateApplied += (s, e) =>
{
dp4.BlackoutDates.AddDatesInPast();
dp4.BlackoutDates?.AddDatesInPast();
};
}

4
samples/ControlCatalog/Pages/CalendarPage.xaml.cs

@ -11,11 +11,11 @@ namespace ControlCatalog.Pages
this.InitializeComponent();
var today = DateTime.Today;
var cal1 = this.FindControl<Calendar>("DisplayDatesCalendar");
var cal1 = this.Get<Calendar>("DisplayDatesCalendar");
cal1.DisplayDateStart = today.AddDays(-25);
cal1.DisplayDateEnd = today.AddDays(25);
var cal2 = this.FindControl<Calendar>("BlackoutDatesCalendar");
var cal2 = this.Get<Calendar>("BlackoutDatesCalendar");
cal2.BlackoutDates.AddDatesInPast();
cal2.BlackoutDates.Add(new CalendarDateRange(today.AddDays(6)));
}

11
samples/ControlCatalog/Pages/CarouselPage.xaml.cs

@ -16,6 +16,11 @@ namespace ControlCatalog.Pages
public CarouselPage()
{
this.InitializeComponent();
_carousel = this.Get<Carousel>("carousel");
_left = this.Get<Button>("left");
_right = this.Get<Button>("right");
_transition = this.Get<ComboBox>("transition");
_orientation = this.Get<ComboBox>("orientation");
_left.Click += (s, e) => _carousel.Previous();
_right.Click += (s, e) => _carousel.Next();
_transition.SelectionChanged += TransitionChanged;
@ -25,11 +30,7 @@ namespace ControlCatalog.Pages
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
_carousel = this.FindControl<Carousel>("carousel");
_left = this.FindControl<Button>("left");
_right = this.FindControl<Button>("right");
_transition = this.FindControl<ComboBox>("transition");
_orientation = this.FindControl<ComboBox>("orientation");
}
private void TransitionChanged(object sender, SelectionChangedEventArgs e)

44
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -13,8 +13,14 @@
<pc:ThirdComponentConverter x:Key="ThirdComponent" />
</UserControl.Resources>
<Grid ColumnDefinitions="Auto,10,Auto">
<Grid Grid.Column="0"
<Grid ColumnDefinitions="Auto,10,Auto,10,Auto"
RowDefinitions="Auto,Auto">
<ColorPicker Grid.Column="0"
Grid.Row="1" />
<ColorView Grid.Column="0"
Grid.Row="0"
ColorSpectrumShape="Ring" />
<Grid Grid.Column="2"
Grid.Row="0"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
<ColorSpectrum x:Name="ColorSpectrum1"
@ -41,39 +47,11 @@
ColorModel="Hsva"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
<ColorPreviewer Grid.Row="5"
ShowAccentColors="True"
IsAccentColorsVisible="True"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
</Grid>
<Grid Grid.Column="2"
Grid.Row="0"
ColumnDefinitions="Auto,Auto,Auto"
RowDefinitions="Auto,Auto">
<ColorSlider Grid.Column="0"
Grid.Row="0"
IsAlphaMaxForced="True"
IsSaturationValueMaxForced="False"
ColorComponent="{Binding Components, ElementName=ColorSpectrum2, Converter={StaticResource ThirdComponent}}"
ColorModel="Hsva"
Orientation="Vertical"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
<ColorSpectrum x:Name="ColorSpectrum2"
Grid.Column="1"
Grid.Row="0"
Color="Green"
Shape="Ring"
Height="256"
Width="256" />
<ColorSlider Grid.Column="2"
Grid.Row="0"
ColorComponent="Alpha"
ColorModel="Hsva"
Orientation="Vertical"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
<ColorPreviewer Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="1"
ShowAccentColors="True"
HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
<Grid Grid.Column="4"
Grid.Row="0">
</Grid>
</Grid>
</UserControl>

45
samples/ControlCatalog/Pages/CompositionPage.axaml

@ -0,0 +1,45 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pages="clr-namespace:ControlCatalog.Pages"
x:Class="ControlCatalog.Pages.CompositionPage">
<StackPanel>
<TextBlock Classes="h1">Implicit animations</TextBlock>
<Grid ColumnDefinitions="*,10,40" Margin="0 0 40 0">
<ItemsControl x:Name="Items">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.DataTemplates>
<DataTemplate DataType="pages:CompositionPageColorItem">
<Border
pages:CompositionPage.EnableAnimations="True"
Padding="10" BorderBrush="Gray" BorderThickness="2"
Background="{Binding ColorBrush}" Width="100" Height="100" Margin="10">
<TextBlock Text="{Binding ColorHexValue}"/>
</Border>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
<GridSplitter Margin="2" BorderThickness="1" BorderBrush="Gray"
Background="#e0e0e0" Grid.Column="1"
ResizeDirection="Columns" ResizeBehavior="PreviousAndNext"
/>
<Border Grid.Column="2">
<LayoutTransformControl
HorizontalAlignment="Center"
MinWidth="30">
<LayoutTransformControl.LayoutTransform>
<RotateTransform Angle="90"/>
</LayoutTransformControl.LayoutTransform>
<TextBlock>Resize me</TextBlock>
</LayoutTransformControl>
</Border>
</Grid>
</StackPanel>
</UserControl>

153
samples/ControlCatalog/Pages/CompositionPage.axaml.cs

@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.VisualTree;
namespace ControlCatalog.Pages;
public partial class CompositionPage : UserControl
{
private ImplicitAnimationCollection _implicitAnimations;
public CompositionPage()
{
AvaloniaXamlLoader.Load(this);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
this.FindControl<ItemsControl>("Items").Items = CreateColorItems();
}
private List<CompositionPageColorItem> CreateColorItems()
{
var list = new List<CompositionPageColorItem>();
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 185, 0)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 231, 72, 86)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 120, 215)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 153, 188)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 122, 117, 116)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 118, 118, 118)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 141, 0)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 232, 17, 35)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 99, 177)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 45, 125, 154)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 93, 90, 88)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 76, 74, 72)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 247, 99, 12)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 234, 0, 94)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 142, 140, 216)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 183, 195)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 104, 118, 138)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 105, 121, 126)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 202, 80, 16)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 195, 0, 82)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 107, 105, 214)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 3, 131, 135)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 81, 92, 107)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 74, 84, 89)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 218, 59, 1)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 227, 0, 140)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 135, 100, 184)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 178, 148)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 86, 124, 115)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 100, 124, 100)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 239, 105, 80)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 191, 0, 119)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 116, 77, 169)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 1, 133, 116)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 72, 104, 96)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 82, 94, 84)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 209, 52, 56)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 194, 57, 179)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 177, 70, 194)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 204, 106)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 73, 130, 5)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 132, 117, 69)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 67, 67)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 154, 0, 137)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 136, 23, 152)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 137, 62)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 124, 16)));
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 126, 115, 95)));
return list;
}
private void EnsureImplicitAnimations()
{
if (_implicitAnimations == null)
{
var compositor = ElementComposition.GetElementVisual(this)!.Compositor;
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(400);
var rotationAnimation = compositor.CreateScalarKeyFrameAnimation();
rotationAnimation.Target = "RotationAngle";
rotationAnimation.InsertKeyFrame(.5f, 0.160f);
rotationAnimation.InsertKeyFrame(1f, 0f);
rotationAnimation.Duration = TimeSpan.FromMilliseconds(400);
var animationGroup = compositor.CreateAnimationGroup();
animationGroup.Add(offsetAnimation);
animationGroup.Add(rotationAnimation);
_implicitAnimations = compositor.CreateImplicitAnimationCollection();
_implicitAnimations["Offset"] = animationGroup;
}
}
public static void SetEnableAnimations(Border border, bool value)
{
var page = border.FindAncestorOfType<CompositionPage>();
if (page == null)
{
border.AttachedToVisualTree += delegate { SetEnableAnimations(border, true); };
return;
}
if (ElementComposition.GetElementVisual(page) == null)
return;
page.EnsureImplicitAnimations();
ElementComposition.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations =
page._implicitAnimations;
}
}
public class CompositionPageColorItem
{
public Color Color { get; private set; }
public SolidColorBrush ColorBrush
{
get { return new SolidColorBrush(Color); }
}
public String ColorHexValue
{
get { return Color.ToString().Substring(3).ToUpperInvariant(); }
}
public CompositionPageColorItem(Color color)
{
Color = color;
}
}

22
samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs

@ -18,29 +18,29 @@ namespace ControlCatalog.Pages
DataContext = new ContextPageViewModel();
_textBox = this.FindControl<TextBox>("TextBox");
_textBox = this.Get<TextBox>("TextBox");
var cutButton = this.FindControl<Button>("CutButton");
var cutButton = this.Get<Button>("CutButton");
cutButton.Click += CloseFlyout;
var copyButton = this.FindControl<Button>("CopyButton");
var copyButton = this.Get<Button>("CopyButton");
copyButton.Click += CloseFlyout;
var pasteButton = this.FindControl<Button>("PasteButton");
var pasteButton = this.Get<Button>("PasteButton");
pasteButton.Click += CloseFlyout;
var clearButton = this.FindControl<Button>("ClearButton");
var clearButton = this.Get<Button>("ClearButton");
clearButton.Click += CloseFlyout;
var customContextRequestedBorder = this.FindControl<Border>("CustomContextRequestedBorder");
var customContextRequestedBorder = this.Get<Border>("CustomContextRequestedBorder");
customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel);
var cancellableContextBorder = this.FindControl<Border>("CancellableContextBorder");
var cancellableContextBorder = this.Get<Border>("CancellableContextBorder");
cancellableContextBorder.ContextFlyout!.Closing += ContextFlyoutPage_Closing;
cancellableContextBorder.ContextFlyout!.Opening += ContextFlyoutPage_Opening;
}
private ContextPageViewModel _model;
private ContextPageViewModel? _model;
protected override void OnDataContextChanged(EventArgs e)
{
if (_model != null)
@ -55,7 +55,7 @@ namespace ControlCatalog.Pages
private void ContextFlyoutPage_Closing(object sender, CancelEventArgs e)
{
var cancelCloseCheckBox = this.FindControl<CheckBox>("CancelCloseCheckBox");
e.Cancel = cancelCloseCheckBox.IsChecked ?? false;
e.Cancel = cancelCloseCheckBox?.IsChecked ?? false;
}
private void ContextFlyoutPage_Opening(object sender, EventArgs e)
@ -63,13 +63,13 @@ namespace ControlCatalog.Pages
if (e is CancelEventArgs cancelArgs)
{
var cancelCloseCheckBox = this.FindControl<CheckBox>("CancelOpenCheckBox");
cancelArgs.Cancel = cancelCloseCheckBox.IsChecked ?? false;
cancelArgs.Cancel = cancelCloseCheckBox?.IsChecked ?? false;
}
}
private void CloseFlyout(object sender, RoutedEventArgs e)
{
_textBox.ContextFlyout.Hide();
_textBox.ContextFlyout?.Hide();
}
public void CustomContextRequested(object sender, ContextRequestedEventArgs e)

6
samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs

@ -15,15 +15,15 @@ namespace ControlCatalog.Pages
this.InitializeComponent();
DataContext = new ContextPageViewModel();
var customContextRequestedBorder = this.FindControl<Border>("CustomContextRequestedBorder");
var customContextRequestedBorder = this.Get<Border>("CustomContextRequestedBorder");
customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel);
var cancellableContextBorder = this.FindControl<Border>("CancellableContextBorder");
var cancellableContextBorder = this.Get<Border>("CancellableContextBorder");
cancellableContextBorder.ContextMenu!.ContextMenuClosing += ContextFlyoutPage_Closing;
cancellableContextBorder.ContextMenu!.ContextMenuOpening += ContextFlyoutPage_Opening;
}
private ContextPageViewModel _model;
private ContextPageViewModel? _model;
protected override void OnDataContextChanged(EventArgs e)
{
if (_model != null)

8
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@ -21,7 +21,7 @@ namespace ControlCatalog.Pages
var dataGridSortDescription = DataGridSortDescription.FromPath(nameof(Country.Region), ListSortDirection.Ascending, new ReversedStringComparer());
var collectionView1 = new DataGridCollectionView(Countries.All);
collectionView1.SortDescriptions.Add(dataGridSortDescription);
var dg1 = this.FindControl<DataGrid>("dataGrid1");
var dg1 = this.Get<DataGrid>("dataGrid1");
dg1.IsReadOnly = true;
dg1.LoadingRow += Dg1_LoadingRow;
dg1.Sorting += (s, a) =>
@ -37,7 +37,7 @@ namespace ControlCatalog.Pages
};
dg1.Items = collectionView1;
var dg2 = this.FindControl<DataGrid>("dataGridGrouping");
var dg2 = this.Get<DataGrid>("dataGridGrouping");
dg2.IsReadOnly = true;
var collectionView2 = new DataGridCollectionView(Countries.All);
@ -45,7 +45,7 @@ namespace ControlCatalog.Pages
dg2.Items = collectionView2;
var dg3 = this.FindControl<DataGrid>("dataGridEdit");
var dg3 = this.Get<DataGrid>("dataGridEdit");
dg3.IsReadOnly = false;
var items = new List<Person>
@ -58,7 +58,7 @@ namespace ControlCatalog.Pages
dg3.Items = collectionView3;
var addButton = this.FindControl<Button>("btnAdd");
var addButton = this.Get<Button>("btnAdd");
addButton.Click += (a, b) => collectionView3.AddNew();
}

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

@ -9,12 +9,12 @@ namespace ControlCatalog.Pages
public DateTimePickerPage()
{
this.InitializeComponent();
this.FindControl<TextBlock>("DatePickerDesc").Text = "Use a DatePicker to let users set a date in your app, " +
this.Get<TextBlock>("DatePickerDesc").Text = "Use a DatePicker to let users set a date in your app, " +
"for example to schedule an appointment. The DatePicker displays three controls for month, day, and year. " +
"These controls are easy to use with touch or mouse, and they can be styled and configured in several different ways. " +
"Order of month, day, and year is dynamically set based on user date settings";
this.FindControl<TextBlock>("TimePickerDesc").Text = "Use a TimePicker to let users set a time in your app, for example " +
this.Get<TextBlock>("TimePickerDesc").Text = "Use a TimePicker to let users set a time in your app, for example " +
"to set a reminder. The TimePicker displays three controls for hour, minute, and AM / PM(if necessary).These controls " +
"are easy to use with touch or mouse, and they can be styled and configured in several different ways. " +
"12 - hour or 24 - hour clock and visiblility of AM / PM is dynamically set based on user time settings, or can be overridden.";

76
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -1,29 +1,57 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.DialogsPage">
<StackPanel Orientation="Vertical" Spacing="4" Margin="4">
<CheckBox Name="UseFilters">Use filters</CheckBox>
<Button Name="OpenFile">_Open File</Button>
<Button Name="OpenMultipleFiles">Open _Multiple File</Button>
<Button Name="SaveFile">_Save File</Button>
<Button Name="SelectFolder">Select Fo_lder</Button>
<Button Name="OpenBoth">Select _Both</Button>
<UserControl x:Class="ControlCatalog.Pages.DialogsPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Margin="4"
Orientation="Vertical"
Spacing="4">
<TextBlock x:Name="PickerLastResultsVisible"
Classes="h2"
IsVisible="False"
Text="Last picker results:" />
<ItemsPresenter x:Name="PickerLastResults" />
<TextBlock Text="Windows:" />
<TextBlock Margin="0, 8, 0, 0"
Classes="h1"
Text="Window dialogs" />
<Button Name="DecoratedWindow">Decorated _window</Button>
<Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button>
<Button Name="Dialog" ToolTip.Tip="Shows a dialog">_Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button>
<Button Name="OwnedWindow">Own_ed window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button>
<Expander Header="Window dialogs">
<StackPanel Spacing="4">
<Button Name="DecoratedWindow">Decorated _window</Button>
<Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button>
<Button Name="Dialog" ToolTip.Tip="Shows a dialog">_Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button>
<Button Name="OwnedWindow">Own_ed window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button>
</StackPanel>
</Expander>
<TextBlock Margin="0,20,0,0" Text="Pickers:" />
<CheckBox Name="UseFilters">Use filters</CheckBox>
<Expander Header="FilePicker API">
<StackPanel Spacing="4">
<CheckBox Name="ForceManaged">Force managed dialog</CheckBox>
<CheckBox Name="OpenMultiple">Open multiple</CheckBox>
<Button Name="OpenFolderPicker">Select Fo_lder</Button>
<Button Name="OpenFilePicker">_Open File</Button>
<Button Name="SaveFilePicker">_Save File</Button>
<Button Name="OpenFileFromBookmark">Open File Bookmark</Button>
<Button Name="OpenFolderFromBookmark">Open Folder Bookmark</Button>
</StackPanel>
</Expander>
<Expander Header="Legacy OpenFileDialog">
<StackPanel Spacing="4">
<Button Name="OpenFile">_Open File</Button>
<Button Name="OpenMultipleFiles">Open _Multiple File</Button>
<Button Name="SaveFile">_Save File</Button>
<Button Name="SelectFolder">Select Fo_lder</Button>
<Button Name="OpenBoth">Select _Both</Button>
</StackPanel>
</Expander>
<TextBlock x:Name="PickerLastResultsVisible"
Classes="h2"
IsVisible="False"
Text="Last picker results:" />
<ItemsPresenter x:Name="PickerLastResults" />
<TextBox Name="BookmarkContainer" Watermark="Bookmark" />
<TextBox Name="OpenedFileContent"
MaxLines="10"
Watermark="Picked file content" />
</StackPanel>
</UserControl>

262
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -1,13 +1,21 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Dialogs;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
#pragma warning disable 4014
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
#pragma warning disable CS0618 // Type or member is obsolete
#nullable enable
namespace ControlCatalog.Pages
{
public class DialogsPage : UserControl
@ -16,15 +24,18 @@ namespace ControlCatalog.Pages
{
this.InitializeComponent();
var results = this.FindControl<ItemsPresenter>("PickerLastResults");
var resultsVisible = this.FindControl<TextBlock>("PickerLastResultsVisible");
var results = this.Get<ItemsPresenter>("PickerLastResults");
var resultsVisible = this.Get<TextBlock>("PickerLastResultsVisible");
var bookmarkContainer = this.Get<TextBox>("BookmarkContainer");
var openedFileContent = this.Get<TextBox>("OpenedFileContent");
var openMultiple = this.Get<CheckBox>("OpenMultiple");
string lastSelectedDirectory = null;
IStorageFolder? lastSelectedDirectory = null;
List<FileDialogFilter>? GetFilters()
List<FileDialogFilter> GetFilters()
{
if (this.FindControl<CheckBox>("UseFilters").IsChecked != true)
return null;
if (this.Get<CheckBox>("UseFilters").IsChecked != true)
return new List<FileDialogFilter>();
return new List<FileDialogFilter>
{
new FileDialogFilter
@ -39,12 +50,23 @@ namespace ControlCatalog.Pages
};
}
this.FindControl<Button>("OpenFile").Click += async delegate
List<FilePickerFileType>? GetFileTypes()
{
if (this.Get<CheckBox>("UseFilters").IsChecked != true)
return null;
return new List<FilePickerFileType>
{
FilePickerFileTypes.All,
FilePickerFileTypes.TextPlain
};
}
this.Get<Button>("OpenFile").Click += async delegate
{
// Almost guaranteed to exist
var fullPath = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName;
var initialFileName = fullPath == null ? null : System.IO.Path.GetFileName(fullPath);
var initialDirectory = fullPath == null ? null : System.IO.Path.GetDirectoryName(fullPath);
var uri = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName;
var initialFileName = uri == null ? null : System.IO.Path.GetFileName(uri);
var initialDirectory = uri == null ? null : System.IO.Path.GetDirectoryName(uri);
var result = await new OpenFileDialog()
{
@ -56,52 +78,49 @@ namespace ControlCatalog.Pages
results.Items = result;
resultsVisible.IsVisible = result?.Any() == true;
};
this.FindControl<Button>("OpenMultipleFiles").Click += async delegate
this.Get<Button>("OpenMultipleFiles").Click += async delegate
{
var result = await new OpenFileDialog()
{
Title = "Open multiple files",
Filters = GetFilters(),
Directory = lastSelectedDirectory,
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
AllowMultiple = true
}.ShowAsync(GetWindow());
results.Items = result;
resultsVisible.IsVisible = result?.Any() == true;
};
this.FindControl<Button>("SaveFile").Click += async delegate
this.Get<Button>("SaveFile").Click += async delegate
{
var filters = GetFilters();
var result = await new SaveFileDialog()
{
Title = "Save file",
Filters = GetFilters(),
Directory = lastSelectedDirectory,
Filters = filters,
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
DefaultExtension = filters?.Any() == true ? "txt" : null,
InitialFileName = "test.txt"
}.ShowAsync(GetWindow());
results.Items = new[] { result };
resultsVisible.IsVisible = result != null;
};
this.FindControl<Button>("SelectFolder").Click += async delegate
this.Get<Button>("SelectFolder").Click += async delegate
{
var result = await new OpenFolderDialog()
{
Title = "Select folder",
Directory = lastSelectedDirectory,
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null
}.ShowAsync(GetWindow());
if (!string.IsNullOrEmpty(result))
{
lastSelectedDirectory = result;
}
lastSelectedDirectory = new BclStorageFolder(new System.IO.DirectoryInfo(result));
results.Items = new [] { result };
resultsVisible.IsVisible = result != null;
};
this.FindControl<Button>("OpenBoth").Click += async delegate
this.Get<Button>("OpenBoth").Click += async delegate
{
var result = await new OpenFileDialog()
{
Title = "Select both",
Directory = lastSelectedDirectory,
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
AllowMultiple = true
}.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions
{
@ -110,35 +129,35 @@ namespace ControlCatalog.Pages
results.Items = result;
resultsVisible.IsVisible = result?.Any() == true;
};
this.FindControl<Button>("DecoratedWindow").Click += delegate
this.Get<Button>("DecoratedWindow").Click += delegate
{
new DecoratedWindow().Show();
};
this.FindControl<Button>("DecoratedWindowDialog").Click += delegate
this.Get<Button>("DecoratedWindowDialog").Click += delegate
{
new DecoratedWindow().ShowDialog(GetWindow());
_ = new DecoratedWindow().ShowDialog(GetWindow());
};
this.FindControl<Button>("Dialog").Click += delegate
this.Get<Button>("Dialog").Click += delegate
{
var window = CreateSampleWindow();
window.Height = 200;
window.ShowDialog(GetWindow());
_ = window.ShowDialog(GetWindow());
};
this.FindControl<Button>("DialogNoTaskbar").Click += delegate
this.Get<Button>("DialogNoTaskbar").Click += delegate
{
var window = CreateSampleWindow();
window.Height = 200;
window.ShowInTaskbar = false;
window.ShowDialog(GetWindow());
_ = window.ShowDialog(GetWindow());
};
this.FindControl<Button>("OwnedWindow").Click += delegate
this.Get<Button>("OwnedWindow").Click += delegate
{
var window = CreateSampleWindow();
window.Show(GetWindow());
};
this.FindControl<Button>("OwnedWindowNoTaskbar").Click += delegate
this.Get<Button>("OwnedWindowNoTaskbar").Click += delegate
{
var window = CreateSampleWindow();
@ -146,13 +165,166 @@ namespace ControlCatalog.Pages
window.Show(GetWindow());
};
this.Get<Button>("OpenFilePicker").Click += async delegate
{
var result = await GetStorageProvider().OpenFilePickerAsync(new FilePickerOpenOptions()
{
Title = "Open file",
FileTypeFilter = GetFileTypes(),
SuggestedStartLocation = lastSelectedDirectory,
AllowMultiple = openMultiple.IsChecked == true
});
await SetPickerResult(result);
};
this.Get<Button>("SaveFilePicker").Click += async delegate
{
var fileTypes = GetFileTypes();
var file = await GetStorageProvider().SaveFilePickerAsync(new FilePickerSaveOptions()
{
Title = "Save file",
FileTypeChoices = fileTypes,
SuggestedStartLocation = lastSelectedDirectory,
SuggestedFileName = "FileName",
DefaultExtension = fileTypes?.Any() == true ? "txt" : null,
ShowOverwritePrompt = false
});
if (file is not null && file.CanOpenWrite)
{
// Sync disposal of StreamWriter is not supported on WASM
#if NET6_0_OR_GREATER
await using var stream = await file.OpenWrite();
await using var reader = new System.IO.StreamWriter(stream);
#else
using var stream = await file.OpenWrite();
using var reader = new System.IO.StreamWriter(stream);
#endif
await reader.WriteLineAsync(openedFileContent.Text);
lastSelectedDirectory = await file.GetParentAsync();
}
await SetPickerResult(file is null ? null : new [] {file});
};
this.Get<Button>("OpenFolderPicker").Click += async delegate
{
var folders = await GetStorageProvider().OpenFolderPickerAsync(new FolderPickerOpenOptions()
{
Title = "Folder file",
SuggestedStartLocation = lastSelectedDirectory,
AllowMultiple = openMultiple.IsChecked == true
});
await SetPickerResult(folders);
lastSelectedDirectory = folders.FirstOrDefault();
};
this.Get<Button>("OpenFileFromBookmark").Click += async delegate
{
var file = bookmarkContainer.Text is not null
? await GetStorageProvider().OpenFileBookmarkAsync(bookmarkContainer.Text)
: null;
await SetPickerResult(file is null ? null : new[] { file });
};
this.Get<Button>("OpenFolderFromBookmark").Click += async delegate
{
var folder = bookmarkContainer.Text is not null
? await GetStorageProvider().OpenFolderBookmarkAsync(bookmarkContainer.Text)
: null;
await SetPickerResult(folder is null ? null : new[] { folder });
lastSelectedDirectory = folder;
};
async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items)
{
items ??= Array.Empty<IStorageItem>();
var mappedResults = items.Select(FullPathOrName).ToList();
bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmark() : "Can't bookmark";
if (items.FirstOrDefault() is IStorageItem item)
{
var resultText = item is IStorageFile ? "File:" : "Folder:";
resultText += Environment.NewLine;
var props = await item.GetBasicPropertiesAsync();
resultText += @$"Size: {props.Size}
DateCreated: {props.DateCreated}
DateModified: {props.DateModified}
CanBookmark: {item.CanBookmark}
";
if (item is IStorageFile file)
{
resultText += @$"
CanOpenRead: {file.CanOpenRead}
CanOpenWrite: {file.CanOpenWrite}
Content:
";
if (file.CanOpenRead)
{
#if NET6_0_OR_GREATER
await using var stream = await file.OpenRead();
#else
using var stream = await file.OpenRead();
#endif
using var reader = new System.IO.StreamReader(stream);
// 4GB file test, shouldn't load more than 10000 chars into a memory.
const int length = 10000;
var buffer = ArrayPool<char>.Shared.Rent(length);
try
{
var charsRead = await reader.ReadAsync(buffer, 0, length);
resultText += new string(buffer, 0, charsRead);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
}
openedFileContent.Text = resultText;
lastSelectedDirectory = await item.GetParentAsync();
if (lastSelectedDirectory is not null)
{
mappedResults.Insert(0, "Parent: " + FullPathOrName(lastSelectedDirectory));
}
}
results.Items = mappedResults;
resultsVisible.IsVisible = mappedResults.Any();
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var openedFileContent = this.Get<TextBox>("OpenedFileContent");
try
{
var storageProvider = GetStorageProvider();
openedFileContent.Text = $@"CanOpen: {storageProvider.CanOpen}
CanSave: {storageProvider.CanSave}
CanPickFolder: {storageProvider.CanPickFolder}";
}
catch (Exception ex)
{
openedFileContent.Text = "Storage provider is not available: " + ex.Message;
}
}
private Window CreateSampleWindow()
{
Button button;
Button dialogButton;
var window = new Window
{
Height = 200,
@ -191,7 +363,22 @@ namespace ControlCatalog.Pages
return window;
}
Window GetWindow() => (Window)this.VisualRoot;
private IStorageProvider GetStorageProvider()
{
var forceManaged = this.Get<CheckBox>("ForceManaged").IsChecked ?? false;
return forceManaged
? new ManagedStorageProvider<Window>(GetWindow(), null)
: GetTopLevel().StorageProvider;
}
private static string FullPathOrName(IStorageItem? item)
{
if (item is null) return "(null)";
return item.TryGetUri(out var uri) ? uri.ToString() : item.Name;
}
Window GetWindow() => this.VisualRoot as Window ?? throw new NullReferenceException("Invalid Owner");
TopLevel GetTopLevel() => this.VisualRoot as TopLevel ?? throw new NullReferenceException("Invalid Owner");
private void InitializeComponent()
{
@ -199,3 +386,4 @@ namespace ControlCatalog.Pages
}
}
}
#pragma warning restore CS0618 // Type or member is obsolete

6
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@ -14,7 +14,7 @@ namespace ControlCatalog.Pages
public DragAndDropPage()
{
this.InitializeComponent();
_DropState = this.Find<TextBlock>("DropState");
_DropState = this.Get<TextBlock>("DropState");
int textCount = 0;
SetupDnd("Text", d => d.Set(DataFormats.Text,
@ -26,8 +26,8 @@ namespace ControlCatalog.Pages
void SetupDnd(string suffix, Action<DataObject> factory, DragDropEffects effects)
{
var dragMe = this.Find<Border>("DragMe" + suffix);
var dragState = this.Find<TextBlock>("DragState"+suffix);
var dragMe = this.Get<Border>("DragMe" + suffix);
var dragState = this.Get<TextBlock>("DragState"+suffix);
async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e)
{

8
samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs

@ -35,7 +35,7 @@ namespace ControlCatalog.Pages
private void SetXamlTexts()
{
var bfxt = this.FindControl<TextBlock>("ButtonFlyoutXamlText");
var bfxt = this.Get<TextBlock>("ButtonFlyoutXamlText");
bfxt.Text = "<Button Content=\"Click me!\">\n" +
" <Button.Flyout>\n" +
" <Flyout>\n" +
@ -45,7 +45,7 @@ namespace ControlCatalog.Pages
" </Flyout>\n" +
" </Button.Flyout>\n</Button>";
var mfxt = this.FindControl<TextBlock>("MenuFlyoutXamlText");
var mfxt = this.Get<TextBlock>("MenuFlyoutXamlText");
mfxt.Text = "<Button Content=\"Click me!\">\n" +
" <Button.Flyout>\n" +
" <MenuFlyout>\n" +
@ -54,7 +54,7 @@ namespace ControlCatalog.Pages
" </MenuFlyout>\n" +
" </Button.Flyout>\n</Button>";
var afxt = this.FindControl<TextBlock>("AttachedFlyoutXamlText");
var afxt = this.Get<TextBlock>("AttachedFlyoutXamlText");
afxt.Text = "<Panel Name=\"AttachedFlyoutPanel\">\n" +
" <FlyoutBase.AttachedFlyout>\n" +
" <Flyout>\n" +
@ -66,7 +66,7 @@ namespace ControlCatalog.Pages
"\n\n In DoubleTapped handler:\n" +
"FlyoutBase.ShowAttachedFlyout(AttachedFlyoutPanel);";
var sfxt = this.FindControl<TextBlock>("SharedFlyoutXamlText");
var sfxt = this.Get<TextBlock>("SharedFlyoutXamlText");
sfxt.Text = "Declare a flyout in Resources:\n" +
"<Window.Resources>\n" +
" <Flyout x:Key=\"SharedFlyout\">\n" +

13
samples/ControlCatalog/Pages/ImagePage.xaml.cs

@ -17,9 +17,9 @@ namespace ControlCatalog.Pages
public ImagePage()
{
InitializeComponent();
_bitmapImage = this.FindControl<Image>("bitmapImage");
_drawingImage = this.FindControl<Image>("drawingImage");
_croppedImage = this.FindControl<Image>("croppedImage");
_bitmapImage = this.Get<Image>("bitmapImage");
_drawingImage = this.Get<Image>("drawingImage");
_croppedImage = this.Get<Image>("croppedImage");
}
private void InitializeComponent()
@ -50,8 +50,11 @@ namespace ControlCatalog.Pages
if (_croppedImage != null)
{
var comboxBox = (ComboBox)sender;
var croppedBitmap = _croppedImage.Source as CroppedBitmap;
croppedBitmap.SourceRect = GetCropRect(comboxBox.SelectedIndex);
if (_croppedImage.Source is CroppedBitmap croppedBitmap)
{
croppedBitmap.SourceRect = GetCropRect(comboxBox.SelectedIndex);
}
}
}

26
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@ -24,11 +24,11 @@ namespace ControlCatalog.Pages
public ItemsRepeaterPage()
{
this.InitializeComponent();
_repeater = this.FindControl<ItemsRepeater>("repeater");
_scroller = this.FindControl<ScrollViewer>("scroller");
_scrollToLast = this.FindControl<Button>("scrollToLast");
_scrollToRandom = this.FindControl<Button>("scrollToRandom");
_scrollToSelected = this.FindControl<Button>("scrollToSelected");
_repeater = this.Get<ItemsRepeater>("repeater");
_scroller = this.Get<ScrollViewer>("scroller");
_scrollToLast = this.Get<Button>("scrollToLast");
_scrollToRandom = this.Get<Button>("scrollToRandom");
_scrollToSelected = this.Get<Button>("scrollToSelected");
_repeater.PointerPressed += RepeaterClick;
_repeater.KeyDown += RepeaterOnKeyDown;
_scrollToLast.Click += scrollToLast_Click;
@ -44,8 +44,10 @@ namespace ControlCatalog.Pages
public void OnSelectTemplateKey(object sender, SelectTemplateEventArgs e)
{
var item = (ItemsRepeaterPageViewModel.Item)e.DataContext;
e.TemplateKey = (item.Index % 2 == 0) ? "even" : "odd";
if (e.DataContext is ItemsRepeaterPageViewModel.Item item)
{
e.TemplateKey = (item.Index % 2 == 0) ? "even" : "odd";
}
}
private void LayoutChanged(object sender, SelectionChangedEventArgs e)
@ -115,7 +117,7 @@ namespace ControlCatalog.Pages
private void ScrollTo(int index)
{
System.Diagnostics.Debug.WriteLine("Scroll to " + index);
var layoutManager = ((TopLevel)VisualRoot).LayoutManager;
var layoutManager = ((TopLevel)VisualRoot!).LayoutManager;
var element = _repeater.GetOrCreateElement(index);
layoutManager.ExecuteLayoutPass();
element.BringIntoView();
@ -123,9 +125,11 @@ namespace ControlCatalog.Pages
private void RepeaterClick(object sender, PointerPressedEventArgs e)
{
var item = (e.Source as TextBlock)?.DataContext as ItemsRepeaterPageViewModel.Item;
_viewModel.SelectedItem = item;
_selectedIndex = _viewModel.Items.IndexOf(item);
if ((e.Source as TextBlock)?.DataContext is ItemsRepeaterPageViewModel.Item item)
{
_viewModel.SelectedItem = item;
_selectedIndex = _viewModel.Items.IndexOf(item);
}
}
private void RepeaterOnKeyDown(object sender, KeyEventArgs e)

2
samples/ControlCatalog/Pages/LabelsPage.axaml.cs

@ -7,7 +7,7 @@ namespace ControlCatalog.Pages
{
public class LabelsPage : UserControl
{
private Person _person;
private Person? _person;
public LabelsPage()
{

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

@ -22,7 +22,7 @@ namespace ControlCatalog.Pages
AvaloniaXamlLoader.Load(this);
}
private MenuPageViewModel _model;
private MenuPageViewModel? _model;
protected override void OnDataContextChanged(EventArgs e)
{
if (_model != null)

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

@ -79,6 +79,8 @@ namespace ControlCatalog.Pages
public interface INativeDemoControl
{
/// <param name="isSecond">Used to specify which control should be displayed as a demo</param>
/// <param name="parent"></param>
/// <param name="createDefault"></param>
IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault);
}
}

8
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -65,7 +65,7 @@
Margin="2" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock>
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #upDown.Value}" VerticalAlignment="Center"
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding DecimalValue}" VerticalAlignment="Center"
Margin="2" HorizontalAlignment="Center"/>
@ -76,21 +76,21 @@
<StackPanel Orientation="Vertical" Margin="10">
<Label Target="upDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of decimal NumericUpDown:</Label>
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center" Value="{Binding DecimalValue}"
VerticalAlignment="Center" Value="{Binding DecimalValue}"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="10">
<Label Target="DoubleUpDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of double NumericUpDown:</Label>
<NumericUpDown Name="DoubleUpDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center" Value="{Binding DoubleValue}"
VerticalAlignment="Center" Value="{Binding DoubleValue}"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="10">
<Label Target="ValidationUpDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">NumericUpDown with Validation Errors:</Label>
<NumericUpDown x:Name="ValidationUpDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center"
VerticalAlignment="Center"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}">
<DataValidationErrors.Error>
<sys:Exception />

23
samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs

@ -28,16 +28,16 @@ namespace ControlCatalog.Pages
public class NumbersPageViewModel : ViewModelBase
{
private IList<FormatObject> _formats;
private IList<FormatObject>? _formats;
private FormatObject _selectedFormat;
private IList<Location> _spinnerLocations;
private IList<Location>? _spinnerLocations;
private double _doubleValue;
private decimal _decimalValue;
public NumbersPageViewModel()
{
SelectedFormat = Formats.FirstOrDefault();
_selectedFormat = Formats.FirstOrDefault();
}
public double DoubleValue
@ -84,15 +84,10 @@ namespace ControlCatalog.Pages
}
}
public IList<CultureInfo> Cultures { get; } = new List<CultureInfo>()
{
new CultureInfo("en-US"),
new CultureInfo("en-GB"),
new CultureInfo("fr-FR"),
new CultureInfo("ar-DZ"),
new CultureInfo("zh-CN"),
new CultureInfo("cs-CZ")
};
// Trimmed-mode friendly where we might not have cultures
public IList<CultureInfo?> Cultures { get; } = CultureInfo.GetCultures(CultureTypes.SpecificCultures)
.Where(c => new[] { "en-US", "en-GB", "fr-FR", "ar-DZ", "zh-CH", "cs-CZ" }.Contains(c.Name))
.ToArray();
public FormatObject SelectedFormat
{
@ -103,7 +98,7 @@ namespace ControlCatalog.Pages
public class FormatObject
{
public string Value { get; set; }
public string Name { get; set; }
public string? Value { get; set; }
public string? Name { get; set; }
}
}

235
samples/ControlCatalog/Pages/PointerCanvas.cs

@ -0,0 +1,235 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Threading;
namespace ControlCatalog.Pages;
public class PointerCanvas : Control
{
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _events;
private IDisposable? _statusUpdated;
private Dictionary<int, PointerPoints> _pointers = new();
private PointerPointProperties? _lastProperties;
private PointerUpdateKind? _lastNonOtherUpdateKind;
class PointerPoints
{
struct CanvasPoint
{
public IBrush Brush;
public Point Point;
public double Radius;
public double? Pressure;
}
readonly CanvasPoint[] _points = new CanvasPoint[1000];
int _index;
public void Render(DrawingContext context, bool drawPoints)
{
CanvasPoint? prev = null;
for (var c = 0; c < _points.Length; c++)
{
var i = (c + _index) % _points.Length;
var pt = _points[i];
var pressure = (pt.Pressure ?? prev?.Pressure ?? 0.5);
var thickness = pressure * 10;
var radius = pressure * pt.Radius;
if (drawPoints)
{
if (pt.Brush != null)
{
context.DrawEllipse(pt.Brush, null, pt.Point, radius, radius);
}
}
else
{
if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null
&& prev.Value.Pressure != null && pt.Pressure != null)
{
var linePen = new Pen(Brushes.Black, thickness, null, PenLineCap.Round, PenLineJoin.Round);
context.DrawLine(linePen, prev.Value.Point, pt.Point);
}
}
prev = pt;
}
}
void AddPoint(Point pt, IBrush brush, double radius, float? pressure = null)
{
_points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius, Pressure = pressure };
_index = (_index + 1) % _points.Length;
}
public void HandleEvent(PointerEventArgs e, Visual v)
{
e.Handled = true;
var currentPoint = e.GetCurrentPoint(v);
if (e.RoutedEvent == PointerPressedEvent)
AddPoint(currentPoint.Position, Brushes.Green, 10);
else if (e.RoutedEvent == PointerReleasedEvent)
AddPoint(currentPoint.Position, Brushes.Red, 10);
else
{
var pts = e.GetIntermediatePoints(v);
for (var c = 0; c < pts.Count; c++)
{
var pt = pts[c];
AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black,
c == pts.Count - 1 ? 5 : 2, pt.Properties.Pressure);
}
}
}
}
private int _threadSleep;
public static DirectProperty<PointerCanvas, int> ThreadSleepProperty =
AvaloniaProperty.RegisterDirect<PointerCanvas, int>(nameof(ThreadSleep), c => c.ThreadSleep, (c, v) => c.ThreadSleep = v);
public int ThreadSleep
{
get => _threadSleep;
set => SetAndRaise(ThreadSleepProperty, ref _threadSleep, value);
}
private bool _drawOnlyPoints;
public static DirectProperty<PointerCanvas, bool> DrawOnlyPointsProperty =
AvaloniaProperty.RegisterDirect<PointerCanvas, bool>(nameof(DrawOnlyPoints), c => c.DrawOnlyPoints, (c, v) => c.DrawOnlyPoints = v);
public bool DrawOnlyPoints
{
get => _drawOnlyPoints;
set => SetAndRaise(DrawOnlyPointsProperty, ref _drawOnlyPoints, value);
}
private string? _status;
public static DirectProperty<PointerCanvas, string?> StatusProperty =
AvaloniaProperty.RegisterDirect<PointerCanvas, string?>(nameof(DrawOnlyPoints), c => c.Status, (c, v) => c.Status = v,
defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
public string? Status
{
get => _status;
set => SetAndRaise(StatusProperty, ref _status, value);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_statusUpdated = DispatcherTimer.Run(() =>
{
if (_stopwatch.Elapsed.TotalMilliseconds > 250)
{
Status = $@"Events per second: {(_events / _stopwatch.Elapsed.TotalSeconds)}
PointerUpdateKind: {_lastProperties?.PointerUpdateKind}
Last PointerUpdateKind != Other: {_lastNonOtherUpdateKind}
IsLeftButtonPressed: {_lastProperties?.IsLeftButtonPressed}
IsRightButtonPressed: {_lastProperties?.IsRightButtonPressed}
IsMiddleButtonPressed: {_lastProperties?.IsMiddleButtonPressed}
IsXButton1Pressed: {_lastProperties?.IsXButton1Pressed}
IsXButton2Pressed: {_lastProperties?.IsXButton2Pressed}
IsBarrelButtonPressed: {_lastProperties?.IsBarrelButtonPressed}
IsEraser: {_lastProperties?.IsEraser}
IsInverted: {_lastProperties?.IsInverted}
Pressure: {_lastProperties?.Pressure}
XTilt: {_lastProperties?.XTilt}
YTilt: {_lastProperties?.YTilt}
Twist: {_lastProperties?.Twist}";
_stopwatch.Restart();
_events = 0;
}
return true;
}, TimeSpan.FromMilliseconds(10));
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_statusUpdated?.Dispose();
}
void HandleEvent(PointerEventArgs e)
{
_events++;
if (_threadSleep != 0)
{
Thread.Sleep(_threadSleep);
}
InvalidateVisual();
var lastPointer = e.GetCurrentPoint(this);
_lastProperties = lastPointer.Properties;
if (_lastProperties.PointerUpdateKind != PointerUpdateKind.Other)
{
_lastNonOtherUpdateKind = _lastProperties.PointerUpdateKind;
}
if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch)
{
_pointers.Remove(e.Pointer.Id);
return;
}
if (e.Pointer.Type != PointerType.Pen
|| lastPointer.Properties.Pressure > 0)
{
if (!_pointers.TryGetValue(e.Pointer.Id, out var pt))
_pointers[e.Pointer.Id] = pt = new PointerPoints();
pt.HandleEvent(e, this);
}
}
public override void Render(DrawingContext context)
{
context.FillRectangle(Brushes.White, Bounds);
foreach (var pt in _pointers.Values)
pt.Render(context, _drawOnlyPoints);
base.Render(context);
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (e.ClickCount == 2)
{
_pointers.Clear();
InvalidateVisual();
return;
}
HandleEvent(e);
base.OnPointerPressed(e);
}
protected override void OnPointerMoved(PointerEventArgs e)
{
HandleEvent(e);
base.OnPointerMoved(e);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
HandleEvent(e);
base.OnPointerReleased(e);
}
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
_lastProperties = null;
base.OnPointerCaptureLost(e);
}
}

109
samples/ControlCatalog/Pages/PointerContactsTab.cs

@ -0,0 +1,109 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Immutable;
namespace ControlCatalog.Pages;
public class PointerContactsTab : Control
{
class PointerInfo
{
public Point Point { get; set; }
public Color Color { get; set; }
}
private static Color[] AllColors = new[]
{
Colors.Aqua,
Colors.Beige,
Colors.Chartreuse,
Colors.Coral,
Colors.Fuchsia,
Colors.Crimson,
Colors.Lavender,
Colors.Orange,
Colors.Orchid,
Colors.ForestGreen,
Colors.SteelBlue,
Colors.PapayaWhip,
Colors.PaleVioletRed,
Colors.Goldenrod,
Colors.Maroon,
Colors.Moccasin,
Colors.Navy,
Colors.Wheat,
Colors.Violet,
Colors.Sienna,
Colors.Indigo,
Colors.Honeydew
};
private Dictionary<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>();
public PointerContactsTab()
{
ClipToBounds = true;
}
void UpdatePointer(PointerEventArgs e)
{
if (!_pointers.TryGetValue(e.Pointer, out var info))
{
if (e.RoutedEvent == PointerMovedEvent)
return;
var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray();
var color = colors[new Random().Next(0, colors.Length - 1)];
_pointers[e.Pointer] = info = new PointerInfo { Color = color };
}
info.Point = e.GetPosition(this);
InvalidateVisual();
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
UpdatePointer(e);
e.Pointer.Capture(this);
e.Handled = true;
base.OnPointerPressed(e);
}
protected override void OnPointerMoved(PointerEventArgs e)
{
UpdatePointer(e);
e.Handled = true;
base.OnPointerMoved(e);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
_pointers.Remove(e.Pointer);
e.Handled = true;
InvalidateVisual();
}
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
_pointers.Remove(e.Pointer);
InvalidateVisual();
}
public override void Render(DrawingContext context)
{
context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size));
foreach (var pt in _pointers.Values)
{
var brush = new ImmutableSolidColorBrush(pt.Color);
context.DrawEllipse(brush, null, pt.Point, 75, 75);
}
}
}

322
samples/ControlCatalog/Pages/PointersPage.cs

@ -1,322 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace ControlCatalog.Pages;
public class PointersPage : Decorator
{
public PointersPage()
{
Child = new TabControl
{
Items = new[]
{
new TabItem() { Header = "Contacts", Content = new PointerContactsTab() },
new TabItem() { Header = "IntermediatePoints", Content = new PointerIntermediatePointsTab() }
}
};
}
class PointerContactsTab : Control
{
class PointerInfo
{
public Point Point { get; set; }
public Color Color { get; set; }
}
private static Color[] AllColors = new[]
{
Colors.Aqua,
Colors.Beige,
Colors.Chartreuse,
Colors.Coral,
Colors.Fuchsia,
Colors.Crimson,
Colors.Lavender,
Colors.Orange,
Colors.Orchid,
Colors.ForestGreen,
Colors.SteelBlue,
Colors.PapayaWhip,
Colors.PaleVioletRed,
Colors.Goldenrod,
Colors.Maroon,
Colors.Moccasin,
Colors.Navy,
Colors.Wheat,
Colors.Violet,
Colors.Sienna,
Colors.Indigo,
Colors.Honeydew
};
private Dictionary<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>();
public PointerContactsTab()
{
ClipToBounds = true;
}
void UpdatePointer(PointerEventArgs e)
{
if (!_pointers.TryGetValue(e.Pointer, out var info))
{
if (e.RoutedEvent == PointerMovedEvent)
return;
var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray();
var color = colors[new Random().Next(0, colors.Length - 1)];
_pointers[e.Pointer] = info = new PointerInfo {Color = color};
}
info.Point = e.GetPosition(this);
InvalidateVisual();
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
UpdatePointer(e);
e.Pointer.Capture(this);
e.Handled = true;
base.OnPointerPressed(e);
}
protected override void OnPointerMoved(PointerEventArgs e)
{
UpdatePointer(e);
e.Handled = true;
base.OnPointerMoved(e);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
_pointers.Remove(e.Pointer);
e.Handled = true;
InvalidateVisual();
}
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
_pointers.Remove(e.Pointer);
InvalidateVisual();
}
public override void Render(DrawingContext context)
{
context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size));
foreach (var pt in _pointers.Values)
{
var brush = new ImmutableSolidColorBrush(pt.Color);
context.DrawEllipse(brush, null, pt.Point, 75, 75);
}
}
}
public class PointerIntermediatePointsTab : Decorator
{
public PointerIntermediatePointsTab()
{
this[TextElement.ForegroundProperty] = Brushes.Black;
var slider = new Slider
{
Margin = new Thickness(5),
Minimum = 0,
Maximum = 500
};
var status = new TextBlock()
{
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
};
Child = new Grid
{
Children =
{
new PointerCanvas(slider, status),
new Border
{
Background = Brushes.LightYellow,
Child = new StackPanel
{
Children =
{
new StackPanel
{
Orientation = Orientation.Horizontal,
Children =
{
new TextBlock { Text = "Thread sleep:" },
new TextBlock()
{
[!TextBlock.TextProperty] =slider.GetObservable(Slider.ValueProperty)
.Select(x=>x.ToString()).ToBinding()
}
}
},
slider
}
},
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Width = 300,
Height = 60
},
status
}
};
}
class PointerCanvas : Control
{
private readonly Slider _slider;
private readonly TextBlock _status;
private int _events;
private Stopwatch _stopwatch = Stopwatch.StartNew();
private Dictionary<int, PointerPoints> _pointers = new();
class PointerPoints
{
struct CanvasPoint
{
public IBrush Brush;
public Point Point;
public double Radius;
}
readonly CanvasPoint[] _points = new CanvasPoint[1000];
int _index;
public void Render(DrawingContext context)
{
CanvasPoint? prev = null;
for (var c = 0; c < _points.Length; c++)
{
var i = (c + _index) % _points.Length;
var pt = _points[i];
if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null)
context.DrawLine(new Pen(Brushes.Black), prev.Value.Point, pt.Point);
prev = pt;
if (pt.Brush != null)
context.DrawEllipse(pt.Brush, null, pt.Point, pt.Radius, pt.Radius);
}
}
void AddPoint(Point pt, IBrush brush, double radius)
{
_points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius };
_index = (_index + 1) % _points.Length;
}
public void HandleEvent(PointerEventArgs e, Visual v)
{
e.Handled = true;
if (e.RoutedEvent == PointerPressedEvent)
AddPoint(e.GetPosition(v), Brushes.Green, 10);
else if (e.RoutedEvent == PointerReleasedEvent)
AddPoint(e.GetPosition(v), Brushes.Red, 10);
else
{
var pts = e.GetIntermediatePoints(v);
for (var c = 0; c < pts.Count; c++)
{
var pt = pts[c];
AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black,
c == pts.Count - 1 ? 5 : 2);
}
}
}
}
public PointerCanvas(Slider slider, TextBlock status)
{
_slider = slider;
_status = status;
DispatcherTimer.Run(() =>
{
if (_stopwatch.Elapsed.TotalSeconds > 1)
{
_status.Text = "Events per second: " + (_events / _stopwatch.Elapsed.TotalSeconds);
_stopwatch.Restart();
_events = 0;
}
return this.GetVisualRoot() != null;
}, TimeSpan.FromMilliseconds(10));
}
void HandleEvent(PointerEventArgs e)
{
_events++;
Thread.Sleep((int)_slider.Value);
InvalidateVisual();
if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch)
{
_pointers.Remove(e.Pointer.Id);
return;
}
if (!_pointers.TryGetValue(e.Pointer.Id, out var pt))
_pointers[e.Pointer.Id] = pt = new PointerPoints();
pt.HandleEvent(e, this);
}
public override void Render(DrawingContext context)
{
context.FillRectangle(Brushes.White, Bounds);
foreach(var pt in _pointers.Values)
pt.Render(context);
base.Render(context);
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (e.ClickCount == 2)
{
_pointers.Clear();
InvalidateVisual();
return;
}
HandleEvent(e);
base.OnPointerPressed(e);
}
protected override void OnPointerMoved(PointerEventArgs e)
{
HandleEvent(e);
base.OnPointerMoved(e);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
HandleEvent(e);
base.OnPointerReleased(e);
}
}
}
}

66
samples/ControlCatalog/Pages/PointersPage.xaml

@ -0,0 +1,66 @@
<UserControl x:Class="ControlCatalog.Pages.PointersPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ControlCatalog.Pages">
<TabControl>
<TabItem Header="Contacts">
<local:PointerContactsTab />
</TabItem>
<TabItem Header="IntermediatePoints">
<Panel TextElement.Foreground="Black">
<local:PointerCanvas x:Name="IntermediatePointsCanvas"
DrawOnlyPoints="True"
Status="{Binding #Status1TextBlock.Text, Mode=OneWayToSource}"
ThreadSleep="{Binding #ThreadSleepSlider.Value}" />
<Border Width="300"
Height="60"
HorizontalAlignment="Right"
VerticalAlignment="Top">
<StackPanel Background="LightYellow">
<TextBlock Text="{Binding #ThreadSleepSlider.Value, StringFormat='Thread sleep: {0} / 500'}" />
<Slider x:Name="ThreadSleepSlider"
Value="50"
Maximum="500"
Minimum="0" />
</StackPanel>
</Border>
<TextBlock x:Name="Status1TextBlock"
HorizontalAlignment="Left"
VerticalAlignment="Top" />
</Panel>
</TabItem>
<TabItem Header="Pressure">
<Panel TextElement.Foreground="Black">
<local:PointerCanvas x:Name="PressureCanvas"
DrawOnlyPoints="False"
Status="{Binding #Status2TextBlock.Text, Mode=OneWayToSource}"
ThreadSleep="0" />
<TextBlock x:Name="Status2TextBlock"
HorizontalAlignment="Left"
VerticalAlignment="Top" />
</Panel>
</TabItem>
<TabItem Header="Capture">
<WrapPanel>
<Border Name="BorderCapture1"
MinWidth="250"
MinHeight="170"
Margin="5"
Padding="50"
Background="{DynamicResource SystemAccentColor}"
ToolTip.Placement="Bottom">
<TextBlock>Capture 1</TextBlock>
</Border>
<Border Name="BorderCapture2"
MinWidth="250"
MinHeight="170"
Margin="5"
Padding="50"
Background="{DynamicResource SystemAccentColor}"
ToolTip.Placement="Bottom">
<TextBlock>Capture 2</TextBlock>
</Border>
</WrapPanel>
</TabItem>
</TabControl>
</UserControl>

78
samples/ControlCatalog/Pages/PointersPage.xaml.cs

@ -0,0 +1,78 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages;
public class PointersPage : UserControl
{
public PointersPage()
{
this.InitializeComponent();
var border1 = this.Get<Border>("BorderCapture1");
var border2 = this.Get<Border>("BorderCapture2");
border1.PointerPressed += Border_PointerPressed;
border1.PointerReleased += Border_PointerReleased;
border1.PointerCaptureLost += Border_PointerCaptureLost;
border1.PointerMoved += Border_PointerUpdated;
border1.PointerEntered += Border_PointerUpdated;
border1.PointerExited += Border_PointerUpdated;
border2.PointerPressed += Border_PointerPressed;
border2.PointerReleased += Border_PointerReleased;
border2.PointerCaptureLost += Border_PointerCaptureLost;
border2.PointerMoved += Border_PointerUpdated;
border2.PointerEntered += Border_PointerUpdated;
border2.PointerExited += Border_PointerUpdated;
}
private void Border_PointerUpdated(object sender, PointerEventArgs e)
{
var textBlock = (TextBlock)((Border)sender).Child;
var position = e.GetPosition((Border)sender);
textBlock.Text = @$"Type: {e.Pointer.Type}
Captured: {e.Pointer.Captured == sender}
PointerId: {e.Pointer.Id}
Position: {(int)position.X} {(int)position.Y}";
e.Handled = true;
}
private void Border_PointerCaptureLost(object sender, PointerCaptureLostEventArgs e)
{
var textBlock = (TextBlock)((Border)sender).Child;
textBlock.Text = @$"Type: {e.Pointer.Type}
Captured: {e.Pointer.Captured == sender}
PointerId: {e.Pointer.Id}
Position: ??? ???";
e.Handled = true;
}
private void Border_PointerReleased(object sender, PointerReleasedEventArgs e)
{
if (e.Pointer.Captured == sender)
{
e.Pointer.Capture(null);
e.Handled = true;
}
else
{
throw new InvalidOperationException("How?");
}
}
private void Border_PointerPressed(object sender, PointerPressedEventArgs e)
{
e.Pointer.Capture((Border)sender);
e.Handled = true;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

27
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@ -1,22 +1,37 @@
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ProgressBarPage">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h2">A progress bar control</TextBlock>
<StackPanel>
<StackPanel Spacing="5">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Maximum</TextBlock>
<NumericUpDown x:Name="maximum" Value="100" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Minimum</TextBlock>
<NumericUpDown x:Name="minimum" Value="0" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Progress Text Format</TextBlock>
<TextBox x:Name="stringFormat" Text="{}{0:0}%" VerticalAlignment="Center"/>
</StackPanel>
<CheckBox x:Name="showProgress" Margin="10,16,0,0" Content="Show Progress Text" />
<CheckBox x:Name="isIndeterminate" Margin="10,16,0,0" Content="Toggle Indeterminate" />
<StackPanel Orientation="Horizontal" Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16">
<StackPanel Spacing="16">
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}" />
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}"
Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" ProgressTextFormat="{Binding #stringFormat.Text}"/>
</StackPanel>
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical" />
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical"
Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" ProgressTextFormat="{Binding #stringFormat.Text}"/>
</StackPanel>
<StackPanel Margin="16">
<Slider Name="hprogress" Maximum="100" Value="40" />
<Slider Name="vprogress" Maximum="100" Value="60" />
<Slider Name="hprogress" Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" Value="40" />
<Slider Name="vprogress" Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" Value="60" />
</StackPanel>
<StackPanel Spacing="10">
<ProgressBar VerticalAlignment="Center" IsIndeterminate="True" />
<ProgressBar VerticalAlignment="Center" IsIndeterminate="True"
Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.value}"/>
<ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
<ProgressBar VerticalAlignment="Center" Value="50" />
<ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />

2
samples/ControlCatalog/Pages/ScreenPage.cs

@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
Window w = (Window)VisualRoot;
Window w = (Window)VisualRoot!;
w.PositionChanged += (sender, args) => InvalidateVisual();
}

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

@ -52,7 +52,7 @@ namespace ControlCatalog.Pages
private IBitmap LoadBitmap(string uri)
{
var assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
var assets = AvaloniaLocator.Current!.GetService<IAssetLoader>()!;
return new Bitmap(assets.Open(new Uri(uri)));
}
@ -60,7 +60,7 @@ namespace ControlCatalog.Pages
{
private Dock _tabPlacement;
public TabItemViewModel[] Tabs { get; set; }
public TabItemViewModel[]? Tabs { get; set; }
public Dock TabPlacement
{
@ -71,10 +71,10 @@ namespace ControlCatalog.Pages
private class TabItemViewModel
{
public string Header { get; set; }
public string Text { get; set; }
public IBitmap Image { get; set; }
public bool IsEnabled { get; set; } = true;
public string? Header { get; set; }
public string? Text { get; set; }
public IBitmap? Image { get; set; }
public bool IsEnabled { get; set; } = true;
}
}
}

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

@ -38,7 +38,7 @@ namespace ControlCatalog.Pages
private class TabStripItemViewModel
{
public string Header { get; set; }
public string? Header { get; set; }
public bool IsEnabled { get; set; } = true;
}
}

4
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -118,7 +118,7 @@
</StackPanel>
</Border>
<Border>
<TextBlock Margin="10" TextWrapping="Wrap">
<RichTextBlock Margin="10" TextWrapping="Wrap">
This <Span FontWeight="Bold">is</Span> a
<Span Background="Silver" Foreground="Maroon">TextBlock</Span>
with <Span TextDecorations="Underline">several</Span>
@ -126,7 +126,7 @@
<Span Foreground="Blue">
using a <Bold>variety</Bold> of <Italic>styles</Italic>
</Span>.
</TextBlock>
</RichTextBlock>
</Border>
</WrapPanel>
</StackPanel>

2
samples/ControlCatalog/ViewModels/ApplicationViewModel.cs

@ -10,7 +10,7 @@ namespace ControlCatalog.ViewModels
{
ExitCommand = MiniCommand.Create(() =>
{
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
lifetime.Shutdown();
}

2
samples/ControlCatalog/ViewModels/ContextPageViewModel.cs

@ -9,7 +9,7 @@ namespace ControlCatalog.ViewModels
{
public class ContextPageViewModel
{
public Control View { get; set; }
public Control? View { get; set; }
public ContextPageViewModel()
{
OpenCommand = MiniCommand.CreateFromTask(Open);

2
samples/ControlCatalog/ViewModels/CursorPageViewModel.cs

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

19
samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs

@ -14,7 +14,7 @@ namespace ControlCatalog.ViewModels
public ItemsRepeaterPageViewModel()
{
Items = CreateItems();
_items = CreateItems();
}
public ObservableCollection<Item> Items
@ -23,12 +23,12 @@ namespace ControlCatalog.ViewModels
set => this.RaiseAndSetIfChanged(ref _items, value);
}
public Item SelectedItem { get; set; }
public Item? SelectedItem { get; set; }
public void AddItem()
{
var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
Items.Insert(index + 1, new Item(index + 1) { Text = $"New Item {_newItemIndex++}" });
Items.Insert(index + 1, new Item(index + 1, $"New Item {_newItemIndex++}"));
}
public void RemoveItem()
@ -66,19 +66,20 @@ namespace ControlCatalog.ViewModels
_newGenerationIndex++;
return new ObservableCollection<Item>(
Enumerable.Range(1, 100000).Select(i => new Item(i)
{
Text = $"Item {i.ToString()} {suffix}"
}));
Enumerable.Range(1, 100000).Select(i => new Item(i, $"Item {i.ToString()} {suffix}")));
}
public class Item : ViewModelBase
{
private double _height = double.NaN;
public Item(int index) => Index = index;
public Item(int index, string text)
{
Index = index;
Text = text;
}
public int Index { get; }
public string Text { get; set; }
public string Text { get; }
public double Height
{

13
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -16,11 +16,11 @@ namespace ControlCatalog.ViewModels
private bool _isMenuItemChecked = true;
private WindowState _windowState;
private WindowState[] _windowStates;
private WindowState[] _windowStates = Array.Empty<WindowState>();
private int _transparencyLevel;
private ExtendClientAreaChromeHints _chromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
private bool _extendClientAreaEnabled;
private bool _systemTitleBarEnabled;
private bool _systemTitleBarEnabled;
private bool _preferSystemChromeEnabled;
private double _titleBarHeight;
@ -47,14 +47,15 @@ namespace ControlCatalog.ViewModels
{
var dialog = new AboutAvaloniaDialog();
var mainWindow = (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
await dialog.ShowDialog(mainWindow);
if ((App.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow is { } mainWindow)
{
await dialog.ShowDialog(mainWindow);
}
});
ExitCommand = MiniCommand.Create(() =>
{
(App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown();
(App.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.Shutdown();
});
ToggleMenuItemCheckedCommand = MiniCommand.Create(() =>

8
samples/ControlCatalog/ViewModels/MenuItemViewModel.cs

@ -5,9 +5,9 @@ namespace ControlCatalog.ViewModels
{
public class MenuItemViewModel
{
public string Header { get; set; }
public ICommand Command { get; set; }
public object CommandParameter { get; set; }
public IList<MenuItemViewModel> Items { get; set; }
public string? Header { get; set; }
public ICommand? Command { get; set; }
public object? CommandParameter { get; set; }
public IList<MenuItemViewModel>? Items { get; set; }
}
}

2
samples/ControlCatalog/ViewModels/MenuPageViewModel.cs

@ -10,7 +10,7 @@ namespace ControlCatalog.ViewModels
{
public class MenuPageViewModel
{
public Control View { get; set; }
public Control? View { get; set; }
public MenuPageViewModel()
{
OpenCommand = MiniCommand.CreateFromTask(Open);

4
samples/ControlCatalog/ViewModels/NotificationViewModel.cs

@ -19,8 +19,8 @@ namespace ControlCatalog.ViewModels
});
}
public string Title { get; set; }
public string Message { get; set; }
public string? Title { get; set; }
public string? Message { get; set; }
public MiniCommand YesCommand { get; }

14
samples/ControlCatalog/ViewModels/TransitioningContentControlPageViewModel.cs

@ -19,7 +19,7 @@ namespace ControlCatalog.ViewModels
{
public TransitioningContentControlPageViewModel()
{
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
var assetLoader = AvaloniaLocator.Current?.GetService<IAssetLoader>()!;
var images = new string[]
{
@ -36,8 +36,8 @@ namespace ControlCatalog.ViewModels
SetupTransitions();
SelectedTransition = PageTransitions[1];
SelectedImage = Images[0];
_SelectedTransition = PageTransitions[1];
_SelectedImage = Images[0];
}
public List<PageTransition> PageTransitions { get; } = new List<PageTransition>();
@ -160,12 +160,12 @@ namespace ControlCatalog.ViewModels
public string DisplayTitle { get; }
private IPageTransition _Transition;
private IPageTransition? _Transition;
/// <summary>
/// Gets or sets the transition
/// </summary>
public IPageTransition Transition
public IPageTransition? Transition
{
get { return _Transition; }
set { this.RaiseAndSetIfChanged(ref _Transition, value); }
@ -201,7 +201,7 @@ namespace ControlCatalog.ViewModels
/// </summary>
public TimeSpan Duration { get; set; }
public async Task Start(Visual from, Visual to, bool forward, CancellationToken cancellationToken)
public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
@ -293,7 +293,7 @@ namespace ControlCatalog.ViewModels
/// <remarks>
/// Any one of the parameters may be null, but not both.
/// </remarks>
private static IVisual GetVisualParent(IVisual from, IVisual to)
private static IVisual GetVisualParent(IVisual? from, IVisual? to)
{
var p1 = (from ?? to)!.VisualParent;
var p2 = (to ?? from)!.VisualParent;

4
samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs

@ -92,7 +92,7 @@ namespace ControlCatalog.ViewModels
public class Node
{
private ObservableCollection<Node> _children;
private ObservableCollection<Node>? _children;
private int _childIndex = 10;
public Node()
@ -106,7 +106,7 @@ namespace ControlCatalog.ViewModels
Header = parent.Header + ' ' + index;
}
public Node Parent { get; }
public Node? Parent { get; }
public string Header { get; }
public bool AreChildrenInitialized => _children != null;
public ObservableCollection<Node> Children => _children ??= CreateChildren();

21
samples/IntegrationTestApp/MainWindow.axaml

@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="IntegrationTestApp.MainWindow"
Name="MainWindow"
Title="IntegrationTestApp">
<NativeMenu.Menu>
<NativeMenu>
@ -94,6 +95,26 @@
</StackPanel>
</DockPanel>
</TabItem>
<TabItem Header="Window">
<StackPanel>
<TextBox Name="ShowWindowSize" Watermark="Window Size"/>
<ComboBox Name="ShowWindowMode" SelectedIndex="0">
<ComboBoxItem>NonOwned</ComboBoxItem>
<ComboBoxItem>Owned</ComboBoxItem>
<ComboBoxItem>Modal</ComboBoxItem>
</ComboBox>
<ComboBox Name="ShowWindowLocation" SelectedIndex="0">
<ComboBoxItem>Manual</ComboBoxItem>
<ComboBoxItem>CenterScreen</ComboBoxItem>
<ComboBoxItem>CenterOwner</ComboBoxItem>
</ComboBox>
<Button Name="ShowWindow">Show Window</Button>
<Button Name="SendToBack">Send to Back</Button>
<Button Name="ExitFullscreen">Exit Fullscreen</Button>
<Button Name="RestoreAll">Restore All</Button>
</StackPanel>
</TabItem>
</TabControl>
</DockPanel>
</Window>

66
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
namespace IntegrationTestApp
{
@ -46,6 +48,62 @@ namespace IntegrationTestApp
}
}
private void ShowWindow()
{
var sizeTextBox = this.GetControl<TextBox>("ShowWindowSize");
var modeComboBox = this.GetControl<ComboBox>("ShowWindowMode");
var locationComboBox = this.GetControl<ComboBox>("ShowWindowLocation");
var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null;
var owner = (Window)this.GetVisualRoot()!;
var window = new ShowWindowTest
{
WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex,
};
if (size.HasValue)
{
window.Width = size.Value.Width;
window.Height = size.Value.Height;
}
sizeTextBox.Text = string.Empty;
switch (modeComboBox.SelectedIndex)
{
case 0:
window.Show();
break;
case 1:
window.Show(owner);
break;
case 2:
window.ShowDialog(owner);
break;
}
}
private void SendToBack()
{
var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
foreach (var window in lifetime.Windows)
{
window.Activate();
}
}
private void RestoreAll()
{
var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
foreach (var window in lifetime.Windows)
{
if (window.WindowState == WindowState.Minimized)
window.WindowState = WindowState.Normal;
}
}
private void MenuClicked(object? sender, RoutedEventArgs e)
{
var clickedMenuItemTextBlock = this.FindControl<TextBlock>("ClickedMenuItem");
@ -64,6 +122,14 @@ namespace IntegrationTestApp
this.FindControl<ListBox>("BasicListBox").SelectedIndex = -1;
if (source?.Name == "MenuClickedMenuItemReset")
this.FindControl<TextBlock>("ClickedMenuItem").Text = "None";
if (source?.Name == "ShowWindow")
ShowWindow();
if (source?.Name == "SendToBack")
SendToBack();
if (source?.Name == "ExitFullscreen")
WindowState = WindowState.Normal;
if (source?.Name == "RestoreAll")
RestoreAll();
}
}
}

35
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -0,0 +1,35 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="IntegrationTestApp.ShowWindowTest"
Name="SecondaryWindow"
Title="Show Window Test">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="ClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}"/>
<Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
<TextBox Name="FrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"
Text="{Binding FrameSize, Mode=OneWay}"/>
<Label Grid.Column="0" Grid.Row="3">Position</Label>
<TextBox Name="Position" Grid.Column="1" Grid.Row="3" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="4">Owner Rect</Label>
<TextBox Name="OwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="5">Screen Rect</Label>
<TextBox Name="ScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="6">Scaling</Label>
<TextBox Name="Scaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True"/>
<Label Grid.Column="0" Grid.Row="7">WindowState</Label>
<ComboBox Name="WindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
<ComboBoxItem>Normal</ComboBoxItem>
<ComboBoxItem>Minimized</ComboBoxItem>
<ComboBoxItem>Maximized</ComboBoxItem>
<ComboBoxItem>Fullscreen</ComboBoxItem>
</ComboBox>
</Grid>
</Window>

40
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@ -0,0 +1,40 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Rendering;
namespace IntegrationTestApp
{
public class ShowWindowTest : Window
{
public ShowWindowTest()
{
InitializeComponent();
DataContext = this;
PositionChanged += (s, e) => this.GetControl<TextBox>("Position").Text = $"{Position}";
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
var scaling = PlatformImpl!.DesktopScaling;
this.GetControl<TextBox>("Position").Text = $"{Position}";
this.GetControl<TextBox>("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}";
this.GetControl<TextBox>("Scaling").Text = $"{scaling}";
if (Owner is not null)
{
var ownerRect = this.GetControl<TextBox>("OwnerRect");
var owner = (Window)Owner;
ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}";
}
}
}
}

4
samples/RenderDemo/App.xaml.cs

@ -29,6 +29,10 @@ namespace RenderDemo
.With(new Win32PlatformOptions
{
OverlayPopups = true,
})
.With(new X11PlatformOptions
{
UseCompositor = true
})
.UsePlatformDetect()
.LogToTrace();

4
samples/RenderDemo/Pages/FormattedTextPage.axaml.cs

@ -55,6 +55,10 @@ namespace RenderDemo.Pages
formattedText.SetFontStyle(FontStyle.Italic, 28, 28);
context.DrawText(formattedText, new Point(10, 0));
var geometry = formattedText.BuildGeometry(new Point(10 + formattedText.Width + 10, 0));
context.DrawGeometry(gradient, null, geometry);
}
}
}

12
samples/RenderDemo/Pages/GlyphRunPage.xaml

@ -2,13 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:RenderDemo.Pages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="RenderDemo.Pages.GlyphRunPage">
<Border
<Grid
ColumnDefinitions="*,*"
Background="White">
<Image
x:Name="imageControl"
Stretch="None">
</Image>
</Border>
<local:GlyphRunControl Grid.Column="0"/>
<local:GlyphRunGeometryControl Grid.Column="1"/>
</Grid>
</UserControl>

113
samples/RenderDemo/Pages/GlyphRunPage.xaml.cs

@ -9,14 +9,6 @@ namespace RenderDemo.Pages
{
public class GlyphRunPage : UserControl
{
private Image _imageControl;
private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
private readonly Random _rand = new Random();
private ushort[] _glyphIndices = new ushort[1];
private char[] _characters = new char[1];
private float _fontSize = 20;
private int _direction = 10;
public GlyphRunPage()
{
this.InitializeComponent();
@ -25,19 +17,43 @@ namespace RenderDemo.Pages
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
public class GlyphRunControl : Control
{
private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
private readonly Random _rand = new Random();
private ushort[] _glyphIndices = new ushort[1];
private char[] _characters = new char[1];
private float _fontSize = 20;
private int _direction = 10;
_imageControl = this.FindControl<Image>("imageControl");
_imageControl.Source = new DrawingImage();
private DispatcherTimer _timer;
DispatcherTimer.Run(() =>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
_timer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_timer.Tick += (s,e) =>
{
UpdateGlyphRun();
InvalidateVisual();
};
_timer.Start();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_timer.Stop();
return true;
}, TimeSpan.FromSeconds(1));
_timer = null;
}
private void UpdateGlyphRun()
public override void Render(DrawingContext context)
{
var c = (char)_rand.Next(65, 90);
@ -57,27 +73,70 @@ namespace RenderDemo.Pages
_characters[0] = c;
var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices);
var drawingGroup = new DrawingGroup();
context.DrawGlyphRun(Brushes.Black, glyphRun);
}
}
var glyphRunDrawing = new GlyphRunDrawing
public class GlyphRunGeometryControl : Control
{
private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
private readonly Random _rand = new Random();
private ushort[] _glyphIndices = new ushort[1];
private char[] _characters = new char[1];
private float _fontSize = 20;
private int _direction = 10;
private DispatcherTimer _timer;
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
_timer = new DispatcherTimer
{
Foreground = Brushes.Black,
GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices)
Interval = TimeSpan.FromSeconds(1)
};
drawingGroup.Children.Add(glyphRunDrawing);
var geometryDrawing = new GeometryDrawing
_timer.Tick += (s, e) =>
{
Pen = new Pen(Brushes.Black),
Geometry = new RectangleGeometry { Rect = new Rect(glyphRunDrawing.GlyphRun.Size) }
InvalidateVisual();
};
drawingGroup.Children.Add(geometryDrawing);
_timer.Start();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_timer.Stop();
_timer = null;
}
public override void Render(DrawingContext context)
{
var c = (char)_rand.Next(65, 90);
if (_fontSize + _direction > 200)
{
_direction = -10;
}
if (_fontSize + _direction < 20)
{
_direction = 10;
}
_fontSize += _direction;
_glyphIndices[0] = _glyphTypeface.GetGlyph(c);
_characters[0] = c;
var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices);
var geometry = glyphRun.BuildGeometry();
(_imageControl.Source as DrawingImage).Drawing = drawingGroup;
context.DrawGeometry(Brushes.Green, null, geometry);
}
}
}

5
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -1,10 +1,8 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Android;
using Avalonia.Android.Platform;
using Avalonia.Android.Platform.Input;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.OpenGL.Egl;
@ -55,14 +53,11 @@ namespace Avalonia.Android
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToConstant(Instance)
.Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
.Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
SkiaPlatform.Initialize();
if (options.UseGpu)
{
EglPlatformOpenGlInterface.TryInitialize();

39
src/Android/Avalonia.Android/AvaloniaActivity.cs

@ -4,10 +4,14 @@ using Android.Content.Res;
using AndroidX.Lifecycle;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls;
using Android.Runtime;
using Android.App;
using Android.Content;
using System;
namespace Avalonia.Android
{
public abstract class AvaloniaActivity<TApp> : AppCompatActivity where TApp : Application, new()
public abstract class AvaloniaActivity : AppCompatActivity
{
internal class SingleViewLifetime : ISingleViewApplicationLifetime
{
@ -20,32 +24,32 @@ namespace Avalonia.Android
}
}
internal Action<int, Result, Intent> ActivityResult;
internal AvaloniaView View;
internal AvaloniaViewModel _viewModel;
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid();
protected abstract AppBuilder CreateAppBuilder();
protected override void OnCreate(Bundle savedInstanceState)
{
var builder = AppBuilder.Configure<TApp>();
CustomizeAppBuilder(builder);
var builder = CreateAppBuilder();
View = new AvaloniaView(this);
SetContentView(View);
var lifetime = new SingleViewLifetime();
lifetime.View = View;
builder.AfterSetup(x =>
{
_viewModel = new ViewModelProvider(this).Get(Java.Lang.Class.FromType(typeof(AvaloniaViewModel))) as AvaloniaViewModel;
View = new AvaloniaView(this);
if (_viewModel.Content != null)
{
View.Content = _viewModel.Content;
}
SetContentView(View);
lifetime.View = View;
View.Prepare();
});
@ -78,5 +82,24 @@ namespace Avalonia.Android
base.OnDestroy();
}
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
ActivityResult?.Invoke(requestCode, resultCode, data);
}
}
public abstract class AvaloniaActivity<TApp> : AvaloniaActivity where TApp : Application, new()
{
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid();
protected override AppBuilder CreateAppBuilder()
{
var builder = AppBuilder.Configure<TApp>();
return CustomizeAppBuilder(builder);
}
}
}

3
src/Android/Avalonia.Android/ChoreographerTimer.cs

@ -29,6 +29,9 @@ namespace Avalonia.Android
_thread = new Thread(Loop);
_thread.Start();
}
public bool RunsInBackground => true;
public event Action<TimeSpan> Tick
{

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

@ -7,6 +7,7 @@ using Android.Views.InputMethods;
using Avalonia.Android.OpenGL;
using Avalonia.Android.Platform.Specific;
using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Android.Platform.Storage;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Platform.Surfaces;
@ -16,11 +17,13 @@ using Avalonia.Input.TextInput;
using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
namespace Avalonia.Android.Platform.SkiaPlatform
{
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo,
ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
{
private readonly IGlPlatformSurface _gl;
private readonly IFramebufferPlatformSurface _framebuffer;
@ -46,6 +49,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
StorageProvider = new AndroidStorageProvider((AvaloniaActivity)avaloniaView.Context);
}
public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
@ -225,6 +229,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public ITextInputMethodImpl TextInputMethod => _textInputMethod;
public INativeControlHostImpl NativeControlHost { get; }
public IStorageProvider StorageProvider { get; }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{

244
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@ -0,0 +1,244 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Android.Content;
using Android.Provider;
using Avalonia.Logging;
using Avalonia.Platform.Storage;
using Java.Lang;
using AndroidUri = Android.Net.Uri;
using Exception = System.Exception;
using JavaFile = Java.IO.File;
namespace Avalonia.Android.Platform.Storage;
internal abstract class AndroidStorageItem : IStorageBookmarkItem
{
private Context? _context;
protected AndroidStorageItem(Context context, AndroidUri uri)
{
_context = context;
Uri = uri;
}
internal AndroidUri Uri { get; }
protected Context Context => _context ?? throw new ObjectDisposedException(nameof(AndroidStorageItem));
public string Name => GetColumnValue(Context, Uri, MediaStore.IMediaColumns.DisplayName)
?? Uri.PathSegments?.LastOrDefault() ?? string.Empty;
public bool CanBookmark => true;
public Task<string?> SaveBookmark()
{
Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
return Task.FromResult(Uri.ToString());
}
public Task ReleaseBookmark()
{
Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
return Task.CompletedTask;
}
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
uri = new Uri(Uri.ToString()!);
return true;
}
public abstract Task<StorageItemProperties> GetBasicPropertiesAsync();
protected string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null)
{
try
{
var projection = new[] { column };
using var cursor = context.ContentResolver!.Query(contentUri, projection, selection, selectionArgs, null);
if (cursor?.MoveToFirst() == true)
{
var columnIndex = cursor.GetColumnIndex(column);
if (columnIndex != -1)
return cursor.GetString(columnIndex);
}
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?.Log(this, "File metadata reader failed: '{Exception}'", ex);
}
return null;
}
public Task<IStorageFolder?> GetParentAsync()
{
using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused.
if (javaFile.ParentFile is {} parentFile
&& AndroidUri.FromFile(parentFile) is {} androidUri)
{
return Task.FromResult<IStorageFolder?>(new AndroidStorageFolder(Context, androidUri));
}
return Task.FromResult<IStorageFolder?>(null);
}
public void Dispose()
{
_context = null;
}
}
internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
{
public AndroidStorageFolder(Context context, AndroidUri uri) : base(context, uri)
{
}
public override Task<StorageItemProperties> GetBasicPropertiesAsync()
{
return Task.FromResult(new StorageItemProperties());
}
}
internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile
{
public AndroidStorageFile(Context context, AndroidUri uri) : base(context, uri)
{
}
public bool CanOpenRead => true;
public bool CanOpenWrite => true;
public Task<Stream> OpenRead() => Task.FromResult(OpenContentStream(Context, Uri, false)
?? throw new InvalidOperationException("Failed to open content stream"));
public Task<Stream> OpenWrite() => Task.FromResult(OpenContentStream(Context, Uri, true)
?? throw new InvalidOperationException("Failed to open content stream"));
private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput)
{
var isVirtual = IsVirtualFile(context, uri);
if (isVirtual)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?.Log(this, "Content URI was virtual: '{Uri}'", uri);
return GetVirtualFileStream(context, uri, isOutput);
}
return isOutput
? context.ContentResolver?.OpenOutputStream(uri)
: context.ContentResolver?.OpenInputStream(uri);
}
private bool IsVirtualFile(Context context, AndroidUri uri)
{
if (!DocumentsContract.IsDocumentUri(context, uri))
return false;
var value = GetColumnValue(context, uri, DocumentsContract.Document.ColumnFlags);
if (!string.IsNullOrEmpty(value) && int.TryParse(value, out var flagsInt))
{
var flags = (DocumentContractFlags)flagsInt;
return flags.HasFlag(DocumentContractFlags.VirtualDocument);
}
return false;
}
private Stream? GetVirtualFileStream(Context context, AndroidUri uri, bool isOutput)
{
var mimeTypes = context.ContentResolver?.GetStreamTypes(uri, FilePickerFileTypes.All.MimeTypes![0]);
if (mimeTypes?.Length >= 1)
{
var mimeType = mimeTypes[0];
var asset = context.ContentResolver!
.OpenTypedAssetFileDescriptor(uri, mimeType, null);
var stream = isOutput
? asset?.CreateOutputStream()
: asset?.CreateInputStream();
return stream;
}
return null;
}
public override Task<StorageItemProperties> GetBasicPropertiesAsync()
{
ulong? size = null;
DateTimeOffset? itemDate = null;
DateTimeOffset? dateModified = null;
try
{
var projection = new[]
{
MediaStore.IMediaColumns.Size, MediaStore.IMediaColumns.DateAdded,
MediaStore.IMediaColumns.DateModified
};
using var cursor = Context.ContentResolver!.Query(Uri, projection, null, null, null);
if (cursor?.MoveToFirst() == true)
{
try
{
var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.Size);
if (columnIndex != -1)
{
size = (ulong)cursor.GetLong(columnIndex);
}
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "File Size metadata reader failed: '{Exception}'", ex);
}
try
{
var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.DateAdded);
if (columnIndex != -1)
{
var longValue = cursor.GetLong(columnIndex);
itemDate = longValue > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(longValue) : null;
}
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "File DateAdded metadata reader failed: '{Exception}'", ex);
}
try
{
var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.DateModified);
if (columnIndex != -1)
{
var longValue = cursor.GetLong(columnIndex);
dateModified = longValue > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(longValue) : null;
}
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "File DateAdded metadata reader failed: '{Exception}'", ex);
}
}
}
catch (UnsupportedOperationException)
{
// It's not possible to get parameters of some files/folders.
}
return Task.FromResult(new StorageItemProperties(size, itemDate, dateModified));
}
}

177
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs

@ -0,0 +1,177 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Provider;
using Avalonia.Platform.Storage;
using AndroidUri = Android.Net.Uri;
namespace Avalonia.Android.Platform.Storage;
internal class AndroidStorageProvider : IStorageProvider
{
private readonly AvaloniaActivity _activity;
private int _lastRequestCode = 20000;
public AndroidStorageProvider(AvaloniaActivity activity)
{
_activity = activity;
}
public bool CanOpen => OperatingSystem.IsAndroidVersionAtLeast(19);
public bool CanSave => OperatingSystem.IsAndroidVersionAtLeast(19);
public bool CanPickFolder => OperatingSystem.IsAndroidVersionAtLeast(21);
public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark));
return Task.FromResult<IStorageBookmarkFolder?>(new AndroidStorageFolder(_activity, uri));
}
public Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark));
return Task.FromResult<IStorageBookmarkFile?>(new AndroidStorageFile(_activity, uri));
}
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
var mimeTypes = options.FileTypeFilter?.Where(t => t != FilePickerFileTypes.All)
.SelectMany(f => f.MimeTypes ?? Array.Empty<string>()).Distinct().ToArray() ?? Array.Empty<string>();
var intent = new Intent(Intent.ActionOpenDocument)
.AddCategory(Intent.CategoryOpenable)
.PutExtra(Intent.ExtraAllowMultiple, options.AllowMultiple)
.SetType(FilePickerFileTypes.All.MimeTypes![0]);
if (mimeTypes.Length > 0)
{
intent = intent.PutExtra(Intent.ExtraMimeTypes, mimeTypes);
}
if (TryGetInitialUri(options.SuggestedStartLocation) is { } initialUri)
{
intent = intent.PutExtra(DocumentsContract.ExtraInitialUri, initialUri);
}
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select file");
var uris = await StartActivity(pickerIntent, false);
return uris.Select(u => new AndroidStorageFile(_activity, u)).ToArray();
}
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var mimeTypes = options.FileTypeChoices?.Where(t => t != FilePickerFileTypes.All)
.SelectMany(f => f.MimeTypes ?? Array.Empty<string>()).Distinct().ToArray() ?? Array.Empty<string>();
var intent = new Intent(Intent.ActionCreateDocument)
.AddCategory(Intent.CategoryOpenable)
.SetType(FilePickerFileTypes.All.MimeTypes![0]);
if (mimeTypes.Length > 0)
{
intent = intent.PutExtra(Intent.ExtraMimeTypes, mimeTypes);
}
if (options.SuggestedFileName is { } fileName)
{
if (options.DefaultExtension is { } ext)
{
fileName += ext.StartsWith('.') ? ext : "." + ext;
}
intent = intent.PutExtra(Intent.ExtraTitle, fileName);
}
if (TryGetInitialUri(options.SuggestedStartLocation) is { } initialUri)
{
intent = intent.PutExtra(DocumentsContract.ExtraInitialUri, initialUri);
}
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Save file");
var uris = await StartActivity(pickerIntent, true);
return uris.Select(u => new AndroidStorageFile(_activity, u)).FirstOrDefault();
}
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
var intent = new Intent(Intent.ActionOpenDocumentTree)
.PutExtra(Intent.ExtraAllowMultiple, options.AllowMultiple);
if (TryGetInitialUri(options.SuggestedStartLocation) is { } initialUri)
{
intent = intent.PutExtra(DocumentsContract.ExtraInitialUri, initialUri);
}
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select folder");
var uris = await StartActivity(pickerIntent, false);
return uris.Select(u => new AndroidStorageFolder(_activity, u)).ToArray();
}
private async Task<List<AndroidUri>> StartActivity(Intent? pickerIntent, bool singleResult)
{
var resultList = new List<AndroidUri>(1);
var tcs = new TaskCompletionSource<Intent?>();
var currentRequestCode = _lastRequestCode++;
_activity.ActivityResult += OnActivityResult;
_activity.StartActivityForResult(pickerIntent, currentRequestCode);
var result = await tcs.Task;
if (result != null)
{
// ClipData first to avoid issue with multiple files selection.
if (!singleResult && result.ClipData is { } clipData)
{
for (var i = 0; i < clipData.ItemCount; i++)
{
var uri = clipData.GetItemAt(i)?.Uri;
if (uri != null)
{
resultList.Add(uri);
}
}
}
else if (result.Data is { } uri)
{
resultList.Add(uri);
}
}
if (result?.HasExtra("error") == true)
{
throw new Exception(result.GetStringExtra("error"));
}
return resultList;
void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (currentRequestCode != requestCode)
{
return;
}
_activity.ActivityResult -= OnActivityResult;
_ = tcs.TrySetResult(resultCode == Result.Ok ? data : null);
}
}
private static AndroidUri? TryGetInitialUri(IStorageFolder? folder)
{
if (OperatingSystem.IsAndroidVersionAtLeast(26)
&& (folder as AndroidStorageItem)?.Uri is { } uri)
{
return uri;
}
return null;
}
}

20
src/Android/Avalonia.Android/SystemDialogImpl.cs

@ -1,20 +0,0 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
namespace Avalonia.Android
{
internal class SystemDialogImpl : ISystemDialogImpl
{
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
throw new NotImplementedException();
}
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
throw new NotImplementedException();
}
}
}

306
src/Avalonia.Base/Animation/Easings/CubicBezier.cs

@ -0,0 +1,306 @@
// ReSharper disable InconsistentNaming
// Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc
using System;
using Avalonia.Utilities;
// ReSharper disable CompareOfFloatsByEqualityOperator
// ReSharper disable CommentTypo
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable TooWideLocalVariableScope
// ReSharper disable UnusedMember.Global
#pragma warning disable 649
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Represents a cubic bezier curve and can compute Y coordinate for a given X
/// </summary>
internal unsafe struct CubicBezier
{
const int CUBIC_BEZIER_SPLINE_SAMPLES = 11;
double ax_;
double bx_;
double cx_;
double ay_;
double by_;
double cy_;
double start_gradient_;
double end_gradient_;
double range_min_;
double range_max_;
private bool monotonically_increasing_;
fixed double spline_samples_[CUBIC_BEZIER_SPLINE_SAMPLES];
public CubicBezier(double p1x, double p1y, double p2x, double p2y) : this()
{
InitCoefficients(p1x, p1y, p2x, p2y);
InitGradients(p1x, p1y, p2x, p2y);
InitRange(p1y, p2y);
InitSpline();
}
public readonly double SampleCurveX(double t)
{
// `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
return ((ax_ * t + bx_) * t + cx_) * t;
}
readonly double SampleCurveY(double t)
{
return ((ay_ * t + by_) * t + cy_) * t;
}
readonly double SampleCurveDerivativeX(double t)
{
return (3.0 * ax_ * t + 2.0 * bx_) * t + cx_;
}
readonly double SampleCurveDerivativeY(double t)
{
return (3.0 * ay_ * t + 2.0 * by_) * t + cy_;
}
public readonly double SolveWithEpsilon(double x, double epsilon)
{
if (x < 0.0)
return 0.0 + start_gradient_ * x;
if (x > 1.0)
return 1.0 + end_gradient_ * (x - 1.0);
return SampleCurveY(SolveCurveX(x, epsilon));
}
void InitCoefficients(double p1x,
double p1y,
double p2x,
double p2y)
{
// Calculate the polynomial coefficients, implicit first and last control
// points are (0,0) and (1,1).
cx_ = 3.0 * p1x;
bx_ = 3.0 * (p2x - p1x) - cx_;
ax_ = 1.0 - cx_ - bx_;
cy_ = 3.0 * p1y;
by_ = 3.0 * (p2y - p1y) - cy_;
ay_ = 1.0 - cy_ - by_;
#if DEBUG
// Bezier curves with x-coordinates outside the range [0,1] for internal
// control points may have multiple values for t for a given value of x.
// In this case, calls to SolveCurveX may produce ambiguous results.
monotonically_increasing_ = p1x >= 0 && p1x <= 1 && p2x >= 0 && p2x <= 1;
#endif
}
void InitGradients(double p1x,
double p1y,
double p2x,
double p2y)
{
// End-point gradients are used to calculate timing function results
// outside the range [0, 1].
//
// There are four possibilities for the gradient at each end:
// (1) the closest control point is not horizontally coincident with regard to
// (0, 0) or (1, 1). In this case the line between the end point and
// the control point is tangent to the bezier at the end point.
// (2) the closest control point is coincident with the end point. In
// this case the line between the end point and the far control
// point is tangent to the bezier at the end point.
// (3) both internal control points are coincident with an endpoint. There
// are two special case that fall into this category:
// CubicBezier(0, 0, 0, 0) and CubicBezier(1, 1, 1, 1). Both are
// equivalent to linear.
// (4) the closest control point is horizontally coincident with the end
// point, but vertically distinct. In this case the gradient at the
// end point is Infinite. However, this causes issues when
// interpolating. As a result, we break down to a simple case of
// 0 gradient under these conditions.
if (p1x > 0)
start_gradient_ = p1y / p1x;
else if (p1y == 0 && p2x > 0)
start_gradient_ = p2y / p2x;
else if (p1y == 0 && p2y == 0)
start_gradient_ = 1;
else
start_gradient_ = 0;
if (p2x < 1)
end_gradient_ = (p2y - 1) / (p2x - 1);
else if (p2y == 1 && p1x < 1)
end_gradient_ = (p1y - 1) / (p1x - 1);
else if (p2y == 1 && p1y == 1)
end_gradient_ = 1;
else
end_gradient_ = 0;
}
const double kBezierEpsilon = 1e-7;
void InitRange(double p1y, double p2y)
{
range_min_ = 0;
range_max_ = 1;
if (0 <= p1y && p1y < 1 && 0 <= p2y && p2y <= 1)
return;
double epsilon = kBezierEpsilon;
// Represent the function's derivative in the form at^2 + bt + c
// as in sampleCurveDerivativeY.
// (Technically this is (dy/dt)*(1/3), which is suitable for finding zeros
// but does not actually give the slope of the curve.)
double a = 3.0 * ay_;
double b = 2.0 * by_;
double c = cy_;
// Check if the derivative is constant.
if (Math.Abs(a) < epsilon && Math.Abs(b) < epsilon)
return;
// Zeros of the function's derivative.
double t1;
double t2 = 0;
if (Math.Abs(a) < epsilon)
{
// The function's derivative is linear.
t1 = -c / b;
}
else
{
// The function's derivative is a quadratic. We find the zeros of this
// quadratic using the quadratic formula.
double discriminant = b * b - 4 * a * c;
if (discriminant < 0)
return;
double discriminant_sqrt = Math.Sqrt(discriminant);
t1 = (-b + discriminant_sqrt) / (2 * a);
t2 = (-b - discriminant_sqrt) / (2 * a);
}
double sol1 = 0;
double sol2 = 0;
// If the solution is in the range [0,1] then we include it, otherwise we
// ignore it.
// An interesting fact about these beziers is that they are only
// actually evaluated in [0,1]. After that we take the tangent at that point
// and linearly project it out.
if (0 < t1 && t1 < 1)
sol1 = SampleCurveY(t1);
if (0 < t2 && t2 < 1)
sol2 = SampleCurveY(t2);
range_min_ = Math.Min(Math.Min(range_min_, sol1), sol2);
range_max_ = Math.Max(Math.Max(range_max_, sol1), sol2);
}
void InitSpline()
{
double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1);
for (int i = 0; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++)
{
spline_samples_[i] = SampleCurveX(i * delta_t);
}
}
const int kMaxNewtonIterations = 4;
public readonly double SolveCurveX(double x, double epsilon)
{
if (x < 0 || x > 1)
throw new ArgumentException();
double t0 = 0;
double t1 = 0;
double t2 = x;
double x2 = 0;
double d2;
int i;
#if DEBUG
if (!monotonically_increasing_)
throw new InvalidOperationException();
#endif
// Linear interpolation of spline curve for initial guess.
double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1);
for (i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++)
{
if (x <= spline_samples_[i])
{
t1 = delta_t * i;
t0 = t1 - delta_t;
t2 = t0 + (t1 - t0) * (x - spline_samples_[i - 1]) /
(spline_samples_[i] - spline_samples_[i - 1]);
break;
}
}
// Perform a few iterations of Newton's method -- normally very fast.
// See https://en.wikipedia.org/wiki/Newton%27s_method.
double newton_epsilon = Math.Min(kBezierEpsilon, epsilon);
for (i = 0; i < kMaxNewtonIterations; i++)
{
x2 = SampleCurveX(t2) - x;
if (Math.Abs(x2) < newton_epsilon)
return t2;
d2 = SampleCurveDerivativeX(t2);
if (Math.Abs(d2) < kBezierEpsilon)
break;
t2 = t2 - x2 / d2;
}
if (Math.Abs(x2) < epsilon)
return t2;
// Fall back to the bisection method for reliability.
while (t0 < t1)
{
x2 = SampleCurveX(t2);
if (Math.Abs(x2 - x) < epsilon)
return t2;
if (x > x2)
t0 = t2;
else
t1 = t2;
t2 = (t1 + t0) * .5;
}
// Failure.
return t2;
}
public readonly double Solve(double x)
{
return SolveWithEpsilon(x, kBezierEpsilon);
}
public readonly double SlopeWithEpsilon(double x, double epsilon)
{
x = MathUtilities.Clamp(x, 0.0, 1.0);
double t = SolveCurveX(x, epsilon);
double dx = SampleCurveDerivativeX(t);
double dy = SampleCurveDerivativeY(t);
return dy / dx;
}
public readonly double Slope(double x)
{
return SlopeWithEpsilon(x, kBezierEpsilon);
}
public readonly double RangeMin => range_min_;
public readonly double RangeMax => range_max_;
}
}

27
src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs

@ -0,0 +1,27 @@
using System;
namespace Avalonia.Animation.Easings;
public class CubicBezierEasing : IEasing
{
private CubicBezier _bezier;
//cubic-bezier(0.25, 0.1, 0.25, 1.0)
internal CubicBezierEasing(Point controlPoint1, Point controlPoint2)
{
ControlPoint1 = controlPoint1;
ControlPoint2 = controlPoint2;
if (controlPoint1.X < 0 || controlPoint1.X > 1 || controlPoint2.X < 0 || controlPoint2.X > 1)
throw new ArgumentException();
_bezier = new CubicBezier(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y);
}
public Point ControlPoint2 { get; set; }
public Point ControlPoint1 { get; set; }
internal static IEasing Ease { get; } = new CubicBezierEasing(new Point(0.25, 0.1), new Point(0.25, 1));
double IEasing.Ease(double progress)
{
return _bezier.Solve(progress);
}
}

BIN
src/Avalonia.Base/Assets/BiDi.trie

Binary file not shown.

BIN
src/Avalonia.Base/Assets/GraphemeBreak.trie

Binary file not shown.

BIN
src/Avalonia.Base/Assets/UnicodeData.trie

Binary file not shown.

13
src/Avalonia.Base/Avalonia.Base.csproj

@ -3,10 +3,13 @@
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AssemblyName>Avalonia.Base</AssemblyName>
<RootNamespace>Avalonia</RootNamespace>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\*.trie" />
<AdditionalFiles Include="composition-schema.xml" />
</ItemGroup>
<Import Project="..\..\build\Base.props" />
<Import Project="..\..\build\Binding.props" />
@ -29,11 +32,15 @@
<InternalsVisibleTo Include="Avalonia.Direct2D1.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.Xaml.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.PlatformSupport, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Web.Blazor, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7"/>
<InternalsVisibleTo Include="Avalonia.Dialogs, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
</ItemGroup>
<ItemGroup>
<Folder Include="Rendering\Composition\Utils" />
</ItemGroup>
</Project>

3
src/Avalonia.Base/AvaloniaObject.cs

@ -951,7 +951,8 @@ namespace Avalonia
public void Dispose()
{
_subscription.Dispose();
// _subscription can be null, if Subscribe failed with an exception.
_subscription?.Dispose();
_owner._directBindings!.Remove(this);
}

1
src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;

2
src/Avalonia.Base/Collections/Pooled/PooledList.cs

@ -1434,7 +1434,7 @@ namespace Avalonia.Collections.Pooled
/// <summary>
/// Returns the internal buffers to the ArrayPool.
/// </summary>
public void Dispose()
public virtual void Dispose()
{
ReturnArray();
_size = 0;

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

Loading…
Cancel
Save