Browse Source

Merge branch 'master' into fix-nullable-path-intersect

pull/8439/head
Max Katz 4 years ago
committed by GitHub
parent
commit
ab21db1d78
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 0
      .ncrunch/ControlCatalog.net6.0.v3.ncrunchproject
  2. 5
      .ncrunch/ControlCatalog.netstandard2.0.v3.ncrunchproject
  3. 15
      Avalonia.sln
  4. 67
      azure-pipelines-integrationtests.yml
  5. 2
      build/SourceGenerators.props
  6. 4
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  7. 6
      native/Avalonia.Native/src/OSX/AvnView.mm
  8. 52
      native/Avalonia.Native/src/OSX/SystemDialogs.mm
  9. 1
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  10. 2
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  11. 6
      native/Avalonia.Native/src/OSX/rendertarget.mm
  12. 4
      samples/BindingDemo/App.xaml
  13. 2
      samples/BindingDemo/BindingDemo.csproj
  14. 5
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  15. 3
      samples/ControlCatalog.NetCore/NativeControls/Gtk/EmbedSample.Gtk.cs
  16. 9
      samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs
  17. 19
      samples/ControlCatalog.NetCore/Program.cs
  18. 1
      samples/ControlCatalog.Web/App.razor.cs
  19. 9
      samples/ControlCatalog/App.xaml
  20. 2
      samples/ControlCatalog/ControlCatalog.csproj
  21. 6
      samples/ControlCatalog/MainView.xaml
  22. 5
      samples/ControlCatalog/MainView.xaml.cs
  23. 1
      samples/ControlCatalog/Pages/ButtonsPage.xaml
  24. 44
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  25. 45
      samples/ControlCatalog/Pages/CompositionPage.axaml
  26. 153
      samples/ControlCatalog/Pages/CompositionPage.axaml.cs
  27. 76
      samples/ControlCatalog/Pages/DialogsPage.xaml
  28. 234
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  29. 15
      samples/ControlCatalog/Pages/ExpanderPage.xaml
  30. 2
      samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
  31. 6
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  32. 13
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
  33. 44
      samples/ControlCatalog/Pages/OpenGlPage.xaml.cs
  34. 27
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  35. 4
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  36. 27
      samples/ControlCatalog/ViewModels/ExpanderPageViewModel.cs
  37. 8
      samples/RenderDemo/App.xaml
  38. 4
      samples/RenderDemo/App.xaml.cs
  39. 286
      samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
  40. 3
      src/Android/Avalonia.Android/AndroidPlatform.cs
  41. 32
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  42. 3
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  43. 8
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  44. 244
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  45. 177
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
  46. 20
      src/Android/Avalonia.Android/SystemDialogImpl.cs
  47. 306
      src/Avalonia.Base/Animation/Easings/CubicBezier.cs
  48. 27
      src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs
  49. 12
      src/Avalonia.Base/Avalonia.Base.csproj
  50. 1
      src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs
  51. 2
      src/Avalonia.Base/Collections/Pooled/PooledList.cs
  52. 56
      src/Avalonia.Base/Controls/Classes.cs
  53. 14
      src/Avalonia.Base/Controls/IClassesChangedListener.cs
  54. 7
      src/Avalonia.Base/Controls/IPseudoClasses.cs
  55. 2
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  56. 5
      src/Avalonia.Base/Input/Cursor.cs
  57. 18
      src/Avalonia.Base/Input/DragEventArgs.cs
  58. 11
      src/Avalonia.Base/Input/GotFocusEventArgs.cs
  59. 5
      src/Avalonia.Base/Input/IInputRoot.cs
  60. 13
      src/Avalonia.Base/Input/IKeyboardDevice.cs
  61. 12
      src/Avalonia.Base/Input/IMouseDevice.cs
  62. 14
      src/Avalonia.Base/Input/IPointerDevice.cs
  63. 2
      src/Avalonia.Base/Input/KeyEventArgs.cs
  64. 12
      src/Avalonia.Base/Input/KeyGesture.cs
  65. 54
      src/Avalonia.Base/Input/MouseDevice.cs
  66. 19
      src/Avalonia.Base/Input/PenDevice.cs
  67. 62
      src/Avalonia.Base/Input/PointerEventArgs.cs
  68. 2
      src/Avalonia.Base/Input/PointerOverPreProcessor.cs
  69. 5
      src/Avalonia.Base/Input/Raw/RawDragEvent.cs
  70. 3
      src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs
  71. 11
      src/Avalonia.Base/Input/TouchDevice.cs
  72. 8
      src/Avalonia.Base/Layout/LayoutManager.cs
  73. 10
      src/Avalonia.Base/Logging/LogArea.cs
  74. 109
      src/Avalonia.Base/Media/GlyphRun.cs
  75. 2
      src/Avalonia.Base/Media/TextDecoration.cs
  76. 51
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  77. 433
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  78. 4
      src/Avalonia.Base/Metadata/IAddChild.cs
  79. 16
      src/Avalonia.Base/Platform/IPlatformGpu.cs
  80. 107
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  81. 88
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  82. 35
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
  83. 40
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  84. 44
      src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
  85. 48
      src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs
  86. 19
      src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs
  87. 29
      src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
  88. 12
      src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs
  89. 20
      src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
  90. 32
      src/Avalonia.Base/Platform/Storage/IStorageFile.cs
  91. 11
      src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
  92. 53
      src/Avalonia.Base/Platform/Storage/IStorageItem.cs
  93. 56
      src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
  94. 17
      src/Avalonia.Base/Platform/Storage/PickerOptions.cs
  95. 43
      src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs
  96. 82
      src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs
  97. 75
      src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs
  98. 24
      src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs
  99. 53
      src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs
  100. 49
      src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs

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>

15
Avalonia.sln

@ -38,6 +38,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
EndProjectSection
@ -205,14 +206,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{7BF6C69D-FC14-43EB-9ED0-782C16F3D5D9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -485,10 +486,6 @@ Global
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}.Release|Any CPU.Build.0 = Release|Any CPU
{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.Build.0 = Release|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -501,6 +498,10 @@ Global
{EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EABE2161-989B-42BF-BD8D-1E34B20C21F1}.Release|Any CPU.Build.0 = Release|Any CPU
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -557,6 +558,8 @@ Global
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

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'

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

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

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

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

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

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

4
samples/BindingDemo/App.xaml

@ -3,7 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="BindingDemo.App">
<Application.Styles>
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
</Application.Styles>
</Application>
</Application>

2
samples/BindingDemo/BindingDemo.csproj

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
</ItemGroup>

5
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -9,7 +9,6 @@
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
<RuntimeIdentifiers>android-arm64;android-x64</RuntimeIdentifiers>
</PropertyGroup>
<ItemGroup>
<AndroidResource Include="..\..\build\Assets\Icon.png">
@ -21,12 +20,12 @@
<RunAOTCompilation>True</RunAOTCompilation>
</PropertyGroup>
<PropertyGroup Condition="'$(RunAOTCompilation)'=='True'">
<!-- PropertyGroup Condition="'$(RunAOTCompilation)'=='True'">
<EnableLLVM>True</EnableLLVM>
<AndroidAotAdditionalArguments>no-write-symbols,nodebug</AndroidAotAdditionalArguments>
<AndroidAotMode>Hybrid</AndroidAotMode>
<AndroidGenerateJniMarshalMethods>True</AndroidGenerateJniMarshalMethods>
</PropertyGroup>
</PropertyGroup -->
<PropertyGroup Condition="'$(AndroidEnableProfiler)'=='True'">
<IsEmulator Condition="'$(IsEmulator)' == ''">True</IsEmulator>

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

19
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,10 +113,11 @@ namespace ControlCatalog.NetCore
{
EnableMultiTouch = true,
UseDBusMenu = true,
EnableIme = true,
EnableIme = true
})
.With(new Win32PlatformOptions
{
EnableMultitouch = true
})
.UseSkia()
.AfterSetup(builder =>

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

9
samples/ControlCatalog/App.xaml

@ -5,6 +5,13 @@
x:CompileBindings="True"
Name="Avalonia ControlCatalog"
x:Class="ControlCatalog.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<Style Selector="TextBlock.h1, TextBlock.h2, TextBlock.h3">
<Setter Property="TextWrapping" Value="Wrap" />
@ -29,7 +36,6 @@
<Style Selector="Label.h3">
<Setter Property="FontSize" Value="12" />
</Style>
<StyleInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" />
</Application.Styles>
<TrayIcon.Icons>
<TrayIcons>
@ -43,6 +49,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>

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

5
samples/ControlCatalog/MainView.xaml.cs

@ -24,11 +24,6 @@ namespace ControlCatalog
{
IList tabItems = ((IList)sideBar.Items);
tabItems.Add(new TabItem()
{
Header = "Dialogs",
Content = new DialogsPage()
});
tabItems.Add(new TabItem()
{
Header = "Screens",
Content = new ScreenPage()

1
samples/ControlCatalog/Pages/ButtonsPage.xaml

@ -90,6 +90,7 @@
</Style>
</Button.Styles>
</Button>
<Button Classes="accent">Accent</Button>
</StackPanel>
<StackPanel Orientation="Vertical"

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

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>

234
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
@ -18,13 +26,16 @@ namespace ControlCatalog.Pages
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.Get<CheckBox>("UseFilters").IsChecked != true)
return null;
return new List<FileDialogFilter>();
return new List<FileDialogFilter>
{
new FileDialogFilter
@ -39,12 +50,23 @@ namespace ControlCatalog.Pages
};
}
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()
{
@ -62,7 +84,7 @@ namespace ControlCatalog.Pages
{
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;
@ -70,11 +92,13 @@ namespace ControlCatalog.Pages
};
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 };
@ -85,14 +109,9 @@ namespace ControlCatalog.Pages
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;
};
@ -101,7 +120,7 @@ namespace ControlCatalog.Pages
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
{
@ -116,20 +135,20 @@ namespace ControlCatalog.Pages
};
this.Get<Button>("DecoratedWindowDialog").Click += delegate
{
new DecoratedWindow().ShowDialog(GetWindow());
_ = new DecoratedWindow().ShowDialog(GetWindow());
};
this.Get<Button>("Dialog").Click += delegate
{
var window = CreateSampleWindow();
window.Height = 200;
window.ShowDialog(GetWindow());
_ = window.ShowDialog(GetWindow());
};
this.Get<Button>("DialogNoTaskbar").Click += delegate
{
var window = CreateSampleWindow();
window.Height = 200;
window.ShowInTaskbar = false;
window.ShowDialog(GetWindow());
_ = window.ShowDialog(GetWindow());
};
this.Get<Button>("OwnedWindow").Click += delegate
{
@ -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() => this.VisualRoot as Window ?? throw new NullReferenceException("Invalid Owner");
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

15
samples/ControlCatalog/Pages/ExpanderPage.xaml

@ -8,26 +8,31 @@
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<Expander Header="Expand Up" ExpandDirection="Up">
<Expander Header="Expand Up" ExpandDirection="Up"
CornerRadius="{Binding CornerRadius}">
<StackPanel>
<TextBlock>Expanded content</TextBlock>
</StackPanel>
</Expander>
<Expander Header="Expand Down" ExpandDirection="Down">
<Expander Header="Expand Down" ExpandDirection="Down"
CornerRadius="{Binding CornerRadius}">
<StackPanel>
<TextBlock>Expanded content</TextBlock>
</StackPanel>
</Expander>
<Expander Header="Expand Left" ExpandDirection="Left">
<Expander Header="Expand Left" ExpandDirection="Left"
CornerRadius="{Binding CornerRadius}">
<StackPanel>
<TextBlock>Expanded content</TextBlock>
</StackPanel>
</Expander>
<Expander Header="Expand Right" ExpandDirection="Right">
<Expander Header="Expand Right" ExpandDirection="Right"
CornerRadius="{Binding CornerRadius}">
<StackPanel>
<TextBlock>Expanded content</TextBlock>
</StackPanel>
</Expander>
</StackPanel>
<CheckBox IsChecked="{Binding Rounded}">Rounded</CheckBox>
</StackPanel>
</StackPanel>
</UserControl>

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

@ -1,5 +1,6 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
@ -8,6 +9,7 @@ namespace ControlCatalog.Pages
public ExpanderPage()
{
this.InitializeComponent();
DataContext = new ExpanderPageViewModel();
}
private void InitializeComponent()

6
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -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 />

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

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

44
samples/ControlCatalog/Pages/OpenGlPage.xaml.cs

@ -90,7 +90,6 @@ namespace ControlCatalog.Pages
private int _vertexBufferObject;
private int _indexBufferObject;
private int _vertexArrayObject;
private GlExtrasInterface _glExt;
private string GetShader(bool fragment, string shader)
{
@ -258,7 +257,6 @@ namespace ControlCatalog.Pages
protected unsafe override void OnOpenGlInit(GlInterface GL, int fb)
{
CheckError(GL);
_glExt = new GlExtrasInterface(GL);
Info = $"Renderer: {GL.GetString(GL_RENDERER)} Version: {GL.GetString(GL_VERSION)}";
@ -298,8 +296,8 @@ namespace ControlCatalog.Pages
GL.BufferData(GL_ELEMENT_ARRAY_BUFFER, new IntPtr(_indices.Length * sizeof(ushort)), new IntPtr(pdata),
GL_STATIC_DRAW);
CheckError(GL);
_vertexArrayObject = _glExt.GenVertexArray();
_glExt.BindVertexArray(_vertexArrayObject);
_vertexArrayObject = GL.GenVertexArray();
GL.BindVertexArray(_vertexArrayObject);
CheckError(GL);
GL.VertexAttribPointer(positionLocation, 3, GL_FLOAT,
0, vertexSize, IntPtr.Zero);
@ -316,12 +314,13 @@ namespace ControlCatalog.Pages
// Unbind everything
GL.BindBuffer(GL_ARRAY_BUFFER, 0);
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
_glExt.BindVertexArray(0);
GL.BindVertexArray(0);
GL.UseProgram(0);
// Delete all resources.
GL.DeleteBuffers(2, new[] { _vertexBufferObject, _indexBufferObject });
_glExt.DeleteVertexArrays(1, new[] { _vertexArrayObject });
GL.DeleteBuffer(_vertexBufferObject);
GL.DeleteBuffer(_indexBufferObject);
GL.DeleteVertexArray(_vertexArrayObject);
GL.DeleteProgram(_shaderProgram);
GL.DeleteShader(_fragmentShader);
GL.DeleteShader(_vertexShader);
@ -338,7 +337,7 @@ namespace ControlCatalog.Pages
GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject);
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject);
_glExt.BindVertexArray(_vertexArrayObject);
GL.BindVertexArray(_vertexArrayObject);
GL.UseProgram(_shaderProgram);
CheckError(GL);
var projection =
@ -369,34 +368,5 @@ namespace ControlCatalog.Pages
if (_disco > 0.01)
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
}
class GlExtrasInterface : GlInterfaceBase<GlInterface.GlContextInfo>
{
public GlExtrasInterface(GlInterface gl) : base(gl.GetProcAddress, gl.ContextInfo)
{
}
public delegate void GlDeleteVertexArrays(int count, int[] buffers);
[GlMinVersionEntryPoint("glDeleteVertexArrays", 3,0)]
[GlExtensionEntryPoint("glDeleteVertexArraysOES", "GL_OES_vertex_array_object")]
public GlDeleteVertexArrays DeleteVertexArrays { get; }
public delegate void GlBindVertexArray(int array);
[GlMinVersionEntryPoint("glBindVertexArray", 3,0)]
[GlExtensionEntryPoint("glBindVertexArrayOES", "GL_OES_vertex_array_object")]
public GlBindVertexArray BindVertexArray { get; }
public delegate void GlGenVertexArrays(int n, int[] rv);
[GlMinVersionEntryPoint("glGenVertexArrays",3,0)]
[GlExtensionEntryPoint("glGenVertexArraysOES", "GL_OES_vertex_array_object")]
public GlGenVertexArrays GenVertexArrays { get; }
public int GenVertexArray()
{
var rv = new int[1];
GenVertexArrays(1, rv);
return rv[0];
}
}
}
}

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" />

4
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -118,7 +118,7 @@
</StackPanel>
</Border>
<Border>
<TextBlock Margin="10" TextWrapping="Wrap">
<RichTextBlock SelectionBrush="LightBlue" IsTextSelectionEnabled="True" 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>

27
samples/ControlCatalog/ViewModels/ExpanderPageViewModel.cs

@ -0,0 +1,27 @@
using Avalonia;
using MiniMvvm;
namespace ControlCatalog.ViewModels
{
public class ExpanderPageViewModel : ViewModelBase
{
private object _cornerRadius = AvaloniaProperty.UnsetValue;
private bool _rounded;
public object CornerRadius
{
get => _cornerRadius;
private set => RaiseAndSetIfChanged(ref _cornerRadius, value);
}
public bool Rounded
{
get => _rounded;
set
{
if (RaiseAndSetIfChanged(ref _rounded, value))
CornerRadius = _rounded ? new CornerRadius(25) : AvaloniaProperty.UnsetValue;
}
}
}
}

8
samples/RenderDemo/App.xaml

@ -3,6 +3,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" />
</Application.Styles>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

4
samples/RenderDemo/App.xaml.cs

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

286
samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml

@ -1,6 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:catalog="using:ControlSamples">
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:catalog="using:ControlSamples">
<Design.PreviewWith>
<Border Width="400"
Height="150">
@ -20,25 +20,136 @@
</Border>
</Design.PreviewWith>
<Styles.Resources>
<x:Double x:Key="PaneCompactWidth">40</x:Double>
<x:Double x:Key="PaneExpandWidth">220</x:Double>
<x:Double x:Key="HeaderHeight">36</x:Double>
<x:Double x:Key="NavigationItemHeight">36</x:Double>
<x:Double x:Key="HamburgerMenuButtonHeight">32</x:Double>
<Thickness x:Key="HeaderMarginCollapsedPane">12,0,0,0</Thickness>
<Thickness x:Key="HeaderMarginExpandedPane">52,0,0,0</Thickness>
<Thickness x:Key="HeaderMarginExpandedOverlayPane">212,0,0,0</Thickness>
<BoxShadows x:Key="NavigationItemShadow">1 1 1 1 #2000, 0 0 1 1 #2fff</BoxShadows>
<BoxShadows x:Key="NavigationContentShadow">0 0 1 1 #2000</BoxShadows>
</Styles.Resources>
<x:Double x:Key="PaneCompactWidth">40</x:Double>
<x:Double x:Key="PaneExpandWidth">220</x:Double>
<x:Double x:Key="HeaderHeight">36</x:Double>
<x:Double x:Key="NavigationItemHeight">36</x:Double>
<x:Double x:Key="HamburgerMenuButtonHeight">32</x:Double>
<Thickness x:Key="HeaderMarginCollapsedPane">12,0,0,0</Thickness>
<Thickness x:Key="HeaderMarginExpandedPane">52,0,0,0</Thickness>
<Thickness x:Key="HeaderMarginExpandedOverlayPane">212,0,0,0</Thickness>
<BoxShadows x:Key="NavigationItemShadow">1 1 1 1 #2000, 0 0 1 1 #2fff</BoxShadows>
<BoxShadows x:Key="NavigationContentShadow">0 0 1 1 #2000</BoxShadows>
<ControlTheme x:Key="NavigationButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="Height" Value="{StaticResource NavigationItemHeight}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Padding" Value="12,0,4,0" />
<Setter Property="Margin" Value="4,0,8,0" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
CornerRadius="{TemplateBinding CornerRadius}"
TextElement.FontFamily="{TemplateBinding FontFamily}"
TextElement.FontSize="{TemplateBinding FontSize}"
TextElement.FontWeight="{TemplateBinding FontWeight}" />
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ ContentPresenter">
<Setter Property="Border.Background" Value="{DynamicResource SystemChromeLowColor}" />
<Setter Property="Border.BoxShadow" Value="{StaticResource NavigationItemShadow}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundUnselectedPointerOver}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="HamburgerMenuTabItem" TargetType="TabItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="Height" Value="{StaticResource NavigationItemHeight}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Padding" Value="12,0,4,0" />
<Setter Property="Margin" Value="4,0,8,0" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="ClipToBounds" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="PART_LayoutRoot"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Panel>
<Border Name="PART_SelectedPipe"
Width="{DynamicResource TabItemPipeThickness}"
Height="{DynamicResource TabItemVerticalPipeHeight}"
Margin="6,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Background="{DynamicResource TabItemHeaderSelectedPipeFill}"
IsVisible="False"
CornerRadius="{DynamicResource ControlCornerRadius}"/>
<ContentPresenter Name="PART_ContentPresenter"
Padding="{TemplateBinding Padding}"
Margin="0"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
TextElement.FontFamily="{TemplateBinding FontFamily}"
TextElement.FontSize="{TemplateBinding FontSize}"
TextElement.FontWeight="{TemplateBinding FontWeight}" />
</Panel>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover">
<Setter Property="Background" Value="{DynamicResource ThemeControlHighlightMidBrush}"/>
<Style Selector="^ /template/ Border#PART_LayoutRoot">
<Setter Property="Background" Value="{DynamicResource SystemChromeLowColor}" />
<Setter Property="BoxShadow" Value="{StaticResource NavigationItemShadow}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundUnselectedPointerOver}" />
</Style>
</Style>
<Style Selector="^:selected">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Style Selector="^ /template/ Border#PART_SelectedPipe">
<Setter Property="IsVisible" Value="True" />
</Style>
</Style>
<Style Selector="^:pressed /template/ Border#PART_LayoutRoot">
<Setter Property="Border.Background" Value="{DynamicResource SystemChromeLowColor}" />
<Setter Property="Border.BoxShadow" Value="{StaticResource NavigationItemShadow}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundUnselectedPressed}" />
</Style>
</ControlTheme>
<!-- HamburgerMenu -->
<Style Selector="catalog|HamburgerMenu">
<ControlTheme x:Key="{x:Type catalog:HamburgerMenu}" TargetType="catalog:HamburgerMenu">
<Setter Property="Padding" Value="12 8 4 0" />
<Setter Property="PaneBackground" Value="{DynamicResource SystemChromeMediumColor}" />
<Setter Property="Background" Value="{DynamicResource SystemChromeMediumColor}" />
<Setter Property="ContentBackground" Value="{DynamicResource SystemAltHighColor}" />
<Setter Property="ItemContainerTheme" Value="{StaticResource HamburgerMenuTabItem}"/>
<Setter Property="TabStripPlacement" Value="Left" />
<Setter Property="Template">
<ControlTemplate>
<Panel Background="{TemplateBinding PaneBackground}">
@ -46,7 +157,8 @@
CompactPaneLength="{StaticResource PaneCompactWidth}"
DisplayMode="Inline"
IsPaneOpen="True"
OpenPaneLength="{StaticResource PaneExpandWidth}">
OpenPaneLength="{StaticResource PaneExpandWidth}"
PaneBackground="Transparent">
<SplitView.Pane>
<Grid Margin="0,0,1,5" RowDefinitions="Auto, *, Auto">
<Panel Height="{StaticResource HeaderHeight}" />
@ -58,8 +170,7 @@
<ItemsPresenter Name="PART_ItemsPresenter"
HorizontalAlignment="Stretch"
ItemTemplate="{TemplateBinding ItemTemplate}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}">
Items="{TemplateBinding Items}">
<ItemsPresenter.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel x:Name="HamburgerItemsPanel"
@ -70,7 +181,7 @@
</ScrollViewer>
<Button x:Name="SettingsButton"
Grid.Row="2"
Classes="NavigationButton"
Theme="{StaticResource NavigationButton}"
Content="Settings"
Flyout="{TemplateBinding (FlyoutBase.AttachedFlyout)}"
IsVisible="{Binding $parent[TabControl].(FlyoutBase.AttachedFlyout), Converter={x:Static ObjectConverters.IsNotNull}}" />
@ -82,6 +193,7 @@
<TextBlock x:Name="HeaderHolder"
VerticalAlignment="Center"
Classes="h1"
Margin="{StaticResource HeaderMarginExpandedPane}"
Text="{Binding $parent[TabControl].SelectedItem.Header, FallbackValue=''}">
<TextBlock.Transitions>
<Transitions>
@ -119,7 +231,7 @@
HorizontalAlignment="Left"
VerticalAlignment="Top"
HorizontalContentAlignment="Center"
Classes="NavigationButton"
Theme="{StaticResource NavigationButton}"
CornerRadius="4"
IsChecked="{Binding #PART_NavigationPane.IsPaneOpen, Mode=TwoWay}">
<PathIcon Data="M3 17h18a1 1 0 0 1 .117 1.993L21 19H3a1 1 0 0 1-.117-1.993L3 17h18H3Zm0-6 18-.002a1 1 0 0 1 .117 1.993l-.117.007L3 13a1 1 0 0 1-.117-1.993L3 11l18-.002L3 11Zm0-6h18a1 1 0 0 1 .117 1.993L21 7H3a1 1 0 0 1-.117-1.993L3 5h18H3Z" Foreground="{TemplateBinding Foreground}" />
@ -127,116 +239,26 @@
</Panel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="catalog|HamburgerMenu /template/ SplitView TextBlock#HeaderHolder">
<Setter Property="Margin" Value="{StaticResource HeaderMarginExpandedPane}" />
</Style>
<Style Selector="catalog|HamburgerMenu /template/ SplitView[IsPaneOpen=True] TextBlock#HeaderHolder">
<Setter Property="Margin" Value="{StaticResource HeaderMarginCollapsedPane}" />
</Style>
<Style Selector="catalog|HamburgerMenu /template/ SplitView[DisplayMode=Overlay][IsPaneOpen=True] TextBlock#HeaderHolder">
<Setter Property="Margin" Value="{StaticResource HeaderMarginExpandedOverlayPane}" />
</Style>
<Style Selector="catalog|HamburgerMenu /template/ SplitView">
<Setter Property="PaneBackground" Value="Transparent" />
</Style>
<Style Selector="catalog|HamburgerMenu /template/ SplitView[DisplayMode=Overlay]">
<Setter Property="PaneBackground" Value="{TemplateBinding PaneBackground}" />
</Style>
<Style Selector="catalog|HamburgerMenu /template/ SplitView[DisplayMode=Overlay]">
<Setter Property="Background" Value="{Binding $parent[TabControl].ContentBackground}" />
</Style>
<Style Selector="catalog|HamburgerMenu /template/ SplitView[DisplayMode=Inline] Border#BackgroundBorder">
<Setter Property="Background" Value="{Binding $parent[TabControl].ContentBackground}" />
<Setter Property="BoxShadow" Value="{StaticResource NavigationContentShadow}" />
</Style>
<Style Selector="catalog|HamburgerMenu /template/ SplitView[DisplayMode=Inline][IsPaneOpen=True] Border#BackgroundBorder">
<Setter Property="CornerRadius" Value="8 0 0 0" />
</Style>
<Style Selector="^ /template/ SplitView[IsPaneOpen=True] TextBlock#HeaderHolder">
<Setter Property="Margin" Value="{StaticResource HeaderMarginCollapsedPane}" />
</Style>
<Style Selector="^ /template/ SplitView[DisplayMode=Overlay][IsPaneOpen=True] TextBlock#HeaderHolder">
<Setter Property="Margin" Value="{StaticResource HeaderMarginExpandedOverlayPane}" />
</Style>
<Style Selector="^ /template/ SplitView[DisplayMode=Overlay]">
<Setter Property="PaneBackground" Value="{TemplateBinding PaneBackground}" />
</Style>
<Style Selector="^ /template/ SplitView[DisplayMode=Overlay]">
<Setter Property="Background" Value="{Binding $parent[TabControl].ContentBackground}" />
</Style>
<Style Selector="^ /template/ SplitView[DisplayMode=Inline] Border#BackgroundBorder">
<Setter Property="Background" Value="{Binding $parent[TabControl].ContentBackground}" />
<Setter Property="BoxShadow" Value="{StaticResource NavigationContentShadow}" />
</Style>
<Style Selector="^ /template/ SplitView[DisplayMode=Inline][IsPaneOpen=True] Border#BackgroundBorder">
<Setter Property="CornerRadius" Value="8 0 0 0" />
</Style>
</ControlTheme>
<!-- HamburgerMenu TabItem -->
<Style Selector="catalog|HamburgerMenu > TabItem, :is(Button).NavigationButton">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="Height" Value="{StaticResource NavigationItemHeight}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Padding" Value="12,0,4,0" />
<Setter Property="Margin" Value="4,0,8,0" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="ClipToBounds" Value="False" />
</Style>
<Style Selector="catalog|HamburgerMenu > TabItem">
<Setter Property="Template">
<ControlTemplate>
<Border Name="PART_LayoutRoot"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Panel>
<Border Name="PART_SelectedPipe"
Width="{DynamicResource TabItemPipeThickness}"
Height="{DynamicResource TabItemVerticalPipeHeight}"
Margin="6,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Background="{DynamicResource TabItemHeaderSelectedPipeFill}" />
<ContentPresenter Name="PART_ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
TextElement.FontFamily="{TemplateBinding FontFamily}"
TextElement.FontSize="{TemplateBinding FontSize}"
TextElement.FontWeight="{TemplateBinding FontWeight}" />
</Panel>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector=":is(Button).NavigationButton">
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
CornerRadius="{TemplateBinding CornerRadius}"
TextElement.FontFamily="{TemplateBinding FontFamily}"
TextElement.FontSize="{TemplateBinding FontSize}"
TextElement.FontWeight="{TemplateBinding FontWeight}" />
</ControlTemplate>
</Setter>
</Style>
<Style Selector="catalog|HamburgerMenu > TabItem /template/ Border#PART_LayoutRoot, :is(Button).NavigationButton /template/ ContentPresenter">
<Setter Property="Border.Background" Value="Transparent" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource SystemBaseHighColor}" />
</Style>
<Style Selector="catalog|HamburgerMenu > TabItem:pointerover /template/ Border#PART_LayoutRoot, :is(Button).NavigationButton:pointerover /template/ ContentPresenter">
<Setter Property="Border.Background" Value="{DynamicResource SystemChromeLowColor}" />
<Setter Property="Border.BoxShadow" Value="{StaticResource NavigationItemShadow}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundUnselectedPointerOver}" />
</Style>
<Style Selector="catalog|HamburgerMenu > TabItem:pressed /template/ Border#PART_LayoutRoot, :is(Button).NavigationButton:pressed /template/ ContentPresenter">
<Setter Property="Border.Background" Value="{DynamicResource SystemChromeLowColor}" />
<Setter Property="Border.BoxShadow" Value="{StaticResource NavigationItemShadow}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundUnselectedPressed}" />
</Style>
<Style Selector=":is(Button).NavigationButton:pressed">
<Setter Property="RenderTransform" Value="none" />
</Style>
</Styles>
</ResourceDictionary>

3
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,7 +53,6 @@ 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())

32
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,16 +24,15 @@ 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();
var lifetime = new SingleViewLifetime();
@ -79,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);
}
}

12
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" />
@ -32,6 +35,11 @@
<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>

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;

56
src/Avalonia.Base/Controls/Classes.cs

@ -1,8 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
#nullable enable
using Avalonia.Utilities;
namespace Avalonia.Controls
{
@ -14,6 +13,8 @@ namespace Avalonia.Controls
/// </remarks>
public class Classes : AvaloniaList<string>, IPseudoClasses
{
private SafeEnumerableList<IClassesChangedListener>? _listeners;
/// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class.
/// </summary>
@ -39,6 +40,11 @@ namespace Avalonia.Controls
{
}
/// <summary>
/// Gets the number of listeners subscribed to this collection for unit testing purposes.
/// </summary>
internal int ListenerCount => _listeners?.Count ?? 0;
/// <summary>
/// Parses a classes string.
/// </summary>
@ -62,6 +68,7 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Add(name);
NotifyChanged();
}
}
@ -89,6 +96,7 @@ namespace Avalonia.Controls
}
base.AddRange(c);
NotifyChanged();
}
/// <summary>
@ -103,6 +111,8 @@ namespace Avalonia.Controls
RemoveAt(i);
}
}
NotifyChanged();
}
/// <summary>
@ -122,6 +132,7 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Insert(index, name);
NotifyChanged();
}
}
@ -154,6 +165,7 @@ namespace Avalonia.Controls
if (toInsert != null)
{
base.InsertRange(index, toInsert);
NotifyChanged();
}
}
@ -169,7 +181,14 @@ namespace Avalonia.Controls
public override bool Remove(string name)
{
ThrowIfPseudoclass(name, "removed");
return base.Remove(name);
if (base.Remove(name))
{
NotifyChanged();
return true;
}
return false;
}
/// <summary>
@ -197,6 +216,7 @@ namespace Avalonia.Controls
if (toRemove != null)
{
base.RemoveAll(toRemove);
NotifyChanged();
}
}
@ -214,6 +234,7 @@ namespace Avalonia.Controls
var name = this[index];
ThrowIfPseudoclass(name, "removed");
base.RemoveAt(index);
NotifyChanged();
}
/// <summary>
@ -224,6 +245,7 @@ namespace Avalonia.Controls
public override void RemoveRange(int index, int count)
{
base.RemoveRange(index, count);
NotifyChanged();
}
/// <summary>
@ -255,6 +277,7 @@ namespace Avalonia.Controls
}
base.AddRange(source);
NotifyChanged();
}
/// <inheritdoc/>
@ -263,13 +286,38 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Add(name);
NotifyChanged();
}
}
/// <inheritdoc/>
bool IPseudoClasses.Remove(string name)
{
return base.Remove(name);
if (base.Remove(name))
{
NotifyChanged();
return true;
}
return false;
}
internal void AddListener(IClassesChangedListener listener)
{
(_listeners ??= new()).Add(listener);
}
internal void RemoveListener(IClassesChangedListener listener)
{
_listeners?.Remove(listener);
}
private void NotifyChanged()
{
if (_listeners is null)
return;
foreach (var listener in _listeners)
listener.Changed();
}
private void ThrowIfPseudoclass(string name, string operation)

14
src/Avalonia.Base/Controls/IClassesChangedListener.cs

@ -0,0 +1,14 @@
namespace Avalonia.Controls
{
/// <summary>
/// Internal interface for listening to changes in <see cref="Classes"/> in a more
/// performant manner than subscribing to CollectionChanged.
/// </summary>
internal interface IClassesChangedListener
{
/// <summary>
/// Notifies the listener that the <see cref="Classes"/> collection has changed.
/// </summary>
void Changed();
}
}

7
src/Avalonia.Base/Controls/IPseudoClasses.cs

@ -19,5 +19,12 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="name">The pseudoclass name.</param>
bool Remove(string name);
/// <summary>
/// Returns whether a pseudoclass is present in the collection.
/// </summary>
/// <param name="name">The pseudoclass name.</param>
/// <returns>Whether the pseudoclass is present.</returns>
bool Contains(string name);
}
}

2
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -55,7 +55,7 @@ namespace Avalonia.Data.Core.Plugins
private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName)
{
if (instance is IReflectableType reflectableType)
if (instance is IReflectableType reflectableType && instance is not Type)
return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags);
var type = instance.GetType();

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

@ -32,10 +32,7 @@ namespace Avalonia.Input
DragCopy,
DragLink,
None,
[Obsolete("Use BottomSide")]
BottomSize = BottomSide
// Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/
// We might enable them later, preferably, by loading pixmax directly from theme with fallback image
// SizeNorthWestSouthEast,

18
src/Avalonia.Base/Input/DragEventArgs.cs

@ -13,9 +13,6 @@ namespace Avalonia.Input
public IDataObject Data { get; private set; }
[Obsolete("Use KeyModifiers")]
public InputModifiers Modifiers { get; private set; }
public KeyModifiers KeyModifiers { get; private set; }
public Point GetPosition(IVisual relativeTo)
@ -35,17 +32,6 @@ namespace Avalonia.Input
return point;
}
[Obsolete("Use constructor taking KeyModifiers")]
public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, InputModifiers modifiers)
: base(routedEvent)
{
Data = data;
_target = target;
_targetLocation = targetLocation;
Modifiers = modifiers;
KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xF);
}
public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
: base(routedEvent)
{
@ -53,10 +39,6 @@ namespace Avalonia.Input
_target = target;
_targetLocation = targetLocation;
KeyModifiers = keyModifiers;
#pragma warning disable CS0618 // Type or member is obsolete
Modifiers = (InputModifiers)keyModifiers;
#pragma warning restore CS0618 // Type or member is obsolete
}
}
}

11
src/Avalonia.Base/Input/GotFocusEventArgs.cs

@ -1,4 +1,3 @@
using System;
using Avalonia.Interactivity;
namespace Avalonia.Input
@ -13,16 +12,6 @@ namespace Avalonia.Input
/// </summary>
public NavigationMethod NavigationMethod { get; set; }
/// <summary>
/// Gets or sets any input modifiers active at the time of focus.
/// </summary>
[Obsolete("Use KeyModifiers")]
public InputModifiers InputModifiers
{
get => (InputModifiers)KeyModifiers;
set => KeyModifiers = (KeyModifiers)((int)value & 0xF);
}
/// <summary>
/// Gets or sets any key modifiers active at the time of focus.
/// </summary>

5
src/Avalonia.Base/Input/IInputRoot.cs

@ -27,10 +27,5 @@ namespace Avalonia.Input
/// Gets or sets a value indicating whether access keys are shown in the window.
/// </summary>
bool ShowAccessKeys { get; set; }
/// <summary>
/// Gets associated mouse device
/// </summary>
IMouseDevice? MouseDevice { get; }
}
}

13
src/Avalonia.Base/Input/IKeyboardDevice.cs

@ -4,19 +4,6 @@ using Avalonia.Metadata;
namespace Avalonia.Input
{
[Flags, Obsolete("Use KeyModifiers and PointerPointProperties")]
public enum InputModifiers
{
None = 0,
Alt = 1,
Control = 2,
Shift = 4,
Windows = 8,
LeftMouseButton = 16,
RightMouseButton = 32,
MiddleMouseButton = 64
}
[Flags]
public enum KeyModifiers
{

12
src/Avalonia.Base/Input/IMouseDevice.cs

@ -1,4 +1,3 @@
using System;
using Avalonia.Metadata;
namespace Avalonia.Input
@ -9,16 +8,5 @@ namespace Avalonia.Input
[NotClientImplementable]
public interface IMouseDevice : IPointerDevice
{
/// <summary>
/// Gets the mouse position, in screen coordinates.
/// </summary>
[Obsolete("Use PointerEventArgs.GetPosition")]
PixelPoint Position { get; }
[Obsolete]
void TopLevelClosed(IInputRoot root);
[Obsolete]
void SceneInvalidated(IInputRoot root, Rect rect);
}
}

14
src/Avalonia.Base/Input/IPointerDevice.cs

@ -1,5 +1,3 @@
using System;
using Avalonia.VisualTree;
using Avalonia.Input.Raw;
using Avalonia.Metadata;
@ -8,18 +6,6 @@ namespace Avalonia.Input
[NotClientImplementable]
public interface IPointerDevice : IInputDevice
{
/// <inheritdoc cref="IPointer.Captured" />
[Obsolete("Use IPointer")]
IInputElement? Captured { get; }
/// <inheritdoc cref="IPointer.Capture(IInputElement?)" />
[Obsolete("Use IPointer")]
void Capture(IInputElement? control);
/// <inheritdoc cref="PointerEventArgs.GetPosition(IVisual?)" />
[Obsolete("Use PointerEventArgs.GetPosition")]
Point GetPosition(IVisual relativeTo);
/// <summary>
/// Gets a pointer for specific event args.
/// </summary>

2
src/Avalonia.Base/Input/KeyEventArgs.cs

@ -9,8 +9,6 @@ namespace Avalonia.Input
public Key Key { get; set; }
[Obsolete("Use KeyModifiers")]
public InputModifiers Modifiers => (InputModifiers)KeyModifiers;
public KeyModifiers KeyModifiers { get; set; }
}
}

12
src/Avalonia.Base/Input/KeyGesture.cs

@ -15,13 +15,6 @@ namespace Avalonia.Input
{ "+", Key.OemPlus }, { "-", Key.OemMinus }, { ".", Key.OemPeriod }, { ",", Key.OemComma }
};
[Obsolete("Use constructor taking KeyModifiers")]
public KeyGesture(Key key, InputModifiers modifiers)
{
Key = key;
KeyModifiers = (KeyModifiers)(((int)modifiers) & 0xf);
}
public KeyGesture(Key key, KeyModifiers modifiers = KeyModifiers.None)
{
Key = key;
@ -63,10 +56,7 @@ namespace Avalonia.Input
}
public Key Key { get; }
[Obsolete("Use KeyModifiers")]
public InputModifiers Modifiers => (InputModifiers)KeyModifiers;
public KeyModifiers KeyModifiers { get; }
public static KeyGesture Parse(string gesture)

54
src/Avalonia.Base/Input/MouseDevice.cs

@ -21,7 +21,6 @@ namespace Avalonia.Input
private readonly Pointer _pointer;
private bool _disposed;
private PixelPoint? _position;
private MouseButton _lastMouseDownButton;
public MouseDevice(Pointer? pointer = null)
@ -29,43 +28,6 @@ namespace Avalonia.Input
_pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
}
[Obsolete("Use IPointer instead")]
public IInputElement? Captured => _pointer.Captured;
[Obsolete("Use events instead")]
public PixelPoint Position
{
get => _position ?? new PixelPoint(-1, -1);
protected set => _position = value;
}
[Obsolete("Use IPointer instead")]
public void Capture(IInputElement? control)
{
_pointer.Capture(control);
}
/// <summary>
/// Gets the mouse position relative to a control.
/// </summary>
/// <param name="relativeTo">The control.</param>
/// <returns>The mouse position in the control's coordinates.</returns>
public Point GetPosition(IVisual relativeTo)
{
relativeTo = relativeTo ?? throw new ArgumentNullException(nameof(relativeTo));
if (relativeTo.VisualRoot == null)
{
throw new InvalidOperationException("Control is not attached to visual tree.");
}
#pragma warning disable CS0618 // Type or member is obsolete
var rootPoint = relativeTo.VisualRoot.PointToClient(Position);
#pragma warning restore CS0618 // Type or member is obsolete
var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo);
return rootPoint * transform!.Value;
}
public void ProcessRawEvent(RawInputEventArgs e)
{
if (!e.Handled && e is RawPointerEventArgs margs)
@ -96,7 +58,6 @@ namespace Avalonia.Input
if(mouse._disposed)
return;
_position = e.Root.PointToScreen(e.Position);
var props = CreateProperties(e);
var keyModifiers = e.InputModifiers.ToKeyModifiers();
switch (e.Type)
@ -145,7 +106,6 @@ namespace Avalonia.Input
private void LeaveWindow()
{
_position = null;
}
PointerPointProperties CreateProperties(RawPointerEventArgs args)
@ -324,19 +284,7 @@ namespace Avalonia.Input
_disposed = true;
_pointer?.Dispose();
}
[Obsolete]
public void TopLevelClosed(IInputRoot root)
{
// no-op
}
[Obsolete]
public void SceneInvalidated(IInputRoot root, Rect rect)
{
// no-op
}
public IPointer? TryGetPointer(RawPointerEventArgs ev)
{
return _pointer;

19
src/Avalonia.Base/Input/PenDevice.cs

@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
@ -14,7 +12,6 @@ namespace Avalonia.Input
public class PenDevice : IPenDevice, IDisposable
{
private readonly Dictionary<long, Pointer> _pointers = new();
private readonly Dictionary<long, PixelPoint> _lastPositions = new();
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
@ -41,9 +38,7 @@ namespace Avalonia.Input
_pointers[e.RawPointerId] = pointer = new Pointer(Pointer.GetNextFreeId(),
PointerType.Pen, _pointers.Count == 0);
}
_lastPositions[e.RawPointerId] = e.Root.PointToScreen(e.Position);
var props = new PointerPointProperties(e.InputModifiers, e.Type.ToUpdateKind(),
e.Point.Twist, e.Point.Pressure, e.Point.XTilt, e.Point.YTilt);
var keyModifiers = e.InputModifiers.ToKeyModifiers();
@ -69,7 +64,6 @@ namespace Avalonia.Input
{
pointer.Dispose();
_pointers.Remove(e.RawPointerId);
_lastPositions.Remove(e.RawPointerId);
}
}
@ -153,17 +147,6 @@ namespace Avalonia.Input
p.Dispose();
}
[Obsolete]
IInputElement? IPointerDevice.Captured => _pointers.Values
.FirstOrDefault(p => p.IsPrimary)?.Captured;
[Obsolete]
void IPointerDevice.Capture(IInputElement? control) => _pointers.Values
.FirstOrDefault(p => p.IsPrimary)?.Capture(control);
[Obsolete]
Point IPointerDevice.GetPosition(IVisual relativeTo) => new Point(-1, -1);
public IPointer? TryGetPointer(RawPointerEventArgs ev)
{
return _pointers.TryGetValue(ev.RawPointerId, out var pointer)

62
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -11,7 +11,7 @@ namespace Avalonia.Input
private readonly IVisual? _rootVisual;
private readonly Point _rootVisualPosition;
private readonly PointerPointProperties _properties;
private Lazy<IReadOnlyList<RawPointerPoint>?>? _previousPoints;
private readonly Lazy<IReadOnlyList<RawPointerPoint>?>? _previousPoints;
public PointerEventArgs(RoutedEvent routedEvent,
IInteractive? source,
@ -43,29 +43,6 @@ namespace Avalonia.Input
{
_previousPoints = previousPoints;
}
class EmulatedDevice : IPointerDevice
{
private readonly PointerEventArgs _ev;
public EmulatedDevice(PointerEventArgs ev)
{
_ev = ev;
}
public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException();
public IInputElement? Captured => _ev.Pointer.Captured;
public void Capture(IInputElement? control)
{
_ev.Pointer.Capture(control);
}
public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo);
public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer;
}
/// <summary>
/// Gets specific pointer generated by input device.
@ -77,28 +54,6 @@ namespace Avalonia.Input
/// </summary>
public ulong Timestamp { get; }
private IPointerDevice? _device;
[Obsolete("Use Pointer to get pointer-specific information")]
public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this));
[Obsolete("Use KeyModifiers and PointerPointProperties")]
public InputModifiers InputModifiers
{
get
{
var mods = (InputModifiers)KeyModifiers;
if (_properties.IsLeftButtonPressed)
mods |= InputModifiers.LeftMouseButton;
if (_properties.IsMiddleButtonPressed)
mods |= InputModifiers.MiddleMouseButton;
if (_properties.IsRightButtonPressed)
mods |= InputModifiers.RightMouseButton;
return mods;
}
}
/// <summary>
/// Gets a value that indicates which key modifiers were active at the time that the pointer event was initiated.
/// </summary>
@ -120,9 +75,6 @@ namespace Avalonia.Input
/// <returns>The pointer position in the control's coordinates.</returns>
public Point GetPosition(IVisual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo);
[Obsolete("Use GetCurrentPoint")]
public PointerPoint GetPointerPoint(IVisual? relativeTo) => GetCurrentPoint(relativeTo);
/// <summary>
/// Returns the PointerPoint associated with the current event
/// </summary>
@ -171,8 +123,6 @@ namespace Avalonia.Input
public class PointerPressedEventArgs : PointerEventArgs
{
private readonly int _clickCount;
public PointerPressedEventArgs(
IInteractive source,
IPointer pointer,
@ -184,13 +134,10 @@ namespace Avalonia.Input
: base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition,
timestamp, properties, modifiers)
{
_clickCount = clickCount;
ClickCount = clickCount;
}
public int ClickCount => _clickCount;
[Obsolete("Use PointerPressedEventArgs.GetCurrentPoint(this).Properties")]
public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton();
public int ClickCount { get; }
}
public class PointerReleasedEventArgs : PointerEventArgs
@ -210,9 +157,6 @@ namespace Avalonia.Input
/// Gets the mouse button that triggered the corresponding PointerPressed event
/// </summary>
public MouseButton InitialPressMouseButton { get; }
[Obsolete("Use InitialPressMouseButton")]
public MouseButton MouseButton => InitialPressMouseButton;
}
public class PointerCaptureLostEventArgs : RoutedEventArgs

2
src/Avalonia.Base/Input/PointerOverPreProcessor.cs

@ -15,6 +15,8 @@ namespace Avalonia.Input
_inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot));
}
public PixelPoint? LastPosition => _lastPointer?.position;
public void OnCompleted()
{
ClearPointerOver();

5
src/Avalonia.Base/Input/Raw/RawDragEvent.cs

@ -8,8 +8,6 @@ namespace Avalonia.Input.Raw
public IDataObject Data { get; }
public DragDropEffects Effects { get; set; }
public RawDragEventType Type { get; }
[Obsolete("Use KeyModifiers")]
public InputModifiers Modifiers { get; }
public KeyModifiers KeyModifiers { get; }
public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type,
@ -21,9 +19,6 @@ namespace Avalonia.Input.Raw
Data = data;
Effects = effects;
KeyModifiers = modifiers.ToKeyModifiers();
#pragma warning disable CS0618 // Type or member is obsolete
Modifiers = (InputModifiers)modifiers;
#pragma warning restore CS0618 // Type or member is obsolete
}
}
}

3
src/Avalonia.Base/Input/Raw/RawTouchEventArgs.cs

@ -19,8 +19,5 @@ namespace Avalonia.Input.Raw
{
RawPointerId = rawPointerId;
}
[Obsolete("Use RawPointerId")]
public long TouchPointId { get => RawPointerId; set => RawPointerId = value; }
}
}

11
src/Avalonia.Base/Input/TouchDevice.cs

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
@ -20,9 +19,6 @@ namespace Avalonia.Input
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
private Pointer? _lastPointer;
IInputElement? IPointerDevice.Captured => _lastPointer?.Captured;
RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown)
{
@ -32,10 +28,6 @@ namespace Avalonia.Input
return rv;
}
void IPointerDevice.Capture(IInputElement? control) => _lastPointer?.Capture(control);
Point IPointerDevice.GetPosition(IVisual relativeTo) => default;
public void ProcessRawEvent(RawInputEventArgs ev)
{
if (ev.Handled || _disposed)
@ -51,7 +43,6 @@ namespace Avalonia.Input
PointerType.Touch, _pointers.Count == 0);
pointer.Capture(hit);
}
_lastPointer = pointer;
var target = pointer.Captured ?? args.Root;
var updateKind = args.Type.ToUpdateKind();
@ -96,7 +87,6 @@ namespace Avalonia.Input
new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind),
keyModifier, MouseButton.Left));
}
_lastPointer = null;
}
if (args.Type == RawPointerEventType.TouchCancel)
@ -104,7 +94,6 @@ namespace Avalonia.Input
_pointers.Remove(args.RawPointerId);
using (pointer)
pointer.Capture(null);
_lastPointer = null;
}
if (args.Type == RawPointerEventType.TouchUpdate)

8
src/Avalonia.Base/Layout/LayoutManager.cs

@ -350,7 +350,7 @@ namespace Avalonia.Layout
{
for (var i = 0; i < count; ++i)
{
var l = _effectiveViewportChangedListeners[i];
var l = listeners[i];
if (!l.Listener.IsAttachedToVisualTree)
{
@ -362,7 +362,7 @@ namespace Avalonia.Layout
if (viewport != l.Viewport)
{
l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport));
_effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport);
l.Viewport = viewport;
}
}
}
@ -414,7 +414,7 @@ namespace Avalonia.Layout
}
}
private readonly struct EffectiveViewportChangedListener
private class EffectiveViewportChangedListener
{
public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport)
{
@ -423,7 +423,7 @@ namespace Avalonia.Layout
}
public ILayoutable Listener { get; }
public Rect Viewport { get; }
public Rect Viewport { get; set; }
}
}
}

10
src/Avalonia.Base/Logging/LogArea.cs

@ -44,5 +44,15 @@ namespace Avalonia.Logging
/// The log event comes from X11Platform.
/// </summary>
public const string X11Platform = nameof(X11Platform);
/// <summary>
/// The log event comes from AndroidPlatform.
/// </summary>
public const string AndroidPlatform = nameof(AndroidPlatform);
/// <summary>
/// The log event comes from IOSPlatform.
/// </summary>
public const string IOSPlatform = nameof(IOSPlatform);
}
}

109
src/Avalonia.Base/Media/GlyphRun.cs

@ -445,7 +445,7 @@ namespace Avalonia.Media
/// </returns>
public int FindGlyphIndex(int characterIndex)
{
if (GlyphClusters == null)
if (GlyphClusters == null || GlyphClusters.Count == 0)
{
return characterIndex;
}
@ -614,17 +614,29 @@ namespace Avalonia.Media
private GlyphRunMetrics CreateGlyphRunMetrics()
{
var firstCluster = 0;
var lastCluster = Characters.Length - 1;
if (!IsLeftToRight)
{
var cluster = firstCluster;
firstCluster = lastCluster;
lastCluster = cluster;
}
if (GlyphClusters != null && GlyphClusters.Count > 0)
{
var firstCluster = GlyphClusters[0];
firstCluster = GlyphClusters[0];
lastCluster = GlyphClusters[GlyphClusters.Count - 1];
_offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
}
var isReversed = firstCluster > lastCluster;
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
var widthIncludingTrailingWhitespace = 0d;
var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount);
var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount);
for (var index = 0; index < GlyphIndices.Count; index++)
{
@ -635,16 +647,16 @@ namespace Avalonia.Media
var width = widthIncludingTrailingWhitespace;
if (IsLeftToRight)
if (isReversed)
{
for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
for (var index = 0; index < glyphCount; index++)
{
width -= GetGlyphAdvance(index, out _);
}
}
else
{
for (var index = 0; index < glyphCount; index++)
for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
{
width -= GetGlyphAdvance(index, out _);
}
@ -654,16 +666,15 @@ namespace Avalonia.Media
height);
}
private int GetTrailingWhitespaceLength(out int newLineLength, out int glyphCount)
{
glyphCount = 0;
newLineLength = 0;
if (Characters.IsEmpty)
private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount)
{
if (isReversed)
{
return 0;
return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
}
glyphCount = 0;
newLineLength = 0;
var trailingWhitespaceLength = 0;
if (GlyphClusters == null)
@ -732,6 +743,78 @@ namespace Avalonia.Media
return trailingWhitespaceLength;
}
private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount)
{
glyphCount = 0;
newLineLength = 0;
var trailingWhitespaceLength = 0;
if (GlyphClusters == null)
{
for (var i = 0; i < Characters.Length;)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (!codepoint.IsWhiteSpace)
{
break;
}
if (codepoint.IsBreakChar)
{
newLineLength++;
}
trailingWhitespaceLength++;
i += count;
glyphCount++;
}
}
else
{
for (var i = 0; i < GlyphClusters.Count; i++)
{
var currentCluster = GlyphClusters[i];
var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
if (!codepoint.IsWhiteSpace)
{
break;
}
var clusterLength = 1;
while (i - 1 >= 0)
{
var nextCluster = GlyphClusters[i - 1];
if (currentCluster == nextCluster)
{
clusterLength++;
i--;
continue;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
glyphCount++;
}
}
return trailingWhitespaceLength;
}
private void Set<T>(ref T field, T value)
{
_glyphRunImpl?.Dispose();

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

@ -209,7 +209,7 @@ namespace Avalonia.Media
var pen = new Pen(Stroke ?? defaultBrush, thickness,
new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap);
drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Size.Width, 0));
drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Metrics.Width, 0));
}
}
}

51
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -63,7 +63,7 @@ namespace Avalonia.Media.TextFormatting
MaxHeight = maxHeight;
MaxLines = maxLines;
MaxLines = maxLines;
TextLines = CreateTextLines();
}
@ -80,7 +80,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="maxLines">The maximum number of text lines.</param>
public TextLayout(
ITextSource textSource,
TextParagraphProperties paragraphProperties,
TextParagraphProperties paragraphProperties,
TextTrimming? textTrimming = null,
double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity,
@ -178,24 +178,18 @@ namespace Avalonia.Media.TextFormatting
return new Rect();
}
if (textPosition < 0 || textPosition >= _textSourceLength)
if (textPosition < 0)
{
var lastLine = TextLines[TextLines.Count - 1];
var lineX = lastLine.Width;
var lineY = Bounds.Bottom - lastLine.Height;
return new Rect(lineX, lineY, 0, lastLine.Height);
textPosition = _textSourceLength;
}
var currentY = 0.0;
foreach (var textLine in TextLines)
{
var end = textLine.FirstTextSourceIndex + textLine.Length - 1;
var end = textLine.FirstTextSourceIndex + textLine.Length;
if (end < textPosition)
if (end <= textPosition && end < _textSourceLength)
{
currentY += textLine.Height;
@ -224,7 +218,7 @@ namespace Avalonia.Media.TextFormatting
}
var result = new List<Rect>(TextLines.Count);
var currentY = 0d;
foreach (var textLine in TextLines)
@ -239,7 +233,7 @@ namespace Avalonia.Media.TextFormatting
var textBounds = textLine.GetTextBounds(start, length);
if(textBounds.Count > 0)
if (textBounds.Count > 0)
{
foreach (var bounds in textBounds)
{
@ -262,7 +256,7 @@ namespace Avalonia.Media.TextFormatting
}
}
if(textLine.FirstTextSourceIndex + textLine.Length >= start + length)
if (textLine.FirstTextSourceIndex + textLine.Length >= start + length)
{
break;
}
@ -305,7 +299,7 @@ namespace Avalonia.Media.TextFormatting
return GetHitTestResult(currentLine, characterHit, point);
}
public int GetLineIndexFromCharacterIndex(int charIndex, bool trailingEdge)
{
if (charIndex < 0)
@ -327,7 +321,7 @@ namespace Avalonia.Media.TextFormatting
continue;
}
if (charIndex >= textLine.FirstTextSourceIndex &&
if (charIndex >= textLine.FirstTextSourceIndex &&
charIndex <= textLine.FirstTextSourceIndex + textLine.Length - (trailingEdge ? 0 : 1))
{
return index;
@ -398,7 +392,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="left">The current left.</param>
/// <param name="width">The current width.</param>
/// <param name="height">The current height.</param>
private static void UpdateBounds(TextLine textLine,ref double left, ref double width, ref double height)
private static void UpdateBounds(TextLine textLine, ref double left, ref double width, ref double height)
{
var lineWidth = textLine.WidthIncludingTrailingWhitespace;
@ -421,7 +415,7 @@ namespace Avalonia.Media.TextFormatting
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
Bounds = new Rect(0,0,0, textLine.Height);
Bounds = new Rect(0, 0, 0, textLine.Height);
return new List<TextLine> { textLine };
}
@ -439,9 +433,9 @@ namespace Avalonia.Media.TextFormatting
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
if(textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
if (textLine == null || textLine.Length == 0 || textLine.TextRuns.Count == 0 && textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
{
if(previousLine != null && previousLine.NewLineLength > 0)
if (previousLine != null && previousLine.NewLineLength > 0)
{
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties);
@ -454,7 +448,7 @@ namespace Avalonia.Media.TextFormatting
}
_textSourceLength += textLine.Length;
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
{
@ -485,12 +479,17 @@ namespace Avalonia.Media.TextFormatting
//Fulfill max lines constraint
if (MaxLines > 0 && textLines.Count >= MaxLines)
{
if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null)
{
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
}
break;
}
}
//Make sure the TextLayout always contains at least on empty line
if(textLines.Count == 0)
if (textLines.Count == 0)
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
@ -501,7 +500,7 @@ namespace Avalonia.Media.TextFormatting
Bounds = new Rect(left, 0, width, height);
if(_paragraphProperties.TextAlignment == TextAlignment.Justify)
if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{
var whitespaceWidth = 0d;
@ -509,7 +508,7 @@ namespace Avalonia.Media.TextFormatting
{
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
if(lineWhitespaceWidth > whitespaceWidth)
if (lineWhitespaceWidth > whitespaceWidth)
{
whitespaceWidth = lineWhitespaceWidth;
}
@ -517,7 +516,7 @@ namespace Avalonia.Media.TextFormatting
var justificationWidth = width - whitespaceWidth;
if(justificationWidth > 0)
if (justificationWidth > 0)
{
var justificationProperties = new InterWordJustification(justificationWidth);

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

@ -166,58 +166,74 @@ namespace Avalonia.Media.TextFormatting
if (distance <= 0)
{
// hit happens before the line, return the first position
var firstRun = _textRuns[0];
if (firstRun is ShapedTextCharacters shapedTextCharacters)
{
return shapedTextCharacters.GlyphRun.GetCharacterHitFromDistance(distance, out _);
}
return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0);
}
return _resolvedFlowDirection == FlowDirection.LeftToRight ?
new CharacterHit(FirstTextSourceIndex) :
new CharacterHit(FirstTextSourceIndex + Length);
if (distance > WidthIncludingTrailingWhitespace)
{
var lastRun = _textRuns[_textRuns.Count - 1];
return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width);
}
// process hit that happens within the line
var characterHit = new CharacterHit();
var currentPosition = FirstTextSourceIndex;
var currentDistance = 0.0;
foreach (var currentRun in _textRuns)
{
switch (currentRun)
if (currentDistance + currentRun.Size.Width < distance)
{
case ShapedTextCharacters shapedRun:
{
characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
currentDistance += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
var offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
continue;
}
characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
break;
}
default:
break;
}
return characterHit;
}
private static CharacterHit GetRunCharacterHit(DrawableTextRun run, int currentPosition, double distance)
{
CharacterHit characterHit;
switch (run)
{
case ShapedTextCharacters shapedRun:
{
characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
var offset = Math.Max(0, currentPosition - shapedRun.Text.Start);
if (!shapedRun.GlyphRun.IsLeftToRight)
{
if (distance < currentRun.Size.Width / 2)
{
characterHit = new CharacterHit(currentPosition);
}
else
{
characterHit = new CharacterHit(currentPosition, currentRun.TextSourceLength);
}
break;
offset = Math.Max(0, offset - shapedRun.Text.End);
}
}
if (distance <= currentRun.Size.Width)
{
break;
}
characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength);
distance -= currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
break;
}
default:
{
if (distance < run.Size.Width / 2)
{
characterHit = new CharacterHit(currentPosition);
}
else
{
characterHit = new CharacterHit(currentPosition, run.TextSourceLength);
}
break;
}
}
return characterHit;
@ -226,136 +242,122 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var isTrailingHit = characterHit.TrailingLength > 0;
var flowDirection = _paragraphProperties.FlowDirection;
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var currentDistance = Start;
var currentPosition = FirstTextSourceIndex;
var remainingLength = characterIndex - FirstTextSourceIndex;
GlyphRun? lastRun = null;
var currentDistance = Start;
for (var index = 0; index < _textRuns.Count; index++)
if (flowDirection == FlowDirection.LeftToRight)
{
for (var index = 0; index < _textRuns.Count; index++)
{
var currentRun = _textRuns[index];
if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength,
flowDirection, out var distance, out _))
{
return currentDistance + distance;
}
//No hit hit found so we add the full width
currentDistance += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
remainingLength -= currentRun.TextSourceLength;
}
}
else
{
var textRun = _textRuns[index];
currentDistance += WidthIncludingTrailingWhitespace;
switch (textRun)
for (var index = _textRuns.Count - 1; index >= 0; index--)
{
case ShapedTextCharacters shapedTextCharacters:
{
var currentRun = shapedTextCharacters.GlyphRun;
var currentRun = _textRuns[index];
if (lastRun != null)
{
if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight &&
currentRun.Characters.Start == characterHit.FirstCharacterIndex &&
characterHit.TrailingLength == 0)
{
return currentDistance;
}
}
if (TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength,
flowDirection, out var distance, out var currentGlyphRun))
{
if (currentGlyphRun != null)
{
distance = currentGlyphRun.Size.Width - distance;
}
//Look for a hit in within the current run
if (currentPosition + remainingLength <= currentPosition + textRun.Text.Length)
{
characterHit = new CharacterHit(textRun.Text.Start + remainingLength);
return currentDistance - distance;
}
var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
//No hit hit found so we add the full width
currentDistance -= currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
remainingLength -= currentRun.TextSourceLength;
}
}
return currentDistance + distance;
}
return currentDistance;
}
//Look at the left and right edge of the current run
if (currentRun.IsLeftToRight)
{
if (_resolvedFlowDirection == FlowDirection.LeftToRight && (lastRun == null || lastRun.IsLeftToRight))
{
if (characterIndex <= currentPosition)
{
return currentDistance;
}
}
else
{
if (characterIndex == currentPosition)
{
return currentDistance;
}
}
private static bool TryGetDistanceFromCharacterHit(
DrawableTextRun currentRun,
CharacterHit characterHit,
int currentPosition,
int remainingLength,
FlowDirection flowDirection,
out double distance,
out GlyphRun? currentGlyphRun)
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var isTrailingHit = characterHit.TrailingLength > 0;
if (characterIndex == currentPosition + textRun.Text.Length && isTrailingHit)
{
return currentDistance + currentRun.Size.Width;
}
}
else
{
if (characterIndex == currentPosition)
{
return currentDistance + currentRun.Size.Width;
}
distance = 0;
currentGlyphRun = null;
var nextRun = index + 1 < _textRuns.Count ?
_textRuns[index + 1] as ShapedTextCharacters :
null;
switch (currentRun)
{
case ShapedTextCharacters shapedTextCharacters:
{
currentGlyphRun = shapedTextCharacters.GlyphRun;
if (nextRun != null)
{
if (nextRun.ShapedBuffer.IsLeftToRight)
{
if (characterIndex == currentPosition + textRun.Text.Length)
{
return currentDistance;
}
}
else
{
if (currentPosition + nextRun.Text.Length == characterIndex)
{
return currentDistance;
}
}
}
else
{
if (characterIndex > currentPosition + textRun.Text.Length)
{
return currentDistance;
}
}
}
if (currentPosition + remainingLength <= currentPosition + currentRun.Text.Length)
{
characterHit = new CharacterHit(currentRun.Text.Start + remainingLength);
lastRun = currentRun;
distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit);
break;
return true;
}
default:
if (currentPosition + remainingLength == currentPosition + currentRun.Text.Length && isTrailingHit)
{
if (characterIndex == currentPosition)
if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft)
{
return currentDistance;
distance = currentGlyphRun.Size.Width;
}
if (characterIndex == currentPosition + textRun.TextSourceLength)
{
return currentDistance + textRun.Size.Width;
}
return true;
}
break;
break;
}
default:
{
if (characterIndex == currentPosition)
{
return true;
}
}
//No hit hit found so we add the full width
currentDistance += textRun.Size.Width;
currentPosition += textRun.TextSourceLength;
remainingLength -= textRun.TextSourceLength;
if (characterIndex == currentPosition + currentRun.TextSourceLength)
{
distance = currentRun.Size.Width;
if (remainingLength <= 0)
{
break;
}
return true;
}
break;
}
}
return currentDistance;
return false;
}
/// <inheritdoc/>
@ -460,20 +462,33 @@ namespace Avalonia.Media.TextFormatting
var startIndex = currentRun.Text.Start + offset;
var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
currentShapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(startIndex + remainingLength) :
new CharacterHit(startIndex));
double startOffset;
double endOffset;
endX += endOffset;
if (currentShapedRun.ShapedBuffer.IsLeftToRight)
{
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
}
else
{
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
currentShapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(startIndex) :
new CharacterHit(startIndex + remainingLength));
if (currentPosition < startIndex)
{
startOffset = endOffset;
}
else
{
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
}
}
startX += startOffset;
endX += endOffset;
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
@ -504,47 +519,40 @@ namespace Avalonia.Media.TextFormatting
}
//Lines that only contain a linebreak need to be covered here
if(characterLength == 0)
if (characterLength == 0)
{
characterLength = NewLineLength;
}
var runwidth = endX - startX;
var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runwidth, Height), currentPosition, characterLength, currentRun);
var runWidth = endX - startX;
var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
{
currentRect = currentRect.WithWidth(currentWidth + runwidth);
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
currentRect = currentRect.WithWidth(currentWidth + runWidth);
var textBounds = result[result.Count - 1];
var textBounds = result[result.Count - 1];
textBounds.Rectangle = currentRect;
textBounds.Rectangle = currentRect;
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
}
currentWidth += runwidth;
currentWidth += runWidth;
currentPosition += characterLength;
if (currentDirection == FlowDirection.LeftToRight)
{
if (currentPosition > characterIndex)
{
break;
}
}
else
if (currentPosition > characterIndex)
{
if (currentPosition <= firstTextSourceIndex)
{
break;
}
break;
}
startX = endX;
@ -571,7 +579,7 @@ namespace Avalonia.Media.TextFormatting
var currentPosition = FirstTextSourceIndex;
var remainingLength = textLength;
var startX = Start + WidthIncludingTrailingWhitespace;
var startX = WidthIncludingTrailingWhitespace;
double currentWidth = 0;
var currentRect = Rect.Empty;
@ -582,7 +590,7 @@ namespace Avalonia.Media.TextFormatting
continue;
}
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex)
{
startX -= currentRun.Size.Width;
@ -601,20 +609,31 @@ namespace Avalonia.Media.TextFormatting
currentPosition += offset;
var startIndex = currentRun.Text.Start + offset;
double startOffset;
double endOffset;
var endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
currentShapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(startIndex + remainingLength) :
new CharacterHit(startIndex));
if (currentShapedRun.ShapedBuffer.IsLeftToRight)
{
if (currentPosition < startIndex)
{
startOffset = endOffset = 0;
}
else
{
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
endX += endOffset - currentShapedRun.Size.Width;
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
}
}
else
{
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
var startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(
currentShapedRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(startIndex) :
new CharacterHit(startIndex + remainingLength));
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
}
startX += startOffset - currentShapedRun.Size.Width;
startX -= currentRun.Size.Width - startOffset;
endX -= currentRun.Size.Width - endOffset;
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
@ -652,41 +671,35 @@ namespace Avalonia.Media.TextFormatting
}
var runWidth = endX - startX;
var currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
if(!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
{
currentRect = currentRect.WithWidth(currentWidth + runWidth);
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
{
currentRect = currentRect.WithWidth(currentWidth + runWidth);
var textBounds = result[result.Count - 1];
var textBounds = result[result.Count - 1];
textBounds.Rectangle = currentRect;
textBounds.Rectangle = currentRect;
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
textBounds.TextRunBounds.Add(currentRunBounds);
}
else
{
currentRect = currentRunBounds.Rectangle;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
}
currentWidth += runWidth;
currentPosition += characterLength;
if (currentDirection == FlowDirection.LeftToRight)
{
if (currentPosition > characterIndex)
{
break;
}
}
else
if (currentPosition > characterIndex)
{
if (currentPosition <= firstTextSourceIndex)
{
break;
}
break;
}
lastDirection = currentDirection;
@ -698,6 +711,8 @@ namespace Avalonia.Media.TextFormatting
}
}
result.Reverse();
return result;
}
@ -1302,8 +1317,14 @@ namespace Avalonia.Media.TextFormatting
switch (textAlignment)
{
case TextAlignment.Center:
return Math.Max(0, (_paragraphWidth - width) / 2);
var start = (_paragraphWidth - width) / 2;
if(paragraphFlowDirection == FlowDirection.RightToLeft)
{
start -= (widthIncludingTrailingWhitespace - width);
}
return Math.Max(0, start);
case TextAlignment.Right:
return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);

4
src/Markup/Avalonia.Markup.Xaml/IAddChild.cs → src/Avalonia.Base/Metadata/IAddChild.cs

@ -1,11 +1,11 @@
namespace Avalonia.Markup.Xaml
namespace Avalonia.Metadata
{
public interface IAddChild
{
void AddChild(object child);
}
public interface IAddChild<T> : IAddChild
public interface IAddChild<T>
{
void AddChild(T child);
}

16
src/Avalonia.Base/Platform/IPlatformGpu.cs

@ -0,0 +1,16 @@
using System;
using Avalonia.Metadata;
namespace Avalonia.Platform;
[Unstable]
public interface IPlatformGpu
{
IPlatformGpuContext PrimaryContext { get; }
}
[Unstable]
public interface IPlatformGpuContext : IDisposable
{
IDisposable EnsureCurrent();
}

107
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs

@ -0,0 +1,107 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO;
[Unstable]
public class BclStorageFile : IStorageBookmarkFile
{
private readonly FileInfo _fileInfo;
public BclStorageFile(FileInfo fileInfo)
{
_fileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));
}
public bool CanOpenRead => true;
public bool CanOpenWrite => true;
public string Name => _fileInfo.Name;
public virtual bool CanBookmark => true;
public Task<StorageItemProperties> GetBasicPropertiesAsync()
{
var props = new StorageItemProperties();
if (_fileInfo.Exists)
{
props = new StorageItemProperties(
(ulong)_fileInfo.Length,
_fileInfo.CreationTimeUtc,
_fileInfo.LastAccessTimeUtc);
}
return Task.FromResult(props);
}
public Task<IStorageFolder?> GetParentAsync()
{
if (_fileInfo.Directory is { } directory)
{
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
}
return Task.FromResult<IStorageFolder?>(null);
}
public Task<Stream> OpenRead()
{
return Task.FromResult<Stream>(_fileInfo.OpenRead());
}
public Task<Stream> OpenWrite()
{
return Task.FromResult<Stream>(_fileInfo.OpenWrite());
}
public virtual Task<string?> SaveBookmark()
{
return Task.FromResult<string?>(_fileInfo.FullName);
}
public Task ReleaseBookmark()
{
// No-op
return Task.CompletedTask;
}
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
try
{
if (_fileInfo.Directory is not null)
{
uri = Path.IsPathRooted(_fileInfo.FullName) ?
new Uri(new Uri("file://"), _fileInfo.FullName) :
new Uri(_fileInfo.FullName, UriKind.Relative);
return true;
}
uri = null;
return false;
}
catch (SecurityException)
{
uri = null;
return false;
}
}
protected virtual void Dispose(bool disposing)
{
}
~BclStorageFile()
{
Dispose(disposing: false);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}

88
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs

@ -0,0 +1,88 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO;
[Unstable]
public class BclStorageFolder : IStorageBookmarkFolder
{
private readonly DirectoryInfo _directoryInfo;
public BclStorageFolder(DirectoryInfo directoryInfo)
{
_directoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo));
if (!_directoryInfo.Exists)
{
throw new ArgumentException("Directory must exist", nameof(directoryInfo));
}
}
public string Name => _directoryInfo.Name;
public bool CanBookmark => true;
public Task<StorageItemProperties> GetBasicPropertiesAsync()
{
var props = new StorageItemProperties(
null,
_directoryInfo.CreationTimeUtc,
_directoryInfo.LastAccessTimeUtc);
return Task.FromResult(props);
}
public Task<IStorageFolder?> GetParentAsync()
{
if (_directoryInfo.Parent is { } directory)
{
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
}
return Task.FromResult<IStorageFolder?>(null);
}
public virtual Task<string?> SaveBookmark()
{
return Task.FromResult<string?>(_directoryInfo.FullName);
}
public Task ReleaseBookmark()
{
// No-op
return Task.CompletedTask;
}
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
try
{
uri = Path.IsPathRooted(_directoryInfo.FullName) ?
new Uri(new Uri("file://"), _directoryInfo.FullName) :
new Uri(_directoryInfo.FullName, UriKind.Relative);
return true;
}
catch (SecurityException)
{
uri = null;
return false;
}
}
protected virtual void Dispose(bool disposing)
{
}
~BclStorageFolder()
{
Dispose(disposing: false);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}

35
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs

@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO;
[Unstable]
public abstract class BclStorageProvider : IStorageProvider
{
public abstract bool CanOpen { get; }
public abstract Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options);
public abstract bool CanSave { get; }
public abstract Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options);
public abstract bool CanPickFolder { get; }
public abstract Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options);
public virtual Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
var file = new FileInfo(bookmark);
return file.Exists
? Task.FromResult<IStorageBookmarkFile?>(new BclStorageFile(file))
: Task.FromResult<IStorageBookmarkFile?>(null);
}
public virtual Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
var folder = new DirectoryInfo(bookmark);
return folder.Exists
? Task.FromResult<IStorageBookmarkFolder?>(new BclStorageFolder(folder))
: Task.FromResult<IStorageBookmarkFolder?>(null);
}
}

40
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@ -0,0 +1,40 @@
using System;
using System.IO;
using System.Linq;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO;
[Unstable]
public static class StorageProviderHelpers
{
public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
{
var name = Path.GetFileName(path);
if (name != null && !Path.HasExtension(name))
{
if (filter?.Patterns?.Count > 0)
{
if (defaultExtension != null
&& filter.Patterns.Contains(defaultExtension))
{
return Path.ChangeExtension(path, defaultExtension.TrimStart('.'));
}
var ext = filter.Patterns.FirstOrDefault(x => x != "*.*");
ext = ext?.Split(new[] { "*." }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
if (ext != null)
{
return Path.ChangeExtension(path, ext);
}
}
if (defaultExtension != null)
{
return Path.ChangeExtension(path, defaultExtension);
}
}
return path;
}
}

44
src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs

@ -0,0 +1,44 @@
using System.Collections.Generic;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Represents a name mapped to the associated file types (extensions).
/// </summary>
public sealed class FilePickerFileType
{
public FilePickerFileType(string name)
{
Name = name;
}
/// <summary>
/// File type name.
/// </summary>
public string Name { get; }
/// <summary>
/// List of extensions in GLOB format. I.e. "*.png" or "*.*".
/// </summary>
/// <remarks>
/// Used on Windows and Linux systems.
/// </remarks>
public IReadOnlyList<string>? Patterns { get; set; }
/// <summary>
/// List of extensions in MIME format.
/// </summary>
/// <remarks>
/// Used on Android, Browser and Linux systems.
/// </remarks>
public IReadOnlyList<string>? MimeTypes { get; set; }
/// <summary>
/// List of extensions in Apple uniform format.
/// </summary>
/// <remarks>
/// Used only on Apple devices.
/// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers.
/// </remarks>
public IReadOnlyList<string>? AppleUniformTypeIdentifiers { get; set; }
}

48
src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs

@ -0,0 +1,48 @@
namespace Avalonia.Platform.Storage;
/// <summary>
/// Dictionary of well known file types.
/// </summary>
public static class FilePickerFileTypes
{
public static FilePickerFileType All { get; } = new("All")
{
Patterns = new[] { "*.*" },
MimeTypes = new[] { "*/*" }
};
public static FilePickerFileType TextPlain { get; } = new("Plain Text")
{
Patterns = new[] { "*.txt" },
AppleUniformTypeIdentifiers = new[] { "public.plain-text" },
MimeTypes = new[] { "text/plain" }
};
public static FilePickerFileType ImageAll { get; } = new("All Images")
{
Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp" },
AppleUniformTypeIdentifiers = new[] { "public.image" },
MimeTypes = new[] { "image/*" }
};
public static FilePickerFileType ImageJpg { get; } = new("JPEG image")
{
Patterns = new[] { "*.jpg", "*.jpeg" },
AppleUniformTypeIdentifiers = new[] { "public.jpeg" },
MimeTypes = new[] { "image/jpeg" }
};
public static FilePickerFileType ImagePng { get; } = new("PNG image")
{
Patterns = new[] { "*.png" },
AppleUniformTypeIdentifiers = new[] { "public.png" },
MimeTypes = new[] { "image/png" }
};
public static FilePickerFileType Pdf { get; } = new("PDF document")
{
Patterns = new[] { "*.pdf" },
AppleUniformTypeIdentifiers = new[] { "com.adobe.pdf" },
MimeTypes = new[] { "application/pdf" }
};
}

19
src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs

@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Options class for <see cref="IStorageProvider.OpenFilePickerAsync"/> method.
/// </summary>
public class FilePickerOpenOptions : PickerOptions
{
/// <summary>
/// Gets or sets an option indicating whether open picker allows users to select multiple files.
/// </summary>
public bool AllowMultiple { get; set; }
/// <summary>
/// Gets or sets the collection of file types that the file open picker displays.
/// </summary>
public IReadOnlyList<FilePickerFileType>? FileTypeFilter { get; set; }
}

29
src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs

@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Options class for <see cref="IStorageProvider.SaveFilePickerAsync"/> method.
/// </summary>
public class FilePickerSaveOptions : PickerOptions
{
/// <summary>
/// Gets or sets the file name that the file save picker suggests to the user.
/// </summary>
public string? SuggestedFileName { get; set; }
/// <summary>
/// Gets or sets the default extension to be used to save the file.
/// </summary>
public string? DefaultExtension { get; set; }
/// <summary>
/// Gets or sets the collection of valid file types that the user can choose to assign to a file.
/// </summary>
public IReadOnlyList<FilePickerFileType>? FileTypeChoices { get; set; }
/// <summary>
/// Gets or sets a value indicating whether file open picker displays a warning if the user specifies the name of a file that already exists.
/// </summary>
public bool? ShowOverwritePrompt { get; set; }
}

12
src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs

@ -0,0 +1,12 @@
namespace Avalonia.Platform.Storage;
/// <summary>
/// Options class for <see cref="IStorageProvider.OpenFolderPickerAsync"/> method.
/// </summary>
public class FolderPickerOpenOptions : PickerOptions
{
/// <summary>
/// Gets or sets an option indicating whether open picker allows users to select multiple folders.
/// </summary>
public bool AllowMultiple { get; set; }
}

20
src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs

@ -0,0 +1,20 @@
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage;
[NotClientImplementable]
public interface IStorageBookmarkItem : IStorageItem
{
Task ReleaseBookmark();
}
[NotClientImplementable]
public interface IStorageBookmarkFile : IStorageFile, IStorageBookmarkItem
{
}
[NotClientImplementable]
public interface IStorageBookmarkFolder : IStorageFolder, IStorageBookmarkItem
{
}

32
src/Avalonia.Base/Platform/Storage/IStorageFile.cs

@ -0,0 +1,32 @@
using System.IO;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Represents a file. Provides information about the file and its contents, and ways to manipulate them.
/// </summary>
[NotClientImplementable]
public interface IStorageFile : IStorageItem
{
/// <summary>
/// Returns true, if file is readable.
/// </summary>
bool CanOpenRead { get; }
/// <summary>
/// Opens a stream for read access.
/// </summary>
Task<Stream> OpenRead();
/// <summary>
/// Returns true, if file is writeable.
/// </summary>
bool CanOpenWrite { get; }
/// <summary>
/// Opens stream for writing to the file.
/// </summary>
Task<Stream> OpenWrite();
}

11
src/Avalonia.Base/Platform/Storage/IStorageFolder.cs

@ -0,0 +1,11 @@
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Manipulates folders and their contents, and provides information about them.
/// </summary>
[NotClientImplementable]
public interface IStorageFolder : IStorageItem
{
}

53
src/Avalonia.Base/Platform/Storage/IStorageItem.cs

@ -0,0 +1,53 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Manipulates storage items (files and folders) and their contents, and provides information about them
/// </summary>
/// <remarks>
/// This interface inherits <see cref="IDisposable"/> . It's recommended to dispose <see cref="IStorageItem"/> when it's not used anymore.
/// </remarks>
[NotClientImplementable]
public interface IStorageItem : IDisposable
{
/// <summary>
/// Gets the name of the item including the file name extension if there is one.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the full file-system path of the item, if the item has a path.
/// </summary>
/// <remarks>
/// Android backend might return file path with "content:" scheme.
/// Browser and iOS backends might return relative uris.
/// </remarks>
bool TryGetUri([NotNullWhen(true)] out Uri? uri);
/// <summary>
/// Gets the basic properties of the current item.
/// </summary>
Task<StorageItemProperties> GetBasicPropertiesAsync();
/// <summary>
/// Returns true is item can be bookmarked and reused later.
/// </summary>
bool CanBookmark { get; }
/// <summary>
/// Saves items to a bookmark.
/// </summary>
/// <returns>
/// Returns identifier of a bookmark. Can be null if OS denied request.
/// </returns>
Task<string?> SaveBookmark();
/// <summary>
/// Gets the parent folder of the current storage item.
/// </summary>
Task<IStorageFolder?> GetParentAsync();
}

56
src/Avalonia.Base/Platform/Storage/IStorageProvider.cs

@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage;
[NotClientImplementable]
public interface IStorageProvider
{
/// <summary>
/// Returns true if it's possible to open file picker on the current platform.
/// </summary>
bool CanOpen { get; }
/// <summary>
/// Opens file picker dialog.
/// </summary>
/// <returns>Array of selected <see cref="IStorageFile"/> or empty collection if user canceled the dialog.</returns>
Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options);
/// <summary>
/// Returns true if it's possible to open save file picker on the current platform.
/// </summary>
bool CanSave { get; }
/// <summary>
/// Opens save file picker dialog.
/// </summary>
/// <returns>Saved <see cref="IStorageFile"/> or null if user canceled the dialog.</returns>
Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options);
/// <summary>
/// Returns true if it's possible to open folder picker on the current platform.
/// </summary>
bool CanPickFolder { get; }
/// <summary>
/// Opens folder picker dialog.
/// </summary>
/// <returns>Array of selected <see cref="IStorageFolder"/> or empty collection if user canceled the dialog.</returns>
Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options);
/// <summary>
/// Open <see cref="IStorageBookmarkFile"/> from the bookmark ID.
/// </summary>
/// <param name="bookmark">Bookmark ID.</param>
/// <returns>Bookmarked file or null if OS denied request.</returns>
Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark);
/// <summary>
/// Open <see cref="IStorageBookmarkFolder"/> from the bookmark ID.
/// </summary>
/// <param name="bookmark">Bookmark ID.</param>
/// <returns>Bookmarked folder or null if OS denied request.</returns>
Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark);
}

17
src/Avalonia.Base/Platform/Storage/PickerOptions.cs

@ -0,0 +1,17 @@
namespace Avalonia.Platform.Storage;
/// <summary>
/// Common options for <see cref="IStorageProvider.OpenFolderPickerAsync"/>, <see cref="IStorageProvider.OpenFilePickerAsync"/> and <see cref="IStorageProvider.SaveFilePickerAsync"/> methods.
/// </summary>
public class PickerOptions
{
/// <summary>
/// Gets or sets the text that appears in the title bar of a folder dialog.
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Gets or sets the initial location where the file open picker looks for files to present to the user.
/// </summary>
public IStorageFolder? SuggestedStartLocation { get; set; }
}

43
src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs

@ -0,0 +1,43 @@
using System;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Provides access to the content-related properties of an item (like a file or folder).
/// </summary>
public class StorageItemProperties
{
public StorageItemProperties(
ulong? size = null,
DateTimeOffset? dateCreated = null,
DateTimeOffset? dateModified = null)
{
Size = size;
DateCreated = dateCreated;
DateModified = dateModified;
}
/// <summary>
/// Gets the size of the file in bytes.
/// </summary>
/// <remarks>
/// Can be null if property is not available.
/// </remarks>
public ulong? Size { get; }
/// <summary>
/// Gets the date and time that the current folder was created.
/// </summary>
/// <remarks>
/// Can be null if property is not available.
/// </remarks>
public DateTimeOffset? DateCreated { get; }
/// <summary>
/// Gets the date and time of the last time the file was modified.
/// </summary>
/// <remarks>
/// Can be null if property is not available.
/// </remarks>
public DateTimeOffset? DateModified { get; }
}

82
src/Avalonia.Base/Rendering/Composition/Animations/AnimationInstanceBase.cs

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition.Animations;
/// <summary>
/// The base class for both key-frame and expression animation instances
/// Is responsible for activation tracking and for subscribing to properties used in dependencies
/// </summary>
internal abstract class AnimationInstanceBase : IAnimationInstance
{
private List<(ServerObject obj, CompositionProperty member)>? _trackedObjects;
protected PropertySetSnapshot Parameters { get; }
public ServerObject TargetObject { get; }
protected CompositionProperty Property { get; private set; } = null!;
private bool _invalidated;
public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters)
{
Parameters = parameters;
TargetObject = target;
}
protected void Initialize(CompositionProperty property, HashSet<(string name, string member)> trackedObjects)
{
if (trackedObjects.Count > 0)
{
_trackedObjects = new ();
foreach (var t in trackedObjects)
{
var obj = Parameters.GetObjectParameter(t.name);
if (obj is ServerObject tracked)
{
var off = tracked.GetCompositionProperty(t.member);
if (off == null)
#if DEBUG
throw new InvalidCastException("Attempting to subscribe to unknown field");
#else
continue;
#endif
_trackedObjects.Add((tracked, off));
}
}
}
Property = property;
}
public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property);
protected abstract ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue);
public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue)
{
_invalidated = false;
return EvaluateCore(now, currentValue);
}
public virtual void Activate()
{
if (_trackedObjects != null)
foreach (var tracked in _trackedObjects)
tracked.obj.SubscribeToInvalidation(tracked.member, this);
}
public virtual void Deactivate()
{
if (_trackedObjects != null)
foreach (var tracked in _trackedObjects)
tracked.obj.UnsubscribeFromInvalidation(tracked.member, this);
}
public void Invalidate()
{
if (_invalidated)
return;
_invalidated = true;
TargetObject.NotifyAnimatedValueChanged(Property);
}
}

75
src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs

@ -0,0 +1,75 @@
// ReSharper disable InconsistentNaming
// ReSharper disable CheckNamespace
using System;
using System.Collections.Generic;
using System.Numerics;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Rendering.Composition.Animations
{
/// <summary>
/// This is the base class for ExpressionAnimation and KeyFrameAnimation.
/// </summary>
/// <remarks>
/// Use the <see cref="CompositionObject.StartAnimation"/> method to start the animation.
/// Value parameters (as opposed to reference parameters which are set using <see cref="SetReferenceParameter"/>)
/// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called.
/// Changing the value of the variable after <see cref="CompositionObject.StartAnimation"/> is called will not affect
/// the value of the ExpressionAnimation.
/// See the remarks section of ExpressionAnimation for additional information.
/// </remarks>
public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase
{
private readonly CompositionPropertySet _propertySet;
internal CompositionAnimation(Compositor compositor) : base(compositor, null!)
{
_propertySet = new CompositionPropertySet(compositor);
}
/// <summary>
/// Clears all of the parameters of the animation.
/// </summary>
public void ClearAllParameters() => _propertySet.ClearAll();
/// <summary>
/// Clears a parameter from the animation.
/// </summary>
public void ClearParameter(string key) => _propertySet.Clear(key);
void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value);
public void SetColorParameter(string key, Media.Color value) => SetVariant(key, value);
public void SetMatrix3x2Parameter(string key, Matrix3x2 value) => SetVariant(key, value);
public void SetMatrix4x4Parameter(string key, Matrix4x4 value) => SetVariant(key, value);
public void SetQuaternionParameter(string key, Quaternion value) => SetVariant(key, value);
public void SetReferenceParameter(string key, CompositionObject compositionObject) =>
_propertySet.Set(key, compositionObject);
public void SetScalarParameter(string key, float value) => SetVariant(key, value);
public void SetVector2Parameter(string key, Vector2 value) => SetVariant(key, value);
public void SetVector3Parameter(string key, Vector3 value) => SetVariant(key, value);
public void SetVector4Parameter(string key, Vector4 value) => SetVariant(key, value);
public string? Target { get; set; }
internal abstract IAnimationInstance CreateInstance(ServerObject targetObject,
ExpressionVariant? finalValue);
internal PropertySetSnapshot CreateSnapshot() => _propertySet.Snapshot();
void ICompositionAnimationBase.InternalOnly()
{
}
}
}

24
src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using Avalonia.Rendering.Composition.Transport;
namespace Avalonia.Rendering.Composition.Animations
{
public class CompositionAnimationGroup : CompositionObject, ICompositionAnimationBase
{
internal List<CompositionAnimation> Animations { get; } = new List<CompositionAnimation>();
void ICompositionAnimationBase.InternalOnly()
{
}
public void Add(CompositionAnimation value) => Animations.Add(value);
public void Remove(CompositionAnimation value) => Animations.Remove(value);
public void RemoveAll() => Animations.Clear();
public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!)
{
}
}
}

53
src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimation.cs

@ -0,0 +1,53 @@
// ReSharper disable CheckNamespace
using System;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition.Animations
{
/// <summary>
/// A Composition Animation that uses a mathematical equation to calculate the value for an animating property every frame.
/// </summary>
/// <remarks>
/// The core of ExpressionAnimations allows a developer to define a mathematical equation that can be used to calculate the value
/// of a targeted animating property each frame.
/// This contrasts <see cref="KeyFrameAnimation"/>s, which use an interpolator to define how the animating
/// property changes over time. The mathematical equation can be defined using references to properties
/// of Composition objects, mathematical functions and operators and Input.
/// Use the <see cref="CompositionObject.StartAnimation"/> method to start the animation.
/// </remarks>
public class ExpressionAnimation : CompositionAnimation
{
private string? _expression;
private Expression? _parsedExpression;
internal ExpressionAnimation(Compositor compositor) : base(compositor)
{
}
/// <summary>
/// The mathematical equation specifying how the animated value is calculated each frame.
/// The Expression is the core of an <see cref="ExpressionAnimation"/> and represents the equation
/// the system will use to calculate the value of the animation property each frame.
/// The equation is set on this property in the form of a string.
/// Although expressions can be defined by simple mathematical equations such as "2+2",
/// the real power lies in creating mathematical relationships where the input values can change frame over frame.
/// </summary>
public string? Expression
{
get => _expression;
set
{
_expression = value;
_parsedExpression = null;
}
}
private Expression ParsedExpression => _parsedExpression ??= ExpressionParser.Parse(_expression.AsSpan());
internal override IAnimationInstance CreateInstance(
ServerObject targetObject, ExpressionVariant? finalValue)
=> new ExpressionAnimationInstance(ParsedExpression,
targetObject, finalValue, CreateSnapshot());
}
}

49
src/Avalonia.Base/Rendering/Composition/Animations/ExpressionAnimationInstance.cs

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition.Animations
{
/// <summary>
/// Server-side counterpart of <see cref="ExpressionAnimation"/> with values baked-in.
/// </summary>
internal class ExpressionAnimationInstance : AnimationInstanceBase, IAnimationInstance
{
private readonly Expression _expression;
private ExpressionVariant _startingValue;
private readonly ExpressionVariant? _finalValue;
protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue)
{
var ctx = new ExpressionEvaluationContext
{
Parameters = Parameters,
Target = TargetObject,
ForeignFunctionInterface = BuiltInExpressionFfi.Instance,
StartingValue = _startingValue,
FinalValue = _finalValue ?? _startingValue,
CurrentValue = currentValue
};
return _expression.Evaluate(ref ctx);
}
public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property)
{
_startingValue = startingValue;
var hs = new HashSet<(string, string)>();
_expression.CollectReferences(hs);
base.Initialize(property, hs);
}
public ExpressionAnimationInstance(Expression expression,
ServerObject target,
ExpressionVariant? finalValue,
PropertySetSnapshot parameters) : base(target, parameters)
{
_expression = expression;
_finalValue = finalValue;
}
}
}

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

Loading…
Cancel
Save