diff --git a/Avalonia.sln b/Avalonia.sln index b21df07628..f33b782479 100644 --- a/Avalonia.sln +++ b/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} diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml index 3f89a9d5f7..d764af8978 100644 --- a/samples/RenderDemo/Pages/AnimationsPage.xaml +++ b/samples/RenderDemo/Pages/AnimationsPage.xaml @@ -308,6 +308,41 @@ + + @@ -332,6 +367,11 @@ + + + Drop + Shadow + diff --git a/samples/SafeAreaDemo.Android/Icon.png b/samples/SafeAreaDemo.Android/Icon.png new file mode 100644 index 0000000000..41a2a618fb Binary files /dev/null and b/samples/SafeAreaDemo.Android/Icon.png differ diff --git a/samples/SafeAreaDemo.Android/MainActivity.cs b/samples/SafeAreaDemo.Android/MainActivity.cs new file mode 100644 index 0000000000..b0f0a6e419 --- /dev/null +++ b/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 + { + } +} diff --git a/samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml b/samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml new file mode 100644 index 0000000000..b6a5777e03 --- /dev/null +++ b/samples/SafeAreaDemo.Android/Properties/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/SafeAreaDemo.Android/Resources/drawable/splash_screen.xml b/samples/SafeAreaDemo.Android/Resources/drawable/splash_screen.xml new file mode 100644 index 0000000000..2e920b4b3b --- /dev/null +++ b/samples/SafeAreaDemo.Android/Resources/drawable/splash_screen.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/samples/SafeAreaDemo.Android/Resources/values/colors.xml b/samples/SafeAreaDemo.Android/Resources/values/colors.xml new file mode 100644 index 0000000000..59279d5d32 --- /dev/null +++ b/samples/SafeAreaDemo.Android/Resources/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/samples/SafeAreaDemo.Android/Resources/values/styles.xml b/samples/SafeAreaDemo.Android/Resources/values/styles.xml new file mode 100644 index 0000000000..2759d2904a --- /dev/null +++ b/samples/SafeAreaDemo.Android/Resources/values/styles.xml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj b/samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj new file mode 100644 index 0000000000..f5d2af79d0 --- /dev/null +++ b/samples/SafeAreaDemo.Android/SafeAreaDemo.Android.csproj @@ -0,0 +1,24 @@ + + + Exe + net7.0-android + 21 + enable + com.avalonia.safeareademo + 1 + 1.0 + apk + False + + + + + Resources\drawable\Icon.png + + + + + + + + diff --git a/samples/SafeAreaDemo.Android/SplashActivity.cs b/samples/SafeAreaDemo.Android/SplashActivity.cs new file mode 100644 index 0000000000..621ad1c675 --- /dev/null +++ b/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 + { + 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))); + } + } +} diff --git a/samples/SafeAreaDemo.Desktop/Program.cs b/samples/SafeAreaDemo.Desktop/Program.cs new file mode 100644 index 0000000000..b07682e8c8 --- /dev/null +++ b/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() + .UsePlatformDetect() + .LogToTrace(); + } +} diff --git a/samples/SafeAreaDemo.Desktop/SafeAreaDemo.Desktop.csproj b/samples/SafeAreaDemo.Desktop/SafeAreaDemo.Desktop.csproj new file mode 100644 index 0000000000..619209892d --- /dev/null +++ b/samples/SafeAreaDemo.Desktop/SafeAreaDemo.Desktop.csproj @@ -0,0 +1,24 @@ + + + WinExe + + net7.0 + enable + true + + + + app.manifest + + + + + + + + + + + + diff --git a/samples/SafeAreaDemo.Desktop/app.manifest b/samples/SafeAreaDemo.Desktop/app.manifest new file mode 100644 index 0000000000..f0a4b00175 --- /dev/null +++ b/samples/SafeAreaDemo.Desktop/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/samples/SafeAreaDemo.iOS/AppDelegate.cs b/samples/SafeAreaDemo.iOS/AppDelegate.cs new file mode 100644 index 0000000000..6990435d78 --- /dev/null +++ b/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 + { + } +} diff --git a/samples/SafeAreaDemo.iOS/Entitlements.plist b/samples/SafeAreaDemo.iOS/Entitlements.plist new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/samples/SafeAreaDemo.iOS/Entitlements.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/SafeAreaDemo.iOS/Info.plist b/samples/SafeAreaDemo.iOS/Info.plist new file mode 100644 index 0000000000..ec04bd5a87 --- /dev/null +++ b/samples/SafeAreaDemo.iOS/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDisplayName + SafeAreaDemo + CFBundleIdentifier + companyName.SafeAreaDemo + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + MinimumOSVersion + 10.0 + UIDeviceFamily + + 1 + 2 + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIStatusBarHidden + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/samples/SafeAreaDemo.iOS/Main.cs b/samples/SafeAreaDemo.iOS/Main.cs new file mode 100644 index 0000000000..1c76dc6bc4 --- /dev/null +++ b/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)); + } + } +} \ No newline at end of file diff --git a/samples/SafeAreaDemo.iOS/Resources/LaunchScreen.xib b/samples/SafeAreaDemo.iOS/Resources/LaunchScreen.xib new file mode 100644 index 0000000000..c6dd636c46 --- /dev/null +++ b/samples/SafeAreaDemo.iOS/Resources/LaunchScreen.xib @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/SafeAreaDemo.iOS/SafeAreaDemo.iOS.csproj b/samples/SafeAreaDemo.iOS/SafeAreaDemo.iOS.csproj new file mode 100644 index 0000000000..71365fe07d --- /dev/null +++ b/samples/SafeAreaDemo.iOS/SafeAreaDemo.iOS.csproj @@ -0,0 +1,18 @@ + + + Exe + net7.0-ios + 10.0 + manual + enable + iossimulator-x64 + + + + + + + + + + diff --git a/samples/SafeAreaDemo/App.xaml b/samples/SafeAreaDemo/App.xaml new file mode 100644 index 0000000000..f5ffbdb32a --- /dev/null +++ b/samples/SafeAreaDemo/App.xaml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/samples/SafeAreaDemo/App.xaml.cs b/samples/SafeAreaDemo/App.xaml.cs new file mode 100644 index 0000000000..e23cb0e04a --- /dev/null +++ b/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(); + } + } +} \ No newline at end of file diff --git a/samples/SafeAreaDemo/Assets/avalonia-logo.ico b/samples/SafeAreaDemo/Assets/avalonia-logo.ico new file mode 100644 index 0000000000..da8d49ff9b Binary files /dev/null and b/samples/SafeAreaDemo/Assets/avalonia-logo.ico differ diff --git a/samples/SafeAreaDemo/SafeAreaDemo.csproj b/samples/SafeAreaDemo/SafeAreaDemo.csproj new file mode 100644 index 0000000000..20bc5ec8fe --- /dev/null +++ b/samples/SafeAreaDemo/SafeAreaDemo.csproj @@ -0,0 +1,27 @@ + + + net7.0 + enable + latest + true + + + + + + %(Filename) + + + Designer + + + + + + + + + + + + diff --git a/samples/SafeAreaDemo/ViewLocator.cs b/samples/SafeAreaDemo/ViewLocator.cs new file mode 100644 index 0000000000..4f71fdbe9c --- /dev/null +++ b/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; + } + } +} diff --git a/samples/SafeAreaDemo/ViewModels/MainViewModel.cs b/samples/SafeAreaDemo/ViewModels/MainViewModel.cs new file mode 100644 index 0000000000..fe58567171 --- /dev/null +++ b/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)); + } + } +} diff --git a/samples/SafeAreaDemo/Views/MainView.xaml b/samples/SafeAreaDemo/Views/MainView.xaml new file mode 100644 index 0000000000..a8f7c2e735 --- /dev/null +++ b/samples/SafeAreaDemo/Views/MainView.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + Fullscreen + Use Safe Area + Hide System Bars + + + + + + + diff --git a/samples/SafeAreaDemo/Views/MainView.xaml.cs b/samples/SafeAreaDemo/Views/MainView.xaml.cs new file mode 100644 index 0000000000..2b651225e7 --- /dev/null +++ b/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; + } + } + } +} diff --git a/samples/SafeAreaDemo/Views/MainWindow.xaml b/samples/SafeAreaDemo/Views/MainWindow.xaml new file mode 100644 index 0000000000..ccd3028bb9 --- /dev/null +++ b/samples/SafeAreaDemo/Views/MainWindow.xaml @@ -0,0 +1,12 @@ + + + diff --git a/samples/SafeAreaDemo/Views/MainWindow.xaml.cs b/samples/SafeAreaDemo/Views/MainWindow.xaml.cs new file mode 100644 index 0000000000..de8f2b05ca --- /dev/null +++ b/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); + } + } +} diff --git a/src/Avalonia.Base/Media/Effects/BlurEffect.cs b/src/Avalonia.Base/Media/Effects/BlurEffect.cs new file mode 100644 index 0000000000..47c86e4e42 --- /dev/null +++ b/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 RadiusProperty = AvaloniaProperty.Register( + nameof(Radius), 5); + + public double Radius + { + get => GetValue(RadiusProperty); + set => SetValue(RadiusProperty, value); + } + + static BlurEffect() + { + AffectsRender(RadiusProperty); + } + + public IImmutableEffect ToImmutable() => new ImmutableBlurEffect(Radius); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/DropShadowEffect.cs b/src/Avalonia.Base/Media/Effects/DropShadowEffect.cs new file mode 100644 index 0000000000..ea931c0a8c --- /dev/null +++ b/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 BlurRadiusProperty = + AvaloniaProperty.Register( + nameof(BlurRadius), 5); + + public double BlurRadius + { + get => GetValue(BlurRadiusProperty); + set => SetValue(BlurRadiusProperty, value); + } + + public static readonly StyledProperty ColorProperty = AvaloniaProperty.Register( + nameof(Color), Colors.Black); + + public Color Color + { + get => GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + public static readonly StyledProperty OpacityProperty = + AvaloniaProperty.Register( + nameof(Opacity), 1); + + public double Opacity + { + get => GetValue(OpacityProperty); + set => SetValue(OpacityProperty, value); + } + + static DropShadowEffectBase() + { + AffectsRender(BlurRadiusProperty, ColorProperty, OpacityProperty); + } +} + +public class DropShadowEffect : DropShadowEffectBase, IDropShadowEffect, IMutableEffect +{ + public static readonly StyledProperty OffsetXProperty = AvaloniaProperty.Register( + nameof(OffsetX), 3.5355); + + public double OffsetX + { + get => GetValue(OffsetXProperty); + set => SetValue(OffsetXProperty, value); + } + + public static readonly StyledProperty OffsetYProperty = AvaloniaProperty.Register( + nameof(OffsetY), 3.5355); + + public double OffsetY + { + get => GetValue(OffsetYProperty); + set => SetValue(OffsetYProperty, value); + } + + static DropShadowEffect() + { + AffectsRender(OffsetXProperty, OffsetYProperty); + } + + public IImmutableEffect ToImmutable() + { + return new ImmutableDropShadowEffect(OffsetX, OffsetY, BlurRadius, Color, Opacity); + } +} + +/// +/// This class is compatible with WPF's DropShadowEffect and provides Direction and ShadowDepth properties instead of OffsetX/OffsetY +/// +public class DropShadowDirectionEffect : DropShadowEffectBase, IDirectionDropShadowEffect, IMutableEffect +{ + public static readonly StyledProperty ShadowDepthProperty = + AvaloniaProperty.Register( + nameof(ShadowDepth), 5); + + public double ShadowDepth + { + get => GetValue(ShadowDepthProperty); + set => SetValue(ShadowDepthProperty, value); + } + + public static readonly StyledProperty DirectionProperty = AvaloniaProperty.Register( + 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); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/Effect.cs b/src/Avalonia.Base/Media/Effects/Effect.cs new file mode 100644 index 0000000000..182e8613f8 --- /dev/null +++ b/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 +{ + /// + /// Marks a property as affecting the brush's visual representation. + /// + /// The properties. + /// + /// After a call to this method in a brush's static constructor, any change to the + /// property will cause the event to be raised on the brush. + /// + protected static void AffectsRender(params AvaloniaProperty[] properties) + where T : Effect + { + var invalidateObserver = new AnonymousObserver( + static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty)); + + foreach (var property in properties) + { + property.Changed.Subscribe(invalidateObserver); + } + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + + /// + 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(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/EffectAnimator.cs b/src/Avalonia.Base/Media/Effects/EffectAnimator.cs new file mode 100644 index 0000000000..70d359911b --- /dev/null +++ b/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 +{ + public override IDisposable? Apply(Animation animation, Animatable control, IClock? clock, + IObservable match, Action? onComplete) + { + if (TryCreateAnimator(out var animator) + || TryCreateAnimator(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([NotNullWhen(true)] out IAnimator? animator) + where TAnimator : EffectAnimatorBase, 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; + } + + /// + /// Fallback implementation of animation. + /// + 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(prop => + typeof(IEffect).IsAssignableFrom(prop.PropertyType)); + } +} + +public abstract class EffectAnimatorBase : Animator where T : class, IEffect? +{ + public override IDisposable BindAnimation(Animatable control, IObservable instance) + { + if (Property is null) + { + throw new InvalidOperationException("Animator has no property specified."); + } + + return control.Bind((AvaloniaProperty)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 +{ + 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 +{ + 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 + ); + } +} diff --git a/src/Avalonia.Base/Media/Effects/EffectConverter.cs b/src/Avalonia.Base/Media/Effects/EffectConverter.cs new file mode 100644 index 0000000000..6ec3bace03 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/EffectExtesions.cs b/src/Avalonia.Base/Media/Effects/EffectExtesions.cs new file mode 100644 index 0000000000..adc287607b --- /dev/null +++ b/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()); + } + + /// + /// Converts a effect to an immutable effect. + /// + /// The effect. + /// + /// The result of calling if the effect is mutable, + /// otherwise . + /// + 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; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/EffectTransition.cs b/src/Avalonia.Base/Media/Effects/EffectTransition.cs new file mode 100644 index 0000000000..b2e0d07355 --- /dev/null +++ b/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; + +/// +/// Transition class that handles with type. +/// +public class EffectTransition : Transition +{ + 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( + IObservable progress, + TAnimator animator, + IEffect? oldValue, IEffect? newValue, TInterface defaultValue, [MaybeNullWhen(false)] out IObservable observable) + where TAnimator : EffectAnimatorBase 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>(animator, progress, Easing, oldI, newI); + return true; + + } + + public override IObservable DoTransition(IObservable progress, IEffect? oldValue, IEffect? newValue) + { + if ((oldValue != null || newValue != null) + && ( + TryWithAnimator(progress, s_blurEffectAnimator, + oldValue, newValue, s_DefaultBlur, out var observable) + || TryWithAnimator(progress, s_dropShadowEffectAnimator, + oldValue, newValue, s_DefaultDropShadow, out observable) + )) + return observable; + + return new IncompatibleTransitionObservable(progress, Easing, oldValue, newValue); + } + + private sealed class IncompatibleTransitionObservable : TransitionObservableBase + { + private readonly IEffect? _from; + private readonly IEffect? _to; + + public IncompatibleTransitionObservable(IObservable 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; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/IBlurEffect.cs b/src/Avalonia.Base/Media/Effects/IBlurEffect.cs new file mode 100644 index 0000000000..716159747c --- /dev/null +++ b/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; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs b/src/Avalonia.Base/Media/Effects/IDropShadowEffect.cs new file mode 100644 index 0000000000..bb97410d7f --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Effects/IEffect.cs b/src/Avalonia.Base/Media/Effects/IEffect.cs new file mode 100644 index 0000000000..698dccf1dd --- /dev/null +++ b/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 +{ + /// + /// Creates an immutable clone of the effect. + /// + /// The immutable clone. + internal IImmutableEffect ToImmutable(); +} + +public interface IImmutableEffect : IEffect, IEquatable +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index ffdfa9aac1..1359ad6603 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/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 { /// diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs index cc030eea04..fc5d0fc043 100644 --- a/src/Avalonia.Base/Rect.cs +++ b/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); + } + /// /// Returns a new with the specified X position. /// diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index df3a70b3e6..814ecdba29 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/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) diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs index bb7372c375..8ecc0028ce 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs +++ b/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(); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index eaa9a70ca0..1ec1362a4c 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/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. /// -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(); + } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs index 19349a5196..b9e6833d21 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs +++ b/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 License.md @@ -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() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index 6fb5ad3741..6e7ef85183 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/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; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index b4e28dc254..1c4e63b34a 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -53,7 +53,10 @@ namespace Avalonia.Rendering.SceneGraph } /// - 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() { diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 731cb97161..5881efce1e 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/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? _logicalChildren; private IResourceDictionary? _resources; @@ -183,21 +183,7 @@ namespace Avalonia /// collection. /// /// - public Classes Classes - { - get - { - return _classes; - } - - set - { - if (_classes != value) - { - _classes.Replace(value); - } - } - } + public Classes Classes => _classes ??= new(); /// /// Gets or sets the control's data context. diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 05159eb4ae..79cc760fc6 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -48,7 +48,7 @@ namespace Avalonia /// public static readonly StyledProperty ClipProperty = AvaloniaProperty.Register(nameof(Clip)); - + /// /// Defines the property. /// @@ -66,6 +66,12 @@ namespace Avalonia /// public static readonly StyledProperty OpacityMaskProperty = AvaloniaProperty.Register(nameof(OpacityMask)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty EffectProperty = + AvaloniaProperty.Register(nameof(Effect)); /// /// Defines the 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); } } + + /// + /// Gets or sets the effect of the control. + /// + public IEffect? Effect + { + get => GetValue(EffectProperty); + set => SetValue(EffectProperty, value); + } + /// /// Gets or sets a value indicating whether to apply mirror transform on this control. diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 31722974ee..91d718dfd8 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -6,7 +6,8 @@ Avalonia.Rendering.Composition.Animations - + + @@ -27,7 +28,8 @@ - + + diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 802f4c6e13..e56e32a5ff 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/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) diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs index 9774b603e8..69b0ffe9a6 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/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 }); diff --git a/src/Avalonia.Controls/Flyouts/Flyout.cs b/src/Avalonia.Controls/Flyouts/Flyout.cs index cebcbb6562..8ec5cfa50a 100644 --- a/src/Avalonia.Controls/Flyouts/Flyout.cs +++ b/src/Avalonia.Controls/Flyouts/Flyout.cs @@ -18,17 +18,7 @@ namespace Avalonia.Controls /// /// Gets the Classes collection to apply to the FlyoutPresenter this Flyout is hosting /// - 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(); /// /// Defines the property. diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index e5373ce42f..38af1b6d7b 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/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(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; diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index f66616f2aa..b066f9d827 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/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; } diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 7a43cd378b..641adde7e2 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/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); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 197815f9a0..5ca2b09eba 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -43,7 +43,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers()); InsertAfter( new AvaloniaXamlIlAvaloniaPropertyResolver(), - new AvaloniaXamlIlReorderClassesPropertiesTransformer() + new AvaloniaXamlIlReorderClassesPropertiesTransformer(), + new AvaloniaXamlIlClassesTransformer() ); InsertBefore( diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 4068caac21..d8524cfd88 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/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)) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXAmlIlClassesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXAmlIlClassesTransformer.cs new file mode 100644 index 0000000000..ed42247d29 --- /dev/null +++ b/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 +{ + /// + /// Converts an attribute syntax property value assignment to a collection syntax property + /// assignment. + /// + /// + /// Converts the property assignment `Classes="foo bar"` to: + /// + /// + /// + /// foo + /// bar + /// + /// + /// + 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; + } + } +} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs new file mode 100644 index 0000000000..babc547209 --- /dev/null +++ b/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; + } + +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 671e4d134c..f48d45f961 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -19,7 +19,9 @@ namespace Avalonia.Skia /// /// Skia based drawing context. /// - 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; diff --git a/tests/Avalonia.Base.UnitTests/Media/EffectTests.cs b/tests/Avalonia.Base.UnitTests/Media/EffectTests.cs new file mode 100644 index 0000000000..f374018438 --- /dev/null +++ b/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(() => 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); + } +} \ No newline at end of file diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 25c0516744..5e30198d00 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/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 = @" +"; + var target = (Button)AvaloniaRuntimeXamlLoader.Load(xaml); + + Assert.Equal(new[] { "foo", "bar" }, target.Classes); + } + [Fact] public void Can_Specify_Flyout_FlyoutPresenterClasses() { diff --git a/tests/Avalonia.RenderTests/Media/EffectTests.cs b/tests/Avalonia.RenderTests/Media/EffectTests.cs new file mode 100644 index 0000000000..9a83b397f4 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj index ca9f5ed974..d149138fe6 100644 --- a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj +++ b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj @@ -6,6 +6,9 @@ + + Media\EffectTests.cs + diff --git a/tests/TestFiles/Skia/Media/Effects/DropShadowEffect.expected.png b/tests/TestFiles/Skia/Media/Effects/DropShadowEffect.expected.png new file mode 100644 index 0000000000..e826b25c65 Binary files /dev/null and b/tests/TestFiles/Skia/Media/Effects/DropShadowEffect.expected.png differ