Browse Source

Merge remote-tracking branch 'origin/master' into refactor/other-controls-itemssource

pull/10831/head
Max Katz 3 years ago
parent
commit
8b4fe1e13a
  1. 42
      Avalonia.sln
  2. 40
      samples/RenderDemo/Pages/AnimationsPage.xaml
  3. BIN
      samples/SafeAreaDemo.Android/Icon.png
  4. 11
      samples/SafeAreaDemo.Android/MainActivity.cs
  5. 5
      samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml
  6. 13
      samples/SafeAreaDemo.Android/Resources/drawable/splash_screen.xml
  7. 4
      samples/SafeAreaDemo.Android/Resources/values/colors.xml
  8. 17
      samples/SafeAreaDemo.Android/Resources/values/styles.xml
  9. 24
      samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj
  10. 30
      samples/SafeAreaDemo.Android/SplashActivity.cs
  11. 21
      samples/SafeAreaDemo.Desktop/Program.cs
  12. 24
      samples/SafeAreaDemo.Desktop/SafeAreaDemo.Desktop.csproj
  13. 18
      samples/SafeAreaDemo.Desktop/app.manifest
  14. 17
      samples/SafeAreaDemo.iOS/AppDelegate.cs
  15. 5
      samples/SafeAreaDemo.iOS/Entitlements.plist
  16. 47
      samples/SafeAreaDemo.iOS/Info.plist
  17. 15
      samples/SafeAreaDemo.iOS/Main.cs
  18. 43
      samples/SafeAreaDemo.iOS/Resources/LaunchScreen.xib
  19. 18
      samples/SafeAreaDemo.iOS/SafeAreaDemo.iOS.csproj
  20. 15
      samples/SafeAreaDemo/App.xaml
  21. 36
      samples/SafeAreaDemo/App.xaml.cs
  22. BIN
      samples/SafeAreaDemo/Assets/avalonia-logo.ico
  23. 27
      samples/SafeAreaDemo/SafeAreaDemo.csproj
  24. 31
      samples/SafeAreaDemo/ViewLocator.cs
  25. 112
      samples/SafeAreaDemo/ViewModels/MainViewModel.cs
  26. 52
      samples/SafeAreaDemo/Views/MainView.xaml
  27. 25
      samples/SafeAreaDemo/Views/MainView.xaml.cs
  28. 12
      samples/SafeAreaDemo/Views/MainWindow.xaml
  29. 13
      samples/SafeAreaDemo/Views/MainWindow.xaml.cs
  30. 22
      src/Avalonia.Base/Media/Effects/BlurEffect.cs
  31. 104
      src/Avalonia.Base/Media/Effects/DropShadowEffect.cs
  32. 93
      src/Avalonia.Base/Media/Effects/Effect.cs
  33. 131
      src/Avalonia.Base/Media/Effects/EffectAnimator.cs
  34. 18
      src/Avalonia.Base/Media/Effects/EffectConverter.cs
  35. 56
      src/Avalonia.Base/Media/Effects/EffectExtesions.cs
  36. 83
      src/Avalonia.Base/Media/Effects/EffectTransition.cs
  37. 29
      src/Avalonia.Base/Media/Effects/IBlurEffect.cs
  38. 84
      src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs
  39. 26
      src/Avalonia.Base/Media/Effects/IEffect.cs
  40. 6
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  41. 9
      src/Avalonia.Base/Rect.cs
  42. 10
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  43. 56
      src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs
  44. 15
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  45. 68
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  46. 28
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  47. 5
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
  48. 18
      src/Avalonia.Base/StyledElement.cs
  49. 20
      src/Avalonia.Base/Visual.cs
  50. 6
      src/Avalonia.Base/composition-schema.xml
  51. 28
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  52. 2
      src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
  53. 12
      src/Avalonia.Controls/Flyouts/Flyout.cs
  54. 35
      src/Avalonia.X11/X11Window.cs
  55. 3
      src/Avalonia.X11/XI2Manager.cs
  56. 3
      src/Avalonia.X11/XLib.cs
  57. 3
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  58. 9
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  59. 46
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXAmlIlClassesTransformer.cs
  60. 50
      src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs
  61. 16
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  62. 73
      tests/Avalonia.Base.UnitTests/Media/EffectTests.cs
  63. 32
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  64. 43
      tests/Avalonia.RenderTests/Media/EffectTests.cs
  65. 3
      tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj
  66. BIN
      tests/TestFiles/Skia/Media/Effects/DropShadowEffect.expected.png

42
Avalonia.sln

@ -244,13 +244,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo", "samples\SafeAreaDemo\SafeAreaDemo.csproj", "{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Android", "samples\SafeAreaDemo.Android\SafeAreaDemo.Android.csproj", "{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Desktop", "samples\SafeAreaDemo.Desktop\SafeAreaDemo.Desktop.csproj", "{4CDAD037-34A2-4CCF-A03A-C6C7B988A572}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.iOS", "samples\SafeAreaDemo.iOS\SafeAreaDemo.iOS.csproj", "{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -595,6 +603,26 @@ Global
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Release|Any CPU.Build.0 = Release|Any CPU
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Release|Any CPU.Build.0 = Release|Any CPU
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}.Release|Any CPU.Deploy.0 = Release|Any CPU
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572}.Release|Any CPU.Build.0 = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Build.0 = Release|Any CPU
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Deploy.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -661,10 +689,14 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{4CDAD037-34A2-4CCF-A03A-C6C7B988A572} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

40
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -308,6 +308,41 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Blur">
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="Effect" Value="blur(0)"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Effect" Value="blur(10)"/>
</KeyFrame>
</Animation>
</Style.Animations>
<Setter Property="Child" Value="{StaticResource Acorn}"/>
</Style>
<Style Selector="Border.DropShadow">
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="Effect" Value="drop-shadow(0 0 0)"/>
</KeyFrame>
<KeyFrame Cue="35%">
<Setter Property="Effect" Value="drop-shadow(5 5 0 Green)"/>
</KeyFrame>
<KeyFrame Cue="70%">
<Setter Property="Effect" Value="drop-shadow(5 5 5 Red)"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Effect" Value="drop-shadow(20 -5 5 Blue)"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>
</UserControl.Styles>
<Grid>
@ -332,6 +367,11 @@
<Border Classes="Test Rect8" Child="{x:Null}" />
<Border Classes="Test Rect9" Child="{x:Null}" />
<Border Classes="Test Rect10" Child="{x:Null}" />
<Border Classes="Test Blur" Background="#ffa0a0a0" BorderThickness="4" BorderBrush="Yellow" Padding="10"/>
<Border Classes="Test DropShadow" Background="Transparent" BorderThickness="4" BorderBrush="Yellow">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center">Drop
Shadow</TextBlock>
</Border>
</WrapPanel>
</StackPanel>
</Grid>

BIN
samples/SafeAreaDemo.Android/Icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

11
samples/SafeAreaDemo.Android/MainActivity.cs

@ -0,0 +1,11 @@
using Android.App;
using Android.Content.PM;
using Avalonia.Android;
namespace SafeAreaDemo.Android
{
[Activity(Label = "SafeAreaDemo.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
public class MainActivity : AvaloniaMainActivity
{
}
}

5
samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml

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

13
samples/SafeAreaDemo.Android/Resources/drawable/splash_screen.xml

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/splash_background"/>
</item>
<item android:drawable="@drawable/icon"
android:width="120dp"
android:height="120dp"
android:gravity="center" />
</layer-list>

4
samples/SafeAreaDemo.Android/Resources/values/colors.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background">#FFFFFF</color>
</resources>

17
samples/SafeAreaDemo.Android/Resources/values/styles.xml

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="MyTheme">
</style>
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="MyTheme.Splash" parent ="MyTheme.NoActionBar">
<item name="android:windowBackground">@drawable/splash_screen</item>
<item name="android:windowContentOverlay">@null</item>
</style>
</resources>

24
samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0-android</TargetFramework>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
<ApplicationId>com.avalonia.safeareademo</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<AndroidEnableProfiledAot>False</AndroidEnableProfiledAot>
</PropertyGroup>
<ItemGroup>
<AndroidResource Include="Icon.png">
<Link>Resources\drawable\Icon.png</Link>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" />
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" />
</ItemGroup>
</Project>

30
samples/SafeAreaDemo.Android/SplashActivity.cs

@ -0,0 +1,30 @@
using Android.App;
using Android.Content;
using Android.OS;
using Avalonia;
using Avalonia.Android;
using Application = Android.App.Application;
namespace SafeAreaDemo.Android
{
[Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)]
public class SplashActivity : AvaloniaSplashActivity<App>
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return base.CustomizeAppBuilder(builder);
}
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
}
protected override void OnResume()
{
base.OnResume();
StartActivity(new Intent(Application.Context, typeof(MainActivity)));
}
}
}

21
samples/SafeAreaDemo.Desktop/Program.cs

@ -0,0 +1,21 @@
using Avalonia;
using System;
namespace SafeAreaDemo.Desktop
{
internal class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
}
}

24
samples/SafeAreaDemo.Desktop/SafeAreaDemo.Desktop.csproj

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects.
One for Windows with net7.0-windows TFM, one for MacOS with net7.0-macos and one with net7.0 TFM for Linux.-->
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" />
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
</Project>

18
samples/SafeAreaDemo.Desktop/app.manifest

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="SafeAreaDemo.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

17
samples/SafeAreaDemo.iOS/AppDelegate.cs

@ -0,0 +1,17 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.iOS;
using Avalonia.Media;
using Foundation;
using UIKit;
namespace SafeAreaDemo.iOS
{
// The UIApplicationDelegate for the application. This class is responsible for launching the
// User Interface of the application, as well as listening (and optionally responding) to
// application events from iOS.
[Register("AppDelegate")]
public partial class AppDelegate : AvaloniaAppDelegate<App>
{
}
}

5
samples/SafeAreaDemo.iOS/Entitlements.plist

@ -0,0 +1,5 @@
<?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/>
</plist>

47
samples/SafeAreaDemo.iOS/Info.plist

@ -0,0 +1,47 @@
<?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>CFBundleDisplayName</key>
<string>SafeAreaDemo</string>
<key>CFBundleIdentifier</key>
<string>companyName.SafeAreaDemo</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>10.0</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

15
samples/SafeAreaDemo.iOS/Main.cs

@ -0,0 +1,15 @@
using UIKit;
namespace SafeAreaDemo.iOS
{
public class Application
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}

43
samples/SafeAreaDemo.iOS/Resources/LaunchScreen.xib

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6214" systemVersion="14A314h" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6207" />
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1" />
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" />
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder" />
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480" />
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES" />
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2022 " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines"
minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
<rect key="frame" x="20" y="439" width="441" height="21" />
<fontDescription key="fontDescription" type="system" pointSize="17" />
<color key="textColor" cocoaTouchSystemColor="darkTextColor" />
<nil key="highlightedColor" />
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SafeAreaDemo" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines"
minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
<rect key="frame" x="20" y="140" width="441" height="43" />
<fontDescription key="fontDescription" type="boldSystem" pointSize="36" />
<color key="textColor" cocoaTouchSystemColor="darkTextColor" />
<nil key="highlightedColor" />
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite" />
<constraints>
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC" />
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk" />
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l" />
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0" />
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9" />
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g" />
</constraints>
<nil key="simulatedStatusBarMetrics" />
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics" />
<point key="canvasLocation" x="548" y="455" />
</view>
</objects>
</document>

18
samples/SafeAreaDemo.iOS/SafeAreaDemo.iOS.csproj

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0-ios</TargetFramework>
<SupportedOSPlatformVersion>10.0</SupportedOSPlatformVersion>
<ProvisioningType>manual</ProvisioningType>
<Nullable>enable</Nullable>
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
<!-- These properties need to be set in order to run on a real iDevice -->
<!--<RuntimeIdentifier>ios-arm64</RuntimeIdentifier>-->
<!--<CodesignKey></CodesignKey>-->
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SafeAreaDemo\SafeAreaDemo.csproj" />
<ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" />
</ItemGroup>
</Project>

15
samples/SafeAreaDemo/App.xaml

@ -0,0 +1,15 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:SafeAreaDemo"
x:Class="SafeAreaDemo.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

36
samples/SafeAreaDemo/App.xaml.cs

@ -0,0 +1,36 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using SafeAreaDemo.ViewModels;
using SafeAreaDemo.Views;
namespace SafeAreaDemo
{
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainViewModel()
};
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = new MainView
{
DataContext = new MainViewModel()
};
}
base.OnFrameworkInitializationCompleted();
}
}
}

BIN
samples/SafeAreaDemo/Assets/avalonia-logo.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

27
samples/SafeAreaDemo/SafeAreaDemo.csproj

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<AvaloniaResource Include="**\*.xaml">
<SubType>Designer</SubType>
</AvaloniaResource>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

31
samples/SafeAreaDemo/ViewLocator.cs

@ -0,0 +1,31 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using MiniMvvm;
namespace SafeAreaDemo
{
public class ViewLocator : IDataTemplate
{
public Control? Build(object? data)
{
if (data is null)
return null;
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = name };
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
}
}

112
samples/SafeAreaDemo/ViewModels/MainViewModel.cs

@ -0,0 +1,112 @@
using Avalonia;
using Avalonia.Controls.Platform;
using MiniMvvm;
namespace SafeAreaDemo.ViewModels
{
public class MainViewModel : ViewModelBase
{
private bool _useSafeArea = true;
private bool _fullscreen;
private IInsetsManager? _insetsManager;
private bool _hideSystemBars;
public Thickness SafeAreaPadding
{
get
{
return _insetsManager?.SafeAreaPadding ?? default;
}
}
public Thickness ViewPadding
{
get
{
return _useSafeArea ? SafeAreaPadding : default;
}
}
public bool UseSafeArea
{
get => _useSafeArea;
set
{
_useSafeArea = value;
this.RaisePropertyChanged();
RaiseSafeAreaChanged();
}
}
public bool Fullscreen
{
get => _fullscreen;
set
{
_fullscreen = value;
if (_insetsManager != null)
{
_insetsManager.DisplayEdgeToEdge = value;
}
this.RaisePropertyChanged();
RaiseSafeAreaChanged();
}
}
public bool HideSystemBars
{
get => _hideSystemBars;
set
{
_hideSystemBars = value;
if (_insetsManager != null)
{
_insetsManager.IsSystemBarVisible = !value;
}
this.RaisePropertyChanged();
RaiseSafeAreaChanged();
}
}
internal IInsetsManager? InsetsManager
{
get => _insetsManager;
set
{
if (_insetsManager != null)
{
_insetsManager.SafeAreaChanged -= InsetsManager_SafeAreaChanged;
}
_insetsManager = value;
if (_insetsManager != null)
{
_insetsManager.SafeAreaChanged += InsetsManager_SafeAreaChanged;
_insetsManager.DisplayEdgeToEdge = _fullscreen;
_insetsManager.IsSystemBarVisible = !_hideSystemBars;
}
}
}
private void InsetsManager_SafeAreaChanged(object? sender, SafeAreaChangedArgs e)
{
RaiseSafeAreaChanged();
}
private void RaiseSafeAreaChanged()
{
this.RaisePropertyChanged(nameof(SafeAreaPadding));
this.RaisePropertyChanged(nameof(ViewPadding));
}
}
}

52
samples/SafeAreaDemo/Views/MainView.xaml

@ -0,0 +1,52 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:SafeAreaDemo.ViewModels"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="SafeAreaDemo.Views.MainView"
x:DataType="vm:MainViewModel">
<Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border BorderBrush="Red"
Margin="{Binding ViewPadding}"
BorderThickness="1">
<Grid>
<Label Margin="5"
Foreground="Red"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Right">View Bounds</Label>
<Label Margin="5"
Foreground="Red"
VerticalAlignment="Bottom"
HorizontalContentAlignment="Right">View Bounds</Label>
</Grid>
</Border>
<Border BorderBrush="LimeGreen"
Margin="{Binding SafeAreaPadding}"
BorderThickness="1">
<DockPanel>
<Label Margin="5"
Foreground="LimeGreen"
DockPanel.Dock="Bottom"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" >Safe Area</Label>
<Grid DockPanel.Dock="Bottom"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<StackPanel Orientation="Vertical"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Label HorizontalAlignment="Left">Options:</Label>
<CheckBox IsChecked="{Binding Fullscreen}">Fullscreen</CheckBox>
<CheckBox IsChecked="{Binding UseSafeArea}">Use Safe Area</CheckBox>
<CheckBox IsChecked="{Binding HideSystemBars}">Hide System Bars</CheckBox>
<TextBox Width="200" Watermark="Tap to Show Keyboard"/>
</StackPanel>
</Grid>
</DockPanel>
</Border>
</Grid>
</UserControl>

25
samples/SafeAreaDemo/Views/MainView.xaml.cs

@ -0,0 +1,25 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using SafeAreaDemo.ViewModels;
namespace SafeAreaDemo.Views
{
public partial class MainView : UserControl
{
public MainView()
{
AvaloniaXamlLoader.Load(this);
}
protected override void OnLoaded()
{
base.OnLoaded();
var insetsManager = TopLevel.GetTopLevel(this)?.InsetsManager;
if (insetsManager != null && DataContext is MainViewModel viewModel)
{
viewModel.InsetsManager = insetsManager;
}
}
}
}

12
samples/SafeAreaDemo/Views/MainWindow.xaml

@ -0,0 +1,12 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:SafeAreaDemo.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:SafeAreaDemo.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SafeAreaDemo.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="SafeAreaDemo">
<views:MainView />
</Window>

13
samples/SafeAreaDemo/Views/MainWindow.xaml.cs

@ -0,0 +1,13 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace SafeAreaDemo.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
AvaloniaXamlLoader.Load(this);
}
}
}

22
src/Avalonia.Base/Media/Effects/BlurEffect.cs

@ -0,0 +1,22 @@
using System;
// ReSharper disable CheckNamespace
namespace Avalonia.Media;
public class BlurEffect : Effect, IBlurEffect, IMutableEffect
{
public static readonly StyledProperty<double> RadiusProperty = AvaloniaProperty.Register<BlurEffect, double>(
nameof(Radius), 5);
public double Radius
{
get => GetValue(RadiusProperty);
set => SetValue(RadiusProperty, value);
}
static BlurEffect()
{
AffectsRender<BlurEffect>(RadiusProperty);
}
public IImmutableEffect ToImmutable() => new ImmutableBlurEffect(Radius);
}

104
src/Avalonia.Base/Media/Effects/DropShadowEffect.cs

@ -0,0 +1,104 @@
// ReSharper disable once CheckNamespace
using System;
// ReSharper disable CheckNamespace
namespace Avalonia.Media;
public abstract class DropShadowEffectBase : Effect
{
public static readonly StyledProperty<double> BlurRadiusProperty =
AvaloniaProperty.Register<DropShadowEffectBase, double>(
nameof(BlurRadius), 5);
public double BlurRadius
{
get => GetValue(BlurRadiusProperty);
set => SetValue(BlurRadiusProperty, value);
}
public static readonly StyledProperty<Color> ColorProperty = AvaloniaProperty.Register<DropShadowEffectBase, Color>(
nameof(Color), Colors.Black);
public Color Color
{
get => GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
public static readonly StyledProperty<double> OpacityProperty =
AvaloniaProperty.Register<DropShadowEffectBase, double>(
nameof(Opacity), 1);
public double Opacity
{
get => GetValue(OpacityProperty);
set => SetValue(OpacityProperty, value);
}
static DropShadowEffectBase()
{
AffectsRender<DropShadowEffectBase>(BlurRadiusProperty, ColorProperty, OpacityProperty);
}
}
public class DropShadowEffect : DropShadowEffectBase, IDropShadowEffect, IMutableEffect
{
public static readonly StyledProperty<double> OffsetXProperty = AvaloniaProperty.Register<DropShadowEffect, double>(
nameof(OffsetX), 3.5355);
public double OffsetX
{
get => GetValue(OffsetXProperty);
set => SetValue(OffsetXProperty, value);
}
public static readonly StyledProperty<double> OffsetYProperty = AvaloniaProperty.Register<DropShadowEffect, double>(
nameof(OffsetY), 3.5355);
public double OffsetY
{
get => GetValue(OffsetYProperty);
set => SetValue(OffsetYProperty, value);
}
static DropShadowEffect()
{
AffectsRender<DropShadowEffect>(OffsetXProperty, OffsetYProperty);
}
public IImmutableEffect ToImmutable()
{
return new ImmutableDropShadowEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity);
}
}
/// <summary>
/// This class is compatible with WPF's DropShadowEffect and provides Direction and ShadowDepth properties instead of OffsetX/OffsetY
/// </summary>
public class DropShadowDirectionEffect : DropShadowEffectBase, IDirectionDropShadowEffect, IMutableEffect
{
public static readonly StyledProperty<double> ShadowDepthProperty =
AvaloniaProperty.Register<DropShadowDirectionEffect, double>(
nameof(ShadowDepth), 5);
public double ShadowDepth
{
get => GetValue(ShadowDepthProperty);
set => SetValue(ShadowDepthProperty, value);
}
public static readonly StyledProperty<double> DirectionProperty = AvaloniaProperty.Register<DropShadowDirectionEffect, double>(
nameof(Direction), 315);
public double Direction
{
get => GetValue(DirectionProperty);
set => SetValue(DirectionProperty, value);
}
public double OffsetX => Math.Cos(Direction * Math.PI / 180) * ShadowDepth;
public double OffsetY => Math.Sin(Direction * Math.PI / 180) * ShadowDepth;
public IImmutableEffect ToImmutable() => new ImmutableDropShadowDirectionEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity);
}

93
src/Avalonia.Base/Media/Effects/Effect.cs

@ -0,0 +1,93 @@
using System;
using Avalonia.Animation;
using Avalonia.Animation.Animators;
using Avalonia.Reactive;
using Avalonia.Rendering.Composition.Expressions;
using Avalonia.Utilities;
// ReSharper disable once CheckNamespace
namespace Avalonia.Media;
public class Effect : Animatable, IAffectsRender
{
/// <summary>
/// Marks a property as affecting the brush's visual representation.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a brush's static constructor, any change to the
/// property will cause the <see cref="Invalidated"/> event to be raised on the brush.
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : Effect
{
var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty));
foreach (var property in properties)
{
property.Changed.Subscribe(invalidateObserver);
}
}
/// <summary>
/// Raises the <see cref="Invalidated"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e);
/// <inheritdoc />
public event EventHandler? Invalidated;
static Exception ParseError(string s) => throw new ArgumentException("Unable to parse effect: " + s);
public static IEffect Parse(string s)
{
var span = s.AsSpan();
var r = new TokenParser(span);
if (r.TryConsume("blur"))
{
if (!r.TryConsume('(') || !r.TryParseDouble(out var radius) || !r.TryConsume(')') || !r.IsEofWithWhitespace())
throw ParseError(s);
return new ImmutableBlurEffect(radius);
}
if (r.TryConsume("drop-shadow"))
{
if (!r.TryConsume('(') || !r.TryParseDouble(out var offsetX)
|| !r.TryParseDouble(out var offsetY))
throw ParseError(s);
double blurRadius = 0;
var color = Colors.Black;
if (!r.TryConsume(')'))
{
if (!r.TryParseDouble(out blurRadius) || blurRadius < 0)
throw ParseError(s);
if (!r.TryConsume(')'))
{
var endOfExpression = s.LastIndexOf(")", StringComparison.Ordinal);
if (endOfExpression == -1)
throw ParseError(s);
if (!new TokenParser(span.Slice(endOfExpression + 1)).IsEofWithWhitespace())
throw ParseError(s);
if (!Color.TryParse(span.Slice(r.Position, endOfExpression - r.Position).TrimEnd(), out color))
throw ParseError(s);
return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1);
}
}
if (!r.IsEofWithWhitespace())
throw ParseError(s);
return new ImmutableDropShadowEffect(offsetX, offsetY, blurRadius, color, 1);
}
throw ParseError(s);
}
static Effect()
{
EffectAnimator.EnsureRegistered();
}
}

131
src/Avalonia.Base/Media/Effects/EffectAnimator.cs

@ -0,0 +1,131 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Media;
// ReSharper disable once CheckNamespace
namespace Avalonia.Animation.Animators;
public class EffectAnimator : Animator<IEffect?>
{
public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock,
IObservable<bool> match, Action? onComplete)
{
if (TryCreateAnimator<BlurEffectAnimator, IBlurEffect>(out var animator)
|| TryCreateAnimator<DropShadowEffectAnimator, IDropShadowEffect>(out animator))
return animator.Apply(animation, control, clock, match, onComplete);
Logger.TryGet(LogEventLevel.Error, LogArea.Animations)?.Log(
this,
"The animation's keyframe value types set is not supported.");
return base.Apply(animation, control, clock, match, onComplete);
}
private bool TryCreateAnimator<TAnimator, TInterface>([NotNullWhen(true)] out IAnimator? animator)
where TAnimator : EffectAnimatorBase<TInterface>, new() where TInterface : class, IEffect
{
TAnimator? createdAnimator = null;
foreach (var keyFrame in this)
{
if (keyFrame.Value is TInterface)
{
createdAnimator ??= new TAnimator()
{
Property = Property
};
createdAnimator.Add(new AnimatorKeyFrame(typeof(TAnimator), () => new TAnimator(), keyFrame.Cue,
keyFrame.KeySpline)
{
Value = keyFrame.Value
});
}
else
{
animator = null;
return false;
}
}
animator = createdAnimator;
return animator != null;
}
/// <summary>
/// Fallback implementation of <see cref="IEffect"/> animation.
/// </summary>
public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue) => progress >= 0.5 ? newValue : oldValue;
private static bool s_Registered;
public static void EnsureRegistered()
{
if(s_Registered)
return;
s_Registered = true;
Animation.RegisterAnimator<EffectAnimator>(prop =>
typeof(IEffect).IsAssignableFrom(prop.PropertyType));
}
}
public abstract class EffectAnimatorBase<T> : Animator<IEffect?> where T : class, IEffect?
{
public override IDisposable BindAnimation(Animatable control, IObservable<IEffect?> instance)
{
if (Property is null)
{
throw new InvalidOperationException("Animator has no property specified.");
}
return control.Bind((AvaloniaProperty<IEffect?>)Property, instance, BindingPriority.Animation);
}
protected abstract T Interpolate(double progress, T oldValue, T newValue);
public override IEffect? Interpolate(double progress, IEffect? oldValue, IEffect? newValue)
{
var old = oldValue as T;
var n = newValue as T;
if (old == null || n == null)
return progress >= 0.5 ? newValue : oldValue;
return Interpolate(progress, old, n);
}
}
public class BlurEffectAnimator : EffectAnimatorBase<IBlurEffect>
{
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
protected override IBlurEffect Interpolate(double progress, IBlurEffect oldValue, IBlurEffect newValue)
{
return new ImmutableBlurEffect(
s_doubleAnimator.Interpolate(progress, oldValue.Radius, newValue.Radius));
}
}
public class DropShadowEffectAnimator : EffectAnimatorBase<IDropShadowEffect>
{
private static readonly DoubleAnimator s_doubleAnimator = new DoubleAnimator();
protected override IDropShadowEffect Interpolate(double progress, IDropShadowEffect oldValue,
IDropShadowEffect newValue)
{
var blur = s_doubleAnimator.Interpolate(progress, oldValue.BlurRadius, newValue.BlurRadius);
var color = ColorAnimator.InterpolateCore(progress, oldValue.Color, newValue.Color);
var opacity = s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity);
if (oldValue is IDirectionDropShadowEffect oldDirection && newValue is IDirectionDropShadowEffect newDirection)
{
return new ImmutableDropShadowDirectionEffect(
s_doubleAnimator.Interpolate(progress, oldDirection.Direction, newDirection.Direction),
s_doubleAnimator.Interpolate(progress, oldDirection.ShadowDepth, newDirection.ShadowDepth),
blur, color, opacity
);
}
return new ImmutableDropShadowEffect(
s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX),
s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY),
blur, color, opacity
);
}
}

18
src/Avalonia.Base/Media/Effects/EffectConverter.cs

@ -0,0 +1,18 @@
using System;
using System.ComponentModel;
using System.Globalization;
namespace Avalonia.Media;
public class EffectConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value)
{
return value is string s ? Effect.Parse(s) : null;
}
}

56
src/Avalonia.Base/Media/Effects/EffectExtesions.cs

@ -0,0 +1,56 @@
using System;
// ReSharper disable once CheckNamespace
namespace Avalonia.Media;
public static class EffectExtensions
{
static double AdjustPaddingRadius(double radius)
{
if (radius <= 0)
return 0;
return Math.Ceiling(radius) + 1;
}
internal static Thickness GetEffectOutputPadding(this IEffect? effect)
{
if (effect == null)
return default;
if (effect is IBlurEffect blur)
return new Thickness(AdjustPaddingRadius(blur.Radius));
if (effect is IDropShadowEffect dropShadowEffect)
{
var radius = AdjustPaddingRadius(dropShadowEffect.BlurRadius);
var rc = new Rect(-radius, -radius,
radius * 2, radius * 2);
rc = rc.Translate(new(dropShadowEffect.OffsetX, dropShadowEffect.OffsetY));
return new Thickness(Math.Max(0, 0 - rc.X),
Math.Max(0, 0 - rc.Y), Math.Max(0, rc.Right), Math.Max(0, rc.Bottom));
}
throw new ArgumentException("Unknown effect type: " + effect.GetType());
}
/// <summary>
/// Converts a effect to an immutable effect.
/// </summary>
/// <param name="effect">The effect.</param>
/// <returns>
/// The result of calling <see cref="IMutableEffect.ToImmutable"/> if the effect is mutable,
/// otherwise <paramref name="effect"/>.
/// </returns>
public static IImmutableEffect ToImmutable(this IEffect effect)
{
_ = effect ?? throw new ArgumentNullException(nameof(effect));
return (effect as IMutableEffect)?.ToImmutable() ?? (IImmutableEffect)effect;
}
internal static bool EffectEquals(this IImmutableEffect? immutable, IEffect? right)
{
if (immutable == null && right == null)
return true;
if (immutable != null && right != null)
return immutable.Equals(right);
return false;
}
}

83
src/Avalonia.Base/Media/Effects/EffectTransition.cs

@ -0,0 +1,83 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
using Avalonia.Media;
// ReSharper disable once CheckNamespace
namespace Avalonia.Animation;
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="IEffect"/> type.
/// </summary>
public class EffectTransition : Transition<IEffect?>
{
private static readonly BlurEffectAnimator s_blurEffectAnimator = new();
private static readonly DropShadowEffectAnimator s_dropShadowEffectAnimator = new();
private static readonly ImmutableBlurEffect s_DefaultBlur = new ImmutableBlurEffect(0);
private static readonly ImmutableDropShadowDirectionEffect s_DefaultDropShadow = new(0, 0, 0, default, 0);
bool TryWithAnimator<TAnimator, TInterface>(
IObservable<double> progress,
TAnimator animator,
IEffect? oldValue, IEffect? newValue, TInterface defaultValue, [MaybeNullWhen(false)] out IObservable<IEffect?> observable)
where TAnimator : EffectAnimatorBase<TInterface> where TInterface : class, IEffect
{
observable = null;
TInterface? oldI = null, newI = null;
if (oldValue is TInterface oi)
{
oldI = oi;
if (newValue is TInterface ni)
newI = ni;
else if (newValue == null)
newI = defaultValue;
else
return false;
}
else if (newValue is TInterface nv)
{
oldI = defaultValue;
newI = nv;
}
else
return false;
observable = new AnimatorTransitionObservable<IEffect?, Animator<IEffect?>>(animator, progress, Easing, oldI, newI);
return true;
}
public override IObservable<IEffect?> DoTransition(IObservable<double> progress, IEffect? oldValue, IEffect? newValue)
{
if ((oldValue != null || newValue != null)
&& (
TryWithAnimator<BlurEffectAnimator, IBlurEffect>(progress, s_blurEffectAnimator,
oldValue, newValue, s_DefaultBlur, out var observable)
|| TryWithAnimator<DropShadowEffectAnimator, IDropShadowEffect>(progress, s_dropShadowEffectAnimator,
oldValue, newValue, s_DefaultDropShadow, out observable)
))
return observable;
return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue);
}
private sealed class IncompatibleTransitionObservable : TransitionObservableBase<IEffect?>
{
private readonly IEffect? _from;
private readonly IEffect? _to;
public IncompatibleTransitionObservable(IObservable<double> progress, Easing easing, IEffect? from, IEffect? to) : base(progress, easing)
{
_from = from;
_to = to;
}
protected override IEffect? ProduceValue(double progress)
{
return progress >= 0.5 ? _to : _from;
}
}
}

29
src/Avalonia.Base/Media/Effects/IBlurEffect.cs

@ -0,0 +1,29 @@
// ReSharper disable once CheckNamespace
using Avalonia.Animation.Animators;
namespace Avalonia.Media;
public interface IBlurEffect : IEffect
{
double Radius { get; }
}
public class ImmutableBlurEffect : IBlurEffect, IImmutableEffect
{
static ImmutableBlurEffect()
{
EffectAnimator.EnsureRegistered();
}
public ImmutableBlurEffect(double radius)
{
Radius = radius;
}
public double Radius { get; }
public bool Equals(IEffect? other) =>
// ReSharper disable once CompareOfFloatsByEqualityOperator
other is IBlurEffect blur && blur.Radius == Radius;
}

84
src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs

@ -0,0 +1,84 @@
// ReSharper disable once CheckNamespace
using System;
using Avalonia.Animation.Animators;
namespace Avalonia.Media;
public interface IDropShadowEffect : IEffect
{
double OffsetX { get; }
double OffsetY { get; }
double BlurRadius { get; }
Color Color { get; }
double Opacity { get; }
}
internal interface IDirectionDropShadowEffect : IDropShadowEffect
{
double Direction { get; }
double ShadowDepth { get; }
}
public class ImmutableDropShadowEffect : IDropShadowEffect, IImmutableEffect
{
static ImmutableDropShadowEffect()
{
EffectAnimator.EnsureRegistered();
}
public ImmutableDropShadowEffect(double offsetX, double offsetY, double blurRadius, Color color, double opacity)
{
OffsetX = offsetX;
OffsetY = offsetY;
BlurRadius = blurRadius;
Color = color;
Opacity = opacity;
}
public double OffsetX { get; }
public double OffsetY { get; }
public double BlurRadius { get; }
public Color Color { get; }
public double Opacity { get; }
public bool Equals(IEffect? other)
{
return other is IDropShadowEffect d
&& d.OffsetX == OffsetX && d.OffsetY == OffsetY
&& d.BlurRadius == BlurRadius
&& d.Color == Color && d.Opacity == Opacity;
}
}
public class ImmutableDropShadowDirectionEffect : IDirectionDropShadowEffect, IImmutableEffect
{
static ImmutableDropShadowDirectionEffect()
{
EffectAnimator.EnsureRegistered();
}
public ImmutableDropShadowDirectionEffect(double direction, double shadowDepth, double blurRadius, Color color, double opacity)
{
Direction = direction;
ShadowDepth = shadowDepth;
BlurRadius = blurRadius;
Color = color;
Opacity = opacity;
}
public double OffsetX => Math.Cos(Direction * Math.PI / 180) * ShadowDepth;
public double OffsetY => Math.Sin(Direction * Math.PI / 180) * ShadowDepth;
public double Direction { get; }
public double ShadowDepth { get; }
public double BlurRadius { get; }
public Color Color { get; }
public double Opacity { get; }
public bool Equals(IEffect? other)
{
return other is IDropShadowEffect d
&& d.OffsetX == OffsetX && d.OffsetY == OffsetY
&& d.BlurRadius == BlurRadius
&& d.Color == Color && d.Opacity == Opacity;
}
}

26
src/Avalonia.Base/Media/Effects/IEffect.cs

@ -0,0 +1,26 @@
// ReSharper disable once CheckNamespace
using System;
using System.ComponentModel;
namespace Avalonia.Media;
[TypeConverter(typeof(EffectConverter))]
public interface IEffect
{
}
public interface IMutableEffect : IEffect, IAffectsRender
{
/// <summary>
/// Creates an immutable clone of the effect.
/// </summary>
/// <returns>The immutable clone.</returns>
internal IImmutableEffect ToImmutable();
}
public interface IImmutableEffect : IEffect, IEquatable<IEffect>
{
}

6
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -180,6 +180,12 @@ namespace Avalonia.Platform
object? GetFeature(Type t);
}
public interface IDrawingContextImplWithEffects
{
void PushEffect(IEffect effect);
void PopEffect();
}
public static class DrawingContextImplExtensions
{
/// <summary>

9
src/Avalonia.Base/Rect.cs

@ -526,6 +526,15 @@ namespace Avalonia
}
}
internal static Rect? Union(Rect? left, Rect? right)
{
if (left == null)
return right;
if (right == null)
return left;
return left.Value.Union(right.Value);
}
/// <summary>
/// Returns a new <see cref="Rect"/> with the specified X position.
/// </summary>

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

@ -252,8 +252,14 @@ public class CompositingRenderer : IRendererWithCompositor
comp.Opacity = (float)visual.Opacity;
comp.ClipToBounds = visual.ClipToBounds;
comp.Clip = visual.Clip?.PlatformImpl;
comp.OpacityMask = visual.OpacityMask;
if (!Equals(comp.OpacityMask, visual.OpacityMask))
comp.OpacityMask = visual.OpacityMask?.ToImmutable();
if (!comp.Effect.EffectEquals(visual.Effect))
comp.Effect = visual.Effect?.ToImmutable();
var renderTransform = Matrix.Identity;
if (visual.HasMirrorTransform)

56
src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs

@ -29,6 +29,8 @@ namespace Avalonia.Rendering.Composition.Expressions
}
}
public bool NextIsWhitespace() => _s.Length > 0 && char.IsWhiteSpace(_s[0]);
static bool IsAlphaNumeric(char ch) => (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z');
@ -238,6 +240,12 @@ namespace Avalonia.Rendering.Composition.Expressions
len = c + 1;
dotCount++;
}
else if (ch == '-')
{
if (len != 0)
return false;
len = c + 1;
}
else
break;
}
@ -254,7 +262,55 @@ namespace Avalonia.Rendering.Composition.Expressions
Advance(len);
return true;
}
public bool TryParseDouble(out double res)
{
res = 0;
SkipWhitespace();
if (_s.Length == 0)
return false;
var len = 0;
var dotCount = 0;
for (var c = 0; c < _s.Length; c++)
{
var ch = _s[c];
if (ch >= '0' && ch <= '9')
len = c + 1;
else if (ch == '.' && dotCount == 0)
{
len = c + 1;
dotCount++;
}
else if (ch == '-')
{
if (len != 0)
return false;
len = c + 1;
}
else
break;
}
var span = _s.Slice(0, len);
#if NETSTANDARD2_0
if (!double.TryParse(span.ToString(), NumberStyles.Number, CultureInfo.InvariantCulture, out res))
return false;
#else
if (!double.TryParse(span, NumberStyles.Number, CultureInfo.InvariantCulture, out res))
return false;
#endif
Advance(len);
return true;
}
public bool IsEofWithWhitespace()
{
SkipWhitespace();
return Length == 0;
}
public override string ToString() => _s.ToString();
}

15
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -18,7 +18,8 @@ namespace Avalonia.Rendering.Composition.Server;
/// they have information about the full render transform (they are not)
/// 2) Keeps the draw list for the VisualBrush contents of the current drawing operation.
/// </summary>
internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
internal class CompositorDrawingContextProxy : IDrawingContextImpl,
IDrawingContextWithAcrylicLikeSupport, IDrawingContextImplWithEffects
{
private IDrawingContextImpl _impl;
@ -155,4 +156,16 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont
if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic)
acrylic.DrawRectangle(material, rect);
}
public void PushEffect(IEffect effect)
{
if (_impl is IDrawingContextImplWithEffects effects)
effects.PushEffect(effect);
}
public void PopEffect()
{
if (_impl is IDrawingContextImplWithEffects effects)
effects.PopEffect();
}
}

68
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs

@ -1,4 +1,6 @@
using System;
using System.Numerics;
using Avalonia.Media;
using Avalonia.Platform;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -13,6 +15,8 @@ namespace Avalonia.Rendering.Composition.Server
internal partial class ServerCompositionContainerVisual : ServerCompositionVisual
{
public ServerCompositionVisualCollection Children { get; private set; } = null!;
private Rect? _transformedContentBounds;
private IImmutableEffect? _oldEffect;
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
{
@ -24,18 +28,76 @@ namespace Avalonia.Rendering.Composition.Server
}
}
public override void Update(ServerCompositionTarget root)
public override UpdateResult Update(ServerCompositionTarget root)
{
base.Update(root);
var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root);
foreach (var child in Children)
{
if (child.AdornedVisual != null)
root.EnqueueAdornerUpdate(child);
else
child.Update(root);
{
var res = child.Update(root);
oldInvalidated |= res.InvalidatedOld;
newInvalidated |= res.InvalidatedNew;
combinedBounds = Rect.Union(combinedBounds, res.Bounds);
}
}
// If effect is changed, we need to clean both old and new bounds
var effectChanged = !Effect.EffectEquals(_oldEffect);
if (effectChanged)
oldInvalidated = newInvalidated = true;
// Expand invalidated bounds to the whole content area since we don't actually know what is being sampled
// We also ignore clip for now since we don't have means to reset it?
if (_oldEffect != null && oldInvalidated && _transformedContentBounds.HasValue)
AddEffectPaddedDirtyRect(_oldEffect, _transformedContentBounds.Value);
if (Effect != null && newInvalidated && combinedBounds.HasValue)
AddEffectPaddedDirtyRect(Effect, combinedBounds.Value);
_oldEffect = Effect;
_transformedContentBounds = combinedBounds;
IsDirtyComposition = false;
return new(_transformedContentBounds, oldInvalidated, newInvalidated);
}
void AddEffectPaddedDirtyRect(IImmutableEffect effect, Rect transformedBounds)
{
var padding = effect.GetEffectOutputPadding();
if (padding == default)
{
AddDirtyRect(transformedBounds);
return;
}
// We are in a weird position here: bounds are in global coordinates while padding gets applied in local ones
// Since we have optimizations to AVOID recomputing transformed bounds and since visuals with effects are relatively rare
// we instead apply the transformation matrix to rescale the bounds
// If we only have translation and scale, just scale the padding
if (CombinedTransformMatrix is
{
M12: 0, M13: 0, M14: 0,
M21: 0, M23: 0, M24: 0,
M31: 0, M32: 0, M34: 0,
M43: 0, M44: 1
})
padding = new Thickness(padding.Left * CombinedTransformMatrix.M11,
padding.Top * CombinedTransformMatrix.M22,
padding.Right * CombinedTransformMatrix.M11,
padding.Bottom * CombinedTransformMatrix.M22);
else
{
// Conservatively use the transformed rect size
var transformedPaddingRect = new Rect().Inflate(padding).TransformToAABB(CombinedTransformMatrix);
padding = new(Math.Max(transformedPaddingRect.Width, transformedPaddingRect.Height));
}
AddDirtyRect(transformedBounds.Inflate(padding));
}
partial void Initialize()

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

@ -54,6 +54,9 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PostTransform = MatrixUtils.ToMatrix(transform);
canvas.Transform = Matrix.Identity;
if (Effect != null)
canvas.PushEffect(Effect);
if (Opacity != 1)
canvas.PushOpacity(Opacity, boundsRect);
if (ClipToBounds && !HandlesClipToBounds)
@ -79,6 +82,9 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopClip();
if (Opacity != 1)
canvas.PopOpacity();
if (Effect != null)
canvas.PopEffect();
}
protected virtual bool HandlesClipToBounds => false;
@ -101,10 +107,18 @@ namespace Avalonia.Rendering.Composition.Server
public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity;
public Matrix4x4 GlobalTransformMatrix { get; private set; }
public virtual void Update(ServerCompositionTarget root)
public record struct UpdateResult(Rect? Bounds, bool InvalidatedOld, bool InvalidatedNew)
{
public UpdateResult() : this(null, false, false)
{
}
}
public virtual UpdateResult Update(ServerCompositionTarget root)
{
if (Parent == null && Root == null)
return;
return default;
var wasVisible = IsVisibleInFrame;
@ -146,6 +160,11 @@ namespace Avalonia.Rendering.Composition.Server
GlobalTransformMatrix = newTransform;
var ownBounds = OwnContentBounds;
// Since padding is applied in the current visual's coordinate space we expand bounds before transforming them
if (Effect != null)
ownBounds = ownBounds.Inflate(Effect.GetEffectOutputPadding());
if (ownBounds != _oldOwnContentBounds || positionChanged)
{
_oldOwnContentBounds = ownBounds;
@ -168,7 +187,7 @@ namespace Avalonia.Rendering.Composition.Server
_combinedTransformedClipBounds =
AdornedVisual?._combinedTransformedClipBounds
?? Parent?._combinedTransformedClipBounds
?? (Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null)
?? new Rect(Root!.Size);
if (_transformedClipBounds != null)
@ -208,9 +227,10 @@ namespace Avalonia.Rendering.Composition.Server
readback.Matrix = GlobalTransformMatrix;
readback.TargetId = Root.Id;
readback.Visible = IsHitTestVisibleInFrame;
return new(TransformedOwnContentBounds, invalidateNewBounds, invalidateOldBounds);
}
void AddDirtyRect(Rect rc)
protected void AddDirtyRect(Rect rc)
{
if (rc == default)
return;

5
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

@ -53,7 +53,10 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public override bool HitTestTransformed(Point p) => Bounds.ContainsExclusive(p);
public override bool HitTestTransformed(Point p)
{
return GlyphRun.Item.Bounds.ContainsExclusive(p);
}
public override void Dispose()
{

18
src/Avalonia.Base/StyledElement.cs

@ -78,7 +78,7 @@ namespace Avalonia
private static readonly ControlTheme s_invalidTheme = new ControlTheme();
private int _initCount;
private string? _name;
private readonly Classes _classes = new Classes();
private Classes? _classes;
private ILogicalRoot? _logicalRoot;
private IAvaloniaList<ILogical>? _logicalChildren;
private IResourceDictionary? _resources;
@ -183,21 +183,7 @@ namespace Avalonia
/// collection.
/// </para>
/// </remarks>
public Classes Classes
{
get
{
return _classes;
}
set
{
if (_classes != value)
{
_classes.Replace(value);
}
}
}
public Classes Classes => _classes ??= new();
/// <summary>
/// Gets or sets the control's data context.

20
src/Avalonia.Base/Visual.cs

@ -48,7 +48,7 @@ namespace Avalonia
/// </summary>
public static readonly StyledProperty<Geometry?> ClipProperty =
AvaloniaProperty.Register<Visual, Geometry?>(nameof(Clip));
/// <summary>
/// Defines the <see cref="IsVisible"/> property.
/// </summary>
@ -66,6 +66,12 @@ namespace Avalonia
/// </summary>
public static readonly StyledProperty<IBrush?> OpacityMaskProperty =
AvaloniaProperty.Register<Visual, IBrush?>(nameof(OpacityMask));
/// <summary>
/// Defines the <see cref="Effect"/> property.
/// </summary>
public static readonly StyledProperty<IEffect?> EffectProperty =
AvaloniaProperty.Register<Visual, IEffect?>(nameof(Effect));
/// <summary>
/// Defines the <see cref="HasMirrorTransform"/> property.
@ -127,6 +133,8 @@ namespace Avalonia
ClipToBoundsProperty,
IsVisibleProperty,
OpacityProperty,
OpacityMaskProperty,
EffectProperty,
HasMirrorTransformProperty);
RenderTransformProperty.Changed.Subscribe(RenderTransformChanged);
ZIndexProperty.Changed.Subscribe(ZIndexChanged);
@ -233,6 +241,16 @@ namespace Avalonia
get { return GetValue(OpacityMaskProperty); }
set { SetValue(OpacityMaskProperty, value); }
}
/// <summary>
/// Gets or sets the effect of the control.
/// </summary>
public IEffect? Effect
{
get => GetValue(EffectProperty);
set => SetValue(EffectProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether to apply mirror transform on this control.

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

@ -6,7 +6,8 @@
<Using>Avalonia.Rendering.Composition.Animations</Using>
<Manual Name="Avalonia.Platform.IGeometryImpl" Passthrough="true"/>
<Manual Name="Avalonia.Media.IBrush" Passthrough="true"/>
<Manual Name="Avalonia.Media.IImmutableBrush" Passthrough="true"/>
<Manual Name="Avalonia.Media.IImmutableEffect" Passthrough="true"/>
<Manual Name="CompositionSurface" />
<Manual Name="CompositionDrawingSurface" />
<Object Name="CompositionVisual" Abstract="true">
@ -27,7 +28,8 @@
<Property Name="TransformMatrix" Type="Matrix4x4" DefaultValue="Matrix4x4.Identity" Animated="true"/>
<Property Name="AdornedVisual" Type="CompositionVisual?" Internal="true" />
<Property Name="AdornerIsClipped" Type="bool" Internal="true" />
<Property Name="OpacityMaskBrush" Type="Avalonia.Media.IBrush?" Internal="true" />
<Property Name="OpacityMaskBrush" Type="Avalonia.Media.IImmutableBrush?" Internal="true" />
<Property Name="Effect" Type="Avalonia.Media.IImmutableEffect?" Internal="true" />
</Object>
<Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/>
<Object Name="CompositionSolidColorVisual" Inherits="CompositionContainerVisual">

28
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -3957,6 +3957,7 @@ namespace Avalonia.Controls
bool focusLeftDataGrid = true;
bool dataGridWillReceiveRoutedEvent = true;
Visual focusedObject = FocusManager.Instance.Current as Visual;
DataGridColumn editingColumn = null;
while (focusedObject != null)
{
@ -3969,22 +3970,29 @@ namespace Avalonia.Controls
// Walk up the visual tree. If we hit the root, try using the framework element's
// parent. We do this because Popups behave differently with respect to the visual tree,
// and it could have a parent even if the VisualTreeHelper doesn't find it.
Visual parent = focusedObject.GetVisualParent();
var parent = focusedObject.Parent as Visual;
if (parent == null)
{
if (focusedObject is Control element)
{
parent = element.VisualParent;
if (parent != null)
{
dataGridWillReceiveRoutedEvent = false;
}
}
parent = focusedObject.GetVisualParent();
}
else
{
dataGridWillReceiveRoutedEvent = false;
}
focusedObject = parent;
}
if (focusLeftDataGrid)
if (EditingRow != null && EditingColumnIndex != -1)
{
editingColumn = ColumnsItemsInternal[EditingColumnIndex];
if (focusLeftDataGrid && editingColumn is DataGridTemplateColumn)
{
dataGridWillReceiveRoutedEvent = false;
}
}
if (focusLeftDataGrid && !(editingColumn is DataGridTemplateColumn))
{
ContainsFocus = false;
if (EditingRow != null)

2
src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs

@ -454,7 +454,7 @@ namespace Avalonia.Controls.Primitives
children.Add(new ListBoxItem
{
Height = ItemHeight,
Classes = new Classes($"{PanelType}Item"),
Classes = { $"{PanelType}Item" },
VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Center,
Focusable = false
});

12
src/Avalonia.Controls/Flyouts/Flyout.cs

@ -18,17 +18,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets the Classes collection to apply to the FlyoutPresenter this Flyout is hosting
/// </summary>
public Classes FlyoutPresenterClasses
{
get => _classes ??= new Classes();
set
{
if (_classes is null)
_classes = value;
else if (_classes != value)
_classes.Replace(value);
}
}
public Classes FlyoutPresenterClasses => _classes ??= new Classes();
/// <summary>
/// Defines the <see cref="FlyoutPresenterTheme"/> property.

35
src/Avalonia.X11/X11Window.cs

@ -24,6 +24,7 @@ using Avalonia.X11.Glx;
using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.XLib;
using Avalonia.Input.Platform;
using System.Runtime.InteropServices;
// ReSharper disable IdentifierTypo
// ReSharper disable StringLiteralTypo
@ -629,6 +630,8 @@ namespace Avalonia.X11
ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_FULLSCREEN);
ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT,
_x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ);
SendNetWMMessage(_x11.Atoms._NET_ACTIVE_WINDOW, (IntPtr)1, _x11.LastActivityTimestamp,
IntPtr.Zero);
}
}
}
@ -735,6 +738,10 @@ namespace Avalonia.X11
{
if (_inputRoot is null)
return;
if (_disabled && args is RawPointerEventArgs pargs && pargs.Type == RawPointerEventType.Move)
return;
Input?.Invoke(args);
if (!args.Handled && args is RawKeyEventArgsWithText text && !string.IsNullOrEmpty(text.Text))
Input?.Invoke(new RawTextInputEventArgs(_keyboard, args.Timestamp, _inputRoot, text.Text));
@ -1201,6 +1208,32 @@ namespace Avalonia.X11
public void SetEnabled(bool enable)
{
_disabled = !enable;
UpdateWMHints();
}
private void UpdateWMHints()
{
var wmHintsPtr = XGetWMHints(_x11.Display, _handle);
XWMHints hints = default;
if (wmHintsPtr != IntPtr.Zero)
{
hints = Marshal.PtrToStructure<XWMHints>(wmHintsPtr);
}
var flags = hints.flags.ToInt64();
flags |= (long)XWMHintsFlags.InputHint;
hints.flags = (IntPtr)flags;
hints.input = !_disabled ? 1 : 0;
XSetWMHints(_x11.Display, _handle, ref hints);
if (wmHintsPtr != IntPtr.Zero)
{
XFree(wmHintsPtr);
}
}
public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint)
@ -1290,6 +1323,8 @@ namespace Avalonia.X11
public bool NeedsManagedDecorations => false;
public bool IsEnabled => !_disabled;
public class SurfacePlatformHandle : IPlatformNativeSurfaceHandle
{
private readonly X11Window _owner;

3
src/Avalonia.X11/XI2Manager.cs

@ -231,7 +231,7 @@ namespace Avalonia.X11
return;
}
if (_multitouch && ev.Emulated)
if (!client.IsEnabled || (_multitouch && ev.Emulated))
return;
if (ev.Type == XiEventType.XI_Motion)
@ -370,6 +370,7 @@ namespace Avalonia.X11
internal interface IXI2Client
{
bool IsEnabled { get; }
IInputRoot InputRoot { get; }
void ScheduleXI2Input(RawInputEventArgs args);
IMouseDevice MouseDevice { get; }

3
src/Avalonia.X11/XLib.cs

@ -375,6 +375,9 @@ namespace Avalonia.X11
[DllImport(libX11)]
public static extern void XSetWMHints(IntPtr display, IntPtr window, ref XWMHints wmhints);
[DllImport(libX11)]
public static extern IntPtr XGetWMHints(IntPtr display, IntPtr window);
[DllImport(libX11)]
public static extern int XGetIconSizes(IntPtr display, IntPtr window, out IntPtr size_list, out int count);

3
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -43,7 +43,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers());
InsertAfter<PropertyReferenceResolver>(
new AvaloniaXamlIlAvaloniaPropertyResolver(),
new AvaloniaXamlIlReorderClassesPropertiesTransformer()
new AvaloniaXamlIlReorderClassesPropertiesTransformer(),
new AvaloniaXamlIlClassesTransformer()
);
InsertBefore<ContentConvertTransformer>(

9
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs

@ -221,15 +221,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
return ConvertDefinitionList(node, text, types, types.RowDefinitions, types.RowDefinition, "row definitions", out result);
}
if (type.Equals(types.Classes))
{
var classes = text.Split(' ');
var classNodes = classes.Select(c => new XamlAstTextNode(node, c, type: types.XamlIlTypes.String)).ToArray();
result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, types.Classes, types.XamlIlTypes.String, classNodes);
return true;
}
if (types.IBrush.IsAssignableFrom(type))
{
if (Color.TryParse(text, out Color color))

46
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXAmlIlClassesTransformer.cs

@ -0,0 +1,46 @@
using System.Linq;
using XamlX.Ast;
using XamlX.Transform;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
/// <summary>
/// Converts an attribute syntax property value assignment to a collection syntax property
/// assignment.
/// </summary>
/// <remarks>
/// Converts the property assignment `Classes="foo bar"` to:
///
/// <code>
/// <StyledElement.Classes>
/// <x:String>foo</String>
/// <x:String>bar</String>
/// </StyledElement.Classes>
/// </code>
/// </remarks>
class AvaloniaXamlIlClassesTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
var types = context.GetAvaloniaTypes();
if (node is XamlAstXamlPropertyValueNode propertyValue &&
propertyValue.IsAttributeSyntax &&
propertyValue.Property is XamlAstClrProperty property &&
property.Getter?.ReturnType.Equals(types.Classes) == true &&
propertyValue.Values.Count == 1 &&
propertyValue.Values[0] is XamlAstTextNode value)
{
var classes = value.Text.Split(' ');
var stringType = context.Configuration.WellKnownTypes.String;
return new XamlAstXamlPropertyValueNode(
node,
property,
classes.Select(x => new XamlAstTextNode(node, x, type: stringType)),
false);
}
return node;
}
}
}

50
src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs

@ -0,0 +1,50 @@
using System;
using Avalonia.Media;
using SkiaSharp;
namespace Avalonia.Skia;
partial class DrawingContextImpl
{
public void PushEffect(IEffect effect)
{
CheckLease();
using var filter = CreateEffect(effect);
var paint = SKPaintCache.Shared.Get();
paint.ImageFilter = filter;
Canvas.SaveLayer(paint);
SKPaintCache.Shared.ReturnReset(paint);
}
public void PopEffect()
{
CheckLease();
Canvas.Restore();
}
SKImageFilter? CreateEffect(IEffect effect)
{
if (effect is IBlurEffect blur)
{
if (blur.Radius <= 0)
return null;
var sigma = SkBlurRadiusToSigma(blur.Radius);
return SKImageFilter.CreateBlur(sigma, sigma);
}
if (effect is IDropShadowEffect drop)
{
var sigma = drop.BlurRadius > 0 ? SkBlurRadiusToSigma(drop.BlurRadius) : 0;
var alpha = drop.Color.A * drop.Opacity;
if (!_useOpacitySaveLayer)
alpha *= _currentOpacity;
var color = new SKColor(drop.Color.R, drop.Color.G, drop.Color.B, (byte)Math.Max(0, Math.Min(255, alpha)));
return SKImageFilter.CreateDropShadow((float)drop.OffsetX, (float)drop.OffsetY, sigma, sigma, color);
}
return null;
}
}

16
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -19,7 +19,9 @@ namespace Avalonia.Skia
/// <summary>
/// Skia based drawing context.
/// </summary>
internal class DrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
internal partial class DrawingContextImpl : IDrawingContextImpl,
IDrawingContextWithAcrylicLikeSupport,
IDrawingContextImplWithEffects
{
private IDisposable?[]? _disposables;
private readonly Vector _dpi;
@ -249,6 +251,12 @@ namespace Avalonia.Skia
}
}
private static float SkBlurRadiusToSigma(double radius) {
if (radius <= 0)
return 0.0f;
return 0.288675f * (float)radius + 0.5f;
}
private struct BoxShadowFilter : IDisposable
{
public readonly SKPaint Paint;
@ -262,12 +270,6 @@ namespace Avalonia.Skia
ClipOperation = clipOperation;
}
private static float SkBlurRadiusToSigma(double radius) {
if (radius <= 0)
return 0.0f;
return 0.288675f * (float)radius + 0.5f;
}
public static BoxShadowFilter Create(SKPaint paint, BoxShadow shadow, double opacity)
{
var ac = shadow.Color;

73
tests/Avalonia.Base.UnitTests/Media/EffectTests.cs

@ -0,0 +1,73 @@
using System;
using Avalonia.Media;
using Xunit;
namespace Avalonia.Base.UnitTests.Media;
public class EffectTests
{
[Fact]
public void Parse_Parses_Blur()
{
var effect = (ImmutableBlurEffect)Effect.Parse("blur(123.34)");
Assert.Equal(123.34, effect.Radius);
}
private const uint Black = 0xff000000;
[Theory,
InlineData("drop-shadow(10 20)", 10, 20, 0, Black),
InlineData("drop-shadow( 10 20 ) ", 10, 20, 0, Black),
InlineData("drop-shadow( 10 20 30 ) ", 10, 20, 30, Black),
InlineData("drop-shadow(10 20 30)", 10, 20, 30, Black),
InlineData("drop-shadow(-10 -20 30)", -10, -20, 30, Black),
InlineData("drop-shadow(10 20 30 #ffff00ff)", 10, 20, 30, 0xffff00ff),
InlineData("drop-shadow ( 10 20 30 #ffff00ff ) ", 10, 20, 30, 0xffff00ff),
InlineData("drop-shadow(10 20 30 red)", 10, 20, 30, 0xffff0000),
InlineData("drop-shadow ( 10 20 30 red ) ", 10, 20, 30, 0xffff0000),
InlineData("drop-shadow(10 20 30 rgba(100, 30, 45, 90%))", 10, 20, 30, 0x90641e2d),
InlineData("drop-shadow(10 20 30 rgba(100, 30, 45, 90%) ) ", 10, 20, 30, 0x90641e2d),
]
public void Parse_Parses_DropShadow(string s, double x, double y, double r, uint color)
{
var effect = (ImmutableDropShadowEffect)Effect.Parse(s);
Assert.Equal(x, effect.OffsetX);
Assert.Equal(y, effect.OffsetY);
Assert.Equal(r, effect.BlurRadius);
Assert.Equal(1, effect.Opacity);
}
[Theory,
InlineData("blur"),
InlineData("blur("),
InlineData("blur()"),
InlineData("blur(123"),
InlineData("blur(aaab)"),
InlineData("drop-shadow(-10 -20 -30)"),
]
public void Invalid_Effect_Parse_Fails(string b)
{
Assert.Throws<ArgumentException>(() => Effect.Parse(b));
}
[Theory,
InlineData("blur(2.5)", 4, 4, 4, 4),
InlineData("blur(0)", 0, 0, 0, 0),
InlineData("drop-shadow(10 15)", 0, 0, 10, 15),
InlineData("drop-shadow(10 15 5)", 0, 0, 16, 21),
InlineData("drop-shadow(0 0 5)", 6, 6, 6, 6),
InlineData("drop-shadow(3 3 5)", 3, 3, 9, 9)
]
public static void PaddingIsCorrectlyCalculated(string effect, double left, double top, double right, double bottom)
{
var padding = Effect.Parse(effect).GetEffectOutputPadding();
Assert.Equal(left, padding.Left);
Assert.Equal(top, padding.Top);
Assert.Equal(right, padding.Right);
Assert.Equal(bottom, padding.Bottom);
}
}

32
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@ -1,24 +1,20 @@
using System;
using System.Collections;
using System.ComponentModel;
using System.Linq;
using System.Xml;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Presenters;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Metadata;
using Avalonia.Styling;
using Avalonia.UnitTests;
using System.Collections;
using System.ComponentModel;
using System.Linq;
using System.Xml;
using Xunit;
using Avalonia.Controls.Documents;
using Avalonia.Metadata;
using Avalonia.Themes.Simple;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
@ -920,6 +916,22 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal(new[] { "foo", "bar" }, target.Classes);
}
[Fact]
public void Can_Specify_Button_Classes_Longform()
{
var xaml = @"
<Button xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Button.Classes>
<x:String>foo</x:String>
<x:String>bar</x:String>
</Button.Classes>
</Button>";
var target = (Button)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new[] { "foo", "bar" }, target.Classes);
}
[Fact]
public void Can_Specify_Flyout_FlyoutPresenterClasses()
{

43
tests/Avalonia.RenderTests/Media/EffectTests.cs

@ -0,0 +1,43 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media;
using Xunit;
#pragma warning disable CS0649
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests;
public class EffectTests : TestBase
{
public EffectTests() : base(@"Media\Effects")
{
}
[Fact]
public async Task DropShadowEffect()
{
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new Border()
{
Background = null,
Margin = new Thickness(40),
Effect = new ImmutableDropShadowEffect(20, 30, 5, Colors.Green, 1),
Child = new Border
{
Background = new SolidColorBrush(Color.FromArgb(128, 0, 0, 255)),
BorderBrush = Brushes.Red,
BorderThickness = new Thickness(5)
}
}
};
await RenderToFile(target);
CompareImages(skipImmediate: true);
}
}
#endif

3
tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj

@ -6,6 +6,9 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.RenderTests\**\*.cs" />
<Compile Update="..\Avalonia.RenderTests\Media\EffectTests.cs">
<Link>Media\EffectTests.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="..\Avalonia.RenderTests\*\*.ttf" />

BIN
tests/TestFiles/Skia/Media/Effects/DropShadowEffect.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Loading…
Cancel
Save