Browse Source

Merge branch 'master' into x11-dpi

pull/18936/head
Julien Lebosquain 8 months ago
committed by GitHub
parent
commit
29357dca31
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      Avalonia.sln
  2. 4
      Directory.Build.props
  3. 6
      api/Avalonia.Skia.nupkg.xml
  4. 138
      api/Avalonia.nupkg.xml
  5. 1
      build/ExternalConsumers.props
  6. 7
      build/SkiaSharp.props
  7. 32
      docs/api-compat.md
  8. 0
      docs/build.md
  9. 15
      docs/debug-xaml-compiler.md
  10. BIN
      docs/images/xcode-product-path.png
  11. 19
      docs/index.md
  12. 71
      docs/macos-native.md
  13. 12
      docs/porting-code-from-3rd-party-sources.md
  14. 30
      docs/release.md
  15. 2
      global.json
  16. 84
      native/Avalonia.Native/src/OSX/AvnView.mm
  17. 105
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  18. 22
      native/Avalonia.Native/src/OSX/StorageProvider.mm
  19. 2
      native/Avalonia.Native/src/OSX/WindowImpl.h
  20. 94
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  21. 2
      native/Avalonia.Native/src/OSX/app.mm
  22. 14
      native/Avalonia.Native/src/OSX/main.mm
  23. 10
      readme.md
  24. 3
      samples/ControlCatalog.iOS/Info.plist
  25. 8
      samples/ControlCatalog/App.xaml.cs
  26. 6
      samples/ControlCatalog/MainView.xaml.cs
  27. 3
      samples/ControlCatalog/Pages/HeaderedContentPage.axaml
  28. 3
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  29. 9
      samples/ControlCatalog/Pages/SliderPage.xaml
  30. 2
      samples/ControlCatalog/Pages/TabControlPage.xaml
  31. 10
      samples/GpuInterop/VulkanDemo/VulkanContent.cs
  32. 1
      samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml
  33. 6
      samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml.cs
  34. 24
      samples/IntegrationTestApp/ShowWindowTest.axaml
  35. 48
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  36. 4
      samples/SafeAreaDemo/App.xaml.cs
  37. 4
      samples/SafeAreaDemo/ViewModels/MainViewModel.cs
  38. 4
      samples/SingleProjectSandbox/App.axaml.cs
  39. 2
      samples/XEmbedSample/Program.cs
  40. 168
      src/Android/Avalonia.Android/AndroidDispatcherImpl.cs
  41. 3
      src/Android/Avalonia.Android/AndroidPlatform.cs
  42. 87
      src/Android/Avalonia.Android/AndroidThreadingInterface.cs
  43. 28
      src/Android/Avalonia.Android/ApplicationLifetime.cs
  44. 5
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  45. 6
      src/Android/Avalonia.Android/AvaloniaAndroidApplication.cs
  46. 23
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  47. 67
      src/Android/Avalonia.Android/AvaloniaView.Input.cs
  48. 21
      src/Android/Avalonia.Android/AvaloniaView.cs
  49. 90
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  50. 74
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  51. 23
      src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs
  52. 12
      src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs
  53. 18
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  54. 118
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  55. 129
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  56. 2
      src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs
  57. 46
      src/Android/Avalonia.Android/SingleViewLifetime.cs
  58. 2
      src/Avalonia.Base/Animation/Animators/BoolAnimator.cs
  59. 12
      src/Avalonia.Base/Animation/TransitionInstance.cs
  60. 3
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  61. 3
      src/Avalonia.Base/Controls/NameScope.cs
  62. 91
      src/Avalonia.Base/Data/Core/PropertyPath.cs
  63. 70
      src/Avalonia.Base/Input/FocusChangingEventArgs.cs
  64. 2
      src/Avalonia.Base/Input/FocusManager.cs
  65. 126
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  66. 34
      src/Avalonia.Base/Input/Gestures.cs
  67. 103
      src/Avalonia.Base/Input/InputElement.cs
  68. 88
      src/Avalonia.Base/Input/KeyboardDevice.cs
  69. 4
      src/Avalonia.Base/Input/PointerEventArgs.cs
  70. 34
      src/Avalonia.Base/Input/PointerPoint.cs
  71. 17
      src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs
  72. 18
      src/Avalonia.Base/Layout/Layoutable.cs
  73. 66
      src/Avalonia.Base/Media/FontManager.cs
  74. 14
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  75. 84
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  76. 11
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  77. 6
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  78. 2
      src/Avalonia.Base/Media/MediaContext.Compositor.cs
  79. 3
      src/Avalonia.Base/Media/MediaContext.cs
  80. 2
      src/Avalonia.Base/Media/RectangleGeometry.cs
  81. 135
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  82. 4
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  83. 37
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  84. 17
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  85. 4
      src/Avalonia.Base/Reactive/LightweightObservableBase.cs
  86. 3
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  87. 7
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  88. 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  89. 11
      src/Avalonia.Base/Styling/StyleInstance.cs
  90. 69
      src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
  91. 16
      src/Avalonia.Base/Threading/Dispatcher.Queue.cs
  92. 33
      src/Avalonia.Base/Threading/DispatcherOperation.cs
  93. 122
      src/Avalonia.Base/Threading/DispatcherPriorityAwaitable.cs
  94. 6
      src/Avalonia.Base/Utilities/CharacterReader.cs
  95. 5
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  96. 5
      src/Avalonia.Base/Utilities/KeywordParser.cs
  97. 3
      src/Avalonia.Base/Utilities/SmallDictionary.cs
  98. 5
      src/Avalonia.Base/Utilities/StyleClassParser.cs
  99. 1
      src/Avalonia.Base/composition-schema.xml
  100. 7
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

1
Avalonia.sln

@ -239,7 +239,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
NOTICE.md = NOTICE.md
NuGet.Config = NuGet.Config
readme.md = readme.md
Settings.StyleCop = Settings.StyleCop
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"

4
Directory.Build.props

@ -9,5 +9,9 @@
<AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
<RunApiCompat>False</RunApiCompat>
<LangVersion>12</LangVersion>
<CreateHardLinksForCopyAdditionalFilesIfPossible>true</CreateHardLinksForCopyAdditionalFilesIfPossible>
<CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>true</CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>
<CreateHardLinksForCopyLocalIfPossible>true</CreateHardLinksForCopyLocalIfPossible>
<CreateHardLinksForPublishFilesIfPossible>true</CreateHardLinksForPublishFilesIfPossible>
</PropertyGroup>
</Project>

6
api/Avalonia.Skia.nupkg.xml

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Skia.SkiaSharpExtensions.ToSKFilterQuality(Avalonia.Media.Imaging.BitmapInterpolationMode)</Target>
<Left>baseline/netstandard2.0/Avalonia.Skia.dll</Left>
<Right>target/netstandard2.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Skia.ISkiaGpuWithPlatformGraphicsContext.TryGetGrContext</Target>

138
api/Avalonia.nupkg.xml

@ -7,6 +7,72 @@
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Data.Core.CastTypePropertyPathElement</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Data.Core.ChildTraversalPropertyPathElement</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Data.Core.EnsureTypePropertyPathElement</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Data.Core.IPropertyPathElement</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Data.Core.PropertyPath</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Data.Core.PropertyPathBuilder</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Data.Core.PropertyPropertyPathElement</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Utilities.CharacterReader</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Utilities.IdentifierParser</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Utilities.KeywordParser</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Utilities.StyleClassParser</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Diagnostics.AppliedStyle.get_HasActivator</Target>
@ -43,6 +109,42 @@
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.get_IsCompleted</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.GetAwaiter</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.GetResult</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.OnCompleted(System.Action)</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetAwaiter</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetResult</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Controls.Primitives.IPopupHost.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect})</Target>
@ -109,6 +211,42 @@
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Controls.Platform.IInsetsManager.DisplayEdgeToEdgePreference</Target>
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Controls.Platform.IInsetsManager.DisplaysEdgeToEdge</Target>
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.Threading.DispatcherPriorityAwaitable</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.Threading.DispatcherPriorityAwaitable`1</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Threading.DispatcherPriorityAwaitable</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Threading.DispatcherPriorityAwaitable`1</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0009</DiagnosticId>
<Target>T:Avalonia.Diagnostics.StyleDiagnostics</Target>

1
build/ExternalConsumers.props

@ -30,5 +30,6 @@
<InternalsVisibleTo Include="UIAutomationProvider, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9" />
<InternalsVisibleTo Include="PresentationCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9" />
<InternalsVisibleTo Include="Avalonia.ForTestingOnly, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.Documents, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
</Project>

7
build/SkiaSharp.props

@ -1,10 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Condition="'$(AvsIncludeSkiaSharp3)' != 'true'">
<PackageReference Include="SkiaSharp" Version="2.88.9" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.9" />
</ItemGroup>
<ItemGroup Condition="'$(AvsIncludeSkiaSharp3)' == 'true'">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="3.119.0" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="3.119.0" />
<PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.0" />

32
docs/api-compat.md

@ -0,0 +1,32 @@
# API Compatibility
Avalonia maintains strict **source and binary compatibility** within major versions. Automated API compatibility checks run on every CI build to enforce this policy—builds will fail if breaking changes are detected.
## When Breaking Changes Are Permitted
Breaking changes are only acceptable under these specific circumstances and **must be approved** by the Avalonia code team:
- **Major version targeting**: When `master` branch is targeting a new major version
- **Accidental public APIs**: When code was unintentionally exposed as a public API and is unlikely to have external dependencies
- **Experimental features**: When the API is explicitly marked as unstable or experimental
## Handling Approved Breaking Changes
When a breaking change is approved, you can bypass CI validation using an API suppression file:
1. **Generate suppression file**: Run `nuke --update-api-suppression true`
2. **Commit changes**: Commit the [updated suppression file](../api/) in a separate commit
> **Note**: The suppression file should only be updated after the breaking change has been reviewed and approved.
## Baseline Version Configuration
API changes are validated against a **baseline version**—the reference point for compatibility checks.
- **Default behavior**: Uses the current major version (e.g., for version 11.0.5, baseline is 11.0.0)
- **Custom baseline**: Override using the [`api-baseline`](https://github.com/AvaloniaUI/Avalonia/blob/56d94d64b9aa6f16200be39b3bcb17f03325b7f9/nukebuild/BuildParameters.cs#L27) parameter with `nuke`
- **Fallback**: When not specified, uses the version defined in [`SharedVersion.props`](https://github.com/AvaloniaUI/Avalonia/blob/56d94d64b9aa6f16200be39b3bcb17f03325b7f9/build/SharedVersion.props#L6)
## Additional Resources
- [API Validation Tool Implementation](https://github.com/AvaloniaUI/Avalonia/pull/12072) - Pull request that introduced this feature

0
Documentation/build.md → docs/build.md

15
docs/debug-xaml-compiler.md

@ -0,0 +1,15 @@
# Debugging the XAML Compiler
The Avalonia XAML compiler can be debugged by setting an MSBuild property which triggers a debugger launch during compilation. This allows you to step through the XAML compilation process and troubleshoot issues.
To enable XAML compiler debugging, set the `AvaloniaXamlIlDebuggerLaunch` MSBuild property to `true` in your project file:
```xml
<PropertyGroup>
<AvaloniaXamlIlDebuggerLaunch>true</AvaloniaXamlIlDebuggerLaunch>
</PropertyGroup>
```
When this property is enabled, the XAML compiler will call `Debugger.Launch()` on startup, which prompts you to attach a debugger to the compiler process.
If you're working with the Sandbox project in the Avalonia repository, you can enable debugging by simply uncommenting the property line in the [project file](https://github.com/AvaloniaUI/Avalonia/blob/56d94d64b9aa6f16200be39b3bcb17f03325b7f9/samples/Sandbox/Sandbox.csproj#L8).

BIN
docs/images/xcode-product-path.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

19
docs/index.md

@ -0,0 +1,19 @@
# Avalonia Developer Documentation
This documentation covers Avalonia framework development. For user documentation on how to use Avalonia, visit https://docs.avaloniaui.net/.
## Getting Started
- [Building Avalonia](build.md) describes how to build Avalonia from source
- You should read the [Contributing guidelines](../CONTRIBUTING.md) before you start
- We have a [Code of Conduct](../CODE_OF_CONDUCT.md)
## Development
- [Debugging the XAML Compiler](debug-xaml-compiler.md)
- [Porting Code from 3rd Party Sources](porting-code-from-3rd-party-sources.md)
## Releases
- [API Compatibility](api-compat.md) describes the API compatibility guarantees provided by Avalonia, and exceptions to these guarantees
- Our [Release Process](release.md) describes the process for creating a new Avalonia release

71
docs/macos-native.md

@ -0,0 +1,71 @@
# macOS Native Code
The macOS platform backend has a native component, written in objective-c and located in [`native/Avalonia.Native/src/OSX`](../native/Avalonia.Native/src/OSX/). To work on this code, open the [`Avalonia.Native.OSX.xcodeproj`](../native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj)project in Xcode.
Changes to the native portion of the code can be recompiled in two ways:
1. Using the [`build.sh`](build.md#build-native-libraries-macos-only) script: this has the downside that it will trigger a full recompile of Avalonia
2. Using `AvaloniaNativeLibraryPath` described below
## Using `AvaloniaNativeLibraryPath `
When you make changes in Xcode and recompile using Cmd+B, the binary will be compiled to a location that can be seen under "Products":
![How to find the product path in Xcode](images/xcode-product-path.png)
To use this build in Avalonia, one can specifiy the path by using `AvaloniaNativePlatformOptions.AvaloniaNativeLibraryPath` in an application's AppBuilder:
```csharp
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<App>()
.UsePlatformDetect()
.With(new AvaloniaNativePlatformOptions
{
AvaloniaNativeLibraryPath = "[Path to your dylib]",
})
```
# Bundling Development Code
In certain situations you need to run an Avalonia sample application as an app bundle. One of these situations is testing macOS Accessibility - Xcode's Accessibility Inspector fails to recognise the application otherwise. To facilitate this, the [`IntegrationTestApp`](../samples/IntegrationTestApp/) has a [`bundle.sh`](../samples/IntegrationTestApp/bundle.sh) script which can be run to create a bundle of that application.
Alteratively, if you need to bundle another project, another solution is to change the sample's output path to resemble an app bundle. You can do this by modifying the output path in the csproj, e.g.:
```xml
<OutputPath>bin\$(Configuration)\$(Platform)\ControlCatalog.NetCore.app/Contents/MacOS</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<UseAppHost>true</UseAppHost>
```
And in the Contents output directory place a valid `Info.plist` file. An example for ControlCatalog.NetCore is:
```xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>ControlCatalog.NetCore</string>
<key>CFBundleDisplayName</key>
<string>ControlCatalog.NetCore</string>
<key>CFBundleIdentifier</key>
<string>ControlCatalog.NetCore</string>
<key>CFBundleVersion</key>
<string>0.10.999</string>
<key>CFBundlePackageType</key>
<string>AAPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleExecutable</key>
<string>ControlCatalog.NetCore</string>
<key>CFBundleIconFile</key>
<string>ControlCatalog.NetCore.icns</string>
<key>CFBundleShortVersionString</key>
<string>0.1</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHighResolutionCapable</key>
<true />
</dict>
</plist>
```

12
docs/porting-code-from-3rd-party-sources.md

@ -0,0 +1,12 @@
# Porting Code from 3rd Party Sources
When porting code or adapting code from other projects with a MIT compatible license, the source file must contain appropriate license headers. For example when porting code from WPF the header should contain:
```
// This source file is adapted from the Windows Presentation Foundation project.
// (https://github.com/dotnet/wpf/)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
```
If the file is a port of a specific commit of file from a 3rd party source, then consider including a permalink to the source file.

30
docs/release.md

@ -0,0 +1,30 @@
# Release Process
This document describes the process for creating a new Avalonia release
## Backports
- Go through PRs with "backport-candidate-*" tags, make sure this list makes sense
- Checkout https://github.com/grokys/avalonia-backport tool from the source code, get github api token
- Checkout stable release branch, like `release/11.0`
- Run backport cherry-picking process to the stable release branch
- Test if everything builds and tests are passing
- Test nightly builds of Avalonia
- Run labeling process (automatically replace "backport-candidate" with "backported" tags) and generate changelog
## Release
- Create a branch named e.g. `release/11.0.9` for the specific minor version
- Update the version number in the file [SharedVersion.props](../build/SharedVersion.props), e.g. `<Version>11.0.9</Version>`
- Add a tag for this version, e.g. `git tag 11.0.9`
- Push the release branch and the tag.
- Wait for azure pipelines to finish the build. Nightly build with 11.0.9 version should be released soon after.
- Using the nightly build run a due diligence test to make sure you're happy with the package.
- On azure pipelines, on the release for your release branch `release/11.0.9` click on the badge for "Nuget Release"
- Click deploy
- Make a release on Github releases
- Press "Auto-generate changelog", so GitHub will append information about new contributors
- Replace changelog with one generated by avalonia-backport tool. Enable discussion for the specific release
- Review the release information and publish.
- Update the dotnet templates, visual studio templates.
- Announce on telegram (RU and EN), twitter, etc

2
global.json

@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.404",
"version": "8.0.411",
"rollForward": "latestFeature"
},
"msbuild-sdks": {

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

@ -13,7 +13,6 @@
{
ComObjectWeakPtr<TopLevelImpl> _parent;
NSTrackingArea* _area;
bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed;
AvnInputModifiers _modifierState;
NSEvent* _lastMouseDownEvent;
AvnPixelSize _lastPixelSize;
@ -386,11 +385,57 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
}
- (BOOL) resignFirstResponder
{
auto window = [self window];
if (window != nullptr && window.keyWindow)
{
[self onLostFocus];
}
return YES;
}
- (void)viewWillMoveToWindow:(NSWindow *)newWindow
{
auto oldWindow = [self window];
if (oldWindow == newWindow)
{
// viewWillMoveToWindow can be called with the same window when the view hierarchy changes
return;
}
if (oldWindow != nullptr)
{
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:@"NSWindowDidResignKeyNotification"
object: oldWindow];
}
if (newWindow != nullptr)
{
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(windowDidResignKey:)
name:@"NSWindowDidResignKeyNotification"
object: newWindow];
}
}
- (void)windowDidResignKey:(NSNotification*)notification
{
auto window = [self window];
if (window != nullptr && notification.object == window && [window firstResponder] == self)
{
[self onLostFocus];
}
}
- (void)onLostFocus
{
auto parent = _parent.tryGet();
if(parent)
if (parent)
parent->TopLevelEvents->LostFocus();
return YES;
}
- (void)mouseMoved:(NSEvent *)event
@ -400,7 +445,6 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
- (void)mouseDown:(NSEvent *)event
{
_isLeftPressed = true;
_lastMouseDownEvent = event;
[self mouseEvent:event withType:LeftButtonDown];
}
@ -413,15 +457,12 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
{
case 2:
case 3:
_isMiddlePressed = true;
[self mouseEvent:event withType:MiddleButtonDown];
break;
case 4:
_isXButton1Pressed = true;
[self mouseEvent:event withType:XButton1Down];
break;
case 5:
_isXButton2Pressed = true;
[self mouseEvent:event withType:XButton2Down];
break;
@ -432,14 +473,12 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
- (void)rightMouseDown:(NSEvent *)event
{
_isRightPressed = true;
_lastMouseDownEvent = event;
[self mouseEvent:event withType:RightButtonDown];
}
- (void)mouseUp:(NSEvent *)event
{
_isLeftPressed = false;
[self mouseEvent:event withType:LeftButtonUp];
}
@ -449,15 +488,12 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
{
case 2:
case 3:
_isMiddlePressed = false;
[self mouseEvent:event withType:MiddleButtonUp];
break;
case 4:
_isXButton1Pressed = false;
[self mouseEvent:event withType:XButton1Up];
break;
case 5:
_isXButton2Pressed = false;
[self mouseEvent:event withType:XButton2Up];
break;
@ -468,7 +504,6 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
- (void)rightMouseUp:(NSEvent *)event
{
_isRightPressed = false;
[self mouseEvent:event withType:RightButtonUp];
}
@ -551,15 +586,6 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
_modifierState = [self getModifiers:modifierFlags];
}
- (void)resetPressedMouseButtons
{
_isLeftPressed = false;
_isRightPressed = false;
_isMiddlePressed = false;
_isXButton1Pressed = false;
_isXButton2Pressed = false;
}
- (void)flagsChanged:(NSEvent *)event
{
auto newModifierState = [self getModifiers:[event modifierFlags]];
@ -706,15 +732,17 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
if (mod & NSEventModifierFlagCommand)
rv |= Windows;
if (_isLeftPressed)
NSUInteger pressedButtons = [NSEvent pressedMouseButtons];
if (pressedButtons & (1 << 0)) // Left mouse button
rv |= LeftMouseButton;
if (_isMiddlePressed)
rv |= MiddleMouseButton;
if (_isRightPressed)
if (pressedButtons & (1 << 1)) // Right mouse button
rv |= RightMouseButton;
if (_isXButton1Pressed)
if (pressedButtons & (1 << 2)) // Middle mouse button
rv |= MiddleMouseButton;
if (pressedButtons & (1 << 3)) // X1 button
rv |= XButton1MouseButton;
if (_isXButton2Pressed)
if (pressedButtons & (1 << 4)) // X2 button
rv |= XButton2MouseButton;
return (AvnInputModifiers)rv;

105
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -435,9 +435,23 @@
return;
}
if(window->WindowState() == Maximized)
// If the window has been moved into a position where it's "zoomed"
// Then it should be set as Maximized.
if (window->WindowState() != Maximized && window->IsZoomed())
{
window->SetWindowState(Normal);
window->SetWindowState(Maximized, false);
}
// We should only return the window state to normal if
// the internal window state is maximized, and macOS says
// the window is no longer zoomed (I.E, the user has moved it)
// Stage Manager will "move" the window when repositioning it
// So if the window was "maximized" before, it should stay maximized
else if(window->WindowState() == Maximized && !window->IsZoomed())
{
// If we're moving the window while maximized,
// we need to let macOS handle if it should be resized
// And not handle it ourselves.
window->SetWindowState(Normal, false);
}
}
@ -462,8 +476,95 @@
}
}
- (BOOL)isPointInTitlebar:(NSPoint)windowPoint
{
auto parent = _parent.tryGetWithCast<WindowImpl>();
if (!parent || !_isExtended) {
return NO;
}
AvnView* view = parent->View;
NSPoint viewPoint = [view convertPoint:windowPoint fromView:nil];
double titlebarHeight = [self getExtendedTitleBarHeight];
// Check if click is in titlebar area (top portion of view)
if (viewPoint.y <= titlebarHeight) {
// Verify we're actually in a toolbar-related area
NSView* hitView = [[self findRootView:view] hitTest:windowPoint];
if (hitView) {
NSString* hitViewClass = [hitView className];
if ([hitViewClass containsString:@"Toolbar"] || [hitViewClass containsString:@"Titlebar"]) {
return YES;
}
}
}
return NO;
}
- (void)forwardToAvnView:(NSEvent *)event
{
auto parent = _parent.tryGetWithCast<WindowImpl>();
if (!parent) {
return;
}
switch(event.type) {
case NSEventTypeLeftMouseDown:
[parent->View mouseDown:event];
break;
case NSEventTypeLeftMouseUp:
[parent->View mouseUp:event];
break;
case NSEventTypeLeftMouseDragged:
[parent->View mouseDragged:event];
break;
case NSEventTypeRightMouseDown:
[parent->View rightMouseDown:event];
break;
case NSEventTypeRightMouseUp:
[parent->View rightMouseUp:event];
break;
case NSEventTypeRightMouseDragged:
[parent->View rightMouseDragged:event];
break;
case NSEventTypeOtherMouseDown:
[parent->View otherMouseDown:event];
break;
case NSEventTypeOtherMouseUp:
[parent->View otherMouseUp:event];
break;
case NSEventTypeOtherMouseDragged:
[parent->View otherMouseDragged:event];
break;
case NSEventTypeMouseMoved:
[parent->View mouseMoved:event];
break;
default:
break;
}
}
- (void)sendEvent:(NSEvent *_Nonnull)event
{
// Event-tracking loop for thick titlebar mouse events
if (event.type == NSEventTypeLeftMouseDown && [self isPointInTitlebar:event.locationInWindow])
{
NSEventMask mask = NSEventMaskLeftMouseDragged | NSEventMaskLeftMouseUp;
NSEvent *ev = event;
while (ev.type != NSEventTypeLeftMouseUp)
{
[self forwardToAvnView:ev];
[super sendEvent:ev];
ev = [NSApp nextEventMatchingMask:mask
untilDate:[NSDate distantFuture]
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
}
[self forwardToAvnView:ev];
[super sendEvent:ev];
return;
}
[super sendEvent:event];
auto parent = _parent.tryGetWithCast<WindowImpl>();

22
native/Avalonia.Native/src/OSX/StorageProvider.mm

@ -235,11 +235,6 @@ public:
panel.title = [NSString stringWithUTF8String:title];
}
if(initialDirectory != nullptr)
{
auto directoryString = [NSString stringWithUTF8String:initialDirectory];
panel.directoryURL = [NSURL URLWithString:directoryString];
}
if(initialFile != nullptr)
{
@ -248,6 +243,12 @@ public:
SetAccessoryView(panel, filters, false);
if(initialDirectory != nullptr)
{
auto directoryString = [NSString stringWithUTF8String:initialDirectory];
panel.directoryURL = [NSURL URLWithString:directoryString];
}
auto handler = ^(NSModalResponse result) {
if(result == NSFileHandlingPanelOKButton)
{
@ -304,11 +305,6 @@ public:
panel.title = [NSString stringWithUTF8String:title];
}
if(initialDirectory != nullptr)
{
auto directoryString = [NSString stringWithUTF8String:initialDirectory];
panel.directoryURL = [NSURL URLWithString:directoryString];
}
if(initialFile != nullptr)
{
@ -317,6 +313,12 @@ public:
SetAccessoryView(panel, filters, true);
if(initialDirectory != nullptr)
{
auto directoryString = [NSString stringWithUTF8String:initialDirectory];
panel.directoryURL = [NSURL URLWithString:directoryString];
}
auto handler = ^(NSModalResponse result) {
if(result == NSFileHandlingPanelOKButton)
{

2
native/Avalonia.Native/src/OSX/WindowImpl.h

@ -71,6 +71,8 @@ BEGIN_INTERFACE_MAP()
void ExitFullScreenMode ();
virtual HRESULT SetWindowState (AvnWindowState state) override;
virtual HRESULT SetWindowState (AvnWindowState state, bool shouldResize);
virtual bool IsModal() override;

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

@ -451,6 +451,10 @@ void WindowImpl::ExitFullScreenMode() {
}
HRESULT WindowImpl::SetWindowState(AvnWindowState state) {
return SetWindowState(state, true);
}
HRESULT WindowImpl::SetWindowState(AvnWindowState state, bool shouldResize) {
START_COM_CALL;
@autoreleasepool {
@ -474,61 +478,63 @@ HRESULT WindowImpl::SetWindowState(AvnWindowState state) {
if (_shown) {
_actualWindowState = _lastWindowState;
switch (state) {
case Maximized:
if (currentState == FullScreen) {
ExitFullScreenMode();
}
if (shouldResize) {
switch (state) {
case Maximized:
if (currentState == FullScreen) {
ExitFullScreenMode();
}
lastPositionSet.X = 0;
lastPositionSet.Y = 0;
lastPositionSet.X = 0;
lastPositionSet.Y = 0;
if ([Window isMiniaturized]) {
[Window deminiaturize:Window];
}
if ([Window isMiniaturized]) {
[Window deminiaturize:Window];
}
if (!IsZoomed()) {
DoZoom();
}
break;
if (!IsZoomed()) {
DoZoom();
}
break;
case Minimized:
if (currentState == FullScreen) {
ExitFullScreenMode();
} else {
[Window miniaturize:Window];
}
break;
case Minimized:
if (currentState == FullScreen) {
ExitFullScreenMode();
} else {
[Window miniaturize:Window];
}
break;
case FullScreen:
if ([Window isMiniaturized]) {
[Window deminiaturize:Window];
}
case FullScreen:
if ([Window isMiniaturized]) {
[Window deminiaturize:Window];
}
EnterFullScreenMode();
break;
EnterFullScreenMode();
break;
case Normal:
if ([Window isMiniaturized]) {
[Window deminiaturize:Window];
}
case Normal:
if ([Window isMiniaturized]) {
[Window deminiaturize:Window];
}
if (currentState == FullScreen) {
ExitFullScreenMode();
}
if (currentState == FullScreen) {
ExitFullScreenMode();
}
if (IsZoomed()) {
if (_decorations == SystemDecorationsFull) {
DoZoom();
} else {
[Window setFrame:_preZoomSize display:true];
auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
if (IsZoomed()) {
if (_decorations == SystemDecorationsFull) {
DoZoom();
} else {
[Window setFrame:_preZoomSize display:true];
auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
[View setFrameSize:newFrame];
}
[View setFrameSize:newFrame];
}
}
break;
}
break;
}
}
WindowEvents->WindowStateChanged(_actualWindowState);

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

@ -30,8 +30,6 @@ ComPtr<IAvnApplicationEvents> _events;
break;
}
[[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"];
[[NSApplication sharedApplication] setHelpMenu: [[NSMenu new] initWithTitle:@""]];

14
native/Avalonia.Native/src/OSX/main.mm

@ -129,14 +129,22 @@ public:
}
}
virtual HRESULT SetShowInDock(int show) override
virtual HRESULT SetShowInDock(int show) override
{
START_COM_CALL;
@autoreleasepool
{
AvnDesiredActivationPolicy = show
? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory;
NSApplication* app = [NSApplication sharedApplication];
NSApplicationActivationPolicy requestedPolicy = show
? NSApplicationActivationPolicyRegular
: NSApplicationActivationPolicyAccessory;
if ([app activationPolicy] != requestedPolicy)
{
[app setActivationPolicy:requestedPolicy];
}
return S_OK;
}
}

10
readme.md

@ -28,7 +28,7 @@ You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/
See our [Get Started](https://avaloniaui.net/gettingstarted?utm_source=github&utm_medium=referral&utm_content=readme_link) guide to begin developing apps with Avalonia UI.
### Visual Studio
The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](https://docs.avaloniaui.net/docs/getting-started).
The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaVS) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](https://docs.avaloniaui.net/docs/getting-started).
### JetBrains Rider
[JetBrains Rider](https://www.jetbrains.com/rider/whatsnew/?mkt_tok=eyJpIjoiTURBNU1HSmhNV0kwTUdFMiIsInQiOiJtNnU2VEc1TlNLa1ZRVkROYmdZYVpYREJsaU1qdUhmS3dxSzRHczdYWHl0RVlTNDMwSFwvNUs3VENTNVM0bVcyNFdaRmVYZzVWTTF1N3VrQWNGTkJreEhlam1hMlB4UVVWcHBGM1dNOUxoXC95YnRQdGgyUXl1YmZCM3h3d3BVWWdBIn0%3D#avalonia-support) now has official support for Avalonia.
@ -49,6 +49,12 @@ Install-Package Avalonia.Desktop
See what others have built with Avalonia UI on our [Showcase](https://avaloniaui.net/showcase?utm_source=github&utm_medium=referral&utm_content=readme_link). We welcome submissions!
## Sponsors
Avalonia development is supported by the generous sponsorship of [Devolutions](https://devolutions.net/?utm_source=pr&utm_medium=partnership&utm_campaign=avalonia&utm_id=C086&member_status=responded).
<a href="https://devolutions.net/?utm_source=pr&utm_medium=partnership&utm_campaign=avalonia&utm_id=C086&member_status=responded"><img width="200" alt="devolutions-color-hr" src="https://github.com/user-attachments/assets/597e3650-0b80-496e-84b2-db51fbe7e06e"/></a>
## Community
Join our community hub to get early access to upcoming features, share your thoughts, and connect directly with the Avalonia team.
[![communityannouncement-banner 1](https://github.com/user-attachments/assets/21950b56-cd28-4574-9a0a-73bb17b89d31)](https://avaloniaui.community)
@ -70,7 +76,7 @@ We have a [range of samples](https://github.com/AvaloniaUI/Avalonia.Samples) to
## Building and Using
See the [build instructions here](Documentation/build.md).
See the [build instructions here](docs/build.md).
## Contributing

3
samples/ControlCatalog.iOS/Info.plist

@ -16,7 +16,6 @@
<array>
<integer>1</integer>
<integer>2</integer>
<integer>3</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
@ -38,5 +37,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

8
samples/ControlCatalog/App.xaml.cs

@ -44,6 +44,10 @@ namespace ControlCatalog
{
desktopLifetime.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() };
}
else if(ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
{
singleViewFactoryApplicationLifetime.MainViewFactory = () => new MainView { DataContext = new MainWindowViewModel() };
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
{
singleViewLifetime.MainView = new MainView { DataContext = new MainWindowViewModel() };
@ -97,6 +101,10 @@ namespace ControlCatalog
newWindow.Show();
oldWindow?.Close();
}
else if (app.ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
{
singleViewFactoryApplicationLifetime.MainViewFactory = () => new MainView { DataContext = new MainWindowViewModel() };
}
else if (app.ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
{
singleViewLifetime.MainView = new MainView();

6
samples/ControlCatalog/MainView.xaml.cs

@ -108,14 +108,14 @@ namespace ControlCatalog
ViewModel.SafeAreaPadding = insets.SafeAreaPadding;
};
ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge;
ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdgePreference;
ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true;
ViewModel.PropertyChanged += async (sender, args) =>
{
if (args.PropertyName == nameof(ViewModel.DisplayEdgeToEdge))
{
insets.DisplayEdgeToEdge = ViewModel.DisplayEdgeToEdge;
insets.DisplayEdgeToEdgePreference = ViewModel.DisplayEdgeToEdge;
}
else if (args.PropertyName == nameof(ViewModel.IsSystemBarVisible))
{
@ -124,7 +124,7 @@ namespace ControlCatalog
// Give the OS some time to apply new values and refresh the view model.
await Task.Delay(100);
ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge;
ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdgePreference;
ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true;
};
}

3
samples/ControlCatalog/Pages/HeaderedContentPage.axaml

@ -13,6 +13,9 @@
CornerRadius="3">
<TextBlock Text="Some content"/>
</HeaderedContentControl>
<GroupBox Header="This is a GroupBox">
<TextBlock Text="Essentially a restyled HeaderedContentControl"/>
</GroupBox>
<HeaderedContentControl Header="This is the image header!">
<Image Source="/Assets/delicate-arch-896885_640.jpg"/>
</HeaderedContentControl>

3
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -20,6 +20,9 @@
<Setter Property="Background" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="VirtualizingStackPanel">
<Setter Property="CacheLength" Value="0.5" />
</Style>
</DockPanel.Styles>
<StackPanel DockPanel.Dock="Top" Margin="4">
<TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock>

9
samples/ControlCatalog/Pages/SliderPage.xaml

@ -21,6 +21,15 @@
IsSnapToTickEnabled="True"
Ticks="0,20,25,40,75,100"
Width="300" />
<Slider Name="ReversedTickedSlider"
Value="0"
Minimum="0"
Maximum="100"
TickPlacement="BottomRight"
IsSnapToTickEnabled="True"
IsDirectionReversed="True"
Ticks="0,20,25,40,75,100"
Width="300" />
<Slider Name="SliderWithTooltip"
Value="0"
Minimum="0"

2
samples/ControlCatalog/Pages/TabControlPage.xaml

@ -9,7 +9,7 @@
<Style Selector="DockPanel.WithContentTemplates">
<Style Selector="^ TabItem">
<Setter Property="ContentTemplate">
<DataTemplate x:CompileBindings="False">
<DataTemplate x:DataType="x:Object">
<Border BorderBrush="Red" BorderThickness="10">
<ContentPresenter Content="{Binding}"/>
</Border>

10
samples/GpuInterop/VulkanDemo/VulkanContent.cs

@ -142,7 +142,7 @@ unsafe class VulkanContent : IDisposable
var model = Matrix4x4.CreateFromYawPitchRoll((float)yaw, (float)pitch, (float)roll);
var vertexConstant = new VertextPushConstant()
var vertexConstant = new VertexPushConstant()
{
Disco = (float)disco,
MinY = _minY,
@ -206,7 +206,7 @@ unsafe class VulkanContent : IDisposable
_pipelineLayout,0,1, &dset, null);
api.CmdPushConstants(commandBufferHandle, _pipelineLayout, ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit, 0,
(uint)Marshal.SizeOf<VertextPushConstant>(), &vertexConstant);
(uint)Marshal.SizeOf<VertexPushConstant>(), &vertexConstant);
api.CmdBindVertexBuffers(commandBufferHandle, 0, 1, _vertexBuffer, 0);
api.CmdBindIndexBuffer(commandBufferHandle, _indexBuffer, 0, IndexType.Uint16);
@ -613,14 +613,14 @@ unsafe class VulkanContent : IDisposable
var vertexPushConstantRange = new PushConstantRange()
{
Offset = 0,
Size = (uint)Marshal.SizeOf<VertextPushConstant>(),
Size = (uint)Marshal.SizeOf<VertexPushConstant>(),
StageFlags = ShaderStageFlags.VertexBit
};
var fragPushConstantRange = new PushConstantRange()
{
//Offset = vertexPushConstantRange.Size,
Size = (uint)Marshal.SizeOf<VertextPushConstant>(),
Size = (uint)Marshal.SizeOf<VertexPushConstant>(),
StageFlags = ShaderStageFlags.FragmentBit
};
@ -809,7 +809,7 @@ unsafe class VulkanContent : IDisposable
private DescriptorSet _descriptorSet;
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct VertextPushConstant
private struct VertexPushConstant
{
public float MaxY;
public float MinY;

1
samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml

@ -9,6 +9,7 @@
<CheckBox Name="WindowForceSystemChrome" Content="Force SystemChrome" />
<CheckBox Name="WindowPreferSystemChrome" Content="Prefer SystemChrome" />
<CheckBox Name="WindowMacThickSystemChrome" Content="Mac Thick SystemChrome" />
<CheckBox Name="WindowShowTitleAreaControl" Content="Show Title Area Control" />
<TextBox Name="WindowTitleBarHeightHint" Text="-1" Watermark="In dips" />
<Button Name="ApplyWindowDecorations"
Content="Apply decorations on this Window"

6
samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml.cs

@ -21,6 +21,12 @@ public partial class WindowDecorationsPage : UserControl
| (WindowForceSystemChrome.IsChecked == true ? ExtendClientAreaChromeHints.SystemChrome : 0)
| (WindowPreferSystemChrome.IsChecked == true ? ExtendClientAreaChromeHints.PreferSystemChrome : 0)
| (WindowMacThickSystemChrome.IsChecked == true ? ExtendClientAreaChromeHints.OSXThickTitleBar : 0);
if (window is ShowWindowTest showWindowTest && WindowShowTitleAreaControl.IsChecked == true)
{
showWindowTest.ShowTitleAreaControl();
}
AdjustOffsets(window);
window.Background = Brushes.Transparent;

24
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -5,8 +5,17 @@
Name="SecondaryWindow"
x:DataType="Window"
Title="Show Window Test">
<integrationTestApp:MeasureBorder Name="MyBorder" Background="{DynamicResource SystemRegionBrush}">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid>
<Grid Name="TitleAreaControl" IsVisible="False"
Background="LightBlue" VerticalAlignment="Top" ZIndex="100"
Margin="0,0,0,0">
<TextBlock Text="Title Area Control (Tabs)"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="DarkBlue" FontWeight="Bold" />
</Grid>
<integrationTestApp:MeasureBorder Name="MyBorder" Background="{DynamicResource SystemRegionBrush}">
<Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Label Grid.Column="0" Grid.Row="1">Client Size</Label>
<TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
Text="{Binding ClientSize, Mode=OneWay}" />
@ -53,12 +62,19 @@
<Label Grid.Row="11" Content="MeasuredWith:" />
<TextBlock Grid.Column="1" Grid.Row="11" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
<StackPanel Orientation="Horizontal" Grid.Row="12">
<Label Grid.Column="0" Grid.Row="12">Mouse Move Event Count</Label>
<TextBox Name="MouseMoveCount" Grid.Column="1" Grid.Row="12" IsReadOnly="True" Text="0" />
<Label Grid.Column="0" Grid.Row="13">Mouse Release Event Count</Label>
<TextBox Name="MouseReleaseCount" Grid.Column="1" Grid.Row="13" IsReadOnly="True" Text="0" />
<StackPanel Orientation="Horizontal" Grid.Row="14" Grid.ColumnSpan="2">
<Button Name="HideButton" Command="{Binding $parent[Window].Hide}">Hide</Button>
<Button Name="AddToWidth" Click="AddToWidth_Click">Add to Width</Button>
<Button Name="AddToHeight" Click="AddToHeight_Click">Add to Height</Button>
</StackPanel>
</Grid>
</integrationTestApp:MeasureBorder>
</integrationTestApp:MeasureBorder>
</Grid>
</Window>

48
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@ -30,6 +30,8 @@ namespace IntegrationTestApp
{
private readonly DispatcherTimer? _timer;
private readonly TextBox? _orderTextBox;
private int _mouseMoveCount;
private int _mouseReleaseCount;
public ShowWindowTest()
{
@ -37,6 +39,10 @@ namespace IntegrationTestApp
DataContext = this;
PositionChanged += (s, e) => CurrentPosition.Text = $"{Position}";
PointerMoved += OnPointerMoved;
PointerReleased += OnPointerReleased;
PointerExited += (_, e) => ResetCounters();
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
_orderTextBox = CurrentOrder;
@ -74,5 +80,47 @@ namespace IntegrationTestApp
private void AddToWidth_Click(object? sender, RoutedEventArgs e) => Width = Bounds.Width + 10;
private void AddToHeight_Click(object? sender, RoutedEventArgs e) => Height = Bounds.Height + 10;
private void OnPointerMoved(object? sender, Avalonia.Input.PointerEventArgs e)
{
_mouseMoveCount++;
UpdateCounterDisplays();
}
private void OnPointerReleased(object? sender, Avalonia.Input.PointerReleasedEventArgs e)
{
_mouseReleaseCount++;
UpdateCounterDisplays();
}
public void ResetCounters()
{
_mouseMoveCount = 0;
_mouseReleaseCount = 0;
UpdateCounterDisplays();
}
private void UpdateCounterDisplays()
{
var mouseMoveCountTextBox = this.FindControl<TextBox>("MouseMoveCount");
var mouseReleaseCountTextBox = this.FindControl<TextBox>("MouseReleaseCount");
if (mouseMoveCountTextBox != null)
mouseMoveCountTextBox.Text = _mouseMoveCount.ToString();
if (mouseReleaseCountTextBox != null)
mouseReleaseCountTextBox.Text = _mouseReleaseCount.ToString();
}
public void ShowTitleAreaControl()
{
var titleAreaControl = this.FindControl<Grid>("TitleAreaControl");
if (titleAreaControl == null) return;
titleAreaControl.IsVisible = true;
var titleBarHeight = ExtendClientAreaTitleBarHeightHint > 0 ? ExtendClientAreaTitleBarHeightHint : 30;
titleAreaControl.Margin = new Thickness(110, -titleBarHeight, 8, 0);
titleAreaControl.Height = titleBarHeight;
}
}
}

4
samples/SafeAreaDemo/App.xaml.cs

@ -19,6 +19,10 @@ namespace SafeAreaDemo
{
desktop.MainWindow = new MainWindow();
}
else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
{
singleViewFactoryApplicationLifetime.MainViewFactory = () => new MainView();
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = new MainView();

4
samples/SafeAreaDemo/ViewModels/MainViewModel.cs

@ -72,7 +72,7 @@ namespace SafeAreaDemo.ViewModels
if (_insetsManager != null)
{
_insetsManager.DisplayEdgeToEdge = value;
_insetsManager.DisplayEdgeToEdgePreference = value;
}
this.RaisePropertyChanged();
@ -129,7 +129,7 @@ namespace SafeAreaDemo.ViewModels
{
_insetsManager.SafeAreaChanged += InsetsManager_SafeAreaChanged;
_displayEdgeToEdge = _insetsManager.DisplayEdgeToEdge;
_displayEdgeToEdge = _insetsManager.DisplayEdgeToEdgePreference;
_hideSystemBars = !(_insetsManager.IsSystemBarVisible ?? false);
}

4
samples/SingleProjectSandbox/App.axaml.cs

@ -21,6 +21,10 @@ public class App : Application
{
desktopLifetime.MainWindow = new MainWindow();
}
else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
{
singleViewFactoryApplicationLifetime.MainViewFactory = () => new MainView();
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
{
singleViewLifetime.MainView = new MainView();

2
samples/XEmbedSample/Program.cs

@ -20,7 +20,7 @@ class Program
.With(new X11PlatformOptions()
{
UseGLibMainLoop = true,
ExterinalGLibMainLoopExceptionLogger = e => Console.WriteLine(e.ToString())
ExternalGLibMainLoopExceptionLogger = e => Console.WriteLine(e.ToString())
})
.UseX11()
.SetupWithoutStarting();

168
src/Android/Avalonia.Android/AndroidDispatcherImpl.cs

@ -0,0 +1,168 @@
using System;
using System.Diagnostics;
using Android.OS;
using Avalonia.Controls.Documents;
using Avalonia.Threading;
using Java.Lang;
using App = Android.App.Application;
using Object = Java.Lang.Object;
namespace Avalonia.Android
{
internal sealed class AndroidDispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing,
IDispatcherImplWithPendingInput
{
[ThreadStatic] private static bool? s_isUIThread;
private readonly Looper _mainLooper;
private readonly Handler _handler;
private readonly Runnable _signaler;
private readonly Runnable _timerSignaler;
private readonly Runnable _wakeupSignaler;
private readonly MessageQueue _queue;
private readonly object _lock = new();
private bool _signaled;
private bool _backgroundProcessingRequested;
public AndroidDispatcherImpl()
{
_mainLooper = App.Context.MainLooper ??
throw new InvalidOperationException(
"Application.Context.MainLooper was not expected to be null.");
if (!CurrentThreadIsLoopThread)
throw new InvalidOperationException("This class should be instanciated from the UI thread");
_handler = new Handler(_mainLooper);
_signaler = new Runnable(OnSignaled);
_timerSignaler = new Runnable(OnTimer);
_wakeupSignaler = new Runnable(() => { });
_queue = Looper.MyQueue();
Looper.MyQueue().AddIdleHandler(new IdleHandler(this));
CanQueryPendingInput = OperatingSystem.IsAndroidVersionAtLeast(23);
}
public event Action? Timer;
private void OnTimer() => Timer?.Invoke();
public event Action? Signaled;
private void OnSignaled()
{
lock (_lock)
_signaled = false;
Signaled?.Invoke();
}
public bool CurrentThreadIsLoopThread
{
get
{
if (s_isUIThread.HasValue)
return s_isUIThread.Value;
var uiThread = OperatingSystem.IsAndroidVersionAtLeast(23)
? _mainLooper.IsCurrentThread
: _mainLooper.Thread.Equals(Java.Lang.Thread.CurrentThread());
s_isUIThread = uiThread;
return uiThread;
}
}
public void Signal()
{
lock (_lock)
{
if(_signaled)
return;
_signaled = true;
_handler.Post(_signaler);
}
}
readonly Stopwatch _clock = Stopwatch.StartNew();
public long Now => _clock.ElapsedMilliseconds;
public void UpdateTimer(long? dueTimeInMs)
{
_handler.RemoveCallbacks(_timerSignaler);
if (dueTimeInMs.HasValue)
{
var delay = dueTimeInMs.Value - Now;
if (delay > 0)
_handler.PostDelayed(_timerSignaler, delay);
else
_handler.Post(_timerSignaler);
}
}
class IdleHandler : Object, MessageQueue.IIdleHandler
{
private readonly AndroidDispatcherImpl _parent;
public IdleHandler(AndroidDispatcherImpl parent)
{
_parent = parent;
}
public bool QueueIdle()
{
_parent.OnIdle();
return true;
}
}
public event Action? ReadyForBackgroundProcessing;
public void RequestBackgroundProcessing()
{
_backgroundProcessingRequested = true;
}
void OnIdle()
{
tailCall:
if (_backgroundProcessingRequested)
{
_backgroundProcessingRequested = false;
ReadyForBackgroundProcessing?.Invoke();
}
if (_backgroundProcessingRequested)
{
// Dispatcher requested background processing again, however if the queue is empty and we
// just return here, Android's Looper will go to sleep and won't call us again and we'll have
// "background" jobs not being processed
// So we need to examine the queue state to prevent that scenario
lock (_lock)
{
// There are higher priority jobs enqueued, we'll be called again
if (_signaled)
return;
}
if (CanQueryPendingInput)
{
if (!HasPendingInput)
// There are no events in the queue, so if we just return here, Looper will go to sleep,
// so just run our logic again
goto tailCall;
// Nothing to do otherwise, we'll be called again after higher priority events get processed
}
else
{
// On this API level we can't check if there is pending input,
// so we explicitly wake up the Looper to make sure that it will call idle hooks again
// before going to sleep
_handler.Post(_wakeupSignaler);
}
}
}
public bool CanQueryPendingInput { get; }
// See check in ctor
#pragma warning disable CA1416
public bool HasPendingInput => !_queue.IsIdle;
#pragma warning restore CA1416
}
}

3
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -12,6 +12,7 @@ using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.Vulkan;
namespace Avalonia
@ -84,7 +85,7 @@ namespace Avalonia.Android
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToSingleton<AndroidPlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
.Bind<IDispatcherImpl>().ToConstant(new AndroidDispatcherImpl())
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()

87
src/Android/Avalonia.Android/AndroidThreadingInterface.cs

@ -1,87 +0,0 @@
using System;
using System.Threading;
using Android.OS;
using Avalonia.Platform;
using Avalonia.Reactive;
using Avalonia.Threading;
using App = Android.App.Application;
namespace Avalonia.Android
{
internal sealed class AndroidThreadingInterface : IPlatformThreadingInterface
{
private Handler _handler;
private static Thread? s_uiThread;
public AndroidThreadingInterface()
{
_handler = new Handler(App.Context.MainLooper
?? throw new InvalidOperationException("Application.Context.MainLooper was not expected to be null."));
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
if (interval.TotalMilliseconds < 10)
interval = TimeSpan.FromMilliseconds(10);
var stopped = false;
Timer? timer = null;
timer = new Timer(_ =>
{
if (stopped)
return;
EnsureInvokeOnMainThread(() =>
{
try
{
tick();
}
finally
{
if (!stopped)
timer!.Change(interval, Timeout.InfiniteTimeSpan);
}
});
},
null, interval, Timeout.InfiniteTimeSpan);
return Disposable.Create(() =>
{
stopped = true;
timer.Dispose();
});
}
private void EnsureInvokeOnMainThread(Action action) => _handler.Post(action);
public void Signal(DispatcherPriority prio)
{
EnsureInvokeOnMainThread(() => Signaled?.Invoke(null));
}
public bool CurrentThreadIsLoopThread
{
get
{
if (s_uiThread != null)
return s_uiThread == Thread.CurrentThread;
var isOnMainThread = OperatingSystem.IsAndroidVersionAtLeast(23)
? Looper.MainLooper?.IsCurrentThread
: Looper.MainLooper?.Thread.Equals(Java.Lang.Thread.CurrentThread());
if (isOnMainThread == true)
{
s_uiThread = Thread.CurrentThread;
return true;
}
return false;
}
}
public event Action<DispatcherPriority?>? Signaled;
}
}

28
src/Android/Avalonia.Android/ApplicationLifetime.cs

@ -0,0 +1,28 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Logging;
namespace Avalonia.Android;
internal class ApplicationLifetime : IActivityApplicationLifetime, ISingleViewApplicationLifetime
{
private Control? _mainView;
public Func<Control>? MainViewFactory { get; set; }
public Control? MainView
{
get => _mainView; set
{
_mainView = value;
Logger.TryGet(LogEventLevel.Warning, LogArea.AndroidPlatform)?.Log(this, "ISingleViewApplicationLifetime.MainView is not fully supported on Android." +
" Consider setting IActivityApplicationLifetime.MainViewFactory.");
if (_mainView != null)
MainViewFactory = () => _mainView;
else
MainViewFactory = null;
}
}
}

5
src/Android/Avalonia.Android/AvaloniaActivity.cs

@ -8,10 +8,10 @@ using Android.OS;
using Android.Runtime;
using Android.Views;
using AndroidX.AppCompat.App;
using Avalonia.Platform;
using Avalonia.Android.Platform;
using Avalonia.Android.Platform.Storage;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
namespace Avalonia.Android;
@ -48,6 +48,9 @@ public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity
SetContentView(_view);
// By default, the view isn't focused if the activity is created anew, so we force focus.
_view.RequestFocus();
_listener = new GlobalLayoutListener(_view);
_view.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener);

6
src/Android/Avalonia.Android/AvaloniaAndroidApplication.cs

@ -9,13 +9,13 @@ namespace Avalonia.Android
{
internal interface IAndroidApplication
{
SingleViewLifetime? Lifetime { get; set; }
ApplicationLifetime? Lifetime { get; set; }
}
public class AvaloniaAndroidApplication<TApp> : global::Android.App.Application, IAndroidApplication
where TApp : Application, new()
{
SingleViewLifetime? IAndroidApplication.Lifetime { get; set; }
ApplicationLifetime? IAndroidApplication.Lifetime { get; set; }
protected AvaloniaAndroidApplication(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
@ -32,7 +32,7 @@ namespace Avalonia.Android
var builder = CreateAppBuilder();
builder = CustomizeAppBuilder(builder);
var lifetime = new SingleViewLifetime();
var lifetime = new ApplicationLifetime();
((IAndroidApplication)this).Lifetime = lifetime;

23
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@ -12,14 +12,20 @@ public class AvaloniaMainActivity : AvaloniaActivity
{
if (Application is IAndroidApplication application && application.Lifetime is { } lifetime)
{
initialContent ??= lifetime.MainView;
initialContent ??= lifetime.MainViewFactory?.Invoke();
_view = new AvaloniaView(this) { Content = initialContent };
lifetime.Activity = this;
_view = new AvaloniaView(this);
Content = initialContent;
}
if (_view is null)
throw new InvalidOperationException("Unknown error: AvaloniaView initialization has failed.");
}
protected override void OnResume()
{
base.OnResume();
if (Avalonia.Application.Current?.TryGetFeature<IActivatableLifetime>()
is AndroidActivatableLifetime activatableLifetime)
@ -28,6 +34,17 @@ public class AvaloniaMainActivity : AvaloniaActivity
}
}
protected override void OnDestroy()
{
base.OnDestroy();
if (Avalonia.Application.Current?.TryGetFeature<IActivatableLifetime>()
is AndroidActivatableLifetime activatableLifetime && activatableLifetime.CurrentMainActivity == this)
{
activatableLifetime.CurrentMainActivity = null;
}
}
protected virtual AppBuilder CreateAppBuilder() => AppBuilder.Configure<Application>().UseAndroid();
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder;
}

67
src/Android/Avalonia.Android/AvaloniaView.Input.cs

@ -0,0 +1,67 @@
using System;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
namespace Avalonia.Android
{
public partial class AvaloniaView : IInitEditorInfo
{
private Func<TopLevelImpl, EditorInfo, IInputConnection>? _initEditorInfo;
public override IInputConnection OnCreateInputConnection(EditorInfo? outAttrs)
{
return _initEditorInfo?.Invoke(_view, outAttrs!)!;
}
void IInitEditorInfo.InitEditorInfo(Func<TopLevelImpl, EditorInfo, IInputConnection> init)
{
_initEditorInfo = init;
}
protected override void OnFocusChanged(bool gainFocus, FocusSearchDirection direction, global::Android.Graphics.Rect? previouslyFocusedRect)
{
base.OnFocusChanged(gainFocus, direction, previouslyFocusedRect);
_accessHelper.OnFocusChanged(gainFocus, (int)direction, previouslyFocusedRect);
}
protected override bool DispatchHoverEvent(MotionEvent? e)
{
return _accessHelper.DispatchHoverEvent(e!) || base.DispatchHoverEvent(e);
}
protected override bool DispatchGenericPointerEvent(MotionEvent? e)
{
var result = _view.PointerHelper.DispatchMotionEvent(e, out var callBase);
var baseResult = callBase && base.DispatchGenericPointerEvent(e);
return result ?? baseResult;
}
public override bool DispatchTouchEvent(MotionEvent? e)
{
var result = _view.PointerHelper.DispatchMotionEvent(e, out var callBase);
var baseResult = callBase && base.DispatchTouchEvent(e);
if(result == true)
{
// Request focus for this view
RequestFocus();
}
return result ?? baseResult;
}
public override bool DispatchKeyEvent(KeyEvent? e)
{
var res = _view.KeyboardHelper.DispatchKeyEvent(e, out var callBase);
if (res == false)
callBase = !_accessHelper.DispatchKeyEvent(e!) && callBase;
var baseResult = callBase && base.DispatchKeyEvent(e);
return res ?? baseResult;
}
}
}

21
src/Android/Avalonia.Android/AvaloniaView.cs

@ -17,7 +17,7 @@ using Avalonia.Rendering;
namespace Avalonia.Android
{
public class AvaloniaView : FrameLayout
public partial class AvaloniaView : FrameLayout
{
private EmbeddableControlRoot _root;
private readonly ViewImpl _view;
@ -71,24 +71,6 @@ namespace Avalonia.Android
_root = null!;
}
protected override void OnFocusChanged(bool gainFocus, FocusSearchDirection direction, global::Android.Graphics.Rect? previouslyFocusedRect)
{
base.OnFocusChanged(gainFocus, direction, previouslyFocusedRect);
_accessHelper.OnFocusChanged(gainFocus, (int)direction, previouslyFocusedRect);
}
protected override bool DispatchHoverEvent(MotionEvent? e)
{
return _accessHelper.DispatchHoverEvent(e!) || base.DispatchHoverEvent(e);
}
public override bool DispatchKeyEvent(KeyEvent? e)
{
if (!_view.View.DispatchKeyEvent(e))
return _accessHelper.DispatchKeyEvent(e!) || base.DispatchKeyEvent(e);
return true;
}
[SupportedOSPlatform("android24.0")]
public override void OnVisibilityAggregated(bool isVisible)
{
@ -149,7 +131,6 @@ namespace Avalonia.Android
{
public ViewImpl(AvaloniaView avaloniaView) : base(avaloniaView)
{
View.Focusable = true;
View.FocusChange += ViewImpl_FocusChange;
}

90
src/Android/Avalonia.Android/ChoreographerTimer.cs

@ -1,33 +1,42 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Android.OS;
using Android.Views;
using Avalonia.Reactive;
using Avalonia.Rendering;
using static Avalonia.Android.Platform.SkiaPlatform.AndroidFramebuffer;
using Java.Lang;
using Looper = Android.OS.Looper;
namespace Avalonia.Android
{
internal sealed class ChoreographerTimer : Java.Lang.Object, IRenderTimer, Choreographer.IFrameCallback
internal sealed class ChoreographerTimer : IRenderTimer
{
private static readonly bool s_supports64Callback = OperatingSystem.IsAndroidVersionAtLeast(10);
private readonly object _lock = new();
private readonly Thread _thread;
private readonly TaskCompletionSource<Choreographer> _choreographer = new();
private readonly ISet<AvaloniaView> _views = new HashSet<AvaloniaView>();
private readonly TaskCompletionSource<IntPtr> _choreographer = new();
private readonly AutoResetEvent _event = new(false);
private readonly GCHandle _timerHandle;
private readonly HashSet<AvaloniaView> _views = new();
private Action<TimeSpan>? _tick;
private long _lastTime;
private int _count;
public ChoreographerTimer()
{
_thread = new Thread(Loop);
_thread.Start();
_timerHandle = GCHandle.Alloc(this);
new Thread(Loop)
{
Priority = ThreadPriority.AboveNormal,
Name = "Choreographer Thread"
}.Start();
new Thread(RenderLoop)
{
Priority = ThreadPriority.AboveNormal,
Name = "Render Thread"
}.Start();
}
public bool RunsInBackground => true;
@ -43,7 +52,7 @@ namespace Avalonia.Android
if (_count == 1)
{
_choreographer.Task.Result.PostFrameCallback(this);
PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle));
}
}
}
@ -65,7 +74,7 @@ namespace Avalonia.Android
if (_views.Count == 1)
{
_choreographer.Task.Result.PostFrameCallback(this);
PostFrameCallback(_choreographer.Task.Result, GCHandle.ToIntPtr(_timerHandle));
}
}
@ -83,20 +92,63 @@ namespace Avalonia.Android
private void Loop()
{
Looper.Prepare();
_choreographer.SetResult(Choreographer.Instance!);
_choreographer.SetResult(AChoreographer_getInstance());
Looper.Loop();
}
public void DoFrame(long frameTimeNanos)
private void RenderLoop()
{
_tick?.Invoke(TimeSpan.FromTicks(frameTimeNanos / 100));
while (true)
{
_event.WaitOne();
long time;
lock (_lock)
{
time = _lastTime;
}
_tick?.Invoke(TimeSpan.FromTicks(time / 100));
}
}
private void DoFrameCallback(long frameTimeNanos, IntPtr data)
{
lock (_lock)
{
if (_count > 0 && _views.Count > 0)
{
Choreographer.Instance!.PostFrameCallback(this);
PostFrameCallback(_choreographer.Task.Result, data);
}
_lastTime = frameTimeNanos;
_event.Set();
}
}
private static unsafe void PostFrameCallback(IntPtr choreographer, IntPtr data)
{
// AChoreographer_postFrameCallback is deprecated on 10.0+.
if (s_supports64Callback)
{
AChoreographer_postFrameCallback64(choreographer, &FrameCallback64, data);
}
else
{
AChoreographer_postFrameCallback(choreographer, &FrameCallback, data);
}
return;
[UnmanagedCallersOnly]
static void FrameCallback(int frameTimeNanos, IntPtr data)
{
var timer = (ChoreographerTimer)GCHandle.FromIntPtr(data).Target!;
timer.DoFrameCallback(frameTimeNanos, data);
}
[UnmanagedCallersOnly]
static void FrameCallback64(long frameTimeNanos, IntPtr data)
{
var timer = (ChoreographerTimer)GCHandle.FromIntPtr(data).Target!;
timer.DoFrameCallback(frameTimeNanos, data);
}
}
}

74
src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs

@ -19,7 +19,7 @@ namespace Avalonia.Android.Platform
{
private readonly Activity _activity;
private readonly TopLevelImpl _topLevel;
private bool _displayEdgeToEdge;
private bool _displaysEdgeToEdge;
private bool? _systemUiVisibility;
private SystemBarTheme? _statusBarTheme;
private bool? _isDefaultSystemBarLightTheme;
@ -27,7 +27,9 @@ namespace Avalonia.Android.Platform
private InputPaneState _state;
private Rect _previousRect;
private Insets? _previousImeInset;
private bool _displayEdgeToEdgePreference;
private readonly bool _usesLegacyLayouts;
private readonly bool _isDisplayEdgeToEdgeForced;
private AndroidWindow Window => _activity.Window ?? throw new InvalidOperationException("Activity.Window must be set.");
@ -50,29 +52,42 @@ namespace Avalonia.Android.Platform
}
}
public bool DisplayEdgeToEdge
public bool DisplayEdgeToEdgePreference
{
get => _displayEdgeToEdge;
get => _displayEdgeToEdgePreference;
set
{
_displayEdgeToEdge = value;
_displayEdgeToEdgePreference = value;
if (OperatingSystem.IsAndroidVersionAtLeast(28) && Window.Attributes is { } attributes)
{
attributes.LayoutInDisplayCutoutMode = value ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default;
}
UpdateDisplayEdgeToEgdeState();
}
}
WindowCompat.SetDecorFitsSystemWindows(Window, !value);
private void UpdateDisplayEdgeToEgdeState()
{
if (_isDisplayEdgeToEdgeForced)
{
_displaysEdgeToEdge = true;
return;
}
if (value)
{
Window.AddFlags(WindowManagerFlags.TranslucentStatus);
Window.AddFlags(WindowManagerFlags.TranslucentNavigation);
}
else
{
SystemBarColor = _systemBarColor;
}
_displaysEdgeToEdge = _displayEdgeToEdgePreference;
if (OperatingSystem.IsAndroidVersionAtLeast(28) && Window.Attributes is { } attributes)
{
attributes.LayoutInDisplayCutoutMode = _displayEdgeToEdgePreference ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default;
}
WindowCompat.SetDecorFitsSystemWindows(Window, !_displayEdgeToEdgePreference);
if (_displayEdgeToEdgePreference)
{
Window.AddFlags(WindowManagerFlags.TranslucentStatus);
Window.AddFlags(WindowManagerFlags.TranslucentNavigation);
}
else
{
SystemBarColor = _systemBarColor;
}
}
@ -81,6 +96,9 @@ namespace Avalonia.Android.Platform
_activity = activity;
_topLevel = topLevel;
// Better detection for target sdk and running api level. Apps can change their target sdk and bypass dotnet's fixed target sdk level.
_isDisplayEdgeToEdgeForced = _activity.ApplicationContext?.ApplicationInfo?.TargetSdkVersion >= (BuildVersionCodes)35 && Build.VERSION.SdkInt >= (BuildVersionCodes)35;
ViewCompat.SetOnApplyWindowInsetsListener(Window.DecorView, this);
if (Build.VERSION.SdkInt < BuildVersionCodes.R)
@ -89,7 +107,7 @@ namespace Avalonia.Android.Platform
_activity.Window?.DecorView.ViewTreeObserver?.AddOnGlobalLayoutListener(this);
}
DisplayEdgeToEdge = false;
DisplayEdgeToEdgePreference = false;
ViewCompat.SetWindowInsetsAnimationCallback(Window.DecorView, this);
}
@ -105,11 +123,11 @@ namespace Avalonia.Android.Platform
var renderScaling = _topLevel.RenderScaling;
var inset = insets.GetInsets(
_displayEdgeToEdge ?
DisplaysEdgeToEdge ?
WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() |
WindowInsetsCompat.Type.DisplayCutout() : 0);
return new Thickness(inset.Left / renderScaling,
return new Thickness(inset.Left / renderScaling,
inset.Top / renderScaling,
inset.Right / renderScaling,
inset.Bottom / renderScaling);
@ -264,7 +282,15 @@ namespace Avalonia.Android.Platform
{
_systemBarColor = value;
if (_systemBarColor is { } color && !_displayEdgeToEdge && _activity.Window != null)
if (_isDisplayEdgeToEdgeForced)
{
// Allow having fully transparent navbars when on api level 35
if (OperatingSystem.IsAndroidVersionAtLeast(35))
Window.NavigationBarContrastEnforced = _systemBarColor != Colors.Transparent;
return;
}
if (_systemBarColor is { } color && !_displaysEdgeToEdge && _activity.Window != null)
{
_activity.Window.ClearFlags(WindowManagerFlags.TranslucentStatus);
_activity.Window.ClearFlags(WindowManagerFlags.TranslucentNavigation);
@ -282,6 +308,10 @@ namespace Avalonia.Android.Platform
}
}
public bool DisplayEdgeToEdge { get => DisplaysEdgeToEdge; set => DisplayEdgeToEdgePreference = value; }
public bool DisplaysEdgeToEdge => _displaysEdgeToEdge;
internal void ApplyStatusBarState()
{
IsSystemBarVisible = _systemUiVisibility;

23
src/Android/Avalonia.Android/Platform/Input/AndroidInputMethod.cs

@ -44,9 +44,6 @@ namespace Avalonia.Android.Platform.Input
public AndroidInputMethod(TView host)
{
if (host.OnCheckIsTextEditor() == false)
throw new InvalidOperationException("Host should return true from OnCheckIsTextEditor()");
_host = host;
_imm = host.Context?.GetSystemService(Context.InputMethodService).JavaCast<InputMethodManager>()
?? throw new InvalidOperationException("Context.InputMethodService is expected to be not null.");
@ -72,6 +69,13 @@ namespace Avalonia.Android.Platform.Input
public void SetClient(TextInputMethodClient? client)
{
if(_client != null)
{
_client.SurroundingTextChanged -= _client_SurroundingTextChanged;
_client.SelectionChanged -= _client_SelectionChanged;
_client.InputPaneActivationRequested -= _client_InputPaneActivationRequested;
}
_client = client;
if (IsActive)
@ -86,13 +90,24 @@ namespace Avalonia.Android.Platform.Input
_client.SurroundingTextChanged += _client_SurroundingTextChanged;
_client.SelectionChanged += _client_SelectionChanged;
_client.InputPaneActivationRequested += _client_InputPaneActivationRequested;
}
else
{
_imm.RestartInput(View);
_inputConnection = null;
_imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.ImplicitOnly);
}
}
private void _client_InputPaneActivationRequested(object? sender, EventArgs e)
{
if(IsActive)
{
_imm.ShowSoftInput(_host, ShowFlags.Implicit);
}
}
private void _client_SelectionChanged(object? sender, EventArgs e)
{
if (_inputConnection is null || _inputConnection.IsInBatchEdit || _inputConnection.IsInUpdate)
@ -149,7 +164,9 @@ namespace Avalonia.Android.Platform.Input
_host.InitEditorInfo((topLevel, outAttrs) =>
{
if (_client == null)
{
return null!;
}
_inputConnection = new AvaloniaInputConnection(topLevel, this);

12
src/Android/Avalonia.Android/Platform/Input/AvaloniaInputConnection.cs

@ -285,14 +285,14 @@ namespace Avalonia.Android.Platform.Input
public ICharSequence? GetTextAfterCursorFormatted(int n, [GeneratedEnum] GetTextFlags flags)
{
var end = Math.Min(_editBuffer.Selection.End, _editBuffer.Text.Length);
return new Java.Lang.String(_editBuffer.Text.Substring(end, Math.Min(n, _editBuffer.Text.Length - end)));
return SafeSubstring(_editBuffer.Text, end, Math.Min(n, _editBuffer.Text.Length - end));
}
public ICharSequence? GetTextBeforeCursorFormatted(int n, [GeneratedEnum] GetTextFlags flags)
{
var start = Math.Max(0, _editBuffer.Selection.Start - n);
var length = _editBuffer.Selection.Start - start;
return _editBuffer.Text == null ? null : new Java.Lang.String(_editBuffer.Text.Substring(start, length));
return SafeSubstring(_editBuffer.Text, start, length);
}
public bool PerformPrivateCommand(string? action, Bundle? data)
@ -330,5 +330,13 @@ namespace Avalonia.Android.Platform.Input
EndBatchEdit();
}
}
private static ICharSequence? SafeSubstring(string? text, int start, int length)
{
if (text == null || text.Length < start + length)
return null;
else
return new Java.Lang.String(text.Substring(start, length));
}
}
}

18
src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs

@ -1,5 +1,6 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Android.Runtime;
using Android.Views;
using Avalonia.Platform;
@ -60,6 +61,19 @@ namespace Avalonia.Android.Platform.SkiaPlatform
[DllImport("android")]
internal static extern void ANativeWindow_unlockAndPost(IntPtr window);
[DllImport("android")]
internal static extern IntPtr AChoreographer_getInstance();
[DllImport("android")]
[UnsupportedOSPlatform("android10.0")]
internal static extern void AChoreographer_postFrameCallback(
IntPtr choreographer, delegate* unmanaged<int, IntPtr, void> callback, IntPtr data);
[DllImport("android")]
[SupportedOSPlatform("android10.0")]
internal static extern void AChoreographer_postFrameCallback64(
IntPtr choreographer, delegate* unmanaged<long, IntPtr, void> callback, IntPtr data);
[DllImport("android")]
internal static extern int ANativeWindow_lock(IntPtr window, ANativeWindow_Buffer* outBuffer, ARect* inOutDirtyBounds);
public enum AndroidPixelFormat
@ -69,6 +83,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
WINDOW_FORMAT_RGB_565 = 4,
}
[StructLayout(LayoutKind.Sequential)]
internal struct ARect
{
public int left;
@ -76,7 +91,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public int right;
public int bottom;
}
[StructLayout(LayoutKind.Sequential)]
internal struct ANativeWindow_Buffer
{
// The number of pixels that are shown horizontally.

118
src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs

@ -1,30 +1,30 @@
using System;
using System.Threading;
using Android.Content;
using Android.Graphics;
using Android.OS;
using Android.Runtime;
using Android.Util;
using Android.Views;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Logging;
using Avalonia.Platform;
using Java.Lang;
namespace Avalonia.Android
{
internal abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, INativePlatformHandleSurface
internal abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback2, INativePlatformHandleSurface
{
bool _invalidateQueued;
private bool _isDisposed;
private bool _isSurfaceValid;
readonly object _lock = new object();
private readonly Handler _handler;
private IntPtr _nativeWindowHandle = IntPtr.Zero;
private PixelSize _size = new(1, 1);
private double _scaling = 1;
internal event EventHandler? SurfaceWindowCreated;
public event EventHandler? SurfaceWindowCreated;
public PixelSize Size => _size;
public double Scaling => _scaling;
IntPtr IPlatformHandle.Handle => _isSurfaceValid && Holder?.Surface?.Handle is { } handle ?
AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, handle) :
default;
IntPtr IPlatformHandle.Handle => _nativeWindowHandle;
string IPlatformHandle.HandleDescriptor => "SurfaceView";
public InvalidationAwareSurfaceView(Context context) : base(context)
protected InvalidationAwareSurfaceView(Context context) : base(context)
{
if (Holder is null)
throw new InvalidOperationException(
@ -32,71 +32,77 @@ namespace Avalonia.Android
Holder.AddCallback(this);
Holder.SetFormat(global::Android.Graphics.Format.Transparent);
_handler = new Handler(context.MainLooper!);
}
public override void Invalidate()
protected override void Dispose(bool disposing)
{
lock (_lock)
{
if (_invalidateQueued)
return;
_handler.Post(() =>
{
if (_isDisposed || Holder?.Surface?.IsValid != true)
return;
try
{
DoDraw();
}
catch (Exception e)
{
Log.WriteLine(LogPriority.Error, "Avalonia", e.ToString());
}
});
}
}
internal new void Dispose()
{
_isDisposed = true;
ReleaseNativeWindowHandle();
base.Dispose(disposing);
}
public void SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
public virtual void SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
{
_isSurfaceValid = true;
Log.Info("AVALONIA", "Surface Changed");
DoDraw();
CacheSurfaceProperties(holder);
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "InvalidationAwareSurfaceView Changed");
}
public void SurfaceCreated(ISurfaceHolder holder)
{
_isSurfaceValid = true;
Log.Info("AVALONIA", "Surface Created");
CacheSurfaceProperties(holder);
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "InvalidationAwareSurfaceView Created");
SurfaceWindowCreated?.Invoke(this, EventArgs.Empty);
DoDraw();
}
public void SurfaceDestroyed(ISurfaceHolder holder)
{
_isSurfaceValid = false;
Log.Info("AVALONIA", "Surface Destroyed");
ReleaseNativeWindowHandle();
_size = new PixelSize(1, 1);
_scaling = 1;
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "InvalidationAwareSurfaceView Destroyed");
}
public virtual void SurfaceRedrawNeeded(ISurfaceHolder holder)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "InvalidationAwareSurfaceView RedrawNeeded");
}
public virtual void SurfaceRedrawNeededAsync(ISurfaceHolder holder, IRunnable drawingFinished)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "InvalidationAwareSurfaceView RedrawNeededAsync");
}
protected void DoDraw()
private void CacheSurfaceProperties(ISurfaceHolder holder)
{
lock (_lock)
var surface = holder?.Surface;
var newHandle = IntPtr.Zero;
if (surface?.Handle is { } handle)
{
_invalidateQueued = false;
newHandle = AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, handle);
}
Draw();
}
protected abstract void Draw();
public string HandleDescriptor => "SurfaceView";
public PixelSize Size => new(Holder?.SurfaceFrame?.Width() ?? 1, Holder?.SurfaceFrame?.Height() ?? 1);
if (Interlocked.Exchange(ref _nativeWindowHandle, newHandle) is var oldHandle
&& oldHandle != IntPtr.Zero)
{
AndroidFramebuffer.ANativeWindow_release(oldHandle);
}
var frame = holder?.SurfaceFrame;
_size = frame != null ? new PixelSize(frame.Width(), frame.Height()) : new PixelSize(1, 1);
_scaling = Resources?.DisplayMetrics?.Density ?? 1;
}
public double Scaling => Resources?.DisplayMetrics?.Density ?? 1;
private void ReleaseNativeWindowHandle()
{
if (Interlocked.Exchange(ref _nativeWindowHandle, IntPtr.Zero) is var oldHandle
&& oldHandle != IntPtr.Zero)
{
AndroidFramebuffer.ANativeWindow_release(oldHandle);
}
}
}
}

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

@ -5,9 +5,7 @@ using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using AndroidX.AppCompat.App;
using Avalonia.Android.Platform.Input;
using Avalonia.Android.Platform.Specific;
@ -15,13 +13,11 @@ using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Android.Platform.Storage;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering.Composition;
@ -30,14 +26,11 @@ using ClipboardManager = Android.Content.ClipboardManager;
namespace Avalonia.Android.Platform.SkiaPlatform
{
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy
{
private readonly IGlPlatformSurface _gl;
private readonly IFramebufferPlatformSurface _framebuffer;
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
private readonly AndroidMotionEventsHelper _pointerHelper;
private readonly AndroidInputMethod<ViewImpl> _textInputMethod;
private readonly AndroidInputMethod<AvaloniaView> _textInputMethod;
private readonly INativeControlHostImpl _nativeControlHost;
private readonly IStorageProvider? _storageProvider;
private readonly AndroidSystemNavigationManagerImpl _systemNavigationManager;
@ -45,7 +38,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly ClipboardImpl _clipboard;
private readonly AndroidLauncher? _launcher;
private readonly AndroidScreens? _screens;
private ViewImpl _view;
private SurfaceViewImpl _view;
private WindowTransparencyLevel _transparencyLevel;
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@ -55,17 +48,13 @@ namespace Avalonia.Android.Platform.SkiaPlatform
throw new ArgumentException("AvaloniaView.Context must not be null");
}
_view = new ViewImpl(avaloniaView.Context, this, placeOnTop);
_textInputMethod = new AndroidInputMethod<ViewImpl>(_view);
_view = new SurfaceViewImpl(avaloniaView.Context, this, placeOnTop);
_textInputMethod = new AndroidInputMethod<AvaloniaView>(avaloniaView);
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
_pointerHelper = new AndroidMotionEventsHelper(this);
_gl = new EglGlPlatformSurface(this);
_framebuffer = new FramebufferManager(this);
_clipboard = new ClipboardImpl(avaloniaView.Context.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>());
_screens = new AndroidScreens(avaloniaView.Context);
RenderScaling = _view.Scaling;
if (avaloniaView.Context is Activity mainActivity)
{
_insetsManager = new AndroidInsetsManager(mainActivity, this);
@ -78,15 +67,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_systemNavigationManager = new AndroidSystemNavigationManagerImpl(avaloniaView.Context as IActivityNavigationService);
Surfaces = new object[] { _gl, _framebuffer, _view };
var gl = new EglGlPlatformSurface(this);
var framebuffer = new FramebufferManager(this);
Surfaces = [gl, framebuffer, _view];
Handle = new AndroidViewControlHandle(_view);
}
public IInputRoot? InputRoot { get; private set; }
public virtual Size ClientSize => _view.Size.ToSize(RenderScaling);
public Size? FrameSize => null;
public Size ClientSize => _view.Size.ToSize(RenderScaling);
public double RenderScaling => _view.Scaling;
public Action? Closed { get; set; }
@ -110,16 +100,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public Compositor Compositor => AndroidPlatform.Compositor ??
throw new InvalidOperationException("Android backend wasn't initialized. Make sure .UseAndroid() was executed.");
public virtual void Hide()
{
_view.Visibility = ViewStates.Invisible;
}
public void Invalidate(Rect rect)
{
if (_view.Holder?.Surface?.IsValid == true) _view.Invalidate();
}
public Point PointToClient(PixelPoint point)
{
return point.ToPoint(RenderScaling);
@ -140,18 +120,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
InputRoot = inputRoot;
}
public virtual void Show()
{
_view.Visibility = ViewStates.Visible;
}
public double RenderScaling { get; }
void Draw()
{
Paint?.Invoke(new Rect(new Point(0, 0), ClientSize));
}
public virtual void Dispose()
{
_systemNavigationManager.Dispose();
@ -159,7 +127,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view = null!;
}
protected virtual void OnResized(Size size)
protected void OnResized(Size size)
{
Resized?.Invoke(size, WindowResizeReason.Unspecified);
}
@ -169,25 +137,19 @@ namespace Avalonia.Android.Platform.SkiaPlatform
Resized?.Invoke(size, WindowResizeReason.Layout);
}
class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo
sealed class SurfaceViewImpl : InvalidationAwareSurfaceView
{
private readonly TopLevelImpl _tl;
private Size _oldSize;
private double _oldScaling;
public ViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context)
public SurfaceViewImpl(Context context, TopLevelImpl tl, bool placeOnTop) : base(context)
{
_tl = tl;
if (placeOnTop)
SetZOrderOnTop(true);
}
public TopLevelImpl TopLevelImpl => _tl;
protected override void Draw()
{
_tl.Draw();
}
protected override void DispatchDraw(global::Android.Graphics.Canvas canvas)
{
// Workaround issue #9230 on where screen remains gray after splash screen.
@ -210,60 +172,38 @@ namespace Avalonia.Android.Platform.SkiaPlatform
base.DispatchDraw(canvas);
}
protected override bool DispatchGenericPointerEvent(MotionEvent? e)
{
var result = _tl._pointerHelper.DispatchMotionEvent(e, out var callBase);
var baseResult = callBase && base.DispatchGenericPointerEvent(e);
return result ?? baseResult;
}
public override bool DispatchTouchEvent(MotionEvent? e)
public override void SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
{
var result = _tl._pointerHelper.DispatchMotionEvent(e, out var callBase);
var baseResult = callBase && base.DispatchTouchEvent(e);
return result ?? baseResult;
}
public override bool DispatchKeyEvent(KeyEvent? e)
{
var res = _tl._keyboardHelper.DispatchKeyEvent(e, out var callBase);
var baseResult = callBase && base.DispatchKeyEvent(e);
return res ?? baseResult;
}
base.SurfaceChanged(holder, format, width, height);
void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
{
var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling);
var newSize = Size.ToSize(Scaling);
var newScaling = Scaling;
if (newSize != _oldSize)
{
_oldSize = newSize;
_tl.OnResized(newSize);
}
base.SurfaceChanged(holder, format, width, height);
}
public sealed override bool OnCheckIsTextEditor()
{
return true;
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (newScaling != _oldScaling)
{
_oldScaling = newScaling;
_tl.ScalingChanged?.Invoke(newScaling);
}
}
private Func<TopLevelImpl, EditorInfo, IInputConnection>? _initEditorInfo;
public void InitEditorInfo(Func<TopLevelImpl, EditorInfo, IInputConnection> init)
public override void SurfaceRedrawNeeded(ISurfaceHolder holder)
{
_initEditorInfo = init;
// Compositor Renderer handles Paint event in-sync, which is perfect for sync SurfaceRedrawNeeded
_tl.Paint?.Invoke(new Rect(new Point(), Size.ToSize(Scaling)));
base.SurfaceRedrawNeeded(holder);
}
public sealed override IInputConnection OnCreateInputConnection(EditorInfo? outAttrs)
public override void SurfaceRedrawNeededAsync(ISurfaceHolder holder, IRunnable drawingFinished)
{
return _initEditorInfo?.Invoke(_tl, outAttrs!)!;
_tl.Compositor.RequestCompositionUpdate(drawingFinished.Run);
base.SurfaceRedrawNeededAsync(holder, drawingFinished);
}
}
public IPopupImpl? CreatePopup() => null;
@ -302,10 +242,13 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle;
bool EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy.SkipWaits => true;
PixelSize EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Size => _view.Size;
double EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Scaling => _view.Scaling;
public PixelSize Size => _view.Size;
internal AndroidKeyboardEventsHelper<TopLevelImpl> KeyboardHelper => _keyboardHelper;
public double Scaling => RenderScaling;
internal AndroidMotionEventsHelper PointerHelper => _pointerHelper;
public void SetTransparencyLevelHint(IReadOnlyList<WindowTransparencyLevel> transparencyLevels)
{

2
src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs

@ -1,9 +1,11 @@
using System;
using Android.Views;
namespace Avalonia.Android.Platform.Specific
{
public interface IAndroidView
{
[Obsolete("Use TopLevel.TryGetPlatformHandle instead, which can be casted to AndroidViewControlHandle.")]
View View { get; }
}
}

46
src/Android/Avalonia.Android/SingleViewLifetime.cs

@ -1,46 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Android;
internal class SingleViewLifetime : ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime
{
private Control? _mainView;
private AvaloniaMainActivity? _activity;
/// <summary>
/// Since Main Activity can be swapped, we should adjust lifetime as well.
/// </summary>
public AvaloniaMainActivity Activity
{
[return: MaybeNull] get => _activity!;
internal set
{
if (_activity != null)
{
_activity.Content = null;
}
_activity = value;
_activity.Content = _mainView;
}
}
public Control? MainView
{
get => _mainView;
set
{
if (_mainView != value)
{
_mainView = value;
if (_activity != null)
{
_activity.Content = _mainView;
}
}
}
}
public TopLevel? TopLevel => _activity?._view?.TopLevel;
}

2
src/Avalonia.Base/Animation/Animators/BoolAnimator.cs

@ -10,8 +10,6 @@
{
if(progress >= 1d)
return newValue;
if(progress >= 0)
return oldValue;
return oldValue;
}
}

12
src/Avalonia.Base/Animation/TransitionInstance.cs

@ -33,9 +33,17 @@ namespace Avalonia.Animation
// ^- normalizedDelayEnd
// [<---- normalizedInterpVal --->]
var normalizedInterpVal = 1d;
double normalizedInterpVal;
if (!MathUtilities.AreClose(_duration.TotalSeconds, 0d))
if (t < _delay)
{
normalizedInterpVal = 0d;
}
else if (MathUtilities.AreClose(_duration.TotalSeconds, 0d))
{
normalizedInterpVal = 1d;
}
else
{
var normalizedTotalDur = _delay + _duration;
var normalizedDelayEnd = _delay.TotalSeconds / normalizedTotalDur.TotalSeconds;

3
src/Avalonia.Base/Collections/AvaloniaDictionary.cs

@ -167,9 +167,8 @@ namespace Avalonia.Collections
/// <inheritdoc/>
public bool Remove(TKey key)
{
if (_inner.TryGetValue(key, out var value))
if (_inner.Remove(key, out var value))
{
_inner.Remove(key);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"{CommonPropertyNames.IndexerName}[{key}]"));

3
src/Avalonia.Base/Controls/NameScope.cs

@ -68,9 +68,8 @@ namespace Avalonia.Controls
else
{
_inner.Add(name, element);
if (_pendingSearches.TryGetValue(name, out var tcs))
if (_pendingSearches.Remove(name, out var tcs))
{
_pendingSearches.Remove(name);
tcs.SetResult(element);
}
}

91
src/Avalonia.Base/Data/Core/PropertyPath.cs

@ -1,91 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Data.Core
{
public class PropertyPath
{
public IReadOnlyList<IPropertyPathElement> Elements { get; }
public PropertyPath(IEnumerable<IPropertyPathElement> elements)
{
Elements = elements.ToArray();
}
}
public class PropertyPathBuilder
{
readonly List<IPropertyPathElement> _elements = new List<IPropertyPathElement>();
public PropertyPathBuilder Property(IPropertyInfo property)
{
_elements.Add(new PropertyPropertyPathElement(property));
return this;
}
public PropertyPathBuilder ChildTraversal()
{
_elements.Add(new ChildTraversalPropertyPathElement());
return this;
}
public PropertyPathBuilder EnsureType(Type type)
{
_elements.Add(new EnsureTypePropertyPathElement(type));
return this;
}
public PropertyPathBuilder Cast(Type type)
{
_elements.Add(new CastTypePropertyPathElement(type));
return this;
}
public PropertyPath Build()
{
return new PropertyPath(_elements);
}
}
public interface IPropertyPathElement
{
}
public class PropertyPropertyPathElement : IPropertyPathElement
{
public IPropertyInfo Property { get; }
public PropertyPropertyPathElement(IPropertyInfo property)
{
Property = property;
}
}
public class ChildTraversalPropertyPathElement : IPropertyPathElement
{
}
public class EnsureTypePropertyPathElement : IPropertyPathElement
{
public Type Type { get; }
public EnsureTypePropertyPathElement(Type type)
{
Type = type;
}
}
public class CastTypePropertyPathElement : IPropertyPathElement
{
public CastTypePropertyPathElement(Type type)
{
Type = type;
}
public Type Type { get; }
}
}

70
src/Avalonia.Base/Input/FocusChangingEventArgs.cs

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Interactivity;
namespace Avalonia.Input
{
public class FocusChangingEventArgs : RoutedEventArgs
{
/// <summary>
/// Provides data for focus changing.
/// </summary>
internal FocusChangingEventArgs(RoutedEvent routedEvent) : base(routedEvent)
{
}
/// <summary>
/// Gets or sets the element that focus has moved to.
/// </summary>
public IInputElement? NewFocusedElement { get; internal set; }
/// <summary>
/// Gets or sets the element that previously had focus.
/// </summary>
public IInputElement? OldFocusedElement { get; init; }
/// <summary>
/// Gets or sets a value indicating how the change in focus occurred.
/// </summary>
public NavigationMethod NavigationMethod { get; init; }
/// <summary>
/// Gets or sets any key modifiers active at the time of focus.
/// </summary>
public KeyModifiers KeyModifiers { get; init; }
/// <summary>
/// Gets whether focus change is canceled.
/// </summary>
public bool Canceled { get; private set; }
internal bool CanCancelOrRedirectFocus { get; init; }
/// <summary>
/// Attempts to cancel the current focus change
/// </summary>
/// <returns>true if focus change was cancelled; otherwise, false</returns>
public bool TryCancel()
{
Canceled = CanCancelOrRedirectFocus;
return Canceled;
}
/// <summary>
/// Attempts to redirect focus from the targeted element to the specified element.
/// </summary>
public bool TrySetNewFocusedElement(IInputElement? inputElement)
{
if(CanCancelOrRedirectFocus)
{
NewFocusedElement = inputElement;
}
return inputElement == NewFocusedElement;
}
}
}

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

@ -78,7 +78,7 @@ namespace Avalonia.Input
else
{
_focusRoot = null;
keyboardDevice.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
keyboardDevice.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None, false);
return false;
}
}

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

@ -1,5 +1,7 @@
using System;
using System.Diagnostics;
using System.Security.Cryptography;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
@ -20,19 +22,23 @@ namespace Avalonia.Input.GestureRecognizers
private bool _scrolling;
private Point _trackedRootPoint;
private IPointer? _tracking;
private Stopwatch? _stopWatch;
private int _gestureId;
private Point _pointerPressedPoint;
private VelocityTracker? _velocityTracker;
// Movement per second
private Vector _inertia;
private Vector? _inertia;
private ulong? _lastMoveTimestamp;
private TimeSpan _lastTime;
private TimeSpan _inertiaStartTime;
private int _currentInertiaGestureId;
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll),
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll),
o => o.CanHorizontallyScroll, (o, v) => o.CanHorizontallyScroll = v);
/// <summary>
@ -47,7 +53,7 @@ namespace Avalonia.Input.GestureRecognizers
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> IsScrollInertiaEnabledProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(nameof(IsScrollInertiaEnabled),
o => o.IsScrollInertiaEnabled, (o,v) => o.IsScrollInertiaEnabled = v);
o => o.IsScrollInertiaEnabled, (o, v) => o.IsScrollInertiaEnabled = v);
/// <summary>
/// Defines the <see cref="ScrollStartDistance"/> property.
@ -102,6 +108,7 @@ namespace Avalonia.Input.GestureRecognizers
{
EndGesture();
_tracking = e.Pointer;
_inertia = null;
_gestureId = ScrollGestureEventArgs.GetNextFreeId();
_trackedRootPoint = _pointerPressedPoint = point.Position;
_velocityTracker = new VelocityTracker();
@ -121,7 +128,7 @@ namespace Avalonia.Input.GestureRecognizers
if (CanVerticallyScroll && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > ScrollStartDistance)
_scrolling = true;
if (_scrolling)
{
{
// Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance
_trackedRootPoint = new Point(
_trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? ScrollStartDistance : -ScrollStartDistance),
@ -147,7 +154,8 @@ namespace Avalonia.Input.GestureRecognizers
protected override void PointerCaptureLost(IPointer pointer)
{
if (pointer == _tracking) EndGesture();
if (pointer == _tracking)
EndGesture();
}
void EndGesture()
@ -155,15 +163,16 @@ namespace Avalonia.Input.GestureRecognizers
_tracking = null;
if (_scrolling)
{
_stopWatch?.Stop();
_stopWatch = null;
_inertia = default;
_scrolling = false;
Target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
_gestureId = 0;
_lastMoveTimestamp = null;
}
}
}
protected override void PointerReleased(PointerReleasedEventArgs e)
{
@ -172,7 +181,8 @@ namespace Avalonia.Input.GestureRecognizers
_inertia = _velocityTracker?.GetFlingVelocity().PixelsPerSecond ?? Vector.Zero;
e.Handled = true;
if (_inertia == default
if (_inertia == null
|| _inertia == Vector.Zero
|| e.Timestamp == 0
|| _lastMoveTimestamp == 0
|| e.Timestamp - _lastMoveTimestamp > 200
@ -181,53 +191,63 @@ namespace Avalonia.Input.GestureRecognizers
else
{
_tracking = null;
var savedGestureId = _gestureId;
var st = Stopwatch.StartNew();
var lastTime = TimeSpan.Zero;
Target!.RaiseEvent(new ScrollGestureInertiaStartingEventArgs(_gestureId, _inertia));
DispatcherTimer.Run(() =>
{
// Another gesture has started, finish the current one
if (_gestureId != savedGestureId)
{
return false;
}
var elapsedSinceLastTick = st.Elapsed - lastTime;
lastTime = st.Elapsed;
var speed = _inertia * Math.Pow(InertialResistance, st.Elapsed.TotalSeconds);
var distance = speed * elapsedSinceLastTick.TotalSeconds;
var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance);
Target!.RaiseEvent(scrollGestureEventArgs);
if (!scrollGestureEventArgs.Handled || scrollGestureEventArgs.ShouldEndScrollGesture)
{
EndGesture();
return false;
}
// EndGesture using InertialScrollSpeedEnd only in the direction of scrolling
if (CanVerticallyScroll && CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
{
EndGesture();
return false;
}
else if (CanVerticallyScroll && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
{
EndGesture();
return false;
}
else if (CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd)
{
EndGesture();
return false;
}
return true;
}, TimeSpan.FromMilliseconds(16), DispatcherPriority.Background);
_stopWatch = Stopwatch.StartNew();
_lastTime = _stopWatch.Elapsed;
_inertiaStartTime = _lastTime;
_currentInertiaGestureId = _gestureId;
Target!.RaiseEvent(new ScrollGestureInertiaStartingEventArgs(_gestureId, _inertia.Value));
MediaContext.Instance.RequestAnimationFrame(OnAnimationRequested);
}
}
}
private void OnAnimationRequested(TimeSpan _)
{
// Calculate the current speed and dispatch the next inertia event. This is done asynchronously so we have run the events
// with Input priority
Dispatcher.UIThread.InvokeAsync(() =>
{
// Another gesture has started, finish the current one
if (_gestureId != _currentInertiaGestureId || _stopWatch == null || _inertia is not Vector inertia)
{
return;
}
var timeSpan = _stopWatch.Elapsed;
var elapsedSinceLastTick = timeSpan - _lastTime;
_lastTime = timeSpan;
var speed = inertia * Math.Pow(InertialResistance, (_lastTime - _inertiaStartTime).TotalSeconds);
var distance = speed * elapsedSinceLastTick.TotalSeconds;
var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance);
Target!.RaiseEvent(scrollGestureEventArgs);
if (!scrollGestureEventArgs.Handled || scrollGestureEventArgs.ShouldEndScrollGesture)
{
EndGesture();
return;
}
// EndGesture using InertialScrollSpeedEnd only in the direction of scrolling
if (CanVerticallyScroll && CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
{
// NO-OP
}
else if (CanVerticallyScroll && Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
{
EndGesture();
return;
}
else if (CanHorizontallyScroll && Math.Abs(speed.X) < InertialScrollSpeedEnd)
{
EndGesture();
return;
}
// Reschedule on the next animation frame. TopLevel.RequestAnimationFrame isn't available on the Base project, so we use the global MediaContext
MediaContext.Instance.RequestAnimationFrame(OnAnimationRequested);
}, DispatcherPriority.Input);
}
}
}

34
src/Avalonia.Base/Input/Gestures.cs

@ -218,9 +218,16 @@ namespace Avalonia.Input
public static void RemoveScrollGestureInertiaStartingHandler(Interactive element, EventHandler<ScrollGestureInertiaStartingEventArgs> handler) =>
element.RemoveHandler(ScrollGestureInertiaStartingEvent, handler);
private static object? GetCaptured(RoutedEventArgs? args)
{
if (args is not PointerEventArgs pointerEventArgs)
return null;
return pointerEventArgs.Pointer?.Captured ?? pointerEventArgs.Source;
}
private static void PointerPressed(RoutedEventArgs ev)
{
if (ev.Source is null)
if (GetCaptured(ev) is not { } source)
{
return;
}
@ -228,11 +235,11 @@ namespace Avalonia.Input
if (ev.Route == RoutingStrategies.Bubble)
{
var e = (PointerPressedEventArgs)ev;
var visual = (Visual)ev.Source;
var visual = (Visual)source;
if(s_gestureState != null)
{
if(s_gestureState.Value.Type == GestureStateType.Holding && ev.Source is Interactive i)
if(s_gestureState.Value.Type == GestureStateType.Holding && source is Interactive i)
{
i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Cancelled, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e));
}
@ -246,8 +253,8 @@ namespace Avalonia.Input
if (e.ClickCount % 2 == 1)
{
s_gestureState = new GestureState(GestureStateType.Pending, e.Pointer);
s_lastPress.SetTarget(ev.Source);
s_lastPressPoint = e.GetPosition((Visual)ev.Source);
s_lastPress.SetTarget(source);
s_lastPressPoint = e.GetPosition((Visual)source);
s_holdCancellationToken = new CancellationTokenSource();
var token = s_holdCancellationToken.Token;
var settings = ((IInputRoot?)visual.GetVisualRoot())?.PlatformSettings;
@ -256,7 +263,7 @@ namespace Avalonia.Input
{
DispatcherTimer.RunOnce(() =>
{
if (s_gestureState != null && !token.IsCancellationRequested && e.Source is InputElement i && GetIsHoldingEnabled(i) && (e.Pointer.Type != PointerType.Mouse || GetIsHoldWithMouseEnabled(i)))
if (s_gestureState != null && !token.IsCancellationRequested && source is InputElement i && GetIsHoldingEnabled(i) && (e.Pointer.Type != PointerType.Mouse || GetIsHoldWithMouseEnabled(i)))
{
s_gestureState = new GestureState(GestureStateType.Holding, s_gestureState.Value.Pointer);
i.RaiseEvent(new HoldingRoutedEventArgs(HoldingState.Started, s_lastPressPoint, s_gestureState.Value.Pointer.Type, e));
@ -267,8 +274,8 @@ namespace Avalonia.Input
else if (e.ClickCount % 2 == 0 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
{
if (s_lastPress.TryGetTarget(out var target) &&
target == e.Source &&
e.Source is Interactive i)
target == source &&
source is Interactive i)
{
s_gestureState = new GestureState(GestureStateType.DoubleTapped, e.Pointer);
i.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e));
@ -283,10 +290,12 @@ namespace Avalonia.Input
{
var e = (PointerReleasedEventArgs)ev;
var source = GetCaptured(ev);
if (s_lastPress.TryGetTarget(out var target) &&
target == e.Source &&
e.InitialPressMouseButton is MouseButton.Left or MouseButton.Right &&
e.Source is Interactive i)
target == source &&
e.InitialPressMouseButton is MouseButton.Left or MouseButton.Right &&
source is Interactive i)
{
var point = e.GetCurrentPoint((Visual)target);
var settings = ((IInputRoot?)i.GetVisualRoot())?.PlatformSettings;
@ -325,9 +334,10 @@ namespace Avalonia.Input
if (ev.Route == RoutingStrategies.Bubble)
{
var e = (PointerEventArgs)ev;
var source = GetCaptured(e);
if (s_lastPress.TryGetTarget(out var target))
{
if (e.Pointer == s_gestureState?.Pointer && ev.Source is Interactive i)
if (e.Pointer == s_gestureState?.Pointer && source is Interactive i)
{
var point = e.GetCurrentPoint((Visual)target);
var settings = ((IInputRoot?)i.GetVisualRoot())?.PlatformSettings;

103
src/Avalonia.Base/Input/InputElement.cs

@ -82,12 +82,24 @@ namespace Avalonia.Input
public static readonly RoutedEvent<GotFocusEventArgs> GotFocusEvent =
RoutedEvent.Register<InputElement, GotFocusEventArgs>(nameof(GotFocus), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="GettingFocus"/> event.
/// </summary>
public static readonly RoutedEvent<FocusChangingEventArgs> GettingFocusEvent =
RoutedEvent.Register<InputElement, FocusChangingEventArgs>(nameof(GettingFocus), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="LostFocus"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> LostFocusEvent =
RoutedEvent.Register<InputElement, RoutedEventArgs>(nameof(LostFocus), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="LosingFocus"/> event.
/// </summary>
public static readonly RoutedEvent<FocusChangingEventArgs> LosingFocusEvent =
RoutedEvent.Register<InputElement, FocusChangingEventArgs>(nameof(LosingFocus), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="KeyDown"/> event.
/// </summary>
@ -187,6 +199,11 @@ namespace Avalonia.Input
/// </summary>
public static readonly RoutedEvent<TappedEventArgs> TappedEvent = Gestures.TappedEvent;
/// <summary>
/// Defines the <see cref="RightTapped"/> event.
/// </summary>
public static readonly RoutedEvent<TappedEventArgs> RightTappedEvent = Gestures.RightTappedEvent;
/// <summary>
/// Defines the <see cref="Holding"/> event.
/// </summary>
@ -213,6 +230,8 @@ namespace Avalonia.Input
GotFocusEvent.AddClassHandler<InputElement>((x, e) => x.OnGotFocusCore(e));
LostFocusEvent.AddClassHandler<InputElement>((x, e) => x.OnLostFocusCore(e));
GettingFocusEvent.AddClassHandler<InputElement>((x, e) => x.OnGettingFocus(e));
LosingFocusEvent.AddClassHandler<InputElement>((x, e) => x.OnLosingFocus(e));
KeyDownEvent.AddClassHandler<InputElement>((x, e) => x.OnKeyDown(e));
KeyUpEvent.AddClassHandler<InputElement>((x, e) => x.OnKeyUp(e));
TextInputEvent.AddClassHandler<InputElement>((x, e) => x.OnTextInput(e));
@ -224,6 +243,11 @@ namespace Avalonia.Input
PointerCaptureLostEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerCaptureLost(e));
PointerWheelChangedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerWheelChanged(e));
TappedEvent.AddClassHandler<InputElement>((x, e) => x.OnTapped(e));
RightTappedEvent.AddClassHandler<InputElement>((x, e) => x.OnRightTapped(e));
DoubleTappedEvent.AddClassHandler<InputElement>((x, e) => x.OnDoubleTapped(e));
HoldingEvent.AddClassHandler<InputElement>((x, e) => x.OnHolding(e));
// Gesture only handlers
PointerMovedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerMoved(e), handledEventsToo: true);
PointerPressedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerPressed(e), handledEventsToo: true);
@ -249,6 +273,15 @@ namespace Avalonia.Input
remove { RemoveHandler(GotFocusEvent, value); }
}
/// <summary>
/// Occurs before the control receives focus.
/// </summary>
public event EventHandler<FocusChangingEventArgs>? GettingFocus
{
add { AddHandler(GettingFocusEvent, value); }
remove { RemoveHandler(GettingFocusEvent, value); }
}
/// <summary>
/// Occurs when the control loses focus.
/// </summary>
@ -258,6 +291,15 @@ namespace Avalonia.Input
remove { RemoveHandler(LostFocusEvent, value); }
}
/// <summary>
/// Occurs before the control loses focus.
/// </summary>
public event EventHandler<FocusChangingEventArgs>? LosingFocus
{
add { AddHandler(LosingFocusEvent, value); }
remove { RemoveHandler(LosingFocusEvent, value); }
}
/// <summary>
/// Occurs when a key is pressed while the control has focus.
/// </summary>
@ -367,6 +409,15 @@ namespace Avalonia.Input
remove { RemoveHandler(TappedEvent, value); }
}
/// <summary>
/// Occurs when a right tap gesture occurs on the control.
/// </summary>
public event EventHandler<TappedEventArgs>? RightTapped
{
add { AddHandler(RightTappedEvent, value); }
remove { RemoveHandler(RightTappedEvent, value); }
}
/// <summary>
/// Occurs when a hold gesture occurs on the control.
/// </summary>
@ -515,6 +566,8 @@ namespace Avalonia.Input
{
FocusManager.GetFocusManager(this)?.ClearFocusOnElementRemoved(this, e.Parent);
}
IsKeyboardFocusWithin = false;
}
/// <summary>
@ -543,6 +596,16 @@ namespace Avalonia.Input
OnGotFocus(e);
}
protected virtual void OnGettingFocus(FocusChangingEventArgs e)
{
}
protected virtual void OnLosingFocus(FocusChangingEventArgs e)
{
}
/// <summary>
/// Invoked when an unhandled <see cref="GotFocusEvent"/> reaches an element in its
/// route that is derived from this class. Implement this method to add class handling
@ -704,6 +767,46 @@ namespace Avalonia.Input
{
}
/// <summary>
/// Invoked when an unhandled <see cref="TappedEvent"/> reaches an element in its
/// route that is derived from this class. Implement this method to add class handling
/// for this event.
/// </summary>
/// <param name="e">Data about the event.</param>
protected virtual void OnTapped(TappedEventArgs e)
{
}
/// <summary>
/// Invoked when an unhandled <see cref="RightTappedEvent"/> reaches an element in its
/// route that is derived from this class. Implement this method to add class handling
/// for this event.
/// </summary>
/// <param name="e">Data about the event.</param>
protected virtual void OnRightTapped(TappedEventArgs e)
{
}
/// <summary>
/// Invoked when an unhandled <see cref="DoubleTappedEvent"/> reaches an element in its
/// route that is derived from this class. Implement this method to add class handling
/// for this event.
/// </summary>
/// <param name="e">Data about the event.</param>
protected virtual void OnDoubleTapped(TappedEventArgs e)
{
}
/// <summary>
/// Invoked when an unhandled <see cref="HoldingEvent"/> reaches an element in its
/// route that is derived from this class. Implement this method to add class handling
/// for this event.
/// </summary>
/// <param name="e">Data about the event.</param>
protected virtual void OnHolding(HoldingRoutedEventArgs e)
{
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);

88
src/Avalonia.Base/Input/KeyboardDevice.cs

@ -129,41 +129,89 @@ namespace Avalonia.Input
}
public void SetFocusedElement(
IInputElement? element,
IInputElement? element,
NavigationMethod method,
KeyModifiers keyModifiers)
{
SetFocusedElement(element, method, keyModifiers, true);
}
public void SetFocusedElement(
IInputElement? element,
NavigationMethod method,
KeyModifiers keyModifiers,
bool isFocusChangeCancellable)
{
if (element != FocusedElement)
{
var interactive = FocusedElement as Interactive;
if (FocusedElement != null &&
(!((Visual)FocusedElement).IsAttachedToVisualTree ||
_focusedRoot != ((Visual?)element)?.VisualRoot as IInputRoot) &&
_focusedRoot != null)
bool changeFocus = true;
var losingFocus = new FocusChangingEventArgs(InputElement.LosingFocusEvent)
{
ClearChildrenFocusWithin(_focusedRoot, true);
OldFocusedElement = FocusedElement,
NewFocusedElement = element,
NavigationMethod = method,
KeyModifiers = keyModifiers,
CanCancelOrRedirectFocus = isFocusChangeCancellable
};
interactive?.RaiseEvent(losingFocus);
if (losingFocus.Canceled)
{
changeFocus = false;
}
SetIsFocusWithin(FocusedElement, element);
_focusedElement = element;
_focusedRoot = ((Visual?)_focusedElement)?.VisualRoot as IInputRoot;
interactive?.RaiseEvent(new RoutedEventArgs
if (changeFocus && losingFocus.NewFocusedElement is Interactive newFocus)
{
RoutedEvent = InputElement.LostFocusEvent,
});
var gettingFocus = new FocusChangingEventArgs(InputElement.GettingFocusEvent)
{
OldFocusedElement = FocusedElement,
NewFocusedElement = losingFocus.NewFocusedElement,
NavigationMethod = method,
KeyModifiers = keyModifiers,
CanCancelOrRedirectFocus = isFocusChangeCancellable
};
interactive = element as Interactive;
newFocus.RaiseEvent(gettingFocus);
interactive?.RaiseEvent(new GotFocusEventArgs
if (gettingFocus.Canceled)
{
changeFocus = false;
}
element = gettingFocus.NewFocusedElement;
}
if (changeFocus)
{
NavigationMethod = method,
KeyModifiers = keyModifiers,
});
// Clear keyboard focus from currently focused element
if (FocusedElement != null &&
(!((Visual)FocusedElement).IsAttachedToVisualTree ||
_focusedRoot != ((Visual?)element)?.VisualRoot as IInputRoot) &&
_focusedRoot != null)
{
ClearChildrenFocusWithin(_focusedRoot, true);
}
SetIsFocusWithin(FocusedElement, element);
_focusedElement = element;
_focusedRoot = ((Visual?)_focusedElement)?.VisualRoot as IInputRoot;
interactive?.RaiseEvent(new RoutedEventArgs(InputElement.LostFocusEvent));
_textInputManager.SetFocusedElement(element);
RaisePropertyChanged(nameof(FocusedElement));
(element as Interactive)?.RaiseEvent(new GotFocusEventArgs
{
NavigationMethod = method,
KeyModifiers = keyModifiers,
});
_textInputManager.SetFocusedElement(element);
RaisePropertyChanged(nameof(FocusedElement));
}
}
}

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

@ -143,9 +143,9 @@ namespace Avalonia.Input
}
/// <summary>
/// Returns the current pointer point properties
/// Gets the state the pointer device had when this event occurred.
/// </summary>
protected PointerPointProperties Properties => _properties;
public PointerPointProperties Properties => _properties;
}
public enum MouseButton

34
src/Avalonia.Base/Input/PointerPoint.cs

@ -31,7 +31,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Provides extended properties for a PointerPoint object.
/// Describes the state of a pointer device when it generated a specific input.
/// </summary>
public record struct PointerPointProperties
{
@ -41,47 +41,47 @@ namespace Avalonia.Input
public Rect ContactRect { get; }
/// <summary>
/// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device.
/// Gets whether the device's primary action (e.g. <see cref="MouseButton.Left"/>) was active when this input occurred.
/// </summary>
public bool IsLeftButtonPressed { get; } = false;
/// <summary>
/// Gets a value that indicates whether the pointer input was triggered by the tertiary action mode of an input device.
/// Gets whether the device's tertiary action (e.g. <see cref="MouseButton.Middle"/>) was active when this input occurred.
/// </summary>
public bool IsMiddleButtonPressed { get; } = false;
/// <summary>
/// Gets a value that indicates whether the pointer input was triggered by the secondary action mode (if supported) of an input device.
/// Gets whether the device's secondary action (e.g. <see cref="MouseButton.Right"/>) was active when this input occurred.
/// </summary>
public bool IsRightButtonPressed { get; } = false;
/// <summary>
/// Gets a value that indicates whether the pointer input was triggered by the first extended mouse button (XButton1).
/// Gets whether the device's first extended action (e.g. <see cref="MouseButton.XButton1"/>) was active when this input occurred.
/// </summary>
public bool IsXButton1Pressed { get; } = false;
/// <summary>
/// Gets a value that indicates whether the pointer input was triggered by the second extended mouse button (XButton2).
/// Gets whether the device's second extended action (e.g. <see cref="MouseButton.XButton2"/>) was active when this input occurred.
/// </summary>
public bool IsXButton2Pressed { get; } = false;
/// <summary>
/// Gets a value that indicates whether the barrel button of the pen/stylus device is pressed.
/// Gets whether the barrel button of the pen/stylus device was pressed when this input occurred.
/// </summary>
public bool IsBarrelButtonPressed { get; } = false;
/// <summary>
/// Gets a value that indicates whether the input is from a pen eraser.
/// Gets whether the input was generated by the use of a pen eraser.
/// </summary>
public bool IsEraser { get; } = false;
/// <summary>
/// Gets a value that indicates whether the digitizer pen is inverted.
/// Gets whether the input was generated by an inverted digitizer pen.
/// </summary>
public bool IsInverted { get; } = false;
/// <summary>
/// Gets the clockwise rotation in degrees of a pen device around its own major axis (such as when the user spins the pen in their fingers).
/// Gets the clockwise rotation in degrees of the pen around its own major axis (such as when the user spins the pen in their fingers) when this input occurred.
/// </summary>
/// <returns>
/// A value between 0.0 and 359.0 in degrees of rotation. The default value is 0.0.
@ -89,7 +89,7 @@ namespace Avalonia.Input
public float Twist { get; } = 0.0F;
/// <summary>
/// Gets a value that indicates the force that the pointer device (typically a pen/stylus) exerts on the surface of the digitizer.
/// Gets the force that the pointer device (typically a pen/stylus) was exerting on the surface of the digitizer when this input occurred.
/// </summary>
/// <returns>
/// A value from 0 to 1.0. The default value is 0.5.
@ -97,23 +97,17 @@ namespace Avalonia.Input
public float Pressure { get; } = 0.5f;
/// <summary>
/// Gets the plane angle between the Y-Z plane and the plane that contains the Y axis and the axis of the input device (typically a pen/stylus).
/// Gets how many degrees of rotation the pen/stylus was tilted left (negative values) or right (positive values) when this input occurred.
/// </summary>
/// <returns>
/// The value is 0.0 when the finger or pen is perpendicular to the digitizer surface, between 0.0 and 90.0 when tilted to the right of perpendicular, and between 0.0 and -90.0 when tilted to the left of perpendicular. The default value is 0.0.
/// </returns>
public float XTilt { get; } = 0.0F;
/// <summary>
/// Gets the plane angle between the X-Z plane and the plane that contains the X axis and the axis of the input device (typically a pen/stylus).
/// Gets how many degrees of rotation the pen/stylus was tilted up (negative values) or down (positive values) when this input occurred.
/// </summary>
/// <returns>
/// The value is 0.0 when the finger or pen is perpendicular to the digitizer surface, between 0.0 and 90.0 when tilted towards the user, and between 0.0 and -90.0 when tilted away from the user. The default value is 0.0.
/// </returns>
public float YTilt { get; } = 0.0F;
/// <summary>
/// Gets the kind of pointer state change.
/// Gets the state change that triggered this input. This is typically a button press or release.
/// </summary>
public PointerUpdateKind PointerUpdateKind { get; } = PointerUpdateKind.LeftButtonPressed;

17
src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs

@ -28,6 +28,11 @@ namespace Avalonia.Input.TextInput
/// Fires when client wants to reset IME state
/// </summary>
public event EventHandler? ResetRequested;
/// <summary>
/// Fires when client requests the input panel be opened.
/// </summary>
public event EventHandler? InputPaneActivationRequested;
/// <summary>
/// The visual that's showing the text
@ -78,7 +83,12 @@ namespace Avalonia.Input.TextInput
SetPreeditText(preeditText);
}
public virtual void ShowInputPanel() { }
//TODO12: remove
[Obsolete]
public virtual void ShowInputPanel()
{
RaiseInputPaneActivationRequested();
}
protected virtual void RaiseTextViewVisualChanged()
{
@ -99,6 +109,11 @@ namespace Avalonia.Input.TextInput
{
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void RaiseInputPaneActivationRequested()
{
InputPaneActivationRequested?.Invoke(this, EventArgs.Empty);
}
protected virtual void RequestReset()
{

18
src/Avalonia.Base/Layout/Layoutable.cs

@ -596,15 +596,15 @@ namespace Avalonia.Layout
(width, height) = LayoutHelper.RoundLayoutSizeUp(new Size(width, height), scale);
}
width += margin.Left + margin.Right;
height += margin.Top + margin.Bottom;
if (width > availableSize.Width)
width = availableSize.Width;
if (height > availableSize.Height)
height = availableSize.Height;
width += margin.Left + margin.Right;
height += margin.Top + margin.Bottom;
if (width < 0)
width = 0;
@ -948,5 +948,17 @@ namespace Avalonia.Layout
{
return new Size(Math.Max(size.Width, 0), Math.Max(size.Height, 0));
}
internal override void SynchronizeCompositionProperties()
{
base.SynchronizeCompositionProperties();
if (CompositionVisual is { } visual)
{
// If the visual isn't using layout rounding, it's possible that antialiasing renders to pixels
// outside the current bounds. Extend the dirty rect by 1px in all directions in this case.
visual.ShouldExtendDirtyRect = !UseLayoutRounding;
}
}
}
}

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

@ -109,9 +109,9 @@ namespace Avalonia.Media
var familyName = fontFamily.FamilyNames[i];
if(_fontFamilyMappings != null && _fontFamilyMappings.TryGetValue(familyName, out var mappedFontFamily))
if (_fontFamilyMappings != null && _fontFamilyMappings.TryGetValue(familyName, out var mappedFontFamily))
{
if(mappedFontFamily.Key != null)
if (mappedFontFamily.Key != null)
{
key = mappedFontFamily.Key;
}
@ -123,6 +123,11 @@ namespace Avalonia.Media
familyName = mappedFontFamily.FamilyNames.PrimaryFamilyName;
}
if (familyName == FontFamily.DefaultFontFamilyName)
{
return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
}
if (TryGetGlyphTypefaceByKeyAndName(typeface, key, familyName, out glyphTypeface) &&
glyphTypeface.FamilyName.Contains(familyName))
{
@ -274,6 +279,11 @@ namespace Avalonia.Media
var familyName = fontFamily.FamilyNames[i];
var source = key.Source.EnsureAbsolute(key.BaseUri);
if(familyName == FontFamily.DefaultFontFamilyName)
{
familyName = DefaultFontFamily.Name;
}
if (TryGetFontCollection(source, out var fontCollection) &&
fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
{
@ -286,58 +296,6 @@ namespace Avalonia.Media
return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface);
}
/// <summary>
/// Tries to create a synthetic glyph typefacefor specified source glyph typeface and font properties.
/// </summary>
/// <param name="fontManager">The font manager implementation.</param>
/// <param name="glyphTypeface">The source glyph typeface.</param>
/// <param name="style">The requested font style.</param>
/// <param name="weight">The requested font weight.</param>
/// <param name="syntheticGlyphTypeface">The created synthetic glyph typeface.</param>
/// <returns>
/// <c>True</c>, if the <see cref="FontManager"/> could create a synthetic glyph typeface, <c>False</c> otherwise.
/// </returns>
internal static bool TryCreateSyntheticGlyphTypeface(IFontManagerImpl fontManager, IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight,
[NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
{
if (fontManager == null)
{
syntheticGlyphTypeface = null;
return false;
}
if (glyphTypeface is IGlyphTypeface2 glyphTypeface2)
{
var fontSimulations = FontSimulations.None;
if (style != FontStyle.Normal && glyphTypeface2.Style != style)
{
fontSimulations |= FontSimulations.Oblique;
}
if ((int)weight >= 600 && glyphTypeface2.Weight < weight)
{
fontSimulations |= FontSimulations.Bold;
}
if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream))
{
using (stream)
{
fontManager.TryCreateGlyphTypeface(stream, fontSimulations,
out syntheticGlyphTypeface);
return syntheticGlyphTypeface != null;
}
}
}
syntheticGlyphTypeface = null;
return false;
}
internal IReadOnlyList<Typeface> GetFamilyTypefaces(FontFamily fontFamily)
{
var key = fontFamily.Key;

14
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@ -71,14 +71,16 @@ namespace Avalonia.Media.Fonts
if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
{
if(_fontManager != null && FontManager.TryCreateSyntheticGlyphTypeface(_fontManager, glyphTypeface, style, weight, out var syntheticGlyphTypeface))
var matchedKey = new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
if(matchedKey != key)
{
glyphTypeface = syntheticGlyphTypeface;
if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface))
{
glyphTypeface = syntheticGlyphTypeface;
}
}
//Make sure we cache the found match
glyphTypefaces.TryAdd(key, glyphTypeface);
return true;
}
}
@ -143,7 +145,7 @@ namespace Avalonia.Media.Fonts
}
}
bool IFontCollection2.TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{
familyTypefaces = null;

84
src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs

@ -23,7 +23,7 @@ namespace Avalonia.Media.Fonts
public abstract bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch,
[NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch,
public virtual bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, FontStretch stretch,
string? familyName, CultureInfo? culture, out Typeface match)
{
match = default;
@ -59,6 +59,88 @@ namespace Avalonia.Media.Fonts
return false;
}
public virtual bool TryCreateSyntheticGlyphTypeface(
IGlyphTypeface glyphTypeface,
FontStyle style,
FontWeight weight,
FontStretch stretch,
[NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface)
{
syntheticGlyphTypeface = null;
//Source family should be present in the cache.
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
{
return false;
}
var fontManager = FontManager.Current.PlatformImpl;
var key = new FontCollectionKey(style, weight, stretch);
var currentKey =
new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch);
if (currentKey == key)
{
return false;
}
if (glyphTypeface is not IGlyphTypeface2 glyphTypeface2)
{
return false;
}
var fontSimulations = FontSimulations.None;
if (style != FontStyle.Normal && glyphTypeface2.Style != style)
{
fontSimulations |= FontSimulations.Oblique;
}
if ((int)weight >= 600 && glyphTypeface2.Weight < weight)
{
fontSimulations |= FontSimulations.Bold;
}
if (fontSimulations != FontSimulations.None && glyphTypeface2.TryGetStream(out var stream))
{
using (stream)
{
if (fontManager.TryCreateGlyphTypeface(stream, fontSimulations, out syntheticGlyphTypeface))
{
//Add the TypographicFamilyName to the cache
if (!string.IsNullOrEmpty(glyphTypeface2.TypographicFamilyName))
{
AddGlyphTypefaceByFamilyName(glyphTypeface2.TypographicFamilyName, syntheticGlyphTypeface);
}
foreach (var kvp in glyphTypeface2.FamilyNames)
{
AddGlyphTypefaceByFamilyName(kvp.Value, syntheticGlyphTypeface);
}
return true;
}
return false;
}
}
return false;
void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface)
{
var typefaces = _glyphTypefaceCache.GetOrAdd(familyName,
x =>
{
return new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
});
typefaces.TryAdd(key, glyphTypeface);
}
}
public abstract void Initialize(IFontManagerImpl fontManager);
public abstract IEnumerator<FontFamily> GetEnumerator();

11
src/Avalonia.Base/Media/Fonts/IFontCollection.cs

@ -59,5 +59,16 @@ namespace Avalonia.Media.Fonts
/// <c>True</c>, if the <see cref="IFontCollection2"/> could get the list of typefaces, <c>False</c> otherwise.
/// </returns>
bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces);
/// <summary>
/// Try to get a synthetic glyph typeface for given parameters.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface we try to synthesize.</param>
/// <param name="style">The font style.</param>
/// <param name="weight">The font weight.</param>
/// <param name="stretch">The font stretch.</param>
/// <param name="syntheticGlyphTypeface"></param>
/// <returns>Returns <c>true</c> if a synthetic glyph typface can be created; otherwise, <c>false</c></returns>
bool TryCreateSyntheticGlyphTypeface(IGlyphTypeface glyphTypeface, FontStyle style, FontWeight weight, FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? syntheticGlyphTypeface);
}
}

6
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@ -91,9 +91,11 @@ namespace Avalonia.Media.Fonts
}
//Try to create a synthetic glyph typeface
if (FontManager.TryCreateSyntheticGlyphTypeface(_fontManager.PlatformImpl, glyphTypeface, style, weight, out var syntheticGlyphTypeface))
if (TryCreateSyntheticGlyphTypeface(glyphTypeface, style, weight, stretch, out var syntheticGlyphTypeface))
{
glyphTypeface = syntheticGlyphTypeface;
return true;
}
}
@ -159,7 +161,7 @@ namespace Avalonia.Media.Fonts
}
}
bool IFontCollection2.TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
public bool TryGetFamilyTypefaces(string familyName, [NotNullWhen(true)] out IReadOnlyList<Typeface>? familyTypefaces)
{
familyTypefaces = null;

2
src/Avalonia.Base/Media/MediaContext.Compositor.cs

@ -31,7 +31,7 @@ partial class MediaContext
_pendingCompositionBatches[compositor] = commit;
commit.Processed.ContinueWith(_ =>
_dispatcher.Post(() => CompositionBatchFinished(compositor, commit), DispatcherPriority.Send),
TaskContinuationOptions.ExecuteSynchronously);
CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
return commit;
}

3
src/Avalonia.Base/Media/MediaContext.cs

@ -173,9 +173,8 @@ internal partial class MediaContext : ICompositorScheduler
public void RemoveTopLevel(object key)
{
if (_topLevels.TryGetValue(key, out var info))
if (_topLevels.Remove(key, out var info))
{
_topLevels.Remove(key);
info.Renderer.Stop();
}
}

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

@ -100,7 +100,7 @@ namespace Avalonia.Media
}
/// <inheritdoc/>
public override Geometry Clone() => new RectangleGeometry(Rect);
public override Geometry Clone() => new RectangleGeometry(Rect, RadiusX, RadiusY);
private protected sealed override IGeometryImpl? CreateDefiningGeometry()
{

135
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@ -2,6 +2,7 @@
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
@ -89,90 +90,110 @@ namespace Avalonia.Media.TextFormatting
public IEnumerator<GlyphInfo> GetEnumerator() => _glyphInfos.GetEnumerator();
internal void ResetBidiLevel(sbyte paragraphEmbeddingLevel) => BidiLevel = paragraphEmbeddingLevel;
int IReadOnlyCollection<GlyphInfo>.Count => _glyphInfos.Length;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Finds a glyph index for given character index.
/// Splits the <see cref="TextRun"/> at specified length.
/// </summary>
/// <param name="characterIndex">The character index.</param>
/// <returns>
/// The glyph index.
/// </returns>
private int FindGlyphIndex(int characterIndex)
/// <param name="textLength">The text length.</param>
/// <returns>The split result.</returns>
public SplitResult<ShapedBuffer> Split(int textLength)
{
if (characterIndex < _glyphInfos[0].GlyphCluster)
// make sure we do not overshoot
textLength = Math.Min(Text.Length, textLength);
if (textLength <= 0)
{
return 0;
var emptyBuffer = new ShapedBuffer(
Text.Slice(0, 0), _glyphInfos.Slice(_glyphInfos.Start, 0),
GlyphTypeface, FontRenderingEmSize, BidiLevel);
return new SplitResult<ShapedBuffer>(emptyBuffer, this);
}
if (characterIndex > _glyphInfos[_glyphInfos.Length - 1].GlyphCluster)
// nothing to split
if (textLength == Text.Length)
{
return _glyphInfos.Length - 1;
return new SplitResult<ShapedBuffer>(this, null);
}
var comparer = GlyphInfo.ClusterAscendingComparer;
var sliceStart = _glyphInfos.Start;
var glyphInfos = _glyphInfos.Span;
var glyphInfosLength = _glyphInfos.Length;
var searchValue = new GlyphInfo(default, characterIndex, default);
// the first glyph’s cluster is our “zero” for this sub‐buffer.
// we want an absolute target cluster = baseCluster + textLength
var baseCluster = glyphInfos[0].GlyphCluster;
var targetCluster = baseCluster + textLength;
var start = glyphInfos.BinarySearch(searchValue, comparer);
// binary‐search for a dummy with cluster == targetCluster
var searchValue = new GlyphInfo(0, targetCluster, 0, default);
var foundIndex = glyphInfos.BinarySearch(searchValue, GlyphInfo.ClusterAscendingComparer);
if (start < 0)
{
while (characterIndex > 0 && start < 0)
{
characterIndex--;
int splitGlyphIndex; // how many glyph‐slots go into "leading"
int splitCharCount; // how many chars go into "leading" Text
searchValue = new GlyphInfo(default, characterIndex, default);
start = glyphInfos.BinarySearch(searchValue, comparer);
}
if (foundIndex >= 0)
{
// found a glyph info whose cluster == targetCluster
// back up to the start of the cluster
var i = foundIndex;
if (start < 0)
while (i > 0 && glyphInfos[i - 1].GlyphCluster == targetCluster)
{
return -1;
i--;
}
splitGlyphIndex = i;
splitCharCount = targetCluster - baseCluster;
}
while (start > 0 && glyphInfos[start - 1].GlyphCluster == glyphInfos[start].GlyphCluster)
else
{
start--;
}
return start;
}
// no exact match need to invert so ~foundIndex is the insertion point
// the first cluster > targetCluster
var invertedIndex = ~foundIndex;
/// <summary>
/// Splits the <see cref="TextRun"/> at specified length.
/// </summary>
/// <param name="length">The length.</param>
/// <returns>The split result.</returns>
internal SplitResult<ShapedBuffer> Split(int length)
{
if (Text.Length == length)
{
return new SplitResult<ShapedBuffer>(this, null);
if (invertedIndex >= glyphInfosLength)
{
// happens only if targetCluster ≥ lastCluster
// put everything into leading
splitGlyphIndex = glyphInfosLength;
splitCharCount = Text.Length;
}
else
{
// snap to the start of that next cluster
splitGlyphIndex = invertedIndex;
var nextCluster = glyphInfos[invertedIndex].GlyphCluster;
splitCharCount = nextCluster - baseCluster;
}
}
var firstCluster = _glyphInfos[0].GlyphCluster;
var lastCluster = _glyphInfos[_glyphInfos.Length - 1].GlyphCluster;
var firstGlyphs = _glyphInfos.Slice(sliceStart, splitGlyphIndex);
var secondGlyphs = _glyphInfos.Slice(sliceStart + splitGlyphIndex, glyphInfosLength - splitGlyphIndex);
var start = firstCluster < lastCluster ? firstCluster : lastCluster;
var firstText = Text.Slice(0, splitCharCount);
var secondText = Text.Slice(splitCharCount);
var glyphCount = FindGlyphIndex(start + length);
var leading = new ShapedBuffer(
firstText, firstGlyphs,
GlyphTypeface, FontRenderingEmSize, BidiLevel);
var first = new ShapedBuffer(Text.Slice(0, length),
_glyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
// this happens if we try to find a position inside a cluster and we moved to the end
if(secondText.Length == 0)
{
return new SplitResult<ShapedBuffer>(leading, null);
}
var second = new ShapedBuffer(Text.Slice(length),
_glyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var trailing = new ShapedBuffer(
secondText, secondGlyphs,
GlyphTypeface, FontRenderingEmSize, BidiLevel);
return new SplitResult<ShapedBuffer>(first, second);
return new SplitResult<ShapedBuffer>(leading, trailing);
}
internal void ResetBidiLevel(sbyte paragraphEmbeddingLevel) => BidiLevel = paragraphEmbeddingLevel;
int IReadOnlyCollection<GlyphInfo>.Count => _glyphInfos.Length;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

4
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -182,9 +182,9 @@ namespace Avalonia.Media.TextFormatting
#if DEBUG
if (first.Length != length)
if (first.Length < length)
{
throw new InvalidOperationException("Split length mismatch.");
throw new InvalidOperationException("Split length too small.");
}
#endif
var second = new ShapedTextRun(splitBuffer.Second!, Properties);

37
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -371,15 +371,31 @@ namespace Avalonia.Media.TextFormatting
{
var shapedBuffer = textShaper.ShapeText(text, options);
var previousLength = 0;
for (var i = 0; i < textRuns.Count; i++)
{
var currentRun = textRuns[i];
var splitResult = shapedBuffer.Split(currentRun.Length);
var splitResult = shapedBuffer.Split(previousLength + currentRun.Length);
results.Add(new ShapedTextRun(splitResult.First, currentRun.Properties));
if(splitResult.First.Length == 0)
{
previousLength += currentRun.Length;
}
else
{
previousLength = 0;
shapedBuffer = splitResult.Second!;
results.Add(new ShapedTextRun(splitResult.First, currentRun.Properties));
}
if(splitResult.Second is null)
{
return;
}
shapedBuffer = splitResult.Second;
}
}
@ -921,7 +937,20 @@ namespace Avalonia.Media.TextFormatting
ResetTrailingWhitespaceBidiLevels(preSplitRuns, paragraphProperties.FlowDirection, objectPool);
}
var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
var remainingTextRuns = new TextRun[preSplitRuns.Count];
//Measured lenght might have changed after a possible line break was found so we need to calculate the real length
var splitLength = 0;
for(var i = 0; i < preSplitRuns.Count; i++)
{
var currentRun = preSplitRuns[i];
remainingTextRuns[i] = currentRun;
splitLength += currentRun.Length;
}
var textLine = new TextLineImpl(remainingTextRuns, firstTextSourceIndex, splitLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection,
textLineBreak);

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

@ -706,6 +706,11 @@ namespace Avalonia.Media.TextFormatting
lastBounds = currentBounds;
if(coveredLength <= 0)
{
throw new InvalidOperationException("Covered length must be greater than zero.");
}
remainingLength -= coveredLength;
}
@ -1090,7 +1095,8 @@ namespace Avalonia.Media.TextFormatting
var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
//Adjust characterLength by the cluster offset to only cover the remaining length of the cluster.
var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset;
var characterLength = Math.Max(0, Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength -
endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset);
if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length)
{
@ -1172,7 +1178,8 @@ namespace Avalonia.Media.TextFormatting
startIndex -= clusterOffset;
}
var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset;
var characterLength = Math.Max(0, Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength -
endHit.FirstCharacterIndex - endHit.TrailingLength) - clusterOffset);
if (characterLength == 0 && currentRun.Text.Length > 0 && startIndex < currentRun.Text.Length)
{
@ -1408,10 +1415,12 @@ namespace Avalonia.Media.TextFormatting
}
var width = widthIncludingWhitespace;
var isRtl = _paragraphProperties.FlowDirection == FlowDirection.RightToLeft;
for (var i = _textRuns.Length - 1; i >= 0; i--)
for (int i = 0; i < _textRuns.Length; i++)
{
var currentRun = _textRuns[i];
var index = isRtl ? i : _textRuns.Length - 1 - i;
var currentRun = _textRuns[index];
if (currentRun is ShapedTextRun shapedText)
{

4
src/Avalonia.Base/Reactive/LightweightObservableBase.cs

@ -20,7 +20,7 @@ namespace Avalonia.Reactive
private List<IObserver<T>>? _observers = new List<IObserver<T>>();
public bool HasObservers => _observers?.Count > 0;
public IDisposable Subscribe(IObserver<T> observer)
{
_ = observer ?? throw new ArgumentNullException(nameof(observer));
@ -168,6 +168,8 @@ namespace Avalonia.Reactive
for(int i = 0; i < count; i++)
{
observers[i].OnNext(value);
// Avoid memory leak by clearing the reference.
observers[i] = null!;
}
ArrayPool<IObserver<T>>.Shared.Return(observers);

3
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Collections.Pooled;
@ -187,7 +188,7 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
{
_queuedSceneInvalidation = false;
SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));
}, DispatcherPriority.Input), TaskContinuationOptions.ExecuteSynchronously);
}, DispatcherPriority.Input), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
}

7
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
@ -114,7 +115,7 @@ namespace Avalonia.Rendering.Composition
if (pending != null)
pending.Processed.ContinueWith(
_ => Dispatcher.Post(_triggerCommitRequested, DispatcherPriority.Send),
TaskContinuationOptions.ExecuteSynchronously);
CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
else
_triggerCommitRequested();
}
@ -202,10 +203,10 @@ namespace Avalonia.Rendering.Composition
{
lock (_pendingBatchLock)
{
if (_pendingBatch.Processed == t)
if (_pendingBatch?.Processed == t)
_pendingBatch = null;
}
}, TaskContinuationOptions.ExecuteSynchronously);
}, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
_nextCommit = null;
return commit;

6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -254,6 +254,12 @@ namespace Avalonia.Rendering.Composition.Server
{
if (rc == default)
return;
// If the visual isn't using layout rounding, it's possible that antialiasing renders to pixels
// outside the current bounds. Extend the dirty rect by 1px in all directions in this case.
if (ShouldExtendDirtyRect && RenderOptions.EdgeMode != EdgeMode.Aliased)
rc = rc.Inflate(new Thickness(1));
Root?.AddDirtyRect(rc);
}

11
src/Avalonia.Base/Styling/StyleInstance.cs

@ -25,6 +25,7 @@ namespace Avalonia.Styling
private bool _isActive;
private List<ISetterInstance>? _setters;
private List<IAnimation>? _animations;
private List<IDisposable>? _animationApplyDisposables;
private LightweightSubject<bool>? _animationTrigger;
public StyleInstance(
@ -69,8 +70,9 @@ namespace Avalonia.Styling
if (_animations is not null && control is Animatable animatable)
{
_animationTrigger ??= new LightweightSubject<bool>();
_animationApplyDisposables ??= new List<IDisposable>();
foreach (var animation in _animations)
animation.Apply(animatable, null, _animationTrigger);
_animationApplyDisposables.Add(animation.Apply(animatable, null, _animationTrigger));
if (_activator is null)
_animationTrigger.OnNext(true);
@ -81,6 +83,13 @@ namespace Avalonia.Styling
{
base.Dispose();
_activator?.Dispose();
if (_animationApplyDisposables != null)
{
foreach (var item in _animationApplyDisposables)
{
item.Dispose();
}
}
}
public new void MakeShared() => base.MakeShared();

69
src/Avalonia.Base/Threading/Dispatcher.Invoke.cs

@ -445,7 +445,8 @@ public partial class Dispatcher
// so it is safe to modify the operation outside of the lock.
// Just mark the operation as aborted, which we can safely
// return to the user.
operation.DoAbort();
operation.Status = DispatcherOperationStatus.Aborted;
operation.CallAbortCallbacks();
}
}
@ -671,4 +672,70 @@ public partial class Dispatcher
/// </summary>
public DispatcherPriorityAwaitable<T> AwaitWithPriority<T>(Task<T> task, DispatcherPriority priority) =>
new(this, task, priority);
/// <summary>
/// Creates an awaitable object that asynchronously resumes execution on the dispatcher.
/// </summary>
/// <returns>
/// An awaitable object that asynchronously resumes execution on the dispatcher.
/// </returns>
/// <remarks>
/// This method is equivalent to calling the <see cref="Resume(DispatcherPriority)"/> method
/// and passing in <see cref="DispatcherPriority.Background"/>.
/// </remarks>
public DispatcherPriorityAwaitable Resume() =>
Resume(DispatcherPriority.Background);
/// <summary>``
/// Creates an awaitable object that asynchronously resumes execution on the dispatcher. The work that occurs
/// when control returns to the code awaiting the result of this method is scheduled with the specified priority.
/// </summary>
/// <param name="priority">The priority at which to schedule the continuation.</param>
/// <returns>
/// An awaitable object that asynchronously resumes execution on the dispatcher.
/// </returns>
public DispatcherPriorityAwaitable Resume(DispatcherPriority priority)
{
DispatcherPriority.Validate(priority, nameof(priority));
return new(this, null, priority);
}
/// <summary>
/// Creates an awaitable object that asynchronously yields control back to the current dispatcher
/// and provides an opportunity for the dispatcher to process other events.
/// </summary>
/// <returns>
/// An awaitable object that asynchronously yields control back to the current dispatcher
/// and provides an opportunity for the dispatcher to process other events.
/// </returns>
/// <remarks>
/// This method is equivalent to calling the <see cref="Yield(DispatcherPriority)"/> method
/// and passing in <see cref="DispatcherPriority.Background"/>.
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The current thread is not the UI thread.
/// </exception>
public static DispatcherPriorityAwaitable Yield() =>
Yield(DispatcherPriority.Background);
/// <summary>
/// Creates an cawaitable object that asynchronously yields control back to the current dispatcher
/// and provides an opportunity for the dispatcher to process other events. The work that occurs when
/// control returns to the code awaiting the result of this method is scheduled with the specified priority.
/// </summary>
/// <param name="priority">The priority at which to schedule the continuation.</param>
/// <returns>
/// An awaitable object that asynchronously yields control back to the current dispatcher
/// and provides an opportunity for the dispatcher to process other events.
/// </returns>
/// <exception cref="InvalidOperationException">
/// The current thread is not the UI thread.
/// </exception>
public static DispatcherPriorityAwaitable Yield(DispatcherPriority priority)
{
// TODO12: Update to use Dispatcher.CurrentDispatcher once multi-dispatcher support is merged
var current = UIThread;
current.VerifyAccess();
return UIThread.Resume(priority);
}
}

16
src/Avalonia.Base/Threading/Dispatcher.Queue.cs

@ -134,7 +134,14 @@ public partial class Dispatcher
private void ExecuteJob(DispatcherOperation job)
{
lock (InstanceLock)
{
if(job.Status != DispatcherOperationStatus.Pending)
return;
_queue.RemoveItem(job);
job.Status = DispatcherOperationStatus.Executing;
}
job.Execute();
// The backend might be firing timers with a low priority,
// so we manually check if our high priority timers are due for execution
@ -236,11 +243,16 @@ public partial class Dispatcher
}
}
internal void Abort(DispatcherOperation operation)
internal bool Abort(DispatcherOperation operation)
{
lock (InstanceLock)
{
if (operation.Status != DispatcherOperationStatus.Pending)
return false;
_queue.RemoveItem(operation);
operation.DoAbort();
operation.Status = DispatcherOperationStatus.Aborted;
}
return true;
}
// Returns whether or not the priority was set.

33
src/Avalonia.Base/Threading/DispatcherOperation.cs

@ -11,7 +11,7 @@ namespace Avalonia.Threading;
public class DispatcherOperation
{
protected readonly bool ThrowOnUiThread;
public DispatcherOperationStatus Status { get; protected set; }
public DispatcherOperationStatus Status { get; internal set; }
public Dispatcher Dispatcher { get; }
public DispatcherPriority Priority
@ -115,13 +115,13 @@ public class DispatcherOperation
public bool Abort()
{
lock (Dispatcher.InstanceLock)
if (Dispatcher.Abort(this))
{
if (Status != DispatcherOperationStatus.Pending)
return false;
Dispatcher.Abort(this);
CallAbortCallbacks();
return true;
}
return false;
}
/// <summary>
@ -254,20 +254,15 @@ public class DispatcherOperation
return GetTask().GetAwaiter();
}
internal void DoAbort()
internal void CallAbortCallbacks()
{
Status = DispatcherOperationStatus.Aborted;
AbortTask();
_aborted?.Invoke(this, EventArgs.Empty);
}
internal void Execute()
{
lock (Dispatcher.InstanceLock)
{
Status = DispatcherOperationStatus.Executing;
}
Debug.Assert(Status == DispatcherOperationStatus.Executing);
try
{
using (AvaloniaSynchronizationContext.Ensure(Dispatcher, Priority))
@ -311,7 +306,19 @@ public class DispatcherOperation
internal virtual object? GetResult() => null;
protected virtual void AbortTask() => (TaskSource as TaskCompletionSource<object?>)?.SetCanceled();
protected virtual void AbortTask()
{
object? taskSource;
lock (Dispatcher.InstanceLock)
{
Debug.Assert(Status == DispatcherOperationStatus.Aborted);
// There is no way for TaskSource to become not-null after being null with aborted tasks,
// so it's safe to save it here and use after exiting the lock
taskSource = TaskSource;
}
(taskSource as TaskCompletionSource<object?>)?.SetCanceled();
}
private static CancellationToken CreateCancelledToken()
{

122
src/Avalonia.Base/Threading/DispatcherPriorityAwaitable.cs

@ -1,40 +1,130 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Threading;
public class DispatcherPriorityAwaitable : INotifyCompletion
/// <summary>
/// A simple awaitable type that will return a DispatcherPriorityAwaiter.
/// </summary>
public struct DispatcherPriorityAwaitable
{
private readonly Dispatcher _dispatcher;
private protected readonly Task Task;
private readonly Task? _task;
private readonly DispatcherPriority _priority;
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task task, DispatcherPriority priority)
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task? task, DispatcherPriority priority)
{
_dispatcher = dispatcher;
Task = task;
_task = task;
_priority = priority;
}
public void OnCompleted(Action continuation) =>
Task.ContinueWith(_ => _dispatcher.Post(continuation, _priority));
public bool IsCompleted => Task.IsCompleted;
public DispatcherPriorityAwaiter GetAwaiter() => new(_dispatcher, _task, _priority);
}
/// <summary>
/// A simple awaiter type that will queue the continuation to a dispatcher at a specific priority.
/// </summary>
/// <remarks>
/// This is returned from DispatcherPriorityAwaitable.GetAwaiter()
/// </remarks>
public struct DispatcherPriorityAwaiter : INotifyCompletion
{
private readonly Dispatcher _dispatcher;
private readonly Task? _task;
private readonly DispatcherPriority _priority;
internal DispatcherPriorityAwaiter(Dispatcher dispatcher, Task? task, DispatcherPriority priority)
{
_dispatcher = dispatcher;
_task = task;
_priority = priority;
}
public void OnCompleted(Action continuation)
{
if(_task == null || _task.IsCompleted)
_dispatcher.Post(continuation, _priority);
else
{
var self = this;
_task.ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
{
self._dispatcher.Post(continuation, self._priority);
});
}
}
/// <summary>
/// This always returns false since continuation is requested to be queued to a dispatcher queue
/// </summary>
public bool IsCompleted => false;
public void GetResult()
{
if (_task != null)
_task.GetAwaiter().GetResult();
}
}
/// <summary>
/// A simple awaitable type that will return a DispatcherPriorityAwaiter&lt;T&gt;.
/// </summary>
public struct DispatcherPriorityAwaitable<T>
{
private readonly Dispatcher _dispatcher;
private readonly Task<T> _task;
private readonly DispatcherPriority _priority;
public void GetResult() => Task.GetAwaiter().GetResult();
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority)
{
_dispatcher = dispatcher;
_task = task;
_priority = priority;
}
public DispatcherPriorityAwaitable GetAwaiter() => this;
public DispatcherPriorityAwaiter<T> GetAwaiter() => new(_dispatcher, _task, _priority);
}
public sealed class DispatcherPriorityAwaitable<T> : DispatcherPriorityAwaitable
/// <summary>
/// A simple awaiter type that will queue the continuation to a dispatcher at a specific priority.
/// </summary>
/// <remarks>
/// This is returned from DispatcherPriorityAwaitable&lt;T&gt;.GetAwaiter()
/// </remarks>
public struct DispatcherPriorityAwaiter<T> : INotifyCompletion
{
internal DispatcherPriorityAwaitable(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority) : base(
dispatcher, task, priority)
private readonly Dispatcher _dispatcher;
private readonly Task<T> _task;
private readonly DispatcherPriority _priority;
internal DispatcherPriorityAwaiter(Dispatcher dispatcher, Task<T> task, DispatcherPriority priority)
{
_dispatcher = dispatcher;
_task = task;
_priority = priority;
}
public new T GetResult() => ((Task<T>)Task).GetAwaiter().GetResult();
public void OnCompleted(Action continuation)
{
if(_task.IsCompleted)
_dispatcher.Post(continuation, _priority);
else
{
var self = this;
_task.ConfigureAwait(false).GetAwaiter().OnCompleted(() =>
{
self._dispatcher.Post(continuation, self._priority);
});
}
}
/// <summary>
/// This always returns false since continuation is requested to be queued to a dispatcher queue
/// </summary>
public bool IsCompleted => false;
public new DispatcherPriorityAwaitable<T> GetAwaiter() => this;
}
public void GetResult() => _task.GetAwaiter().GetResult();
}

6
src/Avalonia.Base/Utilities/CharacterReader.cs

@ -2,11 +2,7 @@ using System;
namespace Avalonia.Utilities
{
// TODO12: This should not be public
#if !BUILDTASK
public
#endif
ref struct CharacterReader
internal ref struct CharacterReader
{
private ReadOnlySpan<char> _s;

5
src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -3,10 +3,7 @@ using System.Globalization;
namespace Avalonia.Utilities
{
#if !BUILDTASK
public
#endif
static class IdentifierParser
internal static class IdentifierParser
{
public static ReadOnlySpan<char> ParseIdentifier(this
#if NET7SDK

5
src/Avalonia.Base/Utilities/KeywordParser.cs

@ -2,10 +2,7 @@ using System;
namespace Avalonia.Utilities
{
#if !BUILDTASK
public
#endif
static class KeywordParser
internal static class KeywordParser
{
public static bool CheckKeyword(this ref CharacterReader r, string keyword)
{

3
src/Avalonia.Base/Utilities/SmallDictionary.cs

@ -292,9 +292,8 @@ internal struct InlineDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey,
}
else if (_data is Dictionary<TKey, TValue?> dic)
{
if (!dic.TryGetValue(key, out value))
if (!dic.Remove(key, out value))
return false;
dic.Remove(key);
}
value = default;

5
src/Avalonia.Base/Utilities/StyleClassParser.cs

@ -3,10 +3,7 @@ using System.Globalization;
namespace Avalonia.Utilities
{
#if !BUILDTASK
public
#endif
static class StyleClassParser
internal static class StyleClassParser
{
public static ReadOnlySpan<char> ParseStyleClass(this ref CharacterReader r)
{

1
src/Avalonia.Base/composition-schema.xml

@ -34,6 +34,7 @@
<Property Name="OpacityMaskBrush" ClientName="OpacityMaskBrushTransportField" Type="Avalonia.Media.IBrush?" Private="true" />
<Property Name="Effect" Type="Avalonia.Media.IImmutableEffect?" Internal="true" />
<Property Name="RenderOptions" Type="Avalonia.Media.RenderOptions" />
<Property Name="ShouldExtendDirtyRect" Type="bool" Internal="true" />
</Object>
<Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/>
<Object Name="CompositionSolidColorVisual" Inherits="CompositionContainerVisual">

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

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
@ -188,7 +188,10 @@ namespace Avalonia.Controls.ApplicationLifetimes
if (w.Owner is null)
{
var ignoreCancel = force || (ShutdownMode == ShutdownMode.OnMainWindowClose && w != MainWindow);
w.CloseCore(WindowCloseReason.ApplicationShutdown, isProgrammatic, ignoreCancel);
var reason = e.IsOSShutdown ?
WindowCloseReason.OSShutdown :
WindowCloseReason.ApplicationShutdown;
w.CloseCore(reason, isProgrammatic, ignoreCancel);
}
}

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

Loading…
Cancel
Save